niedziela, 14 grudnia 2008

UP-DDD in Action: Hermetyczne agregaty



Drogie dzieci - w dzisiejszym odcinku z serii UP-DDD in Action zobaczycie jak nasz bohater Bob Budowniczy rozprawia się z brakiem hermetyzacji i proceduralną ignorancją.



Kontynuując temat enkapsulacji rozpoczęty w poprzednim poście zobaczmy jak wygląda on w kontekście Domain Driven Design.

DDD opiera się na starych i sprawdzonych technikach obiektowych więc proceduralne łańcuszki typu "train wreck" zbesztane w poprzednim poście nie mają prawa pojawić się w kodzie aspirującym do miana DDD.

Analizując specyfikacje building blocks z DDD widać, że Encje, VO i Agregaty posiadające odpowiedzialność biznesową (nie będące standardowym anemicznym modelem) są w zgodzie z paradygmatem Abstrakcji.

Dodatkowe założenie odnośnie Agregatów jest takie, że hermetyzują one swą strukturę. Oznacza to, że nie powinny zdradzać szczegółów swych składowych. Agregat posiada root (główną encję), która publikuje niejako zestaw odpowiedzialności/zdolności w celu ochrony wnętrzności przed światem zewnętrznym. Jeżeli świat zewnętrzny nie wie nic o wnętrznościach agregatu, to wówczas implementacja agregatu może z czasem się zmieniać (aby nadążać za nowymi wymaganiami). Dzięki temu zmiany są lokalne i nie mają wpływu na świat zewnętrzny.

Weźmy na ten przykład arcy interesujący model zamówienia;) Niech zamówienie posiada datę, miejsce dostawy oraz listę zamówionych produktów. Zamówienie będące agregatem nie może na przykład udostępniać gettera do listy produktów ponieważ istnieje możliwość modyfikacji tejże listy "poza świadomością" samego zamówienia.
Zakładając iż tworzymy obiektowy model biznesu i jego reguł to chcielibyśmy aby każda operacja na produktach była wykonywana poprzez samo zamówienie, które jest swego rodzaju mózgiem biznesowym.

Obiekt zamówienia posiadając wiedzę o swych wnętrznościach może na ten przykład domówić dodanie do niego pewnego produktu jeżeli miejsce dostawy zawiera się w pewnej strefie, klient nie jest pełnoletni, albo data realizacji wykracza poza jakąś tam granicę pewnej akcji marketingowej.

W tym akurat przykładzie jakimś rozwiązaniem byłoby upublicznienie iteratora po iście zamówień. Ale załóżmy przypadek bardziej ogólny - nie chcemy zdradzać żadnych szczegółów i kropka.

No dobra, wystarczy tego teoretycznego bełkotu! W praktyce niestety często istnieje konieczność dobrania się do wnętrzności obiektu. Na przykład po to aby wyeksportować zamówienie do PDF lub XML. Że o takim szczególe jak jego wyświetlenie na GUI nie wspomnę;P

Podchodząc do zamówienia jak do struktury danych potrzebowalibyśmy procedur, które odpowiadają za przejrzenie wnętrzności zamówienia i stworzenie odpowiednio PDFa lub XMLa prezentującego dane o zamówieniu i jego wewnętrznej strukturze (GUI to problem osobny - wręcz filozoficzny, więc o nim osobny akapit później).

Na szczęście istnieje wzorzec projektowy Budowniczego. Idea wzorca jest prosta jak nie przymierzając budowa cepa. Zakłada ona istnienie dyrektora budowy i robotnika (budowniczego). Dyrektor wie CO trzeba zrobić a robotnik wie JAK to zrobić.



Na powyższym przykładzie klasa Order jest dyrektorem budowy. Gdy ktoś poprosi dyrektora o wyeksportowanie zamówienie wówczas on wydaje rozkazy delegując pracę do budowniczego. Dyrektor (zamówienie) wie skąd pobrać dane do eksportu - z własnych prywatnych wnętrzności. Natomiast robotnik (budowniczy) jest abstrakcją w najczystszej formie - interfejsem. Konkretny budowniczy wie jak zająć się surowcami przekazywanymi mu przez dyrektora budowy. Na przykład budowniczy PDFów zna doskonale jakiś silnik do generowania dokumentów. Budowniczy XMLi zna format w jakim należy zbudować plik tekstowy. Budowniczy DTO wie jak należy skonstruować obiekt transferowy.

Mamy tutaj doskonałe rozdzielenie odpowiedzialności: hermetycznych struktur biznesowych i wiedzy o ich semantyce od technicznych aspektów formatów danych eksportowych.

Powyższy przykład można rozszerzyć o mały aspekt optymalizacyjny. Bo co jeżeli dany budowniczy nie jest z jakiegoś powodu zainteresowany na przykład listą zamówień? Najprościej byłoby aby jego implementacja metody buildNextProduct() po prostu ignorowała przekazywane produkty. Niestety mamy wówczas niepotrzebną iterację. Zamiast tego możemy rozszerzyć interfejs budowniczego o metodę isProductListNeeded(). Iterujemy po liście jedynie wówczas gdy metoda zwróci wartość true.



Wspomniałem wcześniej o aspekcie GUI. Popularne frameworki zasadzają się na refleksji i wymagają by istniały gettery do składowych obiektu, które chcemy wyświetlić. Heh jednym ze sposobów na wyświetlenie zamówienie w JSF mogłoby być stworzenie takiej implementacji budowniczego, która tworzy jakiś UIComponent który to następnie dodajemy do widoku - niestety jak dla mnie jest to nakładanie gaci przez głowę.

Jakimś rozwiązaniem jest zastosowanie Data Transfer Objects. Czyli nasz agregat biznesowy może eksportować się do DTO, które to DTO jest bindowane z widokiem. DTO ma już gettery i settery więc doskonale pasuje do frameworków.

Rozwiązanie to ma oczywiście plusy dodatnie i ujemne.

Przede wszystkim dzięki przepakowaniu obiektów biznesowych do głupich DTO nie udostępniamy w warstwie UI metod biznesowych (pamiętajmy, że Agregaty je posiadają). Serwisy aplikacyjne posługujące się DTO nadają się wówczas do bycia Web Serwisami - nie chcemy przecież upubliczniać naszego modelu biznesowego oraz jego metod (odpowiedzialności). DTO mogą zawierać jedynie istotne z punktu widzenia usługi dane, prezentować je w spłaszczony (prostszy) sposób oraz stanowić warstwę interfejsu do systemu pod którą możemy swobodnie zmieniać nasz model bez obawy o ciągłe modyfikacje klientów.

W małych projektach zmniejsza to oczywiście w znaczny sposób produktywność (rapid developnent hehehe). Zamiast spokojnie stukać sobie przeglądarkę do bazy musimy nakładać gacie przez głowę i przepakowywać dane bawiąc się samemu ze sobą w kotka i myszkę:/


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

Swoją drogą to dziwne, że frameworki nie wspierają dobrych modeli programowania a skupiają się jedynie na przykryciu technikaliów takich jak na przykład HTTP, transakcje, rozproszenie obiektów, bindowanie danych warstwą abstrakcji...

Nawet frameowrki, które nazywają się dumnie frameworkami aplikacji nie wspierają żadnego modelu aplikacji a hermetyzują jedynie niskopoziomowe aspekty techniczne.

W którymś z następnych odcinków zobaczymy jak wygląda to w http://www.qi4j.org/

5 komentarzy:

Irek Matysiewicz pisze...

No cóż, my też udostępniamy wnętrzności obiektów tylko po to by użytkowik mógł je sobie edytować na formatce.
Twój pomysł z DTO też niewiele naprawia: bebechy są ciągle udostepniane, tyle, że okrężną drogą (za pośrednictwem DTO). Dla mnie to ciągle to samo co getery i setery, tylko przeniesione do innej klasy. Jedyny megaplus DTO to łatwy remoting.

Podobny pomysł z Builderem ktoś miał na JavaWorld:
- http://www.javaworld.com/cgi-bin/mailto/x_java.cgi?pagetosend=/export/home/httpd/javaworld/javaworld/jw-01-2004/jw-0102-toolbox.html&pagename=/javaworld/jw-01-2004/jw-0102-toolbox.html&pageurl=http://www.javaworld.com/javaworld/jw-01-2004/jw-0102-toolbox.html&site=jw_core
Tylko jak to zastosować np. do JSF czy Seama?
Mamy rok 2008, a ciągle brak dobrych frameworków, zwłaszcza do GUI.

Jest też odwrotne soluszyn zwane NakedObjects:
- http://en.wikipedia.org/wiki/Naked_objects
- http://www.nakedobjects.org/tutorial/index.shtml
Udostępniamy wszystkie wnętrzności Encji a GUI z nich niech się generuje samo. :-)
Tylko czy takie GUI będzie używalne, a kod rozszerzalny?

Sławek Sobótka pisze...

Irku - dzięki za link do artykułu samego mistrza - Allena Holuba. Chyba nikt nie wyjaśni tego jaśniej od niego. Przyznam, że pomysł na zastosowanie Budowniczego w GUI przyszedł mi po przeczytaniu jego wspaniałej książki: http://helion.pl/ksiazki/wzopro.htm
Polecam ją każdemu, kto lubi bawić się w sposób Object Orieted. Sam jestem pod jej wielkim wpływem i to właściwie ta książka zmieniła moje podejście do OO.

Odnośnie Naked Objects to od jakiegoś czasu noszę się z zamiarem zinwestygowania tego tematu głębiej. Boję się tylko czy to wygenerowane GUI będzie akceptowalne przez klienta hehehe. Oraz czy życzenia typu "tego czekboksa proszę bardziej z prawej i kwiatuszek zamiast lebelki" są w ogóle możliwe do zrealizowania...

Anonimowy pisze...

Pożyteczny post, choć trochę przeładowany metaforami ;)

