niedziela, 18 października 2009

JUG, JDD, DDD, Cooluary v3 - podsumowanie



Tydzień upłynął pod znakiem Domain Driven Design.

Wtorek: prezentacja "DDD + impl w Seam" na lubelskim JUG.

Był to beta test przed piątkową ewangelizacją DDD w Krakowie podczas Java Developers' Day. Przy okazji podziękowania za feedback, który otrzymałem od wielu osób po wtorkowym wystąpieniu. Dzięki niemu udało mi się skompresować ponad 2h do 45 min na potrzeby JDD:)

Sobota: COOluary i powtórka DDD. Tym razem już 3h, sam DDD bez Seam - panowie słusznie założyli, że technikalia są drugorzędne.

Zarówno podczas prezentacji na JUG jak i DDD pojawiło się kłopotliwe pytanie odnośnie poniższego slajdu:



Slajd prezentuje Building Block, przy pomocy których to modelujemy domenę biznesową.
Ale zanim przejdziemy do pytania - przyda się wstęp. Jeżeli ktoś był na prezentacji, to może spokojnie przewinąć się niżej...


NIEKRÓTKI WSTĘP

Logikę dzielimy na dwie warstwy:
- Warstwę logiki aplikacji
- warstwę modelu domenowego

Warstwa logiki aplikacji jest cienka nie bez kozery. Zawiera ona logikę specyficzną dla danej aplikacji i jednocześnie nie będącą logiką reguł biznesowych - przykładowo: transakcje, bezpieczeństwo, jakiś mailing, ale również koszyk w sklepie internetowym. Model zamówienia i produktów to logika biznesowa, ale sam koszyk jest ficzerem aplikacji.

Warstwa modelu domenowego jest budowana z klas grających pewne role. Z ról wynika odpowiedzialność tych klas. Czyli opieramy się na paradygmacie Responsibility Driven Design.

Te role to:
Encje - identyfikowalne (posiadająće ID) byty biznesowe. Nie tylko rzeczowniki, ale również np metafory przejścia/zmiany stanu. Co ważne z rolą encji wiąże się realizacja odpowiedzialności biznesowych. Zatem mówimy stanowcze NIE dla anemicznego modelu;)
Inaczej niż w popularnych ORMach encja jak najbardziej powinna mieć metody biznesowe.

Value Objects - obiekty nieidentyfikowalne i zwykle niemodyfikowalne. Jeżeli 2 Vo mają takie same atrybuty to są wówczas tożsame. VO również mogą posiadać odpowiedzialność biznesową. W pewnych podejściach są marginalizowane a w innych wręcz wypierają encje - ale to temat na osobne posta, więc zostawmy.

Agregaty - Encje, które zawierają w sobie grafy encji lub VO. Co ważne agregaty hermetyzują swą implementację więc wszelkie operacje na wnętrznościach wykonujemy przez metody biznesowe encji głównej będącej Aggregate-root.

Repozytoria - zarządzają persystencją Encji/agregatów. To oczywiście jedynie interfejsy; implementacje są warstwę niżej. Nie ma tu metod wyszukujących po kryteriach pochodzących z GUI znanych z DAO.

Fabryki - hermetyzują złożoną (zwykle) logikę tworzenia agregatów.

Servisy - servisy biznesowe (nie mylić z aplikacyjnymi) zawierają logikę, której nie sposób sensownie przypisać do żadnej encji/agregatu/vo.

Polityki - to nic innego jak Wzorzec Strategii. Enkapsulują zmienność/wariacje logiki biznesowej. Jest to kluczowa figura podczas modelowania i rozmowy z expertem biznesowym.

Zdarzenia biznesowe - chodzi po prostu o decoupling artefaktów biznesowych od technicznych obiektów obsługujących. Dodatkowo możemy mieć asynchroniczność.

Przykład agregatu: Zamówienie zawierające pozycje zamówienia:


@Entity
public class Order{
@Id private OrderId id;
@OneToMany private List<OrderItem> items = new ArrayList();
private BigDecimal sum = new BigDecimal(0);
//.... status, createDate, rebatePolicy, productRepository,...

public void add(Product product, int quantity){
OrderItem oi = orderItemFactory.build(product, quantity, rebatePolicy);
//TODO jeżeli produkt jest już na liście to możemy jedynie zmienić ilość w odpowiednim order item
items.add(oi);
sum = sum.add(oi.getCost());
}

public void submit(){
if (status != Status.NEW)
throw new InvalidStateException();
status = Status.IN_PROGRESS;
createDate = new Date();
eventsManager.handle(orderEventsFactory.orderSubmitted(this));
}

public Iterator<OrderItem> getOrderItems(){
return items.iterator();
}
}


