Pokazywanie postów oznaczonych etykietą modularny monolit. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą modularny monolit. Pokaż wszystkie posty

sobota, 22 października 2022

Komunikacja między Modułami Monolitu: zlecanie prac


Realizacja nowej funkcjonalności biznesowej często wykracza poza ramy jednego Modułu Monolitu. Jako że muszą się ze sobą komunikować m.in. w celu oddelegowania zadań dalej - warto przeanalizować sobie rodzaje komunikacji jakie możemy zaimplementować.

 

🔌 Wariant #1: Fasada

Moduł B jest całkowicie zależny od modułu A

W tym przypadku moduł bieżący (B) w którym piszemy zleconą nam funkcjonalność musi oddelegować część prac do innego zewnętrznego modułu (A). Delegowanie pracy w praktyce polega na wywołaniu void'owskiej metody (Command) Fasady leżącej w zewnętrznym module

W opisywanym przypadku moduł bieżący jest tym znajdującym się w dole strumienia. Całkowicie musi się on poddać kontraktowi komunikacji ustanowionego przez upstream'owy moduł zewnętrzny. Moduł A wie jakich składników będzie potrzebował do wykonania swojego zadania, dlatego stanowią one parametry metody Fasady. W gestii modułu downstream'owego leży to by takowe parametry dostarczyć - warto się zastanowić czy posiadanie przez niego takich informacji jest w ogóle poprawne. 

Warto podkreślić, że moduł A nie wie nic o swoich klientach - udostepnia API (Interfejs Fasady) do komunikacji z innymi modułami i nie dba o to czy jest ich pięciu czy nie ma ani jednego. W tym przypadku to moduł zewnętrzny A jest niezależny od innych modułów. Idąc tym tropem: odpowiedzmy sobie najpierw na pytanie dlaczego moduł bieżący B do wykonania w pełni swojej funkcjonalności musi oddelegować część pracy do zewnętrznego modułu - czy granice zostały poprawnie wytyczone?

Moduł B z kolei wie dokładnie o istnieniu innego modułu A i jak się z nim komunikować (API Fasady). Jest to więc jawna deklaracja komunikacji jednostronnej: 

  • wiem co chcę zrobić,
  • wiem kto to zrobi,
  • wiem dokładnie jak zmusić tego kogoś do wykonania tej czynności.

 

📨 Wariant #2: Event'y

W tym wypadku moduł bieżący (A) w którym dodajemy funkcjonalność, informuje inne bliżej nieokreślone moduły o zaistnieniu pewnego zdarzenia wewnątrz swoich granic. Robi to emitując Event Aplikacyjny (Sync/Async) - nie troszcząc się o to kiedy i przez kogo zostanie on obsłużony.  

Moduły B i C są całkowicie zależne od Modułu A

Zaimplementowanie Event'u w Warstwie Aplikacji wychodzącego poza granicę bieżącego modułu jest swego rodzaju zdefiniowaniem kontraktu komunikacji

Moduł bieżący (A) nie dba o to czy Event'y zostaną obsłużone, istotne jest to tylko dla zewnętrznych modułów (downstream'owych) które decydują się na nasłuchiwanie na tego typu zdarzenia.

Downstream'owe moduły B i C muszą dostosować się do kontraktu ustalonego przez moduł A. Muszą one być w stanie wykonać swoją pracę na postawie danych znajdujących się w Event'cie. 

W tym wypadku, nie możemy powiedzieć że moduł A chce oddelegować część pracy do innych modułów. Jedynie daje im znać, że coś się u niego zdarzyło. 

Jest to więc całkowicie inna sytuacja niż gdyby miał on skorzystać z Fasad modułów B & C wywołując ich metody void'owskie. 

Możnaby ponownie wypisać stwierdzenia jak w przypadku poprzedniej sekcji - Stan bieżącego modułu uległ zmianie:

  • nie wiem kogo to interesuje,
  • nie wiem czy będzie miało to jakiekolwiek konsekwencje.

 

🔎 Porównanie

W przypadku korzystania z Fasady niejawnie zakładamy, że coś musi się zadziać i nie możemy obejść się bez tej funkcjonalności z zewnętrznego modułu. Wydaje się, że wybierając takie rozwiązanie funkcjonalność wykonywana za Fasadą (upstream) jest naprawdę ważna. 

W przypadku emitowania Event'ów przez moduł bieżący - nie dbamy o to jaki klient/klienty obsłużą to zdarzenie. Zastanawiam się czy z takim podejściem, obsługujące to zdarzenie moduły downstreamo'we wykonują funkcjonalności drugorzędne... tak - ale tylko z perspektywy bieżącego modułu.

