Ukoronowaniem Domain Driven Design, jak sama nazwa wskazuje, jest domena. Na bardzo wstępnym etapie wiedzy ta warstwa kojarzy się zazwyczaj z encjami przepełnionymi dużą liczbą setterów/getterów.

Poznajmy zatem czym jest oraz jak powinna wyglądać warstwa domenowa aplikacji opartej o DDD.

Encje

Najbardziej rozpoznawalną częścią domeny są encje (ang. entity). Na pierwszy rzut oka można by stwierdzić, że każda encja jest odpowiednikiem tabeli w bazie danych. Do tego przyzwyczaił nas Doctrine. Nomenklatura DDD nie wyszczególnia tego, jaki jest sposób zapisu encji. Równie dobrze co baza danych, możemy chcieć zapisać encję w plikach (np. JSON, CSV), nie będącymi bazami danych serwisach wspierających aplikację (np. Redis, ElasticSearch) czy w końcu zewnętrznych usługach. Co w takim razie definiuje, że klasa, którą chcemy utworzyć jest encją?

Według DDD, Encją nazywamy obiekt, który posiada własną tożsamość (ang. identity). Oznacza to, że możemy mieć w systemie dwa egzemplarze tej samej klasy, które przechowują dokładnie te same informacje, jednak nie są one tożsame. Różni je tożsamość, która jest przetrzymywana w dodatkowym polu nazywanym najczęściej identyfikatorem. Z perspektywy DDD nie jest dla nas istotne, czy ten identyfikator jest typu liczbowego (najczęściej spotykane przy współpracy z bazami danych), czy tekstowego (np. w formacie UUID).

Encje są klasami, których egzemplarze zawsze powinny zostawać w stanie poprawnym w kontekście logiki biznesowej. Oznacza to, że do tworzenia encji powinniśmy korzystać z wyspecjalizowanego konstruktora, który wprowadzi obiekt już na starcie w stan poprawny. Dodatkowo, mówiąc o encjach nie powinniśmy zapominać o niezmiennikach (ang. entity invariants), to jest regułach, które powodują, że każda metoda encji również wprowadza ją w stan poprawny. Przykładem złamania reguły niezmienników są settery, przez które kontrola poprawności stanu encji zostaje przeniesiona nie na encję, lecz na serwisy na nich pracujące.

Ostatnią cechą charakteryzującą Encję jest to, że zostaje ona persystowana w systemie, aby w dowolnej chwili można było się do niej odnieść.

Obiekty Wartości

Obiekty Wartości (ang. value obects), zaraz po encjach, są najbardziej rozpoznawalnym rodzajem klas w obrębie warstwy domeny. Bardzo często mówi się, że jedyna różnica między obiektem wartości a Encją, to jest jej tożsamość, czyli przechowywanie identyfikatora. Nie jest to jednak prawda; zgodnie z zasadami DDD, cechują się one nieco większą liczbą charakterystyk.

Na samym początku należy przyjrzeć się temu, czym jest wartość. Jako wartość możemy traktować coś, co mierzy (np. waga, długość), liczy (kwota zamówienia, czas trwania kursu) bądź opisuje (imię, SKU, tag). Dodatkowo, wartość może mieć więcej niż jedną składową. Przykładem tego są kwoty w sklepie składające się z ilości oraz waluty, lub metadane strony internetowej. Każdy z wyżej wymienionych rodzajów wartości może być przechowywany oraz kontrolowany przez klasę obiektu wartości. Dodatkowo, obiekty wartości są szczególnie wykorzystywane w warstwie domeny do reprezentowania wartości przechowywanych w encjach.

Następną z charakterystyk omawianego w tym punkcie elementu domeny jest niezmienność wartości (ang. immutability). Oznacza to, że klasa obiektu wartości powinna móc przyjmować wszystkie składowe wartości przez konstruktor, który jako jedyny może zmienić (a właściwie określić) stan obiektu. Każda kolejna metoda w klasie może służyć jedynie do zwrócenia (części lub całości) informacji o przechowywanej wartości. Jeżeli potrzebujemy zmodyfikować wartość, to realizujemy to poprzez utworzenie nowego egzemplarza obiektu wartości, a następnie przypisanie go do konkretnego pola encji. Niezmienność wartości jest kluczowa, kiedy przypisujemy jeden obiekt wartości do co najmniej dwóch egzemplarzy encji. Gdybyśmy pozwolili na modyfikowalność wartości, to po zmianie wartości podczas pracy na pierwszej encji, wartość drugiej z nich również zostałaby zmodyfikowana.

