Opisz bibliotekę wartą poznania i napisz dlaczego to właśnie AutoMapper

Automapper – czytając kilka komentarzy pod poprzednim postem wywołuje nie małe emocje. Ja jednak nadal uważam go za dobre i warte poznania narzędzie. Dodatkowo obiecałem, że napiszę więcej niż parę słów o nim, także do dzieła!

Automapper autorem jest Jimmy Bogard i jeśli miałbym opisać jego funkcjonalność swoimi słowami to działałby tak:

“To narzędzie pozwala na (prawie) bezbolesne mapowanie klas,
zapomnijcie o ręcznym przepisywanie wartości z Foo.A do Goo.A – pozwólcie tę pracę wykonać komputerowi.
Jeśli nie wierzycie w czary, to automapper jest jedną z tych bibliotek która przywróci wam wiarę.”
Jarosław Stadnicki dla jstadnicki.blogspot.com
Wracając do czarno-białej rzeczywistości (lub biało czarnej, w zależności od theme w vs). Zanim poznałem AM, gdy musiałem mapować jedną klasę na drugą, korzystałem z metod statycznych, konstruktorów kopiujących, lub innych domowych rozwiązań, jednak zawsze kończyło się to na pisaniu kodu samodzielnie. Pole po polu, właściwość po właściwości. Czasem prowadziło to połączenia ze sobą dwóch światów (dopiero teraz to do mnie dotarło):

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  public class Book
   2:  {
   3:      public Book(BookVM source)
   4:      {
   5:          this.Id = source.Id;
   6:          this.Title = source.Title;
   7:   
   8:      }
   9:      public long Id { get; set; }
  10:      public string Title { get; set; }
  11:  }
  12:   
  13:  public class BookVM
  14:  {
  15:      public BookVM(Book source)
  16:      {
  17:          this.Id = source.Id;
  18:          this.Title = source.Title;
  19:      }
  20:   
  21:      public long Id { get; set; }
  22:      public string Title { get; set; }
  23:  }

Ten sposób powoduje, ze obie klasy muszą o sobie wiedzieć, a skoro tak to, po co je rozdzielać, po co mapować, po co w ogóle dwie osobne klasy, źle!.

Dzięki AM można rozdzielić te dwie klasy i korzystać z nich zupełnie nie zależnie:

   1:  namespace AutoMapper
   2:  {
   3:      class Program
   4:      {
   5:          static void Main(string[] args)
   6:          {
   7:              // initialize AM
   8:              Mapper.CreateMap<Book, BookVM>();
   9:              Mapper.CreateMap<BookVM, Book>();
  10:   
  11:              // somewhere in code
  12:              var src = new Book { Id = 3, Title = "See sharp in 24" };
  13:              var dst = Mapper.Map<BookVM>(src);
  14:   
  15:          }
  16:      }
  17:   
  18:      public class Book
  19:      {
  20:          public long Id { get; set; }
  21:          public string Title { get; set; }
  22:      }
  23:   
  24:      public class BookVM
  25:      {
  26:          public long Id { get; set; }
  27:          public string Title { get; set; }
  28:      }
  29:  }

Jak zauważycie wszędzie podaje mapowanie obu kierunkach, a korzystam tylko z jednego. Nie jest to konieczne, powstało to na wypadek gdybym napisał jakiś kod który będzie z tego korzystać i zostało. W przypadku powyżej linijka 8 jest zbędna i można się jej spokojnie pozbyć. Podobnie będzie w przykładach poniżej.

Efekt działania programu i wartość dst:

Raz zdefiniowane mapowanie można wykorzystać później, jeśli jakaś klasa będzie zawierać nasze książki AM skorzysta już z wcześniej zdefiniowanych reguł:

   1:  using System.Collections.Generic;
   2:   
   3:  namespace AutoMapper
   4:  {
   5:      class Program
   6:      {
   7:          static void Main(string[] args)
   8:          {
   9:              // initialize AM
  10:              Mapper.CreateMap<Book, BookVM>();
  11:              Mapper.CreateMap<BookVM, Book>();
  12:              Mapper.CreateMap<Person, PersonViewModel>();
  13:              Mapper.CreateMap<PersonViewModel, Person>();
  14:              
  15:              //somewhere later in code
  16:              var p = new Person
  17:                          {
  18:                              Id = 1,
  19:                              FirstName = "Jarek",
  20:                              Books =
  21:                                  new List<Book>
  22:                                      {
  23:                                          new Book { Id = 1, Title = "Automapper in 24" },
  24:                                          new Book { Id = 2, Title = "Dependency Injection in 24" },
  25:                                          new Book { Id = 3, Title = "See sharp in 24" },
  26:                                      }
  27:                          };
  28:              var vm = Mapper.Map<PersonViewModel>(p);
  29:          }
  30:      }
  31:   
  32:      public class Person
  33:      {
  34:          public long Id { get; set; }
  35:          public string FirstName { get; set; }
  36:          public List<Book> Books { get; set; }
  37:      }
  38:   
  39:      public class PersonViewModel
  40:      {
  41:          public long Id { get; set; }
  42:          public string FirstName { get; set; }
  43:          public List<BookVM> Books { get; set; }
  44:      }
  45:   
  46:      public class Book
  47:      {
  48:          public long Id { get; set; }
  49:          public string Title { get; set; }
  50:      }
  51:   
  52:      public class BookVM
  53:      {
  54:          public long Id { get; set; }
  55:          public string Title { get; set; }
  56:      }
  57:  }

Po wykonaniu linii 28 vm będzie wyglądać tak:

Jeśli któreś z pól nazywa się inaczej, można machnąć jedno linijkową instrukcję, która wyjaśni że Name to FirstName, np.

   1:  public class Book
   2:  {
   3:      public long Id { get; set; }
   4:      public string Title { get; set; }
   5:  }
   6:   
   7:  public class BookVM
   8:  {
   9:      public long Id { get; set; }
  10:      public string Title { get; set; }
  11:  }
  12:      
  13:  public class Person
  14:  {
  15:      public long Id { get; set; }
  16:      public string FirstName { get; set; }
  17:      public List<Book> Books { get; set; }
  18:  }
  19:   
  20:  public class PersonViewModel
  21:  {
  22:      public long Id { get; set; }
  23:      public string Name { get; set; }
  24:      public List<BookVM> Books { get; set; }
  25:  }
  26:   
  27:  static void Main(string[] args)
  28:  {
  29:      // initialize AM
  30:      Mapper.CreateMap<Book, BookVM>();
  31:      Mapper.CreateMap<BookVM, Book>();
  32:   
  33:      Mapper.CreateMap<Person, PersonViewModel>()
  34:          .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName));
  35:      Mapper.CreateMap<PersonViewModel, Person>()
  36:          .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
  37:   
  38:      //somewhere later in code
  39:      var p = new Person { FirstName = "jarek", Id = 1 };
  40:      var x = Mapper.Map<PersonViewModel>(p);            
  41:  }

Zmienna x będzie miała taką wartość (co mam nadzieję już nikogo nie będzie dziwić)

Konwertować możemy także tylko część właściwości, np. z klasy person można wyciągnąć tylko kolekcję książek. Oczywiście można to zrobić na dwa sposoby, ręcznie (bleh) i AM (yeah):

   1:  static void Main(string[] args)
   2:  {
   3:      // initialize AM
   4:      Mapper.CreateMap<Book, BookVM>();
   5:      Mapper.CreateMap<BookVM, Book>();
   6:   
   7:      Mapper.CreateMap<Person, PersonViewModel>()
   8:          .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName));
   9:      Mapper.CreateMap<PersonViewModel, Person>()
  10:          .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
  11:   
  12:      Mapper.CreateMap<Person, List<BookVM>>()
  13:          .ConvertUsing(x => Mapper.Map<List<BookVM>>(x.Books));
  14:   
  15:     //somewhere later in code
  16:      var p = new Person
  17:                  {
  18:                      Id = 1,
  19:                      FirstName = "Jarek",
  20:                      Books =
  21:                          new List<Book>
  22:                              {
  23:                                  new Book { Id = 1, Title = "Automapper in 24" },
  24:                                  new Book { Id = 2, Title = "Dependency Injection in 24" },
  25:                                  new Book { Id = 3, Title = "See sharp in 24" },
  26:                              }
  27:                  };
  28:   
  29:      var books = Mapper.Map<List<BookVM>>(p);
  30:   
  31:  }

W book będzie to czego oczywiście oczekujemy:

Najważniejsza część kodu w tym przypadku ukryła się w linijkach 12 i 13.

W przypadku gdy typy są do siebie trochę mniej pasujące? Np. coś co nancy dostarcza “z pudełka”, czyli string rozdzielony separatorami do kolekcji obiektów – hę? Prosta sprawa żeby mieć tak samo bez nancy:

   1:  using System.Collections.Generic;
   2:   
   3:  namespace AutoMapper
   4:  {
   5:      using System;
   6:      using System.Linq;
   7:   
   8:      class Program
   9:      {
  10:          static void Main(string[] args)
  11:          {
  12:              // initialize AM
  13:              Mapper.CreateMap<string, List<BookVM>>()
  14:                  .ConvertUsing<StringToBookViewModel>();
  15:   
  16:              string booksnames = "Lessie, Pinokio, Guliwer";
  17:              var booksvm = Mapper.Map<List<BookVM>>(booksnames);
  18:          }
  19:      }
  20:   
  21:      public class StringToBookViewModel : ITypeConverter<string, List<BookVM>>
  22:      {
  23:          public List<BookVM> Convert(ResolutionContext context)
  24:          {
  25:              var source = context.SourceValue as string;
  26:              var titles = source.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
  27:              var result = new List<BookVM>();
  28:              titles.ToList().ForEach(x => result.Add(new BookVM { Title = x, Id = -1 }));
  29:              return result;
  30:          }
  31:      }
  32:   
  33:      
  34:      public class BookVM
  35:      {
  36:          public long Id { get; set; }
  37:          public string Title { get; set; }
  38:      }
  39:  }

Nie będzie dla nikogo zaskoczeniem taki widok:

Jeśli ktoś się zastanawia co daje string na obiekty, to może warto się zastanowić nad np. obsługą tagów. Gdy ktoś dodaje listę tagów do posta, wpisu, książki, etc, dostajemy od użytkownika ciąg stringów, taki konwerter jak powyżej może podmienić pojedyncze stringi na pełnoprawne obiekty z poprawnym ID – jak? W poprzednim wpisie podałem sposób jak połączyć moc DependencyInjection oraz Automappera, wystarczy dla takiego konwertera dostarczyć repozytorium i w trakcie konwersji można sprawdzić czy dany tag istnieje czy nie; i wreszcie ustawić id obiektu na poprawną wartość.

Z dobroci, które oferuje AM jest także możliwość zdefiniowania zachowania dla obiektów które są nullem, dzięki czemu można pozbyć się uciążliwej ifologi z kodu:

   1:  using System.Collections.Generic;
   2:   
   3:  namespace AutoMapper
   4:  {
   5:      using System;
   6:      using System.Linq;
   7:   
   8:      class Program
   9:      {
  10:          static void Main(string[] args)
  11:          {
  12:              // initialize AM
  13:              Mapper.CreateMap<Book, BookVM>();
  14:              Mapper.CreateMap<BookVM, Book>();
  15:   
  16:              Mapper.CreateMap<Person, PersonViewModel>()
  17:                  .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName))
  18:                  .ForMember(d => d.Books, o => o.NullSubstitute(new List<BookVM>()));
  19:   
  20:              Mapper.CreateMap<PersonViewModel, Person>()
  21:                  .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
  22:   
  23:              var p = new Person { FirstName = "jarek", Id = 1 };
  24:              var x = Mapper.Map<PersonViewModel>(p);
  25:          }
  26:      }
  27:   
  28:   
  29:      public class Person
  30:      {
  31:          public long Id { get; set; }
  32:          public string FirstName { get; set; }
  33:          public List<Book> Books { get; set; }
  34:      }
  35:   
  36:      public class PersonViewModel
  37:      {
  38:          public long Id { get; set; }
  39:          public string Name { get; set; }
  40:          public List<BookVM> Books { get; set; }
  41:      }
  42:   
  43:      public class Book
  44:      {
  45:          public long Id { get; set; }
  46:          public string Title { get; set; }
  47:      }
  48:   
  49:      public class BookVM
  50:      {
  51:          public long Id { get; set; }
  52:          public string Title { get; set; }
  53:      }
  54:  }

