Tworzenie nowej aplikacji

Jaki jest pierwszy krok przy rozpoczęciu pracy nad nową aplikacją biznesową? Czytamy specyfikacje i sprawdzamy jakie są wymagania funkcjonalne. Rozbijamy zadania na mniejsze. W większości przypadków celem takiego podziału jest oszacowanie potrzebnego czasu pracy. W kolejnym kroku dzielimy pracę pomiędzy poszczególnych członków zespołu. Projektujemy strukturę bazy danych – czasem przez lidera zespołu lub wskazanego programistę. Przystępujemy do pisania właściwego kodu.

Czy jest coś złego w tym podejściu? Do tej pory robiliśmy wszystko dobrze! Prawda?
Odpowiedź brzmi: tak i nie! Dobrze, że przystąpiliśmy do pracy. Źle, że nie bierzemy pod uwagę zarządzania i rozwoju naszych projektów.
Pomyśl o wszystkich projektach nad którymi pracowałeś na przestrzeni lat używając tradycyjnego podejścia. Czy napotkałeś, któryś z poniższych problemów?

  1. Twój projekt miał te sam funkcje realizowane w różnych miejscach.
  2. Masz więcej niż jeden obiekt dla tego samego elementu.
  3. Posiadasz obiekt, który ma różne właściwości, które nie są w rzeczywistości atrybutami tego obiektu.
  4. Posiadasz brak lub bardzo słabą zależność pomiędzy powiązanymi elementami.
  5. Patrząc na swoje obiekty nie jest możliwe, aby zrozumieć co tak naprawdę robi cała aplikacja.

Jestem pewien, że zmagałeś się z tymi problemami wielokrotnie. Ale wiesz dlaczego? Powód jest prosty, tradycyjnie podejście nie prowadzi nas przez projektowanie systemu w sposób Up to Bottom (z góry na dół) – zaczynamy od generalnych założeń a następnie skupiamy się na szczegółach. Zachęca do wybrania odwrotnego podejścia, tj. Bottom-Up - zaczynamy od założeń szczegółowych a dopiero później skupiamy się na założeniach generalnych. Zostańmy na chwilę w tym punkcie. Co należy wiedzieć podczas projektowania systemu? Jaki jest cel naszego klienta? Zaczynamy od ogółu a następnie pojawiają się liczne, mniejsze funkcjonalności naszego systemu, które pozwolą na osiągnięcie wszystkich założeń.

Kiedy jednak zaczynamy nasz projekt od szczegółów nie mamy do końca pewności jak będą one używane z wyższych poziomów naszej aplikacji oraz jak będzie ich pełna funkcjonalność.

Czy kiedykolwiek słyszałeś, żeby programista z Twojego zespołu mówił, że nie ma wiedzy o funkcjonowaniu całej aplikacji? Prawdopodobnie tak! Myślę, że można zrozumieć przyczyny. Projekt aplikacji nie odzwierciedla wiernie rzeczywistości. W takim wypadku każdy programista specjalizuje się w części projektu nad którą pracuje. I to niestety jest przykra rzeczywistość.

Czy w takim wypadku należy odrzucić podejście w którym zaczynamy od utworzenia bazy danych? Nie do końca! Jeżeli jednak mamy przygotować bardzo skomplikowany projekt aplikacji, podejście Bottom-Up może nie pozwolić nam na zaprojektowanie odpowiedniego modelu obiektowego.

Jakie jest zatem rozwiązanie?
Rozwiązaniem tym jest DDD – Domain Driven Development.


Wprowadzenie

Domain Driven Development nie jest technologią ani metodologią. Dostarcza nam praktyki oraz terminologię do podejmowania odpowiednich kroków projektowych oraz pozwala na skoncentrowanie się nad przyśpieszeniem procesu wytwarzania skomplikowanych aplikacji biznesowych.


Konepcje do omówienia

  1. Zrozumienie domeny.
  2. Ubiquitous Language.
  3. Contexts oraz Bounded Contexts.
  4. Entities oraz Value Objects.
  5. Aggregates oraz Aggregate Roots.
  6. Persistence Ignorance.
  7. Repository.
  8. Domain Service.