Kolejną cechą obiektów wartości jest ich porównywalność. Każda klasa reprezentująca wartość może mieć własne reguły, które definiują, czy jeden egzemplarz jest równy drugiemu. W najprostszym tego przykładzie, możemy porównywać wartość poprzez zwykły operator porównywania. Nic nie stoi jednak na przeszkodzie, aby np. porównywać adres e-mail bez rozróżniania liter wielkich od małych. Jeżeli mamy zestaw parametrów definiujący wariant produktu, to niekoniecznie będzie nas interesowała kolejność tych parametrów przechowywanych w kolekcji. Tego typu reguł można mnożyć, w zależności od potrzeb biznesowych. Istotne jest, aby za przeprowadzanie porównania wartości odpowiedzialna była klasa obiektu wartości.

Zdarzenia

Jeżeli wydarzył się w przeszłości istotny dla nas punkt w czasie, to znaczy, że nastąpiło zdarzenie (ang. event). Każdą klasę reprezentującą zdarzenie z definicji powinniśmy nazywać tak, aby odnosiła się do przeszłości. Dodatkowo, zdarzenie jako punkt, który wydarzył się w przeszłości, jest niemodyfikowalne. Oznacza to, że do konstrukcji zdarzenia powinniśmy korzystać z konstruktora, a metody zawarte w klasie nie mogą zmieniać stanu obiektu.

Niezmienność zdarzenia jest bardzo istotna i łatwo jest ją zaburzyć. Bardzo powszechną praktyką jest załączanie do niego pełnego obiektu encji. Niestety, taka operacja jest tutaj niedozwolona. Powodem tego jest fakt tego, że jeżeli w encji zajdzie zmiana (bo może), to stan zdarzenia niejawnie zostanie zmieniony. Przez tego typu problemy powstają błędy trudne do debugowania. Śledzenie zmian wartości jest mocno zaburzone, przez co łatwo jest podjąć błędne decyzje w sprawie naprawienia błędu. Pozytywnie na niezmienność zdarzeń wpłynie natomiast korzystanie z pozostałych niezmiennych wartości; mowa tutaj szczególnie o typach prostych oraz o obiektach wartości.

Zdarzenia w warstwie domeny najczęściej są wychwytywane przez warstwę aplikacji, która na podstawie swojej wiedzy decyduje, co dalej z nimi zrobić. Mamy tutaj do wyboru dwie opcje: wykonanie kolejnych operacji w trybie natychmiastowym (synchronicznym) lub wykonanie odroczone (asynchroniczne). Jeżeli mówimy o tym drugim, to musimy pamiętać, że każde zdarzenie zostaje zserializowane przed wysyłką do odpowiedniego miejsca. Oznacza to, że powinniśmy również zadbać o poprawną serializację oraz deserializację wszystkich wartości przechowywanych przez zdarzenia.

Ostatnią charakterystyką zdarzeń w DDD jest pozostawanie zamkniętym na publikowanie zdarzeń między systemami. Gdybyśmy zezwolili na tego typu zabieg, to musielibyśmy w jakikolwiek sposób zadbać o deserializację zdarzenia po stronie systemu zewnętrznego. Na to jednak najczęściej nie mamy wpływu. Jeżeli jednak mamy na to wpływ (np. znamy się z zespołem rozwijającym ten zespół), to istnieje wtedy potrzeba współdzielenia klas zdarzeń między systemami. Powoduje to natomiast dodatkowe narzuty, takie jak dodatkowa komunikacja między zespołami, czy dodatkowe zależności między aplikacjami, które niejednokrotnie utrudniają pracę nad aplikacjami oraz idąc dalej: ich wdrożenie.

Wyjątki

Specjalnego rodzaju zdarzeniami są klasy wyjątków (ang. exception). Informują one o tym, że zaistniała sytuacja, która z punktu widzenia logiki domenowej jest niedopuszczalna. Klasy wyjątków są jednym z tych rodzajów klas, które mogą występować w każdej warstwie aplikacji opartej o Domain Driven Development.