Bieżący moduł komunikacja z Fasadą emitowanie Event’ów
Hierarchia downstream upstream
Czy tworzy kontrakt nie tak
Czy wie kto będzie wykonywał czynność tak nie
Czy mamy pewność wykonania zleconych prac tak nie
Czy konsekwencje prac są znane tak nie
Sposób komunikacji Sync Sync/Async

Moduły w Niebieskiej Książce Eric'a Evans'a


Large Scale Domain Concept

 
Eric sugeruje by rozpatrywać moduły jako bloki budulcowe taktycznego DDD takiej samej rangi jak Agregaty, Encje czy Value Object'y. Jest to koncept większej skali, grupujący inne mniejsze powiązane ze sobą koncepty domenowe. Od strony kodu źródłowego moduły będą reprezentowane przez konkretne namespace'y zawierający obiekty i interfejsy.
  
Moduł pochodzi z modelu domenowego, a jego nazwa z Ubiquitous Language - nie powinien więc on być tylko elementem kodu źródłowego służącego do zredukowania złożoności. Ułatwia to komunikację z ekspertami domenowymi ponieważ możemy szybko ustalić kontekst poruszanego problemu czy też posługiwać się bardziej ogólnym pojęciami rozumianymi przez każdą ze stron.

Moduł skupiający mniejsze domenowe pojęcia
Spoglądając na aplikację z lotu ptaka, wprowadzone moduły sprawiają, że zyskujemy nową perspektywę wglądu w tworzony system. Możemy spojrzeć na to w jaki sposób koncepty większej skali ze sobą współpracują, bez niepotrzebnego rozpraszania się złożonością (liczne klasy i interfejsy) którą heremtyzują. 

Możliwość spojrzenia na aplikację monolityczną z wysokości nie jest sprawą oczywistą i dostępną od tak. Najpierw należy wykonać pracę analizującą dokładną zawartość modułów oraz ustalić relację między nimi nazwiązane. Niewątpliwie jest to opłacalna inwestycja ponieważ daje nam możliwość lepszego zrozumienia domeny problemu. Na podstawie takiego przeglądu możemy dojść do wniosku, że niektóre z modułów mają zbyt dużo odpowiedzialności lub relacje między modułami są nielogiczne lub dwukierunkowe.    


Relacje z innymi Modułami 

Relacje między modułami

Dzieląc klasy i interfejsy na moduły wartoby było monitorować ich relacje z innymi modułami. Nie powinniśmy rozpatrywać zależności modułów między sobą jako coś złego, to normalne że komunikują się ze sobą nawzajem. Relację można traktować jako element modelu i wartość samą w sobie.
 
Uważnie śledząc kierunek relacji modułów możemy sprawdzić w jaki sposób moduły są od siebie zależne. Powinniśmy pilnować by liczba relacji w module lokalny do modułów zewnętrznych była jak najmniejsza i jednokierunkowa. Jeżeli tak nie jest to warto przeanalizować funkcjonalności w innych modułach od których moduł lokalny jest zależny. 
 
Jeżeli koncept domenowy z modułu zewnętrznego jest spójny z tymi znajdującymi się w module lokalnym wszystko wskazuje na to, że to w nim powinna być umiejscowiona funkcjonalność. 


Low Coupling & High Cohesion

 
Zasada jak najmniejszej ilości powiązań (Low Coupling) jest ściśle związana z zasadą wysokiej spójności (High Cohesion) konceptów/reguł/logiki. Jeżeli moduł ma wiele powiązań z innymi modułami (High Coupling) to znaczy, że funkcjonalności dotyczące jednej dziedziny problemu zostały rozbite na kilka modułów - stąd potrzeba wielu powiązań między nimi. 
 
Istotną korzyścią płynącą ze stosowania się do tych zasad jest fakt, że o wiele łatwiej pracować z wysoce spójnymi i luźno powiązanymi modułami. Programiści wprowadzający zmiany w takim obszarze będą mieli do czynienia z klasami reprezentującymi tylko konkretną część domeny. 
 
Liczba klas w module będzie więc ograniczona, dlatego nie powinniśmy być przeciążeni kognitywnie analizując je wszystkie razem w celu zrozumienia pełnego konceptu. Ewentualne powiązania do innych bytów będą nieliczne i jasno zdefiniowane.   
       