Dzięki NullSubstitute zamiast nulla w książkach mamy pustą kolekcję po której możemy spokojnie iterować.

Dopiero teraz (wow!) zauważyłem, że AM domyślnie stosuje taką strategię. Propopnuje wrócić do trzeciego obrazka i sprawdzić. Obiekt klasy Person ma null na kolekcji książek, ale już PersonViewModel ma pustą kolekcję książek. Całkiem możliwe że Jimmy też nie lubi ifologi. Tak czy siak, warto znać opcję NullSubstitute.

Przykłady powyżej to najczęściej używane konstrukcje używane przeze mnie w moich projektach domowych jak i komercyjnych, jak do tej pory rozwiązywały one wszystkie nasze problemy. Co więcej jeśli czasem coś nie działało jak powinno zawsze mogliśmy w najgorszym wypadku wrócić do ITypeConverter i napisać własne mapowanie od a do z, jednak nadal zachowując standardy AM.
Mogę się założyć, że są jeszcze cuda, które umożliwia AM a o których nie wiem, ale nawet teraz jest to jedno lepszych narzędzi, z których na pewne nie przestanę korzystać.
Jeśli macie uwagi lub pytania, chętnie wdam się w dyskusje.

Dependency injection i automapper

Od dłuższego czasu korzystam z automappera, jest to genialne rozwiązanie gdy trzeba mapować jeden obiekt na drugi. Gdy w klasach, z i do której chcemy mapować wszystkie typy właściwości/pól i ich nazwy się zgadzają nie trzeba robić praktycznie nic, gdy pojawiają się inne nazwy, wystarczy tylko wskazać że pole właściwość W w klasie A to właściwość X w klasie B i tyle, raz i spokój do końca projektu. Gdy typy się nie zgadzają, można skorzystać z konwerterów, zarówno na poziomie właściwości jak i całych klas. Jeśli nie wiesz o czym mówię wróć 🙂 tutaj za kilka dnia, a ja obiecuje że popełnie wpis o automapperze. Opis – Automappera. Ale nie o tym chciałem napisać. Jeśli ktoś nie korzysta to polecam, jeśli potrzeba pomocy to proszę dać znać, a teraz do rzeczy.

Czasem konwertery aby dobrze wykonać swoją robotę potrzebują innej klasy, oczywiści można ją stworzyć w konstruktorze, ale to nie będzie ładne rozwiązanie, wszystko przez mocne wiązania, które ostatnio wyszły z mody. Na szczęście wymyślono wstrzykiwanie zależności, a automapper nie przeszkadza w skorzystaniu z tej idei. Co zrobić, aby to zadziałało?

Wystarczy taki kawałek kodu dodać do inicjalizacji automappera

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  public static void Initialize(IDependencyResolver current)
   2:  {
   3:      Mapper.Initialize(cfg => cfg.ConstructServicesUsing(t => current.GetService(t)));
   4:      // here goes other initialization of automapper          
   5:  }

Pewnie się zapytacie skąd i jak jest wołane? Ja bym się pytał. Więc tak to robię ja, w Application_Start w global.asax.cs

   1:  ModelMapper.Initialize(DependencyResolver.Current);

Potem gdy chcemy skorzystać z jakiegoś konwertera, na liście parametrów w kontruktorze można podać to czego potrzbujemy i czekać aż framework zrobic swoją magię:

   1:  public class Id2ConceptStatistics : TypeConverter<long, ConceptStatistics>
   2:  {
   3:      private readonly IUnitOfWork unitOfWork;
   4:   
   5:      public Id2ConceptStatistics(IUnitOfWork unitOfWork)
   6:      {
   7:          this.unitOfWork = unitOfWork;
   8:      }
   9:   
  10:      protected override ConceptStatistics ConvertCore(long id)
  11:      {
  12:          var conceptStatistics = this.unitOfWork.ConceptStatistics.GetByID(id);
  13:          var result = conceptStatistics ?? new ConceptStatistics();
  14:          return result;
  15:      }
  16:  }

Powyżej przykład na to jak z long (który jest id) można zmapować na pełno prawną klasę, która jest zagnieżdżona gdzieś w innej klasie.

To tyle.

Testowanie klas abstrakcyjnych

Pewnie każdy na swojej ścieżce programistycznej spotkał się z klasą abstrakcyjną. Wrzucamy tam kod, który zdaje się być domyślną implementacją pewnej grupy klas i szkoda nam kopiować tego zachowania do każdej z nich osobna. Skoro wszystkie zachowują się podobny sposób, czasem tylko dodając coś od siebie, to warto wykorzystać dziedziczenie i napisać mniej (DRY). Czyli prawdopodobny wydaje się taki scenariusz:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
max-height: 500px;
overflow: auto;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  namespace AbstractTesting
   2:  {
   3:      public abstract class AbstractClass
   4:      {
   5:          public virtual int Method(int input)
   6:          {
   7:              switch (input)
   8:              {
   9:                  case 1:
  10:                      return 1;
  11:                  case 2:
  12:                      return 4;
  13:                  case 3:
  14:                      return 8;
  15:                  default:
  16:                      return 0;
  17:              }
  18:          }
  19:      }
  20:   
  21:      public class ConcreteClass1 : AbstractClass
  22:      {
  23:          public override int Method(int input)
  24:          {
  25:              switch (input)
  26:              {
  27:                  case 1:
  28:                      return -1;
  29:                  default:
  30:                      return base.Method(input);
  31:              }
  32:   
  33:          }
  34:      }
  35:   
  36:      public class ConcreteClass2 : AbstractClass
  37:      {
  38:          public override int Method(int i)
  39:          {
  40:              switch (i)
  41:              {
  42:                  case 4:
  43:                      return 16;
  44:                  default:
  45:                      return base.Method(i);
  46:   
  47:              }
  48:          }
  49:      }
  50:   
  51:      public class ConcreteClass3 : AbstractClass
  52:      {
  53:      }
  54:   
  55:      public class ConcreteClass4 : AbstractClass
  56:      {
  57:          public override int Method(int i)
  58:          {
  59:              switch (i)
  60:              {
  61:                  case 1:
  62:                      return -1;
  63:                  case 2:
  64:                      return -2;
  65:                  case 3:
  66:                      return -3;
  67:                  default:
  68:                      return base.Method(i);
  69:              }
  70:          }
  71:      }
  72:   
  73:      public class ConcreteClass5 : AbstractClass
  74:      {
  75:          public override int Method(int i)
  76:          {
  77:              switch (i)
  78:              {
  79:                  case 1:
  80:                      return -1;
  81:                  case 2:
  82:                      return -4;
  83:                  case 3:
  84:                      return -8;
  85:                  default:
  86:                      return 1;
  87:              }
  88:          }
  89:      }
  90:  }