Kod zawiera kilka charakterystycznych dla DDD konstrukcji.

  • lista produktów jest prywatna i nie ma do niej gettera a już niedopuszczalny jest setter. Dostęp do listy poprzez iterator, który nie umożliwia zmiany.
    Pamiętajmy, że w JPA nie musimy uzywać getterów/setterów - wystarczy adnotacji na polu, wówczas ORM używa refeksji.
  • operacje biznesowe poprzez odpowiednie metody: add, submit
  • operacja add przyjmuje produkt - szczegółem impl jest tworzenie wewnątrz zamówienia jakiegoś OrderItem
  • operacja add zmienia wewnętrzny stan - co by było gdyby można było zmieniać listę udostępnioną getterem? Kto musiałby pamiętać o zmianie sumy?
  • operacja submit zmienia jakieś pola - jakie? to szczegół. Pojawią się nowe pola w encji Order - wówczas zmieniamy tylko metodę submit. Operacja dodatkowo może sprzeciwić się rzucając wyjątek oraz generuje darzenie.



"KŁOPOTLIWE" PYTANIE
Czy agregat/encja może wołać: repozytorium/servis/fabrykę?
Ogólnie: jakie zależności (pomiędzy czym a czym) są dozwolone w DDD?


Przyznam się, że sam miałem kiedyś tego typu pytania... Nie dawały mi spokoju: jakie są ograniczenia, co z czym można łączyć a co jest zabronione? Co gorsza, członkowie grup dyskusyjnych podawali sprzeczne "złote" zasady.

Zasada jest prosta: możesz robić wszystko co jest dobre dla modelu.
Wszystkie building blocks DDD są równo traktowane - nie ma ważnych i ważniejszych, mogą korzystać z siebie nawzajem jeżeli wynika to z logiki biznesu.

Dobre wyjaśnienie tego problemu znajdziecie w prezentacji Is Domain-Driven Design More than Entities and Repositories? Tak, tak - sarkastyczny tytuł oznacza, ze J. Nilsson będzie lekko kpił sobie z prymitywnych interpretacji DDD typu JavATE;)

Ale jak uniknąć kodu spaghetti w warstwie modelu domenowego jeżeli zaczniemy w nim łączyć wszystko ze wszystkim? Przecież DDD ma chronić nas przed chaosem a nie sprzyjać jego generowaniu.

Podczas COOLuarów doszliśmy do sformułowania dobrej zasady: Jeżeli z logiki biznesowej wynikają pewne powiązania to nie należy ich unikać. Jeżeli logika biznesowa charakteryzuje się pewnych poziomem złożoności - wynikającej z natury danego procesu, to nie unikniemy powiązań. Ważne aby nie wprowadzać dodatkowej złożoności (przypadkowej).

Dobrą metaforą warstwy logiki aplikacji jest scenariusz dla aktorów, którymi są Building blocks. Albo inaczej: kod logiki aplikacji niejako żongluje klockami biznesowymi. Chodzi o to aby czytać taki scenariusz i "widzieć" czystą logikę.
Więcej na ten temat w końcówce posta Understandability.

Jeżeli teraz jeden z aktorów nie chce pobrudzić się i wywołać np repozytorium to musimy zrobić to wprost w głównym wątku scenariusza (a wynik sztucznie przekazać gdzieś do środka aktora). Powoduje to pojawienie się sztucznych zgrzytów w scenariuszu. I zamiast ułatwiać zrozumienie go - utrudnia.

//=========================

J. Nilsson twierdzi, że jest to typowe pytanie zadawane, przez każdego adepta DDD. Wg mnie taka jest kolej rzeczy - w każdej dziedzinie. Wynika to z modelu kompetencji braci Dreyfus gdzie na początku potrzebujemy dokładnych wytycznych jak zrobić. Później uczymy się, że to zależy od tego co chcemy osiągnąć.

5 komentarzy:

iirekm pisze...

W moim obecnym projekcie też są takie rozkminy, że encja musi się odwoływać do repozytorium czy serwisu z warstwy domenowej.

Jeśli nie znamy praktyki, która by tego zabraniała, a użycie repo czy serwisu w danym miejscu wydaje się naturalne to dlaczego by nie. Inaczej skończyłoby się to przeniesieniem części logiki domenowej (tej co wymaga repo) do warstwy aplikacji (która na pewno może wołać repo) - niezbyt dobre soluszyn.
Ja widzę 2 powody dla których są takie opory przed tym:
- przyzwyczajenie do modelu anemicznego (gdzie jest ostry podział na warstwy: encje - DAO - fasady)
- drobne trudności techniczne z realizacją wstrzykiwania beanów Springowych do encji; jednak to się da zrobić za pomocą interceptorów Hibernate'a (encje z bazy) i w Factories (encje nowe)

