Domain Driven Design pt.2 – rodzaje obiektów

W poprzednim artykule z serii Domain Driven Design przedstawiłem pole zastosowań metodologii DDD, jej otoczkę w postaci zarządzania projektem oraz język domeny (DSL). W dzisiejszym wpisie chciałbym natomiast przedstawić metodologię DDD w sposób bliższy implementacji.

Tak jak może już wspomniałem w poprzednim wpisie, metodologia DDD odnosi się do warstwy modelu w paradygmacie MVC. Natomiast wewnątrz tej warstwy modelu w MVC, DDD wprowadza rozdział na warstwę domenową (właściwy model) i warstwę infrastruktury. Dodatkowo istnieje kilka rodzajów obiektów, które mogą występować w obu warstwach:

  • Encje (Entities)
  • Wartości (Value Objects)
  • Usługi (Services)

Czym jest warstwa domenowa ?

Warstwa domenowa odpowiada naszemu modelowanemu zagadnieniu a więc opierając się na poprzednim przykładzie hurtowni internetowej będą to obiekty takie jak Kontrahent, Przedstawiciel Handlowy, Faktura, Zamówienie, Oferta Handlowa etc.

Czym jest warstwa infrastruktury ?

Warstwa infrastruktury odpowiada za wszelkie „techniczne” działania jakie są potrzebne by warstwa domenowa mogła dobrze funkcjonować i współpracować z resztą aplikacji. A więc rzeczy typu utrwalanie (zapis do bazy), operacje na systemie plików, komunikacja z webservice’ami etc.

Typowym przedstawicielem warstwy infrastruktury są mappery relacyjno-obiektowe np. Doctrine. Jednak nie wszystkie z nich nadają się by być częścią warstwy infrastruktury, Doctrine w wersji 2 się nadaje, natomiast w wersji 1 nie. Dlaczego ? Metodologia DDD zakłada całkowity rozdział warstwy infrastruktury od warstwy domenowej, niestety chcąc utrwalać obiekty domenowe w Doctrine 1 musiały by dziedziczyć one z klasy Doctrine_Row co tworzy niepożądaną zależność. Dzięki temu, że warstwa domeny jest niezależna od warstwy infrastruktury możemy np. zmienić w którymś momencie ORM-a bez konieczności zmiany naszych klas w warstwie domeny.

Rodzaje obiektów

Wspomniałem wcześniej o kilku rodzajach obiektów, jak tworzyć więc klasy odpowiednich rodzajów ?

Klasy encji są to klasy które w konkretnym modelu domeny mają szczególne znaczenie, takie by stać się rozróżnialnymi (potocznie mówiąc mają własne ID). Klasy wartości natomiast różnią się od klas encji tym, że nie są rozróżnialne (potocznie mówiąc nie mają własnego ID, zawsze są przypisane do jakiejś encji). To czy dana klasa stanie się klasa encji czy wartości zależy od wielu czynników.

Załóżmy, że mamy aplikację do obsługi Urzędu Pocztowego, z modelowania wyszły nam dwie klasy Adres i Człowiek, w przypadku UP dla poczty najważniejszy jest adres i on też będzie encją, natomiast kto mieszka pod tym adresem jest w pewnym sensie sprawą drugorzędną (przynajmniej dla listów, które nie są polecone) będzie więc klasą wartości. Dla odmiany kiedy tworzymy aplikacje dla Hurtowni Internetowej to ważniejszy dla nas będzie Człowiek/Klient (on w końcu płaci za towar) i to on będzie encją, natomiast adres będzie wartością.

Trzecim rodzajem klas, które występują w DDD są klasy usług. Są to klasy, do których „wrzuca” się wszystkie metody, które nie pasują by je dołożyć do klas encji. Definicja ta niewiele mówi, nasuwa się więc pytanie – co powinno być w klasach encji ?

Metody w klasach encji powinny odpowiadać wszelkim (rzeczywistym) działaniom jakie wykonujemy na pojedynczym jej egzemplarzu. Przykładem niech będzie klasa Zamówienia z hurtowni internetowej. Będzie ona zawierać metody:

class Zamowienie {     
    public function dodajTowar($towar);
    public function usunTowar($towar);
    public function obliczCene();
    public function pobierzPodsumowanie();
    public function dodajOdbiorce($odbiorca);
    public function dodajRabat($rabat);
    public function wybierzSposobPlatnosci();
    public function wybierzSposobTransportu();
}

