niedziela, 17 maja 2009

Ograniczony kontekst

Jedną z najważniejszych koncepcji Domain Driven Design jest ograniczony kontekst (ang. bounded context). Postanowiłem o niej napisać z kilku powodów. Po pierwsze nie widziałem żeby ktoś ją świadomie stosował. Po drugie wydaje mi się ciekawa. Po trzecie, gdy komuś o niej opowiadam, to widzę coś na kształt "wielkich oczu", zdziwienia.

Starożytna, chińska mapa świata, Chiny w centrum świata, reszta państw dookołaWspółczesna mapa polityczna

Powyższe rysunki obrazują dwa różne modele tej samej, niezwykle skomplikowanej dziedziny, z którą związane są rozmaite problemy. Tą dziedziną jest oczywiście świat, w którym żyjemy ;). Zatem który model jest lepszy ? Ten po lewej, czy ten po prawej ? Niestety nie ma poprawnej odpowiedzi na tak postawione pytanie. Przed udzieleniem odpowiedzi należałoby bowiem wyjaśnić: "lepszy do czego" lub raczej "bardziej użyteczny do czego".

Model po lewej, na którym Chiny są w centrum świata, doskonale rozwiązuje problem informowania odbiorcy o tym, że Chiny są najważniejszym państwem na świecie, a na inne państwa nie warto zwracać uwagi. Natomiast nie bardzo nadaje się do nauczania geografii w gimnazjum ;). Model po prawej pokazuje jakie są państwa, ich granice i ich położenie na kontynentach we współczesnym świecie. Dzieci w gimnazjum mogłyby więc z powodzeniem korzystać zeń na lekcjach geografii, w przeciwieństwie do starożytnych Chińczyków, którzy gotowi pomyśleć, że najważniejszym państwem na świecie jest Rosja ;).

Czy istnieje model, który rozwiązuje wszystkie problemy, które mogą się pojawić w jego dziedzinie ? Odpowiedź brzmi nie, powyżej pewnego poziomu złożoności. Gdybyśmy spróbowali stworzyć mapę, która zawiera wszystkie państwa, wszystkie rzeki, jeziora, miasta, drogi, bogactwa naturalne, a do tego informuje, że Chiny są najważniejszym państwem na świecie, to zapewne efekt naszej pracy byłby zupełnie nieczytelny. Zamiast rozwiązywać te wszystkie problemy, nie rozwiązywałby żadnego.

Czas najwyższy na morał tej opowiastki. Analogiczne zjawisko zachodzi w świecie oprogramowania biznesowego. Gdy mamy bardzo skomplikowaną dziedzinę biznesową i duży zespół programistów (np. 20 osób), i próbujemy zbudować aplikację w oparciu o jeden model, to prawdopodobieństwo porażki tego przedsięwzięcia jest bardzo wysokie. Przez porażkę rozumiem tutaj niekoniecznie niemożność wydania działającej aplikacji. Porażką może być nieczytelny kod, nieoptymalne działanie, przekroczony budżet, dużo błędów, narastające trudności przy wypuszczaniu nowych wersji, programiści uciekający z projektu ;), itd.

Jeden model w skomplikowanych projektach nie zadziała zasadniczo z dwóch powodów. Po pierwsze, tak jak w przypadku map, trudno efektywnie rozwiązywać dwa koncepcyjnie oddalone problemy biznesowe przy użyciu tylko jednego modelu. Przez "efektywne rozwiązanie" rozumiem poprawną, przejrzystą, łatwą w rozbudowie implementację. Problem narasta gdy mamy całą masę takich problemów każdy jakby "z innej beczki". Po drugie ciężko 20 programistom implementować jednocześnie jeden model - jest prawie pewne, że prędzej czy później jeden drugiemu namiesza, nie zrozumie intencji drugiego. Podsumowując: jeden model dziedziny o dużej złożoności prowadzi do syfu w kodzie ;).

Koncepcja ograniczonego kontekstu ma na celu pomóc nam uporać się z tym problemem. Jest tym samym jednym z ważniejszych pomysłów DDD na walkę ze złożonością w sercu oprogramowania. Zakłada ona jasne zdefiniowanie granic koncepcyjnie oddalonych od siebie problemów biznesowych. Dla każdego takiego ograniczonego zbioru (kontekstu) tworzymy osobny model i osobny wszechobecny język (ang. ubiquitous language). W każdym kontekście pracuje ograniczona ilość programistów, tak aby nie zakłócali sobie pracy. Nic nie stoi na przeszkodzie, a nawet jest wskazane, aby programistów "przełączać" pomiędzy kontekstami - jak powiedział Martin Fowler, high cohesion i low coupling dotyczą klas, a nie programistów ;).