iirekm pisze...

I takie małe pytanka:

Dlaczego tu użyłeś "ProductId id"? ID-ki jak na mnie mocno śmierdzą bazą danych.
ID-ki mogą być potrzebne w fasadach eksportowanych przez sieć, żeby nie słać całego obiektu Product w tą i z powrotem.
Ja bym po prostu w tej encji zamiast ProductId użył po prostu Product. Nie ma zanieczyszczenia domeny elementami sieciowo-bazodanowymi a wtedy przeciwnicy tego slajdu by się nie czepiali. :-)

A co do iteratora: ciągle można usuwać elementy, bo iterator ma metodę remove().
Jakieś soluszyn na kolekcje read-only to użycie Collections.unmodifiableList(), ale wtedy z nagłówka metody nie widać, czy dana kolekcja jest do odczytu czy do odczytu i zapisu. Gdyby to ode mnie zależało, do Javy dodałbym też kolekcje tylko do odczytu (np. List -> ReadOnlyList).

Sławek Sobótka pisze...

Jak zwykle masz rację Irek. Przeniesienie logiki warstwę wyżej spowoduje niezgrabne i sztuczne "scenariusze", o których pisałem.
Powód, który diagnozujesz - jest również tym, co u mnie powodowało na początki blokady.

A co do technicznych aspektów w Springu, to można dać @Configurable dla encji + protopyp w XML i masz wstrzykiwanie do encji.

Ja w Seam na prezentacji zaproponowałem wstrzykiwanie do repozytoriów i fabryk, które to seterami ustawiają zależności na encjach. Niezbyt elegancko, ale póki co musi tak w Seam.

Odnośnie pytania o ID...
Masz rację - to wyższa warstwa może pobierać encję i komunikować się z zamówieniem przy jej pomocy. Chyba nawet bym tak zrobił - chciałem jedynie pokazać wykorzystanie repozytorium w encji (ale chyba zmienię przykład żeby nie wprowadzał ludzi w błąd).

Ale zauważ, że posługuję się tam specjalną klasą klucza (ProductID a nie Long albo String), która ma już jakieś znaczenie biznesowe. Z tego względu to podejście może się jakoś bronić...

Odnośnie jeszcze iteratora... Tak, masz rację - aby zrobić to porządnie można by zwracać iterator kopi albo nawet kopię niemodyfikowalną.

Ale jest jeszcze inne podejście- mówi o nim J Nielsen w podlinkowanej prezentacji: z agregatu można zwracać Value Objects. Służą jako "wziernik" do środka agregatu, ale nie zdradzamy dzięki ich zastosowaniu prawdziwych klas modelu.

Jest to oczywiście mocno narzutowe podejście, niemal w stylu DTO, ale niemodyfikowalne obiekty mogą przydać się np przy współbieżności.

Albo w Core Domian gdzie inwestujemy więcej czasu aby totalnie enkapsulować domenę.

iirekm pisze...

@Configurable wymaga zaawansowanego AOP - cuda z classloaderami lub javaagentami. Może to być ciężkie w niektórych środowiskach, np. na jakichś mniej popularnych serwerach aplikacji, czy w środowiskach, gdzie są mocno ograniczone uprawnienia (np. w apletach). Chyba tylko w aplikacjach standalone albo gdy przenośność nie jest ważna można tego użyć z 100% pewnością, że zadziała zawsze i wszędzie.


Obydwa sposoby (z AOP i z Hibernatowymi interceptorami) są opisane tu: http://www.jblewitt.com/blog/?p=129

iirekm pisze...

> Odnośnie jeszcze iteratora... Tak, masz rację - aby zrobić to porządnie można by zwracać iterator kopi albo nawet kopię niemodyfikowalną.

Co do kopii, to kopiowanie większych kolekcji może wymagać czasu. Dodatkowo oznacza zaciągnięcie całości kolekcji Hajbernejtowy lazy-loadingiem, podczas gdy np. wołacz potrzebuje pierwszych pięciu elementów.

Ja jednak wolę podejście albo z własnym typem kolekcji (co nie ma metod do modyfikacji), albo z Collection.unmodifiableList(), albo można zwrapować iteratora tak, by metoda remove rzucała UnsupportedOperationException.