Metody rozszerzające – testowanie i porządek w api.

Jak każdemu porządnemu developerowi zdarza mi się czasem napisać testy. Jak każdemu porządnemu developerowi, czasem zdarza mi się wykorzystać mechanizm metod rozszerzających (jeśli nie wiesz o czym mówię sprawdź na msdn). Jak każdy prawdziwy developer, chciałem przetestować logikę, która była wykorzystywana w jednej z takich metod. W zasadzie to nie w samej metodzie, chciałem sprawdzić czy zostanie wywołana z wartościami, które są dla mnie ważne.
Zacznę od metod rozszerzających, a testowanie przyjdzie samo.

Tak wygląda interfejs udostępniony przez samą (bez metod rozszerzających) klasę List:

Aby przewinąć listę na sam dół wystarczą trzy wciśnięcia Page Down. Jak widać jedyny import, z którego korzystam to System.Collections.Generic, wymagany aby móc skorzystać z List

To się stanie gdy dodam System.Linq: (obrazek się troszkę nie mieści)

Pojawi się nowa funkcjonalność, zdefiniowana w System.Linq, która pozwala robić cuda z listą i innymi IEnumerable (jak również innymi obiektami). Po imporcie aby przewinąć listę metod, trzeba osiem razy walnąć w Page Down.

Co zrobić, aby lista nowych funkcjonalności nie zaśmiecała dostępnego api? Można to wszystko ukryć przy pomocy jednej klasy, która przekierowuje żądania dalej, do właściwej części, gdzie zdefiniowane będą rozszerzenia
Normalnie implementacja rozszerzeń może wyglądać jakoś tak:

.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:  namespace ConsoleApplication
   2:  {
   3:   
   4:      class Program
   5:      {
   6:          static void Main(string[] args)
   7:          {
   8:              MyClass mc = new MyClass();
   9:          }
  10:      }
  11:   
  12:      public static class MyClassExtension
  13:      {
  14:          public static void Foo0(this MyClass target) { }
  15:          public static void Foo1(this MyClass target) { }
  16:          public static void Foo2(this MyClass target) { }
  17:          public static void Foo3(this MyClass target) { }
  18:          public static void Foo4(this MyClass target) { }
  19:          public static void Foo5(this MyClass target) { }
  20:          public static void Foo6(this MyClass target) { }
  21:          public static void Foo7(this MyClass target) { }
  22:          public static void Foo8(this MyClass target) { }
  23:          public static void Foo9(this MyClass target) { }
  24:      }
  25:   
  26:      public class MyClass
  27:      {
  28:      }
  29:  }

Po takiej implementacji otrzymujemy cztery metody podstawowe, oraz dodatkowy dziesięć nowych z rozszerzenia w MyClassExtension.

Można trochę to wszystko uprościć dodając odpowiednią warstwę abstrakcji (abstrakcja was uwolni, zapamiętajcie to). Opis poniżej:

   1:  namespace ConsoleApplication
   2:  {
   3:      using System;
   4:   
   5:      class Program
   6:      {
   7:          static void Main(string[] args)
   8:          {
   9:              MyClass mc = new MyClass();
  10:              mc.ExtensionService().Foo0();
  11:          }
  12:      }
  13:   
  14:      public static class MyClassExtension
  15:      {
  16:          public static Func<MyClass, IMyClassExtensionService> ExtensionFactory { get; set; }
  17:   
  18:          static MyClassExtension()
  19:          {
  20:              ExtensionFactory = target=> new MyClassExtensionReleaseVersion(target);
  21:          }
  22:   
  23:          public static IMyClassExtensionService ExtensionService(this MyClass target)
  24:          {
  25:              return ExtensionFactory(target);
  26:          }
  27:      }
  28:   
  29:      public interface IMyClassExtensionService
  30:      {
  31:          void Foo0();
  32:          void Foo1();
  33:          void Foo2();
  34:          void Foo3();
  35:          void Foo4();
  36:          void Foo5();
  37:          void Foo6();
  38:          void Foo7();
  39:          void Foo8();
  40:          void Foo9();
  41:      }
  42:   
  43:      public class MyClassExtensionReleaseVersion : IMyClassExtensionService
  44:      {
  45:          private MyClass myclass;
  46:   
  47:          public MyClassExtensionReleaseVersion(MyClass target)
  48:          {
  49:              this.myclass = target;
  50:          }
  51:   
  52:          public void Foo0() { }
  53:          public void Foo1() { }
  54:          public void Foo2() { }
  55:          public void Foo3() { }
  56:          public void Foo4() { }
  57:          public void Foo5() { }
  58:          public void Foo6() { }
  59:          public void Foo7() { }
  60:          public void Foo8() { }
  61:          public void Foo9() { }
  62:      }
  63:   
  64:      public class MyClass
  65:      {
  66:      }
  67:  }

Tak się to prezentuje teraz:

Zacznę od linijki 29 gdzie zdefiniowany został interfejs ze wszystkimi metodami, które mają działać jako rozszerzenie dla klasy MyClass. Linia 43 to implementacja tego interfejsu, warto zauważyć że klasa ta nie jest statyczna, oraz nie posiada statycznych pól, a metody nie przyjmują parametrów. W konstruktorze otrzymuje ona obiekt, na rzecz którego ma działać. Najlepsze zostawiam na koniec, linijka 14, klasa udostępniająca rozszerzenie dla MyClass. W ExtensionService (23) wykorzystywana jest właściwość ExtensionFactory, który jest zwyczajną metodą wytwórczą, zwracającą nowy obiekt MyClassExtensionReleaseVersion, do którego przesyła obiekt, na rzecz którego ma zostać wywołana metoda rozszerzająca. Mam nadzieję, że kod tłumaczy to prościej niż ja. Linia 10 to nowy sposób na wywołanie metod rozszerzających. 
Właściwość ExtensionFactory została zdefiniowana jako publiczna, co umożliwia jego zmianę w razie potrzeby testowania. Tak samo obiekt rozszerzający jest deklarowany poprzez interfejs, w związku z czym w testach można wykorzystać np. MyClassExtensionMockVersion i sprawdzać czy klasa zachowuje się tak jak tego oczekujemy.

Ja skorzystałem z tego rozwiązania podczas testowania nawigacji w projekcie, który korzysta z PRISMa, tam do zmiany widoków wykorzystywany jest interfejs IRegionManager i kilka metod rozszerzających ten interfejs. Chciałem sprawdzić, czy po wykonaniu operacji X jedna z moich klas zarząda zmiany widoku. Jedynym sposobem, na sprawdzenie parametrów przesłanych w query było stworzenie i podstawienie własnej implementacji metod rozszerzających. Na początku testów mam taki zapis:

   1:  Capture.Common.Prism.PrismExtensions.ServiceFactory = p => new NavigationServiceMock(p);

