post_ico4

Wzorce projektowe, cz. 9 Builder

Budowniczy jest wzorcem, gdzie proces tworzenia obiektu podzielony jest na kilka mniejszych etapów, a każdy z nich może być implementowany na wiele sposobów. Dzięki takiemu rozwiązaniu możliwe jest tworzenie różnych reprezentacji obiektów w tym samym procesie konstrukcyjnym.


Diagram klas wzorca Builder

Standardowo wzorzec składa się z dwóch podstawowych elementów. Pierwszy z nich oznaczony jest jako Builder – jego celem jest dostarczenie interfejsu do tworzenia obiektów nazywanych produktami (product). Drugim elementem jest obiekt oznaczony jako ConcreteBuilder, a jego celem jest tworzenie konkretnych reprezentacji produktów przy pomocy zaimplementowanego interfejsu Builder. W ConcreteBuilder zawarte są procedury odpowiedzialne za konstrukcje i inicjalizację obiektu. Strukturę wzorca uzupełnia obiekt Director (zwany także czasem kierownikiem, nadzorcą – tak jak na budowie ;)), który zleca konstrukcję produktów poprzez obiekt Builder dbając o to, aby proces budowy przebiegał w odpowiedniej kolejności.


Diagram sekwencji wzorca Builder

Przykładowa implementacja

<?php

class Product {

    private $part1;
    private $part2;

    public function setPart1($part1) {
        $this->part1 = $part1;
    }
    public function getPart1() {
        return $this->part1;
    }
    public function setPart2($part2) {
        $this->part2 = $part2;
    }
    public function getPart2() {
        return $this->part2;
    }

}

interface Builder {
    public function buildPart1();
    public function buildPart2();
}

class ConcreteBuilder implements Builder {
    private $product;

    public function __construct() {
        $this->product = new Product();
    }
    public function buildPart1() {
        $this->product->setPart1("1. faza budowy");
    }
    public function buildPart2() {
        $this->product->setPart2("2. faza budowy");
    }
    public function getProduct() {
        return $this->product;
    }

}

class Director{
    private $builder;

    public function __construct() {
        $this->builder = new ConcreteBuilder();
    }
    public function construct() {
        $this->builder->buildPart1();
        $this->builder->buildPart2();
    }
    public function getResult() {
        return $this->builder->getProduct();
    }
}

// testy
$director = new Director();
$director->construct();
$product = $director->getResult();
echo $product->getPart1(); // wyswetli "1. faza budowy"
echo $product->getPart2(); // wyswieli "2. faza budowy"

?>

Przykład z życia wzięty

Tworząc serwis udostępniający filmy wideo chcemy mieć możliwość wyboru technologii do generowania odtwarzacza wideo. Możemy użyć do tego budowniczych.

<?php
class Player {
    private $playerName;

    public function setplayerName($playerName) {
        $this->playerName = $playerName;
    }
    public function render() {
        return $this->playerName;
    }
}

interface Builder {
    public function buildPlayer();
    public function getPlayer();
}

class FlashBuilder implements Builder {
    private $player;

    public function __construct() {
        $this->player = new Player();
    }
    public function buildPlayer() {
        $this->player->setplayerName("Player w Flash");
    }
    public function getPlayer() {
        return $this->player;
    }
}

class HTMLBuilder implements Builder {
    private $player;

    public function __construct() {
        $this->player = new Player();
    }
    public function buildPlayer() {
        $this->player->setplayerName("Player w HTML5");
    }
    public function getPlayer() {
        return $this->player;
    }
}

class Director{
    private $builder;

    public function __construct(Builder $builder) {
        $this->builder = $builder;
    }
    public function construct() {
        $this->builder->buildPlayer();
    }
    public function getResult() {
        return $this->builder->getPlayer();
    }
}

// testy
$html = new HTMLBuilder();
$flash = new FlashBuilder();
$director = new Director($flash);
$director->construct();
$player = $director->getResult();
echo $player->render(); // wyswietli "player w flash"

$director2 = new Director($html);
$director2->construct();
$player2 = $director2->getResult();
echo $player2->render(); // wyswietli "player w HTML5"

?>

Dwaj budowniczy FlashBuilder i HTMLBuilder mają za zadanie stworzyć obiekt Video z uwzględnieniem wykorzystanej technologii. Z kolei Director „pilnuje” poprawnej kolejności wywoływania metod. Dzięki wykorzystaniu Dependency Injection możemy łatwo dodawać kolejnych budowniczych.

Zalety i wady

