Facade

Wzorce strukturalne

ukończona

1. Definicja

Fasada (Facade) to wzorzec strukturalny, który dostarcza uproszczony, jednolity interfejs do złożonego podsystemu klas, bibliotek lub frameworków.

Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use. — Gang of Four, Design Patterns

Najprościej rzecz ujmując: Fasada nie dodaje nowej funkcjonalności — ona ją ukrywa.

Podsystem, który owija, dalej robi to samo co wcześniej. Zmienia się tylko to, że klient nie musi już znać jego wewnętrznej struktury, kolejności wywołań ani zależności między poszczególnymi klasami. Wystarczy, że zna jedną, prostą metodę.


2. Przykład z życia: zobaczmy jak to zrozumieć?

Wyobraźmy sobie recepcję w hotelu.

Kiedy meldujemy się jako goście, nie rozmawiamy osobno z:

  • działem sprzątania, żeby przygotować pokój,
  • kuchnią, żeby zapytać o możliwość zamówienia śniadania,
  • ochroną, żeby wydała nam kartę dostępu,
  • działem księgowości, żeby rozliczyć rezerwację.

Zamiast tego podchodzimy do jednej osoby recepcjonisty. To on w naszym imieniu koordynuje wszystkie te działy. My wypowiadamy jedno zdanie: "Chciałbym się zameldować", a cała złożoność dzieje się gdzieś za kulisami, poza naszym polem widzenia.

Recepcjonista nie zastępuje sprzątaczki ani kucharza, oni nadal wykonują swoją pracę dokładnie tak samo jak wcześniej. On po prostu daje nam jeden prosty punkt kontaktu zamiast zmuszać nas do samodzielnego ogarniania całego hotelu.

Ćwiczenie myślowe

Zastanówmy się, co by się stało, gdyby hotel nie miał recepcji. Każdy gość musiałby:

  1. znać strukturę organizacyjną hotelu,
  2. wiedzieć, w jakiej kolejności skontaktować się z poszczególnymi działami,
  3. samodzielnie obsłużyć każdą zmianę procedury (np. nowy system kart dostępu).

Dokładnie ten sam problem pojawia się w kodzie, gdy klient musi bezpośrednio operować na wielu klasach podsystemu, znać ich kolejność inicjalizacji i zależności między nimi. Fasada to nasz "recepcjonista", to znaczy jeden punkt wejścia do skomplikowanego systemu.


3. Przykład - naruszenie (brak Fasady)

Załóżmy, że budujemy system zamawiania jedzenia online. Złożenie zamówienia wymaga współpracy kilku niezależnych podsystemów: magazynu, płatności, dostawy i powiadomień.

// Podsystem 1: sprawdzanie dostępności produktów
class Inventory {
    public boolean checkStock(String productId, int quantity) {
        // sprawdzenie stanu magazynowego
        return true;
    }

    public void reserveItems(String productId, int quantity) {
        // rezerwacja towaru w magazynie
    }
}

// Podsystem 2: obsługa płatności
class PaymentProcessor {
    public boolean validateCard(String cardNumber) {
        // walidacja karty płatniczej
        return true;
    }

    public void charge(String cardNumber, double amount) {
        // obciążenie karty kwotą zamówienia
    }
}

// Podsystem 3: logistyka dostawy
class ShippingService {
    public String calculateDeliveryDate(String address) {
        // wyliczenie przewidywanej daty dostawy
        return "2026-07-10";
    }

    public void scheduleDelivery(String address, String date) {
        // zaplanowanie kuriera
    }
}

// Podsystem 4: powiadomienia
class NotificationService {
    public void sendOrderConfirmation(String email, String orderId) {
        // wysłanie maila z potwierdzeniem
    }
}

A teraz kod klienta, który musi obsłużyć całe zamówienie samodzielnie:

class OrderController {
    public void placeOrder(String productId, int quantity, String cardNumber,
                            double amount, String address, String email) {

        Inventory inventory = new Inventory();
        PaymentProcessor payment = new PaymentProcessor();
        ShippingService shipping = new ShippingService();
        NotificationService notifications = new NotificationService();

        // klient musi znać dokładną kolejność operacji!
        if (!inventory.checkStock(productId, quantity)) {
            throw new RuntimeException("Brak towaru");
        }

        if (!payment.validateCard(cardNumber)) {
            throw new RuntimeException("Nieprawidłowa karta");
        }

        inventory.reserveItems(productId, quantity);
        payment.charge(cardNumber, amount);

        String deliveryDate = shipping.calculateDeliveryDate(address);
        shipping.scheduleDelivery(address, deliveryDate);

        notifications.sendOrderConfirmation(email, "ORDER-123");
    }
}