Tak wygląda mój lipny serwis, który działa zamiast tego z Prism:

   1:  public class NavigationServiceMock : INavigation
   2:  {
   3:      private IRegionManager p;
   4:   
   5:      public NavigationServiceMock(IRegionManager p)
   6:      {
   7:          this.p = p;
   8:      }
   9:   
  10:      public IRegionManager AddToRegion(string regionName, object view)
  11:      {
  12:          throw new NotImplementedException();
  13:      }
  14:   
  15:      public IRegionManager RegisterViewWithRegion(string regionName, Func<object> getContentDelegate)
  16:      {
  17:          throw new NotImplementedException();
  18:      }
  19:   
  20:      public IRegionManager RegisterViewWithRegion(string regionName, Type viewType)
  21:      {
  22:          throw new NotImplementedException();
  23:      }
  24:   
  25:      public void RequestNavigate(string regionName, string source)
  26:      {
  27:          throw new NotImplementedException();
  28:      }
  29:   
  30:      public void RequestNavigate(string regionName, Uri source)
  31:      {
  32:          LoadTradeViewModelTest.NavigationServiceMock_RequestedNavigatedTargetRegionName = regionName;
  33:          LoadTradeViewModelTest.NavigationServiceMock_ReqestedNavigatedTargetUri = source.OriginalString;
  34:      }
  35:   
  36:      public void RequestNavigate(string regionName, string source, Action<NavigationResult> navigationCallback)
  37:      {
  38:          throw new NotImplementedException();
  39:      }
  40:   
  41:      public void RequestNavigate(string regionName, Uri source, Action<NavigationResult> navigationCallback)
  42:      {
  43:          throw new NotImplementedException();
  44:      }
  45:  }

Najważniejsze w tym wszystkim to RequestNavigate (30) gdzie zapisuje żądany region i uri. Później w teście porównuje czy są takie jak oczekiwałem. Wszystkie NotImplementedException są nie używane w testach, dlatego też mogły pozostać w takiej domyślnie oferowanej przez VS formie.
Normalna implementacja przekazuje wszystkie parametry do oryginalnej implementacji.

Pojawia się pewien problem, przy próbie stworzenie podobnego rozwiązania dla klas generycznych. Ten problem to kompilator, który cały czas twierdzi że metody rozszerzające muszą być w klasach statycznych, nie generycznych. Same metody mogą być generyczne. Aby to obejść na szybko wymyśliłem coś takiego

   1:  namespace ConsoleApplication
   2:  {
   3:      using System;
   4:      using System.Collections.Generic;
   5:   
   6:      class Program
   7:      {
   8:          static void Main(string[] args)
   9:          {
  10:              List<int> l = new List<int>();
  11:              l.GetExtension().Foo1();
  12:          }
  13:      }
  14:   
  15:      public static class ExtensionsFactory<T>
  16:      {
  17:          public static Func<List<T>, IListExtensionService> Factory { get; set; }
  18:      }
  19:   
  20:      public static class ListExtension
  21:      {
  22:          public static IListExtensionService GetExtension<T>(this List<T> target)
  23:          {
  24:              if (ExtensionsFactory<T>.Factory == null)
  25:              {
  26:                  ExtensionsFactory<T>.Factory = o => new ListExtensionSerivce<T>(o);
  27:              }
  28:   
  29:              return ExtensionsFactory<T>.Factory(target);
  30:          }
  31:      }
  32:   
  33:      public interface IListExtensionService
  34:      {
  35:          void Foo1();
  36:      }
  37:   
  38:      public class ListExtensionSerivce<T> : IListExtensionService
  39:      {
  40:          private List<T> list;
  41:   
  42:          public ListExtensionSerivce(List<T> target)
  43:          {
  44:              this.list = target;
  45:          }
  46:   
  47:          public void Foo1()
  48:          {
  49:              Console.WriteLine("foo and the list");
  50:          }
  51:      }
  52:  }

Cały trik w tym przypadku, to przeniesienie odpowiedzialności na wytwarzanie obiektów z ListExtension do ExtensionsFactory. A podczas wywołania metody, należny się upewnić że fabryka już istnieje.

Myślę że wystarczy mojego wymądrzania się na dziś. Podoba się? Nie podoba się? Macie inne sposoby? Pytania? Uwagi? Cokolwiek?

ps
Mam nadzieję, że więcej wyjaśniłem niż zakręciłem.
ps2
Jak już wszystko napiszę, to czytam czy jest ok, a potem i tak mam lekkiego stresa, że coś głupiego napisałem.

RX extensions w przykładach

RxExtension – to biblioteka od Microsoftu ułatwiająca programowanie asynchroniczne. Opiera się na istniejących interfejsach IObservable oraz IObserver.
W RX wiadomości są traktowane jako strumienie danych, do których należy się przypiąć i reagować na pojawienie się nowej wiadomości. Najnowsza wersja ma już cyferkę 2, ale nie jest jeszcze oznaczona jako stabilna. Ja do nauki wykorzystałem wersję 1 oraz książeczkę dostępną na stronie RX – Dev Labs Hands On. Na Channel9 są jakieś filmy na temat RX. Poniżej pokaże kilka przykładów i opis jak korzystać z RX, źródła pochodzą z książki, będą dostępne razem z książką i projektem MSVC. Drobna uwaga co do książki i źródeł: w książce podane są dwa pliki do których należny dodać referencję, aktualna wersja RX dostarcza tylko jednej biblioteki – System.Reactive, natomiast System.CoreEx jest już w systemie. Ja korzystałem z .net 4.0. Podobnie kilka metod wymienionych w książce nie jest już dostępne w związku z czym zostały zamienione na inne dostępne i spełniające te same wymagania.

Zaczynamy od poznania interfejsów:

.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:      IObservable<int> source = Observable.Empty<int>();
   4:      IObserver<int> handler = null;
   5:   
   6:      IDisposable subscription = source.Subscribe();
   7:      Console.WriteLine("Press ENTER to unsubscribe and dispose");
   8:      Console.ReadLine();
   9:   
  10:      subscription.Dispose();
  11:  }

RX opiera się o dwa interfejsy IObservable oraz IObserver. Klasa Observable pochodzi System.Reactive.Linq. Kod powyżej nie jest zbytnio porywający, ale pokazuje, że po subskrypcji do źródła danych otrzymujemy instancję IDisposable, którą trzeba będzie wywalić do kosza po zakończeniu pracy. Później będzie to zrobione przy użyciu mechanizmu using.

Interfejs IObserver definiuje trzy metody, które muszę zostać zdefiniowane:

   1:  public interface IObserver<in T>
   2:  {
   3:      void OnCompleted();
   4:      void OnError(Exception error);
   5:      void OnNext(T value);
   6:  }
  • OnCompleted: źródełko wyschło
  • OnNext:  nowa informacja
  • OnError: coś poszło nie tak

Podczas dopisywania się do obiektu, który ma dostarczać informacji można wykorzystać wyżej wymieniony interfejs lub skorzystać z wyrażeń lambda. To drugie podejście jest prostsze, dodatkowo nie wymagane jest definiowanie dla wszystkich metod z interfejsu.
Na początek pełna deklaracja:

   1:  static void Main(string[] args)
   2:  {
   3:      IObservable<int> source = Observable.Empty<int>();
   4:   
   5:      IDisposable subscription = source.Subscribe(
   6:          x=> Console.WriteLine("Has new value {0}", x),
   7:          ex=>Console.WriteLine("Exception caught {0}", ex.Message),
   8:          ()=>Console.WriteLine("No more items")
   9:          );
  10:   
  11:      Console.WriteLine("ENTER to dispose");
  12:      subscription.Dispose();
  13:  }

W tym przypadku od razu zostanie wywołana metoda OnCompleted, ponieważ źródło danych jest puste. Po zakończeniu pracy zwalniamy zasoby przez wywołanie Dispose na obiekcie zwróconym po subskrypcji. Co ważne, aby móc korzystać z rozszerzeń dla RX należy dodać referencję oraz using do System.Reactive.Linq.