Warto zwrócić uwagę, że pilnowanie by implementowany byt był luźno powiązany i wysoce spójny powinniśmy stosować zarówno podczas pracy z modułami jak i obiektami - zasada jest uniwersalna.


Ciągły Refactor

 
Tak jak pozostałe building block'i taktycznego DDD wymagają przeprowadzania ciągłej refaktoryzacji - tak samo powinniśmy postępować z modułami. 
 
W przypadku modułów refaktoryzacji zazwyczaj powinno być poddawane umiejscowienie konceptów/reguł/logiki w nich zawartych. Jeżeli łamiemy zasadę wysokiej spójności powinniśmy przenieść funkcjonalność do innego modułu. 
 

Moduł A posiada niespójny koncept

 

Kiedy żaden z istniejących modułów nie wydaje się wystarczająco odpowiednim miejscem należy rozważyć utworzenie nowego modułu. Wskazówki co do jego potencjalnej nazwy może dać rozmowa przeprowadzona z przedstawicielem biznesu. Zazwyczaj jest to wcześniej nieodkryty bądź niejawny koncept domenowy.

Jak zauważa Evans, programiści nie są skorzy do przeprowadzania refaktoru modułów i zadowalają ich granice/nazwy ustalone na samym początku ich powstania, a jak często wspominał, pierwotny model jest zazwyczaj naiwny.
 
Można wyciągnąć wnioski, że są one kłopotliwe do refaktoru ponieważ:
  • wymagają znacznie większego zakresu prac niż refaktor obiektów,
  • wymagają spojrzenia na kod z innej perspektywy w celu dostrzeżenia ewentualnych miejsc do poprawy,  
  • trudno wpaść na lepszy pomysł podziału na moduły
    bądź programiści w ogóle nie biorą pod uwagę tego, że ten aspekt projektu mógłby zostać ulepszony.
Warto więc rozpatrywać następujące aspekty modułów i zastanowić się nad ich poprawnością:
  • granice, 
  • nazewnictwo, 
  • ukryte koncepty, 
  • nieaktualne byty.
 
Powinniśmy liczyć się z tym, że prace refaktoryzacyjne w tych obszarach nie będą tak często przeprowadzane i same moduły pod względem ich aktualności "będą stały w tyle" za resztą konceptów.  

 

Opracowanie na podstawie

📚 Eric Evans "Domain Driven Design: Tackling Complexity in the Heart of Software" str. 109-115.

niedziela, 19 kwietnia 2020

Komunikacja pomiędzy komponentami (Fasady i Adaptery)

 
Platon's Cave źródło



    Tworząc aplikację opartą na komponentach - pomimo ich niezależności - czasami przychodzi potrzeba komunikacji między nimi. Zależności powinny być ograniczone do niezbędnego minimum, a liczba udostępnionych klas jak najmniejsza. Należy też dbać o to by komunikacja między komponentami była jednostronna. Przypadek w którym wymiana danych następuje dwustronnie, świadczy o niewłaściwym rozdzieleniu odpowiedzialności w modułach.

Bezpośrednie odwołania do zewnętrznego komponentu


╔════════════════════════════════════╗     ╔════════════════════════════════════╗
║          Component Order           ║     ║         Component Product          ║
╠════════════════════════════════════╣     ╠════════════════════════════════════╣
║ ┌───────┐                          ║     ║ ┌───────┐                          ║
║ │ Class │            ┌───────┐     ║     ║ │ Class │               ┌───────┐  ║
║ └───────┘┌───────┐   │ Class │     ║     ║ └───────┘   ┌───────┐   │ Class │  ║
║          │ Class │   └───────┘     ║     ║      ┌─────>│ Class │   └───────┘  ║
║          └───────┘                 ║     ║      │      └───────┘              ║
║    ┌───────┐                       ║     ║      │   ┌───────┐      ┌───────┐  ║
║    │ Class ├───────────────────────╫─────╫──────┘   │ Class │   ┌─>│ Class │  ║
║    └───────┘                       ║     ║          └───────┘   │  └───────┘  ║
║                  ┌───────┐         ║     ║ ┌────────────────────┤             ║
║      ┌───────┐   │ Class ├─────────╫─────╫─┤  ┌───────┐         │   ┌───────┐ ║
║      │ Class │   └───────┘         ║     ║ └─>│ Class │         └──>│ Class │ ║
║      └───────┘                     ║     ║    └───────┘             └───────┘ ║
╚════════════════════════════════════╝     ╚════════════════════════════════════╝


    W tym przypadku Order Component musi mieć pełną wiedzę na temat usług jakie oferuje Product Component