Co jest złe?

  • OrderController musi znać wszystkie cztery podsystemy oraz dokładną kolejność ich wywołań jeśli ktoś zamieni miejscami rezerwację towaru i walidację karty, zamówienie może się rozjechać.
  • Każdy nowy ekran (np. formularz zamówienia w aplikacji mobilnej) będzie musiał powielić dokładnie tę samą logikę koordynacji — kopiuj-wklej tej samej sekwencji w wielu miejscach.
  • Zmiana w jednym podsystemie (np. dodanie kroku weryfikacji antyfraudowej w płatnościach) wymusza modyfikację każdego miejsca w kodzie, które korzysta z zamówień.
  • Testowanie OrderController wymaga zamockowania czterech niezależnych klas naraz, co czyni testy jednostkowe znacznie bardziej kruche i rozwlekłe.

4. Przykład - zgodny z Fasadą

Podejdźmy do tego inaczej: zamiast każdego klienta zmuszać do rozmowy z czterema "działami hotelu" osobno, wprowadźmy naszego "recepcjonistę" — klasę OrderFacade.

class OrderFacade {

    private final Inventory inventory;
    private final PaymentProcessor payment;
    private final ShippingService shipping;
    private final NotificationService notifications;

    public OrderFacade() {
        this.inventory = new Inventory();
        this.payment = new PaymentProcessor();
        this.shipping = new ShippingService();
        this.notifications = new NotificationService();
    }

    // Jedna metoda ukrywa całą złożoność koordynacji podsystemów
    public String placeOrder(String productId, int quantity, String cardNumber,
                              double amount, String address, String email) {

        if (!inventory.checkStock(productId, quantity)) {
            throw new RuntimeException("Brak towaru");
        }

        if (!payment.validateCard(cardNumber)) {
            throw new RuntimeException("Nieprawidłowa karta");
        }

        inventory.reserveItems(productId, quantity);
        payment.charge(cardNumber, amount);

        String deliveryDate = shipping.calculateDeliveryDate(address);
        shipping.scheduleDelivery(address, deliveryDate);

        String orderId = "ORDER-123";
        notifications.sendOrderConfirmation(email, orderId);

        return orderId;
    }
}

Kod klienta upraszcza się do jednego wywołania:

class OrderController {

    private final OrderFacade orderFacade = new OrderFacade();

    public void placeOrder(String productId, int quantity, String cardNumber,
                            double amount, String address, String email) {

        // klient nie wie i nie musi wiedzieć, ile podsystemów kryje się za tym wywołaniem
        String orderId = orderFacade.placeOrder(productId, quantity, cardNumber,
                                                  amount, address, email);
    }
}

Co się zmieniło krok po kroku?

  • Cała wiedza o kolejności operacji (sprawdź stan magazynu → zwaliduj kartę → zarezerwuj → obciąż → zaplanuj dostawę → powiadom) trafiła w jedno miejsce — do OrderFacade.
  • OrderController (i każdy przyszły klient tej logiki, np. kontroler REST albo zadanie w tle) wywołuje tylko jedną metodę i nie zna szczegółów implementacyjnych podsystemów.
  • Gdy w płatnościach pojawi się nowy krok (np. weryfikacja antyfraudowa), zmieniamy tylko OrderFacade — żaden kod klienta się nie zmienia.
  • Testując OrderController, mockujemy jedną zależność (OrderFacade), a nie cztery osobne klasy.

Co ważne: podsystemy (Inventory, PaymentProcessor, ShippingService, NotificationService) nadal istnieją i nadal można ich używać bezpośrednio, jeśli ktoś potrzebuje bardziej szczegółowej kontroli. Fasada niczego nie zamyka na siłę — po prostu daje wygodną, uproszczoną ścieżkę dla typowych przypadków użycia.


5. Diagram struktury

────────────────────────────────────────────────────────────────────────────


                      ┌───────────────────────┐
   Client  ───────▶   │   OrderFacade         │
                      └───────────────────────┘
                       │      │          │   │
             ┌─────────┘      │          │   └────────────┐
             ▼                ▼          ▼                ▼
      ┌────────────┐  ┌──────────────┐  ┌───────────┐  ┌───────────────────┐
      │ Inventory  │  │PaymentProcess│  │ Shipping  │  │NotificationService│
      └────────────┘  └──────────────┘  └───────────┘  └───────────────────┘

   Client zna tylko OrderFacade.
   OrderFacade zna i koordynuje wszystkie podsystemy.
   Podsystemy nie wiedzą o istnieniu Fasady ani o sobie nawzajem.