Jednoelementowa kolekcja intów:

   1:  static void Main(string[] args)
   2:  {
   3:      IObservable<int> source = Observable.Return(42);
   4:   
   5:      var subscriber = source.Subscribe(
   6:          x=>Console.WriteLine("Value: {0}",x),
   7:          ex=>Console.WriteLine("Exception: {0}", ex.Message),
   8:          ()=>Console.WriteLine("end!")
   9:          );
  10:   
  11:      Console.WriteLine("ENTER to dispose");
  12:      subscriber.Dispose();
  13:  }

Kod powyżej zwróci raz wartość 42 (sens życia), a następnie zakończy poprzez wywołanie OnCompleted. Następnie zwolnienie zasobów. Ponownie wykorzystanie Reactive.Linq do stworzenia jednoelementowej kolekcji.

Aby zrobić coś ciekawszego można podmienić kod definiujący źródło na coś takiego:

   1:  IObservable<int> source = Observable.Range(5, 7);

Spowoduje do wygenerowanie cyfr od 5 do 11, następnie zakończy przez OnCompleted.

Aby nie zanudzać prostymi przykładami, RX umożliwia stworzenie obserwowanej pętli for, robi się to w taki sposób:

   1:  static void Main(string[] args)
   2:  {
   3:      IObservable<int> source = Observable
   4:          .Generate(0,          // initial state
   5:          i => i < 10,    // condition
   6:          i => i + 1,        // iteration step
   7:          i => i * i);       // iteration operation
   8:   
   9:      using (var s = source.Subscribe(
  10:          x => Console.WriteLine(x)    // only the working stuff will be handled
  11:                                          // no errors and no exceptions
  12:                                          // no information about sequence finish either
  13:          )) { };
  14:  }

Coraz ciekawiej co nie?
Zaczynamy od 0, następnie warunkiem jest i mniejsze od 10, w każdym kroku i będzie zwiększane co 1, a wynikiem operacji ma być i*i, wynik mnożenia nie jest zapisywany do i, tylko zwracany jako wynik. W przeciwnym wypadku pętla skończyła by się za wcześnie.
Podczas dopinania się do źródła definiujemy tylko metodę OnNext, która przyjmuje jeden parametr, w ten sposób nie zostaniemy powiadomieni o skończeniu się danych lub o wystąpieniu błędu. Dodatkowo wykorzystany zostanie mechanizm using, na który spadnie odpowiedzialność zwolnienia zasobów z IDisposable.

Jeśli zastanawialiście się jak to możliwe że pętla nie zakończy skoro w klamrach using nic nie ma, nie dziwcie się, dla mnie na początku to także było dziwne. Ale jeśli odpalić debuggera i sprawdzić wątki, to wszystkie wywołania są dokonywane z głównego wątku. Dopiero po skończeniu się zasobów w IObservable, zostanie wykonany kod z klamerek.

Żeby nie było nudno, teraz przykład z normalniejszym kodem, takim który jest w klamerkach:

   1:  static void Main(string[] args)
   2:  {
   3:      var source = Observable.Generate(
   4:          0,
   5:          i => i < 10,
   6:          i => i + 1,
   7:          i => i * i,
   8:          i => TimeSpan.FromSeconds(i)
   9:          );
  10:   
  11:      using (var s = source.Subscribe(
  12:          x => Console.WriteLine("next: {0}", x),
  13:          ex => Console.WriteLine("exception: {0}", ex.Message),
  14:          () => Console.WriteLine("no more")))
  15:      {
  16:          Console.WriteLine("ENTER");
  17:          Console.ReadLine();
  18:      }
  19:  }

Dwie zmiany zostały wprowadzone; dodałem 1 sekundowe opóźnienie w generowaniu wartości i, co powoduje że wywoływania zaczynają wreszcie być prawdziwie asynchroniczne. To powoduje, potrzebę umieszczenie Console.ReadLine w ciele using, w przeciwnym wypadku, nie zdążymy odczytać nawet pierwszej wartości. Walnięcie ENTER w trakcie działania dema, spowoduje jego zakończenie.
Okno wątków w debuggerze, pokazują że podczas wołania OnNext, wykorzystywany jest dodatkowy Worker Thread, stworzony przez RX.

Teraz drobna zmiana, do naszej konsoli dołożona zostanie prosta forma (WinForms – przykłady z książki są na tym oparte, więc i ja z tego skorzystałem). Będzie służyć za pośrednika do źródła danych, załóżmy sobie że jest taki kod:

   1:  static void Main(string[] args)
   2:  {
   3:      var label = new Label();
   4:      var form = new Form
   5:      {
   6:          Controls = { label }
   7:      };
   8:   
   9:      var moves = Observable.FromEventPattern<MouseEventArgs>(form, "MouseMove");
  10:   
  11:      using (moves
  12:          .Subscribe(
  13:              x => label.Text = x.EventArgs.Location.ToString(),
  14:              ex => label.Text = ex.Message,
  15:              () => label.Text = "Mouse is over?!" ) )
  16:      {
  17:          Application.Run(form);
  18:   
  19:      }
  20:  }

Pojawia się tutaj prosta forma z tekstem do którego będziemy zapisywać informacje o aktualnym położeniu myszy. To co interesujące znajduje się w linijce 9, gdzie tworzymy źródło danych na podstawie przychodzących zdarzeń z formy, następnie zapisujemy się do nich. Ponieważ w tym przykładnie nie stosowane są żadne specjalne opóźnienia (o nich poniżej), nie potrzeba tutaj wykorzystywać Dispatchera czy obiekty synchronizacji pomiędzy wątkami. W okienku wątków widać, że każde wywołanie metody OnNext realizowane jest w głównym wątku UI, dlatego bezpieczna jest modyfikacja labelki.

   1:  static void Main(string[] args)
   2:  {
   3:      var label = new Label();
   4:      var form = new Form
   5:      {
   6:          Controls = { label }
   7:      };
   8:   
   9:      var moves = Observable.FromEventPattern<MouseEventArgs>(form, "MouseMove");
  10:   
  11:      using (moves
  12:          .Subscribe(
  13:              x => label.Text = x.EventArgs.Location.ToString(),
  14:              ex => label.Text = ex.Message,
  15:              () => label.Text = "Mouse is over?!" ) )
  16:      {
  17:          Application.Run(form);
  18:      }
  19:  }

Najciekawsza jest linia 9, gdzie wydarzenia generowane przez formę/mysz są definiowane jako źródło danych, a następnie w linii 13 wartość ta zostaje wyłuskana z argumentów oraz zapisana w labelce na formatce.

Kolejny przykład będzie rozwinięciem tego powyżej:

   1:  static void Main(string[] args)
   2:  {
   3:      var textbox = new TextBox();
   4:      var form = new Form
   5:      {
   6:          Controls = { textbox }
   7:      };
   8:   
   9:      var moves = Observable
  10:          .FromEventPattern<MouseEventArgs>(form, "MouseMove")
  11:          .Select(e => e.EventArgs.Location);
  12:   
  13:      var texts = Observable
  14:          .FromEventPattern<EventArgs>(textbox, "TextChanged")
  15:          .Select(e => (e.Sender as TextBox).Text);
  16:   
  17:      var msubs = moves.Subscribe(x => Console.WriteLine("mouse position: {0}", x));
  18:      var ksubs = texts.Subscribe(x => Console.WriteLine("Textbox text: {0}", x));
  19:   
  20:      using (new CompositeDisposable(msubs, ksubs))
  21:      {
  22:          Application.Run(form);
  23:      }
  24:  }

Jak widać tutaj ponownie nasłuchujemy na wiadomości generowane przez ruch myszy, ale dodatkowo oczekujemy na wpisywany przez użytkownika tekst w formatce. Warto zauważyć, że w przypadku tekstu, źródłem danych nie jest formatka, ale sama kontrolka. W tym przykładzie pod uwagę bierzemy tylko pozytywne wywołania OnNext. Dodatkowo subskrypcja wykorzystuje projekcję (Select) i ostatecznej rozgrywce (17 i 18) otrzymujemy tylko takie wartości, które są dla nas najbardziej interesujące. Na sam koniec przykładu, wykorzystujemy klasę CompositeDisposable, która umożliwia wykorzystanie mechanizmu using na więcej niż jednym obiekcie IDisposable.

Kolejnym ciekawy rozszerzenie dla RX jest mechanizm Distinct, który wyśle powiadomienie tylko wtedy, gdy jest ono różne od poprzedniego.

   1:  var texts = Observable
   2:                .FromEventPattern<EventArgs>(textbox, "TextChanged")
   3:                .Select(e => (e.Sender as TextBox).Text)
   4:                .Do(e => Console.WriteLine("Before DistinctUntilChanged: {0}", e))
   5:                .DistinctUntilChanged();

Cały kod jest taki sam jak poprzednio, zmienia się tylko definicja źródła danych dla tekstu. Linia 4 pokazywać będzie, że wiadomości są przesyłane – metoda Do. Natomiast linia 5 zatrzyma ich dalszą propagację, gdy będę one takie same jak poprzednio otrzymane. Należy zwrócić uwagę podczas używania DistinctUntilChanged na kolejność jego użycia. Jeśli zostanie wywołany za wcześnie, kolejne eventy mogą nie zostać przesłane. W ramach ćwiczeń można wstawić go przed Select i zobaczyć co się dzieje. W tym przykładzie, pod uwagę brana będzie zawartości kontrolki w textboxie. A więc, gdy zaznaczona zostanie literka i zostanie zastąpiona przez taką samą, obiekt nasłuchujący nie zostanie powiadomiony o takie zmianie.

Następne rozszerzenie dla RX to Throttle, który powoduje że powiadomienia są przesyłane z pewnym opóźnieniem, każda nowa aktualizacja na źródle resetuje ten licznik. Dla przykładu, gdy chcemy udostępnić mechanizm który działa jak słownik z podpowiedziami, aby nie włączać wyszukiwania dla każdej wpisanej literki, można dodać małe opóźnienie rzędu kilkudziesięciu milisekund, które dla użytkownika będzie nie zauważalne, a jego wykorzystanie spowoduje zmniejszenie wykorzystania zasobów, ponieważ ilość generowanych zapytać będzie mniejsza. Poniżej przykład definicji takiego źródła danych:

   1:  var moves = Observable
   2:            .FromEventPattern<MouseEventArgs>(form, "MouseMove")
   3:            .Select(e => e.EventArgs.Location)
   4:            .Throttle(TimeSpan.FromMilliseconds(100));
   5:   
   6:  var texts = Observable
   7:            .FromEventPattern<EventArgs>(textbox, "TextChanged")
   8:            .Select(e => (e.Sender as TextBox).Text)
   9:            .Throttle(TimeSpan.FromMilliseconds(500))
  10:            .DistinctUntilChanged();

W pierwszy przypadku, informację o pozycji myszy otrzymamy gdy użytkownik okres około 100 ms nie będzie poruszać myszą. Należy być świadomym, że nie dostaniemy wszystkich wartości pośrednich, tylko ostatnią pozycję myszy. W drugim przypadku, jest dodatkowe założenie, użytkownik nie może zmieniać wartości tekstu przez około 500ms, a dodatkowo wartość musi być inna niż poprzednia – modyfikator DistinctUntilChanged. W bardzo prosty sposób ograniczyć można ilość powiadomień, które obsłużyć musi obiekt obsługujący kontrolkę.

Wcześniej mówiłem o tym, że będzie o pełniejszej asynchroniczności oraz o kontrolkach, czas najwyższy na rozwiązanie tej tajemnicy. Jeśli wiemy, że obsługa zdarzeń będzie wymagała interakcji z kontrolkami (lub kolekcjami – np. ObservableCollection w WPF) wymagającymi synchronizami z głównym wątkiem UI należy wykorzystać odpowiednie rozszerzenie, które umożliwia obserwację zdarzeń wykonywaną na głównym wątku aplikacji – zamieszane? Kod was oświeci:

   1:  static void Main(string[] args)
   2:  {
   3:      TextBox t1 = new TextBox();
   4:      Label l1 = new Label { Left = t1.Width + 20 };
   5:      Form f1 = new Form
   6:      {
   7:          Controls = { t1, l1 }
   8:      };
   9:   
  10:      var source = Observable
  11:          .FromEventPattern(t1, "TextChanged")
  12:          .Throttle(TimeSpan.FromMilliseconds(250))
  13:          .Select(x=>(x.Sender as TextBox).Text)
  14:          .DistinctUntilChanged();
  15:   
  16:      using (source
  17:          .ObserveOn(WindowsFormsSynchronizationContext.Current)
  18:          .Subscribe(x => l1.Text = x))
  19:      {
  20:          Application.Run(f1);
  21:      }
  22:  }

Jak widać, źródło danych działa teraz prawdziwie asynchronicznie (wykorzystanie Throttle), aby móc wykonać kod zdefiniowany w OnNext (linia 18) należy wcześniej powiedzieć że obsługa (obserwacja) ma nastąpić w WindowsFormsSynchronizationContext.Current (dla WPF to będzie Application.Current.Dispatcher lub Dispatcher.Current) – lub w implementacji wykorzystać mechanizm synchronizacji, jak zawsze to zależy od potrzeb implementacji. W takiej sytuacji źródło działa w osobnym wątku, ale powiadomienia i ich obsługa nastąpi w wątku głównym, gdzie można modyfikować UI.

