Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Simon says...

Szymon Pobiega o architekturze i inżynierii oprogramowania
[PL] Mentoring DDD: Powstawanie modelu
Na wstępie chciałbym zaznaczyć, że w żadnym razie nie jestem ekspertem od DDD. Ja się wciąż i dopiero uczę i moje posty z serii "Mentoring DDD" są wyrazami tegoż procesu (samo)kształcenia. Dlaczego więc "mentoring"? Ano dlatego, że wspieram Domain Driven Design w mojej firmie poprzez konsultacje, pomoc duchową i cielesną. Staram się przewidywać problemy i zdobywać wiedzę na temat sposobów ich rozwiązania zanim się pojawią. Nie posiadam jednak (jeszcze) tej wiedzy w postaci przetworzonej w trwałe połączenia między neuronami. Z tego powodu jeśli macie jakiekolwiek uwagi na temat tego, o czym piszę: coś jest nie tak, z czymś się nie zgadzacie itp. to bardzo proszę o (krytyczne) komentarze.

Model anemiczny


Początki wdrażania DDD w firmie/projekcie wiążą się często z pojęciem anemiecznego modelu domeny. Kiedy próbujemy kogoś przekonać do DDD, ale nie umiemy dobrze wyjaśnić sensu tej metodyki, efektem są właśnie anemiczne modele. Zjawisko to może także być skutkiem niechęci osób zaangażowanych do stosowania tego "nowego dziwnego podejcia". Poniższy diagram jest przykładem anemicznego modelu domeny.



jego anemiczność bierze się z następujących cech:
  • brak definicji metod
  • brak opisu charakteru poszczególnych obiektów (encja, obiekt reprezentujący wartość)
  • brak podziału na agregaty
Brak definicji metod skutkuje niezdefiniowanym zachowaniem modelu. Efektem tego jest brak wiedzy na temat dynamiki relacji pomiędzy jego elementami. Kto przypina Adres do Jednostki? Kto dołącza jednostkę do Hierarchii? Jaki jest zewnętrzny interfejs modelu? Na te wszystkie pytania powinien odpowiadać dobry diagram modelu domeny.

Brak opisu charakteru poszczególnych obiektów stanowi kolejny problem na drodze do zrozumienia dynamiki modelu. Nie mamy bowiem informacji o tym, które obiekty na diagramie mają własną tożsamość, a które są jedynie kontenerami na dane. Czy adres jest encją? Na pewno w pewnych sytuacjach mógłby, ale czy w tej przedstawionej na diagramie? Jeśli autor (jak w tym wypadku) wprowadził rozróżnienie między relacją agregacji, a kompozycją, możemy wyciągnąć na tej podstawie pewne wnioski: kompozycja sugeruje, że obiekt zawierany nie ma własnej tożsamości. Jest to jednak tylko sugestia, a nie twarda reguła.

Brak podziału na agregaty powoduje problemy podczas próby implementacji kodu wykonującego operacje bazodanowe na modelu domeny. Nie wiemy bowiem, w jaki sposób powinno wyglądać kaskadowanie zapisu/usuwania w ramach poszczególnych relacji. Czy dodanie Relacji do kolekcji w obiekcie Hierarchii powinno skutkować jej zapisaniem do bazy danych? Czy może trzeba osobno wywołać Save dla tej Relacji? Czy mogę odwołać się do Adresu korzystając z jego identyfikatora (klucza głównego)?

Są to klasyczne problemy, z którymi wielokrotnie zetknąłem się w moich zmaganiach z DDD. O ile brak metod na diagramie jest dosyć oczywisty, o tyle koncepcja rozróżniania encji od obiektów wartości oraz koncepcja agregatów wymaga przyswojenia sobie pewnej dawki wiedzy teoretycznej.

Odżywianie modelu anemicznego


Mając dany model anemiczny, możemy spróbować go odżywić. Dlaczego w ogóle mówię "mając dany"? Ano dlatego, że często faza szkicowania diagramów na kartkach skutkuje takim modelem. Tyle, że podczas szkicowania intuicyjnie "czujemy" dynamikę naszego obrazka. Teraz jedyne, co musimy zrobić, to zakodować to "uczucie" w sposób zgodny ze specyfikacją UML. Efektem tego może być na przykład diagram taki jak poniższy.



Staramy się jak największą część informacji przekazać w standardowy, UML-owy sposób. Niejasności i sugestie możemy wyrazić za pomocą komentarzy. Prześledźmy po kolei wszystkie dodatkowe fakty dotyczące naszego modelu, jakie udało się przekazać tym diagramem.

Najbardziej rzucają się w oczy definicje agregatów. Są cztery. Najmniejszy, czwarty agregat zawierający jedynie klasę UnitType nie został odgrodzony granicą. Agregowanie Jednostki i Adresu oraz oznaczenie Jednostki stereotypem "aggregate root" informuje o tym, że tylko obiekty klasy Unit możemy bezpośrednio pobierać z bazy danych. Do obiektu Adresu możemy się dostać jedynie poprzez odwołanie do odpowiedniego property Jednostki.

