1.0 Budowniczy – definicja
Budowniczy to kreacyjny wzorzec projektowy umożliwiający tworzenie złożonych obiektów krok po kroku. Dzięki temu wzorcowi będziemy mogli budować różne typy oraz reprezentacje obiektu używając tego samego kodu konstrukcyjnego.
Wzorzec projektowy Budowniczy proponuje ekstrakcję kodu konstrukcyjnego obiektu z jego klasy i umieszczenie go w osobnych obiektach zwanych budowniczymi. Zatem usuwamy konstruktory a kod tworzenia umieszczamy w oddzielnej klasie. Wzorzec Budowiczy zastosujemy zatem w przypadku kiedy dana klasa ma bardzo dużo pól i nie chcemy dla niej tworzyć dużej liczby konstruktorów, jednocześnie nie chcemy aby użytkownik miał dostęp np. do setterów.
2.0 Builder z klasą wewnętrzną
To implementacja wzorca Builder w której chcemy zapewnić swobodę osobie która będzie tworzyła obiekt, pozwalając jej na ustawiane wartości danych pól, ale po utworzeniu obiektu nie chcemy aby przy nim coś zmieniała.
2.1 Builder wersja klasyczna
To implementacja wzorca Builder, w której nie chcemy dawać możliwości ustawiania wartości poszczególnych pól po utworzeniu obiektu. występuje tutaj dodatkowy interfejs oraz klasa Dyrektor, która będzie wiedziała jak utworzyć dany obiekt, jest to jakby ukryte przed użytkownikiem.
3.0 Schemat UML wzorca z klasa wewnętrzną
W naszym przykładzie będziemy budować samochód i tak też zaprezentujemy schemat UML.
3.1 Schemat UML Builder wersja klasyczna
4.0 Implementacja wzorca Builder
Zobaczmy prostą implementację symulującą budowę samochodu z różnych komponentów bez wykorzystania wzorca Builder. Stwórzmy klasę Car, gdzie umieścimy nasze pole odpowiadające poszczególnym podzespołom samochodu. Dodamy do tego dwa konstruktory reprezentujące różne wyposażenie samochodu, dalej gettery i settery – finalnie otrzymamy dość duża klasę.
public class Car {
private String silnik;
private String podwozie;
private String nadwozie;
private String koła;
private String kierownica;
private String skrzynia_biegow_manual;
private String skrzynia_biegow_automat;
public Car(String silnik, String podwozie, String nadwozie, String koła, String kierownica, String skrzynia_biegow_automat) {
this.silnik = silnik;
this.podwozie = podwozie;
this.nadwozie = nadwozie;
this.koła = koła;
this.kierownica = kierownica;
this.skrzynia_biegow_automat = skrzynia_biegow_automat;
}
public Car(String silnik, String podwozie, String nadwozie, String koła, String kierownica, String skrzynia_biegow_manual) {
this.silnik = silnik;
this.podwozie = podwozie;
this.nadwozie = nadwozie;
this.koła = koła;
this.kierownica = kierownica;
this.skrzynia_biegow_manual = skrzynia_biegow_manual;
}
public String getSilnik() {
return silnik;
}
public void setSilnik(String silnik) {
this.silnik = silnik;
}
public String getPodwozie() {
return podwozie;
}
public void setPodwozie(String podwozie) {
this.podwozie = podwozie;
}
public String getNadwozie() {
return nadwozie;
}
public void setNadwozie(String nadwozie) {
this.nadwozie = nadwozie;
}
public String getKoła() {
return koła;
}
public void setKoła(String koła) {
this.koła = koła;
}
public String getKierownica() {
return kierownica;
}
public void setKierownica(String kierownica) {
this.kierownica = kierownica;
}
public String getSkrzynia_biegow_manual() {
return skrzynia_biegow_manual;
}
public void setSkrzynia_biegow_manual(String skrzynia_biegow_manual) {
this.skrzynia_biegow_manual = skrzynia_biegow_manual;
}
public String getGetSkrzynia_biegow_automat() {
return getSkrzynia_biegow_automat;
}
public void setGetSkrzynia_biegow_automat(String getSkrzynia_biegow_automat) {
this.getSkrzynia_biegow_automat = getSkrzynia_biegow_automat;
}
}
Jak widać powyżej, konstruktory mają sporo pól, robi się trochę mało przejrzyście, a co będzie gdy doją różne typy samochodów i sporo, sporo więcej podzespołów? Taki konstruktor będzie zwyczajnie niezjadliwy.
Dokończmy jednak dzieła i zaimplementujmy metodę main,w której stworzymy dwa obiekty (samochody), jeden ze skrzynia manualną a drugi to automat.
public class Main {
public static void main(String[] args) {
Car car1 = new Car("silnik", "podwozie", "nadwozie", "koła", "kierownica", "skrzynia_biegow_manual");
Car car2 = new Car("silnik", "podwozie", "nadwozie", "koła", "kierownica", "skrzynia_biegow_automat");
}
}
Jak widać tworzenie takich obiektów nie jest wcale przyjemne, są zbyt rozbudowane. Pora na naszego bohatera dzisiejszego odcinka czyli wzorzec projektowy Builder. Stwórzmy zatem tą samą symulację budowy samochodu z różnych komponentów z wykorzystaniem klasycznego podejście dla wzorca Builder. Zgodnie ze schematem UML zacznijmy od implementacji Interfejsu CarBuilder.
public interface CarBuilder {
void buildSilnik();
void buildPodwozie();
void buildNadwozie();
void buildKoła();
void buildKierownica();
void buildSkrzyniaManual();
void buildSkrzyniaAutomat();
Car getCar();
}
Następnie stwórzmy klasę Car. Zwróć uwagę, że tym razem wcale nie tworzymy konstruktorów własnych.
public class Car {
private String silnik;
private String podwozie;
private String nadwozie;
private String koła;
private String kierownica;
private String skrzynia_biegow_manual;
private String skrzynia_biegow_automat;
public String getSilnik() {
return silnik;
}
public void setSilnik(String silnik) {
this.silnik = silnik;
}
public String getPodwozie() {
return podwozie;
}
public void setPodwozie(String podwozie) {
this.podwozie = podwozie;
}
public String getNadwozie() {
return nadwozie;
}
public void setNadwozie(String nadwozie) {
this.nadwozie = nadwozie;
}
public String getKoła() {
return koła;
}
public void setKoła(String koła) {
this.koła = koła;
}
public String getKierownica() {
return kierownica;
}
public void setKierownica(String kierownica) {
this.kierownica = kierownica;
}
public String getSkrzynia_biegow_manual() {
return skrzynia_biegow_manual;
}
public void setSkrzynia_biegow_manual(String skrzynia_biegow_manual) {
this.skrzynia_biegow_manual = skrzynia_biegow_manual;
}
public String getGetSkrzynia_biegow_automat() {
return getSkrzynia_biegow_automat;
}
public void setGetSkrzynia_biegow_automat(String getSkrzynia_biegow_automat) {
this.skrzynia_biegow_automat = skrzynia_biegow_automat;
}
}
Podążając za schematem UML stwórzmy przykładową klasę reprezentującą dany typ samochodu np. Sedan, która implementować będzie interfejs CarBuilder. Analogicznie można utworzyć kolejne klasy reprezentujące różne typy samochodów np. Kombi czy Kabriolet.
public class SedanCarBuilder implements CarBuilder {
private Car car;
public SedanCarBuilder() {
this.car = new Car();
}
@Override
public void buildSilnik() {
this.car.setSilnik("silnik sedan");
}
@Override
public void buildPodwozie() {
this.car.setPodwozie("podwozie sedan");
}
@Override
public void buildNadwozie() {
this.car.setNadwozie("nadwozie sedan");
}
@Override
public void buildKola() {
this.car.setKoła("kola sedan");
}
@Override
public void buildKierownica() {
this.car.setKierownica("kierownica sedan");
}
@Override
public void buildSkrzyniaBiegowManual() {
this.car.setSkrzynia_biegow_manual("skrzynia manual sedan");
}
@Override
public Car getCar() {
return car;
}
}
Czas na naszego CarDirectora – klasę która zarządzać będzie budową samochodu, spójrzmy na implementację poniżej. Tworzymy prywatne pole z obiektem typu CarBuilder, któremu przypisujemy wartość przekazaną w konstruktorze.
public class CarDirector {
private CarBuilder carBuilder;
public CarDirector(CarBuilder carBuilder) {
this.carBuilder = carBuilder;
}
public void buildCar(){
carBuilder.buildSilnik();
carBuilder.buildPodwozie();
carBuilder.buildNadwozie();
carBuilder.buildKola();
carBuilder.buildKierownica();
carBuilder.buildSkrzyniaBiegowManual();
}
public Car getCar(){
return this.carBuilder.getCar();
}
}
Czas na metodę main, która poskłada nam wszystko w całość:
public class Main {
public static void main(String[] args) {
SedanCarBuilder sedanCarBuilder = new SedanCarBuilder();
KombiCarBuilder kombiCarBuilder = new KombiCarBuilder();
CarDirector sedanCarDirector = new CarDirector(sedanCarBuilder);
sedanCarDirector.buildCar();
CarDirector kombiCarDirector = new CarDirector(kombiCarBuilder);
kombiCarDirector.buildCar();
Car sedanCar = sedanCarDirector.getCar();
Car kombiCar = kombiCarDirector.getCar();
}
}
Na początku tworzymy obiekt reprezentujący dany typ samochodu np. sedanCarBuilder, następnie przechodzimy do utworzenia obiektu naszego Dyrektora, któremu przekazujemy jako argument w konstruktorze typ naszego samochodu. Nasz Dyrektor jest już gotowy aby wywołać metodę główna buildCar(). Dzięki temu że przekazaliśmy dyrektorowi gotowy obiekt reprezentujący typu samochodu a pamiętamy że jest to obiekt typu CarBuilder a więc wykorzystujemy tutaj polimorfizm, nasza metoda buildCar() wywoływana na obiekcie sedanCarDirector wykonuje dla nas całą pracę. Implementacja metod dla klasy Kombi może być inna niż dla klasy Sedan ale dalej dyrektor wywoła ta samą metodą buildCar aby uzyskać gotowy samochód i to wszystko prawda, że nie takie skomplikowane? POmyśolcie teraz ile części i podzespołów ma samochód i jak by wyglądał nasz kod bez wdrożenia wzorca builder – konstruktory były by tak rozbudowane, że ciężko było by nad tym zapanować.
5.0 Podsumowanie
Stosowanie wzorca Budowniczy ma sens tylko gdy produkty w programie są złożone i wymagają kompleksowej konfiguracji. Zatem wzorzec budowniczy zastosujemy wówczas gdy nie chcemy tworzyć dużych i skomplikowanych konstruktorów.
Są dwa główne rodzaje wzorca builder: z klasą wewnętrzna oraz builder klasyczny.
Zalety:
- Możesz konstruować obiekty etapami, odkładać niektóre etapy, lub wykonywać je rekursywnie.
- Możesz wykorzystać ponownie ten sam kod konstrukcyjny budując kolejne reprezentacje produktów.
- Zasada pojedynczej odpowiedzialności. Można odizolować skomplikowany kod konstrukcyjny od logiki biznesowej produktu.
Wady:
- Kod staje się bardziej skomplikowany.
Wskazówka: W javie jest biblioteka zewnętrzna lombok, która zawiera w sobie implementację wzorca builder i ułatwia pisanie kodu z jego wykorzystaniem.