Domain Driven Design, czyli programowanie przez modelowanie

Podczas 58. spotkania Krakowskiej Grupy Developerów .NET miałem przyjemność poprowadzić prezentację pt. “Domain Driven Design, czyli programowanie przez modelowanie”. Ponieważ zakładam, że nie wszyscy jesteście z Krakowa i uczestniczyliście w spotkaniu, pozwolę sobie opisać o czym mówiłem. Prezentację możecie obejrzeć lub ściągnąć (format pptx) stąd (via SlideShare), a przykłady w kodzie stąd.

Rozwiązywanie problemów

Na początku chciałbym skontrastować “klasyczne” podejście do rozwiązywania problemów z podejściem wykorzystującym model. Można to zilustrować następująco:

Podejście klasyczne

Podejście z wykorzystaniem modelu

W wersji klasycznej programista implementuje bezpośrednio wymagania, tak jak zostały one spisane przez analityka (zwykle w formie 10/100/miliona linii “The system shall…”). Dobrze, jeśli w ten proces wpleciona jest pętla sprzężenia zwrotnego, która sprawia, że implementacja pierwszych wymagań ma wpływ na dalszy przebieg procesu analizy.

W podejściu “Domain-Driven” tak naprawdę nie istnieją role analityka i programisty. Istnieje tylko jedna rola — modelarz. Modelarz zajmuje się budową modelu rzeczywistości użytecznego w rozwiązaniu konkretnego problemu. Współpracuje on bardzo blisko z ekspertami dziedzinowymi, przetwarzając posiadaną przez nich wiedzę na spójny, niesprzeczny, a co najważniejsze, wykonywalny, model. Dopiero na bazie tego modelu tworzone jest ostateczne rozwiązanie. Możemy odnaleźć dwie pętle sprzężenia zwrotnego: pierwsza dotyczy samego modelu (im więcej modelarz stworzył, tym lepiej rozumie problem), a druga modelu w kontekście aplikacji (im więcej wiemy, co ma robić aplikacja, tym lepiej wiemy, które aspekty należy dokładniej modelować).

Cechy modelu

Skoro już wiemy, do czego przydaje się model, warto się zastanowić jakie cechy powinien posiadać dobry model. Oto moja osobista lista w kategorii “model powinien być”:

Użyteczny

Nie ma sensu budowa idealnego modelu rzeczywistości, ponieważ byłby on tak samo skomplikowany jak ta rzeczywistość. Z punktu widzenia złożoności obliczeniowej jest to niewykonalne. Znane powiedzenie mówi, że wszystkie modele są błędne, ale niektóre są użyteczne. Model, który budujemy na potrzeby naszego systemu powinien odzwierciedlać tylko ten fragment rzeczywistości, w ramach którego działa ten system i tylko pod kątem czynności bezpośrednio związanych z jego funkcjonowaniem.

Hermetyczny

Model jest kodyfikacją pewnych reguł rządzących modelowanym wycinkiem rzeczywistości. Reguły te nie powinny wyciekać z modelu, co oznacza, że jego klienci (użytkownicy) nie powinni musieć o nich wiedzieć. Przykładem może być reguła, że w modelu programu HR pracownik może mieć w danym momencie co najwyżej jednego pracodawcę. To model, a nie klient, powinien zadbać o to, aby przy zmianie pracy pracownik został odłączony od poprzedniego pracodawcy.

Podatny na modyfikacje

Jest to sprzeczne z zasadą Open-Closed Principle (open for extensions, closed for modifications), jednak w przypadku modeli sprawdza się bardzo dobrze. Aby model był nadążał za szybko zmieniającą się rzeczywistością, sam także musi się zmieniać. Nie chcemy przecież, aby skostniały model aplikacji był dla organizacji jej używającej hamulcem rozwoju, prawda?

Zrozumiały

