środa, 9 września 2009

Ktoś sprzątać musi aby bałaganić mógł ktoś


Wszyscy zapewne pamiętamy burzliwą dyskusją Joela Spolskyego z Uncle Bobem (skrót na infoq)

Odbiła się ona szerokim echem dyskusji na ogólny temat: rzemiosło, profesja, jakość czy moźe lepiej byle jak, byle szybko, byle jako tako zadowolić klienta?

Kto ma rację?

Oczywiście ideałem byłoby dobrze i tanio. Ale jak intuicyjnie czujemy - tak to można tylko w mordę dostać;)

Prawda jak zwykle leży gdzieś po środku.

/*
 * W rozważaniach pomijam specyficzne przypadki typu
 * "firma nieustannie na dorobku"
 * albo specyficzny rodzaj dostawcy
 *
 * Zakładam, że bawimy się w projektach perspektywicznych
 * i jesteśmy świadomi zaciąganego kredytu bałaganu
 */


Ciekawą analizę tego zjawiska oraz racjonalne i pragmatyczne podejście do problemu przedstawił ostatnio w swojej genialnej (i jak zwykle niemiłosiernie ospałej) prezentacji sam guru DDD: Errrrrric Evaaaaans: Strategic Design - Responsibility Traps.

Poruszana tematyka zaczyna być omawiana od 30. minuty, ale gorąco wszystkich zachęcam do obejrzenia całości. Evans po mistrzowsku (i z typowym dla siebie poczuciem humoru) buduje od początku kontekst aby w 30. minucie wygarnąć nam co o nas myśli.
Wcześniej dowiecie się min. jakich strategicznych błędów nie popełniać podczas modernizacji systemu. Okazuje się, że standardowe 3 podejścia są z góry skazane na porażkę.

Prezentacja jest wg. mnie tak dobra, że w moim rankingu zajmuje miejsce 2. - zmieniając tym samym ostatnie notowania.


ZAŁOŻENIA (SAD BUT TRUE)
System jako całość nie może być dobrze przemyślany i zaprojektowany. KROPKA.
Nigdy nie będzie dostatecznie dużo: czasu, pieniędzy, wiedzy biznesowej, ludzi z odpowiednimi kwalifikacjami, czasu, pieniędzy, czasu, pieniędzy, czasu, pieniędzy,...

CO Z TEGO WYNIKA
Mamy 2 możliwości:
a) Wszystko zrobić "byle jak"
b) Pewną część zrobić porządnie i zgodnie z zasadami sztuki (oczywiście kosztem tego, że pozostała część będzie jakości mniejszej niż w punkcie a). W którą część i dlaczego warto zainwestować wysiłek - dowiemy się już za chwilę.

PROBLEM
Evans wytyka często spotykany problem. Ja nazwałbym go "złym rozłożeniem potencjału".
Evans opisuje taki oto często powtarzający się wzorzec: najlepsi (w sensie doświadczenia, intuicji, smaku a nie np certyfikatów) programiści/projektanci/architekci zajmują się tworzeniem tak zwanej "platformy". W zależności od systemu może to być wewnętrzny framework, główne biblioteki - ogólnie najczęściej są to jakieś zawiłości techniczne.

Natomiast reszta teamu - radośni hakierzy odpowiadają zwykle za dostarczanie corowych ficzerów biznesowych budowanych na bazie tejże tworzonej przez lokalnych guru platformy. Oczywiście ficzerów okraszonych zaokrąglonymi rogami w GUI - zgodnie z najnowszą modą żadna kanciasta forma nie jest dozwolona;)

Dostarczają oni funkcjonalności nazwanych przez Evansa sexy capabilities. Robią to tak jak potrafią, czyli byle jak byle szybko:)

Klient jest oczywiście zachwycony nowymi seksownymi możliwościami i nie zważa na marudzenie "nudziarzy od platformy", którzy narzekają, że znowu muszą sprzątać. Zresztą... po co sprzątać skoro działa?
Hero of the day to ten, kto zrobił zaokrąglony guzik zwiększający obroty o 1%;)

