sobota, 14 stycznia 2023

Kontekst pierwotny, a reużywalność obiektu

By oszczędzić sobie czasu i korzystać z przychylności programowania obiektowego – kod który napiszemy raz, możemy ponownie wykorzystać w nowych przypadkach. Myślę że zasada DRY, zawsze siedzi nam z tyłu głowy i zna ją każdy zawodowy programista. Niestety w programowaniu nic nie jest czarno-białe i po latach pracy nad różnymi projektami pojawiło się kilka wątpliwości co do ślepego stosowania DRY.

Co robi metoda i dlaczego?

Rola obiektu jest właściwie kontekstem jego wywołania. Przeanalizujmy przypadek w którym posiadamy Encje/WriteModel Product z jeszcze nienazwaną metodą:

namespace App\Offer\Domain\Entity;

final class Product 
{
	/* ... */
	
	public function ____________(): array 
	{
		return [
			'name' => $this->name,
			'description' => $this->description,
			'price' => $this->price,
			'currency' => $this->currency,
			'created_at' => $this->createdAt->format('Y-m-d H:i:s')	
		];
	}
} 

Co możemy odczytać z tego kodu?

    • metoda zwraca
array’a który jest strukturą danych. Dodanie do niej @docBlock - /** @return array<string, mixed> */ nie wiele wniesie do klientów bo i tak by nie iterowali po zwróconej przez metodę tablicy,
    •
array będący strukturą danych budowany jest na podstawie wewnętrznego stanu obiektu. Na przykładzie tego nie widać, ale załużmy, że serializacji podlega każde pole klasy,
    • Jest to metody typu Query (CQS) – niemodyfikująca wewnętrznego stanu obiektu czyli pozbawiona efektów ubocznych.

Kolejne wnioski możemy wyciągnąć patrząc na (jak dotąd) jedynego klienta korzystającego z tej metody:

namespace App\Offer\Infrastructure\Repository;

use App\Offer\Domain\Repository\ProductRepository;
use App\Offer\Domain\Entity\Product;
use Doctrine\DBAL\Connection;

final class ProductMySQLRepository implements ProductRepository
{
	public function __construct(private Connection $connection) {}
	
	public function save(Product $product): void 
	{
		$this->connection->executeStatement(
			'INSERT INTO product ...',
			$product->____________()
		);
	}
}


    • nienazwana metoda wywoływana jest w Warstwie Infrastruktury,
    • zmapowany stan Write Modelu reprezentował będzie jeden wiersz w tabeli product,

Biorąc pod uwagę fakt, że klasa
ProductMySQLRepository jest jedynym klientem metody Product::__________ - możemy założyć, że powstała ona specjalnie by być wykorzystana do utworzenia wiersza w tabeli. Programista stanął przed problemem – „jak mogę utrwalić Encję Product – nie tworząc dla niej całej armi getterów?”. Załóżmy, że zdecydował się utworzyć metodę mapującą stan na dane oczekiwane przez drugi parametr Doctrine’owego Connection::executeStatement. Czy nie przewidując dla niej innego wykorzystania (w tym punkcie czasu) w przyszłości, wywołanie jej w innym kontekście – całkowicie innym niż INSERT bazodanowy – będzie akceptowalne?

Jaka nazwa jest najodpowiedniejsza?

Do wyboru mamy:
    1.
toArray
    2.
asArray
    3.
serialize
    4.
jsonSerialize
    5.
map

Takie podejście sprawia, że w nazwie metody nie ma śladu po kontekście jej użycia, a który jest w całej historii powstania tej metody bardzo istotny. Bezkontekstowa nazwa metody sprawia złudne wrażenie, że wykorzystanie jej ponownie w zupełnie innym kontekście, w zupełnie innej roli jest całkowicie usprawiedliwione.

Powinniśmy więc odpowiedzieć sobie na jedno pytanie: „Czy nazwa metody powinna określać intencje jej pierwotnego użycia?” - moim zdaniem nie. Niezmienia to jednak wspomnianej już wielokrotnie istotności kontekstu dla którego została wprowadzona, na którego musimy zwracać uwagę podczas ponownego użycia metody.
 
Na potrzeby omawianego przykładu możemy ustalić, że
asArray będzie prawdziwą nazwą Product::__________ metody. Dlaczego akturat ta nazwa została wybrana spośród innych? - można powiedzieć, że chodzi o konwencje w projekcie. Często musimy serializować obiekty do tabel, powody tego są różne. Tak jak w opisanym przykładzie, raz potrzebujemy przesłać dane do tabeli bazodanowej, innym razem wysłać je poza aplikację. Musimy przyjąć jedną konwencję nazewniczą dla tego typu metod – na co kolwiek się ostatecznie zdecydujemy, nie będzie miało ostatecznego wpływu na jakość projektu.   

Jeden obiekt – dwa konteksty użycia

Zapisywanie produktu do bazy danych zostało wdrożone na produkcję miesiące temu. Biznes jednak nie pozostawił tematu na zawsze i postanowił wzbogacić moduł oferty o nowy feature. Na każde dodanie nowego produktu miał być wysyłany email do zewnętrznego systemu z szablonem umożliwiającym dynamiczne dodanie danych produktu. Podczas tworzenia templatki, można by korzystać z zestawu kluczy, których wartości zawsze byłyby dostępne wraz z wysyłanym emailem. By jeszcze skomplikować sprawę, za te czynności (wysyłanie emaili i zarządzanie templatkami) odpowiedzialny by był zewnętrzny system.   


Spoglądajć na kod odpowiedzialny za zapisywanie produktu sprawa wygląda dość prosto bo... 

namespace App\Offer\Application\Handler;

use App\Offer\Domain\Entity\Product;
use App\Offer\Domain\Repository\ProductRepository;

final class CreateProductHandler 
{
	public function __construct(
		private ProductRepository $productRepository;
	) {}