W tym artykule skupimy się na przejściu przez różne koncepcje w porównaniu do świata rzeczywistego – nie będziemy wchodzić w techniczne szczegóły. Postaram się również nie prezentować żadnego kodu. Zrozumienie pojęć jest tutaj najważniejsze, wdrożenie będzie dużo łatwiejsze. Najtrudniejsze to zmienić proces swojego myślenia.


Zrozumienie domeny

Zgodnie z definicją z Wikipedii: „jest to podejście do tworzenia oprogramowania kładące nacisk na takie definiowanie obiektów i komponentów systemu oraz ich zachowań, aby wiernie odzwierciedlały rzeczywistość. Dopiero po utworzeniu takiego modelu należy rozważyć zagadnienia związane z techniczną realizacją.”

Skupmy się teraz na przykładzie z rzeczywistości. Załóżmy, że jesteś zaangażowany w zaprojektowanie budynku. Wymagania są następujące:

  • Określona wielkość działki przeznaczona na budowę.
  • Budynek będzie miał 6 pięter.
  • Każde piętro będzie miało 4 apartamenty.

Co jest domeną w powyższym przykładzie?
Domeną jest budynek(?). Należy jednak pamiętać, że jeżeli rozważamy budynek jako naszą domenę nie możemy zapomnieć o kilku dodatkowych wymaganiach. Budynek, który mamy zaprojektować ma składać się z apartamentów w których będą mieszkać ludzie. Dlatego też pojęcie „budynku” powoduje, że pominęliśmy kilka detali. Jeżeli nasz domenę zawęzimy do „budynku mieszkalnego” będzie to poprawna definicja.

Jeżeli teraz przejdziemy do rozmowy z inżynierami budowy, którzy będą zaangażowani w realizację projektu okaże się, że pojęcie „budynku mieszkalnego” jest dużo bardziej zrozumiałe niż sam „budynek”. Mam nadzieje, że zauważyliście małą zmianę w naszym języku. Wykonawca mówi Ci o zaprojektowaniu budynku w którym będą 4 apartamenty na każdym z 6-ciu pięter. Jeżeli teraz wyślesz inżynierów na plac budowy mogą oni pominąć istotne wymagania związane z zaprojektowaniem budynku mieszkalnego. Jeżeli jednak użyjemy pojęcie „budynku mieszkalnego” będzie to zdecydowanie bardziej jednoznaczne i pozwoli na podjęcie prawidłowych decyzji.
Przejdźmy teraz do kolejnego konceptu jakim jest „Wszędobylski język”.


Ubiquitous Language

Koncepcja jest prosta, zarówno deweloper jak i firma powinny dzielić wspólny język, który będzie zrozumiały dla obu stron. Co równie ważne, język taki powinien być nastawiony na terminologię biznesową a nie techniczną.

Przykład 1
Niepoprawny język: Stosunek długości do szerokości małych pokojów powinien być jak 4:3.
Poprawny język: Długość pokojów dziecięcych będzie wynosiła 5 metrów a szerokość 3.5 metra.
Warto zauważyć, że pojęcia jak „mały pokój” czy „stosunek” są pojęciami bardzo technicznymi. Dlatego definicja pokoju dziecięcego czy sypialni oraz zdefiniowanie dokładnych rozmiarów są dużo bardziej zrozumiałe dla każdej ze stron.


Contexts oraz Bounded Contexts

Bounded Context - ograniczony kontekst, może być rozważany jako miniaturowa aplikacja, która zawiera swoją własną domenę, własny kod oraz trwałe mechanizmy. Wewnątrz ograniczonego kontekstu powinna być logiczna spójność, każdy ograniczony kontekst powinien być niezależny od każdego innego ograniczonego kontekstu.

Przykład:
System e-Commerce. Początkowo można powiedzieć, że to aplikacja przeznaczona do dokonywania zakupów. Ale jeżeli spojrzymy z bliższej perspektywy możemy zauważyć również inne konteksty: magazyn, dostawy czy rachunki.

Dzielenie dużych aplikacji na ograniczone konteksty powinno pozwolić na bardziej modularną budowę aplikacji, która będzie łatwiejsza w zarządzaniu i rozwoju. Każdy z tych ograniczonych kontekstów ma swoją własną funkcjonalność i może pracować niezależnie od pozostałych modułów. Poprzez taki podział bardziej oczywistym staje się gdzie powinna znajdować się logika odpowiedzialna za daną funkcjonalność. Dzięki temu można również uniknąć BBOM (Big ball of mud, tzw. duża kula błota).


