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 │ ║
║ └───────┘ ║ ║ └───────┘ └───────┘ ║
╚════════════════════════════════════╝ ╚════════════════════════════════════╝
║ 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 │ ║
║ └───────┘ ║ └╥────┘ └───────┘ └───────┘ ║
╚════════════════════════════════════╝ ╚════════════════════════════════════╝
║ 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 │ ║
║ └───────┘ ║ └╥────┘ └───────┘ └───────┘ ║
╚════════════════════════════════════╝ ╚════════════════════════════════════╝
║ 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 }
Brak komentarzy:
Prześlij komentarz