Co potrzebowalibyśmy jeszcze w kontekście zamówień ? Na pewno klient w swojej aplikacji chciałby mieć możliwość wyszukiwania Zamówień wg jakiś kryteriów, dotyczy to wszystkich zamówień, stworzymy więc klasę usługową, która będzie repozytorium zamówień. Jeżeli korzystamy z Doctrine’a 2, zostanie ona automatycznie wygenerowana przez ORM-a i będzie zawierała metody w rodzaju findBy(/*...*/), dzięki którym wyszukamy interesujące nas zamówienia. Repozytorium takie umożliwi również zapisywanie tych obiektów.

Klasę, która jest usługą utworzymy również w przypadku gdy będziemy potrzebowali np stworzyć raporty, wykresy czy inne zestawienia. Tworząc model domenowy należy pamiętać by nie wpaść w pułapkę anemicznego modelu domeny. Co to oznacza ? Anemiczny model domeny to taki model domenowy w którym w encjach są tylko gettery i settery, natomiast cała reszta metod „wyemigrowała” nam do klas usługowych.

W kolejnej części cyklu postaram się zaprezentować jakiś mały przykładowy projekcik napisany w metodologii DDD. Stay tuned ;).

  1. Alan Gabriel Bem pisze:

    Jak zwykle bardzo fajnie i rzeczowo…..

    ….ale jak mogłeś nie wspomnieć nic o repozytoriach (i ewentualnie o fabrykach), które odgrywają tak duża rolę w faktycznym podziale dziedzina-infrastruktura?

    :)

  2. Alan Gabriel Bem pisze:

    Ups, sorry, uciekł mi ten akapit mówiący o repozytoriach.

    Mimo wszystko potraktowałeś je (repozytoria) bardzo po łebkach oraz nie mogę się z Tobą zgodzić, że repozytoria są serwisami.

  3. Wojciech Soczyński pisze:

    Fakt, może trochę potraktowałem repozytoria i fabryki „po łebkach”, wynika to jednak z faktu, że oba rodzaje obiektów zwykle dostajemy gotowe w ramach jakiegoś frameworku lub innych bibliotek (Doctrine czy kontener DI).
    Natomiast uważam, że najistotniejszą rzeczą w całym DDD jest właściwe wymodelowanie klas encji i serwisów. Bez tego, projekt w ddd raczej nie ma szans na szczęśliwy finisz.
    Co do zakwalifikowania Repozytoriów jako serwisy, wynika to z tego, że po pierwsze świadczą one usługi na rzecz klas domenowych, po drugie nie mają własnej tożsamości (nie są encjami), oraz atrybutów (nie są wartościami). Tak to wygląda z mojej perspektywy, jednak mogę się mylić 😉

  4. Michał Wujas pisze:

    Gdyby DDD dało się nauczyć na podstawie tutoriali pracę nad projektem gdzie jest kilkaset encji i banda programistów stałaby się znacznie przyjemniejsza 😉
    A encje w dużych projektach lubią się rozrastać do niemożliwych rozmiarów bo występują w bardzo różnych kontekstach i wtedy to już tylko zdrowy rozsądek ratuje…

  5. Alan Gabriel Bem pisze:

    Co do zakwalifikowania Repozytoriów jako serwisy, wynika to z tego, że po pierwsze świadczą one usługi na rzecz klas domenowych, po drugie nie mają własnej tożsamości (nie są encjami), oraz atrybutów (nie są wartościami). Tak to wygląda z mojej perspektywy, jednak mogę się mylić 😉

    Ja się uczyłem, że serwisy powinny wykonywać logikę biznesową, która nie pasuje do żadnych z encji, stąd ich pełna nazwa „Domain Services” np.

    
    $konto1->przelej(1000, $konto2); // przelewany 1000 dolców z rachunku 1 do rachunku 2
    

    Z perspektywy „czystości” kodu jest ładnie i schludnie, ale z perspektywy eksperta dziedziny mamy wielkie WTF! – jak jedno konto może nadzorować przelew na drugie?

    
    $przelewacz->przelej(1000, $konto1, $konto2); // przelewacz jest serwisem, nie encją
    

    I już wszystko wróciło do normy.

    Kiedyś na taką odpowiedź, od razu zapytałbym się „taaaaa, mądralo? A gdzie ja mam niby wysyłać maile, jak nie w serwisie, po np. zapisaniu encji?”. Podpowiem: „Domain Events” :)

    Repozytoria natomiast są samodzielnymi i odrębnymi bytami (w świecie DDD), których zadaniem jest głównie perzystencja obiektów. Nie wtryniają się w logikę biznesową.

    wynika to jednak z faktu, że oba rodzaje obiektów zwykle dostajemy gotowe w ramach jakiegoś frameworku lub innych bibliotek (Doctrine czy kontener DI).

    1. Osobiście nigdy nie zastąpiłbym porządnej fabryki kontenerem IoC.
    Aby lepiej zobrazować o co mi chodzi, zadam pytanie: Umiesz zastąpić buildera kontenerem? No właśnie nie bardzo – po prostu się nie da – a oba (builder i fabryka) robią to samo, tworzą obiekt, który jest na tyle skomplikowany, że nie wystarczy zwykłe new Object(); – z tym, że builder pozwala ingerować w sam proces tego tworzenia.
    Fabryki są częścią infrastruktury i jako takie nie powinny być zastępowane czymś co pochodzi z warstwy aplikacji (a tam rezyduje kontener).

    2. Warstwa dziedziny jest świadoma interfejsu warstwy infrastruktury. Dlatego ja zawsze tworzę w niej (w. dziedziny) przynajmniej interfejsy dla repozytoriów, fabryk i serwisów. Te interfejsy, mogą potem być implementowane w warstwie infrastruktury przez np. repozytoria oparte o Doctrine2, ale też własne klasy z obsługą PDO, etc. – nie polegam tylko na tym co dostarczają mi narzędzia.

    Nadmienię, że kontenera IoC używam już jednak przy pobieraniu fabryk.

    Natomiast uważam, że najistotniejszą rzeczą w całym DDD jest właściwe wymodelowanie klas encji i serwisów. Bez tego, projekt w ddd raczej nie ma szans na szczęśliwy finisz.

    Dla mnie w idealnym projekcie DDD, dziedzina powinna być tak skończenie wymodelowana, że:
    – Każdy kto spojrzy na diagram od razu wie o co „cho”
    – Na podstawie implementacji kodu z tego diagramu można zbudować cała infrastrukturę (stąd te interfejsy)

    Kończę właśnie aplikację do pracy dyplomowej – mały project manager – w którym, aby udowodnić tą tezę: warstwa dziedziny jest zupełnie innym projektem (logicznie: eer, bo jest warstwą dziedziny; fizycznie: mieści się w zupełnie innym repozytorium GIT). Cała warstwę infrastruktury mam już wewnątrz projektu głównej aplikacji (Symfony2 + Doctrine2).

    P.S. Skasuj poprzedni post ;P Zrobiłem mały edit, żeby lepiej się czytało.

  6. Wojciech Soczyński pisze:

    @alan: przekonałeś mnie, chociaż szczerze powiem, że dopiero raczkuje w temacie DDD i oba artykuły które popełniłem, bardziej traktuje jako opis swojego researchu nad tematem. Tym bardziej fajnie jest mieć feedback od kogoś kto ma już pewne doświadczenie w tej dziedzinie. Jeżeli to możliwe, chciałbym zobaczyć Twoją pracę dyplomową wraz z kodem (oczywiście jak skończysz), może pokusisz się o publikacje jej w jakimś fachowym piśmie lub portalu ?
    @michał wujas: mógłbyś podać przykład modelu domeny w którym są tak duże encje ?

  7. Michał Wujas pisze:

    Przykładem „dużej” encji jest sklep w systemie sklepów internetowych. Do encji tej należy wiele obiektów od tych bazujących na rekordzie z bazy danych jak produkty, klienci, zamówienia, magazyn po kontener na pliki, szatę graficzną etc.

    Problem pojawia się gdy implementujemy funkcjonalności typu generacja faktury w pdfie dla klienta indywidualnego, taka czynność wykorzystuje (w kilku procentach) każdy obiekt z przedstawionych powyżej. Jednak przy jej tworzeniu przerost encji sprawia trudność w poruszaniu się po całym ekosystemie, problemy z wydajnością czy zbytnie powiązanie obiektów ze sobą.

    Drugim przykładem jest użytkownik który może pełnić wiele ról jednocześnie (właściciel sklepu, kupujący – w 4 typach, użytkownik poczty, uczestnik programu partnerskiego etc.).
    Można by w danym kontekście pracować na encji „użytkownik poczty” i mieć dostęp tylko do jego kontekstu gdyby nie fakt że dojdzie do scenariusza w którym użytkownik pełni 2-3 funkcje jednocześnie.

  8. Wojciech Soczyński pisze:

    @michal wujas: Hm zastanawia mnie czy taka duża encja jak cały sklep ma w ogóle sens ? Może sensowniejsze było by jakieś inne podejście np zamiast tworzyć ekosystem w którym każdy sklep jest podmiotem aplikacji lepiej by było wydzielić sklep jako osobną aplikację i stworzyć osobną aplikacje do zarządzania nimi. Co do użytkowników to rzeczywiście jest to problematyczne, ciekawy jestem też gdzie leży granica metodologi DDD.

  1. There are no trackbacks for this post yet.

Leave a Reply

Informuj mnie o odpowiedziach poprzez e-mail. Możesz również subskrybować wpis bez zostawiania komentarza.