Dzisiaj porozmawiamy trochę o jednym z bardziej popularnych wzorców projektowych, a mianowicie o Budowniczym (Builder). Jest to jeden z kreacyjnych wzorców projektowych.
A na co to komu?
Dzięki użyciu budowniczego możemy z pewnością osiągnąć przynajmniej dwie rzeczy:
- oddzielimy tworzenie obiektu od jego reprezentacji,
- nasz kod powinien stać się trochę bardziej przyjazny dla oka
The good old way…
Na początku zobaczmy jak będzie wyglądał zwykły obiekt POJO. Jako przykład stwórzmy klasę, która będzie zawierała wszystkie informacje o mailu, który będzie mógł być później wysłany.
public class Mail { private final String to; private final String cc; private final String subject; private final String content; public Mail(String to, String cc, String subject, String content) { this.to = to; this.cc = cc; this.subject = subject; this.content = content; } public String getTo() { return to; } public String getCc() { return cc; } public String getSubject() { return subject; } public String getContent() { return content; } }Jak widzimy, nasza klasa ma za zadanie przechowanie informacji o tym do kogo mail będzie wysłany (to), dodatkowych odbiorów (cc), temat maila (subject) oraz jego treść (content).
Stwórzmy teraz instancje naszej klasy:
final Mail mail = new Mail("to@lantkowiak.pl", "cc@lantkowiak.pl", "My subject", "Some content");Co prawda ten kod nie wygląda jeszcze zbyt strasznie, ale wyobraźmy sobie teraz, iż do naszej klasy dodajemy jeszcze kilka pól jak bcc, mime type, załączniki i jeszcze kilka innych. Wtedy nasza klasa trochę się rozrośnie i stworzenie instancji tej klasy nie będzie wyglądało zbyt przyjemnie, szczególnie mając na uwadze, iż dużo pól naszej klasy ma ten sam typ.
A co jeśli…
Możemy się okazać, iż część pól w naszej klasie nie jest obowiązkowa przy tworzeniu naszego obiektu. W naszym przykładzie pole cc nie musi być obowiązkowe. Co w takim przypadku powinniśmy zrobić?
Możemy stworzyć dodatkowy konstruktor bez pola cc Tylko co jeżeli pól opcjonalnych będziemy mieć kilka? Tworzenie wszystkich kombinacji konstruktorów zdecydowanie odpada.
W takim przypadku możemy przecież stworzyć POJO z setterami/getterami
public class Mail { private String to; private String cc; private String subject; private String content; public String getTo() { return to; } public String getCc() { return cc; } public String getSubject() { return subject; } public String getContent() { return content; } public void setTo(String to) { this.to = to; } public void setCc(String cc) { this.cc = cc; } public void setSubject(String subject) { this.subject = subject; } public void setContent(String content) { this.content = content; } }I stwórzmy teraz instancje naszej klasy, tylko tym razem bez inicjowania pola cc:
final Mail mail = new Mail(); mail.setTo("to@lantkowiak.pl"); mail.setSubject("My subject"); mail.setContent("Some content");Podejście to pozwala nam stworzyć instancje naszego obiektu tylko z wybranymi przez nas polami. Dzięki setterom jest również trudniej o pomyłkę związaną z kolejnością przekazywanych parametrów do konstruktora.
Czy można zrobić to ładniej?
Wydaję mi się, iż można I właśnie budowniczy przyjdzie nam z pomocą
Budowniczy pomaga nam w tworzeniu skomplikowanych obiektów poprzez danie możliwości tworzenia (budowania) ich kawałek po kawałku. Stwórzmy zatem budowniczego do naszej klasy
public class Mail { private String to; private String cc; private String subject; private String content; private Mail() { } public String getTo() { return to; } public String getCc() { return cc; } public String getSubject() { return subject; } public String getContent() { return content; } public static Builder builder() { return new Builder(); } public static class Builder { private Mail mail; public Builder() { this.mail = new Mail(); } public Builder to(final String to) { this.mail.to = to; return this; } public Builder cc(final String cc) { this.mail.cc = cc; return this; } public Builder subject(final String subject) { this.mail.subject = subject; return this; } public Builder content(final String content) { this.mail.content = content; return this; } public Mail build() { return this.mail; } }Zmiany, których dokonaliśmy w naszej klasie to:
- stworzyliśmy prywatny konstruktor do klasy Mail, aby nikt nie stworzył nam naszego obiektu na lewo ;),
- dodaliśmy statyczną metodę builder(), która zwraca nową instancje budowniczego,
- dodaliśmy samego budowniczego.
Klasa budowniczego zawiera w sobie pole z instancją budowanego obiektu, metody odpowiadające ustawianym polom oraz metodę build(), która wieńczy dzieło i zwraca nam naszą zbudowaną instancje klasy Mail. Zwróćmy uwagę, iż metody budowniczego zwracają instancje budowniczego. Dzięki czemu możemy zbudować nasz obiekt w sposób 'płynny’ (fluent interface)
final Mail mail = Mail.builder() .to("to@lantkowiak.pl") .subject("My subject") .content("Some content") .build();I w ten oto sposób udało nam się stworzyć prostego budowniczego
A co jeśli… (cz. 2)
Wcześniej zauważyliśmy, iż część pól z naszej klasy może być opcjonalna. Warto, żebyśmy też popatrzyli w drugą stronę – część pól z może być konieczna do poprawnego zainicjowania naszego obiektu. W naszej klasie Mail możemy uznać, iż pola to, subject i content są obowiązkowe. I co teraz?
The good old way jeszcze raz
Wróćmy na sekundę do podejścia z POJO. Problem ten możemy tutaj rozwiązać poprzez stworzenie konstruktora, który będzie przyjmować obowiązkowe parametry oraz stworzeniem setterów dla parametrów opcjonalnych.
public class Mail { private final String to; private String cc; private final String subject; private final String content; public Mail(String to, String subject, String content) { this.to = to; this.subject = subject; this.content = content; } public String getTo() { return to; } public String getCc() { return cc; } public String getSubject() { return subject; } public String getContent() { return content; } public void setCc(String cc) { this.cc = cc; } }Tylko w tym podejściu znów powraca problem z wieloma parametrami w konstruktorze, które nie wyglądają zbyt ładnie oraz dodatkowo nie jest trudno o pomyłkę.
I wróćmy do budowniczego
A jak możemy ten problem rozwiązać w budowniczym?
Możemy obowiązkowe parametry przekazać do konstruktora budowniczego, ale oczywiście rezygnujemy z tej opcji
Do rozwiązania tego problemu musimy trochę zmodyfikować naszego budowniczego, którego już stworzyliśmy.
Na początku stwórzmy sobie kilka prostych interfejsów.
public interface ToStep { public SubjectStep to(final String to); } public interface SubjectStep { public ContentStep subject(final String subject); } public interface ContentStep { public OptionalSteps content(final String content); } public interface OptionalSteps { public OptionalSteps cc(final String cc); public Mail build(); }Zauważmy, iż dla wszystkich obowiązkowego pola stworzyliśmy interfejs, który zawiera dokładnie jedną metodę odpowiadającą za ustawienie tego pola. Zwróćmy uwagę też, iż metoda ustawiające dane pole zwraca interfejs do ustawienia kolejnego obowiązkowego pola. Wyjątkiem jest interfejs ContentStep, który ustawia ostatnie obowiązkowe pole i zwraca interfejs OptionalSteps, który zawiera możliwość ustawienia wszystkich opcjonalnych pól oraz metodę build(), która zwraca nam gotowy obiekt.
Teraz musimy użyć tych interfejsów w naszym budowniczym.
private static class Builder implements ToStep, SubjectStep, ContentStep, OptionalSteps { private Mail mail; public Builder() { this.mail = new Mail(); } public SubjectStep to(final String to) { this.mail.to = to; return this; } public OptionalSteps cc(final String cc) { this.mail.cc = cc; return this; } public ContentStep subject(final String subject) { this.mail.subject = subject; return this; } public OptionalSteps content(final String content) { this.mail.content = content; return this; } public Mail build() { return this.mail; } }Sama implementacja znacząco się nie zmieniła. Teraz nasz builder implementuje stworzone przez nas interfejsy. Musieliśmy też zmienić zwracane typy w metodach budowniczego.
I ostatnia zmiana, którą musieliśmy zrobić to zmiana typu zwracanego w metodzie tworzącej naszego budowniczego, żeby zwracała interfejs do pierwszego obowiązkowego pola.
public static ToStep builder() { return new Builder(); }Samo budowanie obiektu nie zmieniło się w porównaniu do tego, jak było w naszym pierwszym budowniczym, ale dzięki tym kilku interfejsom klient używający naszego budowniczego będzie musiał podać wszystkie obowiązkowe pola, żeby utworzyć obiekt.
I to jest właśnie to, co chcieliśmy osiągnąć