Warstwa domeny w Domain Driven Design jest prawdziwą wisienką na torcie. Powinniśmy dbać o nią, aby zawsze była czysta. Dziś przedstawię Wam kilka heurystyk, których stosowanie spowoduje, że Wasza domena nabierze barw 🙂

1. Konstruktor i poprawny stan encji

Zgodnie z Domain Driven Design encja jest rodzajem obiektu, którego stan zawsze powinien być poprawny z perspektywy realizowanego biznesu. Jeżeli założymy sobie, że są dwa pola, które zawsze muszą być skonfigurowane aby encja była w stanie poprawnym, to muszą być one skonfigurowane już od samego początku. Przykładem tutaj może być encja produktu z domeny eCommerce: produkt zawsze musi mieć nazwę oraz cenę. Chyba żaden biznes z domeny sklepowej nie wyobraża sobie, aby było inaczej.

Egzemplarze encji powinny wychodzić z odpowiednich fabryk, które następnie powinny używać konstruktora w celu ich utworzenia. I to jest tak na prawdę jedyne miejsce, w którym ten konstruktor jest wykorzystywany. Jak możecie przeczytać w moim innym wpisie, Doctrine nie korzysta z naszych konstruktorów w celu tworzenia zapisanych w bazie encji, więc mamy dowolność co do ich definicji.

2. Nie wszystkie pola muszą mieć settery! 😉

Drugą kwestią, która jest związana z poprawnością stanu encji, jest odpowiedzialność za to, kto ma prawo do modyfikacji jej składowych i w jakim celu. Jeżeli damy wszystkim dostęp do pola za pomocą settera, to z perspektywy warstwy domeny stracimy kontrolę nad tym polem. Stracimy informację o wszystkich kontekstach, w których to pole jest modyfikowane. Jeżeli będziemy chcieli odpowiedzieć sobie na pytanie „Kiedy modyfikujemy to pole”, to będziemy musieli szukać informacji po serwisach, command handlerach i innych subscriberach. A powinniśmy móc znaleźć tą odpowiedź po przejrzeniu interfejsu interesującej nas encji.

Wyobraźmy sobie, że mamy do czynienia z encją (tak tak, znowu eComerce) pozycji w koszyku. Każda pozycja ma dowiązanie do encji koszyka. Jeżeli nasz biznes nie zakłada opcji przenoszenia pozycji między koszykami, to te na stałe będą przywiązane do encji koszyka. Zatem w encji pozycji setter dla koszyka nie będzie potrzebny. Tak samo, ta pozycja będzie miała na pewno jakąś nazwę. O ile nie mamy w naszym sklepie funkcji do customowego nazywania pozycji koszyka, to setter również nie będzie tu potrzebny. Settery nie są również potrzebne z perspektywy działania samego Doctrine. Technicznie rzecz ujmując, do konstruowania encji Doctrine stosuje mechanizm deserializacji obiektów. Mechanizm ten zupełnie nie ma powiązania z setterami encji. Szerzej o tym temacie możecie przeczytać w moim wpisie o Cyklu życia encji w Doctrine 2.

W tym miejscu również należy wspomnieć o niezmiennikach, czyli przekładając na programistyczne – metodach encji, które zmieniają jej stan. Możemy (a nawet powinniśmy!) mieć je w naszych encjach. Np. zmieniając cenę produktu w naszym sklepie jest duże prawdopodobieństwo, że ta cena jest przywiązana do jakiejś waluty. Dlatego zamiast osobnego settera na wysokość ceny oraz osobnego settera na walutę, powinniśmy mieć jedną metodę, która przyjmie te dwa parametry. Jeżeli uznamy to za stosowne, to zamiast tego możemy zastosować tutaj Value Object, który zagreguje te dwa pola.

3. Nie bój się rzucać wyjątków

Stosowanie modelu anemicznego, czyli takiego, który składa się w większości z setterów oraz getterów, skutkuje tym, że stan poprawny naszej encji rozpływa się pomiędzy serwisy. Ciężko jest wtedy kontrolować poprawność tego stanu. Tworzymy różne walidatory, stosujemy je w wielu miejscach. Mnożymy je, bo w jednym miejscu trzymamy jedną regułę, a w drugim drugą. Całość przestaje trzymać się kupy.

Kiedy stosujemy metody realizujące niezmienniki biznesowe, to mamy ciągłą możliwość kontroli poprawności danych, które przychodzą do encji. Przywołując wcześniej wspomnianą pozycję koszyka – nie powinna on posiadać pola ilości z wartością mniejszą niż jeden. Tą regułę na spokojnie możemy skontrolować, czy to w konstruktorze, czy to w metodzie realizującej niezmiennik związany z aktualizacją tej wartości. Jeżeli wartość tego pola nie jest odpowiednia – wyrzućmy wyjątek. Niech inne części systemu martwią się, co z tym fantem zrobić – zalogować tą informację, czy wyrzucić na ekran odpowiedni komunikat. Nasza domena ma być czysta i ma jasno sygnalizować, kiedy są dane, których nie może przyjąć. To samo tyczy się obiektów wartości oczywiście.

4. Zwracanie Value Objectu złożonego z pól encji

