Domain-Driven Design – Eric Evans (streszczenie) [ZAWIESZONE]

Przedstawiam tutaj streszczenie książki Erica Evansa Domain-Driven Desing Tackling Complexity in the Heart of Software (kolejne rozdziały będą się pojawiały wraz z postępem w czytaniu; na ten moment zawiesiłem czytanie tej książki przez prozaiczny powód – są pozycje na rynku, które mogą wnieść więcej do mojego asortymentu na ten moment niż niniejsza książka).

Part I – Putting the Domian Model to Work

1. Crunching the knowledge

W tym rozdziale autor opisuje jak zgłębiać (dosłownie „miażdżyć”) wiedzę. Na podstawie jego doświadczeń ze zdobywaniem wiedzy na temat płytek drukowanych (Printed-Circuit Board), przedstawia jak przebiega ten proces. Wymaga on z pewnością wielu rozmów z ekspertami z danej dziedziny, na podstawie których następnie podejmowana jest pierwsza próba stworzenia modelu. Lecz jak wspomina autor proces ten to ciągła nauka, co może oczywiście wpływać na model danych.

2. Communication and the Use of Language

Cały rozdział skupia się wokół tak zwanego „wszechobecnego języka” (ubiquitous language). Idea przyświecająca autorowi to to, aby cały zespół mówił wspólnym językiem – zarówno eksperci domenowi, jak i programiści. Powinien on być używany przy okazji każdej rozmowy odnośnie projektu. Język ten powinien być oczywiście odzwierciedlony w kodzie, dokumentacji, diagramach i wszystkich dokumentach dotyczących projektu.

3. Binding Model and Implementation

Rozdział ten opisuje istotność powiązania modelu z implementacją (kodem źródłowym). Najczęstszym problemem w utrzymaniu spójności jest to, że model „ładnie wygląda na papierze” i nie bierze pod uwagę ograniczeń języka programowania. Stąd jeśli model sprawia duże problemy programistom, powinien on zostać zaktualizowany, tak aby programowanie było „naturalne” a model sam w sobie dalej przedstawiał założenia projektu. Autor wskazuje także, że nie wszystkie języki programowania pozwalają na to, żeby korzystać z podejścia Domain-Driven Desing. Języki obiektowe takie jak Java na to pozwalają, lecz na przykład język Fortran już nie (tam domena jest tylko jedna – szeroko pojęta matematyka). Inną wskazówką jaką daje autor jest to, że w danym projekcie powinien być jeden model, na którym opiera się kod. Rozdział kończy stwierdzenie, że aby to wszystko osiągnąć dobrze by było aby każdy z twórców modelu powinien znać przynajmniej podstawy programowania a programiści powinni czynnie uczestniczyć w modelowaniu używając wszechobecnego języka.

Part II – The Building Blocks of a Model-Driven Design

4. Isolating the Domain

W rozdziale tym autor opowiada o podejściu jakie powinno być stosowane aby Domain-Driven Design działało. Przytacza on tutaj pojęcie architektury warstwowej i pokazuje jej cztery elementy: interfejs użytkownika, aplikacja (serwisy, komponenty), domena (logika biznesowa oraz obiekty domenowe), infrastruktura (system operacyjny, baza danych). Według autora idealnym podejściem jest aby wiedza domenowa nie była rozsiana pomiędzy wszystkimi warstwami a była tylko i wyłącznie w warstwie domeny. Dodatkowo każda z warstw powinna polegać tylko i wyłącznie na warstwie leżącej niżej (warstwy zostały podane w kolejności od najwyższej do najniższej). Autor wspomina także o wzorcu „The Smart UI” (w rzeczywistości określa go antywzorcem), który to nie może zostać zastosowany w aplikacjach opartych o podejście Domain-Driven Design.

5. A model Expressed in Software