Zalety:

  • Duża możliwość zróżnicowania wewnętrznych struktur klas.
  • Duża skalowalność (dodawanie nowych reprezentacji obiektów jest uproszczone).
  • Większa możliwość kontrolowania tego, w jaki sposób tworzony jest obiekt (proces konstrukcyjny jest niezależny od elementów, z których składa się tworzony obiekt.

Wady:

  • Duża liczba obiektów reprezentujących konkretne produkty.
  • Nieumiejętne używanie wzorca może spowodować nieczytelność kodu (jeden produkt może być tworzony przez zbyt wielu budowniczych).

Zastosowanie

Wzorzec budowniczego stosowany jest do oddzielenia sposobu tworzenia obiektów od tego jak te obiekty mają wyglądać. Przykładem jest oprogramowanie konwertujące tekst z jednego formatu na drugi. Algorytm odczytujący i interpretujący dane wejściowe jest oddzielony od algorytmu tworzącego dane wyjściowe. Dzięki takiemu rozwiązaniu możliwe jest zastosowanie jednego obiektu odczytującego dane wejściowe oraz wielu obiektów konwertujących odczytane dane do różnych formatów (ASCII, HTML, RTF, itp.), co zwiększa skalowalność rozwiązania.

Innym zastosowaniem może być stworzenie narzędzia, które będzie miało różną implementację zależnie od użytej technologii – tak jak w omawianym przykładzie odtwarzacz wideo.

Powiązane tematy

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (Brak ocen, bądź pierwszy!)
Loading...
  • M4ver

    Witam, możesz mi wytłumaczyć linijkę :$player = $director->getResult();

    echo $player->render(); // wyswietli „player w flash”

    Dziwne jest to, że metoda render zwraca return $this->player tak samo jak getPlayer w takim razie dlaczego jak napiszę echo $director->getResult() zwraca bład że nie można go konwertować na string?

    • Witam,
      zauważ, że klasa Player przechowuje w polu $player string. a klasy FlashBuilder i HTML5builder – instancję obiektu klasy Player. Metoda $director->getResult() zwraca obiekt klasy Player, a nie string. Stąd taki komunikat.

      Trochę niefortunnie nadałem takie same nazwy polom i jest to mniej przejrzyste. Zaraz to poprawię.

  • Kunimodus

    Witam,
    czytałem opisy wzorców na tej stronie i jestem zaciekawiony, na czym bazuje Twoja strona. Na wielu linki wyglądają tak, jakby każda strona znajdowała się w innym katalogu (np. http://lukasz-socha.pl/php/wzorce-projektowe-cz-9-builder/ ) i wciąż nie znalazłem odpowiedzi, jak to jest zorganizowane – podpowiesz?

  • xyz

    Tekst skopiowany z wiki…

    • Jeżeli zarzucasz mi plagiat, podaj konkretne przykłady.

      Tworząc cały cykl o wzorcach korzystałem z rożnych materiałów pomocniczych. W tym cyklu nie tworzę nowych wzorców, tylko opisuję te istniejące. Siłą rzeczy w niektórych miejscach tekst może być podobny.

  • Artur

    Wygląda to bardzo podobnie do użycia wzorca projektowego: Strategia
    Bynajmniej wygląda to tak w przykładzie z odtwarzaczem wideo,

  • Tomasz Staszewski

    Panie Łukaszu z reguły nie udzielam się w komentarzach :) Ale tutaj zrobię mały wyjątek. Na wstępie dziękuję za tą stronę. A teraz do rzeczy. W powyższym przykładzie: „Przykład z życia wzięty” stworzył Pan dwie klasy FlashBuilder i HtmlBuilder rozumiem, że to tylko przykład ale w konstruktorze tworzy Pan w obu tych klasach obiekt Player, czy nie lepiej było by aby każda z tych klas tworzyła odpowiednio obiekty FlashPlayer i HtmlPlayer, bo o ile dobrze zrozumiałem każdy builder powinien tworzyć unikatowy obiekt a nie ten sam, oczywiście może tworzyć ten sam ale czy w przykładzie nie powinien tworzyć osobno dla zachowania przejrzystości i uniknięcia zamieszania – chyba, że coś mi umknęło.

    • Ideą tego wzorca jest właśnie tworzenie produktów na bazie tej samej klasy, ale przez innych budowniczych. Stworzenie 2 klas: FlashPlayer i HtmlPlayer straciłoby sens.

      Playery we Flashu i w HTML5 mają za zadanie to samo robić – odtwarzać wideo, zatrzymywać je itd. Różnią się tylko technologią.

      Dzięki wzorcowi Builder można w łatwy sposób stworzyć playery w różnych technologiach, ale z takim samym interfejsem.

      FlashBuilder stworzy kod z tagiem Natomiast HTMLBuilder stworzy kod z tagiem .

      W dalszej części kodu nie interesuje mnie w jakiej technologii jest odtwarzacz. Wywołuję po prostu $player->render(); i mi generuje odtwarzacz na stronie.

      „powinien tworzyć unikatowy obiekt a nie ten sam, oczywiście może tworzyć ten sam ale czy w przykładzie nie powinien tworzyć osobno dla zachowania przejrzystości i uniknięcia zamieszania – chyba, że coś mi umknęło.”

      No i budowniczy tworzą osobne obiekty. Przypominam, że obiekt to nie to samo co klasa. Obiekt jest instancją klasy.

  • Tomasz Staszewski

    Jeszcze jedno przyszło mi do głowy czy nie lepiej aby każdy z bilderów w tym wypadku HTMLBuilder i
    FLASHLBuilder obiekt samego playera wytwarzały w metodzie buildPlayer a nie w konstruktorze, w tym przykładzie nie ma to znaczenia ale przy bardziej skomplikowanych systemach wywołanie kilka razy metody construct w klasie Director zwróci mi instancję tego samego obiektu a moim zdaniem powinien być wytworzony nowy obiekt, ma to szczególne znaczenie jeżeli wytworzony obiekt został przekazany gdzieś dalej w aplikacji i nie chcemy aby zmieniał swojego stanu już po wytworzeniu. Oczywiście można powołać do życie nowe instancje bilderów które wytworzą nowe obiekty playerów, ale nie mamy pewności że ktoś będzie o tym pamiętał.

    • Tworzę obiekt player, ponieważ dzięki temu mam do niego dostęp od razu – nie muszę się bawić w wywoływanie kolejnej metody.

      Trochę może niefortunnie nazwałem metodę. Metoda contsruct() w Direvtor u mnie wywołuje zestaw metod, które mają za zadanie wygenerowanie odtwarzacza – nadanie nazwy, stworzenie kodu HTML itd.

      Oczywiście moja implementacja nie jest jedyną słuszną. W jaki sposób dana rzecz zostanie rozwiązana zależy od programisty i konkretnej sytuacji. Tutaj np. http://www.algorytm.org/wzorce-projektowe/budowniczy-builder/builder-j.html obiekty są tworzone z użyciem setterów.