namespace CompanyName\OrderComponent\Service;

use CompanyName\ProductComponent\Model\ProductInterface; 
use CompanyName\ProductComponent\Model\ProductFactory;

final class CreateOrder
{
    private ProductInterface $product;

    public function __constructor(ProductFactory $productFactory)
    {
        $this->product = $productFactory->getInstance();
    }
 
    // Interfejs publiczny
}

 
⚠️ Komponent Order musi posiadać wiedzę na temat współdziałania ze sobą klas z innego komponentu, ich umiejscowienia i przeznaczenia, 
⚠️ ma dostęp do klas reprezentujących szerokie spektrum zachowań, gdy tak naprawdę potrzebuje tylko części z nich,
⚠️ jest całkowicie uzależniony od interfejsu publicznego klas zewnętrznego modułu, w przypadku jego zmiany istniałaby możliwość modyfikacji kodu który z nich korzysta.

    Komponent Produktu który pomimo, że jest niezależnym bytem udostępnia swoje usługi innym komponentom, a w związku z tym powinien w jakiś sposób demonstrować ten zamiar. Nawiązując komunikacje z zewnętrznym komponentem, nie wiemy jakie zachowania mogą wykraczać poza jego granicę, a który absolutnie nie powinny.   

    Ujawnianie zewnętrznych usług można zrealizować tworząc specjalną klasę Fasady przeznaczonej tylko w tym celu. Leży ona na granicy komponentu i zbiera wszystkie zachowania, które mogą być udostępnione klientom. Prezentując usługi w ten sposób tworzymy niejako komunikacyjny interfejs publiczny dla innych części systemu, ograniczając wiedzę na temat jego wewnętrznego działania.

    Jest to oczywiście wszystko w codziennych decyzjach (dobrej woli) zespołu deweloperskiego by przestrzegać z dyscypliną tych zasad, ponieważ nie ma fizyczny barier by ominąć Fasadę. 

Fasada po stronie usługodawcy



╔════════════════════════════════════╗     ╔════════════════════════════════════╗
║           Component Order          ║     ║          Component Product         ║
╠════════════════════════════════════╣     ╠════════════════════════════════════╣
║ ┌───────┐                          ║    ┌╨────┐ ┌───────┐                     ║
║ │ Class │            ┌───────┐     ║    │  F  │ │ Class │                     ║
║ └───────┘┌───────┐   │ Class │     ║    │     │ └───────┘          ┌───────┐  ║
║          │ Class │   └───────┘     ║    │  A  │         ┌───────┐  │ Class │  ║
║          └───────┘                 ║    │     ├────────>│ Class │  └───────┘  ║
║    ┌───────┐                       ║    │  S  │         └───────┘             ║
║    │ Class ├───────────────────────╫───>│     │      ┌───────┐     ┌───────┐  ║
║    └───────┘                       ║    │  A  │      │ Class │  ┌─>│ Class │  ║
║                  ┌───────┐         ║    │     │      └───────┘  │  └───────┘  ║
║                  │ Class ├─────────╫───>│  D  │  ┌──────────────┤             ║
║      ┌───────┐   └───────┘         ║    │     ├──┤   ┌───────┐  │   ┌───────┐ ║
║      │ Class │                     ║    │  A  │  └──>│ Class │  └──>│ Class │ ║
║      └───────┘                     ║    └╥────┘      └───────┘      └───────┘ ║
╚════════════════════════════════════╝     ╚════════════════════════════════════╝


Na fasadę InternalCommunication został nałożony interfejs InternalCommunicationInterface by ułatiwć testowanie.

namespace CompanyName\ProductComponent\Facade;

use CompanyName\ProductComponent\Model\ProductInterface; 
use CompanyName\ProductComponent\Model\ProductFactory;

final class InternalCommunication implements InternalCommunicationInterface 
{
    private ProductFactory $productFactory;

    public function __constructor(ProductFactory $productFactory)
    {
        $this->productFactory = $productFactory;
    }

    /**
     * @inheritDoc
     */
    public function getProduct(): ProductInterface
    {
        return $this->productFactory->getInstance();
    }
}

Klient Fasady nie ma wiedzy na temat tworzenia klasy ProductInterface:


namespace CompanyName\OrderComponent\Service;

use CompanyName\ProductComponent\Model\ProductInterface; 
use CompanyName\ProductComponent\Facade\InternalCommunicationInterface;

final class CreateOrder
{
    private ProductInterface $product;

    public function __constructor(InternalCommunicationInterface $productFacade)
    {
        $this->product = $productFacade->getProduct();
    }
 