We wstępie autor opisuje o czym będzie rozdział (będzie o konkretnych wzorcach) oraz o powiązaniach między obiektami. Według niego powinniśmy patrzeć na nie (powiązania) ze szczególną uwagą i dostosować je do modelu biznesowego. Po wstępie opisywane są kolejne wzorce:
Entities – są to obiekty, których najbardziej istotnym elementem jest ich „tożsamość” (ang. identity) – czyli element dzięki któremu wiemy, że obiekt A i obiekt B to to same obiekty, mimo że ich atrybuty się różnią. Przykładowo George Bush w wieku lat 15 i w wieku lat 35 to wciąż ta sama osoba (ta sama tożsamość), chodź już na przykład atrybut wiek będzie inny pomiędzy dwoma obiektami. Może też tak być, że mamy dwie osoby o takich samych atrybutach (na przykład dwie osoby o imieniu i nazwisku George Bush) a w rzeczywistości są to inne osoby. Stąd nie tożsamość nie powinna być oparta o atrybuty, tylko o jakiś specjalny atrybut zapewniający unikalność lub też zestaw atrybutów zapewniający unikalność.
Value Objects – w odróżnieniu do entities, te nie posiadają tożsamości (jakiegoś rodzaju identyfikatora, są rozróżniane za pomocą atrybutów) – reprezentują proste dane takie jak np. kolor, waluta czy też data. Autor zaleca aby value objects były niezmiennikami (ang. immutable objects) oraz żeby nie miały powiązań dwustronnych (A <-> B; value object A ma powiązanie do B i value object B ma powiązanie do A). Dzięki temu możliwe są różnego rodzaje optymalizacje takie jak na przykład nie tworzenie za każdym razem nowego obiektu, jeśli jego atrybuty są takie same.
Services – czasami pojawiają się operacje nie związane z konkretnym typem obiektów, w takim wypadku rozwiązaniem są serwisy. Autor nawiązuje tutaj do serwisów jakie istnieją w naszym otoczeniu (jak na przykład serwis samochodowy). Opisane są trzy charakterystyki czyniące serwis, dobrym: operacje (serwisu) nie przynależą do Entitties lub Value Objects, interfejs jest zbudowany na podstawie elementów modelu domeny, operacje są bezstanowe. Autor także podkreśla fakt potrzeby rozróżnienia serwisów pomiędzy tymi przynależnymi do warstwy aplikacji, warstwy infrastruktury jak i warstwy domenowej – każdy serwis powinien przynależeć do konkretnej warstwy.
Modules – jak opisuje to autor, są starym, uznanym elementem w projektowaniu. Lecz w podejściu Domain-Driven Design moduły mają znaczenie w całym modelu (co nie zawsze jest tak oczywiste). Największą bolączką modułów jest trudność ich refaktoryzacji, a w początkowych etapach projektu często podejmowane są niepoprawne decyzje przez co refaktoryzacja jest koniecznością. Stąd w podejściu DDD powinniśmy być otwarci na refaktoryzację także na poziomie modułów. Autor często wspomina o tym, że powinniśmy się kierować dwoma przesłankami podczas budowy/rozbudowy modułów: moduły powinny być spójne oraz powinny się cechować małym powiązaniem z innymi modułami. Następnie opisany jest problem Javy z modułami – w Javie nie ma modułów i należy odpowiednio zarządzać pakietami. Według autora trzeba uważać na to, żeby infrastruktura/frameworki nie wymuszały na nas rozrzucania jednego logicznego modelu pomiędzy wieloma klasami z różnych modułów (przytaczany jest przykład J2EE).
Koniec rozdziału opisuje paradygmaty modelowania. Tutaj główny nacisk jest położony na podejście obiektowe, jako że jest ono już dobrze ugruntowane, warto z niego korzystać (zamiast wykorzystywać nowinki jeszcze dobrze nie sprawdzone) i opierać na nim aplikacje. Oczywiście w konkretnych sytuacjach warto skorzystać z innych paradygmatów, takich jak na przykład silniki reguł (rules engines), aczkolwiek należy przy tym pamiętać, że dalej powinniśmy się opierać o „wszechobecny język” (oraz o zdrowy rozsądek – nie ma sensu zaprzęgać całego silnika aby rozwiązać dosyć prosty problem).

6. The Life Cycle of a Domain Object

