Wzorzec projektowy Budowniczy

Wprowadzenie

Wzorce projektowe to rozwiązania popularnych problemów programistycznych. Wyróżniamy następujące grupy wzorców:

  • wzorce kreacyjne, które zajmują się tworzeniem obiektów, np. Fabryka, Budowniczy, Singleton.
  • wzorce strukturalne, które pomagają grupować obiekty w większe struktury, np. Adapter, Dekorator.
  • wzorce behawioralne, które wspomagają definiowanie ścieżek komunikacyjnych między obiektami, np. Obserwator, Strategia.

Budowniczy (Builder) jest kreacyjnym wzorcem projektowym. Jego głównym zadaniem jest tworzenie złożonych obiektów etapami, krok po kroku. Najczęściej używamy go w przypadku, gdy klasa ma bardzo dużo pól i nie chcemy dla niej tworzyć dużej liczby konstruktorów.

Problem i rozwiązanie

Tworzymy system, który pozwala budować użytkowników z różnymi konfiguracjami konta. Każde konto ma różne domyślne ustawienia (rola, uprawnienia, dostęp do funkcji, itp.).

Bez zastosowania wzorca projektowego tworzylibyśmy obiekty wprowadzając wszystkie domyślne ustawienia do konstruktora, przez co konstruktor byłby rozbudowany, a czasami nie wszystkie parametry byłyby potrzebne za każdym razem. Innym rozwiązaniem byłoby utworzenie wielu różnych konstruktorów, które różniłyby się ilością przyjmowanych pól. Oba rozwiązania utworzyłyby niezły bałagan i zawirowanie w kodzie klasy.

Z pomocą przychodzi wzorzec Budowniczy, który proponuje umieszczenie kodu konstrukcyjnego w osobnych obiektach zwanych budowniczymi. Ponadto pozwala na budowanie obiektu krok po kroku. Co więcej, wykorzystując interfejs, można utworzyć wiele klas, które będą implementowały metody na swój sposób.

Opcjonalnie można użyć kierownika, który ukrywa kolejkę wywołań budowniczego oraz określa kolejność prac wykonywanych przez budowniczego.

Schemat działania

Głównym elementem poniższego schematu jest interfejs UserBuilderInterface. Posiada on deklaracje publicznych metod, które będą pozwalały na ustawienie wartości odpowienich pól oraz metodę getUser(), która zwróci gotowego użytkownika. Występuje dwóch budowniczych RegularBuilderInterface i Admin BuilderInterface, którzy implementują interfejs. Oczywiście nic nie stoi na przeszkodzie, aby utworzyć więcej budowniczych.

Pracę budowniczych nadzoruje kierownik UserDirector, który w konstruktorze przyjmuje budowniczego, dzięki któremu jest w stanie wywołać dwie metody – pierwszą odpowiedzialną za ustawienie pól (budowę) w odpowiedniej kolejności, a druga zwraca gotowego użytkownika.

Implementacja

Struktura folderów i plików projektu

  • builder
    • user
      • AdminUserBuilder.php
      • RegularUserBuilder.php
      • User.php
      • UserBuilderInterface.php
      • UserDirector.php
    • index.php

Kod źródłowy

Plik User.php

<?php
declare(strict_types=1);

namespace user;

class User
{
  private string $username;
  private string $role;
  private array $permissions;

  public function setUsername(string $username): void
  {
    $this->username = $username;
  }

  public function setRole(string $role): void
  {
    $this->role = $role;
  }

  public function setPermissions(array $permissions): void
  {
    $this->permissions = $permissions;
  }

  public function __toString(): string
  {
    return
      "Użytkownik: " . $this->username . "<br/>" .
      "Rola: " . $this->role . "<br/>" .
      "Uprawnienia: " . implode(', ', $this->permissions) . "<br/><br/>";
  }
}

Plik UserBuilderInterface.php

<?php
declare(strict_types=1);

namespace user;

interface UserBuilderInterface
{
  public function setUsername(string $username): void;
  public function setRole(): void;
  public function setPermissions(): void;