O sukcesie modelu i całego projektu decyduje, czy wszyscy jego interesariusze rozumieją przestrzeń problemu. Model powinien stanowić podstawę porozumienia i bazę dla wszędobylskiego języka (ubiquitous language) służącego do komunikacji między wykonawcami systemu (ludźmi technicznymi), a ekspertami dziedzinowymi. W dzisiejszych czasach języki programowania takie, jak C#, są na tyle elastyczne, że pisanie kodu (modelu) tak, aby był zrozumiały dla nieprogramistów nie stanowi już większego problemu.

Wydajnie implementowalny

Mimo, iż model nie powinien być zależny od jakiejkolwiek konkretnej technologii, powinien jednak być (w jakiejś technologii) wydajnie implementowalny. Nie chcemy być zależni od NHibernate czy innego ORM-a, jednak akceptowanie faktu, że model będzie przechowywany trwale w jakiejś relacyjnej bazie danych za pośrednictwem jakiegoś ORM-a pozwala zoptymalizować wiele kwestii. Nie ma sensu udawać niezależności od wszystkich aspektów technologii.

VN:F [1.8.7_1070]
Rating: 5.0/5 (1 vote cast)

Jak zwątpiłem w transakcje

Transakcje to fajna sprawa. Polubiłem je od pierwszego użycia. Zostałem oczarowany przez ich magiczną właściwość — zwalniają z myślenia o spójności danych. Cool, przecież nie lubię myśleć. Jeszcze bardziej byłem oczarowany, gdy odkryłem transakcje rozproszone. To dopiero jazda. Mogę coś “zapdejtować” na tej bazie, na tej drugiej bazie i jeszcze wrzucić komunikat do kolejki MSMQ i wszystko wykona się transakcyjnie — w całości lub wcale.

Od dłuższego czasu zaczynam jednak wątpić w transakcje, szczególnie te rozproszone. Zbyt często ja sam lub ktoś ze znajomych wpada przez nie w pułapkę bez wyjścia. Zamiast używać najodpowiedniejszej technologii, musimy wtedy wziąć taką, która współdziała z naszym sposobem zarządzania transakcjami.

Najczęstszym problemem, na który natrafiałem jest odbieranie i wysyłanie komunikatów do kolejek z jednoczesnym zapisem danych w bazie. Pierwsza rzecz, która przychodzi w takim wypadku do głowy, to transakcja, która obejmuje zarówno infrastrukturę kolejkową, jak i RDBMS. Pierwsza nie oznacza, niestety, najlepsza.

Nieodpowiednia technologia

Wymaganie transakcyjności w komunikacji z RDBMS i kolejkami pchnęło mnie kilka lat temu do wykorzystania SQL Server Service Broker jako kolejki komunikatów. Unikalną cechą Service Broker’a jest fakt, że żyjąc wewnątrz silnika bazodanowego, jest w naturalny sposób zintegrowany z mechanizmami transakcyjnymi SQL Server. Nie ma potrzeby stosowania (wolnych) transakcji rozproszonych, aby wysłać komunikat i zrobić przysłowiowy “apdejt”.

Niestety poza tą jedną zaletą, Service Broker ma bardzo wiele wad, z których największą jest brak dobrego gotowego API w C#, o modelu usługowym (np. WCF) nie wspominając. Jest bardzo trudny w użyciu i utrzymywaniu. Definitywnie był to zły wybór technologiczny, podyktowany jedynie chęcią zastosowania mechanizmu transakcji.

Niezgodność API

W każdym nietrywialnym systemie wykorzystuje się wiele zewnętrznych bibliotek. Bardzo rzadko pochodzą one od jednego dostawcy. Zdecydowanie częściej jest to mieszanka rozwiązań open source oraz COTS. Problem pojawia się, gdy dwie z bibliotek mają współpracować w ramach jednej transakcji. Weźmy jako przykład NHibernate i NServiceBus. Ten pierwszy posiada własną abstrakcję reprezentującą transakcje, podczas gdy pod spodem korzysta z transakcji ADO.NET. Ten drugi wykorzystuje transakcje System.Transactions do dostępu do MSMQ.