Celowo pomijam dodatkowe metody w klasie bazowej i klasach pochodnych, nie na tym chce się tutaj skupić.
Teraz można zadać sobie pytanie, jak mam to przetestować? Przynajmniej ja je sobie zadawałem. Czy testować w każdej klasie tylko to co wnosi ona nowego, a dla klasy klasy bazowej napisać specjalnego mocka, który umożliwi stworzenie jej instancji, a następnie przetestowanie wspólnego kodu. Czy też pominąć w testach klasę bazową, ale dla każdej z klas dziedziczących pisać powtarzający się test, sprawdzający wszystkie warunki switcha? Jest też trzecie rozwiązanie tego problemu, sugerując się podpowiedziami kolegów z pracy, trzeba być wystarczająco odważnym, wrzucić na produkcję i zobaczyć czy zabangla. Ale tego przykładu nie będę tutaj analizować.

Zamiast rozwodzić się nad tym, co jest lepsze, a co gorsze, sprawdzimy jak to wychodzi w praniu. Pierwsze podejście, napisać mniej testów:

   1:  [TestFixture]
   2:  public class AbstractTestClass
   3:  {
   4:      [Test]
   5:      public void t0_1()
   6:      {
   7:          var sut = new MockAbstractClass();
   8:          Assert.AreEqual(1, sut.Method(1));
   9:      }
  10:   
  11:      [Test]
  12:      public void t1_4()
  13:      {
  14:          var sut = new MockAbstractClass();
  15:          Assert.AreEqual(4, sut.Method(2));
  16:      }
  17:   
  18:      [Test]
  19:      public void t3_8()
  20:      {
  21:          var sut = new MockAbstractClass();
  22:          Assert.AreEqual(8, sut.Method(3));
  23:      }
  24:   
  25:      [Test]
  26:      public void t4_0()
  27:      {
  28:          var sut = new MockAbstractClass();
  29:          Assert.AreEqual(0, sut.Method(4));
  30:      }
  31:  }
  32:   
  33:  public class MockAbstractClass : AbstractClass
  34:  {
  35:  }
  36:   
  37:  [TestFixture]
  38:  public class TestC1
  39:  {
  40:      [Test]
  41:      public void t0_1()
  42:      {
  43:          var sut = new ConcreteClass1();
  44:          Assert.AreEqual(-1, sut.Method(1));
  45:      }
  46:  }
  47:   
  48:  [TestFixture]
  49:  public class TestC2
  50:  {
  51:      [Test]
  52:      public void t0_16()
  53:      {
  54:          var sut = new ConcreteClass2();
  55:          Assert.AreEqual(16, sut.Method(4));
  56:      }
  57:  }
  58:   
  59:  [TestFixture]
  60:  public class TestC4
  61:  {
  62:      [Test]
  63:      public void t1_m1()
  64:      {
  65:          var sut = new ConcreteClass4();
  66:          Assert.AreEqual(-1, sut.Method(1));
  67:      }
  68:   
  69:      [Test]
  70:      public void t2_m2()
  71:      {
  72:          var sut = new ConcreteClass4();
  73:          Assert.AreEqual(-2, sut.Method(2));
  74:      }
  75:   
  76:      [Test]
  77:      public void t3_m3()
  78:      {
  79:          var sut = new ConcreteClass4();
  80:          Assert.AreEqual(-3, sut.Method(3));
  81:      }
  82:   
  83:      [Test]
  84:      public void t4_0()
  85:      {
  86:          var sut = new ConcreteClass4();
  87:          Assert.AreEqual(0, sut.Method(4));
  88:      }
  89:  }
  90:   
  91:  [TestFixture]
  92:  public class TestC5
  93:  {
  94:      [Test]
  95:      public void t1_m1()
  96:      {
  97:          var sut = new ConcreteClass5();
  98:          Assert.AreEqual(-1, sut.Method(1));
  99:      }
 100:   
 101:      [Test]
 102:      public void t2_m2()
 103:      {
 104:          var sut = new ConcreteClass5();
 105:          Assert.AreEqual(-4, sut.Method(2));
 106:      }
 107:   
 108:      [Test]
 109:      public void t3_m3()
 110:      {
 111:          var sut = new ConcreteClass5();
 112:          Assert.AreEqual(-8, sut.Method(3));
 113:      }
 114:   
 115:      [Test]
 116:      public void t4_0()
 117:      {
 118:          var sut = new ConcreteClass5();
 119:          Assert.AreEqual(1, sut.Method(16));
 120:      }
 121:  }

