Czy logika aplikacyjna to część modelu domeny?
Czym jest logika aplikacyjna i co powinno się w niej znaleźć skoro jest czymś osobnym niż logika domenowa? Jak ma się ten podział do architektury warstwowej lub portów i adapterów? Czy termin logika aplikacyjna, którego używamy na co dzień, jest jasny i porządkuje naszą pracę, czy wręcz przeciwnie?
Ten artykuł nie będzie zestawieniem cytatów ze znanych książek i próbą wydestylowania z nich precyzyjnej definicji. Zamiast tego proponuję krytyczną analizę co z rzeczywistości chcemy zamodelować i jakie podziały najlepiej temu służą.
Czym jest model?
Skoro w ogóle mówimy o modelu, to warto zacząć od zastanowienia się czym on jest. Najkrócej jest to odwzorowanie rzeczywistości, w którym staramy się uwypuklić jakiś jej aspekt pomijając pozostałe. Więcej na ten temat w osobnym artykule.
Skoro model ma być odzwierciedleniem rzeczywistości, to podziały tego modelu (np. na logikę aplikacyjną i domenową), powinny jakoś odnosić się do modelowanej rzeczywistości. Odróżnienie różnych rodzajów modelu powinno opierać się na jakiejś różnicy w biznesie. Jeżeli tak nie jest, to taki podział nie odpowiada rzeczywistości, a w związku z tym raczej nie jest w stanie być dobrym narzędziem do porządkowania naszej pracy.
Czy model use case'u to coś technicznego?
Logika aplikacyjna często traktowana jest jako coś czysto technicznego, co nie powinno być modelem domeny. Już samo odróżnienie jej od logiki domenowej sugeruje takie właśnie rozumienie. Przy takim rozumieniu logika aplikacyjna nie jest w ogóle częścią modelu domeny. Co jest jednak najważniejszym elementem logiki aplikacyjnej? Zwykle command handler lub service. Co one modelują? Poszczególne use case'y, które obsługuje system. Co wchodzi w skład standardowego command handlera lub metody service'u? Odczyt danych, koordynacja zachowań poszczególnych elementów modelu, zapis zmodyfikowanych danych, publikacja zdarzeń, etc. Czy to są kwestie techniczne czy domenowe? Przyjrzyjmy się im po kolei.
Odczyt danych i zapis danych
Czy odczyt danych (agregatów z DDD, anemicznych encji, read modelu, etc.) to tylko technikalia? Jeżeli rozważamy to jak odbywa się ten odczyt (zapytania do bazy, ORMy, deserializacje) to są to z pewnością kwestie związane stricte z wykorzystywaną aktualnie technologią. Ale czy sam fakt odczytu tych danych jest kwestia techniczną? Raczej nie. Zanim wynaleziono komputery ludzie także "odczytywali" dane np. z zeszytu, wyszukiwali je w biurkach, szafach w archiwum, etc. Skoro to robili, to sam zapis danych, przechowanie ich na później, zapewnienie możliwości łatwego wyszukania, także istniał zanim pojawiły się systemy IT. Nawet fakt, że taki odczyt i zapis trwa jakiś czas i blokuje kolejne czynności, chyba że powierzymy je komuś / czemuś innemu, nie jest kwestią związaną z komputerami, tylko z fizycznym światem. Takich metod jak invoiceRepository.getBy(id)
czy invoiceRepository.save(invoice)
nie traktowałbym jako kodu nie mającego związku z biznesem. Jest to implementacja tej części modelu, która mówi nam o konieczności zachowania pewnych informacji na przyszłość i możliwości ponownego ich odczytania.
Koordynacja zachowań poszczególnych elementów modelu
Tu chyba nie ma wątpliwości, że jest to kwestia biznesowa a nie czysto techniczna.
Publikacja zdarzeń
Tu sprawa ma się analogicznie jak za zapisem i odczytem danych. Komunikacja z konkretnym brokerem jest kwestią techniczną. Samo informowanie innych o pewnym fakcie jest już jak najbardziej kwestią biznesową niezależną od istnienia systemów IT. Trzy podstawowe sposoby komunikacji wykorzystywane w systemach IT: synchroniczne żądanie (service call), asynchroniczne polecenie (command) i asynchroniczne poinformowanie o fakcie (event) miały swój wzór w interakcjach między ludźmi, a nie w konkretnych mechanizmach technicznych.
Process Model i Deep Model
Uznanie logiki aplikacyjnej za czysto techniczną nie wydaje się więc tak trafne jak można na pierwszy rzut oka sądzić. Czy jest jakieś lepsze wyjście? Czy jesteśmy skazani na mieszanie kwestii technicznych i domenowych lub sztuczne komplikowanie implementacji? A może cały problem jest sztuczny?
Moim zdaniem ten problem jest sztuczny i wynika z jednej strony z nadgorliwości w tropieniu i oddzielaniu technicznego kodu, a z drugiej w oderwaniu podziałów architektonicznych od modelowanej rzeczywistości. Samo rozróżnienie logika aplikacyjna - logika domenowa jest więc zbudowane na sztucznym problemie. Rozwiązania sztucznych problemów, zwykle nie pomagają w porządkowaniu jakiegokolwiek ludzkiego działania.
Może zamiast dzielić logikę na aplikacyjną i domenową podzielmy ją na Model Procesu i Głęboki Model, które łącznie stanowią Model Domeny.
Model Procesu skupia się na tym co dzieje się w domenie. Są to więc:
- poszczególne use case’y (np. złóż reklamację)
- sekwencje operacji biznesowych (np. wycenianie zamówienia, które składa się kilku kroków: pobranie cen katalogowych, naliczanie rabatów, przewalutowanie)
- asynchroniczne procesy (np. rejestracja konta wymagająca: ustawienia hasła, weryfikacji danych, zgody managera)
W DDD komponenty takie określa się jako Serwisy aplikacyjne.
Czym natomiast jest Głęboki Model? Jest to ta część modelu domeny, która skupia się na tym jak wykonywane są poszczególne operacje i reprezentowane informacje. Są to więc:
- wartość (np. wyceniony produkt) - Value Object z DDD
- identyfikowalne zestawy danych, zachowań modyfikujących te dane i reguł zgodnie z którymi zachodzą modyfikacje (np. grafik i operacja rezerwacji terminu) - Agregat z DDD
- dane, których modyfikacja nie podlega regułom (np. deklaratywne dane kontaktowe) - Struktura danych / Anemiczna Encja
- algorytmy (np. sposób obliczania podatku) - Serwis domenowy z DDD
- dobór algorytmów (np. wybór polityki rabatowej na dany dzień dla danego klienta) - Fabryka z DDD
Dlaczego ten model jest “głęboki”? Dlatego, że jest przykryty przez warstwę procesu i nie jest bezpośrednio dostępny przy interakcjach z systemem. Użytkownicy końcowi, czy zintegrowane systemy nie muszą (a nawet nie powinny) być świadome koncepcji, które się w nim znajdują. Jest tym co “pod maską”, o czym wiedza nie jest potrzebna do użycia funkcji udostępnionych w warstwie procesu.
Taki podział pozwala nam lepiej ustrukturyzować nasz model. Procesy mają inne powody do zmiany niż komponenty wykorzystywane do ich realizacji. Przykładowo: sposób obliczanie podatku nie zależy od tego w ramach jakiego use case’a doszło do obowiązku jego naliczenia; zmiana procesu składania zamówienia nie wpływa na reguły dostępności produktów na magazynie.
Nasuwa się tu analogia do orkiestry symfonicznej. Każdy muzyk wraz ze swoim instrumentem ma pewne możliwości i ograniczenia. Co innego da się zagrać na skrzypcach, a co innego na klarnecie. Ta sama orkiestra może jednak zagrać wiele różnych utworów. Żeby było to możliwe potrzebny jest dyrygent i partytura. Dyrygent i partytura to procesy biznesowe. Muzycy i ich instrumenty to komponenty realizujące te procesy. Potrzebujemy obydwu tych części modelu i nie ma ma sensu zacierać granicy między nimi.
Jak to się ma do architektury aplikacyjnej?
Jak rozróżnienie na model procesu i głęboki model ma się do architektury aplikacyjnej? Czy każdy z nich powinien być w osobnej warstwie?
Tu trzeba odpowiedzieć sobie na pytanie do czego służą warstwy. Ich podstawowym celem jest podzielenie modelu na spójne części o jasno określonych zależnościach. Każdą z warstw można zrozumieć znając jedynie API warstw na których ona bazuje, a nie ich implementację. Warstwy nadrzędne "wiedzą o" i "korzystają z" warstw podrzędnych, zależność przeciwna jest wykluczona.
Model procesu musi wiedzieć o głębokim modelu, gdyż realizuje on swoje zadanie jedynie koordynując zachowania komponentów głębokiego modelu. Zależność odwrotna nie powinna jednak mieć miejsca. Komponenty głębokiego modelu powinny być niezależne od aktualnego przebiegu procesu biznesowego. Przykładowo, to jak liczymy cenę nie zależy od nowego sposobu obsługi koszyka, to jak określamy czy produkt jest dostępny nie zależy od dołożenia procesu zwrotów. Model tego co się dzieje w systemie wie o modelu tego w jaki sposób się to dzieje.
Z tego względu model procesu i głęboki model idealnie nadają się na osobne warstwach architektoniczne np. warstwę aplikacji i warstwę domeny z Hexagonal Architecture (mimo, że nazwy te nie oddają idei przedstawionej w tym artykule). Warstwa, w której znajduje się model procesu zależy od warstwy głębokiego modelu, odwrotna zależność powinna być wykluczona i zabezpieczona środkami technicznymi (zależnie od konkretnej technologii).
A co z technikaliami?
No dobrze, ale co zrobić z technikaliami i jak nie mieszać ich z implementacją modelu domeny? Tu do dyspozycji mamy dwa podstawowe rozwiązania.
Pierwszym jest architektura portów i adapterów. Wszystko co jest kodem czysto technicznym powinno znaleźć się po stronie adapterów. Porty modelują co chcemy uzyskać (zapis, pobranie danych, poinformowanie o zdarzeniu, etc.), adaptery zawierają techniczną implementację pozwalającą to osiągnąć. Adaptery są zależne od portów, zależność w drugą stronę oczywiście nie istnieje.
Drugim jest dodawanie dekoratorów. Jest to przydatne do t. zw. corss-cutting concerns, czyli logowania, obsługi błędów, transakcji, etc. Tu analogicznie jak przy portach i adapterach, model procesu nie musi wiedzieć o implementacji ewentualnych dekoratorów. Ciekawą kwestią są tu transakcje, ale to temat na osobny artykuł.
Co się stanie gdy logikę aplikacyjną uznamy za czysto techniczną?
Na koniec zobaczmy jakie konsekwencje będzie miało potraktowanie na serio logiki aplikacyjnej jako czysto technicznej.
Po pierwsze cały kod koordynujący pracę różnych komponentów w ramach konkretnego use case’a musi zostać usunięty z command handlerów. Powstaje wtedy pytanie do jakiego typu komponentów go przenieść?
Można uznać, że złożone use case’y to błąd w modelu i każdy powinien sprowadzać się do wywołania jednego zachowania jednego komponentu np. metody agregatu z DDD. Problem w tym, że w realnych domenach jest to założenie nie do zrealizowania. Jeżeli będziemy starali się je za wszelką cenę utrzymać, to skończymy z modelem nieadekwatnym do rzeczywistości i nadmiernie złożonym. Będzie to niestety złożoność przypadkowa, a nie wynikająca z samej domeny.
Jeżeli dopuścimy złożone use case’y, to będziemy musieli użyć dla nich nowego komponentu poza warstwą logiki aplikacyjnej. Będzie to komponent identyczny do cammand handlera, tylko o innej nazwie znajdujący się w innej warstwie.
A co zostanie w samym command handlerze? No właśnie nic, albo sam zapis i odczyt z bazy, na siłę uznany za coś co trzeba za wszelką cenę odseparować od modelu domeny. Problem w tym, że żeby wiedzieć co trzeba odczytać i zapisać, trzeba będzie znać implementację komponentu modelującego ostatecznie nasz use case. Komponenty te będą więc zawsze tworzyły nierozłączne pary. Czy nie jest to dość sztuczne rozwiązanie?
Pozostałe artykuły w tej serii
Artykuły w tej serii będą analizą różnych wzorców i technik przez pryzmat tego, co z rzeczywistości biznesowej one modelują. Patrzenie na narzędzia, które stosujemy na co dzień, w ten sposób, może ułatwić zrozumienie, dlaczego pewne techniki mają sens i w jakich granicach mają one sens.
- Programowanie jako modelowanie
- Czym jest model?
- Jakiego modelu potrzebujemy
- Czy logika aplikacyjna to część modelu domeny?
- Co modelują transakcje?
- Co agreguje Agregat?
- Czy Saga to transakcja rozproszona?
- Kim jest modelarz?
Marcin Markowski
Architekt, trener, zwolennik podejścia Software Craftsmanship i ścisłej współpracy z biznesem. Specjalizuje się w modelowaniu opartym o Domain Driven Design i projektowaniu architektury systemów.
Zaczynał od consultingu biznesowego, później przeszedł do IT. Pracował zarówno nad systemami „enterprise”, jak i tworzył od podstaw rozwiązania dla małych firm. Próbował wejść w świat startupów z własnym produktem. Ostatecznie został jednak w IT, gdzie działa jako konsultant i trener.