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 backend-u, 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 builder-a 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 builder-em, 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
- Wzorzec budowniczy jest kreacyjnym wzorcem projektowym.
- Warto skorzystać z buildera, jeśli logika tworzenia obiektu jest skomplikowana lub etapowa.
- Bardzo często, wraz z budowniczym używa się klasy kontrolującej (kierownika).
Dodaj komentarz