Będąc przy Adresie, zauważamy, że został on oznaczony stereotypem "value object". Oznacza to, że obiekt ten nie ma własnej tożsamości oraz że dwa Adresy posiadające identyczne dane są mogą być uważane za identyczne. Nie ma więc czegoś takiego jak unikalny identyfikator Adresu. Dzięki temu możemy dane zwiazane z adresem osadzić bezpośrednio w tabeli przechowującej dane jednostek. Konsekwencją takiego postępowania jest przyspieszenie wszelkich operacji na obiektach Jednostki (o jedno zapytanie / komendę mniej).

Wracając do agregatów... Agregat osoby jest trywialny. Ciekawsze są agregaty związane z obiektami Hierarchii. Tutaj także występuje korzeń oraz obiekt "value object", z tym, że w tym wypadku obiekty wartości (Relacje) zawarte są w kolekcji. Informacja o tym, że Relacje nie mają własnej tożsamości pozwala nam dokonać pewnej optymalizacji jeśli chodzi o mapowanie NHibernate - wykorzystać kolekcję komponentów.

Własności "Master" oraz "Slave" w klasie Relacji zostały, tak jak i cała klasa (czego niestety nie widać), oznaczone jako "internal". Wzmacniamy w ten sposób komunikat, że są to obiekty do wyłącznego użycia przez Hierarchię i nie powinny być dostępne na zewnątrz modelu.

Destylacja modelu


Ponieważ nie znam lepszego odpowiednika dla angielskiego "refining the model", posłużę się polskim "destylacja". Wydaje mi się, że oddaje ono lepiej sens tego, co czynimy z modelem, niż klasyczne "refaktoryzacja". Destylacja ma na celu pogłębienie wiedzy o problemie, czego odzwierciedleniem jest model lepiej odwzorowujący rzeczywistość lub bardziej użyteczny dla programisty.

Destylacja zwykle nie rozpoczyna się sama z siebie. Musi zaistnieć jakiś czynnik napędowy. W naszym wypadku takim czynnikiem było lepsze zrozumienie wymagania dotyczącego przypisania pracownika do jednostki. Okzało się, że przypisanie to powinno być zrealizowane w ramach pewnej hierarchii. Czyli, że pracownik może pracować w wielu jednostkach - w ramach każdej hierarchii w innej jednostce.

Zmiana ta spowodowała konieczność rozszerzenia obiektu Relacji między jednostkami o referencję do kolekcji Osób - pracowników Jednostki podrzędnej danej relacji. To jeszcze nic strasznego, dało się tę zmianę wprowadzić w starym modelu. Ciekawiej zrobiło się, gdy przeanalizowaliśmy, w jaki sposób metody warstwy usługowej korzystają z modelu.

Wniosek był prosty i można go wyprowadzić już z samego diagramu: do każdej operacji na Hierarchii niezbędne jest wczytanie do pamięci wszystkich relacji między elementami tej hierarchii. Do pobrania elementów podrzędnych danego elementu niezbędne jest pobranie całej definicji Hierarchii. I tak dalej, i tak dalej. Podsumowując: nasz model jest całkowicie niewydajny, jeśli chodzi o dostęp do danych. Chyba zapomnieliśmy, że mamy jakąś bazę danych, w ogóle...

Wracamy więc do kartki i ołówka i zastanawiamy się, co można zmienić, aby model był implementowalny. Najlepiej samą koncepcję. Jeśli poprzedni model był zorientowany na Hierarchię, spróbujmy zbudować model zorientowany na coś innego. Na przykład na Element Hierarchii. Zamiast traktować hierarchię jako jeden wielki obiekt zarządzający swoimi elementami, spróbujmy wzmocnić pojęcie Elementu rozpraszając kontrolę nad całością hierarchii do poszczególnych obiektów składowych.

Pozwalając Elementowi decydować o tym, kto jest jego obiektem nadrzędnym oraz czy jest korzeniem hierarchii, efektywnie zwalniamy się z konieczności ładowania do pamięci ogromnej ilości danych. Zauważmy, że w poprzedniej wersji metoda "AddRelation" Hierarchii ma dwa argumenty: jednostkę nadrzędną i podrzędną. W nowej wersji metoda "SetSuperior" wymaga obiektu Hierarchii oraz nowego elementu nadrzędnego. Obiekty biorące udział w operacji są takie same! Zmieniło się tylko przypisanie odpowiedzialności. Wniosek jest prosty: unikajmy "ciężkich" obiektów zarządzających kolekcjami "lekkich". Zamiast tego wzmacniajmy te "lekkie" elementy kolekcji tak, aby same były w stanie zarządzać sobą i swoim otoczeniem. Oto lokalizacja odpowiedzialności.

Ostatnim krokiem destylacji jest wydzielenie wspólnych zachowań. W tym wypadku łatwo zauważyć, że wspólne dla części domeny odpowiedzialnej za Osoby oraz części odpowiedzialnej za Jednostki jest pojęcie Hierarchii. Dzięki opiewanemu przeze mnie wcześniej wsparciu NHibernate dla typów generycznych, możemy ostatecznie stworzyć coś takiego:



Na samej górze diagramu widoczna jest klasa bazowa dla wszystkich elementów modelu domeny (wzorzec Layer Supertype). Poniżej znajduje się pas trzech klas, które stanowią sedno modelu. To właśnie związki między klasami reprezentującymi Hierarchię, Element oraz Relację zapewniają wymaganą funkcjonalność. Pozostałe klasy (umieszczone poniżej) stanowią jedynie konkretne realizacje wspomnianych związków.

W tym modelu poprzez dziedziczenie klas Jednostki i Osoby z klasy Element wyrażamy fakt, że najważniejszą cechą wspomnianych dwóch obiektów w kontekście realizowanego projektu jest bycie elementem hierarchii.

Szybkie wnioski


Na koniec postanowiłem zamieścić wnioski z powyższego, nieco przydługiego, tekstu, w formie krótkiej listy:
  1. Model należy dobrze odżywić, aby nie był anemiczny
  2. Informacja o charakterze obiektu (encja, obiekt "value object") jest częścią modelu
  3. Dobre rozróżnienie agregacji i kompozycji jest bardzo pomocne przy określaniu granic agregatów i budowie mapowań NHibernate
  4. Model musi posiadać interfejs użyteczny dla warstw wyższych
  5. Model musi być wydajny w kontekście planowanej technologii użycia
  6. Lokalizacja odpowiedzialności pomaga ograniczyć zapotrzebowanie na dane
  7. Destylacja modelu jest procesem niezbędnym i nie należy oczekiwać, że pierwsza wersja modelu będzie dobra

Bibliografia


  1. Fowler, Martin. Patterns of Enterprise Application Architecture. s.l. : Addison-Wesley, 2003.
  2. Nilsson, Jimmy. Applying Domain-Driven Design and Patterns. s.l. : Addison-Wesley, 2008.
  3. Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. s.l. : Addison-Wesley, 2004.

Opublikowane 21 czerwca 2009 13:52 przez simon

Komentarze:

# Simon says... : [PL] Mentoring DDD: Powstawanie modelu @ 21 czerwca 2009 18:57

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

dotnetomaniak.pl

# re: [PL] Mentoring DDD: Powstawanie modelu @ 22 czerwca 2009 09:08

Całkiem sympatycznie zaczyna to wyglądać. Mam nadzieje ze pokażesz kiedys też pliki mapowań od NH ?

rod

# re: [PL] Mentoring DDD: Powstawanie modelu @ 22 czerwca 2009 11:44

bardzo fajny artykul, pracuje z DDD juz jakis czas i zawsze ciesza nowe przyklady, podejscia i dobre praktyki

jaroosh

# re: [PL] Mentoring DDD: Powstawanie modelu @ 25 czerwca 2009 16:41

Bardzo fajny artykuł - mam pytanie czy spotkałeś się z problemem typów nullowalnych w nHibernate? Praktyka ze stosowanie typów hibernate jest b. slaba ...

K1

# re: [PL] Mentoring DDD: Powstawanie modelu @ 25 czerwca 2009 22:24

@rod @jaroosh @K1

Dzięki:) Cieszę się, że Wam się podobało.

@rod

Jeśli chodzi o pliki mapowań to z tym może być problem, jako że są to już typowe dokumenty będące własnością intelektualną mojej firmy. Postaram się jednak nieco je przerobić, żeby pokazać co ciekawe sztuczki. Póki co odsyłam do bloga Ayende, który jakiś czas temu całkiem ciekawy kurs mapowań NHibernate prowadził. Także na CodeBetter był jakiś czas temu kurs NHib

@K1

Nie miałem nigdy problemów z typami nullowalnymi w NHib. Stosowałem praktycznie od początku Nullable<T> z wszelkiej maści intami, bajtami i enumami i nie miałem z tym problemów. Nie używałem nigdy tej biblioteki do nullowalnych typów wbudowanej w NHib, albowiem kiedy zaczynałem swoja przygodę z NHib, była ona już deprecated.

simon

# Agregacja: powinni tego zabronić? @ 8 lipca 2009 18:37

Czytając Agile: Principles, Patterns and Practices natknąłem się (dla posiadaczy książki - na stronie

Simon says...

# [PL] Mentoring DDD: O agregacji i kompozycji (znowu) oraz asocjacji. @ 27 lipca 2009 15:42

Agregacja (a.) i kompozycja (k.) są jednymi z najczęściej wykorzystywanych relacji w UML-owych diagramach

Simon says...

Komentarze anonimowe wyłączone