Pokrycie kodu powinno być 100%. Wszystkie test powinny przejść na zielono. Skoro wszystko działa dobrze, to po co się zastanawiać na innym rozwiązaniem? A to dlatego, drogie dzieciaczki, że jeśli wprowadzicie teraz jakąś zmianę w klasie Abstract, to wszystkie testy klas pochodnych nadal będą się świecić na zielono. Pomimo tego, że zmieni się ich zachowanie. W ten sposób nie dowiecie się (z testów), że np. klasa Concret3 od teraz zachowuje się zupełnie inaczej, inne korzystające z domyślnego zachowania też oszalały. Ktoś może powiedzieć, że to poprawne zachowanie, skoro dziedziczysz po czymś/kimś i zdajesz się na jego domyślną implementacje to zmieniasz się razem z nią jak chorągiewka na wietrze. Tylko czy o to chodzi? Czy klasy nie powinny mieć własnego rozumku? Klient korzysta z implementacji klasy 1,2,3,4 czy 5 a nie klasy podstawowej. W takiej sytuacji każda klasa, a w zasadzie test dla każdej z klas powinien wykryć zmiany w jej zachowaniu, poinformować o tym, że zostało ono zmienione i od teraz nie spełnia wcześniej przyjętego rozumowania. Moim zdaniem tak powinny wyglądać testy:

   1:  namespace AbstractTesting
   2:  {
   3:      [TestFixture]
   4:      public class ConcreteClass1Test
   5:      {
   6:          [Test]
   7:          public void T001_M1()
   8:          {
   9:              var sut = new ConcreteClass1();
  10:   
  11:              var result = sut.Method(1);
  12:   
  13:              Assert.AreEqual(-1, result);
  14:          }
  15:   
  16:          [Test]
  17:          public void T002_M2()
  18:          {
  19:              var sut = new ConcreteClass1();
  20:   
  21:              var result = sut.Method(2);
  22:   
  23:              Assert.AreEqual(4, result);
  24:          }
  25:   
  26:          [Test]
  27:          public void T003_M3()
  28:          {
  29:              var sut = new ConcreteClass1();
  30:   
  31:              var result = sut.Method(3);
  32:   
  33:              Assert.AreEqual(8, result);
  34:          }
  35:   
  36:          [Test]
  37:          public void T004_M155()
  38:          {
  39:              var sut = new ConcreteClass1();
  40:   
  41:              var result = sut.Method(155);
  42:   
  43:              Assert.AreEqual(0, result);
  44:          }
  45:      }
  46:   
  47:      [TestFixture]
  48:      public class ConcreteClass2Test
  49:      {
  50:          [Test]
  51:          public void T001_M1()
  52:          {
  53:              var sut = new ConcreteClass2();
  54:   
  55:              var result = sut.Method(1);
  56:   
  57:              Assert.AreEqual(1, result);
  58:          }
  59:   
  60:          [Test]
  61:          public void T002_M2()
  62:          {
  63:              var sut = new ConcreteClass2();
  64:   
  65:              var result = sut.Method(2);
  66:   
  67:              Assert.AreEqual(4, result);
  68:          }
  69:   
  70:          [Test]
  71:          public void T003_M3()
  72:          {
  73:              var sut = new ConcreteClass2();
  74:   
  75:              var result = sut.Method(3);
  76:   
  77:              Assert.AreEqual(8, result);
  78:          }
  79:   
  80:          [Test]
  81:          public void T004_M4()
  82:          {
  83:              var sut = new ConcreteClass2();
  84:   
  85:              var result = sut.Method(4);
  86:   
  87:              Assert.AreEqual(16, result);
  88:          }
  89:   
  90:          [Test]
  91:          public void T005_M55()
  92:          {
  93:              var sut = new ConcreteClass2();
  94:   
  95:              var result = sut.Method(55);
  96:   
  97:              Assert.AreEqual(0, result);
  98:          }
  99:      }
 100:   
 101:      [TestFixture]
 102:      public class ConcreteClass3Test
 103:      {
 104:          [Test]
 105:          public void T001_M1()
 106:          {
 107:              var sut = new ConcreteClass3();
 108:   
 109:              var result = sut.Method(1);
 110:   
 111:              Assert.AreEqual(1, result);
 112:          }
 113:   
 114:          [Test]
 115:          public void T002_M2()
 116:          {
 117:              var sut = new ConcreteClass3();
 118:   
 119:              var result = sut.Method(2);
 120:   
 121:              Assert.AreEqual(4, result);
 122:          }
 123:   
 124:          [Test]
 125:          public void T003_M3()
 126:          {
 127:              var sut = new ConcreteClass3();
 128:   
 129:              var result = sut.Method(3);
 130:   
 131:              Assert.AreEqual(8, result);
 132:          }
 133:   
 134:          [Test]
 135:          public void T004_M55()
 136:          {
 137:              var sut = new ConcreteClass3();
 138:   
 139:              var result = sut.Method(55);
 140:   
 141:              Assert.AreEqual(0, result);
 142:          }
 143:      }
 144:   
 145:      [TestFixture]
 146:      public class ConcreteClass4Test
 147:      {
 148:          [Test]
 149:          public void T001_M1()
 150:          {
 151:              var sut = new ConcreteClass4();
 152:   
 153:              var result = sut.Method(1);
 154:   
 155:              Assert.AreEqual(-1, result);
 156:          }
 157:   
 158:          [Test]
 159:          public void T002_M2()
 160:          {
 161:              var sut = new ConcreteClass4();
 162:   
 163:              var result = sut.Method(2);
 164:   
 165:              Assert.AreEqual(-2, result);
 166:          }
 167:   
 168:          [Test]
 169:          public void T003_M3()
 170:          {
 171:              var sut = new ConcreteClass4();
 172:   
 173:              var result = sut.Method(3);
 174:   
 175:              Assert.AreEqual(-3, result);
 176:          }
 177:   
 178:          [Test]
 179:          public void T004_M55()
 180:          {
 181:              var sut = new ConcreteClass4();
 182:   
 183:              var result = sut.Method(55);
 184:   
 185:              Assert.AreEqual(0, result);
 186:          }
 187:      }
 188:   
 189:      [TestFixture]
 190:      public class ConcreteClass5Test
 191:      {
 192:          [Test]
 193:          public void T001_M1()
 194:          {
 195:              var sut = new ConcreteClass5();
 196:   
 197:              var result = sut.Method(1);
 198:   
 199:              Assert.AreEqual(-1, result);
 200:          }
 201:   
 202:          [Test]
 203:          public void T002_M2()
 204:          {
 205:              var sut = new ConcreteClass5();
 206:   
 207:              var result = sut.Method(2);
 208:   
 209:              Assert.AreEqual(-4, result);
 210:          }
 211:   
 212:          [Test]
 213:          public void T003_M3()
 214:          {
 215:              var sut = new ConcreteClass5();
 216:   
 217:              var result = sut.Method(3);
 218:   
 219:              Assert.AreEqual(-8, result);
 220:          }
 221:   
 222:          [Test]
 223:          public void T004_M55()
 224:          {
 225:              var sut = new ConcreteClass5();
 226:   
 227:              var result = sut.Method(55);
 228:   
 229:              Assert.AreEqual(1, result);
 230:          }
 231:      }
 232:  }