Oczywiście przyrost bałaganu jest większy niż możliwości jego sprzątania i mamy problem...

ROZWIĄZANIE: DESTYLACJA
Kto jak kto, ale my Słowianie mamy doświadczenie w destylacji więc mógłbym sobie odpuścić ten rozdział;)




Pomysł Evansa polega na wydestylowaniu domeny corowej. Zacznijmy od tego, że Evans wyróżnia 3 klasy domen:

- Core Domain - są to te specyficzne aspekty biznesu, będące powodem dla którego w ogóle warto tworzyć system. Przykładowo to one warunkują przewagę klienta nad konkurencją, lub odróżniają go od innych. To właśnie w tym miejscu mieszkają "sexy capabilities".

I to właśnie w ten kawałek (a powinien być relatywnie mały) inwestujemy największy wysiłek umysłowy.

To tutaj jest miejsce dla całej artylerii sprawdzonych technik naszego rzemiosła: szczegółowa analiza, archetypy biznesowe, wzorce projektowe, narzut na hermetyzację domeny, narzut na otwartość na rozbudowę, narzut na testability...

Core powinien być dobrze uniezależniony od pozostałych domen, które z definicji są w jakimś sensie "mniej godne zaufania" (bo np niestabilne).

- Supporting Domain - dodatkowe ficzery, bez których jednak można się obyć. Ten kawałek systemu może nawet np outsourceować. Jego jakość z założenia może być niska.

- Generic Domain - specyficzne domeny typu podsystem fakturowania lub biblioteka do operowania na grafach. Najlepiej kupić/uzyć gotowe rozwiązanie. Pamiętając o unikaniu zależności ze strony Core Domain.


Dobrym przykładem ilustrującym relatywizm tego pojęcia w zależności do biznesu jest system komentarzy użytkowników wystawianych kontrahentom.
W ebay jest to corowa funkcja (bez niej nikt nie kupiłby niczego od nieznajomej osoby). Dla amazona to po prostu jakiś poboczny ficzer (supportig domain) - ludzie i tak kupią jeżeli czegoś potrzebują lub po prostu mają ochotę.

Pamiętajmy, że developerzy nie są w stanie (ba, nie powinni) określić co należy a co nie do Core Domain w konkretnym przypadku danego klienta. Określenie Core Domain to jedna z głównych rzeczy, którą trzeba z niego wydusić;)




Chyba już domyślacie się jakie rozwiązanie sugeruje Evans...
Core Team zajmuje się Core Domain.
Najważniejsza część systemu ma szansę być zrobiona zgodnie ze sztuką a członkowie tego teamu zyskują uznanie jako dostawcy "sexy capabilities":)


Mam nadzieję, że moja recenzja/streszczenie zachęci Was (a szczególnie managerów) do poświęcenia godziny cennego czasu na prezentację Evansa.
Ciekaw jestem co sądzicie na temat tego podejścia - liczę na dyskusję równie owocną jak ta z przedostatniego posta:)

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

Prowokacyjnie dodam, że Joel Spolsky stosuje jednak strategię "klasyczną". Flagowe oprogramowanie w ofercie firmy Joela: Copilot (soft dla helpdesku pozwalający na zdalną pracę na maszynie "petenta") to nic innego jak nakładka na OpenSourceowy projekt na licencji GPL ;PPP

Czyli ni mniej ni więcej: ktoś zrobił platformę a partacze Joela dodali "sexy capabilities" - a biznes chyba się kręci:)



I jeszcze druga myśl, która mi się nasunęła: Nierzadko występują takie przypadki, gdy naprawdę doświadczony developer w ogóle nie interesuje się pewną domeną biznesową. Po prostu uważamy (niebezpodstawnie) za interesują mniej więcej w takim samym stopniu jak zeszłoroczny śnieg. Wolimy zamiast tego skupić się na rozwoju w kierunku technicznym z uwagi na jego ogólność wynikającą z abstrakcyjności. Nic na siłę, każdy powinien znaleźć sobie optymalne zainteresowania...

sobota, 5 września 2009

Anticorruption layer

Dziś łikendowy post z serii geek humor.