	public function __invoke(/*...*/): void 
	{
		/*...*/
		$this->productRepository->save($product);
	}
}

...jedyne co musimy zrobić to wstrzyknąć serwis odpowiedzialny za komunikację z zewnętrznym serwisem wysyłającym emaile. Jako że mamy obiekt $product pod ręką, wraz z możliwością jego łatwej serializacji do struktury tablicowej – możemy pokusić się o implementację z jego wykorzystaniem. Metoda __invoke potrafiąca zlecać wysyłkę email’a wyglada nastepująco:

public function __invoke(/*...*/): void 
{
	/*...*/
	$this->productRepository->save($product);
	$this->externalEmailService->send($adminEmail, $product->asArray());
}

W tym momencie struktura tablicowa zwracana przez metodę Product::asArray jest wykorzystywana dwuktornie w różnych  kontekstach:
    1. zapisu wiersza tabeli bazy MySQL,
    2. templatce emaila znajdującego się w zewnętrznym systemie  

Jak pokazuje poniższy kod w zewnętrznym systemie znalazły się „pojęcia” związane z wewnątrz-systemową reprezentacją zapisu encji z innego systemu.

Utworzono produkt {{name}}

{{created_at}}

{{description}}

Rozważmy plusy i minusy

✅ nie musimy robić nic więcej niż wywołanie metody która została już wcześniej napisana. Nazwa metody serializującej nie zawiera w sobie żadnego kontekstu w którym została utworzona dlatego korzystamy z niej z czystym sumieniem,

❌ powiązaliśmy ze sobą niejawnie dwa konteksty: zmiana klucza w zserializowanym produkcie wprowadzona na potrzeby kontekstu bazodanowego, będzie miała też swoje konsekwencje w kontekście zewnętrznego serwisu do wysyłki emaili. Ewentualne zmiany, które muszą być wprowadzone tylko w jednym z kontekstów będą wymagały wprowadzenia małych hacków,
❌ do zewnętrznego serwisu wysyłającego emaile mogą trafić nadmiarowe/poufne dane, być może o nieodpowiednich nazwach kluczy. Możemy tego uniknąć stosując drobne hacki – przemapowanie tablicy, unsetowanie,
❌ korzystamy z metody utworzonej w celu utrwalaniu Write Modelu – w sposób jakiego nie zakładał jej twórca. Traktując zwracany array jako View Model,
❌ Gdybyśmy zdecydowali się na wysyłkę emaila w Event Subscriberze nie mielibyśmy takiego łatwego dostępu do encji $product jak wcześniej. Musielibyśmy albo pozyskać ją z repozytorium  i w dalszym ciągu, z uporem maniaka korzystać z niej jak z Read Modelu.
 

Z pozoru prostrze rozwiązanie może powodować więcej problemów w przyszłości niż rozwiązuje teraz.

View Model w nowym kontekście

By uchronić się przed silnym couplingiem pomiędzy dwoma wspomnianymi kontekstami, nie możemy cały czas polegać na jednej tablicowej strukturze danych. Musimy wprowadzić nowy byt, a z racji tego, że zserializowana encja Product była początkowo wykorzystywana w kontekście zapisu danych do tabeli bazodanowej – tą część pozostawimy w pierotnej postaci. W kontekście wysyłki maili zostanie utworzona nowa klasa typu View Model.

Obiekty tego typu:
    • stanowią warstwę buforową pomiędzy rdzeniem naszej aplikacji, a światem zewnętrznym,
    •  są niemutowalne, służą jedynie jako struktura danych bez żadnych zachowań,
    • powinny posiadać odpowiednio sformatowane dane przeznaczone dla klienta.

Przykładowa implementacja mogłaby prezentować się następująco:
namespace App\Offer\Application\ViewModel;

final class ProductEmail 
{
	public function __construct(
		private string $name,
		private string $description,
		private float $price,
		private string $currency,
		private DateTimeImmutable $createdAt
	) {}
	
	public function asArray(): array 
	{
		return [
			'name' => $this->name,
			'description' => $this->description,
			'price' => number_format($this->price) . ' ' . strtoupper($this->currency),
			'createdAt' => $this->createdAt->format('Y-m-d H:i:s')	
		];
	}
}

Jak widać klucze jak i formatowanie udostepnianych danych są specjalnie dostosowane pod wymagania bytów ze świata zewnętrznego. Nie jesteśmy teraz od niczego zależni, dlatego na nowe wymagania biznesowe możemy dowolnie zmieniać serializację View Modelu – dodawać nowe klucze, modyfikować nazwy już istniejących jak i zmieniać foramtowanie samych wartości. Problemem może okazać się samo instancjonowanie tego obiektu, najpewniej w metodzie repozytorium powiązanym z Encją Product, niemniej jednak jest to niewielki koszt za cenę dobrego designe’u.

Koszt jaki musimy ponieść to:

💰 Utworzenie nowej klasy View Model’u
💰 Instancjonowanie VM, najpewniej w nowej metodzie repozytorium
💰 dodając do Encji
Product nowe pole, nie doda się ono automatycznie do View

Modelu co miałoby miejsce w pierwszej inkarnacji „Wysyłki emaila na utworzenie produktu”. Niewątpliwie rozpatrujemy to jako zaletę, ale również jako koszt – rzeczy o której należy pamiętać i potęcjalnie wykonać.

Złożoność

Możemy odnieść wrażenie, że wraz z dodawaniem kolejnych klas/metod rośnie złożoność projektu. W tym przypadku jest to złudne wrażenie. Pomyślmy, korzystając z jednej struktury tworzymy silny coupling pomiędzy modułami, które nie powinny być ze sobą powiązane na tym poziomie:

Dodajemy nowe pole do Encji Product ➡️ pojawia się nowy klucz możliwy do wykorzystania w templatce emaila.


lub:

