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:

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.