W ostatnim poście wspomniałem o hermetyzacji modelu domenowego poprzez Anticorruption layer, z którą to komunikujemy się poprzez Command i Query i z której nie wyciekają Encje.

Zobaczmy jak Eric Evans zobrazował to zagadnienie w 14 rozdziale swej książki (strona 367):



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

Więcej tego typu humoru w ospałych prezentacjach Evansa.

czwartek, 3 września 2009

Lazy loading w kontekście paradygmatu Command-query Separation



ODŚWIEŻENIE KONTEKSTU

Kilka miesięcy temu popełniłem dwa posty na temat Lazy loadingu (wiem, że nie chce się Wam ich czytać więc streszczam):

Lazy Loading a sprawa wydajności - post traktujący ogólnie o drastycznym spadku wydajności w sytuacjach gdy LL jest stosowany w niewłaściwym momencie.

Chodzi głównie o klasyczny "n+1 Select problem", który potrafi totalnie zamulić system. W skrócie: problem pojawia się gdy pobieramy z EnityManagera/Sesji hibernate kolekcję a następnie iterujemy po niej i getterami pobieramy zagregowane obiekty. Wówczas do bazy wysyłane jest 1 zapytanie o kolekcję oraz dla każdego z jej n elementów koleje zapytania dociągające potrzebne obiekty.

Podobne schorzenie występuje gdy naiwnie ustawimy w mapowaniu FetchMode na EAGER.

DAO a sprawa Lazy Loading - post poruszający problem tak zwanej cieknącej abstrakcji, czyli "brudzenia" kodu logiki warstw wyższych szczegółami technicznymi typu zamknięta sesji persystencji.

Oczywiście mamy sprawdzony sposób na cieknącą abstrakcję - podejście Open Session in View (rozwiązanie w Springu np przez Mateusza Mirackiego). Niestety w tym przypadku łatwo dopuścić do opisanego powyżej "n+1 Select Problem". Ciekawe rozwiązanie z testowaniem ilości wysyłanych do bazy zapytań przy pomocy statystyk Hibernate zaproponował w komentarzu do mojego posta Milus.


CREDO

Od razu śpieszę wyjaśnić - żeby nie było, że jestem jakimś fanatycznym wrogiem Lazy loadingu czy ORM w ogólności. Wręcz przeciwnie, namiętnie go stosuję od 5 lat i uważam za bardzo wygodny młotek w większości sytuacji.


PROBLEM
Od czasu opublikowania tych postów dostałem kilka maili z zapytaniami o sposoby radzenia sobie z powyższymi problemami.

Pod postami wywiązały się też krótkie dyskusje prezentujące możliwe podejścia.

Właściwie to chodzi głównie o problemem z wydajnością, ponieważ filozoficzny problem cieknącej abstrakcji prawie nikogo nie boli. Nauczyliśmy się z nim żyć i raczej się nad nim nie zastanawiamy (na szczęście Jacek podziela moje rozterki).

PRAWDZIWY PROBLEM
Wg mnie prawdziwy problem leży w samym podejściu do dostępu do danych, czyli architekturze aplikacji. Problem polega na zbytnim uogólnieniu.

Tak samo traktujemy dwa diametralnie różnie rodzaje obiektów:
- obiekty domenowe, które wykonują operacje biznesowe (lub na których to wykonujemy modyfikacje w podejściu proceduralnym)
- dane "przekrojowe" potrzebne jedynie do prezentacji (np wyświetlenia na GUI) wycinka aktualnego stanu systemu

CQS
Teraz nadszedł wreszcie czas na przedstawienie tytułowego bohatera tego posta: zapomniany i zakurzony paradygmat: Command-query Separation.

Paradygmat tez zakłada, że system posiada "interfejs", przez który wysyłamy do niego polecenia oraz osobny, przez który odpytujemy o dane. Nigdy nie projektujemy operacji, które zarówno coś modyfikują jak i odczytują dane.

W jaki sposób możemy wykorzystać to podejście w systemach enterprise?
"Interfejsem" przez który klienty (nie klienci) komunikują się z systemem może być warstwa aplikacji. Cienka warstwa, która zajmuje się wszystkim, oprócz logiki biznesowej i dostępu do danych.