Istotą klas wyjątków jest to, aby każda z nich nazwana została w sposób jednoznacznie mówiący, co się dokładnie stało. Podobnie, jak klasy zdarzeń, do wyjątków załączamy kontekst, który będzie dla nas istotny w momencie analizy sytuacji, która nastąpiła. Kontekst ten powinien być niemodyfikowalny, ponieważ raz wyrzuconego zdarzenia (wyjątkowego) nie powinniśmy modyfikować. Zatem będziemy przekazywali do konstruktora typy proste oraz obiekty wartości.

Dodatkowo pamiętać należy, że wyjątki są zazwyczaj serializowane do postaci logów, zatem musimy zadbać o dobrą ich serializację. O deserializację dbać zazwyczaj nie musimy, ponieważ najczęściej nie ma potrzeby zamiany logów z powrotem do postaci obiektów.

Agregaty

Agregatami (ang. aggregate) nazywamy specjalnego rodzaju serwisy, które implementują wzorzec fasady dla całej warstwy domeny. Jeżeli jakiś proces ma zostać wykonany wewnątrz domeny, to musi on zostać zrealizowany przez konkretny agregat. To agregat posiada pełną wiedzę na temat wszystkich procesów oraz zależności istniejących wewnątrz domeny.

Każdy agregat pracuje na bycie nazywanym rdzeniem agregatu (ang. aggregate root). Przed pracą agregatu ten rdzeń musi zostać skonstruowany. Rdzeniem jest oczywiście encja, która posiada zestaw niezmienników, dzięki któremu cały proces może zaistnieć. Według zasad Domain Driven Design, rdzeń agregatu powinien być tylko jeden i posiadać formę drzewiastą.

Dodatkową kwestią dotyczącą agregatów jest to, że podczas jednej transakcji (np. bazodanowej) możemy pracować na tylko jednym egzemplarzu agregatu. Najczęściej realizowane jest to w ten sposób, że do odpowiedniego busa w systemie wysyłana jest komenda, która następnie jest procesowana przez odpowiadający jej handler. O poprawną transakcyjność dba sam handler, bądź jest to zadanie dla odpowiedniego middleware wewnątrz command busa. Dalszą (i w zasadzie główną) odpowiedzialnością handlera będzie wywołanie logiki konstruującej agregat oraz następnie praca na nim.

Ze względu na bezpieczeństwo transakcji, wewnątrz agregatu nie powinniśmy pracować na innym agregacie. Jeżeli logika biznesowa wewnątrz agregatu jest zbyt rozległa, to znaczy, że powinniśmy wydzielić dodatkowe serwisy domenowe.

Serwisy domenowe

Serwisy domenowe (ang. domain services) realizują fragmenty logiki biznesowej, z których korzystają agregaty. Jeżeli mamy logikę, która powinna być współdzielona między kilkoma agregatami, to powinna zostać ona umieszczona właśnie w obrębie serwisu domenowego. Dodatkowo, odpowiednie rozbicie logiki biznesowej na serwisy domenowe spowoduje, że kod agregatów będzie bardziej czytelny oraz zwięzły. Nic nie stoi na przeszkodzie, abyśmy wewnątrz serwisów domenowych mogli wywoływać logikę innych serwisów domenowych. Jednakże wszystko powinno być robione z umiarem tak, aby proces biznesowy był prosty do zrozumienia.

Fabryki

Fabryki (ang. factories), podobnie do klas wyjątków, są rodzajem klas, który może występować w dowolnej warstwie DDD. Konstruować możemy wszystkie obiekty klas, które nie mają dostępu do kontenera zależności. Będą to zatem: encje, obiekty wartości, zdarzenia oraz wyjątki.

Dodatkowym zadaniem fabryki może by również konstrukcja skomplikowanych agregatów, które przed użyciem muszą być gdzieś skonstruowane. Mowa tutaj o konstruowaniu rdzeniu agregatu, który musi być dostępny przed jego agregatu.

Profesjonalista, zajmujący się na co dzień aplikacjami biznesowymi w ekosystemie PHP. Jego pasją jest odkrywanie nowych konceptów programistycznych oraz wzorce architektoniczne. Uwielbia również pisać testy, gdyż jak sam uważa, dobry kod to przetestowany kod.

Comments are closed.