Chciałbym nawiązać tą notką do mojej pierwszej notki z tego bloga. Była to analiza dostępnych technik odwracania zależności w kontekście aplikacji o tzw. architekturze cebulowej. Od tego czasu moje poglądy na ten temat nieco się zmieniły, stąd nagląca potrzeba aktualizacji.

Dlaczego w ogóle zajmuję się tym tematem? Wujek Bob, w jednej ze swoich ostatnich notek, poruszył problem uzależnienia od technicznych aspektów związanych z odwróceniem zależności. Jako anty-przykład podał rozwiązanie, w którym kod aplikacji bezpośrednio odwołuje się do framework-u DI w celu pobrania zależnych obiektów. Jego post zwraca uwagę na problem: sukces wprowadzenia inversion of control do mainstream-u programowania sprawił, że wielu zaczęło tego wzorca nadużywać lub używać niezgodnie z intencjami autorów.

Tyle tytułem wstępu. Co dalej? Chciałbym przedstawić Wam swoje podejście do inversion of control w kontekście aplikacji zbudowanych w oparciu o model domeny. W takiej aplikacji możemy wyróżnić dwa główne powody, dla których chcielibyśmy odwrócić sterowanie:

  • Nie chcemy uzależniać się od konkretnej, mimo, iż i tak prawdopodobnie będzie tylko jedna implementacja, np. ponieważ chcemy zachować możliwość testowania jednostkowego z wykorzystaniem mock-ów.
  • Architektura rozwiązania zakłada w pewnym miejscu odwrócenie zależności, które zrealizowane jest poprzez wstrzyknięcie do zależnego komponentu interfejsu reprezentującego jego zależność

Pierwszy powód jest natury całkowicie technicznej. W mojej pracy najczęściej spotykam się z nim przy okazji tzw. repozytoriów, czyli obiektów, które stanowią abstrakcję magazynu przechowującego obiekty modelu domeny. Szansa, że w ciągu pierwszych kilku lat rozwoju produktu pojawi się potrzeba wymiany mechanizmu przechowywania tych danych jest, bądźmy realistami, naprawdę nikła. Niemniej jednak wielu (w tym ja) pracowicie definiuje interfejsy repozytoriów i umieszcza je w assembly modelu domeny. Dlaczego? Szczerze mówiąc nie znajduję dla siebie wytłumaczenia poza faktem, iż pozbycie się tych interfejsów dodałoby do moich klas komend (lub ogólniej, warstwy usługowej aplikacji) zależność od NHibernate. Pewnie jednak następnym razem spróbuje bez repozytoriów… A co z wspomnianym testowaniem jednostkowym? Moje testy i tak mają albo charakter jednostkowy i testują model w pamięci, ale integracyjny i wtedy wykorzystują bazę SQLite.

Drugi powód jest znacznie ciekawszy. Występuje zwykle na granicy modelu domeny. Różne obiekty modelu, w odpowiedzi na zlecone operacje, potrzebują przekazać pewne dane na zewnątrz. Ponieważ zależność od obiektów modelu do komponentów warstw wyższych byłaby niezgodna z architekturą, należy tę zależność odwrócić. Jak? Oto kilka technik:

Wstrzykiwanie zależności

Technika ta zakłada definiowanie interfejsów obiektów zależnych na poziomie modelu domeny. Poszczególne implementacje są wstrzykiwane do obiektów modelu za pomocą specjalnego frameworka. W tym momencie pojawiają się dwa problemy.

Pierwszy problem ma naturę techniczną. Wstrzykiwanie odbywa się zwykle na etapie tworzenia instancji. Jeśli więc wykorzystujemy gotowe narzędzie O/RM, takie jak NHibernate, musimy się “wpiąć” w jakiś punkt rozszerzenia (mając nadzieje, że takowy istnieje) i wykorzystać nasz ulubiony kontener DI do wstrzyknięcia zależności.

Drugi problem jest poważniejszy, ponieważ jego źródło ma naturę logiczną. Reprezentacja zależności w formie pola lub właściwości psuje czystość modelu. Przykład? Klasa Customer reprezentująca klienta ma metodę SendNotification wysyłającą do klienta e-mail z pogróżkami. W tym celu wykorzystuje komponent IEmailSender. Oto jak mógłby wyglądać ten scenariusz z wykorzystaniem wstrzykiwania przez właściwości:

public interface IEmailSender
{
   void SendEmail(string address);
}

public class Customer
{
   public string Name { get; set;}
   public string EmailAddress { get; set;}
   public IEmailSender EmailSender { get; set; }

   public void SendEmail()
   {
      EmailSender.SendEmail(EmailAddress);
   }
}

Problem polega na tym, że powyższy kod sugeruje, że częścią składową modelu klienta jest mechanizm wysyłania e-maili, co jest oczywiście nieprawdą. Czym powyższy kod różni się od przypadku, w którym komponent obsługi poczty elektronicznej byłby wykorzystany bezpośrednio (new EmailSender())? Z punktu widzenia modelowania, niestety niczym. Cechuje się od jedynie (albo aż) zwiększoną testowalnością.

Double Dispatch

Pominę w tym miejscu wstęp teoretycznym, dotyczący tego czym Double Dispatch jest, i jak się ma do wzorca Visitor. W kontekście DDD jego wykorzystanie polegałoby na tym, że do operacji obiektu modelu przekazujemy, jako argument, obiekt zależności. Tak to wygląda w kodzie:

public class Customer
{
   public string Name { get; set;}
   public string EmailAddress { get; set;}

   public void SendEmailThrough(IEmailSender sender)
   {
      sender.SendEmail(EmailAddress);
   }
}

Co zyskujemy? Wyraźny komunikat, że komponent wysyłający nie jest częścią modelu klienta, ale że ten model potrafi go wykorzystać w celu wysłania listu.

Service Locator

Bardzo podobny efekt można uzyskać stosując wzorzec lokalizatora usług:

public class Customer
{
   public string Name { get; set;}
   public string EmailAddress { get; set;}

   public void SendEmail()
   {
      ServiceLocator.Current.GetInstance<IEmailSender>()
         .SendEmail(EmailAddress);
   }
}

Różnica polega na tym, że to obiekt modelu domeny explicite inicjuje pobranie referencji do zależności. Dlaczego już nie uważam, że to jest dobry pomysł? Ponieważ Double Dispatch pozwala na pisanie bardziej wyrazistego kodu (kontrakt metody od razu specyfikuje jej zależności), a jednocześnie nie jest związany z problemami natury technicznej (jak wstrzykiwanie). Jest więc obiektywnie patrząc lepszy.

Domain Events

Po raz kolejny odwołam się tutaj do świetnej serii artykułów Udiego Dahana dotyczącej zdarzeń domenowych. Zdarzenia są bowiem kolejną techniką wypychania informacji z modelu pozwalającą na odwrócenie zależności. Dlaczego uważam, że zdarzenia są lepsze niż Double Dispatch? Ponieważ promują dobre praktyki związane z modelowaniem. Przyjrzyjmy się naszemu klientowi bardziej całościowo:

public class CheckUnpaidContractsCommand
{
   public IEmailSender EmailSender { get; set; }
   public void Invoke()
   {
      Customer c = //...
      if (c.CheckUnpaidContracts())
      {
         c.SendEmailThrough(EmailSender);
      }
   }
}

public class Customer
{
   public string Name { get; set;}
   public string EmailAddress { get; set;}
   public bool HasUnpaidContracts { get; set; }

   public void SendEmailThrough(IEmailSender sender)
   {
      sender.SendEmail(EmailAddress);
   }

   public bool CheckUnpaidContracts()
   {
      //...
      HasUnpaidContracts = true;
      return true;
   }
}

Komenda sprawdzenia zaległości orkiestruje wywołanie dwóch metod na obiekcie modelu, wyraźnie pogwałcając zasadę tell, don’t ask. Mimo, iż możemy tutaj zauważyć prawidłowe wykorzystanie wzorca Double Dispatch — zależność jest wstrzykiwana w obiekcie warstwy wyższej, a następnie przekazywana do wywołania metody warstwy niższej — kod nie jest koncepcyjnie optymalny. W jaki sposób pomogą nam zdarzenia? Otóż pozwalają one tworzyć modele o postaci:

sprawdź niezapłacone rachunki i, jeśli takowe są, opublikuj zdarzenie “klient ma zaległości”

Sprytne, prawda? I brzmi tak biznesowo, a nie technicznie. A w kodzie wygląda tak:

public class CheckUnpadiContractsCommand
{
   public void Invoke()
   {
      Customer c = //...
      c.CheckUnpaidContracts()
   }
}

public class SendEmailIfCustomerHasUnpaidContracts : IEventHandler<CustomerHasUnpaidContractsEvent>
{
   public IEmailSender EmailSender { get; set; }
   public void Handle(CustomerHasUnpaidContractsEvent @event)
   {
      EmailSender.SendEmail(@event.Subject.EmailAddress);
   }
}

public class Customer
{
   public string Name { get; set;}
   public string EmailAddress { get; set;}
   public bool HasUnpaidContracts { get; set; }

   public void CheckUnpaidContracts()
   {
      //...
      HasUnpaidContracts = true;
      DomainEvents.Publish(new CustomerHasUnpaidContractsEvent(this));
      return true;
   }
}

Infrastruktura zdarzeniowa, którą opublikował Udi (jedna prosta klasa) pozwala na zgrabną rejestrację event handler-ów. Model ma teraz jasno określoną odpowiedzialność — poinformować świat zewnętrzny, że klient ma zaległości. Kod komendy jest zaś prosty, taki jak powinien być od początku. Dodatkowym, niebagatelnym, zyskiem wynikającym z takiego podejścia jest wydzielenie kodu polityki powiadamiania dłużników e-mailem do osobnej klasy implementującej dobrze znany interfejs. Dzięki temu zmiana polityki (na np. powiadamianie SMSem) jest dziecinnie prosta i nie wiąże się z modyfikacją kod modelu domeny.

Istnieje tylko jeden problem związany z tym rozwiązaniem: zdarzenia domenowe nie pozwalają na pobieranie informacji zwrotnej — są jednokierunkowe.

Podsumowanie

Podsumowanie jest krótkie. Jeśli interakcja modelu z otoczeniem jest jednokierunkowa (lub da się sprowadzić do jednokierunkowej!) — zdarzenia. W przeciwnym wypadku — Double Dispatch. Oczywiście jest to ogromne uproszczenie i na pewno są przypadki, w których takie postępowanie nie jest optymalne. Jest ono jednak całkiem niezłą heurystyką, dlatego warto mieć je w pamięci.

VN:F [1.8.7_1070]
Rating: 4.7/5 (7 votes cast)
Wzorce odwracania zależności w aplikacji z modelem domeny4.757