Notki po polsku
Domain Driven Design, czyli programowanie przez modelowanie
Jun 28th
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:
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.
Jak zwątpiłem w transakcje
Jun 17th
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.
Tunel SOAP w SOAP i WCF
Jun 10th
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
- 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.
- Generowany jest unikalny identyfikator żądania. Żądanie, wzbogacone o dodatkowy nagłówek zawierający wygenerowany identyfikator, jest kolejkowane w oczekiwaniu na wywołanie Fetch.
- 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.
- W odpowiedzi na Fetch żądanie (przesyłane jako odpowiedź) trafia do serwerowego końca tunelu.
- Serwerowy koniec tunelu forwarduje je (bez jakiejkolwiek ingerencji bądź analizy) do nasłuchującego lokalnie procesu serwera.
- Proces serwera zwraca odpowiedź do serwerowego końca tunelu.
- 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.
- 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?
Ncqrs ciąg dalszy
Jun 4th
W poprzedniej notce powiedziałem wiele dobrego na temat Ncqrs. Aby być fair, tym razem chciałbym się skupić na kilku mankamentach, które udało mi się zauważyć podczas zabawy z tym frameworkiem.
Ncqrs nie jest jeszcze gotowy do wdrożenia produkcyjnego out-of-the-box. Nie ma co udawać. Biblioteka potrzebuje jeszcze trochę czasu, aby dojrzeć. Z drugiej strony, jeśli komuś zależałoby na jej funkcjonalności i bardzo chciał jej użyć, zawsze może zostać commiterem i poprawić te i owe braki. Na obecnym etapie, do pracy z Ncqrs jest wymagana bardzo dobra znajomość jej wewnętrznych mechanizmów, co czyni ją niepraktyczną w standardowych zespołach deweloperskich (poza R&D).
Serce Ncqrs, czyli definicja korzenia agregatu, jest zbudowane w oparciu o dziedziczenie — klasy aplikacji dziedziczą z frameworkowych klas bazowych. To dobre na początek, jednak wcześniej czy później przychodzi czas, kiedy trzeba przejść na model POCO. Jest on nie tylko bardziej przyjazny dla dewelopera aplikacji, ale także dla twórcy rozszerzeń.
W Ncqrs w tym momencie brakuje możliwości definiowania encji zawieranych w agregacie (poza korzeniem oczywiście). Z mojego doświadczenia wynika, że takie encje to raczej rzadkość, jednak się zdarzają. Problem w tym, że kiedy się już zdarzają, to są bardzo potrzebne.
Nie pisałem tego wszystkiego ot tak sobie. Jeśli jesteś pasjonatem oprogramowania, masz trochę wolnego czasu i chciałbyś pobawić się z naprawdę ciekawymi problemami, Ncqrs czeka na Ciebie! Pracy jest dużo, a te trzy problemy, które wymieniłem w tym poście, to tylko wierzchołek góry lodowej.
Ncqrs
May 30th
W ciągu ostatnich dwóch tygodni moją uwagę przykuł na dobre nowy framework open source — Ncqrs (witryna CodePlex Ncqrs znajduje się tutaj). Jak sugeruje nazwa, Ncqrs służy do budowy systemów w oparciu o wzorzec architektoniczny Command-Query Responsibility Separation (CQRS). To, czego nazwa nie mówi, to fakt, że Ncqrs narzuca pewną specyficzną implementację wspomnianego wzorca, a mianowicie tę opartą o technikę Event Sourcing. Na podstawie posta Grega Younga można by się czepiać, że nazwa Ncqrs nie jest zbyt trafna, ale odłóżmy kwestie nomenklatury na bok. Czym jest Ncqrs i jak to się stało, że mnie tak zafascynował?
Zasada działania
Ncqrs jest całościowym rozwiązaniem służącym do budowy systemów opartych o silny model domeny, którego stan jest przechowywany za pomocą strumienia zdarzeń. Poniższy diagram prezentuje workflow dla pojedynczego przypadku użycia w Ncqrs.
Komendy
Punktem wejścia do Ncqrs są komendy. Są to obiekty, który reprezentują żądania wykonania pewnej operacji na modelu domeny. Komendy są mapowane na operacje za pomocą rozbudowanego rozszerzalnego mechanizmu. Najprostsza implementacja mappera opiera się na dwóch atrybutach, które określają, czy dana komenda ma wykonywać metodę istniejącego obiektu (w nomenklaturze Ncqrs — korzenia agregatu), czy też tworzyć nowy obiekt. Properties komendy są mapowane (na podstawie nazwy) do parametrów wybranej metody lub konstruktora. Ostatecznie, (jeśli to konieczne) z magazynu danych podnoszony jest odpowiedni obiekt i wykonywana jest odpowiednia metoda (lub konstruktor).
Operacje biznesowe
Operacje i konstruktory obiektów są w Ncqrs wywoływane tylko za pośrednictwem komend. Ich jedynym zadaniem jest wykonanie logiki biznesowej. Nie mogą one bezpośrednio modyfikować stanu obiektu. Zamiast tego, dozwolonym mechanizm modyfikacji stanu jest zgłaszanie zdarzeń. Operacje biznesowe mogą także komunikować się ze światem zewnętrznym.
Stosowanie zdarzeń
Jak już wspomniałem na wstępie, zdarzenia są sposobem przechowywania stanu obiektów w Ncqrs. Operacja biznesowa może zgłosić jedno lub więcej zdarzeń. Dla każdego z nich framework wyszukuje odpowiedniej metody je przetwarzającej. Mechanizm ten jest oczywiście rozszerzalny, a out-of-the-box Ncqrs zapewnia dwa sposoby wiązania zdarzenia z metodą obiektu biznesowego przeznaczoną do jego obsługi: za pomocą konwencji oraz za pomocą atrybutów. Ncqrs wywołuje znalezioną metodę przekazując jej zgłoszone zdarzenie.
Przetwarzanie zdarzeń
Metoda przetwarzająca zdarzenie modyfikuje stan obiektu na podstawie danych przekazanych zdarzeniu. Tylko tyle i aż tyle. Metoda ta nie powinna zawierać żadnej logiki biznesowej (warunkowej), ani mieć jakichkolwiek skutków ubocznych (komunikacja z innymi systemami itp.).
Zapewne chcielibyście zapytać po co tyle komplikacji? Dlaczego operacja biznesowa nie może zmodyfikować stanu? Odpowiedź jest prosta. Ponieważ stan obiektów jest reprezentowany jako strumień zdarzeń przez nie wygenerowany, aby odtworzyć obiekt niezbędne jest stworzenie jego pustej instancji, a następnie przetworzenie (w kolejności!) wszystkich zapisanych zdarzeń — oczywiście za pomocą odpowiednich metod przetwarzających. Metody te są więc nie tylko stosowane do modyfikacji stanu podczas przetwarzania, ale także do odtwarzania tego stanu podczas podnoszenia obiektu z trwałego magazynu.
Dzięki takiemu podejściu systemy Event Sourcing (jak Ncqrs) zapewniają, za darmo, ślad audytowy, który ma gwarancję poprawności, ponieważ jest on wykorzystywany do budowy obiektów podczas normalnego działania systemu.
Publikowanie i denormalizacja zdarzeń
Jeśli wszystko do tej pory przebiegło prawidłowo, wszystkie zgłoszone zdarzenia są publikowane. Oczywiście, także w tym wypadku Ncqrs pozwala wymienić mechanizm publikacji zdarzeń. Domyślny wykorzystuje komunikację wewnątrz procesu, ale dostępny jest także taki, który wykorzystuje NServiceBus.
Publikowanie zdarzeń ma dwa cele. Po pierwsze, pozwala powiadomić zainteresowane systemy zewnętrzne o zmianach stanu naszego systemu. Polega to na eskalowaniu “lokalnych” (dotyczących naszego systemu) zdarzeń do statusu zdarzeń “globalnych” (mających znaczenia dla całego środowiska systemów). Stąd już tylko jeden krok do pełnej Event Driven Architecture (EDA).
Drugim celem publikowania zdarzeń jest tzw. denormalizacja, czyli aktualizacja podsystemu obsługi zapytań. Jaki podsystem? O co chodzi? Dokładny opis zagadnienia CQRS znajduej się tutaj. W tym miejscu wspomnę tylko, że systemy CQRS wykorzystują zwykle dwa osobne magazyny danych dla przetwarzania komend oraz do realizacji zapytań. Do synchronizacji tego drugiego magazynu danych wykorzystywane są właśnie denormalizatory zdarzeń. Proces denormalizacji polega na wykonaniu w bazie danych dla zapytań modyfikacji, które wynikają z opublikowanego zdarzenia. Skąd taka nazwa? Otóż zdarzenia stanowią znormalizowaną (pozbawioną redundancji) postać danych. W magazynie dla zapytań zaś, te same dane mogą mieć wiele reprezentacji, ponieważ nadrzędnym celem jest optymalizacja czasu realizacji zapytań.
Dlaczego to może działać?
Jest kilka powodów, które sprawiają, że (pozornie) szalona idea reprezentacji stanu obiektów jako ciągu zdarzeń może działać w praktyce. Oto kilka z nich:
- zapewnia darmowy, gwarantowany, ślad audytowy
- możliwość wykonywania tzw. snapshot’ów (czyli pełnych zrzutów zserializowanego obiektu biznesowego) co N zdarzeń. Dzięki temu odtwarzanie obiektu wymaga jedynie przetworzenia zdarzeń, które nastąpiły po ostatnim snapthot’cie
- w klasycznym DDD (z użyciem O/RM) podczas podnoszenia obiektu z bazy danych tak naprawdę pobierane jest wiele wierszy danych (np. za pomocą podzapytań i złączeń). W przypadku Event Sourcingu pobieranych jest kilka wierszy przechowujących zdarzenia. Oba podejścia mają więc podobną złożoność na poziomie bazy danych.
Zapraszam Was do zabawy z Ncqrs. Postaram się odpowiedzieć na wszystkie Wasze pytania.







