Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Simon says...

Szymon Pobiega o architekturze i inżynierii oprogramowania
Re: NHibernateStarter...
Czyli nawiązanie do świetnego posta Procent-a na temat jego sposobu budowy aplikacji. Poniżej zamieszczam moje aktualne poglądy na ten temat. Zieloną czcionką wyróżniłem fragmenty porównujące oba rozwiązania.

O czym w ogóle mowa

Zawsze na początku należy sobie zadań takie pytanie. O jakich w ogóle aplikacjach mówimy? Procent opisywał przypadek nietrywialnej aplikacji biznesowej (OLTP), więc i ja posłużę się takową. Termin "nietrywialny" oznacza dla mnie znaczący stopień skomplikowania i/lub zmienności domeny problemu i implikuje (dla mnie) konieczność zastosowania praktyk związanych z Domain-Driven Design. Nie oznacza to, że do wszystkiego stosuję DDD, o nie! Akurat przypadkiem ostatnio jestem zaangażowany w tworzenie tego rodzaju systemów...

Poniższy stanowi dla mnie punkt wyjścia — swego rodzaju model referencyjny, który ułatwia mi projektowanie kolejnych aplikacji, ponieważ nie muszę zaczynać całego procesu za każdym razem od początku. Nie jest jednak sztywny i zawsze konfrontuje go z wymaganiami (funkcjonalnymi i niefunkcjonalnymi) konkretnej budowanej aplikacji.

Cebula ma warstwy, ogry mają warstwy...

...i aplikacje także mają warstwy. W moim referencyjnym modelu aktualnie znajdują się cztery warstwy fizyczne (tiers):
  • cienki klient (przeglądarka)
  • warstwa GUI (ASP.NET)
  • warstwa aplikacji
  • serwer bazy danych (RDBMS)
GUI ASP.NET jest oczywiście hostowane na IIS. Warstwa ta komunikuje się bezpośrednio w warstwą bazy danych. Warstwa aplikacji zrealizowana jest za pomocą serwisu Windows, który pozwala na realizację:
  • zadań harmonogramowanych
  • usług WCF
  • usług NServiceBus
Ta warstwa także ma bezpośredni dostęp do bazy danych. Nie ma w moim modelu miejsca na przesyłanie danych Web Service-em, typowego dla promowanego przez Microsoft modelu n-tier. Dlaczego? Dlatego, że, jak zaraz się dowiecie, nie wykorzystuję (prawie) warstwy dostępu do danych, więc nie ma wysyłających i odbiorców danych. Nie kastruję swojej architektury wykorzystując O/RM jedynie jako implementację serwisu dostępu do danych.

Warstwy logiczne (layers) są w większości wspólne zarówno dla GUI, jak i dla warstwy aplikacji:

Warstwa persystencji modelu domeny

Zawiera implemenację tzw. repozytoriów, czyli elementów modelu domeny, których zadaniem jest wytworzenie iluzji, że całość modelu znajduje się w pamięci. Interfejs repozytoriów ogranicza się do metod pobierających (zwykle po ID) oraz insertujących dla najważniejszych obiektów biznesowych (aggregate roots). Drugim elementem warstwy persystencji są mapowania O/RM odwzorowujące klasy modelu domeny na struktury bazodanowe.

(Szczątkowa) warstwa dostępu do danych

Zawiera metody dostępu do danych niezbędne do realizacji elementów interfejsu użytkownika, w szczególności takich, jak widoki list oraz szczegółów (master-detail). Dla każdego obiektu, który jest osobno prezentowany na GUI istnieje interfejs IXxxFinder (wraz z implementacją). Interfejs ten typowo zawiera metody: GetById, Find, GetCount, FindBySomething itp. Zwracają one obiekty DTO, które są tworzone nie na bazie obiektów modelu domeny (AutoMapper?), ale na bazie danych z bazy.

To różnica w porównaniu do tego, co proponuje Procent. Dlaczego wybrałem takie rozwiązanie? Dlatego, że pozwala mi oddzielić łatwo logikę domenową od GUI. Obiekty modelu nie muszą eksponować danych na potrzeby interfejsu, a co za tym idzie — są bardziej stabilne. Wszelkie komendy z GUI do modelu domeny przechodzą przez warstwę aplikacji, dzięki czemu GUI w żadnym punkcie nie styka się bezpośrednio z modelem.

I jeszcze drobny szczegół implementacyjny: zwykle pakuję tę warstwę wraz z warstwą persystencji modelu do jednego assembly, aby uprościć strukturę wdrożeniową.

Warstwa modelu domeny

Tu zlokalizowane jest serce aplikacji — większość logiki biznesowej. Ta warstwa nie referuje zwykle do żadnej zewnętrznej biblioteki. Kod ma być czytelny nawet dla nie-programisty — powinien mieć sens biznesowy. Na temat tego, co zawiera dobry model można poczytać tu i tam, nie będę więc się na ten temat rozpisywał. To, co jest ważne w kontekście tego posta to to, że z tej warstwy nie ma odwołań do bazy danych. Żadnych.