Usuwamy pole z Encji Product ➡️ Zewnętrzny System przestaje wysyłać emaile o utworzeniu produktu.


I z szerszej perspektywy:

Modyfikujemy Domenę w Module Offer ➡️ Zewnętrzny System do emaili przestaje działać.


Nowy programista w projekcie, mógłby przeoczyć ten fakt i nie być nawet świadom takiego efektu ubocznego podczas dodawania nowego atrybutu Encji. Tego typu nieprzewidziane efekty uboczne są przyczyną powstania trudnych do wytropienia błędów, sprawiając, że złożoność oprogramowania rośnie.

Czas zweryfikował: rola jest jedna

Osobne byty dla innych kontekstów dają niesamowitą swobodę, ale jeżeli początkowo są identyczne i za każdym razem trzeba nanosić takie same zmiany do dwóch klas – znaczy, że nie powinniśmy w ogóle wprowadzać takiego rozdzielenia, bo pierwotny obiekt odgrywa taką samą rolę w dwóch kontekstach. 

Podsumowanie

Tak jak zostało wykazane w opisywanym przypadku, by usunąć złożoność i ukryty coupling, musiały być utworzone dodatkowe byty. W konsekwencji tego, mogą pojawiać się głosy innych członków team’u:

Czasami trudno jest przewidzieć konsekwencje niektórych decyzji podczas implementacji. Dlatego warto czasami zatrzymać się i przemyśleć niektóre kwestie, jakie będą ich konsekwencję. Z pozoru „skomplikowanie” projektu dodaniem kilki nowych klas może okazać się zabawienne w czasie jego dalszego życia. Zachęcam więc do przemyśleń w tej kwestji.

 

sobota, 7 stycznia 2023

Weryfikacja stanu obiektu gdy stan nie jest ujawniony

 

Spotkałem się ostatnio z ciekawym przypadkiem weryfikowania wyniku testu jednostkowego. Testowaniu została poddana void'owska metoda Agregatu i nijak nie można było wyciągnąć na zewnątrz stanu obiektu, który powinien ulec zmianie w wyniku wywołania metody under test, by można było go zweryfikować z oczekiwaniami. Agregat nie posiadał powiązanych z tym stanem getter'ów, wygenerowane event'y nie mogły zostać jawnie zwrócone poza obiektu (\Prooph\EventSourcing\AggregateRoot tego nie udostępniał) i żadna inna metoda jakkolwiek nie wyrzucała stanu na zewnątrz. Dodanie takich funkcji na siłę, tylko na potrzeby wykonania testu jednostkowego wydawało się nadużyciem więc też takie rozwiązanie nie wchodziło w grę.

Przez powyższe problemy, trzeba było wykazać się pomysłowością by napisać test - kolega z zespołu wpadła na takie oto rozwiązanie:

public function test(): void
{
    $objectUnderTest = new Person('Jan', 'Kowalski');

    // When
    // ...

    Closure::fromCallable(function () {
        TestCase::assertSame('Jan', $this->firstName);
        TestCase::assertSame('Kowalski', $this->lastName);
    })->bindTo($objectUnderTest, Person::class)();
}

Gdy pierwszy raz zobaczyłem takie podejście byłem zaskoczony że tak się da, zaraz potem uznałem że tak nie powinno się robić. Tyle raz powtarzano, że testowaniu powinno podlegać jedynie publiczne API obiektu, a jego wewnętrzny stan powinien zostać w ukryciu... i jest to mądre podejście. Dzięki temu mamy wolną rękę w przeprowadzaniu refaktoru - raz napisany test, daje nam możliwość weryfikowania czy ulepszanie wewnętrznej struktury obiektu po zmianach działa dalej tak jak tego oczekujemy.   

To rozwiązanie łamię zasadę, wiążąc test z ukrytymi przed resztą kodu produkcyjnego wnętrznościami Agregatu. Potem jednak zacząłem się zastanawiać na ile realnie problematyczny będzie on w utrzymaniu i czy czasem od dawna nie mierzymy się z tego typu problemami.

🔒 Kod produkcyjny dalej nie wie o stanie Agregatu

Niewątpliwą zaletą zastosowania metody Closure::bindTo jest fakt, że nie wpływa ona w żadnym stopniu na kod produkcyjny. Nie utworzyliśmy specjalnych getter'ów wykorzystanych tylko na potrzeby testów co w przyszłości mogłoby skłonić innych developerów do skorzystania z nich. W pewnym stopniu getter jest formą udostępniania wartości 1:1 z tym co jest wewnętrznym stanem obiektu. Dodanie getter'a wygląda jak rozszerzenie publicznego API, ale tylko na pozór. Bo jeżeli zmienimy wewnętrzny stan to będziemy musieli zmienić owego getter'a. Można by zastanowić się nad innymi realnymi problemami z tym związanymi, ale jest to temat na inny wpis.   

W każdym razie wydaje mi się, że problem tworzenia takich getter'ów istnieje w światku PHP, jest stosunkowo często stosowany i tylko sprawia wrażenie niegroźnego - dlatego powinniśmy zwracać na to uwagę. 

Stosowanie Closure::bindTo zamias dedykowanych getterów chroni kod produkcyjny.

🇬🇧 Szkoła Londyńska

Pisząc test jednostkowy klasy typu serwis (np. Event Subscriber'a, Command Handler'a) według paradygmatów tz. Szkoły Londyńskiej musimy najpierw zamockować jego zależności. Można to zrobić za pomocą customowych implementacji InMemoryRepository bądź przy użyciu narzędzi takich jak Prophecy/MockObject.

I tutaj właśnie zacząłem dostrzegać podobieństwo względem opisywanego przykładu. Dokładnie wiemy jak w teście jednostkowym należy zbudować zależność wymaganą przez object under test oraz jak będzie wyglądała interakcja pomiędzy zależnością, a testowanym obiektem.