COMMAND
Ok, chcemy coś zrobić w systemie, wysyłamy do niego Polecenie. Tak jak wspomniałem "interfejsem" jest warstwa aplikacji "opublikowana" jako jakieś bezstanowe servisy lub obiekty stanowe - zależnie od wymagań.
Jeżeli klient wyśle do tej warstwy Command, wówczas pobiera ona z Repozytorium jakieś encje (lub agregaty w DDD). Dalej na encjach/agregatach uruchamiamy ich metody biznesowe - jeżeli bawimy się obiektowo, lub wywołujemy jakieś biznesowe servisy przekazując im jako parametry pobrane właśnie encje. Nic specjalnego, klasyczna architektura warstwowa.

Natomiast w tym właśnie przypadku - gdy wysyłamy do systemu Command mający zwykle na celu wykonanie jakiś operacji biznesowych - jak najbardziej możemy (ba powinniśmy) radośnie korzystać z Lazy loadingu. Jest to jak najbardziej właściwy moment ponieważ natura takich operacji jest zwykle taka, że pobieramy kilka obiektów, które wchodzą ze sobą w jakąś interakcję (lub władają nimi servisy). Rzeczone obiekty biznesowe ewentualnie potrzebują do wypełnienia swej biznesowej odpowiedzialności zagregowanych składników. Raczej nic złego się nie stanie, gdy zamiast 3 prostych zapytań do bazy wyślemy ich 5 czy nawet 10.
Czasem nawet będzie to bardziej wskazane niż join - zależy do natury danych.

Cała operacja wykonuje się w obrębie metody z warstwy aplikacji więc jest objęta transakcją i ma cały czas otwartą sesję persystencji więc nie martwimy się o wyjątki Lazy loadingu.


QUERY
Natomiast jeżeli do systemu trafia Query, czyli zapytanie o dane, to wówczas sprawa wygląda nieco inaczej...
Przede wszystkim warstwa aplikacji nie ma pod sobą warstwy z logiką (żadnych servisów biznesowych). Czy potrzebujecie abstrakcji dostępu do danych (DAO/Repozytorium)? Raczej nie zmienicie nigdy źródła danych. Nie ma sensu również testowanie jednostkowe "Finderów" aplikacyjnych z podmienionymi na mocki DAO. Czyli czyste pobieranie danych.

W mniej złożonych systemach możemy sobie pozwolić na zwrócenie encji w odpowiedzi na kwerendę.
W bardziej poważnych raczej nie możemy pozwolić sobie na ujawnianie klientom naszego modelu, więc zwrócimy jakiś Data Transfer Object (DTO). Hermetyzacja modelu to podstawa - dzięki temu może on ewoluować iteracyjnie bez obaw o zniweczenie pracy teamu dospawującego prezentację.


QUERY ZWRACAJĄCE ENCJE
Jeżeli zdecydujemy się na zwracanie encji to musimy uporać się z paroma problemami technicznymi:
- wydajność: zwykle scenariusz obsługi kwerendy to pobranie kolekcji danych. Wówczas mamy jak w banku opisany na wstępie "n+1 Select problem". Rozwiązanie jest bardzo proste - wystarczy się pofatygować i napisać zapytanie z klauzulą JOIN FETCH. Przykładowo SELECT p FROM Person p JOIN FETCH p.addresses - dzięki temu chciwie/łapczywie (nie wiem, na które rozkoszne tłumaczenie się zdecydować) pobierzemy osoby wraz z podciągniętymi adresami. Po prostu ORM wygeneruje SQLa z JOINem.

Niezbyt dobrym pomysłem jest ustawienie w mapowaniu powiązania obiektów z FetchMode.EAGER. Spowoduje ono, że zawsze wyciągając jeden obiekt pobierzemy jego "dziecko". Owszem są sytuacje, gdzie z kontekstu biznesowego takie podejście jest sensowne, ale zwykle stanowią zdecydowaną mniejszość. Zwykle w jednym Use Case zależy nam na pobraniu np osób z adresami a winnym adresy są zbędne.