Warstwa aplikacji

Jest bardzo cienką warstwą zlokalizowaną ponad modelem domeny. Stanowi jego fasadę. Wszystkie odwołania do modelu przechodzą tędy. Faktycznie jest to zestaw pod-warstw zlokalizowanych na tym samym poziomie abstrakcji dostosowanych do "aktora" wywołującego dany przypadek użycia:
  • użytkownika, poprzez interfejs WWW ASP.NET
  • infrastruktury komunikacji asynchronicznej (NServiceBus)
  • infrastruktury komunikacji synchronicznej (WCF)
  • modelu domeny
Ten ostatni przypadek jest szczególnie interesujący. Dotyczy on zdarzeń domenowych (jak to opisał Udi): obiekty modelu zgłaszają zdarzenia, które są obsługiwane przez obiekty warstwy aplikacji. Jest to sposób na eleganckie wypychanie informacji z modelu.

W tej warstwie wykorzystuję NHibernate bezpośrednio lub za pośrednictwem repozytoriów (w zależności od nastroju, czytaj: specyfiki projektu). Nie mam, podobnie jak Procent, żadnych oporów przed bezpośrednim dostępem do O/RM-a. Wszelkie warstwy abstrakcji nie mają sensu, ponieważ i tak zmiana O/RM-a na inny (EF 4?) jest nierealna. Nie używam jednak konstrukcji typu DataAccessFacade.InTransaction, ponieważ moimi transakcjami zwykle zarządza zewnętrzny framework (NSB). Tylko w wypadku wywołań z poziomu ASP.NET wykorzystuję transakcje bezpośrednio, ale nawet w tym wypadku są to nie transakcje NHibernate, ale System.Transactions. Dlaczego? Ponieważ nigdy nie wiem, czy dany przypadek użycia nie zostanie rozbudowany o wysyłanie komunikatów NSB, a w tym wypadku muszę użyć transakcji rozproszonych (SQL + MSMQ). System.Transactions zapewnia mi automatyczną, przeźroczystą eskalację transakcji.

GUI ASP.NET / aplikacja serwisu Windows

Nic specjalnego w tej warstwie — ot standardowe aplikacje.

Podstawa to dobry stos

W tym wypadku chodzi o stos technologii. Chodzi o zestaw narzędzi, którymi posługujesz się tworząc aplikację. Z czasem każdy wypracowuje sobie swoje własne preferencje. Moje dosyć szybko ewoluują. Stan na dziś jest następujący:

NHibernate

Używam NHibernate także jako warstwy dostępu do danych, ale przede wszystkim jako biblioteki do persystencji modelu domeny. O ile tę pierwszą dałoby się łatwo wykonać samemu, o tyle ta druga jest bardziej skomplikowana i mocno wykorzystuje zaawansowane funkcje NHibernate, takie jak leniwe ładowanie, cache 2-go poziomu czy też dziedziczenie.

W przeciwieństwie do Procenta wykorzystuję kontekstowe sesji w NHibernate (zamiast delegata ją zwracającego). Dlaczego? Ponieważ w porównaniu z jego modelem (o ile go dobrze rozumiem) u mnie NHibernate wykorzystywany jest w znacznie mniejszej liczbie klas. Nie boli mnie więc specjalnie wstrzykiwanie obiektu ISessionFactory i pobieranie z niego sesji kontekstowej.

Nie korzystam także z Fluent NHibernate. Nie widzę, aby jego użycie dawało jakąś znaczącą przewagę nad plikami hbm, szczególnie, jeśli podepnie się schemy XSD do Visual Studio, a R# przy zmianie nazwy property sam aktualizuje hbm-a.

W chwili obecnej rozważam zastąpienie NHibernate przez rozwiązanie Marka Nijhofa oparte o ideę CQRS Grega Younga w następnym projekcie. Oczywiście, o ile projekt na tyle niewielki, abym mógł pozwolić sobie na taki eksperyment.

NServiceBus

O tej bibliotece wiele pisałem i wiele jeszcze planuje napisać. Obecnie jest ona głównym mechanizmem komunikacji wykorzystywanym w moich aplikacjach. Komendy zlecające wykonanie operacji na modelu transportowane są kolejkami MSMQ. Efekt zaś (wypychany w formie zdarzeń domenowych) jest propagowany za pomocą publikowanych wiadomości. Więcej na temat wykorzystania NSB w omawianym rodzaju aplikacji można przeczytać tutaj.

RhinoMocks

Jeśli już korzystam z frameworku do mock-owania, wybieram ten. Staram się jednak nie korzystać. Dlaczego? Ponieważ dobrze zaprojektowany i wykonany model domeny może być testowany w izolacji od bazy danych bez użycia mockera. Jeśli już koniecznie muszę coś zamockować, korzystam ze znakomitego tutoriala by Procent.

MSTest

To pewnie kontrowersyjna opinia. Używam MSTest, ponieważ odpalanie testów NUnit bez R# lub TestDriven.NET jest mało praktyczne. Zauważyłem, że adopcja testowania jest większa, jeśli testy można uruchamiać prościej. Gdybym miał ten komfort, że każdy programista dysponowałby R#, wybrałbym NUnit.