Musimy jawnie oprogramować to związanie by test mógł przejść na zielono. Test oczywiście weryfikuje publiczne API serwisu, ale wie też coś o jego wewnętrznej pracy. Zmieniając zależności serwisu będziemy musieli zaktualizować testy - dokładnie tak samo jak w opisywanym przypadku wykorzystania Closure::bindTo.

Jest jednak subtelna różnica, w przypadku Closure::bindTo funkcja anonimowa musi dokładnie wiedzieć w jakim polu znajduje się wartość czyli pożądana przez nas zmiana stanu. W przypadku weryfikowania mock'ów zaś, ta informacja w dalszym ciągu jest przed nami ukryta, ale niewątpliwie w jednym i drugim przypadku odwołujemy się podczas wykonywania asercji do wewnętrznego stanu obiektu.   

Jak widać ten problem towarzyszył w projekcie w którym pracuje praktycznie od samego początku jego powstania, a mimo to dało się z nim żyć - co więcej - nikt nie uznawał tego za problematyczne. W opisywanym przypadku pojawił się jedynie w nieco innej formie (inny typ testowanego obiektu), ale to dalej nic nowego z czym wcześniej się nie borykaliśmy.  

🗃 Dla jakich typów obiektów?

Na samym początku muszę zauważyć, że chodzi o zmianę stanu obiektu dlatego z tego rozwiazania należałoby korzystać tylko w przypadku testowania metod void'owskich. Takich metod nie posiadają obiekty typu:

❌ Event
❌ DTO
❌ Value Object

Serwisy z metodami void'owskimi też weryfikujemy w inny sposób - sprawdzając ich z'mock'owane zależności, dlatego kolejno odpadają nam:

❌ Event Subscriber
❌ Command Handler
❌ Serwis Aplikacyjny/Domenowy

To co właściwie pozostało to:

✅ Encja
✅ Agregat 

Lecz tylko w przypadku gdy takowe już nie udostępniają swojego stanu dla innego kodu produkcyjnego!

🩻 Inny sposób udostępniania stanu

Taki sam efekt możnaby osiągnąć stosując refleksje:

public function test(): void
{
    $objectUnderTest = new Person('Jan', 'Kowalski');

    // When
    // ...

    $reflection = new ReflectionClass(Person::class);
    $firstNameProperty = $reflection->getProperty('firstName');
    $firstNameProperty->setAccessible(true);
    $lastNameProperty = $reflection->getProperty('lastName');
    $lastNameProperty->setAccessible(true);

    self::assertSame('Jan', $firstNameProperty->getValue($objectUnderTest));
    self::assertSame('Kowalski', $lastNameProperty->getValue($objectUnderTest));
}

W porównaniu z zastosowaniem Closure::bindTo wypada podobnie jeżeli chodzi o podejście czy konsekwencje dla potencjalnego refaktoru. Więcej linii kodu negatywnie wpływa na czytelność i w wątpliwość możemy poddawać szybkość działania, co nie powinno stanowić problemu gdy ten kod tak czy inaczej nie będzie działał produkcyjnie. 

♻️ Warstwa abstrakcji na asercję

Można by dodatkowo schować taką implementację przed testem w customowej klasie Assert. Jeżeli z biegiem czasu pojawi się jakiś sposób na lepsze udostępnianie stanu niż za pomocą Closure::bindTo lub refleksji to sama klasa testowa nie będzie wymagała modyfikacji. Dodatkowo wprowadzenie specjalnej klasy asercji wpłynie na poprawę czytelności samego testu.

public function test(): void
{
    $objectUnderTest = new Person('Jan', 'Kowalski');
    
    // When
    // ...
    
    PersonAssert::sameName('Jan', 'Kowalski', $objectUnderTest);
} 

📝 Podsumowanie

Koniec końców uważam, że zastosowanie Closure::bindTo było dobrym pomysłem. Pomimo tego, że udostępniamy prywatny stan na zewnątrz, napisany test jednostkowy przynosi wartość. Co prawda jesteśmy mniej odporni na zmiany klasy poddanej testom, ale z drugiej strony nie mieliśmy zbytniego wyboru. Moglibyśmy użyć testów integracyjnych lecz nie są one tak precyzyjne jak testy jednostkowe. Niekiedy nie dysponujemy takim rozwiązaniem w projekcie co uniemożliwiłoby całkowicie otestowanie takiej klasy. Korzystanie z Closure::bindTo powinno być ograniczone tylko do absolutnej konieczności gdy nie mamy innego wyboru, jest ono obarczone pewnym obciążeniem, ale na tyle małym by w dalszym ciągu utrzymać wszystko w ryzach. 

Jedyne nad czym trzeba się zastanowić to czy weryfikacja działania obiektu jest więcej warta niż poniższe efekty uboczne:

⚠️ zmiana wewnętrznego stanu wymaga naprawienia testu
⚠️ zmiana nazwy pola sprawi że komunikat nieprzechodzącego testu nie będzie do końca jasny (UndefinedProperty)
⚠️ narzędzia do analizy statycznej mogą zgłaszać błąd w związku z nieznanym polem

    

 

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.

sobota, 15 października 2022

Mockowanie Repozytoriów

Testowanie jednostkowe klas posiadających jako zależności Repozytoria np. Seriwsów/Command Handler'ów może być kłopotliwe. Jeżeli zdycydujemy się na sztuczne implementacje Repozytoriów tzw. *InMemoryRepository – powinniśmy być świadomi kwestii związanych z ich późniejszym utrzymaniem – jakie problemy rozwiązują, a jakie stwarzają. Kod metody poddawanej testom nie jest tym nad czym chaciałbym się skupić w tym wpisie, istotne jest jedynie wywołanie w niej metody Query (CQS) Repozytorium.

 

Podejścia do Mock’owania Repozytoriów