W tym rozdziale autor skupia się na wzorcach, związanych z cyklem życia obiektów, takich jak: agregaty, fabryki i repozytoria.
Agregaty (aggregates) – autor rozpoczyna podrozdział od problemu skomplikowanych relacji pomiędzy obiektami a co często za tym idzie skomplikowanych transakcji (gdy musimy usuwać dzieci, dzieci dzieci i tak dalej) prowadzących dalej do spowolnienia systemu. Zaproponowanym rozwiązaniem są właśnie agregaty, które to stanowią pewnego rodzaju grupy obiektów mocno skorelowanych ze sobą i posiadających jednego reprezentanta grupy – aggregate root. W rozdziale są opisane podstawowe zasady tworzenia agregatów, takie jak:
1. Aggregate root jest najczęściej encją (entity) i jest on reprezentantem, który posiada globalny identyfikator. Dodatkowo to on odpowiada za sprawdzanie tak zwanych niezmienników (są to reguły biznesowe typu „każdy samochód musi posiadać 4 koła”).
2. Elementy (Entity lub Value Object) znajdujące się wewnątrz agregatów posiadają identyfikatory unikalne wewnątrz agregatu.
3. Do elementów wewnątrz agregatów ma dostęp tylko i wyłącznie aggregate root. Może on (aggregate root) w określonych przypadkach przekazywać elementy wewnętrzne dalej (upubliczniać je), ale najczęściej dzieje się to poprzez stworzenie kopii.
4. Z bazy danych możemy jedynie dostać aggregate root, do innych obiektów można się dostać tylko za pomocą powiązań.
5. Obiekty składające się na agregat mogą zawierać referencje do innych aggregate root.
6. Operacja usunięcia musi obejmować cały agregat.
7. Jeśli następuje jakakolwiek zmiana w agregacie, wszystkie niezmienniki muszą zostać zachowane (patrz punkt pierwszy).
Zakończenie podrozdziału to przykład oparty o przejście od „standardowego” podejścia do agregatu (w ramach problemu podejmującego zamówienia).
Fabryki (factories) – czasami stworzenie obiektu jest dosyć złożonym procesem, przykładem może być tutaj składanie silnika. W takich przypadkach, dobrym rozwiązaniem jest fabryka i do tego namawia autor, żeby skomplikowane tworzenie obiektów cedować na fabryki zamiast umieszczać całą logikę w konstruktorze. Fabryki, jako takie, nie są elementami modelu DDD, lecz są pewnego rodzaju pomocnikami. Wcześniej wspomniane agregaty są naturalnymi kandydatami do tego, aby były tworzone za pomocą fabryk. Autor wskazuje na cechy jakimi powinny się cechować fabryki, i są to:
1. Atomowość operacji – cały obiekt jest tworzony za pomocą pojedynczego wywołania fabryki.
2. Obiekty zwracane przez fabrykę powinny raczej być abstrakcją tego co jest tworzone, raczej niż konkretnymi klasami.
Należy także zwrócić uwagę na to, żeby nie przesadzić – nie zawsze potrzebna jest fabryka, a w szczególności:
1. Jeśli klasa jest zwykłym typem nie biorącym udziału w dziedziczeniu.
2. Dla klienta ważny jest wybór konkretnej implementacji i nie chciałby, żeby fabryka robiła to za niego (dobrym przykładem są kolekcje w Javie).
3. Klient dostarcza wszystkie argumenty potrzebne do stworzenia obiektu (bez sensu jest zamienić new TestObject(a,b,c,d) na Factory.create(a,b,c,d) ).
4. W końcu gdy konstrukcja jest banalnie prosta.
Rodzi się też pytanie o niezmienniki – czy powinny one być zachowywane przez fabrykę czy obiekt sam w sobie? W większości przypadków niezmienniki powinny być sprawdzane przez obiekty same w sobie, jako że mogą one zmieniać stan w trakcie działania aplikacji (np. entity) i wtedy trzeba by było pytać się fabryki czy jest dalej ok – brzmi dziwnie. Czasami jednak, takie rzeczy jak unikalność identyfikatorów w ramach agregatu może być odpowiedzialnością fabryki.
Fabryki dotyczące entity oraz value objectów powinny się nieco różnić – fabryka value objectów będzie przyjmowała z pewnością wszystkie argumenty mogące wpłynąć na ostateczny wygląd takiego obiektu, gdzie fabryka dla entity będzie przyjmowała tylko niezbędne elementy potrzebne do stworzenia entity/agregatu.
Następne zagadnienie, którym może zajmować się fabryka to rekonstrukcja obiektów (sytuacja gdy wyciągamy obiekt z bazy danych lub innego źródła i chcemy w nie tchnąć życie). W tym kontekście fabryka powinna:
1. Nie nadawać nowego identyfikatora obiektu – powinna użyć już nadanego (odczytanego z bazy lub innego źródła).
2. Powinna pozwolić na odczyt niepoprawnych danych – jeśli odczytany obiekt łamie niezmienniki to i tak powinien zostać odczytany jako że jest już w źródle danych. Oczywiście należy się tutaj zatroszczyć o rozwiązanie tego problemu, co nie jest łatwym zadaniem.
Repozytoria (repositories) – rozdział zaczyna się od pewnego rodzaju nakreślenia problemu związanego z repozytoriami . Autor następnie przypomina o tym, że powinniśmy się opierać na agregatach, nawet w kontekście bazy danych – chodzi o to, żeby z poziomu repozytorium był możliwy dostęp tylko do root agragate (nie powinno być możliwe bezpośrednie wyciąganie wewnętrznych elementów agregatu. W dalszej części opisane są podstawy działania repozytoriów – stworzenie odpowiednich metod dostępu, zapisu czy też możliwości wyszukiwania za pomocą kryteriów. Po omówieniu podstawowych metod, autor skupia się na chwilę, na odpytywaniu repozytorium i tutaj przedstawione są dwie techniki. Jedna z nich to zapisanie na stałe zapytania do bazy danych (oczywiście w repozytorium), a drugie (bardziej eleganckie) to posługiwanie się kryteriami – specjalnie stworzoną abstrakcją, pozwalającą na zadanie odpowiedniego zapytania wykorzystując do tego obiektowość. Jak wcześniej już wspomniano – klient nie powinien mieć bezpośredniego dostępu do elementów agregatów, tak samo nie powinien się zastanawiać nad implementacją repozytorium (nie powinno go to obchodzić). Lecz z drugiej strony deweloper piszący repozytorium powinien dbać o odpowiednią wydajność – przykładowo nie pobierać wszystkich kolumn z bazy danych, żeby obliczyć ilość rekordów. W tym momencie pojawia się kilka podpowiedzi odnośnie implementacji repozytoriów:
1. Jeśli to możliwe, spróbuj zwracać typy abstrakcyjne a nie konkretne klasy (opierając się o relacyjną bazę danych może to być trudne). Lub też repozytorium nie musi dotyczyć jednego obiektu, lecz grupy zbliżonych do siebie obiektów.
2. Jeśli możesz to wykorzystaj buforowanie (caching).
3. Kontrolę transakcji pozostaw klientowi – to najczęściej tam jest logika biznesowa, która tym powinna sterować.
Dodatkową rzeczą, którą warto wziąć pod uwagę jest to, że często aplikacje są oparte o jakiś framework (jak na przykład Hiberanate czy Spring). Autor odradza „walkę z frameworkiem”, raczej zaleca próbę dostosowania się do tego jak działa framework, zamiast tworzenia na siłę swoich repozytoriów. W następnej części omawiane są relacji pomiędzy fabrykami a repozytoriami – i tak, repozytorium może wykorzystywać fabrykę do stworzenia obiektu (w momencie gdy dane są wyciągane z bazy danych). Ale klient też może skorzystać z fabryki w celu przygotowania obiektu do zapisu przez repozytorium. Zakończenie opisu repozytoriów stanowi krótki opis projektowania obiektów w sytuacji gdy baza danych to baza relacyjna. Wyróżnione są tutaj trzy przypadki:
1. Baza danych służy głównie jako miejsce zapisu obiektów – w takim przypadku obiekty nie powinny się mocno różnić od rekordów w bazie danych (nawet gdy wymaga tego naruszenie poziomów normalizacji)
2. Baza danych została zaprojektowana dla innego systemu (w początkowych założeniach projektu nie było mowy o obiektach) – tutaj można pójść w dwie strony – spróbować dostosować model do zastanej bazy danych lub też stworzyć inny i wspierać dwa modele.
3. Baza danych służy głównie jako miejsce zapisu obiektów ale ma także inne zadania – w takich przypadkach trudnym, ale dobrym rozwiązaniem jest próba podziału bazy danych na część odpowiadającą za zapis obiektów i tą która służy innym celom.