Czym zatem jest BBOM?

Jest to niepoprawnie zaprojektowana aplikacja, chaotycznie zorganizowany z kodem ciągnącym się w nieskończoność. Systemy takie przysparzają wielu problemów z zarządzaniem, nie mówiąc już o rozbudowanie takich projektów. Ważne informację są powielane w wielu miejscach, często są wystawione dla wszystkich klas. Ogólna struktura takiego projektu nie jest dobrze zdefiniowana. Jest to coś czego zdecydowanie powinniśmy unikać!

Wróćmy na chwilę do projektu naszego „budynku mieszkalnego”. Składa się on z kilku ograniczonych kontekstów:

  • dostarczenie energii;
  • miejsce parkingowe dla samochodu;
  • apartament;
  • itd.
Skupmy się na apartamencie. Jest to z zasadzie połączenie różnych pomieszczeń. Pokoje są wyposażone w różne elementy wnętrza takie jak okna czy drzwi. Zadajmy sobie teraz dwa pytania:
  1. Możecie wyobrazić sobie pokój bez okien?
  2. Czy okno ma tożsamość bez pokoju w którym się znajduje?
Odpowiedzią na te pytania są kolejne koncepcje Domain Driven Development:
  • Entity;
  • Value Object;
  • Aggregates & Aggregate root.


Entity

„To jest moja encja, istnieje wiele podobnych, ale ta jest moja.”

Kluczową cechą charakterystyczną encji jest jej tożsamość – jest unikalna w obrębie systemu a żadna inna encja, nie ważne jak bardzo podobna nie jest tą samą encją chyba, że ma taką samą tożsamość.

Przykład:

  1. Twoja sypialnia w apartamencie.
  2. Kontakt na Facebook’u.
  3. Artykuł na tej stronie.


Value Object

Kluczową cechą obiektów wartościowych jest to, że nie mają tożsamości. Jest to pewne uproszczenie ale cechą takich obiektów jest reprezentowanie czegoś tylko przez jego atrybut. Dwa obiekty mogą mieć identyczne wartości atrybutów, w tym przypadku są identyczne. Nie mają one jednakże innej wartości niż tej na podstawie ich atrybutów. Innym wspólnym aspektem obiektów wartościowych jest to, że powinny być niezmienne, po utworzeniu nie mogą być zmienione.

Przykład:

  1. Okna w pokojach.
  2. Adres osób zarejestrowanych na stronie internetowej.
  3. Kryteria wyszukiwania w wyszukiwarce.

Adnotacja: Obiekt wartościowy może stać się encją w zależności od sytuacji. Możesz znaleźć takich scenariusz? Jeżeli wymagania funkcjonalne wyszukiwarki każą zapisywać kryteria wyszukiwania w bazie danych tak aby pozostali użytkownicy mogli skorzystać z wpisanych wcześniej kryteriów. W takim scenariuszu kryteria wyszukiwania posiadają swoją własną tożsamość zamiast bycia obiektem wartościowym.

Teraz już wiesz czym jest encja i obiekty wartościowe. W Domain Driven Development encje i obiekty wartościowe mogą istnieć niezależnie. W niektórych przypadkach może pojawić się relacja w której encja oraz obiekt wartościowy nie posiadają żadnej wartości w jego kontekście.
Przykład:

  1. Okno może zostać zdefiniowane tylko jeżeli jest pokój.
  2. Notatka do zamówienia może istnieć tylko wtedy jeżeli zamówienie zostało złożone.
  3. Szczegóły pytania mogą pojawić się tylko jeżeli pytanie zostało zadane.


Aggregate oraz Aggregate Root

W powyższym przykładzie:

  • Pokój, zamówienie oraz pytania są naszymi aggregate root - jest encją, która jest rodzicem. Na przykładzie zamówienia: Pozycja zamówienia oraz zamówienie. Oba tworzą aggregate(agreagat), który jest zamówieniem a konkretna encja – zamówienie jest naszym aggregate root;
  • Okno, notatka oraz szczegółowe pytanie są naszymi agregatami.

Przykład:

  1. Szczegóły pytania nie powinny zostać zapisane do momentu, kiedy samo pytanie nie zostanie zapisane.
  2. Szczegóły pytania nie powinny zostać zwrócone do momentu, aż nie zostanie zwrócone pytanie.