Na potrzeby testu jednostkowego musimy odtworzyć wierną kopię klasy produkcyjnej. Np. posiadamy interfejs repozytorium OrderRepository:

interface OrderRepository 
{
	/** @return Order[] */
	public function getUnpaidOrders(): iterable;
}

 

Abstrahując od tego jak wyglądałaby implementacja klasy produkcyjnej OrderMySQLRepository, skupmy się na jej odpowiedniku utworzonym na potrzeby testu:

final class OrderInMemoryRepository implements OrderRepository
{
	/** @param Order[] $orders */
	public function __construct(private array $orders) {}
	
	public function getUnpaidOrders(): iterable
	{
		return array_filter(
			$this->orders,
			static fn ($order) => $order->isUnpaid() 
		);
	}
}


Implementacja metody OrderInMemoryRepository::getUnpaidOrders została tak napisana by zawsze zwracała odpowiednią kolekcję zamówień - logicznie zgodną z nazwą metody. Dzięki takiej implementacji dysponujemy całkiem poręcznym narzędziem do pisania testów jednostkowych. W przypadku gdy interfejs posiadałby by inne metody – też w takim stopniu odtwarzające rzeczywistą implementację – moglibyśmy używać tego samego obiektu OrderInMemoryRepository w wielu przypadkach testowych. Jest to dość złożony TestDouble typu Fake, który niejako posiada więdzę na temat tego jak działa produkcyjna implementacja.

Moglibyśmy przyjąć inną taktykę w której OrderInMemoryRepository jest maksymalnie okrojony z implementacji:

final class OrderInMemoryRepository implements OrderRepository
{
	/** @param Order[] $orders */
	public function __construct(private array $orders) {}
	
	public function getUnpaidOrders(): iterable
	{
		return $this->orders;
	}
}

 

Jako, że OrderInMemoryRepository::getUnpaidOrders zawsze zwraca taką samą kolekcję jak ta dostarczona do konstruktora – można powiedzieć, że jest to pewna forma Stub’a.Ciężar doboru odpowiednich danych wejściowych spoczywa na metodzie w której sztuczne Repozytorium zostało utworzone – np. Fixtura, metoda testowa/setUp.

Korzystanie z wariantu Stub jest niemal identyczne jakbyśmy używali biblioteki mokującej np. MockObject czy Prophecy. W dwóch przypadkach musimy staranie dobrać zwrotkę metody do aktualnego przypadku testowego. Pomiędzy przygotowaną kolekcją obiektów wrzucaną w metodę obiektu z biblioteki mokującej/czy konstruktorem Stub *InMemoryRepository, a zwróceniem danych z wywołanej metody Repozytorium, nic więcej się nie dzieje - nie następuje żadne filtrowanie.

Fake Repository

❌ jest silnie sprzężony z Produkcyjnym Repozytorium (po każdej zmianie repozytorium MySQL, musimy sprawdzić czy *InMemoryRepository nie wymaga modernizacji, czy w dalszym ciągu wiernie odzwierciedla prawdziwą implemetację),

✅ koncept stojący za nazwą metod jest jawnie zapisany w kodzie,

✅ odpowiednio przygotowany może być wykorzystany w wielu przypadkach testowych.

 

Stub Repository

❌ musi być indywidualnie przygotowany pod każdy przypadek testowy. Dobór zwrotek jego metod stoi po stronie developera – jest więc nie do końca jawny,

❌ w przypadku zmiany metody Produkcyjnego Repozytorium, musimy zweryfikować wszystkie przypadki testowe kodu, który bezpośrednio polega na tej metodzie. Czy w dalszym ciągu przygotowane przez developera obiekty zwracane przez metodę sztucznego Repozytorium spełniają logikę która stoi za ich nazwą? 

✅ zaletą jest mniej kodu do utrzymania, ale to tylko dlatego, że koncepty te nie są zawarte w kodzie z czego wynikają opisane wyżej problemy. W przypadku gdy istnieje tylko jeden klient metody Repozytorium możemy zastosować Stub’a – wraz z rozrostem repozytorium o nowe metody, zwiększeniem się liczby klientów należy rozwarzyć refaktoryzację w stronę Fake’a.

Koniec końców, czy stosujemy jedną czy drugą metodę – cały czas jesteśmy w tej samej sytuacji – musimy inscenizować dostarczanie danych do testowanej metody. W przypadku Fake’ów z bardziej skomplikowaną implementacją, dysponujemy po prostu bardziej wszechstronnym narzędziem kosztem jego późniejszego utrzymania. 

 

Utrzymanie klasy Fake InMemory Repository


Nowe wymagania biznesowe w czasie kolejnych iteracji wymuszają zmianę repozytoriów - prawdziwych i sztucznych.

Przedstawione na diagramie zmiany interfejsu Repozytorium, są oczywiście napędzane decyzjami biznesu co do nowych feature’ów. Jak widać testując jednostkowo metodę wywołującą metody Repozytorium, musimy wprowadzić do systemu testowego nowy byt. Każda zmiana implementacji repozytoriów będzie wymuszała na nas prace związane z jego utrzymaniem. Można wywnioskować, że klasy *InMemoryRepository to dodatkowy koszt jaki musimy ponieść za cenę bezpieczeństwa czyli łatwiejszego wprowadzania zmian w projekcie.

Błędne będzie jednak założenie, że Mock’owanie Repozytoriów zawsze daje nam wspomniane bezpieczeństwo. W przypadku gdy pomiędzy Klasą Produkcyjną, a Test Double pojawią się rozbieżności w działaniu – wprowadzone nieświadomie/omyłkowo przez programistę – testy jednostkowe mogą przechodzić na zielono, podczas gdy tzw. Produkcja będzie rzucał błędami lub działała niezgodnie z oczekiwaniami. Jest to ryzyko z którego powinniśmy zdawać sobie sprawę podczas synchronizacji Fake InMemory Repository z Repozytorium Produkcyjnym.

