Wzorce Projektowe: Fasada
Fasada
Z pewnością w codziennym życiu zdarzyło Ci się odczuwać korzyści płynące z łatwego używania urządzeń takich jak pilot telewizora, system smart home czy choćby panel sterowania windą. W praktyce był to ukryty bohater naszego dzisiejszego wpisu, czyli strukturalny wzorzec projektowy o nazwie fasada.
Gotowe repozytorium z kodem znajdziesz tutaj
Podstawowe założenia wzorca
Maksymalnie upraszczając, głównym założeniem Fasady jest dostarczenie jednego prostego interfejsu zamiast rozbudowanych struktur wraz z zależnościami. Dzięki temu ukrywamy szczegóły implementacyjne poszczególnych elementów systemu, a korzystanie z niego jest o wiele prostsze
Brzmi groźnie? Graficzny schemat pomoże Ci to sobie zwizualizować
Implementacja
Skupmy się teraz na implementacji Fasady na podstawie przykładu smart home. Inteligentny system powinien posiadać funkcje sterowania drzwiami wejściowymi, oświetleniem oraz temperaturą wewnątrz pomieszczeń.
Uproszczone klasy sterujące elementami domu mogą wyglądać następująco:
class Door { lock() { return 'Doors locked'; } unlock() { return 'Doors opened'; } } class Light { adjustBrightness(level: number) { return `Brightness level set to ${level}%`; } } class Temperature { setTemperature(temp: number) { return `Temperature set to ${temp}°C`; } }
Błędne podejście polega na samodzielnym inicjalizowaniu obiektów klas, zależności i wywoływanie metod w odpowiedniej kolejności, z właściwymi parametrami:
const door = new Door(); const light = new Light(); const temp = new Temperature(); // Evening Mode // Turn on at 7 PM door.lock() light.adjustBrightness(70) temp.setTemperature(22) // Morning Mode // Turn on at 7 AM door.unlock() light.adjustBrightness(100) temp.setTemperature(20)
Ten przykład jest prosty, ale co gdyby logika była bardziej skomplikowana? Wywoływanie wielu metod i przekazywanie do nich odpowiednich argumentów niesie ze sobą wiele ryzyk takich jak np:
- pomylenie kolejności wywołań
- pominięcie wykonania jakiegoś fragmentu kodu
- duplikacja
Na szczęście mamy do dyspozycji znacznie lepsze rozwiązanie. Najpierw tworzymy klasę zarządzającą zależnościami i dostarczającą prosty interface:
class HomeFacade { private door: Door; private light: Light; private temp: Temperature; constructor() { this.door = new Door(); this.light = new Light(); this.temp = new Temperature(); } // simple one method to handle complex evening mode eveningMode() { this.door.lock() this.light.adjustBrightness(70) this.temp.setTemperature(22) } // simple one method to handle complex morning mode morningMode() { this.door.unlock() this.light.adjustBrightness(100) this.temp.setTemperature(20) } }
A teraz wykorzystanie funkcjonalności przez klienta sprowadza do wywołania odpowiednich metod bez wiedzy tajemnej jak to działa “pod spodem”
const home = new HomeFacade(); // Turn on at 7 PM home.eveningMode(); // Turn on at 7 AM home.morningMode();
Bez wzorca fasady kod kliencki musiałby bezpośrednio wchodzić w interakcję z każdą składową systemu i tym samym dowiadywać się o szczegółach implementacyjnych. Dzięki fasadzie maksymalnie upraszczamy cały proces i ukrywamy to czego nie powinniśmy pokazywać na zewnątrz.
Jeśli w przyszłości ulegnie zmianie np. sposób sterowania drzwiami to osoba używająca systemu nie będzie musiała się tym przejmować
Wady i zalety wzorca
Nie ma rzeczy idealnych, podobnie jest w tym przypadku, spójrz jakie korzyści i zagrożenia niesie ze sobą ten wzorzec:
Bonus przykład z Reacta
A teraz coś dla fanów frontendu. Wzorce projektowe mają to do siebie, że najwięcej przykładów bazuje na programowaniu obiektowym. Warto jednak pamiętać, że są one także sposobem myślenia i podejściem do rozwiązywania powszechnych problemów, które można zastosować niezależnie od klas i obiektów.
Z pewnością tworząc frontendową aplikację przydarzyło Ci się stworzenie jakiegoś popupa, modal’a czy dialog boxa. Wszystkie one mają wspólną cechę którą jest zarządzanie logiką otwierania i zamykania samego siebie. Dobrą praktyką jest wydzielenie osobnego hooka, aby nie duplikować tej logiki w wielu komponentach.
Spójrzmy jak taki hook może wyglądać:
function usePopup() { const [isOpened, setIsOpened] = useState(false); const openPopup = () => setIsOpened(true); const closePopup = () => setIsOpened(false); return { isOpened, openPopup, closePopup }; }
I możesz teraz zastanawiać się co to ma do fasady? Całkiem sporo, zauważ że logika otwierania i zamykania Popupa (mimo, że bardzo prosta) jest szczegółem implementacyjnym i osoba używająca tego hooka nie powinna się nią przejmować.
Zamiast tego hook eksportuje “interface” pod postacią obiektu i daje do dyspozycji 2 konkretne metody: openPopup, closePopup oraz jedno pole: isOpened. Dzięki temu użytkownik nie musi się zastanawiać jak sprawić, aby popup się otworzył oraz który stan zaktualizować i w jaki sposób.
Podsumowanie
W dzisiejszym wpisie obejrzeliśmy fasadę z dwóch stron zarówno w kontekście backendowym jak i od strony frontendu. Ten wzorzec jest niezwykle uniwersalny, a okazje do jego zastosowania kryją się na każdym rogu aplikacji.
Zawsze, gdy myślimy o tym patternie możemy mieć w głowie te 2 zdania:
Z punktu widzenia osoby korzystającej z naszych klas i funkcjonalności nie jest istotne ile i jakich metod używamy do wykonania czegoś ani jakie zależności są do tego potrzebne. Celem klienta jest rozwiązanie swojego problemu np. poprzez wywołanie tylko jednej metody