W przypadku użycia obu technologii w jednej transakcji, pojawia się problem, jak sprawić, aby każda z bibliotek mogła wykorzystywać swoje API odwołując się do tej samej fizycznej transakcji. W wypadku wspomnianej pary, jedynym rozwiązaniem jest pozwolić NHibernate używać zewnętrznych transakcji System.Transactions. Jest to jednak zamiana jednego problemu na inny. Do niedawna bowiem pojawiał się w NServiceBus wyciek pamięci, ponieważ zachowanie NHibernate w wypadku współpracy z System.Transactions jest bardzo słabo udokumentowane i łatwo o błąd wynikający z niezrozumienia.

Błedne implementacje transakcyjności

Jonathan Oliver opisał swoje testy kompatybilności różnych silników bazodanowych z transakcjami rozproszonymi na swoim blogu. Wnioski nie są optymistyczne: jedynie sterowniki do SQL Server i Oracle w pełni i bez problemów je obsługują.

Z drugiej strony nie dziwie się, że mniej płatne lub darmowe rozwiązania nie wspierają rozproszonych transakcji. Zdecydowana większość systemów radzi sobie bez nich, więc zysk (w sensie pieniędzy z licencji lub satysfakcji użytkowników) z implementacji wsparcia dla nich byłby znikomy.

Proste rozwiązanie

Rozwiązanie jest oczywiście proste. Wymaga jednak nieco innego podejścia do projektowania komunikacji. Wystarczy zadbać o to, aby każdy komunikat był

  • albo idempotentny (wielokrotne przetworzenie takiego komunikatu daje taki sam efekt, jak przetworzenie jednokrotne),
  • albo jednoznacznie identyfikowalny (unikalne ID).

Drugi przypadek można sprowadzić do pierwszego dodając rejestr przetworzonych komunikatów zawierający ich unikalne ID i przed obsłużeniem komunikatu sprawdzać, czy aby nie został przetworzony wcześniej.

Po spełnieniu któregoś z powyższych warunków zyskujemy możliwość rozłącznego zarządzania transakcją związaną z obieraniem komunikatu (MSMQ, ServiceBroker) oraz transakcją bazodanową. Ta pierwsza powinna być zatwierdzana dopiero po zatwierdzeniu tej drugiej. Powoduje to, że mamy pewność, iż każdy komunikat zostanie przetworzony co najmniej raz. Z drugiej strony idempotentność gwarantuje nam, że skutki wielokrotnego przetworzenia będą takie, jak jednokrotnego. Ostatecznie więc uzyskujemy semantykę dokładnie raz — taką samą jak przy zastosowaniu transakcji rozproszonych.

Praktyka

Dokładnie taki mechanizm zastosowałem w swoim ostatnim systemie. Pozwolił mi on na użycie klienta Service Broker (użycie tej kolejki było narzucone z góry) w połączeniu z NHibernate bez konieczności integracji obu technologii. Ponieważ transakcje były rozdzielone, klient kolejek nie musiał wiedzieć nic o dostępie do danych i vice versa. Prawdopodobnie zaoszczędziło mi to kilka dni pracy przy implementacji, testach i poprawianiu bugów w warstwie integracyjnej. Nauczyłem się także, że najlepszą strategią integracji technologii jest unikanie integracji technologii, kiedy to tylko możliwe.

VN:F [1.8.7_1070]
Rating: 5.0/5 (1 vote cast)

Fractal structures

For the last few days I was mainly pair-programming (pair-refactoring to be honest) some Silverlight UI code. I must confess I really rarely write UI code these days. Actually, I don’t remember the last time I’ve written a ASP.NET control from scratch. I did write some UI for DDDSample.Net but it was not production code after all.

MVVM

When I bumped into this Model-View-View Model (MVVM) stuff, I was a little confused. Then came the moment of enlightenment. I realized I saw all of this in totally different context – in loosely coupled system architecture. Look at this:

