You are on page 1of 5

INŻYNIERIA OPROGRAMOWANIA

Sławomir Sobótka

Domain Driven Design krok po kroku
Część IVa: Skalowalne systemy w kontekście DDD
- architektura Command-query Responsibility Se-
gregation (stos Write)
Czy możliwe jest stworzenie systemu, który będzie charakteryzował się otwartym
na rozbudowę modelem, eleganckim, testowalnym i utrzymywalnym kodem, a jed-
nocześnie będzie przygotowany do skalowania? Czy narzędzia typu Object-relatio-
nal mapper są panaceum na wszystkie problemy persystencji w systemach bizneso-
wych? Czy baza relacyjna to zawsze najlepszy pomysł na przechowywanie danych?
Na te i inne pytania odpowiemy sobie w kolejnej odsłonie naszej serii.

WSTĘP Plan serii
Po omówieniu ogólnej idei DDD, technik zarówno taktycznego, Niniejszy tekst jest czwartym artykułem z serii mającej na celu
jak i strategicznego modelowania oraz sposobów wykorzysta- szczegółowe przedstawienie kompletnego zestawu technik mo-
nia popularnych frameworków przyszedł czas na przyjrzenie delowania oraz nakreślenie kompletnej architektury aplikacji
się architekturze systemu jako całości. Zakładamy, że tworzy- wspierającej DDD.
my system, co do którego postawiono następujące wymagania Część I: Podstawowe Building Blocks DDD;
niefunkcjonalne: Część II: Zaawansowane modelowanie DDD – techniki strategicz-
ne: konteksty i architektura zdarzeniowa;
PP dostęp poprzez wiele technologii klienckich: web (w tym Część III: Szczegóły implementacji aplikacji wykorzystującej DDD
ajax), mobile, web service..., na platformie Java – Spring Framework;
PP skalowanie poszczególnych modułów (w sensie wydzielo- Część IV: Skalowalne systemy w kontekście DDD – architektura CqRS;
Część V: Kompleksowe testowanie aplikacji opartej o DDD;
nych jako produkty funkcjonalności) w celu zapewnienia
Część VI: Behavior Driven Development – Agile drugiej generacji.
wydajności,
PP odporność na awarie pojedynczych modułów – system jako PP Java – Spring
całość pozostaje stabilny, PP Java – EJB 3.1
PP stworzenie platformy otwartej na rozszerzenia poprzez wpi- PP .NET - C#
nanie pluginów.
DWIE KLASY PROBLEMÓW
W celu spełnienia tych wymagań:
Typowy system klasy enterprise obsługuje dwa typy operacji:
PP zrewidujemy tradycyjne podejście do architektur warstwo-
wych, rozwijając je w kierunku Architektury Porst & Adapters PP rozkazy wykonujące pewne operacje biznesowe – co prowa-
(ramka „W sieci”), dzi zwykle do modyfikacji pewnej (relatywnie niedużej) ilości
PP zastanowimy się nad kwestią spójności danych – kiedy jest danych, a w konsekwencji do konieczności utrwalenia tychże
ona krytyczna, a w jakich wypadkach możemy pozwolić so- zmodyfikowanych danych,
bie na chwilową niespójność, zyskując skalowalność, PP kwerendy (w tym kontekście niekoniecznie w sensie SQL)
PP przyjrzymy się miejscu, jakie Object-relational Mapper zaj- odpytujące część danych składowanych w systemie.
muje w systemie oraz kiedy jego wykorzystanie nie jest
racjonalne, Statystycznie rzecz biorąc, ilość obsługiwanych rozkazów do ilo-
PP zastanowimy się nad odpowiednimi modelami danych i ich ści obsługiwanych kwerend (w jakimś interwale czasu) ma się
formami, jak 1 do kilkustet (tysięcy). Innymi słowy odczytów jest o kilka
PP powrócimy do zagadnienia zdarzeń omawianych w poprzed- rzędów wielkości więcej niż zapisów – śmiało możemy przyjąć,
nich częściach, aby wykorzystać ich pełen potencjał, że dla typowego systemu klasy enterprise może być 5 rzędów
PP Projekt referencyjny. wielkości.
Drugim aspektem – obok statystycznego - który różni oba
Wszystkich tych Czytelników, którzy już teraz chcieliby zapo- typy operacji jest aspekt jakościowy modelu danych. Model da-
znać się z kolejnymi zagadnieniami naszej serii, zapraszam do nych potrzebny do wykonania operacji na modelu biznesowym
odwiedzenia strony projektu „DDD&CqRS Laven”, której adres oraz dane potrzebne do prezentacji wizualnej lub komunikacji z
znajduje się w ramce „W sieci”. Dostępne wersje: innymi modułami to zwykle inne struktury.

