Dzisiaj o popularnym wzorcu strukturalnym: fasada. Wielu programistów nie zdaje sobie nawet sprawy, że z niego korzysta. Z pewnych względów (a raczej niepoprawnych implementacji) wzorzec facade może być postrzegany jako „anty pattern”.
Agregator operacji
Fasada jest jednym z moich ulubionych wzorców projektowych. Zastosowanie tego wzorca strukturalnego w projekcie dostarcza przede wszystkim przejrzystości oraz zwinnej enkapsulacji operacji. Odpowiednia agregacja poleceń pozwala na posiadanie czytelnego interfejsu dla złożonej z wielu „kroków” logiki. Klient używający tak przygotowanego API nie musi zagłębiać się w szczegóły implementacji, która może składać się np. z wielu wywołań metod, na kilku serwisach.
O tym wzorcu wspominałem już przy okazji omawiania service class, gdzie pokazałem prosty przykład fasady. Niemniej jednak fasadę można wyobrażać sobie dosłownie tak, jak zostało to zaprezentowane na obrazku wyróżniającym tego wpisu. Piękny front budynku, a w jego wnętrzu znajdują się pomieszczenia, w których realizowane są pewne specyficzne zadania (np. toaleta).
export class Facade {
constructor(
private readonly service1: Service1,
private readonly service2: Service2,
private readonly service3: Service3
) {}
action1() {
const result1 = this.service1.action();
if (result1) {
this.service2.action();
return;
}
this.service3.action();
}
}
Kiedy i gdzie używać wzorca fasady?
We wstępie wspomniałem, że wiele osób nie zdaje sobie nawet sprawy, że używa fasady (ale ja dziś rymuję <zamyslonyPingwin.jpg>). I tak faktycznie jest, bo naszej klasy wcale nie musimy nazywać fasadą, aby agregowała ona w sobie jakieś operacje. Cytując Szekspira: „To, co zowiem różą, pod inną nazwą równie by pachniało”. Przykładem mogą być serwisy domenowe, które w swoich metodach zawierają wywołania na wielu agregatach. Jest fasada? Jest, a nazywa się service – no podwójny agent.
W tym wzorcu chodzi jednak bardziej o agregowanie wywołań skomplikowanych operacji na wysokim poziomie abstrakcji, np. operacji na serwisach, które to mogą działać na jeszcze niższych warstwach. Nazywanie więc klasy fasadą ma więcej słuszności dopiero wtedy, kiedy jest ona faktycznie frontem do skomplikowanych odwołań, które jako całość dostarczają pewną wartość, a nie chcemy, aby klient znał szczegóły implementacji. Wolimy, aby wywołana została jedna metoda fasady, a nie trzy metody z dwóch serwisów i jednego repozytorium (nie tylko serwisami fasada żyje).
export class PublicationContentFacade {
constructor(
private readonly censorshipService: CensorshipService,
private readonly normalizationService: PublicationContentNormalizationService,
private readonly contentRepository: PublicationContentRepository
) {}
review(publicationId: PublicationId, reviewerId: ReviewerId) {
const content = this.contentRepository.findOneOrFail(publicationId);
content.assignReviewer(reviewerId);
this.censorshipService.censorProfanity(content);
this.normalizationService.removeExcessSpaces(content);
this.contentRepository.save(content);
}
}
Taką fasadę można użyć tak naprawdę wszędzie, gdzie potrzebna jest logika w niej zawarta. Najczęściej (przynajmniej ja) fasady widuję w kontrolerach. Z dwóch głównych powodów. Pierwszy z nich to przypadek źle dobranej architektury aplikacyjnej do problemu projektu. Ratujemy się wtedy tak, jak możemy, a fasady w tym przypadku mogą odchudzić kontrolery. Drugi, to sytuacja kiedy dana operacja składa się (tak jak powyżej) z kilku etapów, więc zamiast duplikować kod w kilku miejscach, opakowujemy go fasadą, której użyjemy w docelowych metodach. Ważne jest, aby fasada była usytuowana, jako wysoka warstwa abstrakcji modułu.
export class PublicationContentController {
constructor(
private readonly contentRepository: PublicationContentRepository,
private readonly contentFacade: PublicationContentFacade,
) {}
@Post()
review(@Body() { publicationId, reviewerId }: ReviewPublicationRequest): Result {
this.contentFacade.review(publicationId, reviewerId);
return Result.accepted();
}
}
Trzeba uważać na SRP
W przypadku fasad można bardzo łatwo przesadzić i złamać zasadę pojedynczej odpowiedzialności (Single Responsibility Principle) i zrobić z tego anty wzorzec. Dzieje się tak wtedy, gdy nasza klasa agreguje zbyt wiele. Mamy kilka metod i przepełniony zależnościami konstruktor. O tym problemie wspominałem już w odniesieniu do serwisów, jednak w przypadku fasad możemy wpaść w dokładnie tę samą pułapkę. Moja rada jest dokładnie taka sama, jak w przypadku service classes: budujmy małe, atomowe fasady, które udostępnią interfejs zorientowany na konkretny problem i nie będą przerośniętym gorylem.
Podsumowując
- Fasada jest wzorcem strukturalnym pozwalającym na agregację procesów.
- Używając facade pattern możemy zamaskować skomplikowane operacje za pomocą jednej metody.
- Podczas implementacji trzeba uważać, aby klasa nie rozrosła się za bardzo.
Dodaj komentarz