Wprowadzenie
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
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.).
Uwaga: w przykładzie klasa
Userma niewiele pól, żeby zachować czytelność. W rzeczywistych systemach obiekty mogą mieć wiele pól i złożone konfiguracje, wtedy użycie wzorca Budowniczy jest bardziej uzasadnione.
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.
Rozwiązanie
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 sekwencję wywołań budowniczego oraz kontroluje kolejność wykonywania kroków budowy.
Implementacja
Struktura wzorca
Głównym elementem poniższego diagramu UML jest interfejs UserBuilder. Deklaruje on publiczne metody umożliwiające ustawienie wartości odpowiednich pól oraz metodę getUser(), która zwraca gotowego użytkownika.
Występuje dwóch budowniczych – RegularUserBuilder oraz AdminUserBuilder – którzy implementują ten interfejs i definiują szczegóły budowy obiektu. Oczywiście nic nie stoi na przeszkodzie, aby utworzyć więcej budowniczych, odpowiadających innym konfiguracjom użytkownika.
Proces budowy nadzoruje kierownik UserDirector, który w konstruktorze przyjmuje obiekt typu UserBuilder. Dzięki temu może wywołać odpowiednie metody budowniczego w określonej kolejności (etapowanie budowy), a następnie zwrócić gotowy obiekt.

Struktura plików
- builder
- user.py
- builder.py
- director.py
- main.py
Kod źródłowy
user.py
from dataclasses import dataclass, field
from typing import List
@dataclass
class User:
username: str = ""
role: str = ""
permissions: List[str] = field(default_factory=list)
def __str__(self) -> str:
return (
f"Użytkownik: {self.username}\n"
f"Rola: {self.role}\n"
f"Uprawnienia: {', '.join(self.permissions)}\n"
)
Code language: Python (python)
builder.py
from abc import ABC, abstractmethod
from user import User
class UserBuilder(ABC):
@abstractmethod
def set_username(self, username: str) -> None:
pass
@abstractmethod
def set_role(self) -> None:
pass
@abstractmethod
def set_permissions(self) -> None:
pass
def get_user(self) -> User:
pass
class AdminUserBuilder(UserBuilder):
def __init__(self):
self._reset()
def _reset(self):
self._user = User()
def set_username(self, username: str) -> None:
self._user.username = username
def set_role(self) -> None:
self._user.role = "admin"
def set_permissions(self) -> None:
self._user.permissions = ["read", "write", "delete", "ban_users"]
def get_user(self) -> User:
user = self._user
self._reset()
return user
class RegularUserBuilder(UserBuilder):
def __init__(self):
self._reset()
def _reset(self):
self._user = User()
def set_username(self, username: str) -> None:
self._user.username = username
def set_role(self) -> None:
self._user.role = "user"
def set_permissions(self) -> None:
self._user.permissions = ["read", "comment"]
def get_user(self) -> User:
user = self._user
self._reset()
return user
Code language: Python (python)
director.py
from builder import UserBuilder
from user import User
class UserDirector:
def __init__(self, builder: UserBuilder):
self._builder = builder
def create_user(self, username: str) -> None:
self._builder.set_username(username)
self._builder.set_role()
self._builder.set_permissions()
def get_user(self) -> User:
return self._builder.get_user()
Code language: Python (python)
main.py
from builder import AdminUserBuilder, RegularUserBuilder
from director import UserDirector
def main():
print("--- Tworzenie zwykłego użytkownika ---")
regular_builder = RegularUserBuilder()
director = UserDirector(regular_builder)
director.create_user("mateusz")
print(director.get_user())
print("--- Tworzenie administratora ---")
admin_builder = AdminUserBuilder()
director = UserDirector(admin_builder)
director.create_user("admin")
print(director.get_user())
print("--- Tworzenie użytkownika bez kierownika ---")
regular_builder = RegularUserBuilder()
regular_builder.set_username("bez_kierownika")
regular_builder.set_role()
regular_builder.set_permissions()
print(regular_builder.get_user())
if __name__ == "__main__":
main()
Code language: Python (python)
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
--- Tworzenie użytkownika bez kierownika ---
Użytkownik: bez_kierownika
Rola: user
Uprawnienia: read, comment
Code language: Python (python)Podsumowanie działania
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 wystąpił w przypadku budowniczego dla admina.
Taki sposób budowania obiektów niesie ze sobą wiele korzyści. Umożliwia zróżnicowanie wewnętrznych struktur budowanych obiektów, dzięki czemu możemy otrzymać obiekty tego samego typu, ale o różnych konfiguracjach. Dodatkowo pozwala kontrolować proces tworzenia – kierownik decyduje o kolejności wykonywania kroków przez budowniczego. Co więcej, wydzielenie skomplikowanego kodu konstrukcyjnego do osobnej klasy budowniczego oraz etapowania do klasy kierownika sprawia, że klient nie musi przejmować się tymi szczegółami. Dzięki temu spełniamy zasadę Single Responsibility (pierwszą zasadę SOLID).
Kiedy nie używać wzorca
Wzorzec Budowniczy nie zawsze jest najlepszym rozwiązaniem. Nie warto go stosować, gdy:
- tworzony obiekt ma niewiele pól,
- konstrukcja obiektu jest prosta i nie wymaga etapowania,
- nie istnieje wiele wariantów konfiguracji,
- nie zależy nam na kontroli kolejności budowy,
- obiekt można w czytelny sposób skonfigurować przez parametry konstruktora lub wartości domyślne.
W takich przypadkach zastosowanie wzorca może prowadzić do nadmiernego rozbudowania struktury projektu oraz wprowadzenia niepotrzebnej liczby klas.
