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:

Wzorzec adapter

Strukturalny wzorzec projektowy adapter to spore udogodnienie w walce z systemami legacy. Pozwala użyć niepasującego interfejsu w innym. Dzięki temu zabiegowi pomimo, iż nie posiadamy spójnej abstrakcji możemy zastosować pożądaną logikę.

Sprawdź ten wpis

Wzorzec dekorator

Strukturalny wzorzec projektowy dekorator, to lekarstwo na problemy z mnogością różnych podtypów. Pozwala on na dynamiczne generowanie kolejnych typów obiektów bazujących na pierwotnym typie, bez konieczności deklarowania nowych, dedykowanych klas.

Sprawdź ten wpis

Wzorzec metoda wytwórcza (factory metod)

Jeżeli masz do czynienia z tworzeniem różnych obiektów bazujących na predefiniowanym modelu, to powinieneś/aś skorzystać z metody wytwórczej. Kreacyjny wzorzec projektowy factory method pozwala być fancy w tworzeniu obiektów.

Sprawdź ten wpis

SOLID z przykładami w TypeScript

SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.

Sprawdź ten wpis

Jak zostać architektem oprogramowania?

Aby projektować dobrą architekturę oprogramowania trzeba najpierw zaznajomić się z jej przesłankami, rodzajami, a także sposobami wdrażania. Architektura nie jest przecież czymś stałym, co wszędzie implementowane jest w jednakowy sposób.

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.