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