W tym artykule — kolejnym z serii o wzorcach projektowych — zajmę się wzorcem należącym do rodziny wzorców kreacyjnych o nazwie Builder lub, jak kto woli, Budowniczy.
Czym jest wzorzec Budowniczy?
Rzućmy okiem na skróconą definicję.
„Rusz głową! Wzorce Projektowe”
Builder wyodrębnia kod konstrukcyjny obiektu z jego klasy i umieszcza w oddzielnych obiektach określanych mianem budowniczych. Konstrukcja obiektu jest dzielona na określone kroki. Obiekt zostaje utworzony, gdy wywołuje się ciąg takich kroków przy pośrednictwie budowniczego.
Nie trzeba wywoływać wszystkich etapów — można zredukować ilość wywołań do tylko tych etapów, które są najbardziej potrzebne do określenia nam wymaganych cech obiektu.
Builder — praktyczny przykład w języku Java
W tym przykładzie nasz budowniczy posłuży nam do tworzenia wojowników w grze (załóżmy, że z gatunku RPG) w świecie fantasy. Mamy do wyboru trzy rasy: człowiek, elf i krasnal oraz trzy klasy: żołnierz, mag i paladyn. Każdemu wojownikowi oprócz wymienionych cech, dobieramy oczywiście odpowiednią broń i pancerz.
Najpierw trzeba utworzyć interfejs Budowniczego i umieścić w nim poszczególne etapy tworzenia produktu, które będą wspólną częścią wszystkich typów budowniczych.
public interface Builder {
void setName(String name);
void setBreed(Breed breed);
void setWarriorClass(WarriorClass warriorClass);
void setWeapon(Weapon weapon);
void setArmor(Armor armor);
}
Zbiór ras, klas, broni i pancerzy zadeklarowałem jako typy wyliczeniowe enum.
public enum Breed {
HUMAN,
ELF,
DWARF
}
public enum WarriorClass {
SOLDIER,
SORCERER,
PALADIN
}
public enum Weapon {
SWORD,
AXE,
BOW,
MAGIC_STICK
}
public enum Armor {
LIGHT,
MEDIUM,
HEAVY
}
Następnie należy utworzyć implementacje określane mianem Budowniczych rzeczywistych (konkretnych). Za ich pomocą można tworzyć produkty, które niekoniecznie muszą mieć wspólny interfejs.
Zacznijmy od budowniczego ludzkich wojowników.
public class HumanWarriorBuilder implements Builder {
private String name;
private WarriorClass warriorClass;
private Weapon weapon;
private Armor armor;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setWarriorClass(WarriorClass warriorClass) {
this.warriorClass = warriorClass;
}
@Override
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
@Override
public void setArmor(Armor armor) {
this.armor = armor;
}
public HumanWarrior getResult() {
return new HumanWarrior(name, warriorClass, weapon, armor);
}
}
Metoda getResult() zwraca nam gotowy produkt.
Budowniczy elfich i krasnoludzkich wojowników wyglądają analogicznie.
public class ElfWarriorBuilder implements Builder {
private String name;
private WarriorClass warriorClass;
private Weapon weapon;
private Armor armor;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setWarriorClass(WarriorClass warriorClass) {
this.warriorClass = warriorClass;
}
@Override
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
@Override
public void setArmor(Armor armor) {
this.armor = armor;
}
public ElfWarrior getResult() {
return new ElfWarrior(name, warriorClass, weapon, armor);
}
}
public class DwarfWarriorBuilder implements Builder {
private String name;
private WarriorClass warriorClass;
private Weapon weapon;
private Armor armor;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setWarriorClass(WarriorClass warriorClass) {
this.warriorClass = warriorClass;
}
@Override
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
@Override
public void setArmor(Armor armor) {
this.armor = armor;
}
public DwarfWarrior getResult() {
return new DwarfWarrior(name, warriorClass, weapon, armor);
}
}
Gdy mamy już rzeczywistych budowniczych, możemy zadeklarować klasy produktów.
public class HumanWarrior {
private String name;
private final Breed breed = Breed.HUMAN;
private WarriorClass warriorClass;
private Weapon weapon;
private Armor armor;
public HumanWarrior(String name, WarriorClass warriorClass, Weapon weapon, Armor armor) {
if (name == null) {
this.name = "Unknown";
} else {
this.name = name;
}
this.warriorClass = warriorClass;
this.weapon = weapon;
this.armor = armor;
}
public String getName() {
return name;
}
public Breed getBreed() {
return breed;
}
public WarriorClass getWarriorClass() {
return warriorClass;
}
public Weapon getWeapon() {
return weapon;
}
public Armor getArmor() {
return armor;
}
}
Jak wspomniałem wyżej, część etapów tworzenia produktu może zostać pominięta. W tym przypadku, jeśli etap nadawania imienia wojownikowi zostanie zaniechany, otrzyma on domyślne imię „Nieznane”, czyli „Unknown”. Omijając pozostałe właściwości, otrzymamy wartość null.
Wojownicy pozostałych ras, wyglądają analogicznie.
public class ElfWarrior {
private String name;
private final Breed breed = Breed.ELF;
private WarriorClass warriorClass;
private Weapon weapon;
private Armor armor;
public ElfWarrior(String name, WarriorClass warriorClass, Weapon weapon, Armor armor) {
if (name == null) {
this.name = "Unknown";
} else {
this.name = name;
}
this.warriorClass = warriorClass;
this.weapon = weapon;
this.armor = armor;
}
public String getName() {
return name;
}
public Breed getBreed() {
return breed;
}
public WarriorClass getWarriorClass() {
return warriorClass;
}
public Weapon getWeapon() {
return weapon;
}
public Armor getArmor() {
return armor;
}
}
public class DwarfWarrior {
private String name;
private final Breed breed = Breed.DWARF;
private WarriorClass warriorClass;
private Weapon weapon;
private Armor armor;
public DwarfWarrior(String name, WarriorClass warriorClass, Weapon weapon, Armor armor) {
if (name == null) {
this.name = "Unknown";
} else {
this.name = name;
}
this.warriorClass = warriorClass;
this.weapon = weapon;
this.armor = armor;
}
public String getName() {
return name;
}
public Breed getBreed() {
return breed;
}
public WarriorClass getWarriorClass() {
return warriorClass;
}
public Weapon getWeapon() {
return weapon;
}
public Armor getArmor() {
return armor;
}
}
Wdrażając wzorzec Builder, możemy utworzyć klasę Kierownik określaną również jako Director. Odpowiada on za wykonywanie kroków budowy w należytym porządku. Klasa ta jest opcjonalna, gdyż można tego dokonać równie dobrze w klasie klienckiej, jednak w tym przykładzie pozwolę sobie posłużyć się nią. Director może się przydać, gdy chcemy utworzyć produkty mając określony z góry porządek bądź konfigurację.
public class Director {
public void buildHumanPaladin(Builder builder) {
builder.setName("Eustachy");
builder.setWarriorClass(WarriorClass.PALADIN);
builder.setWeapon(Weapon.SWORD);
builder.setArmor(Armor.MEDIUM);
}
public void buildElfSorcerer(Builder builder) {
builder.setName("Avallac'h");
builder.setWarriorClass(WarriorClass.SORCERER);
builder.setWeapon(Weapon.MAGIC_STICK);
builder.setArmor(Armor.LIGHT);
}
public void buildDwarfSoldier(Builder builder) {
builder.setWarriorClass(WarriorClass.SOLDIER);
builder.setWeapon(Weapon.AXE);
builder.setArmor(Armor.HEAVY);
}
}
Test przykładowego programu
public class WarriorsBuilderTestDrive {
public static void main(String[] args) {
Director director = new Director();
HumanWarriorBuilder humanWarriorBuilder = new HumanWarriorBuilder();
director.buildHumanPaladin(humanWarriorBuilder);
HumanWarrior humanWarrior = humanWarriorBuilder.getResult();
System.out.println("--- Utworzono wojownika ---");
System.out.println("Imię: " + humanWarrior.getName());
System.out.println("Rasa: " + humanWarrior.getBreed());
System.out.println("Klasa: " + humanWarrior.getWarriorClass());
System.out.println("Broń: " + humanWarrior.getWeapon());
System.out.println("Pancerz: " + humanWarrior.getArmor());
ElfWarriorBuilder elfWarriorBuilder = new ElfWarriorBuilder();
director.buildElfSorcerer(elfWarriorBuilder);
ElfWarrior elfWarrior = elfWarriorBuilder.getResult();
System.out.println("\n--- Utworzono wojownika ---");
System.out.println("Imię: " + elfWarrior.getName());
System.out.println("Rasa: " + elfWarrior.getBreed());
System.out.println("Klasa: " + elfWarrior.getWarriorClass());
System.out.println("Broń: " + elfWarrior.getWeapon());
System.out.println("Pancerz: " + elfWarrior.getArmor());
DwarfWarriorBuilder dwarfWarriorBuilder = new DwarfWarriorBuilder();
director.buildDwarfSoldier(dwarfWarriorBuilder);
DwarfWarrior dwarfWarrior = dwarfWarriorBuilder.getResult();
System.out.println("\n--- Utworzono wojownika ---");
System.out.println("Imię: " + dwarfWarrior.getName());
System.out.println("Rasa: " + dwarfWarrior.getBreed());
System.out.println("Klasa: " + dwarfWarrior.getWarriorClass());
System.out.println("Broń: " + dwarfWarrior.getWeapon());
System.out.println("Pancerz: " + dwarfWarrior.getArmor());
}
}
A oto, co „wypluła” konsola:
--- Utworzono wojownika ---
Imię: Eustachy
Rasa: HUMAN
Klasa: PALADIN
Broń: SWORD
Pancerz: MEDIUM
--- Utworzono wojownika ---
Imię: Avallac'h
Rasa: ELF
Klasa: SORCERER
Broń: MAGIC_STICK
Pancerz: LIGHT
--- Utworzono wojownika ---
Imię: Unknown
Rasa: DWARF
Klasa: SOLDIER
Broń: AXE
Pancerz: HEAVY
Process finished with exit code 0
Builder a Fabryka (Factory)
Wzorce z rodziny Fabryka, podobnie jak opisywany w tym artykule Builder, również należą do wzorców kreacyjnych i w podobny sposób hermetyzują operacje niezbędne do utworzenia złożonego obiektu, jednak podstawowa różnica między Fabryką a Budowniczym polega na tym, że ten pierwszy tworzy obiekty w procedurze jednoetapowej jednocześnie narzucając tę procedurę, czego nie można powiedzieć o Builderze.
Więcej o Fabryce (a dokładniej o Fabryce Abstrakcyjnej) przeczytacie tutaj.
Zastosowania wzorca Builder
Wzorzec Builder jest przydatny, gdy chcemy uniknąć tzw. „teleskopowych konstruktorów”. Przypuśćmy, że konstruktor danej klasy przyjmuje wiele opcjonalnych parametrów. Wówczas należałoby wielokrotnie przeciążać konstruktor tworząc jego krótsze wersje z mniejszą ilością parametrów.
Budowniczy przydaje się również, jeśli potrzebujemy tworzyć różne reprezentacje danego produktu.
Wzorzec ten sprawdza się także przy budowaniu drzew kompozytowych bądź innych obiektów z mniej lub bardziej zaawansowaną złożonością.
Wady i zalety wzorca Builder
Niestety nie ma idealnego wzorca, więc ten także nie jest pozbawiony wad.
Wady i zalety podsumowuje poniższa tabela.
Zalety | Wady |
---|---|
Obiekty można budować etapami, odkładając niektóre z nich lub wykonując rekurencyjnie | Tworząc obiekty, klient musi posiadać większą wiedzę z dziedziny problemu niż w przypadku wzorca Fabryka |
Hermetyzacja operacji potrzebnych do utworzenia złożonego obiektu | Wraz z rozwojem kodu zwiększa się jego stopień skomplikowania, ponieważ wdrażając ten wzorzec musimy liczyć się z uzupełnianiem projektu o wiele nowych klas |
Ukrywanie wewnętrznej reprezentacji produktu przed kodem klienckim | |
Możliwość wymiany implementacji produktów — klient wykorzystuje jedynie abstrakcyjny interfejs |
Kod źródłowy do pobrania
Kliknij na poniższy przycisk, jeśli chcesz pobrać kod źródłowy programu przedstawionego w tym artykule.
- https://refactoring.guru/pl/design-patterns/builder
- Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra, Rusz głową! Wzorce Projektowe, wyd. HELION 2017 — rozdział 14, strona 632.
0 komentarzy