Warto też zauważyć, że im bardziej uniwersalna jest metoda repozytorium tzn. posiada filtry, tym trudniej utworzyć jej imitację potrzebną do testów jednostkowych. Odtworzenie poprawnego zachowania wszystkich filtrów może być skomplikowane, a wprowadzona złożoność z tym zwiazana - podatna na błędy w przyszłości. Przy tego typu pracach należy zachować szczególną ostrożność, gdyż jak na ironię, nie posiadamy testu jednostkowego klasy InMemory.

Jeżeli zaś chodzi o sam design, na tak uniwersalnej metodzie Repozytorium polegać będzie zapewne dużo klientów, co będzie skutkowało pojawieniem się zależności między nimi czego wspólnym mianownikiem jest wspomniana „uniwersalna” metoda Repozytorium.

 

Podejście funkcyjne

Możemy przyjąć zupełnie inną taktykę. Zamiast zastanawiać się nad najlepszym sposobem testowania metod które pozyskują dane z repozytoriów - nie testować ich w ogóle. Wiąże się to z destylacją logiki biznesowej zawartej w testowanej metodzie poprzez wydzielenie wszystkich wyowłań metod Query (CQS) do warstwy wyżej.

Metoda Serwisu samy pozyskuje dane z Repozytorium. Taka implementacja wymaga ich mock'owania.

 

W wyniku takiej zmiany design’u: wydestylowany byt staje się Serwisem Domenowym, a warstwa która pozyskuje Encję/Agregaty/VO z repozytoriów – Serwisem Aplikacyjnym, którego domenowy odpowiednik otrzymuje wszystkie potrzebne do przeprowadzenia operacji obiekty, jako parametry metody.

 

Testowany Serwis Domenowy poddawany nie wie nic o klasach Repozytoriów.

Testowaniu jednostkowem podlegać będzie wtedy tylko Serwis Domenowy, dla którego nie będziemy musieli już szykować żadnego Test Double Repozytorium. Serwisy Aplikacyjne będą testowane tylko Integracyjne.

Niektóre przypadki mogą okazać się problematyczne do zaimplementowania – bo jak mamy rozwiązać problem jakiegoś bytu który jest pobierany z repozytorium na podstawie jakiejś decyzji biznesowe?

Podsumowanie

Jak widać mockowanie repozytoriów wiąże się z pewnego rodzaju problemami z których należy sobie zdawać sprawę – jak zwyklę w programowaniu nic nie jest czarno białe i rozwiązując pewien problem godzimy się na wprowadzenie mniejszego. Testy Integracyjne niejako rozwiązują sprawę tworzenia Test Double Repozytoriów w ogóle, niemniej jednak nie zawsze takowe z łatwością można wprowadzić w projekcie.


 

 

Metoda Prywatna vs. Value Object

🔒 Metoda Prywatna

Na samym początku musimy odpowiedzieć sobie na pytanie dlaczego tworzymy metody prywatne:

  • potrzebujemy współdzielić kod w conajmniej dwóch innych metodach danej klasy,
  • chcemy zredukować złożoność i ukryć pewny spójny logicznie kawałek kodu,

Niewątpliwie są  to dobre powody, ale stosowanie metody prywatnej nie jest najlepszym sposobem na uwspólnianie kodu czy redukowania jego złożoności.  

 

💡 Narodziny Konceptu Domenowego

Mamy klasę ShippingService z metodą publiczną getCost zwracającą cenę wysyłki. W samym ciele tej metody zaczyna rosnąć ilość linii kodu weryfikującego czy dostawa powinna być darmowa czy nie. Logika nie jest banalna i na podstawie nowych wytycznych biznesu koncept darmowej wysyłki jest dopiero implementowany w kodzie.

final class ShippingService 
{
	public function getCost(...): Money 
	{
		// call private method
	}

	private function isFree(...): bool 
	{
		// using class dependencies
	}
}

Nie wchodząc w szczegóły implementacji, skupmy się na samym fakcie powstania nowej metody prywatnej ShippingService::isFree - czyli zredukwaniu złożonoście metody głównej ShippingService::getCost. Metoda prywatna enkapsuluje warunki podejmujące decyzję czy wysyłka jest darmowa. Wyłonił się tutaj nowy koncept domenowy i niestety jest on zamodelowany za pomoca metody prywatnej.

Problem pojawia się gdy inny serwis również będzie musiał operować na koncepcie darmowej wysyłki. W tym wypadku potrzebny byłby refaktor - upublicznienie metody bądź wydzielenie jej do nowego serwisu nie wydaje się też dobrym rozwiązaniem. Więc jeżeli pozostawimy to w obecnej formie musimy liczyć się z następującymi konsekwencjami:     

❌ niemożliwe jest ponowne wykorzystanie metody w innym obszarze,

❌ istnieje ryzyko duplikacji logiki - świadomej (przez lenistwo programisty) / nieświadomej (trudniej odnaleźć logikę w metodzie prywatnej niż w osobnej klasie).

❌ modelowanie konceptów domenowych za pomocą metod serwisowych ogranicza nasze możliwości co do rozwoju konceptów które odzwierciedlają. 

Koncept domenowy jest ukryty

 

⚠️ Metoda jako wyrażanie Konceptu Domenowego

Przechowywanie konceptów domenowych w metodach jest bardzo ograniczające w ich potencjalnym dalszym rozwoju. Istnieje bardzo duża szansa, że sam koncept domenowy z czasem będzie się rozwijał nabierając pełniejszego kształtu. 

Jeżeli obecnie oczekujemy tylko odpowiedzi na to czy dla danej kwoty wysyłka jest darmowa to w przyszłości możemy potrzebować np. samego progu cenowego darmowej wysyłki. Wymagałoby to od nas utworzenia kolejnej wyspecjalizowanej metody. 