W powyższym przykładzie pytanie to aggregate root a szczegóły dotyczące pytania to aggregate. Powyższe pojęcia to bardzo ważne składowe DDD.

Do tej pory rozmawialiśmy o obiektach/encjach, kontekstach, agregatach itd. Co z bazą danych? Czy to jest coś o czym zapomnieliśmy?

Odpowiedź brzmi: Nie! Domain Driven Design jest podejściem w którym powinniśmy ignorować trwałość (persistence).


Persistence Ignorance

W DDD nasz celem jest utworzenie obiektu domeny. Należy określić jakie są przedmioty (obiekty) potrzebne do wykonania pożądanych funkcjonalności w systemie. Należy określić relacje pomiędzy obiektami oraz sposób ich interakcji pomiędzy sobą. Należy sprawdzić czy postawione przez klienta cele biznesowe są osiągalne przy użyciu przygotowanego modelu domeny. Gdzie jest istnieje bazy danych? Nie musisz wiedzieć jak i gdzie dane są przechowywane a nawet czy dane muszą być zapisane w trakcie przygotowywania modelu domeny.

Niewiedza o zapisywaniu danych pozwala na przygotowanie modelu wolnego od połączenia się z warstwą bazy danych. W rezultacie aplikacja będzie wolna od połączenia a bazą danych i będzie pozwalała na łatwe przeprowadzanie testów jednostkowych.

Tak! – w prawdziwej aplikacji baza danych jest wymagana. Ale model domeny nie będzie miał żadnej wiedzy na ten temat. Wszystko będzie zarządzane przez Repozytorium, które będzie miało dostęp do bazy danych.


Repository

Jakie jest znaczenie słowa repozytorium?
Repozytorium często odnosi się do miejsca przechowywania danych lub względów bezpieczeństwa.

Jak już wspomnieliśmy wyżej, model domeny nie zna żadnej bazy danych. Będzie on jedynie wiedział, że istnieje repozytorium, które będzie odpowiedzialne za przechowywanie i pobieranie danych. Dla naszego modelu nie jest ważne jak i gdzie będę przechowywane te dane. Może to być SQL Server, Oracle, XML, plik tekstowy itd. Mam nadzieje, że wiesz co oznacza repozytorium.

Przejdźmy do spraw technicznych. Repozytorium pośredniczy pomiędzy domeną a mapowaniem danych za pomocą interfejsu pozwalającego na dostęp do obiektów domeny.

Repozytorium nie jest warstwą dostępu do danych!

Zwróćmy uwagę na to, że w repozytorium nie mówimy w kategoriach „danych”, mówimy w kategoriach aggregate root. Jeżeli pamiętasz, aggregate root może zawierać jedną lub wiele encji oraz obiektów wartościowych. Dzięki temu dość wyraźnie różni się od DAL zwracającego zbiór wierszy z tabeli bazy danych.


Domain Service

Domain Service jest kolejną implementacją koncepcji DDD. Jeżeli encje i obiekty wartościowe są “podmiotami” w naszej domenie, usługi pozwalają na obsługę akcji, operacji czy wszelkich aktywności.

Czy logika nie powinna być bezpośrednio przygotowana na encjach?

Tak! Powinniśmy przygotowywać encje z logiką która odnosi się do nich oraz ich dzieci. Zdarzają się jednak sytuacje, kiedy musimy radzić sobie ze skomplikowanymi operacjami lub operacje te muszą być widoczne na zewnątrz. Dlatego tworzenie takich usług dla różnych gałęzi agregacji (aggregate root) jest dobrym pomysłem. Usługi domeny mogą być rozważane jako frontowa warstwa logiki biznesowej i operacji w danej domenie.


Podsumowanie

W artykule tym starałem się objaśnić podstawowe koncepcje oraz stosowaną terminologię w odniesieniu do świata rzeczywistego. Tworzenie aplikacji zgodnych z koncepcją Domain Driven Development nie należy do najłatwiejszych. Im więcej praktyki tym lepsza będzie dokładność przygotowania kolejnych projektów. Najważniejszym punktem jest myślenie zgodnie z opisaną wyżej konwencją. Jeżeli nie, może okazać się, że w przypadku skomplikowanych projektów pojawi się wiele problemów.