The blue elements on the right is MVVM stuff. It contains of three classic MVVM components and an additional one — an Event Aggregator. It is used for communication between MVVM triads. Application is organized in such a way, that multiple MVVM triads form vertical slices of functionality. These slices are loosely coupled so that they can be changed independently. Actually, when you value loose coupling much, you should avoid any code sharing between slices.

The orange elements are the building blocks of DDD-based distributed application. It receives commands via Services, then it processes them in a Domain Model which is backed by a Data Store. Finally, state changes are communicated to the outside world via a Message Bus.

How do slices communicate?

They use events and (rarely) messages. The name I chose for the diagram, the Event Aggregator, is a little bit misleading, because ViewModels should be able to both publish an event (broadcast to all interested receivers) and send direct message to a concrete receiver. The only shared thing between slices is the definition of messages (sounds like SOA, isn’t it?).

Again, how does it relate to system architecture?

I think the picture says it all. View is an analog of Service interface. View Model is an analog of Domain Model. In fact, one can say that View Model is the Domain Model in the context of user interface as it models the domain of particular UI case. The model is the place where all the information lives, a Data Store if you prefer. Finally, the Event Aggregator (with mentioned earlier unicast capabilities) is an analog of a Message Bus (such as NServiceBus).

How does it work?

Let’s look at the example. There are two slices, A and B. For the purpose of the example, let A be master view (a data grid and search) and B — a details view.

User selects the row on a grid. In response to that View A to generate a command and passes it (by means of data binding) to the View Model A. The View Model validates command, processes it (updates all required properties) and, as a result, it publishes an event “SelectionChanged”.

The Event Aggregator receives the event and searches for its subscribes. It finds View Model B so it forwards the event to it. During processing of event, View Model B requests all the data it needs from the Model B and updates its own properties using retrieved values. View B is automatically updates as it is data-bound to its View Model.

Summary

As you can see, good architected applications tend to have fractal structure. Similar forms can be discovered on various levels of detail. I value such architectures very high. The coherence makes them simple and elegant.

One more thing in case you didn’t noticed. There is a trend recently to focus architecture discussion not on what’s horizontal, but on what’s vertical. As we are (hopefully and at last) moving away from waterfallish processes, we start building applications slice by slice, not layer by layer. These slices do have to have some architecture but not as much design up front. The layers are much less important. I don’t even care if all the slices have the same layers — it can be impractical. What I want to have is the conscious process of choosing the layers of each slice so it can be implemented in the simplest possible way.

VN:F [1.8.7_1070]
Rating: 0.0/5 (0 votes cast)

Tunel SOAP w SOAP i WCF

Podczas wstępnego projektowania systemu, nad którym teraz pracuje, natknęliśmy się na dosyć interesujący problem. Polega on na tym, iż docelowe środowisko wdrożeniowe nie pozwala na komunikację między serwerem WWW, a serwerem aplikacyjnym. Komunikacja odwrotna jest możliwa. Sytuację tę przedstawia poniższy diagram.

Te raczej mocne obostrzenia podyktowane są (podobno) polityką bezpieczeństwa. Niestety są one zabójcze dla naszego systemu, ponieważ ma on służyć do monitorowania i zarządzania procesami uruchomionymi na serwerze aplikacyjnym. Jak więc monitorować i sterować czymś, z czym nie można się połączyć?

Tunel

Odpowiedzią jest tunel SOAP w SOAP wykorzystujący fakt, że komunikacja odwrotna (serwer aplikacyjny wysyła requesty do serwera WWW) jest, jak najbardziej, możliwa. Jak działa taki tunel? Tunel składa się z dwóch końców. Koniec kliencki umiejscowiony jest na maszynie WWW i przyjmuje żądania, które następnie są kolejkowane w pamięci. Serwerowy koniec tunelu jest aplikacją uruchomioną na serwerze aplikacyjnym, która okresowo (raz na kilka sekund) odpytuje (wywołując operację Fetch) koniec kliencki, czy są jakieś zakolejkowane żądania. Jeśli tak, pierwsze żądanie z kolejki jest zwracane w odpowiedzi na wywołanie Fetch. Przesłane żądanie jest następnie forwardowane do odpowiedniego procesu serwerowego. Prześledźmy to na przykładzie.