Co więcej przy nieznajomości projektu możemy mieć problemy z ustaleniem czy taki byt w ogóle istnieje w projekcie - z perspektywy katalogów jest on niewidoczny. 

Dodatkowo cementujemy logikę biznesową wraz ze sposobem pozyskiwania danych (pobieranie danych z repozytorium) co może prowadzić do ich parametryzacji bądź wydzielenia części logiki do kolejnych metod prywatnych.

 

🧩 Value Object

Nie jest to wpis o Value Object'cie samym w sobie dlatego tylko wypiszę jego przewagi względem stosowania metod serwisowych:

  • jest uniezależniony od kontekstu wywołania,
  • nazwa klasy może nadać mu bardziej abstrakcyjny charakter (metoda shippingDays na Value Object ShippingTime::days),  
  • bardzo łatwy w testowaniu,
  • jawna i niezależna jednostka przechowująca wiedzę domenową,
  • widoczny z poziomu katalogów,  
  • bardzo łatwy do ponownego wykorzystania w innym obszarze. 

 

♻️ Refaktoryzacja do Value Object

Pierwszym etapem refaktoryzacji do Value Object'u może być transformacja do prywatnej metody statycznej. W tym wypadku wszystkie parametry wejściowe stałyby się w dalszym toku przekształceń parametrami konstruktora Value Object'u.

Przenosząc odpowiedzialności do Value Object'u i powiązanej z nim Fabryki redukujemy złożonośc samego Serwisu w którym logika ta się wcześniej znajdowała. Zależności przechodzą z Serwisu do Fabryki i to właśnie ona od teraz jest wstrzykiwana do klasy Serwisowej. 

Przekształcenie metody prywatnej na Value Object


sobota, 13 listopada 2021

Core Code & Infrastruktura

Najprościej rzecz ujmując - Core Code - jest to kod niezależny od infrastruktury i kontekstu wywołania. Wzorce służące do jego implementacji to: Command, Serwis, Repozytorium, Fabryka, Read Model, Write Model (Encja) czy Value Object. Z drugiej strony mamy tytułową Infrastrukturę do której należą np. implementacje interfejsów repozytoriów czy kontrolery HTTP/CLI. Stosując podział na Core Code i Infrastrukturę realizujemy ważny cel: odseparowanie kodu realizującego funkcjonalność od szczegółów technicznych. Istotnym elementem w poznaniu każdego z tych wzorców jest zrozumienie w jaki sposób działają ze sobą nawzajem, oraz na jakim etapie cyklu życia aplikacji występują. W tym wpisie nie skupie się na wspomnianych wzorcach, ale na istocie tytułowego podziału na Core Code i Infrstrukturę - czyli dlaczego jest on istotny i jakie korzyści przyniesie.  
 

Odseparowanie od Infrastruktury. Why even bother...?

    Odseparowanie od infrastruktury możemy zrealizować poprzez stosowania wyżej wymienionych wzorców. Jakby się na tym zastanowić to pilnując by kod dzielił się na Core Code i Infrastrukturę, realizujemy główne założenia OOP - wprowadzanie warstw abstrakcji. Biorąc za przykłąd Serwis Aplikacyjny - czytając jego kod, nie musimy zaprzątać sobie głowy szczegółami technicznymi zapisu danych do bazu ponieważ Serwis posiada zależność w postaci Interfejs Repozytorium, która skutecznie ukrywa złożoność tego zagadnienia. Jeżeli będziemy potrzebowali tej wiedzy, po prostu wyszukamy Implementacje Repozytorium ukrywającą techniczne aspekty zapisy. Dzięki temu nie jesteśmy zbyt wcześnie obciążeni więdzą, której na danym etapie w ogóle nie potrzebujemy. 

    Często wymienianą zaletą odseparowania Core Code od Infrastruktury jest wymienialność szczegółów implementacyjnych jak np. baza danych. Prawde mówiąc wymiana bazy na inną jest dość rzadko występująca czynność w czasie życia projektu, ale jeżeli bylibyśmy do niej zmuszeni np. z przyczyn wydajnościowych to przy wydzielonej Warstwie Infrastruktury jesteśmy do tego zdolni porównywalnie mniejszym nakładem pracy. Pojęcie szczegółu implementacyjnego nie ogranicza się jedynie do bazy danych, ale jesto to o wiele szersze spektrum, gdzie do Infrastruktury zaliczamy całe frameworki, biblioteki, połączenia z zewnętrznymu systemami. Od wszystkich tych zależności możemy się odgrodzić warstwą abstrakcji co w nieznanej przyszłości może się okazać nieocenioną zaletą. Jak to powiedział Michael Feathers:  

"Avoid littering direct calls to library classes in your code. You might think that you’ll never change them, but that can become a self-fulfilling prophecy"

    Można wywnioskować, że to co robimy to izolowanie się od technologii służącej do realizowania funkcjonalności, która nie definiuje funkcjonalności samej w sobie. Przez to, że mamy fizycznie rozdzielone miejsca obsługi zapisu do bazy danych konkretnej Encji i definicji samej Encji - utrzymanie projektu staje się prostrze. Na przykład chcąc podnieść wersje biblioteki obsługującej połączenie z bazą danych będziemy operowali jedynie na Warstwie Infrastruktury nie ruszając przy tym kwestii biznesowych, w wyniku czego ryzyko nieumyślnego wprowadzenia zmiany funkcjonalności biznesowej znacząco maleje.