Czasami jest tak, że w jakimś określonym kontekście pewne pola encji stanowią logiczną całość. Z perspektywy naszej encji tak jednak nie musi być. Powołując się ponownie na domenę eCommerce, rozważmy produkt, który posiada nazwę oraz cenę.

Z perspektywy encji produktu są to dwa osobne pola i nie widzimy opcji na to, aby je ze sobą połączyć w jakikolwiek Value Object. Ale w kontekście tego, że na podstawie tych wartości powstaje pozycja w koszyku – możemy założyć, że już jest jakiś sens, aby te pola ze sobą połączyć. Możemy więc utworzyć Value Object, który zagreguje te pola (a pamiętajmy, że tych pól może być więcej niż dwa). Ponieważ z perspektywy encji produktu cena i nazwa nie powinny być jednym bytem, to tutaj zostawiamy to tak, jak było. Natomiast, zamiast tworzyć osobne gettery na te wartości – zróbmy jeden, który w locie zwróci nam utworzony przez nas Value Object z wszystkimi wartościami, których potrzebujemy w kontekście tworzenia pozycji koszyka.

5. Prosty ValueObject i stosowanie Custom Mapping Types

Gdzieś wyżej pisałem o przykładzie z polem ilość w encji pozycji koszyka. W kontekście pozycji koszyka wartość tego pola nigdy nie powinna być mniejsza niż jeden. Jest to doskonała okazja na utworzenie Value Objectu. Jest to również idealny przykład prostego Value Objectu, czyli takiego, który składa się z tylko jednego pola.

Wracając do pola ilości – zamiast w całym systemie wrzucać podobne warunki – możemy przenieść odpowiedzialność za walidację tej wartości właśnie do Value Objectu. Pamiętajmy – obiekty wartości, podobnie jak encje, służą do kontrolowania i walidowania reguł biznesu. Podobnie jak encje – muszą występować w stanie poprawnym. W DDD obiekty wartości od encji różnią się tylko tym, że nie mają własnej tożsamości, czyli nie posiadają identyfikatora. Róźnica z perspektywy kodu będzie taka, że pole quantity z typu int przejdzie na typ klasy obiektu wartości.

Teoria teorią, ale co na to Doctrine? Okazuje się, że z tej strony nie ma żadnego problemu. Doctrine posiada mechanizm Custom Mapping Types, który pomaga zrealizować nasz cel. Polega to na tym, że tworzymy w systemie klasy typów, których zadaniem jest konwersja wartości z encji (typ Value Objectu) na typ rozumiany przez bazę danych (zazwyczaj typ prosty) oraz na odwrót. Dzięki temu mechanizmowi pola naszych encji mogą mieć inne typy, niż te przewidziane przez Doctrine.

6. Złożony ValueObject i stosowanie Embeddables

Oprócz prostych obiektów wartości możemy mieć potrzebę grupowania pól encji, które są ze sobą bardzo sprzężone. Przykładem może tutaj być cena, o której również pisałem wcześniej. Wróćmy również do pozycji koszyka. Ta jest ciekawym przykładem, bo oprócz ceny jednostkowej (która składa się na wartość i walutę) możemy mieć również pełną kwotę pozycji, która będzie efektem pomnożenia ceny jednostkowej przez liczbę sztuk, od której np. odejmiemy kwotę rabatu. Czyli w jednej encji będziemy mieli co najmniej dwa pola, które mogą być Value Objectem tego samego typu. A on sam w sobie agreguje dwa inne pola – cenę i walutę.

Do konstrukcji złożonych obiektów wartości możemy wykorzystać embeddables, czyli mechanizm Doctrine, który został stworzony właśnie do tego celu. Oprócz agregacji pól mamy tutaj również opcję prefiksowania pól bazodanowych, która to pozwala na wykorzystywanie tego samego typu wielokrotnie w obrębie tej samej encji.

7. Stosuj prawo Demeter

Znacie prawo Demeter? Jeżeli nie, to zachęcam do zapoznania się z nim, bo jego stosowanie bardzo mocno wpływa na to, jak wyglądają nasze encje. Nieskromnie wspomnę, że jednym z pierwszych moich wpisów był właśnie ten o Prawie Demeter w modelowaniu domenowym 🙂 .

8. Agregaty, czyli nie do wszystkiego musisz mieć dostęp

Encje, w kontekście Domain Driven Design, są grupowane w agregaty. Jedną z zasad ich stosowania jest to, że nie powinniśmy udostępniać składowych agregatu (encji) na zewnątrz. Czyli w przypadku, jeżeli mamy koszyk wraz z jego pozycjami, to nigdzie w aplikacji, poza agretatem koszyka, nie powinniśmy mieć możliwości operowania na encji pozycji koszyka. Jeżeli chcemy dopytać się o szczegóły dotyczące pozycji koszyka (np. wartość pola quantity), to powinniśmy odpytać się przez rdzeń agregatu nią. Czyli przez encję koszyka. No i ten koszyk powinien nam zwrócić wartość (czy to z typem prostym, czy Value Objectem), ale bez dostępu do samej encji pozycji koszyka.

Więcej na temat agregatów oraz wszystkich reguł (których wymaga ich stosowanie) przeczytasz w moim wpisie o Poszukiwaniu agregatów w Domain Driven Design.

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.