Tunel w działaniu

  1. Serwer WWW wysyła żądanie. Adresatem (nagłówek “To” w SOAP) jest serwer aplikacyjny, jednak żądanie jest fizycznie (na poziomie TCP) wysyłane na adres klienckiego końca tunelu.
  2. Generowany jest unikalny identyfikator żądania. Żądanie, wzbogacone o dodatkowy nagłówek zawierający wygenerowany identyfikator, jest kolejkowane w oczekiwaniu na wywołanie Fetch.
  3. Serwerowy koniec tunelu wywołuje operację Fetch celem pobrania pierwszego oczekującego żądania. Operacja ta zdefiniowana jest w ten sposób, że request jest pusty, a w odpowiedzi przesyłany jest dowolny komunikat SOAP.
  4. W odpowiedzi na Fetch żądanie (przesyłane jako odpowiedź) trafia do serwerowego końca tunelu.
  5. Serwerowy koniec tunelu forwarduje je (bez jakiejkolwiek ingerencji bądź analizy) do nasłuchującego lokalnie procesu serwera.
  6. Proces serwera zwraca odpowiedź do serwerowego końca tunelu.
  7. Odpowiedź jest forwardowana do końca klienckiego poprzez wywołanie operacji Reply, która pozwala na przesłanie dowolnego komunikatu SOAP (w odpowiedzi na Reply przesyłany jest pusty komunikat). Przed wysłanie do komunikatu dodawany jest nagłówek zawierający identyfikator, który zawierało żądanie.
  8. Kliencki koniec tunelu, na podstawie przekazanego identyfikator kojarzy przesłaną za pomocą Reply odpowiedź z oczekującym żądaniem, a następnie odpowiada klientowi przesyłając mu komunikat otrzymany od serwerowego końca tunelu.

Szczegóły implementacyjne

Jest kilka kwestii, o których trzeba pamiętać budując rozwiązanie tego typu. Pierwszą z nich jest konieczność użycia WS-Addressing (czyli de facto WSHttpBinding lub pochodnego). W przeciwnym wypadku informacje o adresacie nie będą przeźroczyście transportowane od klienta do serwera. Nie stanowi to problemu w przypadku prostych zastosowań (no security), jednak uniemożliwia np. stworzenie pewnej sesji (reliable session).

Kolejną kwestią techniczną jest konieczność każdorazowego kopiowania komunikatów WCF przed ich forwardowaniem. Specyfika WCF jest taka, że obiekt Message reprezentujący otrzymany komunikat SOAP może być odczytany co najwyżej raz. Jeśli potrzebujemy więcej razy — musimy utworzyć kopię.

Ustawienie właściwości Action i ReplyAction w atrybucie OperationContract na “*” powoduje, że serwis będzie operował na dowolnych komunikatach SOAP. Jest tylko jedna pułapka. Tylko jedna operacja w kontrakcie może mieć ustawione “*”, ponieważ inaczej nie byłoby wiadomo do której operacji skierować komunikat.

Wyjaśnienie

Nie zachęcam was, broń Boże, to stosowania takich zabawek dla samej idei. Jest to bardzo szczególne rozwiązanie bardzo szczególnego problemu, który zapewne nie występuje zbyt często (oby). Jest to właściwie obejście procedur bezpieczeństwa i, jako takie, powinno być stosowane z rozwagą. W końcu ktoś te procedury w jakimś celu ustalał, prawda?

VN:F [1.8.7_1070]
Rating: 0.0/5 (0 votes cast)

Why do I find Event Sourcing interesting?

What makes Event Sourcing so interesting? I it is not the free, proven, audit trial, nor the possibility of reincarnating object in any particular state it had in the history. It is also not the great performance of add-only event store. It is testability.

Why Event Sourcing makes things testable? By splitting up logic responsible for two distinct things: calculating new state of the object and applying this state change. Normally, these two operations are mixed in one business logic method like this:

public class Customer
{
    public void PlaceOrder(Product product, int quantity)
    {
        CheckProductAvailability();
        CheckCustomerCreditLimit();
        CheckSomethingElse();
        decimal price = CalculateTotalPrice(product, quantity);
        Order o = new Order(product, quantity, price);
        _orders.Add(o);
    }
    //...
}

If you read this carefully, you will see that first 4 lines contain the business logic of state change calculation. The last 2 lines, on the other hand, contain no logic at all. Now, what’s the problem with this code’s testability?

The problem becomes visible, when I want to test how PlaceOrder behaves when adding fifth order. I have to call PlaceOrder four times to prepare state of the object. That is wrong, because my test code invokes a lot of logic during preparation phase. This means that a lot of things can go wrong and nobody likes tests which fail before the actual test code gets executed.

Let’s see if PlaceOrder can be corrected using SOLID principles. Does it have exactly one reason to change? It seems like it does. When order placement logic changes, the method changes, right? Let’s dig deeper. What about the last two lines? Do they change when the rest of method changes? Well, I don’t think so. Looks like they indeed do represent a different concern. So let’s refactor them to a separate method.

public class Customer
{
    public void PlaceOrder(Product product, int quantity)
    {
        CheckProductAvailability();
        CheckCustomerCreditLimit();
        CheckSomethingElse();
        decimal price = CalculateTotalPrice(product, quantity);
        ApplyPlaceOrder(product, quantity, price);
    }
    private void ApplyPlaceOrder(Product product, int quantity, decimal price)
    {
        Order o = new Order(product, quantity, price);
        _orders.Add(o);
    }
    //...
}

Now it looks better. At least from the have one reason to change principle. But I don’t see this improved testability at all. That is because although we split the responsibilities to separate methods, we left the methods tightly coupled to one another. Lets see what we can do about this.

public class Customer
{
    public void PlaceOrder(Product product, int quantity)
    {
        CheckProductAvailability();
        CheckCustomerCreditLimit();
        CheckSomethingElse();
        decimal price = CalculateTotalPrice(product, quantity);
        Apply(new OrderPlacedEvent(product, quantity, price));
    }
    private void OnOrderPlaced(OrderPlacedEvent @event)
    {
        Order o = new Order(@event.Product, @event.Quantity, @event.Price);
        _orders.Add(o);
    }
    //...
}

We introduced a concept of event as a way of encapsulating the state change we are about to apply. Now we can represent state changes by a series of events. The other thing we did was adding the Apply method. What is it? The Apply method is the API of Event Sourcing. It is a way of communicating hey, I, the aggregate root, want to apply the state change represented by this event! Apply invokes a method to apply state change. It can do this in a variety of ways: based on convention (like On… convention in this sample), an attribute or any other. The bottom line is, Apply applies the state change and it can be called externally, for example during tested state preparation.

[TestFixture]
public class CustomerTests
{
    [Test]
    public void When_placing_fifth_order()
    {
        Product product = new Product();
        //Arrange
        Customer c = new Customer();
        c.Apply(new OrderPlacedEvent(product, 1, 10));
        c.Apply(new OrderPlacedEvent(product, 1, 10));
        c.Apply(new OrderPlacedEvent(product, 1, 10));
        c.Apply(new OrderPlacedEvent(product, 1, 10));

        //Act
        c.PlaceOrder(product, 1);

        //Assert...
    }
}

You can see that we invoke no business logic during arrange phase. We only apply predefined, known to be right, state changes. The chance something bad will happen during this phase is minimal, as we certainly tested the OnOrderPlaced method in a different test, didn’t we?

This is what I call well tested code. PlaceOrder method is always tested in isolation, no matter if it is first or fifth order. One cannot overestimate the benefits of a testable codebase. If you happen to play (or work) with Domain-Driven Design, Event Sourcing can be beneficial for you. I see it as the missing piece of puzzle — the model is finally fully testable.

VN:F [1.8.7_1070]
Rating: 0.0/5 (0 votes cast)