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?

Budowniczy jest kreacyjnym wzorcem projektowym, który daje możliwość tworzenia złożonych obiektów etapami, krok po kroku. Wzorzec ten pozwala produkować różne typy oraz reprezentacje obiektu używając tego samego kodu konstrukcyjnego.Refactoring Guru

Rzućmy okiem na skróconą definicję.

Wzorca Builder używamy do hermetyzowania tworzenia produktu w celu jego wieloetapowego inicjowania.Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra,

„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.

Podobnie, jak w poprzednich artykułach z tej serii, zaprezentuję Wam przykład użycia tego wzorca w kodzie, również w języku Java .

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);
    }
} 
Mamy już budowniczego, budowniczych rzeczywistych, produkty i dyrektora. Czas przetestować nasz program .

Test przykładowego programu

Na początku będę potrzebował instancji klasy Director. Następnie, aby produkt został zbudowany, musi zostać utworzony obiekt-budowniczy, którego wykorzysta kierownik. Wreszcie, po zakończeniu budowy możemy przypisać do obiektu-produktu wywołanie metody getResult() obiektu-budowniczego, aby otrzymać gotowe dzieło. I voila .
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.

ZaletyWady
Obiekty można budować etapami, odkładając niektóre z nich lub wykonując rekurencyjnieTworząc obiekty, klient musi posiadać większą wiedzę z dziedziny problemu niż w przypadku wzorca Fabryka
Hermetyzacja operacji potrzebnych do utworzenia złożonego obiektuWraz 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.

Źródła:

0 komentarzy

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *