Wzorzec projektowy Builder
Definicja wzorca Builder
Wzorzec Builder (Budowniczy) – używany jest do oddzielenia sposobu tworzenia obiektu od jego reprezentacji oraz w celu umożliwienia jego wieloetapowego tworzenia.
Przed zastosowaniem wzorca
Dla przykładu stwórzmy klasę Vehicle, która będzie opisywać jakiś pojazd. Klasa ta będzie posiadać dwa pola opisujące cechy pojazdu, takie jak marka oraz kolor.
public class Vehicle {
private final String brand;
private final String color;
public Vehicle(String brand, String color) {
this.brand = brand;
this.color = color;
}
}
Stwórzmy przykładowy obiekt tej klasy.
public class MainBuilder {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle("Volkswagen", "Blue");
}
}
Jak na razie wszystko wydaję się oczywiste i w miarę czytelne. Dodajmy zatem jeszcze cztery pola typu boolean do klasy Vehicle, aby dokładniej opisać tworzony pojazd. Przykładowo dodajmy jeszcze pole radio, airConditioning, electricMirrors oraz heatedSeats.
public class Vehicle {
private final String brand;
private final String color;
private final boolean radio;
private final boolean airConditioning;
private final boolean elekctricMirrors;
private final boolean heatedSeats;
public Vehicle(
String brand,
String color,
boolean radio,
boolean airConditioning,
boolean elekctricMirrors,
boolean heatedSeats
) {
this.brand = brand;
this.color = color;
this.radio = radio;
this.airConditioning = airConditioning;
this.elekctricMirrors = elekctricMirrors;
this.heatedSeats = heatedSeats;
}
}
I utwórzmy nowy obiekt…
public class MainBuilder {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle(
"Volkswagen",
"Blue",
true,
false,
false,
false
);
}
}
Jak widać struktura tworzenia obiektu zaczyna być mało czytelna, ze względu na dużą liczbę argumentów. Nie do końca jest jasne, za co odpowiadają poszczególne argumenty konstruktora. Nie mamy też dużej kontroli nad całym procesem tworzenia obiektu. A co jeśli pól w klasie było by jeszcze więcej? I tylko niektóre z nich były by wymagane podczas tworzenia obiektu. Zaczyna się robić spore zamieszanie, które może zostać rozwiązanie poprzez zastosowanie wzorca Builder.
Po zastosowaniu wzorca
Dzięki zastosowaniu wzorca Builder odpowiedzialność za tworzenie obiektu przenosimy do oddzielnej klasy, w której udostępniamy metody do konfigurowania poszczególnych pól wraz z ewentualną walidacją danych. Dzięki Builderowi mamy pewność, że zostanie utworzona poprawna instancja klasy oraz cały kod staję się bardziej czytelniejszy.
Stwórzmy zatem klasę o nazwie VehicleBuilder z takimi samymi polami opisującymi pojazd co poprzednia klasa Vehicle oraz z metodami umożliwiającymi ustawianie wartości tych pól.
public class VehicleBuilder {
private String brand;
private String color;
private boolean radio;
private boolean airConditioning;
private boolean electricMirrors;
private boolean heatedSeats;
public VehicleBuilder withBrand(String brand) {
this.brand = brand;
return this;
}
public VehicleBuilder withColor(String color) {
this.color = color;
return this;
}
public VehicleBuilder withRadio(boolean radio) {
this.radio = radio;
return this;
}
public VehicleBuilder withAirConditioning(boolean airConditioning) {
this.airConditioning = airConditioning;
return this;
}
public VehicleBuilder withElectricsMirrors(boolean electricMirrors) {
this.electricMirrors = electricMirrors;
return this;
}
public VehicleBuilder withHeatedSeats(boolean heatedSeats) {
this.heatedSeats = heatedSeats;
return this;
}
public Vehicle build() {
if (brand.isEmpty()) {
throw new IllegalStateException("Brand cannot be empty!");
}
if (color.isEmpty()) {
throw new IllegalStateException("Color cannot be empty!");
}
return new Vehicle(brand, color, radio, airConditioning, electricMirrors, heatedSeats);
}
}
Klasa VehicleBuilder to klasa odpowiedzialna za tworzenie obiektu. Dzięki temu, że każda metoda ustawiająca wartość odpowiedniego pola zwraca obiekt tej klasy to cały proces budowania obiektu można wywołać w jednej linii i w dowolnej kolejności. Po wywołaniu metody build() zostaję zwrócony “zbudowany” obiekt typu Vehicle.
Tworzenie obiektu wygląda teraz następująco…
public class MainBuilder {
public static void main(String[] args) {
VehicleBuilder vehicleBuilder = new VehicleBuilder();
Vehicle audi = vehicleBuilder
.withBrand("Audi")
.withColor("Black")
.withRadio(true)
.withAirConditioning(true)
.withElectricsMirrors(true)
.withHeatedSeats(true)
.build();
}
}
Jak widać całość jest czytelniejsza oraz mamy większą kontrolę nad procesem tworzenia obiektu, gdyż poszczególne metody możemy wywoływać w dowolnej kolejności i nie jesteśmy uzależnieni od kolejności argumentów konstruktora. Mamy też pewność, że obiekt został poprawnie utworzony dzięki przeprowadzonej walidacji w metodzie build(), która sprawdza czy wszystkie wymagane pola zostały ustawione.
Zalety wzorca Builder
- Hermetyzuję operacje niezbędne do stworzenia złożonego obiektu,
- Umożliwia tworzenie obiektów w procedurze wielokrokowej w dowolnej kolejności,
- Ukrywa wewnętrzną reprezentację produktu,
- Implementacje produktów mogą być wymieniane.
Wady wzorca Builder
- Większa ilość klas,
- Duplikacja kodu.
Kiedy stosować wzorzec
Wzorzec Builder należy stosować w sytuacji kiedy algorytm tworzenia obiektu złożonego powinien być niezależny od składników tego obiektu i sposobu ich łączenia. A także kiedy proces konstrukcji musi umożliwiać tworzenie rożnych reprezentacji generowanego obiektu.