Wzorzec fasada (Facade)

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.

facade pattern

Podsumowując




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 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.

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

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.