W chwili obecnej eksperymentuje z różnymi frameworkami wspierającymi BDD. Myślę, że w następnym projekcie spróbuje wykorzystać których eksperymentalnie.

Unity

Kolejny kontrowersyjny wybór. Unity używam z trzech powodów. Po pierwsze jest lekki (niewiele kodu). Po drugie przez sentyment (przeczytałem kod Unity, napisałem doń kilka add-in-ów, znam go jak własną kieszeń). Po trzecie zaś Unity ma proste API, które jet zrozumiałe dla programistów nie znających wszystkich słodyczy syntaktycznych C# (lambdy, closure-y).

Nie potrzebuję zwykle zaawansowanych funkcji kontenera, które są wbudowane w Spring-a, czy Castle, więc nie chce za nie płacić dodatkowym skomplikowaniem.

Common.Loggin i log4net

Przez długi czas byłem wielkim fanem Enterprise Library i jej frameworku do logowania. Niestety okazało się, iż jego użycie powoduje problemy ze strony NHibernate, NServiceBus oraz Quartz.Net. Biblioteki te albo korzystają bezpośrednio z log4net, albo używają Common.Logging jako abstrakcji. Ostatecznie poddałem się. Szczególnie, że z czasem okazało się, że nie potrzebuje także pozostałych modułów EntLib (poza jednym, ale o tym zaraz).

Quartz.Net

Quartz jest świetną biblioteką do harmonogramowania zadań. Ma ogromne możliwości, z krtrych zwykle wykorzystuję maleńki ułamek — uruchamiania zadań co zadaną ilość czasu. API Quartz jest jednak tak proste, że nie czuję się przezeń przytłoczony, a mam komfort, że zawsze mogę wpiąć się w zaawansowaną funkcjonalność.

Policy Injection Application Block

PIAB-a używam jako uniwersalnego rozwiązania AOP. Świetnie integruje się z Unity pozwalając na bardzo wygodną realizację takich aspektów jak transakcje czy logowanie wyjątków. Dodatkowo daje mi komfort w postaci możliwości określania polityk w app.config. Dzięki temu mogę spać spokojnie — kiedy moja aplikacja się wysypie administrator może samodzielnie zmienić politykę tak, aby logować każde wywolanie metody itp.

Topshelf

Jest to efektu uboczny projektu MassTransit. Topshelf to uniwersalna biblioteka do budowy serwisów Windows. Poza wieloma świetnym funkcjami, których nie zamierzam tu omawiać, ma jeden killer-feature: projekt wykorzystujący Topshelf uruchamia się jak normalna aplikacja konsolowa z poziomu Visual Studio, ale może być zainstalowany jako serwis Windows na środowisku produkcyjnym.

Testowanie

Osobiście jestem zwolennikiem testowania jednostkowego. Jak już wspominałem, dzięki modelowi domeny nie muszę się uciekać do framework-ów mockujących, aby przetestować model w izolacji (izolacja jest niezbywalną cechą samego modelu). Doceniam także sens posiadania kilku testów integracyjnych, które testują za jednym razem całóść funkcjonalności aplikacji, z tym że w tym wypadku moim celem nie jest 100% pokrycia logiki, ale testowanie wszystkich mechanizmów.

Dlatego też w wypadku testów integracyjnych, mimo iż stosuję SQLite (podobnie jak Procent), używam go w trybie plikowym z możliwością otwarcia wielu sesji. Jest to dla mnie warunek niezbędny, aby sprawdzić, czy dobrze zarządzam sesjami. Gdybym przez całą długość testu (który przechodzi przez wiele przypadków użycia) wykorzystywał jedną sesję, byłby to właściwie test bez O/RM-a. Dzięki wykorzystaniu wielu sesji testowaniu poddawana jest także poprawność mapowań NHibernate.

W ostatnio budowanym modelu pokusiłem się jednak o (w moim mniemaniu) śmiały eksperyment. Zamiast testować jednostkowo, stworzyłem większy (kilkanaście testów) zestaw testów integracyjnych, który pokrywa dokładnie 100% kodu modelu domeny. Pierwszy wniosek, jaki z eksperymentu wyciągnąłem to objawienie, że potrzeba o rząd wielkości mniej testów integracyjnych, aby zapewnić ten sam stopień pokrycia (w porównaniu do testów jednostkowych). Oczywiście, jak już pisałem, 100% niczego nie udowadnia. Ciekaw jestem, jak mój eksperyment sprawdzi się w praktyce patrząc z perspektywy roku czy dwóch.

A Ty, drogi Czytelniku? Jaki jest Twój sposób na budowanie aplikacji OLTP?

Opublikowane 1 grudnia 2009 09:12 przez simon

Komentarze:

# Re: NHibernateStarter... @ 1 grudnia 2009 10:28

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

dotnetomaniak.pl

Komentarze anonimowe wyłączone