Fizyczne rozdzielenie - czyli tworzenie oddzielnych klas domeny problemu oraz infrastrukturowych w osobnych Warstwach. Warstwy według standardowego modelu dzielą się na Aplikację, Domenę oraz Infrastrukturę - realizowane są przez namespace'y. Jest jasno określone, jaką wiedzę mogą posiadać klasy w danej warstwie o bytach (klasy, interfejsy, enum) z innej warstwy. Jako, że PHP natywnie nie ma zaimplementowanego mechanizumu enkapsulacji przestrzeni nazw, nic nie stoi na przeszkodzie by zaimplementować zły kierunek komunikacji np. Wastwa Aplikacji posiada odwołania do klas z Warstwy Infrastruktury. Jedynie dyscyplina developerów w przestrzeganiu zasad może pilnować tej poprawności (być może przy pomocy analizy statycznej).  

    Z wyizolowaną Warstwą Infrastruktury sprawiamy, że możemy stosować TDD bo klasy z Core Code są łatwe w testowaniu jednostkowym. Dodatkowy nakład pracy może prowadzić do powstania modelu domenowego wedle DDD. Stosowanie tych technik niewątpliwie przyczyni się do jakości wytwarzanego oprogramowania. Dodatkowo, tworzenie klas z myślą o odseparowanej infrastrukturze, naturalnie prowadzi do implementacji popularnego wzorca architektonicznego Porty i Adaptery.  

Wewnętrzna jakość oprogramowania

    Przez Jakość Wewnętrzną rozumie się szerokopojętą czytelność kodu, łatwość w utrzymaniu i rozwijaniu aplikacji. Wydaje się, że na jakości kodu zależy głównie programistom, gdyż dla ludzi biznesu jej istotność na pierwszy rzut oka nie jest taka oczywista. Jako że oprogramowanie tworzone jest w sposób iteracyjny, pisane klasy muszą realizować spójne funkcjonalności, tworzyć luźno powiązane większe struktury i być pokryte należytą ilością testów jednostkowych. Nigdy nie wiadomo jakie zmiany najdejdą w kolejnych iteracjach, więc musimy starać się tworzyć solidne elementy budulcowe (klasy/grupy klas/moduły) w całym projekcie. Utrzymując ścisłą dyscypline przy tworzeniu wysokiej jakości oprogramowania sprawiamy, że będziemy mogli modyfikować zachowanie oprogramowania w sposób przewidywalny i bezpieczny, minimalizując ryzyko, że zmiana będzie wymagała dużej przeróbki. 

    Oprogramowanie jest oczywiście tworzone do realizowania celów biznesowych, ale musi ono też służyć developerom. Moglibyśmy napisać kiepski kod spełniający w 100% wymagania klienta (o wysokiej jakości zewnętrznej), ale wprowadzanie zmian w oprogramowaniu niskiej jakości jest skomplikowane, nieprzewidywalne i ryzykowne - z czasem wymagające coraz większej uwagi developera i nakładu czasu. 

Podsumowując: 

  1. im łatwiej i pewniej wprowadzać zmiany w oprogramowaniu tym lepiej
  2. by łatwiej wprowadzać zmiany w oprogramowaniu trzeba pisać kod o wysokiej jakości wewnętrznej
  3. istnieje szereg technik do osiągnięcia wysokiej jakości kodu   

    Jak widać tematy te są pochodną wydzielania Core Code i wzajemnie się zazębiają. Stosując wzorce Rdzenia Aplikacji będziemy mogli łatwo przetestować jednostkowo tworzone obiekty, co dowodzi że kod jest modułowy oraz niezależny od kontekstu co jest równoznaczne z wysoką jakością.    

Wzorce Projektowe

    Wymienione na samym początku wzorce, pomagają w pisaniu kodu odseparowanego od infrastruktury. Opracowane były na podstawie sumy doświadczeń innych programistów w budowaniu aplikacji webowych. Są one dla programisty zestawem jasno zdefiniowanych i sprawdzonych elementów budulcowych. Stosowanie wzorców sprawia, że kod jest czytelny, modułowy i łatwy w testowaniu. Developer znający całą paletę wzorców może korzystać z niej jak przybornika z narzędziami - dobierając narzędzie najlepiej dopasowane do probelmu. Nie trzeba wtedy wymyślać koła na nowo i zastanawiać się nad tym czy zastosowane rozwiązanie jest dobre. Mając sprawdzony zestaw wzorców oraz posiadając praktyczną wiedzę ich stosowania jesteśmy w stanie modyfikować oprogramowanie szybciej i pewniej. 

    W rękach developera jest to czy poszerza on swoją wiedzę w dziedzinie wytwarzania oprogramowania wysokiej jakości. Zgłębianie informacji na temat wzorców projektowych sprawia że, zaczynamy widzieć więcej. Implementując kod możemy wybiec w przyszość, przewidując jakie będzie niósł ze sobą konsekwencje, przed jakimi potencjalnymi problemami nas chroni, w jaki sposób sprawia że jest wysokiej jakości. Korzystając z danego wzorca dziesiątki bądź setki razy, posługujemy się nim coraz lepiej, widzimy jakie warianty się najlepiej sprawdzają, a w jakich sytuacjach lepiej go nie stosować.

    Można powiedzieć, że przybornik z narzędziami developera uzupełniany jest o nowe elementy gdy poszerza on swoją wiedzę - teoretyczną i praktyczną. Warto szlifować swoje umiejętności posługiwania się tymi narzędziami gdyż bezpośrednio przekładają się na umiejętności tworzenia kodu wysokiej jakości, czyniąc nasza pracę prostszą.

Na sam koniec wrzucam ciekawy cytat Matthias'a Noback'a dający wiele do myślenia:   

 “Software always becomes a mess, even if you follow all the best practices for software design  but I’m convinced that if you follow these practices it will take more time to become a mess  and this is a huge competitive advantage”

Źródła 

Wpis jak i cała koncepcja została zaczerpnięta przede wszystkim z przemyśleń Matthias'a Noback'a (blog) zawartych w jego dwóch książkach:

📕 Advanced Web Application Architecture (2020)
📕 Object Design Style Guide (2019) 

Wzmianka o Wewnętrznej jakości oprogramowania została zaczerpnięta z książki:

📕 Growing Object-Oriented Software, Guided by Tests (2009)