    // Interfejs publiczny
}

✔️ Komponent Product udostępnia interfejs publiczny innym częścią systemu, hermetyzując wiedzę o jego wewnętrznym działaniu,
⚠️ komponenty konsumenckie w dalszym ciągu, bezpośrednio operują na obiektach z zewnętrznego komponentu, oraz wiedzą o ich klasach.


Adaptery po stronie usługobiorcy



    Aby jak najbardziej uniezależnić klasy korzystające z usług zewnętrznego komponentu, najlepiej byłoby operować w nich tylko na ich lokalnych odpowiednikach. Obiekt Product w różnych częściach systemu może charakteryzować się innymi zachowaniami determinowanymi przez kontekst komponentu, dlatego najlepiej jakby każdy z nich posiadał swój interfejs dla klas reprezentujących produkty. W tym celu, z innym komponentem komunikować się będą jedynie klasy będące Adapterami realnych obiektów zwracanych przez Fasadę zewnętrznego komponentu.


╔════════════════════════════════════╗        ╔════════════════════════════════════╗
║          Component Order           ║        ║          Component Product         ║
╠════════════════════════════════════╣        ╠════════════════════════════════════╣
║ ┌───────┐                          ║       ┌╨────┐ ┌───────┐                     ║
║ │ Class │            ┌───────┐     ║       │  F  │ │ Class │                     ║
║ └───────┘┌───────┐   │ Class │     ║       │     │ └───────┘          ┌───────┐  ║
║          │ Class │   └───────┘     ║       │  A  │         ┌───────┐  │ Class │  ║
║          └───────┘                 ║       │     ├────────>│ Class │  └───────┘  ║
║    ┌───────┐                   ┌───╨───┐   │  S  │         └───────┘             ║
║    │ Class ├──────────────────>│Adapter│──>│     │      ┌───────┐     ┌───────┐  ║
║    └───────┘                   └───╥───┘   │  A  │      │ Class │  ┌─>│ Class │  ║
║                ┌───────┐       ┌───╨───┐   │     │      └───────┘  │  └───────┘  ║
║                │ Class ├──────>│Adapter│──>│  D  │  ┌──────────────┤             ║
║      ┌───────┐ └───────┘       └───╥───┘   │     ├──┤   ┌───────┐  │   ┌───────┐ ║
║      │ Class │                     ║       │  A  │  └──>│ Class │  └──>│ Class │ ║
║      └───────┘                     ║       └╥────┘      └───────┘      └───────┘ ║
╚════════════════════════════════════╝        ╚════════════════════════════════════╝

    Adapter produktu z innego komponentu implementuje interfejs OrderedProductInterface, który znajduje się w katalogu Model wraz z innymi klasami tego typu w komponencie OrderComponent. Dzięki takiemu zabiegowi, klasy logiki biznesowej znajdujące się w przestrzeni nazw \CompanyName\OrderComponent\* (np. klasy serwisowe) nie zdają sobie sprawy, że operują na owiniętym obiekcie z innego komponentu. Aby ułatwić korzystanie z tego adaptera przez inne klasy, należy zarejestrować go w Kontenerze Zależności i ukryć go pod interfejsem który implementuje. 
    W celu zachowania przejrzystości projektu, należy umieszczać wszystkie klasy komunikujące się z innymi komponentami w jednym katalogu. Spoglądając na ich zadeklarowane importy innych klas, łatwo będzie można dostrzec zależności do klasy z poza komponentu macierzystego. 


namespace CompanyName\OrderComponent\Adapter;

use CompanyName\ProductComponent\{
    Model\ProductInterface,
    Facade\InternalCommunicationInterface
};
use CompanyName\OrderComponent\Model\OrderedProductInterface;

final class ProductAdapter implements OrderedProductInterface
{
    private ProductInterface $product;

    public function __constructor(InternalCommunicationInterface $productFacade)
    {
        $this->product = $productFacade->getProduct();
    }
 
    /**
     * @inheritDoc
     */
    public function someBehavior(): bool 
    {
        return $this->product->someBehavior();
    }
}

    Ostateczna forma serwisu domenowego z komponentu OrderComponent nie posiada żadnej wiedzy na temat innych komponentów.


namespace CompanyName\OrderComponent\Service;

use CompanyName\OrderComponent\Model\OrderedProductInterface;

final class CreateOrder
{
    private OrderedProductInterface $product;

    public function __constructor(OrderedProductInterface $product)
    {
        $this->product = $product;
    }
 
    // Interfejs publiczny
}