66 / 4 . 2012 . (4)  /

„uproszczeń”.. Świat relacyjny na obiektowy mapujemy po to. order. to narzędzie typu ORM nie jest najlepszym rozwiązaniem tego problemu. Natomiast postać ta w przypadku operacji typu Kwerenda PP Mam możliwość korzystania z mechanizmu Lazy Loadingu. nych optymalizuje działanie w tym trybie.save(order). które nie będą wykorzystywane. Decission Support). Ale jeżeli klienty są zdalne (np. wprowadzając warstwy: części serii – przypomnijmy: Order i Invoice to persystentne Agregaty: PP prezentacji. Jeżeli natomiast chcemy wyświetlić na ekranie dane. kujące dane.save(invoice).wraz z wy.bottega. np. PP Pracujemy na puli połączeń zestawionej w trybie READ-WRI- Jeżeli stosujemy ORM (np. nie mam na myśli "edytuję. / www. to używamy odpowiedniego „młotka” do odpowied. WARSTWY SĄ DOBRE. przedstawiona na Rysunku 1 rozdziela odpowiedzialność kodu. która ukaże się w przyszłym miesiącu i będzie klienta biznesowego najważniejsze są tabelki.pl / 67 . Co prawda jest to kwestia oczywista. webowej (ta sama maszyna pobiera i prezentuje dane) nie godnymi mechanizmami typu Lazy Loading. Być może w prostych aplikacjach z prezentacją w technologii PP Pobierać w wygodny sposób obiekty biznesowe . Dodatkowo Agregaty mogą zawierać dane zupełnie nie.isSatisfiedBy(order)) typu Command – operacja biznesowa i zmiana stanu systemu.możemy tutaj tworzyć zarówno anemiczne encje modyfi. Czyli pobieramy kilka obiektów bizneso- wych (Agregatów). Model odpowiedni do operacji biznesowych orderRepository.erp. ale w brak redundancji. public void approveOrder(Long orderId) { Architektura Command-query Responsibility Segregation Order order = orderRepository. throw new OrderOperationException("Order does not Drugi stos „Read” jest mniej złożony i zawiera serwisy wyszu- meet specification". PP Silnik mapera wykonuje niepotrzebne operacje związane ze Stosowalność Object-relational Mapper wsparciem dla Lazy Loading i Dirty Checking. order. czwartą część serii po. która modyfikuje mój obiekt biznesowy (uruchamiając jego metody biznesowe lub settery. zwykle nie jest odpowiedni do szybkiego serwowania danych } } np.programistamag. PP Infrastruktury. DOMAIN DRIVEN DESIGN KROK PO KROKU Z uwagi na obszerność merytoryczną.load(orderId). nie muszą – o czym w dalszej części) wymóc decyzję o rozdzia- invoiceRepository. ki wydajnościowe tej decyzji). zapisujemy go. jest to problem . ALE DWA jąc pod formularz".stan. nie modelu domenowego wiąże się z drastycznym spadkiem kowane przez serwisy. Pisząc „zmieniam stan”. Java Persistence API) do tych klas TE. aby: PP Zdradzam model biznesowy warstwie prezentacji. PP logiki aplikacji. w których można poświęcona Kwerendom. le modelu danych. "serwerem". gdy potrzebuję na ekranie jedynie kilku ko- dążymy do Trzeciej Postaci Normalnej. PP Pobieramy z ORM listę obiektów (zamapowanych na całe ta- ne i skupiamy się na wsparciu dla modelu domenowego . i zapewnianiem kompatybilności starszych PP Utrwalać stan obiektów biznesowych .issuance(order. jeżeli jest W dotychczasowych artykułach opieraliśmy architekturę aplika- on anemiczny). Na Listingu 1 widzimy przykład z poprzedniej cji na stylu warstwowym.Purchase Service operujący na kilku persystentnych Agregatach Capability. Problemy wy- Invoice invoice = invoicingService. niej klasy problemu. jest optymalna z punktu widzenia modyfikacji przypadku komunikacji sieciowej zaczniemy odczuwać skut- danych. podpina.com. skutkuje pojawianiem się iloczynów kartezjańskich (JOIN w który nie ma sensu dla operacji typu "pobierz dane do wy- SQL).. zmieniamy jego stan. public class PurchaseService{ //. PP logiki domenowej (z ew. świetlenia" i prowadzi do dramatycznego problemu z wydaj- istotne w kontekście danej Kwerendy. ze względu na lumn z każdej tabelki (dla bazy nie robi to różnicy. na potrzeby prezentacji.sales.services. dane przekrojowe w postaci tabelki (wszyscy wiemy. dajnościowe opisane w sekcji „Potrzeba separacji” mogą (ale generateTaxPolicy(systemUser)).dodatkowo zyskujemy produktywność w PP Wykonywać na nich operacje biznesowe zmieniające ich stan pracy. w „materiałach ewangelizacyjnych” popularnych platform zmów wykrywania "brudzenia" i mechanizmu kaskadowego korporacyjnych można znaleźć nawoływanie do takich zapisu całych grafów obiektów). Android)? Zdradza- . ale się w poprzednim kroku (korzystając z wygodnych mechani. //sample: Specification Design Pattern na dwa wyraźnie stosy warstw. jak również projektować prawdziwe bezpieczeństwa (wsteczna inżynieria) oraz z koniecznością obiekty modelujące reguły i niezmienniki biznesowe (styl koordynacji prac zespołów pracujących nad "klientem" i Domain Driven Design). Mam na myśli logikę aplikacji (modelującą STOSY WARSTW SĄ JESZCZE LEPSZE Use Case/User Story). W tym wypadku POTRZEBA SEPARACJI na każdym etapie postępujemy nieracjonalnie: Projektując model danych. zwykle wybieramy podejście Relacyj. Serwis Aplikacyjny pl. if (!orderSpecification. Operations. Stos „Write” zawiera opisane Specification<Order> orderSpecification = powyżej warstwy i obsługuje omawiane wcześniej polecenia generateSpecification(systemUser). Separacja stosów nie dotyczy jedynie kodu. gdy wystarczający jest tryb READ – większość baz da- problemów. Policy. podziałem na 4 poziomy modelu: application. Listing 1. przestawiać kolejność ich kolumn).getEntityId()). który zmienił wersji klientów.submit(). nością „N+1 Select Problem”. dzielono na dwa osobne artykuły: niniejszy poświęcony Rozka. zatem pożyteczna dla operacji typu Rozkaz.czyli belki w bazie). Postać ta. że dla zom oraz część.

Jak widać na Listingach 2 i 3 styl ten polega na rozdzieleniu klasycznego Wzorca Projektowego Com. Przedstawiony na Listingu 3 podczas obsługi polecenia. Jeżeli natomiast nie mamy dostępu do AOP.orderId = orderId. Architektura Command-query Responsibility Segregation – dwa sto- ści silnego odwracania kontroli – co zobaczymy już niebawem. Alternatywą dla każdej z metod Serwisowych jest para: Com. ponieważ przy pomocy AOP można osiągnąć w klasycznym podejściu wszystko to. Dzięki mechanizmowi historii } poleceń możemy odrzucać niepożądane duplikaty bez po- public Long getOrderId() { trzeby sięgania do bazy po dane biznesowe. Jako duplikat traktujemy Command oznaczony odpowiednią commands. Ogólne zasady tworzenia Command Handlerów pozostają Wspólny punkt dostępowy pozwala na wprowadzenie intere- takie same jak tworzenia metod Serwisów Aplikacyjnych oma. Transakcyjność i bez. return orderId. Przykładowo: Command Handler jest odpowiednikiem Serwisu Aplikacyjnego z Listingu 1.SubmitOrderCommand – polecenie zatwierdzenia zamó- wienia niosące parametry z warstwy prezentacji adnotacją oraz przechowywany w rejestrze ostatnio obsługi- wanych poleceń (ostatnio.orderId). dzie traktowane jako duplikat (pozwólmy klientom wydawać pieniądze). } mand na 2 klasy. dla public boolean equals(Object obj) { przypomnienia odsyłam do Listingu 1. Komponent Gate został w szczegółach przedstawiony na Rysunku 2 Styl Serwisowy Styl Serwisowy omawialiśmy w poprzednich częściach serii. PP opartym na dostosowanym do architektury klient-serwer Wzorcu Projektowym Command. aplikacje klienckie nie mają dostępu do kodu logiki aplikacyjnej Aplikacje klienckie komunikując się z Serwerem. if (obj instanceof SubmitOrderCommand) { SubmitOrderCommand command = (SubmitOrderCommand) obj.hashCode(). czy dany Command jest oznaczony jako asyn- } chroniczny – jeżeli tak. cej niż raz jest niepożądane. przedstawiony na Li- ogólny coupling modułów. INŻYNIERIA OPROGRAMOWANIA STOS WRITE (COMMAND) Stos „Write” niejawnie omawialiśmy w poprzedni częściach – były to nasze serwisy aplikacyjne. PP Sprawdzenie. (4)  / . Command pl. wów- czas styl Commad daje nam znaczną przewagę dzięki możliwo. to następuje jego odrzucenie.bottega. ale zatwierdzenie tego samego zamówienia wię- public SubmitOrderCommand(Long orderId) { this. } mand i Command Handler. Rysunek 1. Stos „Write” możemy tworzyć w dwóch stylach: PP „klasycznym” w postaci serwisów. Dla projektantów posiłkujących się frameworkiem Spring (lub innym kontenerem wspierającym Aspect Oriented Programming) wybór stylu ma wymiar głównie estetyczny. Listing 2. 68 / 4 . sujących i pożytecznych operacji dodatkowych wykonywalnych wianych w poprzednich częściach. 2012 . Natomiast odpowiadający Wspólny punkt – miejsce na odwracanie mu Command Handler zawiera logikę obsługującą Command. wysyłają – ma to oczywisty wpływ na bezpieczeństwo (dekompilacja) i Command na wspólny punktu dostępowy. co oferuje podejście oparte o styl Command. to wówczas odkładamy jego wyko- @Override nanie np. stingu 4 oraz zilustrowany na Rysunku 2.erp.equals(command. do Kolejki.. czyli w czasie x milisekund lub w @SuppressWarnings("serial") @Command(unique=true) obszarze x MB pamięci lub w ilości x). sy warstw. czy dany Command jest duplikatem (przykła- dowo atak DOS) – jeżeli tak.sales. } Styl Command & Command Handler @Override public int hashCode() { return orderId.. Command – przesyłany przez Tier Kliencki zawiera jedynie parametry żądania. dzięki zastosowaniu Adnotacji interpretowanych przez Spring } Framework. return false. kontroli Dzięki temu w odróżnieniu od klasycznego Wzorce Command. Na- tomiast w niniejszym artykule – bazując na założe- niach i wiedzy z poprzednich części – wprowadzimy pewne rozszerzenia. Przykładowo dodanie public class SubmitOrderCommand implements Serializable{ kilkakrotnie tego samego produktu do zamówienia nie bę- private final Long orderId. PP Sprawdzenie. pieczeństwo zostały wprowadzone poprzez mechanizmy AOP return orderId.application.com.

@Override public Void handle(SubmitOrderCommand command) { Order order = orderRepository. Specification<Order> orderSpecification = generateSpecification(systemUser).class).issuance(order.//skip duplicate } if (isAsynchronous(command)){ //TODO add to the queue.bottega. Command commandAnnotation = command.programistamag.run(command). if (! orderSpecification.command.cqrs.com.com.command. spying.RunEnvironment pozwalające na wprowadzenie dodatko- wych mechanizmów Odwracania Kontroli @Component public class RunEnvironment { public interface HandlersProvider{ CommandHandler<Object. Command pl. DOMAIN DRIVEN DESIGN KROK PO KROKU Listing 3. Queue should send this command to the RunEnvironment return null. } } / www. storing commands. transaction management.pl / 69 . } private boolean isAsynchronous(Object command) { if (! command. } return runEnvironment. public Object run(Object command) { CommandHandler<Object. //Domain logic order.load(command. etc Object result = handler.isSatisfiedBy(order)) throw new OrderOperationException("Order does not meet specification".sales.getEntityId()).StandardGate – wspólny punkt dostępowy @Component public class StandardGate implements Gate { @Inject private RunEnvironment runEnvironment. @Inject private SystemUser systemUser.impl.asynchronous(). @Override public Object dispatch(Object command){ if (! gateHistory. logging. } Listing 4.save(invoice).SubmitOrderCommandHandler – polecenie zatwierdzenia zamówienia niosące parametry z warstwy prezentacji @CommandHandlerAnnotation public class SubmitOrderCommandHandler implements CommandHandler<SubmitOrderCommand. Object> getHandler(Object command).class)) return false. return null. //You can add Your own capabilities here return result. } } Listing 5.impl. security. return commandAnnotation. private GateHistory gateHistory = new GateHistory().getOrderId()). @Inject private InvoicingService invoicingService.getHandler(command). } @Inject private HandlersProvider handlersProfiver.commands.erp.getClass().application. order. //Domain service Invoice invoice = invoicingService.register(command)){ //TODO log.submit(). Środowisko uruchomieniowe pl.bottega. @Inject private InvoiceRepository invoiceRepository. Object> handler = handlersProfiver. //You can add Your own capabilities here: dependency injection.cqrs. Command pl. generateTaxPolicy(systemUser)). invoiceRepository.info(duplicate) return null. orderRepository.com.handlers.isAnnotationPresent(Command. profiling.save(order).handle(command). Void> { @Inject private OrderRepository orderRepository.getAnnotation(Command.bottega.getClass().

W wolnych chwilach działa w community jako: prezes Stowarzyszenia Software Engineering Professionals Polska (http://ssepp. Jeżeli zapisujemy w jednej transakcji kilka Agrega- tów. Zdarzeń możemy również używać jako Behwioralnego Modelu PP Profilowanie działania systematu. Zakładając oczywiśćie. Zdarzenia będą nam również potrzebne. ale za- stanówmy się nad konsekwencją jej stosowania. publicysta w prasie branżowej i blogger (http://art-of-software. Zdarzenia do tej pory stosowaliśmy w celu: Dzięki scentralizowanemu punktowi uruchamiania logiki apli- kacji mamy możliwość dokonania całkowitego odwrócenia kon. Jest to oczywiście jedynie wskazówka. które omawialiśmy w poprzedniej części naszej serii. Odpowiedzialność Środowiska Uruchomieniowego jest pro. to po prostu z wymagań. ale zapisać – tylko jeden.com/cgi/wiki?PortsAndAdaptersArchitecture Sławomir Sobótka slawomir.com.pl). Zdarze- PP Zwrócenie Handlera odpowiedniego dla pracującego w sys.com PP Architektura CqRS http://martinfowler.pl Programujący architekt aplikacji specjalizujący się w technologiach Java i efektywnym wykorzystaniu zdobyczy inżynierii oprogramowania. czego potrzebuje architekt projektujący wy- sokowydajny i skalowalny system. jeżeli musimy zapisać więcej niż je- den Agregat podczas obsługi Command? Jeżeli wynika Przyjrzyjmy się teraz Listingowi 5. 2012 .pdf PP przykładowy projekt – Java (Spring i EJB): http://code. Przypomnijmy. W sieci PP oficjalna strona DDD: http://domaindrivendesign. Trener i doradca w firmie Bottega IT Solutions.sobotka@bottega. Rozwiązaniem mogą być Zdarzenia Do- wisko uruchomienia polecenia.codeplex.com/p/ddd-cqrs-sample/ PP przykładowy projekt . PP Logowanie poczynań klientów. Zdarzenia Co zatem zrobić. Danych i zastąpić nimi Relacyjną Bazę w Stosie Write. PP komunikacji pomiędzy osobnymi Bounded Context troli. który przedstawia środo.html PP Architektura Porst and Adapters: http://c2. że transakcje rozproszone (Double Phase Commit) nie jest tym. to nie możemy sobie pozwolić na ich utrwalanie w osobnych bazach danych. PP Sprawdzenie uprawnień do wykonania Handlera. w dane do odczytu w drugim stosie naszej Architektury CqRS. Ale o tym w kolejnej odsłonie serii. Komponent Gate stanowiący wspólny punkt dostępowy – obsługa Command wysyłanych z aplikacji klienckich Transakcyjność vs wydajność Jedną z głównych zasad projektowania skalowalnych systemów z wykorzystaniem DDD jest zapisywanie podczas transakcji jednego Agregatu. INŻYNIERIA OPROGRAMOWANIA Rysunek 2. menowe. wych”. nia mogą być analizowane przez systemy CEP (Complex Events temie klienta – zagadnienie wersjonowania API i systemy Processing) oraz służyć jako wektory uczące dla Sztucznych wspierające multi-tenant.pl/pdf/materialy/sdj-ddd.NET: http://cqrssample. jej zatwierdzenie albo wycofanie w zależności od powodzenia jak również Polityki DDD) działania Handlera.com/bliki/CQRS.com. aby odświeżać dane PP Wstrzyknięcie zależności do Handlera. którą mo- żemy złamać w uzasadnionych przypadkach. których natychmia- stowe wykonanie nie jest krytyczne PP Rozpoczęcie transakcji przed uruchomianiem Hadlera oraz PP otwarciu systemu na Pluginy (którymi są Listenery Zdarzeń. że Agregaty emitują zdarzenia niosące informa- sta: dopasować do Command odpowiedni ComamndHandler i cje o istotnych w cyklu życia Agregatów „wydarzeniach bizneso- wykonać go.blogspot. PP Dekorowanie Handlerów – wzorzec Dekoratora. (4)  / .org PP wstępny artykuł poświęcony DDD: http://bottega.google. bez użycia kontenera typu Spring: PP odłożeniu wykonania do kolejki operacji.com). Sieci Neuronowych. Odczytać (w celu wykonania operacji biznesowej) możemy oczy- wiście kilka Agregatów. 70 / 4 .