W tym przypadku gdy zmieni się zachowanie klasy podstawowej, wszystkie testy klas korzystających z domyślnej implementacji, automatycznie zgłoszą niezadowolenie ze zmian.
Nic za darmo, w drugim przypadku trzeba napisać więcej testów, a czasem nawet go powtórzyć. Oczywiście można też pozbyć się tego switcha z kodu, albo wyjechać do Niemiec i zbierać ogórki – byle by tylko nie pisać testów. Tutaj miałem na celu pokazanie tego, jak zachowują się oba rozwiązania i które z nich, moim zdaniem jest tym słuszniejszym.

Jak zwykle pozostaje otwarty na krytykę i zastrzegam sobie prawo do popełniania błędów.

Własna konfiguracja w app.config

Jeśli to czytacie, to znaczy że zdążyłem jeszcze z wpisem przed nowym rokiem.  Udało mi się napisać jeszcze jednego posta przed nowym rokiem – jestem królem świata!

Dzisiaj o wykorzystaniu app.config dla własnych celów. Skąd taki pomysł? Otóż jak każdy pewnie ma lub miał w życiu, czasem mieć taką potrzebę aby zmieniać zachowanie aplikacji inaczej niż przez jej przekompilowanie. Wtedy właśnie z pomocą przychodzą pliki .txt, .xml, .config czy dawno dawno temu .ini. Można napisać własne system do obsługi konfiguracji, własne klasy serializujące i deserializujące, można pisać cudne rzeczy. Ale po co, skoro można skorzystać z tego co oferuje app.config. Też tak pomyślałem i rozpętało się piekło. Bo nie da się ot tak dowalić do app.configu swojego kawałka xmla. Trzba to wszystko dokładnie, ale to dokładnie opisać. O ile wytrzymacie ten wpis, to zaraz wszystko wyjaśnię.

Zacznę od początku na prostych obiektach. Np. gdybym chciał w pliku zdefiniować wygląd jakiegoś prostego obiektu, np.

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:      <MyConfiguration>
   4:          <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   5:      </MyConfiguration>
   6:  </configuration>

Wiecie co się stanie przy próbie uruchomienia takiego prostego kodu jak ten:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  static void Main(string[] args)
   2:  {
   3:      var cfg = ConfigurationManager.AppSettings;
   4:  }

Aby konfiguracja zadziałała, trzeba dodać informację do app.cofig o naszej sekcji konfiguracyjnej.

   1:  <configSections>
   2:      <section name="MyConfiguration" type="AppConfigExample.MyConfiguration, AppConfigExample"/>
   3:  </configSections>

W części type podajemy gdzie znajdzie się klasa opisująca naszą konfigurację. Wpis musi zawierać pełny namespace, następnie należy podać także assembly w który się ona znajduje. Tutaj wszystko mam w jednym assembly.
Skoro trzeba podać klasę to trzeba ją także gdzieś i jakoś zdefiniować i podziedziczyć:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:  }

A następnie spróbować się do niej dostać:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration");

Niestety to nadal za mało (powoli buduje nastrój grozy):

Lubię .net i jego wyjątki za czytelne błędy i opisy problemów, lubię też google czy SO za pomoc w rozwiązywaniu tych wyjątków. Chwila szukania i już wiem co należy dodać, aby móc odczytać mój kwadracik z konfiguracji:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("Rectangle")]
   4:      public Rectangle Rectangle
   5:      {
   6:          get { return ((Rectangle)(base["Rectangle"])); }
   7:      }
   8:  }

Skoro wspominamy tutaj o klasie Rectangle to należy ją także jakoś zdefiniować i znowu dziedziczenie:

   1:  public class Rectangle: ConfigurationElement
   2:  {
   3:      [ConfigurationProperty("Color")]
   4:      public ConsoleColor Color
   5:      {
   6:          get { return (ConsoleColor)base["Color"]; }
   7:          set { base["Color"] = value; }
   8:      }
   9:   
  10:      [ConfigurationProperty("Width")]
  11:      public int Width
  12:      {
  13:          get { return (int)base["Width"]; }
  14:          set { base["Width"] = value; }
  15:      }
  16:   
  17:      [ConfigurationProperty("Height")]
  18:      public int Height
  19:      {
  20:          get { return (int)base["Height"]; }
  21:          set { base["Height"] = value; }
  22:      }
  23:   
  24:      [ConfigurationProperty("Fill")]
  25:      public bool Fill
  26:      {
  27:          get { return (bool)base["Fill"]; }
  28:          set { base["Fill"] = value; }
  29:      }
  30:  }

Gdy mamy taki kod możemy spokojnie spróbować swoich sił i uruchomić coś takiego:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
   2:   
   3:  var oc = Console.ForegroundColor;
   4:  Console.ForegroundColor = cfg.Rectangle.Color;
   5:   
   6:  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", cfg.Rectangle.Width, cfg.Rectangle.Height,
   7:                    cfg.Rectangle.Fill);
   8:   
   9:  Console.ForegroundColor = oc;
Co powinno dać taki wynik:

Chyba nie macie dosyć? Nie warto zaprzestawać na tylko jednym takim obiekcie, skoro na świecie jest tyle pięknych prostokątów, załóżmy taki przypadek:

   1:  <MyConfiguration>
   2:      <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <Rectangle Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <Rectangle Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <Rectangle Color="White" Width="15" Height="2" Fill="True"/>
   6:      <Rectangle Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </MyConfiguration>

Co się stanie po uruchomieniu aplikacji z taką konfiguracją?

No racja racja, przecież to kolekcja prostokątów, powinna wyglądać inaczej, bo skąd biedny komputerek może wiedzieć o który obiekt dokładnie nam chodzi.

Macie tak czasami, że wy wiecie że się nie uda czegoś zrobić, a komputer jeszcze walczy? Np. klikniecie przez przypadek na napędzie CD, tam nie ma płyty, wszyscy to wiedzą, ale nie, 15 sekund trzeba czekać zanim komputerek też się dowie. Sieć nie działa, wszyscy wiedzą, a komp walczy. Najgorsze jest to że się nie uczy. Skoro nie było płyty, i napęd się nie wysunął i nie wsunął, to płyty nadal tam nie będzie. Ale kliknij znowu na ikonę z CD i znowu czekasz, bo może jednak jest. Ech takie szybkie i takie głupie.

