Od pewnego czasu mam przyjemność pracować z bardzo fajnymi ludźmi nad projektem, który ma jednocześnie pomóc nam się rozwinąć i stworzyć coś sensownego z korzyścią dla innych. Ogromną zaletą pracy w zespole jest możliwość wymiany doświadczeń pomiędzy kolegami / koleżankami. Miałem ostatnio pewien problem z obiektem, który wymagał dużej ilości informacji potrzebnych do jego utworzenia, co powodowało, że konstruktor stawał się przesadnie długi. Sprawę utrudniał fakt, że Java jest dla mnie w dalszym ciągu językiem mało znanym i nie orientuję się do końca jakie podejście należy zastosować.

Mój pierwszy pomysł polegał na wykorzystaniu Hashtable, ale metoda ta wymagała dodatkowego sprawdzania kluczy z tablicy wewnątrz samego konstruktora i ewentualnego przypisania wartości domyślnej o ile klucz nie znajdowałby się w słowniku. Dodatkowo chciałem skorzystać z mieszanych typów, a użycie typu Object w miejscu wartości dla klucza wymagało dodatkowego kroku polegającego na sprawdzeniu, czy dana wartość jest odpowiedniego typu. Tak przy okazji, jeżeli chcecie się dowiedzieć jak działa Hashtable to zapraszam w to miejsce:

 

Jak działa Hash Table?

 

Z pomocą, zupełnie nieświadomie, przyszedł właśnie mój kolega z zespołu. Podczas naszej wspólnej cotygodniowej rozmowy na Google hangouts przedstawiał działanie jednej z bibliotek, którą postanowiliśmy wykorzystać w naszym projekcie. Moją uwagę zwrócił sposób w jaki Maciej zaimplementował konstruktor jednego z obiektów. Okazało się, że wykorzystał do tego wzorzec projektowy o nazwie Builder. Wiedziałem wcześniej o jego istnieniu, ale jakoś nigdy nie udało mi się go zastosować w swoich projektach. Aż do teraz 😎

 

Załóżmy, że mamy obiekt, który ma kilka pól wymaganych do ustawienia podczas jego tworzenia, a dodatkowo obiekt ten po utworzeniu musi być immutable, czyli nie będzie możliwości jego edycji. Weźmy przykład z mojej ostatniej aplikacji, w której konieczne było zaimplementowanie modelu zwierzaka:

 

 

Jak widzicie po komentarzach, niektóre z pól są wymagane, a niektóre tylko opcjonalne. Tutaj oczywiście powstaje mały problem, bo pola zostały zadeklarowane jako final, w związku z czym wartoś musi zostać do nich przypisana podczas tworzenia obiektu. Z drugiej jednak strony chcemy dać możliwość osobom, które będą korzystały z tego modelu na wybranie, które pola poza wymaganymi zostaną uzupełnione. Pierwsze naiwne rozwiązanie, z którego sam skorzystałem wyglądało tak:

 

 

Chyba jedyną zaletą tego rozwiązania jest fakt, że działa 😅. Jak pewnie się już domyślacie wraz ze wzrostem właściwości dla naszego modelu bardzo szybko zakopiemy się w kolejnych konstruktorach. Dodatkowo osoby z naszego zespołu, które będą korzystały z naszej pracy będą miały bardzo utrudnione życie. Skąd będą na przykład miały wiedzieć jakiego konstruktora użyć, albo jakie domyślne wartości zostaną ustawione dla pól, które nie zostaną wykorzystane podczas tworzenia obiektu? Trzeba przyznać, że takie rozwiązanie przysparza tylko dodatkowych problemów.

 

 

Więc co możemy zrobić? Możemy na przykład skorzystać z tak popularnych getterów / setterów oraz domyślnego, pustego konstruktora. A wyglądałoby to tak:

 

 

Pierwszą poważną wadą takiego rozwiązania jest fakt, że stan naszego obiektu może różnić się w zależności od momentu w jakim inna część aplikacji postanowi z niego skorzystać. Będzie się tak działo dlatego, że innym moduł naszej aplikacji będzie traktował nasz obiekt jako w pełni utworzony, a w rzeczywistości dla pełnej inicjalizacji będziemy musieli wywołać kolejno wszystkie metody set(). A co jeszcze gorsze obiekt ten nie będzie już immutable, czyli będzie można go dowolnie edytować, co najlepszym rozwiązaniem nie jest. Więc co możemy z tym zrobić?

 

 

W tym momencie na scenę wchodzi Builder Pattern. Tak będzie wyglądało jego zastosowanie w przypadku naszego obiektu:

 

 

Do odnotowania jest tutaj kilka istotnych punktów. Po pierwsze konstruktor dla naszego obiektu Dog został ustawiony na prywatny, a co za tym idzie nie ma możliwości uzyskania do niego bezpośredniego dostępu. Oznacza to, że utworzenie nowego obiektu Dog będzie wymagało skorzystania z dedykowanego specjalnie dla niego buildera (taki Pan Budowniczy 😎). Po drugie nasz obiekt będzie immutable, a skorzystać będzie można tylko z getterów, co jest oczywiste w takim przypadku. Po trzecie konstruktor obiektu DogBuilder wymaga tylko dwóch parametrów oznaczonych jako final (dogName i description). Tak będzie wyglądało użycie buildera w praktyce:

 

 

Oczywiście nic nie stoi na przeszkodzie, żeby ustawić tylko wybrane pola obiektu zamiast wszystkich:

 

 

Jak widać takie rozwiązanie nie tylko jest bardzo czytelne, ale przed wszystkim pozwala nam na spełnienie wszystkich początkowych założeń dla naszego obiektu, czyli opcjonalne ustawianie pól + brak możliwości edycji obiektu po jego utworzeniu. Minusem takiego rozwiązania jest konieczność duplikowania pól obiektu Dog w obiekcie DogBiulder, ale moim zdaniem jest to akceptowalny koszt 🤩.

Jeszcze jedną bardzo ważną zaletą jest fakt, że w końcowej fazie budowania obiektu jesteśmy w stanie dokonać walidacji wartości przypisywanych obiektowi. Tak to będzie wyglądało w przykładzie:

 

 

Jeżeli waga wprowadzona dla pas nie spełnia określonych wymogów to zostanie zgłoszony wyjątek. Istotne jest tutaj, aby wartość pola była sprawdzana już po skopiowaniu wszystkich wartości do obiektu. Jest to związane z faktem, że sam obiekt DogBulider nie jest thread-safe, czyli może on zostać zmanipulowany przez dowolny wątek w naszej aplikacji. Sprawdzanie pola weight na obiekcie DogBulider mogłoby zwracać różne wyniki w zależności od momentu, w którym wartość byłaby sprawdzana. Sam obiekt Dog nie jest narażony na takie ryzyko, dlatego można śmiało używać jego wartości do weryfikacji poprawności wprowadzanych danych. Tak by wyglądała niewłaściwa implementacja:

 

 

To by było na tyle. Mam nadzieję, że w tym wpisie udało mi się pokazać jak przydatny jest wzorzec Builder i jak można go odpowiednio wykorzystać.  Mi udało się go z powodzeniem zaimplementować w moim aktualnym projekcie i jestem bardzo zadowolony z rezultatów. W kolejnych wpisach będę starał się przybliżyć Wam inne przydatne wzorce, także wykorzystywane w Swift oraz Kotlin. Do usłyszenia 😎.


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *