19 listopada 2008
Odsłon: 2831
W poprzedniej części przedstawiłem metody ogólnego podziału systemu na warstwy logiczne i fizyczne. Teraz czas zająć się problemem, który poprzednio tylko zarysowałem, czyli: jak wpasować logikę biznesową w strukturę warstw logicznych oraz jak zorganizować przepływ sterowania, żeby mogła być ona niezależna od infrastrukturalnej części systemu?
Przede wszystkim zakładamy, że logika biznesowa ma znaleźć się w całości (albo chociaż w znaczącej części) w warstwie pośredniej. Zatem zgodnie z tym co napisałem poprzednio, warstwa logiki powinna teraz zawierać trzy różne rodzaje elementów:
- Model. Dane "biznesowe" w jakiejś reprezentacji programowej dla wygody przetwarzania. Mogą to być klasy, ale też np. struktury albo DataSety (na wartościowanie ich w kontekście DDD przyjdzie czas nieco później).
- Logika biznesowa. Operacje przeprowadzane na modelu.
- Logika aplikacji. Obsługa wyjątków, autoryzacja, kontrola transakcji, koordynacja pobierania danych z warstw niższych itd.
Elementy warstwy pośredniej. Jak je zorganizować?
Wszystkie te elementy są niezbędne i każdy jeden nie ma sensu bez pozostałych. Jak zatem najlepiej je ze sobą powiązać, jak zorganizować przepływ sterowania i komunikację między nimi? Jest na to znanych co najmniej kilka sposobów. Martin Fowler w książce "Patterns of Enterprise Application Architecture" wymienia trzy najważniejsze z nich: Transaction Script, Table Module oraz Domain Model.
Transaction Script
To prawdopodobnie najpopularniejszy wzorzec, szczególnie w przypadku systemów legacy i zespołów doświadczonych, przyzwyczajonych do tradycyjnych metod. Zgodnie z Transaction Script, cała warstwa logiki powinna zostać zorganizowana w procedury, które z kolei mogą być pogrupowane w klasy. Poszczególne procedury odpowiadają przypadkom użycia systemu. Realizują one całe przetwarzanie: pobierają dane z warstwy niższej, operują na modelu w celu wykonania zadanego scenariusza biznesowego, a na koniec zapisują zmodyfikowane dane lub zwracają je do warstw zewnętrznych.
Transaction Script z lotu ptaka
Przykładowo, implementacja przypadku użycia pt. "Klient otrzymuje specjalną zniżkę, jeżeli wartości jego zamówień przekroczyły graniczną kwotę" według wzorca Transaction Script mogłaby wyglądać następująco:
public class CustomerService
{
public void RewardCustomer(int id)
{
DataTable customers = DB.GetCustomer(id);
DataRow c = customers[0];
if ((decimal)c["TotalOrderAmount"] > 1000.0m)
{
DataTable orders = DB.GetCustomerOrders(id);
for (int i = 0; i < orders.Rows.Count; i++)
{
DB.UpdateOrder((int)orders[i]["Id"],
(decimal)orders[i]["Price"], 0.1f);
}
}
}
}
Zaprezentowany kod wcale nie jest skrajnym przypadkiem, choć na pewno możnaby go upiększyć, gdyby użyto np. wygenerowanych klas i Entity Framework do pobierania danych, jak w poniższym przypadku:
Application layer, podejście proceduralne. Unikać
Nie zmienia to jednak istoty rzeczy: proceduralnego (nieobiektowego) podejścia, traktowania obiektów jedynie jako kontenery na dane, oddzielenia reguł biznesowych od modelu. Transaction Script charakteryzuje się niską elastycznością, duplikacją kodu i znacznie utrudnionym testowaniem. Do tego dochodzą problemy "ideologiczne": programista musi nadmiernie koncentrować się na infrastrukturze, jest oderwany od istoty zagadnienia. Dla tak zorganizowanego kodu nie można zastosować wzorców projektowych, czy choćby prostego polimorfizmu. W praktyce oznacza to cofnięcie o kilkanaście lat pod względem metod programowania.
Gwoli sprawiedliwości należy dodać, że takie podejście nie powoduje automatycznej klęski projektu. Z pewnością ma ono swoje zalety, jak choćby to, że łatwo jest przenieść do takiej struktury starszy kod (zwłaszcza sprzed czasów .NET). Nie wymaga też wielkich umiejętności OOP od programistów, co może mieć wcale niemałe znaczenie ("programowanie obiektowe jest zawsze trudniejsze, niż ci się wydaje"). Za to tzw. programiści bazodanowi poczują się jak ryby w wodzie. Jednak w dłuższej perspektywie tak budowane systemy cierpią na różne przypadłości, opisywane już przeze mnie wyżej - i dlatego poszukujemy czegoś lepszego.
Table Module
Wzorzec podobny do poprzedniego, dlatego opiszę go jedynie skrótowo. Różni się głównie tym, że poszczególne klasy odpowiadają tabelom w bazie danych, jest zatem nieco bardziej zorganizowany niż zorientowany na procedury Transaction Script. Dzięki temu można częściowo uniknąć np. duplikacji kodu (metody jednego modułu są reużywalne dla innych), ale w dłuższej perspektywie Table Module cierpi na podobne problemy co poprzednik. Sprawdza się najlepiej w niedużych projektach, np. serwisach internetowych, o prostej logice biznesowej, sprowadzającej się zwykle do walidacji.
Domain Model
Przykładowy, prosty model dziedziny
Nazwanie Domain Model wzorcem jest trochę naciągane, gdyż oznacza on po prostu podejście polegające na tym, że staramy się odzwierciedlić dziedzinę problemu biznesowego w sposób maksymalnie zgodny z technikami programowania obiektowego. Programista może skupić się w całości na implementacji logiki biznesowej, która znajduje się tam, gdzie jej miejsce, czyli w klasach modelu. Wzorce projektowe są szeroko zastosowane, podobnie jak inne techniki OOP. "Procedury" odpowiadające przypadkom użycia w warstwie pośredniej przygotowują jedynie niezbędną infrastrukturę, delegując wszelkie operacje związane z dziedziną do klas biznesowych. Te z kolei są niezależne od wszelkich rozwiązań technicznych zastosowanych w projekcie - nie odwołują się do żadnych frameworków, kontenerów czy DAL. Kod z poprzedniego przykładu upraszcza się do postaci:
Sytuacja jak wcześniej, podejście obiektowe
Korzyści powinny wydawać się jasne: zwiększona odporność na zmiany, możliwe skuteczne testowanie jednostkowe, przedłużona żywotność projektu, większy porządek w kodzie, bardziej efektywne wykorzystanie czasu i umiejętności programistów. Domain Model błyszczy najbardziej w projektach o skomplikowanej logice biznesowej. Martin Fowler zauważa jednak, że osoby, które nauczyły się tworzyć aplikacje w taki sposób, nie chcą już powracać do poprzednich metod i dążą do stosowania Domain Model we wszystkich swoich projektach.
Podsumowanie
W porządku, ale skoro jest to takie dobre i ma tyle zalet, to dlaczego większość wybiera jednak podejścia alternatywne? Czy są to ignoranci odporni na argumenty? A może nie zdają sobie sprawy z wad, którymi te metody są obarczone? Otóż nie. Głównym powodem, dla którego Domain Model - wzorzec tak stary, jak całe programowanie obiektowe - nie jest stosowany w aplikacjach biznesowych jest to, że jego połączenie z resztą aplikacji jest - uwaga - trudne. Trudne zarówno od strony technicznej, jak i konceptualnej. Do niedawna programiści technologii Microsoft nie mieli do tego odpowiednich narzędzi, ale teraz są już dostępne porządne O/R mappery, frameworki, narzędzia do AOP czy TDD (w kolejnych częściach będę przyglądał się im bliżej). Z kolei od strony konceptualnej problem leży w odpowiedniej strukturze modelu dziedziny.
I tutaj właśnie wchodzi do gry Domain-Driven Design, proponując konkretne, usystematyzowane podejście do konstruowania modelu. Postaram się przedstawić je w kolejnej części z serii.