P-Programowanie

Budowniczy

Ostatania modyfikacja: 26 marca 2018, kategoria: Wzorce projektowe

Budowniczy to jeden ze wzorców projektowych używanych w programowaniu obiektowym. Zalicza się on do rodziny wzorców konstrukcyjnych. Dzięki użyciu budowniczego oddzielamy proces tworzenia obiektu od jego reprezentacji. Jest to dość prosty wzorzec, który jednak sprawia problemy, ze względu na różne warianty w jakich występuje. W tym artykule przedstawię dwie podstawowe implementacje tego wzorca oraz opiszę różnice, jakie między nimi zachodzą.

Różne odmiany budowniczego

Na samym początku należy zdać sobie sprawę z tego, że istnieją dwie popularne odmiany tego wzorca, jedna opisana przez GoF (ang. Gang of Four) a druga (będąca modyfikacją pierwszej) opisana przez Joshue Blocha. W większości przypadków mówiąc o wzorcu budowniczy ma się na myśli jego pierwszą odmianę – w tym artykule także będę się do niej odnosił. Wersja statyczna budowniczego zostanie opisana w jednym z akapitów poniżej.

Po co używać wzorca budowniczy?

Budowniczy ma za zadanie rozwiązać pewien powtarzający się problem programistyczny – konkretniej zapewnia oddzielenie procesu inicjalizacji obiektu od jego reprezentacji. Decydując się na użycie tego wzorca można osiągnąć następujące korzyści:

  • logika mówiąca o tym jak obiekt ma być zbudowany będzie oddzielona od implementacji tej logiki
  • spełnia zasadę otwarty na rozbudowę, zamknięty na modyfikacje (open/closed principle) – łatwo dodać do kodu nowych budowniczych
  • spełnia zasadę odwrócenia zależności (dependency inversion principle)
  • sprzyja samodokumentującemu się kodowi (widząc poszczególnych budowniczych wiemy, co dostarczą)
  • daje doskonałą kontrolę nad etapami budowania kompozytu (np. hermetyzacja obsługi błędów dla danego kroku)

Rzadko zdarza się aby za pomocą wzorca budowniczy inicjalizować proste klasy DTO. Częściej natomiast, gdy złożony model składa się z wielu typów referencyjnych na zasadzie kompozycji.

Kiedy używać budowniczego

Budowniczy najczęściej używany jest wtedy, gdy istnieje potrzeba zbudowania złożonego obiektu (tzw. kompozytu). Złożony obiekt może oznaczać tworzenie „super klasy” – odpowiedzialnej za zbyt wiele rzeczy, a więc złamanie pierwszej zasady SOLID. W pierwszej kolejności należy więc spróbować uprościć złożony model, jednak jeżeli okaże się to niemożliwe, należy zastanowić się nad implementacją wzorca budowniczego. Szczególnie, jeżeli nasz złożony kompozyt ma być budowany na wiele różnych sposobów i stoi za tym jakaś logika.

Należy rozważyć użycie wzorca budowniczy gdy:

  • obiekt, który tworzymy jest złożony i nie da się go uprościć
  • nie da się utworzyć instancji obiektu poprzez jednorazową operację (wieloetapowa inicjalizacja)
  • obiekt, którzy tworzymy, będzie budowany wiele razy w różny sposób

Spójrzmy na taki, niezbyt fajny, kod:

W powyższym kodzie widzimy inicjalizację obiektu klasy Car. Prawdopodobnie chodzi o jakąś uboższą wersję, ponieważ tak możemy wywnioskować z nazwy zmiennej smallCar. Kod przeplatany jest logiką, w pewnych miejscach zostały użyte instrukcje warunkowe a gdzie indziej wzorzec prostej fabryki.

Kod jest i tak stosunkowo prosty, ponieważ brakuje w nim obsługi błędów, czyli instrukcji try/catch.  Reużycie kodu np. w celu zbudowania większego samochodu polegałoby na skopiowaniu tego kodu w całości i zmianie poszczególnych parametrów. Byłoby to podejście bardzo imperatywne, raczej mało obiektowe. W dużym projekcie wielu programistów konstruowałoby instancje klasy Car na swój sposób. Chcąc zmienić proces ich budowy, należałoby to zrobić w wielu miejscach.

Tak mogłaby wyglądać prosta implementacja budowniczego dla powyższego kodu:

Dzięki użyciu wzorca główna metoda programu znacząco się uprościła. Uzyskaliśmy jednolity interfejs polimorficzny służący do tworzenia instancji samochodu.

Rola poszczególnych elementów wzorca

Oto jego podstawowe elementy wzorca, które zostały użyte w powyższym przykładzie:

  • budowniczy (CarBuilder) – dostarcza abstrakcyjny interfejs służący do budowania finalnego produktu
  • konkretny budowniczy (SmallCarBuilder) – dostarcza implementację metodom budowniczego (implementacja)
  • kierownik (CarDirector) – konstruuje obiekt z wykorzystaniem jakiegoś budowniczego (logika)
  • produkt (product) – finalny złożony obiekt, dostarczony przez kierownika, zbudowany za pomocą jakiegoś budowniczego

Widząc pierwszy raz implementację wzorca budowniczy można odnieść wrażenie, że tych klas jest trochę za dużo. To naturalne, szczególnie jeśli rozumie się zasadę działania wzorca fabryki abstrakcyjnej. Poniżej przedstawię swoją krótką analizę tego wzorca, która być może rozwieje kilka wątpliwości:

  • najważniejszymi elementami wzorca jest kierownik oraz konkretny budowniczy. Kierownik skupia logikę budowania, konkretny budowniczy skupia implementację tej logikii
  • budowniczy jest nic nieznaczącym interfejsem (w przykładzie klasa abstrakcyjna) aby zapewnić wspólny interfejs polimorficzny dla konkretnych implementacji budowniczych (spełnienie 4 zasady SOLID odwrócenie zależności)
  • nie możemy zrezygnować z kierownika, chociaż wydaje się nadmiarowy. Niektóre implementacje tego wzorca rezygnują z niego jednak nie jest to wtedy wzorzec budowniczy zaprezentowany przez GoF.
  • dzięki oddzieleniu logiki od implementacji, możemy zmienić logikę budowania obiektu w jednym miejscu (kierownik) i zostanie ona zmieniona w całym systemie – w każdym budowniczym, a one nie będą tego świadome
  • można dojść do wniosku, że połączenie kierownikakonkretnym budowniczym bardzo przypominałoby fabrykę abstrakcyjną

Obsługa błędów

Obsługa błędów potrafi rodzić pewną konsternację wynikającą z tego, że jest wiele miejsc na ich obsłużenie. Zasada jest jednak ta sama co wszędzie – błędy należy zgłaszać tak szybko, jak tylko jest to możliwe. Oznacza to, że wyjątki powinny być rzucane na etapie wywoływań poszczególnych kroków budowniczego.

W statycznym budowniczym (o którym napisałem w akapicie niżej) pewne błędy muszą zostać obsłużone w metodzie budującej. Wynika to z faktu, że programista projektant danego rozwiązania nie ma panowania nad tym, które metody płynnego interfejsu zostaną wywołane i w jakiej kolejności. Oznacza to, że niektóre walidacje możemy wykonać w poszczególnych krokach budowniczego, jednak główną walidację obiektu musimy przeprowadzić w metodzie budującej zwracającej instancję produktu. Dzieje się tak np. gdy chcemy sprawdzić czy zbudowany obiekt samochód został (w ogóle) wyposażony w silnik.

Statyczny budowniczy

Myślę, że w wystarczający sposób opisałem cechy i zalety wzorca budowniczy, który został zaproponowany przez GoF. W programowaniu obiektowym częściej jednak korzysta z się z prostszej wersji budowniczego, która niestety jest nazywana tym samym terminem. Z tego względu czasem mogą pojawiać się pewne niezrozumienia, szczególnie – przykładowo – na rozmowach rekrutacyjnych.

Płynny interfejs

Statyczny budowniczy polega na użyciu metody zwanej płynnym interfejsem (z ang. fluent interface). Polska nazwa brzmi dość głupio, polecam zapamiętać tę angielską.

Podejście płynnego interfejsu polega na takim projektowaniu metod w paradygmacie programowania obiektowego, aby każda z nich zwracała instancję klasy, w obrębie której się znajduje. Standardowy przykład płynnego interfejsy może wyglądać następująco:

Wywołanie poszczególnych metod płynnego interfejsu w danej kolejności zwróci wynik 7. Teraz zapewne widzisz, skąd wzięła się nazwa płynny interfejs. W ten sposób bardzo często projektowane są przeróżne API (z ang. fluent API) oraz właśnie statyczny budowniczy. Nie ma znaczenia ile razy zostaną wywołane metody, za każdym razem zwracana jest instancja tej samej klasy więc możemy wywołać je ponownie.