Wracając do naszego problemu, jak zrobić żeby ten biedny komputerek poradził sobie z wieloma prostokącikami:

   1:  <Rectangles>
   2:      <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <Rectangle Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <Rectangle Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <Rectangle Color="White" Width="15" Height="2" Fill="True"/>
   6:      <Rectangle Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </Rectangles>

Ale wtedy znowu będzie marudzić, że unrecognized element i że “Rectangles” to ten unrecognized. Trzeba więc zaktualizować informację z klasie MyConfiguration na taki:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("Rectangles")]
   4:      public Rectangles Rectangles
   5:      {
   6:          get { return ((Rectangles)(base["Rectangles"])); }
   7:      }
   8:  }

Oraz zdefiniować odpowiednio klasę Rectangles. Możecie spróbować samodzielnie ją zdefiniować i zobaczyć jaki poleci wyjątek, lub też przeczytać dalej i zobaczyć jak robią to inni:

   1:  [ConfigurationCollection(typeof(Rectangle))]
   2:  public class Rectangles : ConfigurationElementCollection
   3:  {
   4:      protected override ConfigurationElement CreateNewElement()
   5:      {
   6:          return new Rectangle();
   7:      }
   8:   
   9:      protected override object GetElementKey(ConfigurationElement element)
  10:      {
  11:          return element.GetHashCode();
  12:      }
  13:  }

Linijka 11 nie wygląda najlepiej. A poza tym może spowodować błąd (podczas domyślnej implementacji GetHashCode), aby go uniknąć należy napisać własne GetHashCode w klasie Rectangle lub dołożyć jakiś klucz, co później ułatwi dostęp do wybranych elementów z kolekcji.
Klasę Rectangle należy uaktualnić o następujące pole:

   1:  [ConfigurationProperty("Id", IsKey = true, IsRequired = true)]
   2:  public int Id
   3:  {
   4:      get { return (int)base["Id"]; }
   5:      set { base["Id"] = value; }
   6:  }

Natomiast metoda GetElementKey wyglądać teraz będzie tak:

   1:  protected override object GetElementKey(ConfigurationElement element)
   2:  {
   3:      return ((Rectangle)element).Name;
   4:  }

Kolekcja elementów w app.config wygląda teraz tak:

   1:  <Rectangles>
   2:      <add Id="1" Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <add Id="2" Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <add Id="3" Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <add Id="4" Color="White" Width="15" Height="2" Fill="True"/>
   6:      <add Id="5" Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </Rectangles>

I znowu coś nie cool, to idiotyczne add – bleh, brzydal. Gdzie się podziało ładne Rectangle? Ponownie zachęcam do sprawdzenia wyjątku jaki leci, gdy zamiast add będzie Rectangle. Na szczęście i to da się napisać lepiej, wystarczy ponownie zaktualizować informację o klasie Rectangles, brakuje tam małej informacji w atrybutach klasy:

   1:  [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
   2:  public class Rectangles : ConfigurationElementCollection { /* unicorns here */ }

Od teraz znowu zamiast add możemy stosować Rectangle i uruchomić taki kod:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
   2:  var oc = Console.ForegroundColor;
   3:   
   4:  foreach (Rectangle rectangle in cfg.Rectangles)
   5:  {
   6:      Console.ForegroundColor = rectangle.Color;
   7:      Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rectangle.Width, rectangle.Height,rectangle.Fill);
   8:      Console.ForegroundColor = oc;
   9:  }

Co według mojego działania da taki wynik:

A ja wybrać ten jeden w swoim rodzaju Rectangl? Skoro mamy jego Id to powinniśmy z niego jakoś skorzystać. Może linq? Potrzebujemy tylko IEnumerable w klasie Rectangles i będzie działać, a więc:

   1:  [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
   2:  public class Rectangles: ConfigurationElementCollection, IEnumerable<Rectangle>
   3:  {
   4:      { /* dragons here */ }
   5:      public new IEnumerator<Rectangle> GetEnumerator()
   6:      {
   7:          int count = base.Count;
   8:          for (int i = 0; i < count; i++)
   9:          {
  10:              yield return base.BaseGet(i) as Rectangle;
  11:          }
  12:      }
  13:  }

I można już napisać coś takiego w kodzie:

   1:  Rectangle rect = cfg.Rectangles.Single(r => r.Id == 2);
   2:   
   3:  Console.ForegroundColor = rect.Color;
   4:  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rect.Width, rect.Height, rect.Fill);

Od teraz wszyscy mogą zazdrościć wam wiedzy o tym, jak lepiej wykorzystywać app.config w swoich aplikacjach.