6. Dlaczego Fasada jest ważna? Kiedy NIE stosować?

Korzyści:

  • Luźniejsze powiązanie (loose coupling) — klient zależy od jednego, stabilnego interfejsu, a nie od wielu klas podsystemu, które mogą się zmieniać niezależnie.
  • Łatwiejsza nauka i wdrożenie — nowa osoba w zespole nie musi rozumieć całego podsystemu płatności, magazynu i logistyki naraz, żeby złożyć zamówienie w kodzie.
  • Mniej duplikacji — logika koordynacji podsystemów istnieje w jednym miejscu, zamiast być kopiowana w każdym kontrolerze, zadaniu cron czy teście.
  • Bezpieczniejszy refactoring — zmiana wewnętrznej struktury podsystemu (np. wymiana dostawcy płatności) nie wymaga dotykania kodu klienta, o ile Fasada zachowa ten sam interfejs.

Kiedy NIE stosować:

  • Gdy podsystem jest już prosty i ma jeden, oczywisty punkt wejścia — dodanie Fasady byłoby tylko niepotrzebną warstwą pośrednią.
  • Gdy różni klienci faktycznie potrzebują drobiazgowej, niezależnej kontroli nad poszczególnymi elementami podsystemu — zbyt "gruba" Fasada może wymusić rezygnację z tej elastyczności lub prowadzić do rozrastania się jej interfejsu o dziesiątki parametrów.
  • Gdy Fasada staje się tzw. God Object — jeśli sama zaczyna zawierać logikę biznesową zamiast tylko koordynować wywołania, naruszamy Single Responsibility Principle, a Fasada przestaje być fasadą.

Konsekwencje zaniedbania w dużych projektach: bez Fasady logika integracji z rozbudowanym podsystemem (np. płatności, integracja z zewnętrznym API, silnik raportowania) zwykle rozprasza się po całej aplikacji. Każdy kontroler, zadanie w tle i test integracyjny odtwarza tę samą sekwencję wywołań na swój sposób, co prowadzi do rozbieżności, trudnych do wyśledzenia błędów i kosztownych migracji, gdy trzeba zmienić dostawcę usługi.


7. Kiedy stosować?

Typowe scenariusze:

  • Integracja z zewnętrznym, złożonym API (płatności, wysyłka SMS, dostawcy chmurowi) — Fasada ukrywa szczegóły protokołu, uwierzytelniania i obsługi błędów za jedną metodą.
  • Warstwa serwisowa w architekturze warstwowej (np. OrderService w aplikacji e-commerce), która koordynuje repozytoria, walidatory i zewnętrzne integracje.
  • Uproszczenie starszego, rozbudowanego systemu (legacy) — zamiast go przepisywać, otaczamy go Fasadą, żeby nowy kod nie musiał znać jego wewnętrznego chaosu.

Fasada w znanych bibliotekach i frameworkach:

  • SLF4J to podręcznikowy przykład Fasady w świecie Javy — dostarcza jeden, jednolity interfejs logowania (Logger.info(), Logger.error()), niezależnie od tego, czy pod spodem działa Log4j, Logback, czy java.util.logging. Zmiana silnika logowania nie wymaga zmiany ani jednej linii kodu korzystającego z SLF4J.
  • Spring JdbcTemplate to Fasada nad surowym JDBC — ukrywa otwieranie połączeń, obsługę wyjątków SQLException i zamykanie zasobów za prostymi metodami typu query() czy update().
  • java.util.concurrent.Executors dostarcza proste metody fabrykujące (newFixedThreadPool(), newCachedThreadPool()), które ukrywają skomplikowaną konfigurację ThreadPoolExecutor.

Fasada jest szczególnie cenna wtedy, gdy podsystem ma wiele klas, ścisłą kolejność wywołań i skomplikowaną konfigurację — czyli wszędzie tam, gdzie klient powinien myśleć w kategoriach "co chcę osiągnąć", a nie "jak dokładnie to działa pod spodem".


Jeden przycisk, wiele systemów. Tak jak pilot do kina domowego jednym przyciskiem "Oglądaj film" włącza projektor, głośniki i przyciemnia światła — Fasada jednym wywołaniem metody uruchamia cały skomplikowany podsystem.