Kontynuując serię wpisów o wzorcach projektowych postanowiłem wziąć na warsztat wzorzec projektowy budowniczy. Wzorzec ten jest wzorcem kreacyjnym, który potrafi znacząco uprościć kod aplikacji, uczynić go bardziej przejrzystym, a jednocześnie sprawić, że dostajemy bardzo proste narzędzie do rozbudowy naszej aplikacji. Zapraszam do lektury.
Spis Treści
Kiedy i Dlaczego właśnie budowniczy?
Wzorzec budowniczy można użyć wówczas, gdy musimy zaimplementować w naszej aplikacji tworzenie obiektów, które strukturalnie są do siebie bardzo podobne, lecz różnią się one implementacjami. Dodatkowo wzorzec ten umożliwia tworzenie obiektów w sposób etapowy. Najlepiej będzie, gdy spróbujemy przedstawić problem na prostym przykładzie jakim będzie tworzenie obiektu śruby.
Śruba wydaje się bardzo prostym przedmiotem lecz mnogość jej typów sprawia że nie łatwo jest stworzyć jedną klasę, która w prosty sposób dałaby radę opisać wiele jej rodzajów. Weźmy przykładowe cechy jakie może mieć jakaś śruba:
- rozmiar gwintu,
- typ gwintu,
- długość gwintu,
- długość całkowita,
- kształt łba,
- pokrycie,
- jego rodzaj,
- klasa wytrzymałości,
- lewo lub prawoskrętność,
- skok gwintu,
- materiał z którego jest wykonana,
- itd.
Gdybyśmy chcieli utworzyć przykładowy konstruktor naszej śruby, powstałby nam niezły potworek, który mógłby wyglądać np. tak.
Sruba srubaPierwsza = new Sruba(6, "metryczna", 20, "walcowy z gniazdem sześciokątnym", "ocynk", "8.8", "prawoskretna", "normalny");
Jeden taki konstruktor można by przeboleć i przemęczyć się z wypełnianiem go, ale co zrobić gdy chcielibyśmy użyć wielu śrub, które różniłyby się tylko długością, np. M6x15, M6x20, M6x30. Najfajniej byłoby gdyby ktoś lub coś, wypełniłoby taki konstruktor za nas.
Zbudujmy budowniczego
Stwórzmy zatem klasę budowniczego naszych obiektów klasy śruba.
Klasa Śruba
package com.company;
public class Sruba {
private double rozmiarGwintu;
private String typ;
private double dlugosc;
private String typLba;
private String pokrycie;
private String klasa;
private String skretnosc;
private String skok;
//setters and getters
}
Klasa budowniczego
package com.company;
public class SrubaTyp1 {
Sruba sruba;
public SrubaTyp1(double rozmiar, double dlugosc){
sruba = new Sruba();
sruba.setRozmiarGwintu(rozmiar);
sruba.setTyp("metryczna");
sruba.setDlugosc(dlugosc);
sruba.setTypLba("walcowy z gniazdem sześciokątnym");
sruba.setPokrycie("ocynk");
sruba.setKlasa("8.8");
sruba.setSkretnosc("prawoskrestna");
sruba.setSkok("normalny");
}
public Sruba getSruba() {
return sruba;
}
}
Zauważ, że teraz aby stworzyć wiele śrub typu 1 wystarczy tylko stworzyć obiekt budowniczego śruby typu 1 oraz obiekt klasy Sruba do którego przypiszemy rezultat pracy budowniczego jak na poniższym przykładzie:
package com.company;
public class Main {
public static void main(String[] args) {
SrubaTyp1 budowniczy1 = new SrubaTyp1(6,20);
Sruba sruba1 = budowniczy1.getSruba();
budowniczy1.setDlugoscSrubby(25); //zmieniam tylko długość
Sruba sruba2 = budowniczy1.getSruba(); //tworzę kolejną śrubę o nowej dlugosci
}
}
Zobacz jak eleganckie rozwiązanie otrzymaliśmy. Zamiast tworzyć wiele długich, mało czytelnych konstruktorów stworzyliśmy klasę, która tworzy obiekty klasy Sruba za nas. Analogicznie do klasy SrubaTyp1 można tworzyć kolejne klasy SrubaTyp2, SrubaTyp3 itd. Aby zapewnić spójność i prawidłowe tworzenie obiektów klasy Sruba stwórzmy interfejs, który będzie musiał być zaimplementowany w każdej klasie budowniczego klasy Sruba.
package com.company;
public interface SrubaTypy {
public void setRozmiarSruby(double rozmiarSruby);
public void setTyp(String typ);
public void setDlugosc(double dlugosc);
public void setTypLba(String typLba);
public void setPokrycie(String pokrycie);
public void setKlasa(String klasa);
public void setSkretnosc(String skretnosc);
public void setSkok(String skok);
}
Mając taki interfejs zmieniam klasę SrubaTyp1, tak aby implementowała zapisany powyżej interfejs:
package com.company;
public class SrubaTyp1 implements SrubaTypy {
Sruba sruba;
public SrubaTyp1(double rozmiar, double dlugosc) {
sruba = new Sruba();
setRozmiarSruby(rozmiar);
setTyp("metryczna");
setDlugosc(dlugosc);
setTypLba("walcowy z gniazdem sześciokątnym");
setPokrycie("ocynk");
setKlasa("8.8");
setSkretnosc("prawoskrestna");
setSkok("normalny");
}
public Sruba getSruba() {
return sruba;
}
@Override
public void setRozmiarSruby(double rozmiar) {
sruba.setRozmiarGwintu(rozmiar);
}
@Override
public void setTyp(String typ) {
sruba.setTyp(typ);
}
@Override
public void setDlugosc(double dlugosc) {
sruba.setDlugosc(dlugosc);
}
@Override
public void setTypLba(String typLba) {
sruba.setTypLba(typLba);
}
@Override
public void setPokrycie(String pokrycie) {
sruba.setPokrycie("ocynk");
}
@Override
public void setKlasa(String klasa) {
sruba.setKlasa(klasa);
}
@Override
public void setSkretnosc(String skretnosc) {
sruba.setSkretnosc(skretnosc);
}
@Override
public void setSkok(String skok) {
sruba.setSkok(skok);
}
}
Zauważ, że teraz mamy sytuację, w której już na etapie tworzenia obiektu budowniczego konstruktor tworzy nam obiekt Śruby i automatycznie wypełnia wszystkie pola. Oprócz tego mamy możliwość dowolnej zmiany wartości pól Sruby poprzez settery w klasie budowniczego. W obecnej sytuacji to budowniczy sam nadzoruje proces kreowania śruby.
Postawmy nad budowniczym kierownika
We wzorcu budowniczy często stosuje się dodatkową klasę kierownika, która czuwa nad etapami i kolejnością wykorzystania metod z klasy budowniczego. Takie podejście sprawia że kod staje się bardziej przejrzysty i łatwiejszy w operowaniu budowniczymi. Zapiszmy poniżej przykładowy kod kierownika:
package com.company;
public class Kierownik {
public void stworzSrubeTypuPierwszego(SrubaTypy budowniczySruby) {
budowniczySruby.setDlugosc(20);
budowniczySruby.setRozmiarSruby(6);
budowniczySruby.setTypLba("Stożkowy z gniazdem sześciokątnym");
}
public void stworzSrubeTypuDrugiego(SrubaTypy budowniczySruby) {
budowniczySruby.setDlugosc(30);
budowniczySruby.setKlasa("8.8");
budowniczySruby.setRozmiarSruby(8);
}
}
Mając takiego kierownika, który instruuje jak ma towrzyć obiekt klasy śruba można odchudzić konstruktor budowniczego i wówczas klasa będzie wyglądać przykładowo tak:
package com.company;
public class SrubaTyp1 implements SrubaTypy {
Sruba sruba;
public SrubaTyp1(double rozmiar, double dlugosc) {
sruba = new Sruba();
}
public Sruba getSruba() {
return sruba;
}
@Override
public void setRozmiarSruby(double rozmiar) {
sruba.setRozmiarGwintu(rozmiar);
}
@Override
public void setTyp(String typ) {
sruba.setTyp(typ);
}
@Override
public void setDlugosc(double dlugosc) {
sruba.setDlugosc(dlugosc);
}
@Override
public void setTypLba(String typLba) {
sruba.setTypLba(typLba);
}
@Override
public void setPokrycie(String pokrycie) {
sruba.setPokrycie("ocynk");
}
@Override
public void setKlasa(String klasa) {
sruba.setKlasa(klasa);
}
@Override
public void setSkretnosc(String skretnosc) {
sruba.setSkretnosc(skretnosc);
}
@Override
public void setSkok(String skok) {
sruba.setSkok(skok);
}
}
Zyskaliśmy lżejszy konstruktor klasy budowniczego, a także wyodrębniliśmy metodykę tworzenia śruby do osobnej klasy, co dało nam przejrzystość i łatwość w budowaniu kolejnych implementacji różnych typów śruby w jednym miejscu poprzez dodawanie nowych metod w klasie kierownika. Dodatkowo w tym momencie umożliwiliśmy uzytkownikowi zaimplementowanie własnej kolejności budowania obiektu, a dodatkowo możemy teraz w klasie kierownika wprowadzić np. instrukcje warunkowe, które będą nadzorowały kolejne etapy tworzenia obiektu śruby.
Aby skorzystać z tak utworzonej struktury klas przebudujmy zatem nasz kod kliencki:
package com.company;
public class Main {
public static void main(String[] args) {
Kierownik kierownik = new Kierownik(); // zatrudniam do pracy kierownika
SrubaTypy budowniczySrubyPierwszy = new SrubaTyp1(); // zatrudniam pierwszego budowniczego
SrubaTypy budowniczySrubyDrugi = new SrubaTyp1(); // zatrudniam drugiego budowniczego
kierownik.stworzSrubeTypuPierwszego(budowniczySrubyPierwszy); //przypisuję do stowrzenia sruby konkretnego budowniczego
kierownik.stworzSrubeTypuDrugiego(budowniczySrubyDrugi); //przypisuję do stowrzenia sruby konkretnego budowniczego
Sruba pierwszaSruba = budowniczySrubyPierwszy.getSruba(); // przypisuje do pierwszaSruba efekt pracy budowniczego pierwszego
Sruba drugaSruba = budowniczySrubyDrugi.getSruba(); // przypisuje do drugaSruba efekt pracy budowniczego drugiego
System.out.println("---SRUBA PIERWSZA---");
System.out.println("Rozmiar gwintu: " + pierwszaSruba.getRozmiarGwintu());
System.out.println("Typ: " + pierwszaSruba.getTyp());
System.out.println("Dlugosc: " + pierwszaSruba.getDlugosc());
System.out.println("Typ Lba: " + pierwszaSruba.getTypLba());
System.out.println("Pokrycie: " + pierwszaSruba.getPokrycie());
System.out.println("Klasa: " + pierwszaSruba.getKlasa());
System.out.println("Skretnosc: " + pierwszaSruba.getSkretnosc());
System.out.println("Skok: " + pierwszaSruba.getSkok());
System.out.println("---SRUBA DRUGA---");
System.out.println("Rozmiar gwintu: " + drugaSruba.getRozmiarGwintu());
System.out.println("Typ: " + drugaSruba.getTyp());
System.out.println("Dlugosc: " + drugaSruba.getDlugosc());
System.out.println("Typ Lba: " + drugaSruba.getTypLba());
System.out.println("Pokrycie: " + drugaSruba.getPokrycie());
System.out.println("Klasa: " + drugaSruba.getKlasa());
System.out.println("Skretnosc: " + drugaSruba.getSkretnosc());
System.out.println("Skok: " + drugaSruba.getSkok());
}
}
Efekt uruchomienia programu będzie następujący:
---SRUBA PIERWSZA---
Rozmiar gwintu: 6.0
Typ: null
Dlugosc: 20.0
Typ Lba: Stożkowy z gniazdem sześciokątnym
Pokrycie: null
Klasa: null
Skretnosc: null
Skok: null
---SRUBA DRUGA---
Rozmiar gwintu: 8.0
Typ: null
Dlugosc: 30.0
Typ Lba: null
Pokrycie: null
Klasa: 8.8
Skretnosc: null
Skok: null
Process finished with exit code 0
Podsumowanie
Wzorzec budowniczy jest średnio skomplikowanym wzorcem projektowym, ale mądrze użyty dodaje aplikacji elastyczności i umożliwia programiście etapowe tworzenie obiektów. Dodatkowo kod kliencki znacząco upraszcza się i jest bardziej przejrzysty, co wynika chociażby z braku potrzeby tworzenia skomplikowanych i rozległych konstruktorów.
Zasadniczą wadą tego wzorca jest to że wymaga stworzenia dodatkowych klas i interfejsów, co w niektórych projektach może niepotrzebnie skomplikować jego strukturę klasową.