A teraz cały kod z wpisu:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Configuration;
   4:  using System.Linq;
   5:  using System.Text;
   6:   
   7:  namespace AppConfigExample
   8:  {
   9:      class Program
  10:      {
  11:          static void Main(string[] args)
  12:          {
  13:              var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
  14:              var oc = Console.ForegroundColor;
  15:   
  16:              foreach (Rectangle rectangle in cfg.Rectangles)
  17:              {
  18:                  Console.ForegroundColor = rectangle.Color;
  19:                  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rectangle.Width, rectangle.Height, rectangle.Fill);
  20:              }
  21:   
  22:              Console.WriteLine("****************************************");
  23:              Rectangle rect = cfg.Rectangles.Single(r => r.Id == 2);
  24:   
  25:              Console.ForegroundColor = rect.Color;
  26:              Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rect.Width, rect.Height, rect.Fill);
  27:   
  28:   
  29:   
  30:              Console.ForegroundColor = oc;
  31:          }
  32:      }
  33:   
  34:   
  35:      public class MyConfiguration : ConfigurationSection
  36:      {
  37:          [ConfigurationProperty("Rectangles")]
  38:          public Rectangles Rectangles
  39:          {
  40:              get { return ((Rectangles)(base["Rectangles"])); }
  41:          }
  42:      }
  43:   
  44:      [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
  45:      public class Rectangles : ConfigurationElementCollection, IEnumerable<Rectangle>
  46:      {
  47:          protected override ConfigurationElement CreateNewElement()
  48:          {
  49:              return new Rectangle();
  50:          }
  51:   
  52:          protected override object GetElementKey(ConfigurationElement element)
  53:          {
  54:              return ((Rectangle)element).Id;
  55:          }
  56:   
  57:          public new IEnumerator<Rectangle> GetEnumerator()
  58:          {
  59:              int count = base.Count;
  60:              for (int i = 0; i < count; i++)
  61:              {
  62:                  yield return base.BaseGet(i) as Rectangle;
  63:              }
  64:          }
  65:      }
  66:   
  67:      public class Rectangle : ConfigurationElement
  68:      {
  69:          [ConfigurationProperty("Id", IsKey = true, IsRequired = true)]
  70:          public int Id
  71:          {
  72:              get { return (int)base["Id"]; }
  73:              set { base["Id"] = value; }
  74:          }
  75:   
  76:          [ConfigurationProperty("Color")]
  77:          public ConsoleColor Color
  78:          {
  79:              get { return (ConsoleColor)base["Color"]; }
  80:              set { base["Color"] = value; }
  81:          }
  82:   
  83:          [ConfigurationProperty("Width")]
  84:          public int Width
  85:          {
  86:              get { return (int)base["Width"]; }
  87:              set { base["Width"] = value; }
  88:          }
  89:   
  90:          [ConfigurationProperty("Height")]
  91:          public int Height
  92:          {
  93:              get { return (int)base["Height"]; }
  94:              set { base["Height"] = value; }
  95:          }
  96:   
  97:          [ConfigurationProperty("Fill")]
  98:          public bool Fill
  99:          {
 100:              get { return (bool)base["Fill"]; }
 101:              set { base["Fill"] = value; }
 102:          }
 103:      }
 104:  }

Oraz app.config:

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:      <configSections>
   4:          <section name="MyConfiguration" type="AppConfigExample.MyConfiguration, AppConfigExample"/>
   5:      </configSections>
   6:      <MyConfiguration>
   7:          <Rectangles>
   8:              <Rectangle Id="1" Color="Red" Width="10" Height="5" Fill="False"/>
   9:              <Rectangle Id="2" Color="Green" Width="12" Height="6" Fill="False"/>
  10:              <Rectangle Id="3" Color="Black" Width="14" Height="3" Fill="True"/>
  11:              <Rectangle Id="4" Color="White" Width="15" Height="2" Fill="True"/>
  12:              <Rectangle Id="5" Color="Gray" Width="16" Height="10" Fill="False"/>
  13:          </Rectangles>
  14:      </MyConfiguration>
  15:  </configuration>

To by było na tyle, bierzcie i korzystajcie z tego, bo to dobre jest.
Pozostaje mi tylko życzyć dobrego w nadchodzącym nowym roku!
Tej!

Czym się różni właściwość klasy od pola klasy?

Wracając jeszcze do filozofowania o kodzie, dziś trochę o cechach klas, a dokładniej: pola i właściwości. Zanim wymyślono idee właściwości dostęp do pól odbywał się na dwa sposoby. Pole w klasie (np. name) mogło być publiczne i każdy miotał nim jak szatan, druga opcja to dostęp kontrolowany przez parę metod typu GetName i SetName. Umożliwiały one kontrolowanie tego kto i na jakich zasadach może korzystać z cech wewnętrznych klasy.
Później, aby pominąć pisanie GetName/SetName, wymyślono właściwości, które w sprytny sposób, miały pozbyć się z kodu klienta getów i setów, czyniąc kod bardziej czytelnym. Jednocześnie pozostawić możliwość kontrolowanie kto i jak ma dostęp do pola, poprzez “ukrytą” implementację tychże get i set. Wygląda więc to tak, że klient korzysta z naszego pola w klasie, które naprawdę jest jest właściwością opakowującą to pole. Zresztą każdy wie jak wygląda i działa właściwość w C#.
Teraz czas na właściwą filozofię, czym się różni właściwość klasy od pola w klasie?

  1. Właściwość może zostać tak zaimplementowana, aby była tylko do zapisu lub tylko do odczytu. Pole w klasie jest najczęściej implementowane czytalne/pisalne (uwielbiam nowo mowę). Czasem tworzy się stałe. (ot czasami tak trzeba).
  2. Właściwość podczas zapisu lub odczytu może nas poczęstować wybornym wyjątkiem, ta sama operacja wykonana na polu nigdy.
  3. Właściwość nie może zostać przekazana jako ref lub out argument metody, pole klasy tak.
  4. Czas wykonania właściwości może być czasem bardzo długi, np. podczas wykonywania synchronizacji wątków, leniwej inicjalizacji właściwości, etc. Dostęp do pola zawsze jest operacją natychmiastową.
  5. Dostęp do właściwości może powodować efekty uboczne, np. w cache, każdy dostęp może zmniejszać czas życia keszu. Dostęp do pola nigdy nie ma takiej możliwości.
  6. Odczytanie właściwości klasy czasem może się wymagać zwiększania zapotrzebowania na pamięć RAM, potrzebę na przeliczenie pewnych zależności, czy wykonanie dodatkowych operacji, może także zwrócić nie pełny wynik. Dostęp do pola, nie wymaga dodatkowych środków, oraz zwraca oryginalną zawartość tego pola.

Nasuwa się pytanie, czy wy będąc klientem klasy X, która oferuje dostęp do właściwości, oczekujecie, że dostęp do właściwości Name będzie:

  • trwał strasznie długo
  • każdy operacja odczytu zmienia, zwraca inną wartość
  • dostaniecie tylko spółgłoski
  • zanim dostaniecie wynik, zużycie pamięci wzrośnie o kilkanaście mega

Takich rzeczy można oczekiwać od metod, to one powinny robić coś skomplikowanego, dostęp do pola powinien być możliwie szybki i prosty. Warto o tym pamiętać, pisząc swój kawałek kodu. Może trzeba wrócić do poczciwego GetName(), GetNameConsonantsOnly(),
GetNameVowelsOnly(), GetNameUsing5MbRamMore() ?

Ot takie tam żywcem zerżnięte informacje z książki którą czytam i pomyślałem, że warto o tym napisać.

ps.
DateTime.Now 😉