Prawie na pewno wystąpi konieczność powiązania kontekstów. Rozwiązanie problemu w jednym kontekście może najpierw wymagać pracy innego kontekstu. Z tego powodu definiuje się tak zwaną mapę kontekstów (ang. context map), czyli zbiór reguł, które pozwalają "tłumaczyć" byty żyjące w jednym kontekście na byty w drugim.



Myślę, że czas na bardziej softwarowy przykład, który notabene pochodzi od Evansa. Dziedziną jest biznes transportowy, który zarabia na przewozie kontenerów pomiędzy różnymi miastami na świecie - jak na obrazku powyżej. Transport odbywa się drogą morską lub lądową, co wiąże się z szeregiem załadunków, rozładunków kontenerów w różnych portach, stacjach kolejowych itp. Kluczowym problemem w takiej dziedzinie jest śledzenie historii transportu towaru. Zadanie to doskonale rozwiązuje model przedstawiony na diagramie poniżej.

Mamy tutaj ładunek (ang. cargo), który posiada specyfikację swojej drogi (ang. route specification) od miejsca pochodzenia do celu transportu. Ładunek posiada również rozkład (ang. itinerary), czyli listę gałęzi (ang. leg). Gałąź to nic innego jak fragment drogi ładunku, np. podróż morska z Hong Kongu do Los Angeles lub podróż pociągiem z Los Angeles do Dallas. Gałąź posiada więc miejsce załadunku i rozładunku towaru. Miejsce załadunku pierwszej gałęzi rozkładu musi być równe miejscu pochodzenia towaru (ang. origin), a miejsce rozładunku ostatniej gałęzi musi być równe miejscu przeznaczenia (ang. destination) - wtedy mówimy, że rozkład spełnia specyfikację drogi. Żeby lepiej zobrazować czym są te gałęzie zamieszczam poniższy rysunek.



Przedstawiony model pozwala pracować z planem transportu towaru. Może np. posłużyć w aplikacji wystawiającej zlecenia przewoźnikom na poszczególnych gałęziach. W tej dziedzinie występują jednak inne problemy. Wyobraźmy sobie, że oprócz śledzenia transportu nasza aplikacja musi określić optymalną drogę (np. najkrótszą albo najtańszą albo najszybszą) z miasta, w którym klient nadaje przesyłkę do miasta docelowego. Tego typu zadanie można rozwiązać stosując teorię grafów (problem najkrótszej drogi). Powstaje pytanie czy powinniśmy do naszego modelu wprowadzać klasy, atrybuty, metody realizujące algorytmy teoriografowe ? DDD i koncepcja ograniczonego kontekstu mówią nie ! Powinniśmy raczej skorzystać z drugiego modelu, w którym będą klasy Graph, Vertex, Edge, Path i być może inne byty, o których można poczytać w książce do teorii grafów. Na tym przykładzie oczywiste staje się pojęcie mapy kontekstów. Musimy jakoś "przetłumaczyć" wynik działania algorytmu wyszukiwania najkrótszej drogi (zapewne obiekt klasy Path) na obiekt rozkładu (klasa Itinerary). Mapa kontekstów będzie w tym konkretnym przykładzie zawierać regułę, np. że atrybut name klasy Vertex odpowiada kodowi miasta, w którym następuje rozładunek/załadunek.

W przedstawionym przykładzie granice pomiędzy dwoma kontekstami są dobrze widoczne, wręcz jaskrawe. Prawdziwe scenariusze pisze jednak życie - wydzielanie kontekstów nie jest oczywiste, bo ich granice często są niewyraźne. Rozwinięcie koncepcji, które tutaj naszkicowałem znajdziecie w książce Evansa - zachęcam do lektury.