Po co korzystać ze statycznego budowniczego

Statyczny budowniczy został po raz pierwszy zaproponowany przez Josha Blocha, w pewnej książce związanej z programowaniem w języku Java. Zauważył on, że dzięki konstrukcji budowniczego z wykorzystaniem płynnego interfejsu można osiągnąć mechanizm dający wiele korzyści:

  • uproszczenie trudnych konstruktorów
  • budowanie skomplikowanych obiektów (ale już bez oddzielenia logiki od implementacji!)
  • tworzenie obiektów niemutowalnych (ang. immutable)
  • długi/wieloetapowy proces inicjalizacji obiektu końcowego

Pozostałe zalety wzorca budowniczy opisane w akapitach wyżej, nie występują we wzorcu budowniczego statycznego opisanego Blocha.

Jak stworzyć statyczny budowniczy

Zasady tworzenia statycznego budowniczego są takie, że nie ma zasad. Dlaczego? Budowniczy opisany przez GoF jest sklasyfikowany jako wzorzec konstrukcyjny, a ze względu na jego zakres jako wzorzec obiektowy. Oznacza to, że po pierwsze, skupia się na procesie inicjalizacji obiektów, a po drugie, bazuje na powiązaniu klas poprzez kompozycje (mimo, że dziedziczenie także występuje).

Statyczny budowniczy nie jest oficjalnym wzorcem projektowym, jest podejściem do tworzenia budowniczego korzystając przy tym z płynnego interfejsu. Jest to bardziej metoda, niżeli przepis na daną architekturę. Przez to, bardzo trudno formalnie opisać jak powinien wyglądać, a co ważniejsze jego implementacja silnie zależy od języka.

W językach silnie typowanych (Java, C#) można statyczny budowniczy utworzyć w następujący sposób:

  1. Dodać do interesującego nas modelu zagnieżdżoną klasę statyczną budowniczego
  2. Dodać do budowniczego odpowiednie metody budujące przestrzegając przy tym płynnego interfejsu
  3. Dodać metodę build(), która zwróci instancję klasy

Oto przykładowy kod:

Jak widzisz kod jest naprawdę przejrzysty. Warto zwrócić uwagę na to, że budowniczy posiada logikę walidującą poszczególne etapy inicjalizacji modelu wewnątrz obiektu budującego. Model jest więc w dalszym ciągu jest pozbawiony zbędnej logiki.

W różnych językach podejście do statycznego budowniczego może być inne. Ostatnio tworząc budowniczego na platformie Angular2 stworzyłem dwie osobne klasy, gdzie jedna była wstrzykiwalnym (@constructable) serwisem zarządzanym przez mechanizm DI Angulara, a druga była budowniczym. Musiałem tak zrobić, ponieważ mój budowniczy w znacznej mierze miał po prostu uprościć konstruktor i zabezpieczyć logikę inicjalizacji obiektu, a tworzenie klas zagnieżdżonych w TypeScript jest zabronione.

Oczywiście, mogłem użyć tzw. funkcji fabrykującej zwracającej nowy kontekst, jednak wybrałem inną metodę: z osobnymi klasami. Zdecydowałem się na to, ponieważ samo nazwanie klasy przedrostkiem Builder w pewien sposób dokumentuje kod dla przyszłego programisty. Nikt nie będzie się musiał zastanawiać dlaczego użyłem funkcji fabrykujących, a nazwa Builder sama przedstawia intencje twórcy (czyli moje).

Podsumowanie

Wzorzec budowniczy jest fajny, statyczny budowniczy też. Dobrze jest rozumieć podstawowe różnice między nimi. Poniekąd są to wzorce podobne, jednak pewne konsekwencje ich stosowania są zupełnie inne. Budowniczy nie ma poważnych wad, a jedną z największych może być jego nadużywanie tam, gdzie nie jest potrzebny – jednak to dotyczy każdego innego wzorca.

Użytkownik Krzysiek napisał:

13 grudnia 2018


Brawa za ciekawe i jasne tłumaczenie nie tak oczywistych zagadnień, poparte fajnymi przykładami kodu !

Autor zdecydowanie powinien pomyśleć o napisaniu i wydaniu książki (papierowej lub e-booka), bo ma talent do przekazywania wiedzy.

Zachęcam Cię do zostawienia komentarza!

Ilość znaków: 0