  public function getUser(): User;
}

Plik AdminUserBuilder.php

<?php
declare(strict_types=1);

namespace user;
use user\{UserBuilderInterface, User};

class AdminUserBuilder implements UserBuilderInterface
{
  private User $user;

  public function __construct()
  {
    $this->user = new User();
  }

  public function setUsername(string $username): void
  {
    $this->user->setUsername($username);
  }

  public function setRole(): void
  {
    $this->user->setRole("admin");
  }

  public function setPermissions(): void
  {
    $this->user->setPermissions(['read', 'write', 'delete', 'ban-users']);
  }

  public function getUser(): User
  {
    return $this->user;
  }
}

Plik RegularUserBuilder.php

<?php
declare(strict_types=1);

namespace user;
use user\{UserBuilderInterface, User};

class RegularUserBuilder implements UserBuilderInterface
{
  private User $user;

  public function __construct()
  {
    $this->user = new User();
  }

  public function setUsername(string $username): void
  {
    $this->user->setUsername($username);
  }

  public function setRole(): void
  {
    $this->user->setRole("user");
  }

  public function setPermissions(): void
  {
    $this->user->setPermissions(['read', 'comment']);
  }

  public function getUser(): User
  {
    return $this->user;
  }
}

Plik UserDirector.php

<?php
declare(strict_types=1);

namespace user;

class UserDirector
{
  private UserBuilderInterface $userBuilderInterface;

  public function __construct(UserBuilderInterface $userBuilderInterface)
  {
    $this->userBuilderInterface = $userBuilderInterface;
  }

  public function createUser(string $username): void
  {
    $this->userBuilderInterface->setUsername($username);
    $this->userBuilderInterface->setRole();
    $this->userBuilderInterface->setPermissions();
  }

  public function getUser(): User
  {
    return $this->userBuilderInterface->getUser();
  }
}

Plik index.php

<?php
declare(strict_types=1);

require_once __DIR__ . "/user/UserBuilderInterface.php";
require_once __DIR__ . "/user/User.php";
require_once __DIR__ . "/user/RegularUserBuilder.php";
require_once __DIR__ . "/user/AdminUserBuider.php";
require_once __DIR__ . "/user/UserDirector.php";

use user\{RegularUserBuilder, AdminUserBuilder, UserDirector};

echo "--- Tworzenie zwykłego użytkownika ---<br/>";
$builder = new RegularUserBuilder();
$director = new UserDirector($builder);
$director->createUser("mateusz");

echo $director->getUser();

echo "--- Tworzenie administratora ---<br/>";
$builder = new AdminUserBuilder();
$director = new UserDirector($builder);
$director->createUser("admin");

echo $director->getUser();

Wynik działania:

--- Tworzenie zwykłego użytkownika ---
Użytkownik: mateusz
Rola: user
Uprawnienia: read, comment

--- Tworzenie administratora ---
Użytkownik: admin
Rola: admin
Uprawnienia: read, write, delete, ban-users

Podsumowanie

Na powyższym przykładzie za pomocą kodu klienckiego najpierw utworzono budowniczego dla zwykłego użytkownika, kolejno ustanowiono kierownika, który za pomocą budowniczego utworzył nowego użytkownika o nazwie mateusz. Analogiczny proces wystapił w przypadku budowniczego dla admina.

Taki sposób budowania obiektów niesie ze sobą wiele korzyści. Po pierwsze mamy możliwość zróżnicowania wewnętrzych struktur budowanych obiektów, dzięki temu możemy otrzymać ten sam obiekt, ale o różnych właściwościach. Po drugie mamy możliwość kontrolowania tego, w jaki sposób tworzony jest obiekt – kierownik ma wpływ na kolejność wykonywania prac przez budowniczego. Po trzecie dzięki wydzieleniu skomplikowanego kodu konstrukcyjnego do osobnej klasy budowniczego, a etapowania do klasy kierownika, klient nie musi się przejmować tymi kwestiami. Dzięki temu spełniamy Zasadę pojedynczej odpowiedzialności (1 Zasada SOLID).

Scroll to Top