5 komentarzy:

  1. Trasy statkow i algorytmy grafowe to przykład innego podziału, na domeny: core, generic i supporting.
    Domena corowa to generalnie powód dla ktorego tworzysz system. Generic to specyficzne domeny, np takie jak grafy - te komponenty najlpiej kupić gotowe. Domeny Supporting to cos bez czego nasz system moze sie obuc - mozna je zlecic praktykantom gdy nie mamy czasu;)

    Natomiast odnosnie bounded context to wlasnie brakuje mi jasnego i klarownego przykladu. Nie wiem czy sam dobrze zrozumialem, ale na przyklad:
    wezmy taki byt jak Osoba. W kontekscie faktury osoba bedzie jakims tam szczegolem. Natomiast w kontekscie np oddzialu szpilanego osoba (pacjent) bedzie dosyc bogatą encją/agregatem. W tym kontekscie nie dbamy o aspekty rozliczania i fakturowania.
    Teraz majac te dwa konteksty (np moduły apliakcji) mozemy je zamodelowa przy pomocy 2 zupelnie nie majacych ze sobą nic wspolnego encji. A to ze encje beda zamapowane na tą sama teblkę to sczegol techniczny;)

    Nie wiem czy nie dokonalem naditerpretacji, ale sam od jakiegos czasu zastanawailem sie nad przykladem czesciej spotykanym niz podane przez Evansa połączenie biznesu i grafów - zwykle problem polega na nakladniu sie wielu "biznesów".

    OdpowiedzUsuń
  2. Przykład statków z grafami nadaje się zarówno do opisu koncepcji generycznych domen jak i do ograniczonych kontekstów. Często generycznej domeny lub domeny wspomagającej używa się w osobnym kontekście, tak jak to ma miejsce w moim przykładzie, a raczej w przykładzie Evansa ;) Gdyby nie stosować osobnego kontekstu i mapy kontekstów, to moglibyśmy używać obiektów z jednego modelu w drugim. W tym konkretnym przykładzie raczej niewskazane byłoby stosowanie obiektów teoriografowych w obiektach definiujących drogę ładunku. Na pewno istnieją przykłady gdzie w modelu kliencie warto stosować obiekty z modelu generycznej domeny, np. gdy generyczny model zapewnia operacje na walutach, pieniądzach itp.

    Najczęściej spotykanym przykładem na konteksty jest integracja systemu, który tworzymy z już istniejącymi systemami (ang. legacy systems). W takich przypadkach nie chcemy aby nasz piękny, nowo-tworzony model został zaburzony brzydkimi modelami lub ich całkowitym brakiem ;) w tych narzuconych nam systemach. Powiązanym wzorcem jest wtedy warstwa antykorupcyjna (ang. anticoruption layer), która zapewnia mapowanie pomiędzy kontekstami.

    Całą masę przykładów mamy w analogicznych sytuacjach do tej z encją Osoba, tj. z encją, do której koncepcyjnie pasuje tak wiele odpowiedzialności, że zawarcie ich wszystkich w jednej klasie byłoby niemożliwe, a przynajmniej nie do zaakceptowania z punktu widzenia OOP ;)

    OdpowiedzUsuń
  3. Witam, ciekawy arykuł :)

    Przykład osoby i szpitala mi sie podoba ale inny, biznesowy. Mamy system fakturowania, który, poza danymi o przedmiocie sprzedaży, wymaga tylko prostych danych o tym kto wystawił fakturę. Osoba jako domena jest tu wręcz trywialna (jedna klasa). Jednak pojawia się nowe wymaganie: kadry płace. Kontekst kadrowopłacowy w tej firmie. I tu idąc tym tropem nasuwa mi sie myśl: kupujemy system kadrowy a klasę Osoba systemu fakturowania zastępujemy (tworzymy z niej...?) interfejsem do nowego kadrowego. System fakturowania praktycznie tego nie odczuje a system kadrowy łatwo zostal zintegrowany z resztą. Czy to słuszny kierunek myślenia?

    OdpowiedzUsuń
  4. Dokładnie o to chodzi - w kontekście fakturowania nie mają znaczenia kadrowe odpowiedzialności osoby. Nie powinniśmy zatem w kontekście faktur używać osoby z tego drugiego kontekstu, lecz zdefiniować trywialną osobę, która będzie miała odpowiedzialność istotną jedynie dla fakturowania. Kontekst fakturowania nie musi się zajmować przechowywaniem osób - po to m.in. kupiliśmy kontekst kadrowy i to właśnie z niego będziemy pobierać osoby prawdopodobnie poprzez jakiś udostępniony przez niego serwis. Po pobraniu osoby z kadr musimy przetłumaczyć ją na osobę z kontekstu fakturowania - po to właśnie definiujemy mapę kontekstów. W tym przykładzie implementacja mapy mogłaby mieć choćby taką postać:

    class KadryFakturyTranslator {
    fakturowanie.Osoba przetlumacz(kadry.Osoba o) {
    .... }
    }

    OdpowiedzUsuń
  5. Witam
    Po jedenastu latach nadal artykuł ciekawy. Ja zgłębiam książke Eriva Evansa od kilku miesięcy (wolno czytam ;-), a przy okazji Event Stormingu pojawia się pojęcie Bounded Contextów. Zastanawiam się nad tym jak w języku polskim to zinterpretować. Dla mnie lepszą nazwą (bardziej dla mnie zrozumiałą) na bounded context jest granica kontekstu.

    OdpowiedzUsuń