sobota, 18 kwietnia 2009

DAO



Jak donoszą brukowce DAO odeszło i zostało zastąpione przez JPA. Czy aby na pewno?

Piotrek w swym poście (będącym niezłym zbiorem argumentów w dyskusji) wyraził zdziwienie pojawiającymi się od pewnego czasu głosami przeciwko DAO. Natomiast ja chciałbym dodać od siebie dodatkowo nieco typowej dla siebie irytacji spowodowanej wąskim spojrzeniem na problem pojawiającym się w głosach autorytetów z wielkiego świata.

Problem potrzeby lub nie warstwy DAO nie jest oczywisty i zależy od kontekstu. Czasem DAO jest niezastąpione a czasem może lekko przeszkadzać. Ale na pewno pozbawione kontekstu autorytarne głosy typu "DAO bad" są bez sensu.


DAO ma na celu separację logiki dostępu do danych od logiki biznesowej/aplikacji.
Co nam to daje?
Szeroką i sensowną listę podał Mario Gleichmann w swoim poście.

Ja ograniczę się do paru z nich:

  • wynikającą z separacji możliwość podmiany sposobu dostępu do danych
  • wynikający z możliwości podmiany zwiększony poziom testability
  • reużywalność logiki dostępu do danych


Przyjrzyjmy się zasadności wylistowanych argumentów:
Podmiana sposobu dostępu do danych - teoretycznie silny argument; praktycznie bezużyteczny. Jak często zdarzyła się Wam taka sytuacja? O ile bardzo często zmienią się (no w zależności od klienta) wariacje logiki biznesowej to zmiana źródła danych? O ile zmienną logikę biznesową warto hermetyzować pod stabilnym interfejsem i jej wariacje enkapsulować we wzorcu strategii to robienie tego z dostępem do danych wydaje się nakładaniem gaci przez głowię. Chyba, że...

Testowanie - separacja dostępu do danych od logiki biznesowej zwiększa dramatycznie poziom testability, czyli podatność kodu na testy. Tak podatność na testy - ponieważ nie każdy kod można łatwo testować.
Jeżeli mam osobną klasę realizującą jakąś funkcjonalność biznesową oraz osobną klasę DAO odczytującą/modyfikującą dane to wówczas:
- możemy poddać testom sam "komponent" DAO - co się oczywiście będzie rzadko zdarzać, więc ten argument się nie liczy;P
- możemy testować samą logikę biznesową - na czas testów nasza logika może używać innej implementacji DAO (np takiej, która nie korzysta z bazy danych). Co na to daje?
Testy logiki biznesowej są szybsze ponieważ nie tracimy czasu na operacje zapisu odczytu danych (których poprawność w tym momencie nas nie interesuje). Czas zapisu/odczytu potencjalnie może być stosunkowo długi, wystarczy wyobrazić sobie źródło danych, które jest bazą zawierającą miliony rekordów, gigantycznym plikiem, czujnikiem chemicznym, web servisem wykonującym się średnio 5 min. Zamiast 100000 testów na godzinę, możemy ich wykonać tylko 10 gdy będziemy ciągnąć za sobą bagaż dostępu do danych.

Reużywalność - posługiwanie się wprost EntityManagerem w kodzie biznesowym do pobierania encji (tak jak chcą tego piewcy śmierci DAO) jest pomysłem arcy-chromym.
Kod w stylu
em.find(Person.class, id)
nie wygląda groźnie - nieprawdaż? Niestety jest on jednak typowy dla tandetnych turoriali na poziomie Hello World. W rzeczywistości jednak piszemy kwerendę ponieważ:
- mamy dynamiczne zapytanie (np doklejamy WHERE person.name = 'xxx' o ile user wpisał imie, itp)
- chcemy dociągnąć chciwie zagregowane obiekty (i nie życzymy sobie lazy loadingu gdyż nierzadko JEST ZŁY - o czym będzie w następnym poście)

Nawet jeżeli w pierwszej iteracji nasze pobranie danych jest prostym find() to zwykle za miesiąc zmieni się w kwerendę. Nie chcemy zatem programować metodą Kopiego-Pasty. Dzięki istnieniu DAO możemy reużywać złożoną logikę zapytań oraz oczywiście zmieniać je w 1 miejscu.