Koncepcja z Builderem pochodzi bodajże od Allena Holuba, ale bez większych strat można to zrealizować prościej i to może nawet bez oddzielnych DTO, ich mapowania, asemblerów itd.

Zauważmy, że tak naprawdę nie ma niczego złego (niezgodnego z OOP) w udostępnieniu dla end-usera get/set dla nazwy klienta, czy jego kraju pochodzenia. To właśnie są rzeczy, które end-user chce zrobić: odczytać nazwę i ustawić nazwę, odczytać kraj pochodzenia i ustawić go. Na tym właśnie polega edycja danych, czy ich wyświetlanie: musimy założyć, że nasz model określone dane udostępnia, jeśli user ma je zobaczyć, co najwyżej mamy pytania: która klasa powinna za to odpowiadać (Customer, CustomerDto, czy jeszcze coś innego) i w jakiej formie mają być one udostępnione (to nie muszą być dokładnie te same dane które widzi user, ale muszą być na tyle podobne, żeby warstwa prezentacji mogła to sobie przetłumaczyć do swoich potrzeb).

Z drugiej strony mamy inny scenariusz, np. jakieś przetwarzanie w logice biznesowej i nie chcemy, żeby w trakcie tego przetwarzania inna klasa odczytywała sobie lub ustawiała ot tak nazwę lub kraj klient (podobnie jak nie chcemy, żeby UI wywoływało metody biznesowe).

Konflikt wynika z tego, że chcemy użyć tego samego interfejsu dla różnych klientów, sugerując się tym, że w implementacji będzie to u nas jeden obiekt. Jednak zgodnie z zasadą segregacji interfejsów powinniśmy projektować je pod potrzeby klas klienckich, bo to do nich one "należą", a nie pod potrzeby implementacji. Wystarczy zatem zdefiniować oddzielny interfejs udostępniający get/set dla nazwy czy kraju i ten właśnie interfejs zwracać do warstwy prezentacji, a dla operacji w logice biznesowej zdefiniować inny interfejs (lub interfejsy), udostępniające metody według reguł Tell, Don't Ask i bez proceduralnych łańcuszków.

Opiszę to może dokładniej na swoim blogu w okolicach świąt...

Sławek Sobótka pisze...

Przykładowo Qi4J problem ten rozwiązano poprzez konteksty i ich miksowanie.
Czyli to o czym piszesz Rafale.

Ogólnie podejście w Qi4J jest ujęte w takiej oto metaforze: jestem obiektem człowiek, ale w zależności od kontekstu jestem programistą, kierowcą, mężem. Jednak zawsze tym samym obiektem.

Niemetaforycznie: przykładowo dla obiektu biznesowego mamy kontekst servera (busiess logic) i klienta (na przykład gettery). Qi4J po prostu wspiera tego typu miksy na poziomie frameworka.

W wolnym czasie zinwestyguję zagadnienie i podzielę się przemyśleniami.

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

No i cieszę się, że wreszcie udało mi się spłodzić jakiś pożyteczny post:)))

Irek Matysiewicz pisze...

DTO tu nic nie pomoże. Podobnie soluszyn Holuba:
obiekt.export(eksporter);
obiekt.import(importer);
Jeśli się uwezmę, to mogę zrobić DTO, które implementuje interfejs eksportera i sobie napisać:
DTO dto = new DTO();
obiekt.export(dto);
[różne operacje na dto]
obiekt.import(dto);

Jego soluszyn tak naprawdę nic nie daje, tylko sprawia, że dostanie się do wnętrza jest nieco niewygodne.


Chyba tego nie da się ukryć na poziomie interfejsów czy klas. Potrzebne są pewne rozszerzenia języka.

C++ ma słowo kluczowe friend:
friend JakaśKlasaZGuiKtóraMożeWołaćPrywatneGetery.
Wadą tego jest, że udostępnia całe wnętrzności klasy dla tej innej klasy, nawet pola prywatne. Do tego zupełnie się nie nadaje w architekturze warstwowej, bo nasza klasa musi wiedzieć jak będzie się nazywała klasa GUI.

Qi4j - nie wiem, nie używałem. Nawet dokumentacja do tego jest szczątkowa.

Spoon ( http://spoon.gforge.inria.fr/ ) - pozwala dodać dodatkowe rozszerzenia do Javy, głównie dzięki anotacjom.
Można np. do geterów i seterów dać anotację:

@Friends({@GUI})
String getName() {...}

Teraz wystarczyłoby Spoonowi powiedzieć, że @GUI to dowolna klasa dziedzicząca po JComponent, czy mająca Seamową anotację @Name i to wszystko. Jeśli nieautoryzowana klasa wywoła tego getera, Spoon wyrzuci błąd kompilacji.


Być może da się to zrobić z AspectJ, ale nie jest to aż tak mocne narządko jak Spoon, i AspectJ jest zgodny z Javą tylko w jedną stronę (nie każdy program w AspectJ jest programem Javy). I nie potrafi tworzyć własnych błędów kompilacji na takim poziomie jak Spoon.