Wzorzec budowniczy (builder)

Wzorzec projektowy budowniczy służy do dynamicznej budowy obiektów. Pozwala na kreowanie różnych typów obiektu w zależności od przesłanych wymagań. Dzięki temu posiadamy centralny punkt odpowiedzialny jedynie za tworzenie wariacji danego typu.


Builder pattern

Wzorzec projektowy budowniczy jest kolejnym projektowym wzorcem kreacyjnym omawianym przeze mnie na blogu. Pozwala on sprytnie budować obiekty etapami i odchudzić konstruktory z wielu różnych parametrów czy flag. Stosując ten pattern możemy bardzo uprościć tworzenie obiektów klas, a zwłaszcza tych, które agregują w sobie wiele wartości, które w podgrupach oznaczają coś innego. Brzmi skomplikowanie? Spokojnie, wcale tak nie jest i zaraz samemu przekonasz się, że stosowanie tego wzorca to czysta przyjemność i wygoda w późniejszym utrzymaniu kodu, który wymaga zastosowania tego patternu.

Przykład budowniczego

Wyobraźmy sobie system, za pomocą którego możemy stworzyć stronę internetową wyklikując poszczególne elementy. W celu późniejszych modyfikacji i chęci dynamicznego renderowania treści, wszystkie ustawienia zapisujemy po stronie backendu, który później zwraca je w postaci odpowiednich obiektów, np. buttonów, paragrafów czy obrazków. Powiedzmy, że dostarczamy możliwość szybkiego generowania predefiniowanych alertów - powiadomień. Jedną z możliwych implementacji jest ta poniżej.

export class Alert {
  constructor(
    private classes = [AlertClass.defaultClass],
    private content?: string,
    private id?: string,
    private data?: Record<string, string>
  ) {}
}

export interface AlertBuilderInterface {
  buildSuccessAlert(alert: Alert): Alert;
  buildErrorAlert(alert: Alert): Alert;
}

export class AlertBuilder implements AlertBuilderInterface {
  buildSuccessAlert(alert: Alert): Alert {
    alert.addClass(AlertClass.alertSuccess);

    alert.addClass(AlertClass.autoDismiss);

    alert.setContent(AlertContent.operationSucceed);

    alert.addDataAttribute(AlertDataAttributes.autoDismissAfter, 5000);

    return alert;
  }

  buildErrorAlert(alert: Alert): Alert {
    alert.addClass(AlertClass.alertDanger);

    alert.addClass(AlertClass.dismissible);

    alert.setContent(AlertContent.somethingWentWrong);

    alert.addDataAttribute(AlertDataAttributes.dismissForm, AlertDismissForm.cross);

    return alert;
  }
}
Powyższy kod nie jest oczywiście poprawny: łamie zasadę CQS oraz SOLIDa. Jak wiemy, jedyną stałą rzeczą w IT jest zmiana. Z pewnością dojdą nowe typy alertów z różnymi treściami, więc właśnie rozpoczęliśmy tworzenie boskiej klasy o nazwie AlertBuilder. Jak temu zaradzić? Tworząc budowniczych na każdą okazję, z pogrupowanymi tematycznie metodami oraz... zatrudniając kierownika!

Prawidłowy przykład wzorca builder

Warto na samym początku zatrudnić kierownika budowy (director pattern), który przypilnuje samych budowniczych, aby skrupulatnie wykonali odpowiednie kroki budowy. On sam zaś będzie dyrygował konkretnym, przekazanym mu budowniczym. W metodzie createAlert() użyłem operatora new, ale równie dobrze mógłbym użyć dedykowanej fabryki, gdyby logika tworzenia samego obiektu była ciut bardziej skomplikowana. Mógłbym też pokombinować z przekazywaniem już gotowego alertu do metody build()

export interface AlertBuilderInterface {
  createAlert();
  buildAppearance();
  buildBehavior();
  getAlert();
}

export class AlertDirector implements AlertDirectorInterface {
  build(builder: AlertBuilderInterface) {
    builder.createAlert();
    builder.buildAppearance();
    builder.buildBehavior();
  }
}

export class AutoDismissibleSuccessAlertBuilder implements AlertBuilderInterface {
   private alert: Alert;

   createAlert() {
     this.alert = new Alert(); 
   }

   buildAppearance() {
     this.alert.addClass(AlertClass.alertSuccess);
     this.alert.addClass(AlertClass.autoDismiss);
     this.alert.setContent(AlertContent.operationSucceed);
   }

   buildBehavior() {
     this.alert.addDataAttribute(AlertDataAttributes.autoDismissAfter, 5000);
   }

   getAlert(): Alert => this.alert;
}

Jak użyć wzorca budowniczy?

Użycie buildera zależy oczywiście od kontekstu, ale przyjmijmy, że mamy serwis, który odpowiada za ustalanie alertów sesyjnych, więc musi zapisywać je do bazy. Treść (sam alert) wygenerujemy builderem, a resztę logiki polecimy klasykiem.

export class SessionAlertsService {
  constructor(
    private readonly alertDirector: AlertDirector,
    private readonly sessionAlertRepository: SessionAlertRepository,
  ) {}

  async add(dto: AddSessionAlertDto) {
    const builder: AlertBuilderInterface = AlertBuildersMap.get(dto.type);

    this.alertDirector.build(builder);

    const sessionAlert = SessionAlertEntity.create({
      userId: dto.userId,
      alert: builder.getAlert()
    });

    await this.sessionAlertRepository.save(sessionAlert);
  }
}

Builder vs. Factory

Mogą pojawić się pewne wątpliwości i pytania, co do tego czym różni się fabryka od budowniczego. W końcu i tu i tu można użyć new Cosik. Ogólnie rzecz biorąc fabryka skupia się na samym tworzeniu obiektu. Może sprawdzać pewne warunki brzegowe i tworzyć spójne z interfejsem obiekty. Dodatkowo, dzięki niej mamy jedno miejsce w systemie, odpowiedzialne za tworzenie konkretnego obiektu. Builder jest natomiast wzorcem do zadań specjalnych, kiedy musimy połączyć komponowanie obiektu z jakąś logiką, np. budowa musi odbywać się w określonej kolejności czy grupa właściwości w obrębie obiektu determinuje jego typ. Kiedy używać wzorca builder? Wtedy, kiedy logika związana z tworzeniem obiektu jest zbyt duża na fabrykę, kiedy konieczne jest szerokie warunkowanie podczas tworzenia. 

Podsumowanie




Polecane wpisy:

KISS: keep it simple, stupid!

Zasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.

Sprawdź ten wpis

Wzorce projektowe

Wzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.

Sprawdź ten wpis

Wzorzec singleton

Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.

Sprawdź ten wpis

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec pełnomocnik (virtual proxy)

Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.