Wzorce Projektowe: Budowniczy (Builder)

Definicja

Budowniczy to wzorzec konstrukcyjny, który oddziela proces tworzenia obiektu od jego reprezentacji – sam sposób tworzenia obiektów jest wydzielony do osobnych obiektów zwanych Konkretnymi Budowniczymi

Kiedy używamy wzorca Budowniczy?

Wzorzec budowniczy stosujemy najczęściej w przypadku potrzeby utworzenia złożonego obiektu (tzw. kompozytu). Złożony obiekt może wymuszać stworzenie klasy, posiadającej zbyt wiele odpowiedzialności, co łamie pierwszą zasadę SOLID.

Użycie wzorca rozważamy w przypadku gdy:

  •  tworzony obiekt jest złożony i nie jesteśmy w stanie go uprościć,
  • konieczna jest wieloetapowa inicjalizacja obiektu (nie można utworzyć obiektu w tylko jednej operacji),
  • obiekt może być tworzony wiele razy i na wiele sposobów.

Różne odmiany Budowniczego

Istnieją dwie odmiany wzorca Budowniczy – ta opisana przez GoF (Gang Of Four) oraz druga, autorstwa Joshue Blocha (statyczna). W programowaniu obiektowym częściej korzysta się z tej drugiej – statycznej.
 

Diagram klas UML wzorca Budowniczy

Przykład użycia (statyczny Budowniczy)

Praktycznie w każdej aplikacji, w module zarządzania użytkownikami podstawową encją jest klasa User. Zakładamy, że nasza klasa, reprezentująca użytkownika będzie mieć pięć następujących atrybutów: firstName, lastName, age, phoneNumber, address.

Zwykle jeśli chcemy utworzyć niezmienną klasę, musimy przekazać wszystkie atrybuty jako parametry do konstruktora. Wygląda to następująco: 

public User (String firstName, String lastName, int age, String phoneNumber, String address) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.phoneNumber = phoneNumber;
    this.address = address;
} 

Co w przypadku gdy przykładowo tylko dwa pierwsze atrybuty są obowiązkowe, a pozostałe trzy opcjonalne? Potrzebować będziemy kilku konstruktorów:

public User (String firstName, String lastName, int age, String phoneNumber) { ... }
public User (String firstName, String lastName, String phoneNumber, String address) { ...  }
public User (String firstName, String lastName, int age) { ...   }
public User (String firstName, String lastName) { ...    } 

Jeśli dodamy nowe atrybuty do klasy User (np. salary) konieczne będzie tworzenie jeszcze większej liczby konstruktorów lub zastosowanie seterów – w każdej z opcji coś tracimy. Właśnie w tym momencie z pomocą przychodzi wzorzec Budowniczy. Wzorzec używa dodatkowej statycznej klasy Budowniczego, która pomaga utworzyć poprawny obiekt User (z uwzględnieniem różnych wariantów atrybutów) bez utraty jego niezmienności

Spójrzmy na kod klasy User z wykorzystaniem statycznego Budowniczego:

public class User 
{
    private final String firstName; // wymagany
    private final String lastName; // wymagany
    private final int age; // opcjonalny
    private final String phoneNumber; // opcjonalny
    private final String address; // opcjonalny
 
    private User(UserBuilder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phoneNumber = builder.phoneNumber;
        this.address = builder.address;
    }
 
    // Brak seterów zapewnie niezmienność obiektu
    public String getFirstName() {
        return firstName;
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public int getAge() {
        return age;
    }
    
    public String getPhoneNumber() {
        return phoneNumber;
    }
    
    public String getAddress() {
        return address;
    }
 
    @Override
    public String toString() {
        return "User: "  + this.firstName + ", " + this.lastName + ", " + this.age + ", " + this.phoneNumber + ", " + this.address;
    }
 
    public static class UserBuilder 
    {
        private final String firstName;
        private final String lastName;
        private int age;
        private String phoneNumber;
        private String address;
 
        public UserBuilder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
        
        public UserBuilder age(int age) {
            this.age = age;
            return this;
        }
        
        public UserBuilder phoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
            return this;
        }
        
        public UserBuilder address(String address) {
            this.address = address;
            return this;
        }
        
        User object public User build() {
            User user =  new User(this);
            return user;
        }
    }
} 

Przetestujmy kod:

public static void main(String[] args) {
    User user1 = new User.UserBuilder("Lucy", "Smith")
    .age(20)
    .phone("111 222 333")
    .address("3907  Hood Avenue")
    .build();
 
    System.out.println(user1);
 
    User user2 = new User.UserBuilder("John", "Snow")
    .age(35)
    .phone("123 456 789")
    .build();
 
    System.out.println(user2);
}
 
Wynik:
"User: Lucy, Smith, 20, 111 222 333, 3907  Hood Avenue"
"User: John, Snow, 35, 123 456 789, null" 

Dzięki zastosowaniu statycznego Budowniczego utworzony obiekt nie ma żadnej metody ustawiającej, dlatego jego stan nie może się zmienić – zapewnia to zakładaną niezmienność stanu obiektu.

Korzyści stosowanie wzorca Budowniczy

Jak widać na powyższym przykładzie liczba wierszy kodu wzrasta co najmniej dwukrotnie podczas użycia wzorca Budowniczy – wzrasta jednak również elastyczność projektowania oraz czytelność kodu.

Wzorzec Budowniczy pozwala zredukować liczbę parametrów w konstruktorze, a co za tym idzie, podczas tworzenia obiektu nie ma potrzeby przekazywania wartości null dla parametrów opcjonalnych.

Kolejną z korzyści jest fakt, że obiekt zawsze tworzony jest w stanie kompletnym – nie musimy czekać aż zostanie wywołana odpowiednia metoda ustawiająca dany atrybut.

Różnice pomiędzy Budowniczym a Fabryką

Zarówno wzorzec Budowniczy jak i Fabryka są konstrukcyjnymi wzorcami projektowymi. Fabryka służy do tworzenia całej rodziny obiektów, Budowniczy tworzy zaś pojedynczy obiekt. Ponadto Budowniczy służy do tworzenia bardziej skomplikowanych, złożonych obiektów – w Fabryce logikę odpowiadającą za tworzenia obiektu da się zhermetyzować w jednej metodzie. Dodatkowo Fabryka tworzy instancję obiektu natychmiastowo, używając wzorca Budowniczy obiekt pobierany jest na żądanie.
 

Podsumowanie

Budowniczy to dosyć prosty i bardzo przydatny wzorzec, który hermetyzuje operacje niezbędne do stworzenia złożonego obiektu. Budowniczy pozwala na tworzenie obiektów w wieloetapowym procesie i w przeciwieństwie do wzorca Fabryki, nie narzuca jednej procedury ich tworzenia.

Leave a Comment

Your email address will not be published. Required fields are marked *