Separacja dla mnie osobiście separacja kodu - choćby zapytania były trywialne albo totalnie niereużywalne jest wystarczającą zaletą przemawiającą za DAO.


Potrafię sobie wyobrazić, że żadne z powyższych uwarunkowań nie występują. Tzn:
- nigdy nie zmieniamy źródła danych - co jest naturalne
- nie testujemy kodu - nie oszukujmy się, taka jest rzeczywistość
- nie mamy złożonych zapytań i logiki sklejania HQLa

Wówczas możemy radośnie używać EntityManagera w kodzie biznesowym. Sugerowałbym jednak kompromis: wystarczą osobne klasy realizujące logikę dostępu do danych - bez abstrakcji: bez interfejsu, bez wstrzykiwania.



Wąski kontekst
W bardzo prostych aplikacjach trzymanie się ściśle architektury z warstwą DAO niesie ze sobą pewnie narzuty i utratę prostoty. Mam tu na myśli niezbyt ambitne ale bardzo potrzebne na rynku i popularne aplikacje typu przeglądarka bazy danych. Po prostu łatwiej i taniej jest wyprodukować za 1 mln zł system, który w przeglądarce wyświetli tabelkę z listą kalesonów w hurtowni niż postawić samą bazę danych i przyuczyć pracowników z podstaw SQL;)))

W tego typu systemach zwykle corowa funkcjonalność to ekrany z różnego rodzaju listami + kontrolki do wpisywanie kryteriów + button SZUKAJ. Do tego ekran podglądu i edycji.

Najbardziej produktywne jest wówczas podejście minimalistyczne. Warstwa prezentacji tworzy obiekt Criteria i przesyła go niżej do wykonania. Przekombinowane podejście polegające na tworzenie warstwy DAO, która abstrahuje źródło danych zmusza nas również do abstrahowania od kryteriów wyszukiwania. GUI musi wysyłać jakieś DTO, które w DAO jest przepakowywane w Criteria. Generalnie: nakładanie gaci przez głowę.

Szerszy kontekst
Dla kontrastu rozważmy hipotetyczny przykład skomplikowanego dostępu do danych.
Wyobraźmy sobie, że w naszym systemie mamy użytkownika przechowywanego w 1 bazie danych. Z niewiadomych przyczyn jego zamówienia są przechowywane w innej bazie. Adresy tegoż użytkownika są pobierane z zewnętrznego systemu przez jakiś WebService.
Załóżmy też, że w zależności od wdrożenia druga baza danych (ta z zamówieniami) oraz źródło adresów są zmienne.

Podążając za DDD stworzylibyśmy agregat User oraz repozytorium UserRepository, które jest w stanie go zebrać i poskładać do kupy. Repozytorium mogłoby używać EntityManagera do pobrania encji User oraz dwóch DAO do pobrania zamówień oraz adresów z abstrakcyjnych źródeł. Problem dostępu do danych może zawierać w sobie również aspekt przepakowania obiektów z jednej domeny (np z innego systemu) do domeny innej.


Przykład powyższy może wydawać się nieco wydumany i przesadzony, ale w rzeczywistości tego typu spawy istnieją i nie należy o tym zapominać. Chciałem tylko pokazać, że architektura dostępu do danych wcale nie jest trywialnym problemem jak przedstawia się to w niektórych materiałach lub (o zgrozo!) przez niektórych członków grup eksperckich JEE. Owszem często taki jest. Ale spektrum problemów jest szerokie i po specyfikacji platformy korporacyjnej spodziewalibyśmy się czegoś więcej.

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

Na zakończenie pamiętajmy aby dobierać sobie młotek do problemu, a architekturę dostępu do danych do... też do problemu:)

Sentencja Alberta Einsteina "Wszystko powinno być tak proste, jak to tylko możliwe, ale nie prostsze" może wydawać się trywialna, ale często o niej zapominamy.

A do implementacji większości systemów i tak nie potrzeba żadnego JPA - wystarczy Excel + jakieś cwane makro... no może Access ostatecznie;P

3 komentarze:

Piotr Paradzinski pisze...

Dzięki za rozwinięcie tematu.

Sugerowany przez Ciebie kompromis uwazam za niezle rozwiazanie.

Zastanawiam sie jednak nad tym jakie dokladnie jest stanowisko pana Adama na temat DAO. Moze nas tylko podpuszcza :) Tym bardziej ze wzgledu na wypowiedz: "I much prefer the Repository (as described in the Domain Driven Design) to the DAO pattern which focus a lot more on the semantic aspect instead of the techical ones and show why the repository abstraction is important."

Moze na GeeCON uda sie to ustalic.

Michał Maryniak pisze...

Bardzo konkretnie podsumowane i przedstawione - pozwolę się jednak w kilku punktach nie zgodzić.

1) Testowanie - oczywiście przeprowadzanie testów na bazie "produkcyjnej" niekoniecznie jest dobrym pomysłem. Mimo to w dalszym ciągu bardzo często może i powinna to być baza SQL (po to przecież mamy JPA aby mieć SQL). Jeżeli WIELKIE I POWAŻNE BAZY zdają się być za "ciężkie". (piszesz: "(...)na czas testów nasza logika może używać innej implementacji DAO (np takiej, która nie korzysta z bazy danych)(...)") - przecież łatwo możemy użyć jakiejś bazy "in memory" (http://en.wikipedia.org/wiki/In-memory_database) - podmianka ta będzie miała miejsce na poziomie konfiguracji, a nie na poziomie dao (którego przecież nie mamy ;-) ).

2. Reużywalność
piszesz: "(...)Dzięki istnieniu DAO możemy reużywać złożoną logikę zapytań oraz oczywiście zmieniać je w 1 miejscu. (...)" Nie możemy jednak zapominać, że w JPA mamy NamedQuery, które oczywiście są parametryzowane. Tak to się raczej powinno robić i naturalnym miejscem umieszczenia takich NamedQuery jest Encja JPA. Mieszanie kodu *QL'owego z kodem "logiki biznesowej" jakoś uderza w moje poczucie estetyki.

Niemniej zdarzyć się może, że konkretne złożone i mocno wydumane zapytanie *QL nie pasuje do żadnej z Encji, gdyż dotyczy wielu bytów i złożonych relacji między nimi - wtedy rzecz jasna coś na kształt DAO pasuje jak ulał.

Nie podważam argumentacji zawartej w podtytule "Szerszy kontekst" - jeżeli sam dostęp jest tak złożony jak w opisanym przypadku - jasne, trzeba go superspecjalnie oprogramować - to nie podlega dyskusji. Jednak nie oszukujmy się nie jest to aż tak często.

Co do pochówku DAO powiedziałbym więc raczej w ten sposób: "śmierć starym 'głupim' DAO" stosowanym w każdej sytuacji wyłącznie ze względu na niedoskonałości danego 'frameworka' - niech jednak żyją 'tworzone celowo i świadomie' nowe 'mądre' DAO ku chwale i radości ich twórców!"

Ot to takie moje 3 grosze. Pozdrawiam

Sławek Sobótka pisze...

Co do testów to jednak idea testów jednostkowych jest taka aby odseparować testowany kawałek i zając się nim "w warunkach laboratoryjnych". Dzięki temu wiesz, że gdy masz błąd to na pewno w testowanej klasie a nie przypadkiem w jej zależnych komponentach (oczywiście przy założeniu, że mock/fake/stub DAO wstrzyknięte na czas testów jest na tyle trywialne, że bezbłędne)

Natomiast w przypadku testów integracyjnych - gdzie testujemy większą całość - baza (np jakiś hypersonic) może być.

Oczywiście w rzeczywistości często jest tak, że jedyne testy jakie mamy to właśnie integracyjne;P

Odnośnie reużywalności to jak zwykle wszystko zależy od projektu i problemu. Ja do tej pory (5 lat hibernate) w większości miałem przypadki, gdzie kwerenda była dynamiczna ponieważ:
- były różne (zmienne) kryteria wyszukiwania
- chciałem chciwie dociągnąć różne zagregowane ancje - w zależności od use case.

Podoba mi się podsumowanie o pochówku - pragmatyczne podejście.