Warto pamiętać, że domyślnie strategia EAGER obowiązuje dla powiązań wiele-jeden, jeden-jeden i warto ją wyłączać.

- Open Session in View - podejście to o ile jest wygodne to niestety pozwala łatwo zapomnieć o tym, że leniwie podciągamy jakieś dane. Po prostu istnieje niemała szansa, że na widoku odwołamy się do adresów osoby a w zapytaniu zapomnimy dopisać JOIN FETCH. Działa? Działa. Muli? W środowisku developerskim z małą ilością danych pewnie nie;P

- Ilość danych - zwykle Use Case gdzie do systemu trafia Query zakłada, że z bazy trzeba pobrać dane "przekrojowe". Czyli dane z wielu tabel, ale z każdej z nich interesuje nas zaledwie kilka kolumn. W małych systemach, gdzie warstwa GUI i warstwa aplikacji stoją na tej samej JVM będzie to w śmigać.

Ale nawet w takiej konfiguracji mamy problem z pobieraniem zbędnych danych. Przykładowo: gdy pobieramy z bazy np dokumenty aby jedynie wyświetlić ich listę (data, autor) a każdy z nich ma kolumnę przechowującą dziesiątki stron textu. Rozwiązaniem jest leniwe ładowanie pól - czyli ich nieładowanie:) W hibernate wymaga to poddanie skompilowanego bytecodu instrumentalizacji.

Innym podejściem może być zamapowanie tabeli przez kilka klas. Przykładowo DocumentFull, DocumentLight, itp... Jeżeli czujecie niesmak na myśl o mnożeniu bytów to nie jesteście sami.

Hibernate pozwala na pobieranie danych wprost do DTO. Tworzymy DTO szyte na miarę danego Use Case, a składnia wygląda tak:
SELECT new pakiet.KlasaDTO(pole1, pole2.podpole) FROM...
Oczywiście przy założeniu, że odpowiedni konstruktor istnieje.

QUERY ZWRACAJĄCE DTO
Jeżeli zdecydujemy się na zwracanie DTO to zapewne dlatego, że potrzebne dane są na tyle przekrojowe, że żaden zestaw encji nie modeluje ich sensownie (i optymalnie).
Innym powodem może być chęć hermetyzacji zmiennego modelu poza stabilną anticorruption layer. Warstwa zapobiegająca gniciu to pojęcie z DDD i ma pragmatyczny sens w nieco bardziej perspektywicznych projektach.

Poza tym możemy być dumni, że nawet nasza architektura wspiera Agile umożliwiając ewolucję modelu domenowego bez rujnowania wszystkiego dookoła:)

Częstym błędem w tym przypadku jest pobieranie z ORM encji a następnie przepakowywanie ich w DTO.
Nie tędy droga...


Pobierzmy tylko to co tak na prawdę jest potrzebne. Najprościej zrealizować to z użyciem wspomnianej konstrukcji Hibernate SELECT NEW. Jednak w złożonych systemach zwykle nieodzowny będzie co najmniej w paru jakiś szyty na miarę i zoptymalizowany SQL. Jakąś abstrakcją nad SQLem może być wówczas np iBATIS mapujący result na DTO.


Jak to zwykle bywa najlepsze będzie podejście hybrydowe. Tam gdzie możemy na to sobie pozwolić zwracamy w wyniku obsługi Query encje - zwiększając tym samym swą produktywność. Natomiast tam gdzie krytyczna jest wydajność lub specjalna struktura danych, zwracamy DTO.


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

Opisane powyżej aspekty wydajności związane z Lazy Loadingiem aplikują się do systemów każdej wielkości - szczególnie "n+1 Select problem". Natomiast rozwiązanie z CQS a szczególnie podejście gdzie zwracamy z kwerendy DTO jest bardziej pracochłonne, przez co aplikuje się do projektów bardziej perspektywicznych.

Ale czy jest sens tworzyć w JEE projekty inne niż perspektywiczne? Do prostych i szybkich zadań typu "przeglądarka do bazy" jest przecież Microsoft Access;P