Następny przykład jest trochę dłuższy, pokazuje jak wykorzystać RX w połączeniu z serwisami implementowanymi w WCF, dokładniejsze omówienie poniżej:

   1:  private static void Main()
   2:  {
   3:      var service = new DictServiceSoapClient("DictServiceSoap");
   4:      Func<string, string, string, IObservable<DictionaryWord[]>> match = Observable
   5:          .FromAsyncPattern<string, string, string, DictionaryWord[]>(
   6:              service.BeginMatchInDict, service.EndMatchInDict);
   7:   
   8:      Func<string, IObservable<DictionaryWord[]>> matchInWordNetByPrefix = term => match("wn", term, "prefix");
   9:   
  10:   
  11:      var t1 = new TextBox();
  12:      var f1 = new Form
  13:                   {
  14:                       Controls = {t1}
  15:                   };
  16:   
  17:   
  18:      IObservable<string> textSource = Observable
  19:          .FromEventPattern<EventArgs>(t1, "TextChanged")
  20:          .Throttle(TimeSpan.FromMilliseconds(250))
  21:          .Select(x => ((TextBox) x.Sender).Text)
  22:          .Where(x => x.Length > 0)
  23:          .DistinctUntilChanged();
  24:   
  25:      IDisposable formRequest = textSource
  26:          .Finally(()=>Console.WriteLine("Finalized"))
  27:          .Subscribe(x => matchInWordNetByPrefix(x)
  28:                              .Finally(() => Console.WriteLine("Finalized: {0}", x))
  29:                              .Subscribe(words =>
  30:                                             {
  31:                                                 Console.WriteLine("{0} - {1}", x, words.Count());
  32:                                                 foreach (DictionaryWord w in words)
  33:                                                 {
  34:                                                     Console.Write("{0},", w.Word);
  35:                                                 }
  36:                                                 Console.WriteLine("n*******************************************");
  37:                                             },
  38:                                         ex =>
  39:                                             {
  40:                                                 Console.WriteLine("n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
  41:                                                 Console.WriteLine(ex.Message);
  42:                                                 Console.WriteLine("n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
  43:                                             }));
  44:   
  45:   
  46:      using (formRequest)
  47:      {
  48:          Application.Run(f1);
  49:      }
  50:  }

Zacznijmy od początku:
Linia 3 definiuje nowe proxy do serwisu WCF, nie istotne z punktu widzenia RX. Następnie linia 4 definiuje zmienną typu Func, będzie to delegat przyjmujący jako dane wejściowe trzy stringi, a zwracający tablicę wyrazów słownikowych (ach ten mój angielski). Dalej (ciągle w linii 4) przypisujemy do tego delegata metodę z RX wykorzystując wcześniej zdeklarowany serwis WCF, a dokładniej dwie jego metody BeginMatchInDict oraz EndMatchInDict. Są to dwie metody, które umożliwiają asynchroniczne wykorzystanie serwisu. Podczas definiowania dostępu do serwisu WCF zaznaczona została opcja generowania zapytań asynchronicznych. Dalej, linia 8 to ponowne zdefiniowanie delegata, tym razem będzie on przyjmować tylko jeden argument – string, który będzie zawierać właściwe dla nas zapytanie, zwracany typ nie ulegnie zmianie. Pozostałe dwa stringi zostają przypisane na stałe. Ich wartość nie jest istotna dla RX. Teraz możemy korzystać asynchronicznie z WCF podając tylko jednego stringa podczas zapytania. Potem “normalna” definicja formatki, później źródełko z danymi;  pierwsze to textbox, drugie to serwis WCF który na podstawie wysłanej części słowa, zwraca tablicę wyrazów, który mogą je dokończyć (np. “yellow a“: yellow adder’s tongue,yellow ageratum,yellow asphodel,yellow avens). Metoda Finally posłuży aby wykonać jakiś kod w momencie zwalniania zasobów zwróconych przez Subscribe. Kod z linijki 28 zostanie wykonany po tym, gdy wszystkie pasujące słowa zostaną wypisane na ekran. Linijka 26 zostanie wywołana po zamknięciu formatki, czyli  zakończeniu using z linii 46.

Jeśli podczas zabawy z tym przykładem zauważycie, że możliwe jest zdefiniowanie nowego pytania do serwisu WCF zanim, przyjdzie odpowiedź na wcześniejsze – to macie racje (za .net rocks – golfclap for you). Występuje tu taka sytuacja. Na rozwiązanie tego problemu zaproponowano dwa podejścia, dla mnie Switch jest (będzie poniżej) czytelniejszy i zrozumiały. W udostępnionym projekcie są oba rozwiązania.

   1:  static void Main(string[] args)
   2:  {
   3:      var t1 = new TextBox();
   4:      var l1 = new ListBox { Top = t1.Height + 10, Height = 250, Width = 150 };
   5:      var f1 = new Form
   6:      {
   7:          Controls = { t1, l1 }
   8:      };
   9:   
  10:      var textSource = Observable
  11:          .FromEventPattern<EventArgs>(t1, "TextChanged")
  12:          .Throttle(TimeSpan.FromMilliseconds(50))
  13:          .Select(x => (x.Sender as TextBox).Text)
  14:          .Where(x => x.Length >= 3)
  15:          .DistinctUntilChanged()
  16:          .Do(Console.WriteLine);
  17:   
  18:      var service = new DictServiceSoapClient("DictServiceSoap");
  19:      var dictSource = Observable
  20:          .FromAsyncPattern<string, string, string, DictionaryWord[]>(service.BeginMatchInDict, service.EndMatchInDict);
  21:   
  22:      Func<string, IObservable<DictionaryWord[]>> matchInWordNetByPrefix = term => dictSource("wn", term, "prefix");
  23:   
  24:      var data = textSource
  25:          .Select(x => matchInWordNetByPrefix(x))
  26:          .Switch();
  27:   
  28:      using (data
  29:          .ObserveOn(WindowsFormsSynchronizationContext.Current)
  30:          .Subscribe(w =>
  31:          {
  32:              l1.Items.Clear();
  33:              l1.Items.AddRange(w.Select(word => word.Word).ToArray());
  34:          },
  35:          ex =>
  36:          {
  37:              MessageBox.Show(ex.Message);
  38:          }))
  39:   
  40:          Application.Run(f1);
  41:  }

Początek bez zmian, dopiero podczas wykorzystywania tego co wpisze użytkownik i przesyłania tego do serwisu WCF, pojawiają się pierwsze zmiany. Na początku (linia 24) czekamy aż, użytkownik coś wpisze, a następnie wysyłamy to do serwisu. Jeśli w między czasie użytkownik zmieni coś w kontrolce, ponownie wyślemy zapytanie, ale z nową wartością. Aby nie obsługiwać wyników dla poprzedniego zapytania wykorzystana zostaje metoda Switch, której działanie polega na zwróceniu wyniku tylko z ostatniego zapytania, wszystkie poprzednie zostają anulowane. Reszta zmian to tylko kosmetyka, w tym przykładnie wyniki pojawiać się będą w kontrolce na formatce.

Na moje oko RX wydaje się być interesującym rozszerzeniem, umożliwiającym uproszczenie i kodu, a już na pewno metody jak Throttle czy DistinctUntilChanged uczynią aplikacje mniej zasobożernymi. Jak z każdą nowo poznaną technologią trzeba trochę czasu, aby poznać wszystkie za i przeciw. Ja na razie jestem na etapie wow. Poprawcie mnie jeśli się mylę, ale RX jest wyczesany.

Projekt, źródła i książka dostępne pod następującym adresem:
Projekt: https://bitbucket.org/jstadnicki/rx-examples
Git: https://bitbucket.org/jstadnicki/rx-examples.git

PRISM – materiały do nauki.

Jestem zafascynowany framworkiem PRISM, czytam, oglądam i piszę jakieś przykładowe aplikacje z wykorzystaniem tej biblioteki. Prism ułatwia tworzenia modularnych i dynamicznych systemów, dostarcza mechanizm do rozwiązywania zależności (Unity Container), umożliwia dynamiczną zmianę zachowania aplikacji poprzez wczytywanie dodatkowych modułów z katalogu lub aktualizację pliku konfiguracyjnego aplikacji – to tylko część z błyszczących bajerów.
Prism wspiera Silverlight, WPF czy Windows Phone 7 oraz mocno promuje wzorzec MVVM. Aby skorzystać z Prism wystarczy zaciągnąć paczkę z codeplex.com, rozpakować oraz dodać referencje do projektu.

Polecam dwa źródła do nauki: dla posiadających dostęp do płatnych szkoleń na Pluralsight taki oto kurs – Introduction to PRISM . Jeśli nie masz dostępu, nie masz się czym martwić na Channel9 jest inny równie dobry, oraz dwu i pół godzinny film akcji, gdzie autor tworzy prostą aplikację do odbierania i wysyłania poczty elektronicznej – Prism & Silverlight. Pierwszy z materiałów jest oparty na WPF, drugi to aplikacja webowa pisana w Silverlight.

Moje proste działania można obserwować na bitbucket.org pod adresem:
https://bitbucket.org/jstadnicki/prismcapture oraz link do repozytorium git https://jstadnicki@bitbucket.org/jstadnicki/prismcapture.git

Konfiguracja nhibernate

Nie taki straszny ten nhibernate jak się początkowo wydaje.
Wiem bo sam sprawdziłem, na początku myślałem, znowu wszystko w xml, nigdy nie wiadomo co i gdzie wpisać, … Nie tym razem, wystarczy obejrzeć sobie ten prosty wstęp i okazuje się, że podłączenie nh do lokalnego pliku z sqlce jest proste.
Chcesz korzystać z postgresql jako bazy danych, nic skomplikowanego. Zacznij od ściągnięcia paczki nhibernate. Następnie rozpakuj na dysk, a w środku znajdź katalog o nazwie “Configuration_Templates”, w nim są przykładowe pliku konfiguracyjne dla różnych baz, które są wspierane przez NH, w tym także postgresql.
Dla pełnego działania nh oraz psql wymagane jest także posiadanie npgsql – data provider.
Można zaciągnąć go z oficjalnej strony lub wykorzystując Application Stack Builder z paczki instalacyjnej do PostgreSQL. (Swoją drogą pewnie za pomocą nugeta też się da)
Należy wybrać wersje (jeśli mamy zainstalowanych kilka) psql a następnie driver, który nas interesuje:

Jak widać ja już mam zainstalowane to czego potrzebowałem. Teraz wystarczy już tylko dodać referencję do .dll i można działać.

Jeszcze tylko żeby zapamiętać, pliku konfiguracyjne dla nhibernate oznaczyłem jako content oraz copy always. Natomiast pliki odpowiadające za mapowanie obiektów jako embedded resource.
W moim przypadku konfiguracja zawiera dane użytkownika oraz hasło i nie może być w taki sposób wykorzystane w profesjonalnym rozwiązaniu, ale na potrzeby nauki oraz czytelności kodu jest jak jest.

Ostatnie żeby pamiętać, nh dostarcza schemat dla xml, aby mieć podpowiadanie składni w VS należy dodać pliki .xsd do projektu, albo wrzucić je do “X:PROGRAM_FILESMicrosoft Visual Studio VERSIONXmlSchemas”. Odpowiednio podmieniając X, PROGRAM_FILES oraz VERSION 😉

Jeśli ktoś chce może skorzystać z moich małych projektów, stworzonych na podstawie tutoriala z początku wpisu. Zawiera on konfiguracja dla mssql oraz pgsql i znajduje się tutaj lub można sklonować to https://bitbucket.org/jstadnicki/nhibernate-examples.git repozytorium.

Więcej nie wiem. Ale się nauczę.

ps.
W zasadzie skoro w tytule jest konfiguracja nhibernate to czemu nie wrzucić jest od razu tutaj:
MSSQL:

.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:  <hibernate-configuration 
   3:      xmlns="urn:nhibernate-configuration-2.2">
   4:      <session-factory>
   5:          <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
   6:          <property name="dialect">NHibernate.Dialect.MsSqlCeDialect</property>
   7:          <property name="connection.driver_class">NHibernate.Driver.SqlServerCeDriver</property>
   8:          <property name="connection.connection_string">Data Source=FirstSample.sdf</property>
   9:          <property name="show_sql">true</property>
  10:      </session-factory>    
  11:  </hibernate-configuration>

Linia 9 jest opcjonalna, powoduje że zapytania generowane przez NH są wypluwane na konsole.

PostgreSQL:

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <hibernate-configuration
   3:      xmlns="urn:nhibernate-configuration-2.2">
   4:      <session-factory name="NHibernate.Test">
   5:          <property name="connection.driver_class">NHibernate.Driver.NpgsqlDriver</property>
   6:          <property name="connection.connection_string">
   7:              Server=localhost;Port=12345;Database=Products;User ID=postgres;Password=1234567890;
   8:          </property>
   9:          <property name="dialect">NHibernate.Dialect.PostgreSQL82Dialect</property>
  10:          <property name="show_sql">True</property>
  11:      </session-factory>    
  12:  </hibernate-configuration>

Jak widać, w tym przypadku czarno na białawym podane jest hasło oraz nazwa użytkownika do bazy. TAK NIE MOŻE SIĘ DZIAĆ.

SOLIDnie po łebkach

Ilu z nas wracając do starego kodu (napisanego wczoraj/ tydzień temu/ miesiąc temu) krzywi się patrząc na bałagan, który po sobie pozostawił? W zasadzie prościej będzie chyba zapytać komu się to nie zdarza. Otóż amerykańscy naukowcy znaleźli na to sposób. No dobra może nie amerykańscy, ale skrót jest z angielskiego – SOLID, rozkłada się on na pięć czynników, a każdy z nich jest znowu jakimś skrótem.

S – (SRP) Single Responsibility Principle
O – (OCP) Open / Closed Principle
L – (LSP) Liskov Substitution Principle
I – (ISP) Interface Segregation Principle
D – (DIP) Dependency Inversion Principle
(podobno wszystko co napisane Helvetica jest mądrzejsze)

O co chodzi z tymi regułami wchodzącymi w skład SOLID? Jest to pięć zasad mówiących o tym jak tworzyć kod, który będzie łatwiej utrzymywać, rozszerzać, czytać, a problem głodu na świecie zniknie. Pitu pitu, już pokazuje przykłady, które będą proste, czasem wręcz naiwne, chodzi o ukazanie idei, nie rozwiązanie jakiegoś rzeczywistego problemu.

Single Resposibility Principle wszyscy piszą że “Nigdy nie powinno być więcej niż jednego powodu do modyfikacji”. Strasznie mnie irytuje taki zapis, bo można napisać “klasa jest odpowiedzialna tylko za jedną logiczną funkcjonalność”. Skoro służy do mierzenia, to nie służy do zapisywania. Jeśli ma konwertować wartości z jednego formatu na inny, to nie powinna ich wysłać do innych części aplikacji. etc., itd., itp.

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
max-height: 300px;
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:   public class BadHardwareDevice
   2:      {
   3:          int hardwareReadingLevel;
   4:          void DisplayHardwareLevelAlarm()
   5:          {
   6:              Console.WriteLine("Current value of {0} is dangerous!", hardwareReadingLevel);
   7:          }
   8:   
   9:          int GetHardwareReadings()
  10:          {
  11:              // reads data from device into hardwareReagingLevel
  12:              // return value from that reading
  13:              return hardwareReadingLevel;
  14:          }
  15:      }

Prosta klasa, ale zajmuje się ona dwiema rzeczami jednocześnie; odczytuje wartość z urządzenia (GetHardwareReadings) oraz wypisuje alarm na konsolę (DisplayHardwareLevelAlarm).
Kod można zmodyfikować na przykład w taki sposób:

   1:  public class BetterHardwareDeviceConsoleLogger
   2:      {
   3:          void DisplayHardwareLevelAlarm(int aLevel)
   4:          {
   5:              Console.WriteLine("Current value of {0} is dangerous!", aLevel);
   6:          }
   7:      }
   8:   
   9:      public class BetterHardwareDeviceReader
  10:      {
  11:          int hardwareReadingLevel;
  12:          int GetHardwareReadings()
  13:          {
  14:              // reads data from device into hardwareReagingLevel
  15:              // return value from that reading
  16:              return hardwareReadingLevel;
  17:          }
  18:      }

Teraz klasa odczytu zajmuje się tylko odczytem, natomiast klasa logująca alarm tylko wypisaniem na konsolę. Dodatkowo jeśli będzie potrzeba wysłania zapisu gdzieś indziej niż na konsolę nie będzie to miało wpływu na działanie innych komponentów. Bez modyfikacji już istniejących klas można stworzyć klasę BetterHardwareDeviceNetworkLogger i wysłać informację o poziomie wartości w świat.

Open / Close Principle wszyscy piszą, że klasa powinna być otwarta na rozszerzenia, a zamknięta na modyfikacje. I bądź mądry. Już nadciągam z chłopską odsieczą i rozumowaniem. Klasa powinna być tak napisana, aby nie trzeba było jej aktualizować gdy pojawi się nowa implementacja np. klasy logującej. Rozszerzenie funkcjonalne aplikacji powinno zostać zaimplementowane poza klasą, ona sama natomiast może z niej skorzystać. Jak zawsze bez kodu ciężko jest tłumaczyć:

   1:  public class BadLogger
   2:      {
   3:          public void LogMessage(string aMessage, BadLogTarger aTarget)
   4:          {
   5:              switch (aTarget)
   6:              {
   7:                  case BadLogTarger.ConLog:
   8:                      // write to console
   9:                      break;
  10:                  case BadLogTarger.NetLog:
  11:                      // write to network
  12:                      break;
  13:                  case BadLogTarger.DevNullLog:
  14:                      // ignore writing
  15:                      break;
  16:              }
  17:          }
  18:      }
  19:    
  20:      public enum BadLogTarger
  21:      {
  22:          ConLog,
  23:          NetLog,
  24:          DevNullLog,
  25:      }

Tutaj aby dodać logowanie np. do pliku należy dodać kolejnego enuma (przewińcie przykładowy kod na dół), a następnie rozszerzyć metodą LogMessage o odpowiedniego case….. dużo roboty. Może lepiej będzie zrobić coś takiego:

   1:  public class BetterLogger
   2:  {
   3:      ILogger logger;
   4:      public BetterLogger(ILogger aLogger)
   5:      {
   6:          this.logger = aLogger;
   7:      }
   8:      
   9:      public void LogMessage( string aMessage )
  10:      {
  11:          logger.LogMessage(aMessage);
  12:      }
  13:  }
  14:   
  15:  public interface ILogger
  16:  {
  17:      void LogMessage(string aMessage);
  18:  }
  19:   
  20:  public class ConLogger: ILogger
  21:  {
  22:      void ILogger.LogMessage(string aMessage)
  23:      {
  24:          // log to console
  25:   
  26:      }
  27:  }
  28:   
  29:  public class NetLogger : ILogger
  30:  {
  31:      public void LogMessage(string aMessage)
  32:      {
  33:          // log to network
  34:      }
  35:  }
  36:   
  37:  public class DevNullLogger : ILogger
  38:  {
  39:      public void LogMessage(string aMessage)
  40:      {
  41:          // log to network
  42:      }
  43:  }

Ojej więcej kodu. Tak tak, ale teraz obczaj to (miły czytelniku), po dodaniu nowego logera (np. FileLoger) implementuje się metodą LogMessage, a następnie przekazują taką klasę do BetterLogger, której nie trzeba już modyfikować. Wszystko działa tak jak poprzednio, a jest nowa funkcjonalność. Klasa jest otwarta na rozszerzenia, ale zamknięta na zmiany – voila.

Liskov Substitution Principle wszyscy strasznie komplikują opis. Mam proste wytłumaczenie tej zasady (proste bo po co komplikować, albo proste bo nie zrozumiałem zasady), jeśli dziedziczysz lub implementujesz pewien interfejs, wszystkie klasy pochodne powinny zachowywać się w podobny (logicznie podobny) sposób. Tak aby obiekt wykorzystujący referencję (wskaźnik) do klasy bazowej, mógł spokojnie wykorzystać funkcjonalność klas bardziej pochodnych i nie zostać zaskoczonym przez nietypowym zachowaniem (patrz kod poniżej). Dodatkowym założeniem jest, że parametry akceptowane przez klasy pochodne mogą być mniej restrykcyjne niż w klasie bazowej (może obsłużyć więcej przypadków), natomiast wartości zwracana mogą być takie same lub mniejsze (nie można zwrócić czegoś, co nie jest określone przez klasa bazowa). Dygresja: Przykład z prostokątem i kwadratem dla mnie ma sens i jest poprawny, nie wiem dlaczego jest uznawany za błędną implementacje, jeśli ktoś potrafi to wytłumaczyć proszę o kontakt.

   1:  public interface BadISettings
   2:  {
   3:      void Save();
   4:      void Load();
   5:  }
   6:   
   7:  public class BadUserSettings : BadISettings
   8:  {
   9:      public void Save()
  10:      {
  11:          // save user settings
  12:      }
  13:   
  14:      public void Load()
  15:      {
  16:          // load user settings
  17:      }
  18:  }
  19:   
  20:  public class BadApplicationSettings : BadISettings
  21:  {
  22:      public void Save()
  23:      {
  24:          // throw InvalidOperationException
  25:          // cannot overwrite application settings
  26:      }
  27:   
  28:      public void Load()
  29:      {
  30:          // load application settings
  31:      }
  32:  }

Co jest nie tak? Otóż ustawienia aplikacja nie chętnie się zapisują. Co wpływa na możliwość ich wykorzystania przez interfejs BadISettings. Interfejs nie definiuje nigdzie, że będzie rzucał wyjątkiem, poza tym nie definiuje on metody tylko po to żeby jej nie wspierać. Jednym z rozwiązań jest coś takiego:

   1:  public interface BetterIReadSettings
   2:  {
   3:      void Read();
   4:  }
   5:   
   6:  public interface BetterIWritableSettings
   7:  {
   8:      void Save();
   9:  }
  10:   
  11:  public class BetterUserSettings : BetterIReadSettings, BetterIWritableSettings
  12:  {
  13:      void Read()
  14:      {
  15:          // read settings
  16:      }
  17:   
  18:      void Save()
  19:      {
  20:          // save settings
  21:      }
  22:  }
  23:   
  24:  public class BetterApplicationSettings : BetterIReadSettings
  25:  {
  26:      public void Read()
  27:      {
  28:          // load application settings
  29:      }
  30:  }

//Pozdrawiam czułe oko Jacka

Teraz można spokojnie wykorzystać wspólny interfejs BetterIWritableSettings bez obaw że coś się wywali lub wyleci w powietrze. A BetterIReadableSettings z jedną metodą? A czy jest w tym coś złego? Zerknij na SRP.

Interface Segregation Principle – różnie o tym piszą, akurat nie ma informacji w rodzimym języku. Klienci naszej klasy nie powinni mieć dostępu do elementów z których nie korzystają. Będzie lepiej jeśli nawet nie będzie o nich wiedzieć. Klient powinien dostać tylko tyle ile potrzebuje, nic więcej. Jak zwykle abstrakcja i interfejsy są jor friend.

   1:  public class BadPerson
   2:  {
   3:      public string FirstName;
   4:      public string LastName;
   5:      public string EmailAddress;
   6:      public string PhoneNumber;
   7:      public string Address;
   8:  }
   9:   
  10:  public class BadEmailer
  11:  {
  12:      public void SendEmail( BadPerson aBadPerson )
  13:      {
  14:          // send email using just email address
  15:      }
  16:  }
  17:   
  18:  public class PhoneCaller
  19:  {
  20:      public void PhoneCall( BadPerson aBadPerson )
  21:      {
  22:          // make phone call using just phone number
  23:      }
  24:  }
  25:   
  26:  public class LetterSender
  27:  {
  28:      public void SendLetter (BadPerson aBadPerson )
  29:      {
  30:          // send letter using firstname, lastname and an address
  31:      }
  32:  }

Tutaj widać że klasa BadPerson agreguje wszystko co dotyczy jakiegoś człowieczka, co dał się wbić w internet za kubek kawy ;). Następnie wszystkie te informacje są przekazywane do innych obiektów, który wykorzystują tylko część z tych danych. Kto wie co one tak naprawdę robią z resztą z nich? Czy nie lepiej zamienić to na coś takiego:

   1:  public interface BetterIEmailable
   2:  {
   3:      string EmailAddress{get;set;}
   4:  }
   5:   
   6:  public interface BetterIPhoneable
   7:  {
   8:      string PhoneNumber{get;set;}
   9:  }
  10:   
  11:  public interface BetterILetterable
  12:  {
  13:      string FirstName{get;set;}
  14:      string LastName{get;set;}
  15:      string Address{get;set;}
  16:  }
  17:   
  18:  public class BetterPerson : BetterIEmailable, BetterILetterable, BetterIPhoneable
  19:  {
  20:      public string EmailAddress { get; set; }
  21:      public string PhoneNumber { get; set; }
  22:      public string FirstName { get; set; }
  23:      public string LastName { get; set; }
  24:      public string Address { get; set; }
  25:  }
  26:   
  27:  public class BetterEmailer
  28:  {
  29:      public void SendEmail(BetterIEmailable aBetterPerson)
  30:      {
  31:          // send email using just email address
  32:      }
  33:  }
  34:   
  35:  public class BetterSender
  36:  {
  37:      public void SendLetter(BetterILetterable aBetterPerson)
  38:      {
  39:          // send letter using firstname, lastname and an address
  40:      }
  41:  }
  42:   
  43:  public class BetterCaller
  44:  {
  45:      public void PhoneCall(BetterIPhoneable aBetterPerson)
  46:      {
  47:          // make phone call using just phone number and first name
  48:      }
  49:  }

Teraz każdy dostaje tylko to czego potrzebuje. Nie zaistnieje problem, w którym klasa wysyłająca pocztę elektroniczną zacznie zapisywać dane teleadresowe, podczas gdy miała wysłać tylko email. Wiecie rozumiecie.

Dependency Inversion Principle – mówi o tym, aby powiązania pomiędzy klasami były luźne. Klasy nie powinny zależeć od właściwej implementacji, a działać na wcześniej zdefiniowanym interfejsie. Dzięki temu prościej jest rozszerzać funkcjonalność lub całkowicie ją zmieniać, oczywiście w ramach interfejsu i zasady LSP.

   1:  public class BadButton
   2:  {
   3:      public BadButton( BadLamp aLamp)
   4:      {
   5:          Lamp = aLamp;
   6:      }
   7:   
   8:      public void Click()
   9:      {
  10:          this.Lamp.Switch();
  11:      }
  12:   
  13:      public BadLamp Lamp { get; private set; }
  14:  }
  15:   
  16:  public class BadLamp
  17:  {
  18:      readonly BadButton button;
  19:      public BadLamp()
  20:      {
  21:          this.button = new BadButton(this);
  22:      }
  23:   
  24:      public void Switch()
  25:      {
  26:          // switch the light on/off
  27:      }
  28:  }

Przykład powyżej to wyjątkowo mocne powiązanie klasy BadButton oraz BadLamp. Nie będą one współpracować z żadną inną klasą niż te zdefiniowane, chyba że dziedziczące po BadLamp, natomiast już na pewno nie zaistnieje możliwość zmiany guzika na inny. Oczywiście istnieje lepsze rozwiązanie:

   1:  public interface BetterIButtonClient
   2:  {
   3:      void Switch();
   4:  }
   5:   
   6:  public interface BetterIButton
   7:  {
   8:      void Click();
   9:  }
  10:   
  11:  public class BetterButton : BetterIButton
  12:  {
  13:      BetterIButtonClient client;
  14:      public BetterButton(BetterIButtonClient aClient)
  15:      {
  16:          client = aClient;
  17:      }
  18:   
  19:      public void Click()
  20:      {
  21:          this.client.Switch();
  22:      }
  23:  }
  24:   
  25:  public class FancyBetterButton : BetterIButton
  26:  {
  27:      BetterIButtonClient client;
  28:      public FancyBetterButton(BetterIButtonClient aClient)
  29:      {
  30:          client = aClient;
  31:      }
  32:   
  33:      public void Click()
  34:      {
  35:          this.client.Switch();
  36:      }
  37:  }
  38:   
  39:  public class BetterLamp : BetterIButtonClient
  40:  {
  41:      BetterIButton betterButton;
  42:      public BetterLamp(BetterIButton aBetterButton)
  43:      {
  44:          betterButton = aBetterButton;
  45:      }
  46:   
  47:      public void Switch()
  48:      {
  49:          // switch the light on/off
  50:      }
  51:  }

Co dostajemy? Otóż button już nie musi działać tylko i wyłącznie z lampą, zadziała także z pralką, lodówką i innym AGD o ile sprzęt będzie obsługiwać interfejs BetterIButtonClient i prostą metodą Switch. A dalej widać, że lampa także może być już bardziej fikuśna, nie zależy ona już tylko od jednego rodzaju Buttona. W dodatku to nie ona jest odpowiedzialna za decydowanie z którego skorzysta (od siebie dodałem idee: DI). Dzięki temu fabryka lamp może w prosty sposób przerzucić się na nowszy model z wykorzystaniem FancyBetterButton-ów. Widać tu wpływy idei OCP. Taki kod prościej jest wykorzytać w innym projektach, ponieważ nie jest on mocno powiązany z bezpośrednią implementacją klasy guzików czy innych obiektów, zdefiniowanych z projekcie.

Podczas pisania kodu należy przede wszystkim korzystać z G.Ł.O.W.Y. to jest najważniejsza zasada! Dopiero później warto podpierać się różnymi ideami i regułami, które proponują inni. Zasady są po to aby je łamać, a nic tak nie uczy człowieka jak popełniane błędy. Zachęcam do łamania zasad SOLID, a potem sprawdzenia czy miało to sens i podzielenia się tym doświadczeniem ze mną i innymi 😉

Podczas wpisu sporą część (bardzo dużą część wiedzy) oparłem na
[1] http://www.blackwasp.co.uk/SOLIDPrinciples.aspx
[2] www.wikipedia.org polska i angielska wersja
[3] http://www.objectmentor.com Tutaj trzeba się porządnie naszukać aby znaleźć materiały
Szczególnie polecam punkt pierwszy.

Uwagi jak zawsze chętnie przyjmę.

ps. Jutro do pracy
ps2. C# jest czytelny, nawet dla nie programujących w C#.
ps3. Link do źródełek ze wszystkimi zasadami  https://www.sugarsync.com/pf/D6056980_3239101_969599