W programowaniu jedną z najważniejszych umiejętności jest rozwiązywanie problemów. Możemy pisać prosty kod, który spełnia bardzo proste funkcje i nigdy nie napotkać na większe niedogodności, ale w miarę gdy nasz kod rośnie, z pewnością musimy się zmierzyć z pewną logiczną organizacją.
Im bardziej skomplikowany jest nasz kod, tym więcej uwagi musimy poświęcić na jego odczytanie, zrozumienie i modyfikację. O ile nie jesteśmy właśnie na początku naszej drogi z daną aplikacją, na pewno możemy rozpoznać, czy dany kod (nasz lub pisany przez kogoś innego) jest używalny (ang. maintainable) i czy pozwala na rozwój funkcjonalności.
Stosowanie wzorców projektowych, jak te przedstawione poniżej pomogą nam nie tylko umiejętnie pisać złożone aplikacje, ale także odczytywać obcy kod i pisać własny tak, by inni mogli go szybko opanować i zastosować. Gang Of Four, czyli 4 autorów książki pt. „Design Patterns: Elements of Reusable Object-Oriented Software” opisało dla nas najczęściej używane wzorce, które przedstawię poniżej wg 3 głównych grup.
Wzorce kreacyjne
Wzorce kreacyjne pomagają nam w rozmaitych sytuacjach tworzenia obiektów i nadzorowania pracy nad nimi. To dzięki wzorcom kreacyjnym możemy strategicznie rozplanować strukturę aplikacji lub jej modułów. Tak jak w fabryce: każdy wytarzany produkt musi być wysokiej jakości i odpowiadać normom wytwórczym.
Metoda Wytwórcza (Factory Method)
Metoda wytwórcza świetnie oddaje charakter fabryczny języka programowania obiektowego. Choć pracujemy w jednej fabryce i produkujemy różne produkty, metoda, z jaką je wytwarzamy jest podobna. Nasze linie produkcyjne przynależą do jednej fabryki i muszą działać zgodnie z jej wytycznymi a produkty opuszczające linię produkcyjną muszą powstawać w podobny sposób.
W świecie realnym taki przypadek może dotyczyć fabryki, która zorientowana jest na jedną grupę produktów ,np. obuwie. Zmieniamy kolorystykę, zmieniamy modele, ale ciągle poruszamy się w obrębie konkretnej metody wytwarzania i podobnego efektu. Mówiąc językiem biznesowym: specjalizujemy się w konkretnej charakterystyce i różnych produktach jednej kategorii.
Przykład: z pomocą klasy abstrakcyjnej tworzymy ogólne pojęcie kreatora – klasy, która organizuje klasy wytwórcze. Sposób w jaki tworzymy obiekty musi więc przebiegać według określonego wzorca. Tworzymy oczywiście klasy dla samych produktów, ale powinny one spełniać też wymaganie jednorodności, będąc przypisanymi do tego samego interfejsu.
Fabryka Abstrakcyjna (Abstract Factory)
Im bardziej rozrasta nam się nasza fabryka, dodając do niej kolejne linie produkcyjne (np. oprócz butów chcemy produkować torebki damskie), tym bardziej abstrakcyjnie musimy podejść do tematu wytwarzania. Nie chodzi już tylko o to w jaki sposób wytwarzamy produkty o podobnej charakterystyce, ale w ogóle o to jak prowadzimy linie produkcyjne.
We wzorcu fabryki abstrakcyjnej mnożą nam się metody wytwórcze i interfejsy, które rozróżniają grupy produktów. Możemy organizować nasze linie produkcyjne tak, by współdziałały w ramach jednej aplikacji, ale były niezależne. To, co łączy ten cały organizm to właśnie fabryka, która definiuje wytwarzanie w ogóle.
Przykład: inicjując produkcję konkretnej fabryki nie musimy wiedzieć co leży pod spodem. Naszym celem jest wyprodukowanie konkretnych produktów. Co więcej, możemy korzystać z dobrodziejstw wiązania produktów w zestawy i włączać konkretny produkt do procesu produkcji innego produktu. W ten sposób z naszej fabryki wyjedzie zarówno torebka damska jak i buty z tej samej kolekcji.
Budowniczy (Builder)
Ten wzorzec projektowy skupia się na procesie wytwórczym. Produkując buty możemy określić na początku wiele parametrów, takich jak kolor, rodzaj podeszwy czy rozmiar, ale im więcej tych zmiennych, tym bardziej nasze oczekiwania wobec wytwarzania się komplikują.
Z pomocą dla naszej fabryki obuwia przychodzi wzorzec Budowniczego. Pozwala on zarządzać sposobem produkcji przez definicję etapów budowy oraz wskazanie tzw. Kierownika – klasy do zarządzania produkcją. Dzięki takiej organizacji możemy wytwarzać różne warianty produktów zachowując jednocześnie kontrolę nad niuansami pojedynczych etapów / części.
Przykład: Wzorzec Budowniczego to bardziej skrupulatne podejście do konkretnego obiektu, który wypuszczamy z naszych rąk. Choć efekt nazywamy tak samo, jego składowe różnią się od siebie. Sposób wytwarzania jest jednorodny i umożliwia dodawanie kolejnych etapów, ale to my na końcu decydujemy, których etapów używamy w konkretnym kontekście.
Prototyp (Prototype)
Każdy z nas słyszał kiedyś to słowo. Prototyp, zwany także szkicem lub makietą, to generalny wzorzec wytwarzania, dzięki czemu linia produkcyjna może się pochwalić identyczną jakością swoich produktów.
W programowaniu obiektowym w PHP prototypem nazywamy możliwość klonowania obiektów zachowując właściwości nadane im podczas budowy prototypu. W skrócie: każdy wytwarzany produkt powinien być identyczny pod względem założeń i zastosowania.
Przykład: Wzorzec Prototypu jest tak naprawdę innym określeniem klonowania. Używając polecenie clone możemy w łatwy sposób wypuścić kolejny egzemplarz, który posiada takie same właściwości i powiązania jak oryginał.
Singleton
Wzorzec Singleton jest zaprzeczeniem sformułowania znanego z SOLID o pojedynczej odpowiedzialności klasy (Single Responsibility Principle). To globalnie dostępny obiekt, który z założenia zawsze powinien być zwracany tak samo. W świecie aplikacji bazodanowych to może być np. połączenie z bazą danych (jeśli aplikacji nie pozwala na użycie wielu połączeń).
Wzorzec Singleton jest często trywializowany, gdyż istnieje ryzyko używania go do wielu przypadków pojedynczej bazy globalnej. Jeśli korzystamy z ogólnej konfiguracji dla wielu modułów, mógłby on spowodować, że urośnie nam liczba klas wykonujących podobne zadania, a jednak zwracających inne dane.
Przykład: Nasza fabryka obuwia mogłaby mieć określony harmonogram przerw w produkcji, na przykład po to by ostudzić rozgrzane maszyny. Taki harmonogram mógłby być wspólny dla wszystkich linii i dostępny z każdego miejsca.
Wzorce strukturalne
O ile wzorce kreacyjne pomagają nam przygotować się do produkcji rozmaitych rzeczy, o tyle wzorce strukturalne pozwalają nam zapanować nad relacjami pomiędzy obiektami.
Adapter
Adapter to tak naprawdę nakładka, która pozwala użyć istniejącej produkcji obiektów do nowego zastosowania (efektu końcowego). Nie zmieniamy tutaj oryginalnego obiektu, ani sposobu jego budowy, ale dodajemy warstwę zewnętrzną, która umożliwia użycie go w nowym kontekście.
Przykład: W przypadku fabryki obuwia może to być dodatkowy etap produkcji nalepiania na obuwie odblasków. Nie zmieniamy w ogóle linii produkcyjnej, a jedynie dodajemy coś do już wytworzonego obiektu.
Most (Bridge)
W produkcji pozornie niezwiązanych ze sobą tworów może dojść do sytuacji, w której muszą się one ze sobą skomunikować. Potrzeba łączenia występuje często w aplikacjach, w których narosło wiele podklas współdzielących podobne właściwości lub metody. Zasada działania jest prosta i podobna do elementu Kierownika we wzorcu Budowniczego. Dla obiektów o wspólnych cechach możemy zdefiniować tzw. Abstrakcję, czyli warstwę kontrolującą jednorodność przebiegu operacji oraz Implementację – warstwę ujednolicającą efekt operacji.
Ten pozornie skomplikowany wzorzec zakłada, że skupiamy się na powiązaniu funkcji różnych obiektów kontrolując, by powielały ten sam schemat. Decydując się na użycie części implementacyjnej, możemy rozwijać zastosowanie tylko części obiektów w innej formie. Całość jednak tworzy warstwę, która ma konkretne ramy.
Przykład: fabryka obuwia już działa jak trzeba, skupmy się więc na marketingu naszych produktów. Choć publikujemy reklamy w prasie, telewizji i w Internecie, sposób przygotowania reklam jest podobny. Choć reklamami wideo zajmuje się inny dział niż PR, forma (reklama) posiada cechy wspólne i podobną implementację (media). W każdej reklamie potrzebny nam jest Copywriter i akceptacja kierownictwa, ale efekt jest różny.
Kompozyt (Composite)
Kompozyt to wzorzec często spotykany we frameworkach. Jego zasada działania przypomina wydzielanie gałęzi w strukturze drzewiastej. Możemy posługiwać się konkretnym modułem naszej aplikacji (np. działem) i dodawać do procesu kolejne etapy (komponenty).
Efektem użycia wzorca Kompozytu jest unikalny zestaw prefabrykatów – elementów, które pochodzą z różnych źródeł, ale reprezentują jedną hierarchię. Najprostszym odniesieniem może być drzewo tagów html – choć są one różne i prezentują różny charakter, razem tworzą spójną całość i strukturę drzewiastą. W Kompozycie klasa nadrzędna deleguje zadania na obiekty podrzędne, dzięki czemu każdy z nich ma sprecyzowane zadanie do wykonania.
Przykład: Linia produkcyjna obuwia może działać jak Kompozyt. Programujemy w niej zadania, które każda z maszyn ma spełnić w określonym procesie: budowa cholewki, podeszwy, zamka lub wiązania, lakierowanie itp. Efektem są różne modele obuwia, ale każde z zadań przypisywane jest do obiektu na podobnej zasadzie.
Dekorator (Decorator)
Wzorzec Dekoratora pojawia się często w refaktoringu kodu. Dostajemy zadanie wykorzystania obecnych składowych w nowym kontekście lub połączeniu składowych dla skupienia większej ilości zadań w jednym procesie. Dekorator umożliwia takie podejście poprzez dodanie nadrzędnej metody kontrolującej.
Przykład: Przyjmijmy, że w naszej fabryce obuwia pracujemy nad produktami bez kontroli jakości. Buty, które wychodzą z linii produkcyjnej różnią się pod względem grubości pokrycia farbą. Jeśli użyjemy wzorca Dekoratora, możemy sprawdzić grubość farby i tym samym wysłać obuwie do domalowania. Nie pozwolimy przecież, by nasz wadliwy produkt trafił w ten sposób do sklepów, prawda?
Fasada (Facade)
Jeśli kiedykolwiek spotkaliście się z wymaganiem integracji jakiegoś api do aplikacji z pewnością przyszło Wam na myśl: jak to ze sobą połączyć? Mnogość funkcji w obcym API oraz niekompatybilność z naszym kodem sprawiła, że zamiast integracji wytworzyliśmy sobie ból głowy. Niepotrzebnie, to właśnie wzorzec fasady ma za zadanie obsługiwać zewnętrzną strukturę tak, by odpowiadała tej naszej – już istniejącej.
Jeśli w przykładowym API istnieje metoda publishJson, a u nas jest writeJsonFile, możemy połączyć te konteksty definiując metodę w fasadzie eksportującą dla nas API w zrozumiały sposób.
Przykład: Fasada sama w sobie nie jest klasą pracującą. Ma za zadanie wydelegować istniejące funkcjonalności do innego systemu. W przypadku fabryki obuwia może to być system obsługi zgłoszeń klientów z różnych źródeł, który drukuje je na drukarce firmowej i przekazuje na biurko działu zajmującego się reklamacjami. Różne pochodzenie, a jedna delegacja.
Wzorce behawioralne
Wzorce behawioralne regulują zachowania pomiędzy obiektami. Dzięki użyciu tych wzorców zyskujemy klarowny obraz relacji pomiędzy nimi i delegacji odpowiedzialności.
Łańcuch zobowiązań (Chain of responsibility)
Łańcuch zobowiązań łatwo przyrównać do proceduralnego stylu programowania, w którym kolejne etapy warunkowane były w określony sposób. Logowanie w aplikacji lub dostęp do określonych funkcjonalności to częste przypadku użycia łańcucha zobowiązań, który po drodze musi sprawdzić kilka, często niezależnych od siebie, warunków.
Wzorzec ten zakłada, że każdy etap (obiekt/proces o różnym pochodzeniu) powinien mieć tożsamy element łańcucha. Gdy spojrzymy na łańcuch do zabezpieczenia roweru przed kradzieżą, każde oczko jest takie same, a o sile łańcucha decyduje siła najsłabszego ogniwa. Łańcuchy często spotykane są we frontendowych językach programowania, gdzie pracuje się na drzewie elementów. Jeśli ogniwo X jest zgodne z wymaganiami, możemy przejść do następnego, podobnie jak w przypadku Iteratora.
Przykład: chcąc sprzedać nasze obuwie robocze dużej firmie, musimy pokonać pewien łańcuch decyzyjny. Od sprzedawcy do dyrektora dzieli nas kilka poziomów, choć każdy z nich musi wyrazić zgodę lub odrzucić nasz wniosek. Czasem nawet przeskoczymy od manager już do dyrekcji, ale o tym decyduje konkretny etap.
Polecenie (Command)
Frontendowi programiści z pewnością znają problem, gdy wiele pozornie identycznych elementów kontrolnych, jak przycisk czy pole tekstowe wykonuje różne zadania. Przycisk może wywołać okno dialogowe, zamknąć okno, pobrać dane z API lub wstawić zawartość do okna edytora. Wzorzec Polecenia pozwala odseparować żądanie danego obiektu od tego, co leży pod każdym obiektem wytwórczym.
Przykład: na tablicy kontrolnej linii produkcyjnej obuwia są różne przyciski. Choć każdy z nich wygląda podobnie, ma przypisane inne zadanie. Zanim jednak żądanie trafi do odpowiedniej maszyny, zostaje zbierane na tablicy kontrolnej. Jest to szczególnie przydatne, gdy etapem pośredniczącym ma być sprawdzenie, czy komenda w ogóle może być wykonana. Nie chcemy przecież zatrzymać produkcji, gdy wszystko idzie poprawnie, prawda?
Iterator
Iterator jest wzorcem pomocniczym, który zamienia serie obiektów w możliwe do nawigacji: następny, obecny, poprzedni. Dzięki temu wzorcowi ujarzmimy wiele obiektów, które chcemy sprawdzać pojedynczo. Ma on wiele cech wspólnych z typem tablicy, jednak nie ujawnia tego, co znajduje się pod spodem każdego elementu.
Przykład: kontrola jakości każdego obuwia nie polega tylko na sprawdzaniu tego co mamy pod ręką. Jeśli uznamy, że kilka par wyszło spod maszyny z wada, możemy iść do następnego elementu lub poprzedniego, by zweryfikować nasze założenia niezależnie, czy sprawdzamy jesienne kozaki, pantofle czy buty sportowe.
Obserwator (Observer)
Obsługa eventów w aplikacji to jeden ze smaczków, który powoduje, że aplikacja staje się naprawdę atrakcyjna. Obserwator, jak nazwa wskazuje, zajmuje się wypatrywaniem akcji, które wymagają powiadamiania innych komponentów aplikacji o swoich zamiarach.
Jednym z czołowych przykładów działania mechanizmu eventów jest powiadamianie w ogóle. Możemy dać znać o wykonanej czynności za pomocą SMSa, maila czy push w przeglądarce. Choć efekt jest różny dla każdego typu powiadomień, aby mogły się w ogóle zadziać wymagane jest działanie obserwatora, który je wyłapie i subskrybentów, którzy mają coś do powiedzenia.
Przykład: Monitorując pracę naszej fabryki obuwia chcielibyśmy wiedzieć, kiedy ukończona zostanie produkcja ilości umożliwiającej zapakowanie produktów na ciężarówkę. Jeśli mechanizm liczący dowie się o osiągnięciu takiej partii powinien poinformować logistykę, że magazyn jest gotowy do wydania oraz biuro, że nastąpi procedura wydania z magazynu.
Stan (State)
Zmiana stanu obiektu może przypominać działanie funkcji switch, która w zależności od parametru, wykonuje określone zadanie. Im więcej wartości tego parametru, tym więcej akcji dopisywanych do instrukcji switch. Aby uniknąć tegoproblemu, warto wprowadzić segregację stanów w osobnych klasach, które reprezentują ten sam mechanizm – zmianę stanu obiektu.
Przykład: faktury, które wystawiamy w księgowości naszej fabryki obuwia mogą mieć różny stan i w zależności od niego wykonać rozmaite działania. Choć wolimy, by faktura została opłacona i wrzucono ją do archiwum, to w przypadku nieotrzymania należności warto skontaktować się z działem wierzytelności i odzyskać pieniądze za wykonaną pracę.
Strategia (Strategy)
Strategie dzielą się na dobre lub złe, ale na pewno każda z nich w tym samym kontekście powinna spełniać jednolite wymagania. Jeśli będziemy definiować strategię w momencie inicjalizacji obiektu, może się okazać, że kroki każdej ze strategii różnią się, choć mają wykazać podobny efekt. Z pomocą przychodzi wzorzec strategii nadrzędne, która dba o pomyślny przebieg niezależnie od indywidualnego podejścia strategii podrzędnej.
Interfejs strategii dba o to, by strategie konkretne implementowały te same mechanizmy. W klasie nadrzędnej zaś możemy posłużyć się dodatkową logiką biznesową do wykonania instrukcji wspólnych.
Przykład: Planowanie produkcji w fabryce to często powtarzalny proces. Choć maszyny uczestniczące w wytworzeniu obuwia są różne, efekt końcowy musi być identyczny – z taśmy ma zjechać para obuwia.
Metoda szablonowa (Template Method)
Metoda szablonowa to nic innego jak zaprogramowanie instrukcji do wykonania w klasie nadrzędnej, a następnie skierowanie naszej uwagi do instrukcji w klasach podrzędnych. Klasa nadrzędna pełni także funkcję szablonu, który definiuje, jakie funkcje mają się wykonać w podklasach. Możemy zdecydować, które metody nadrzędne zostaną nadpisane, jeśli pozwala na to definicja klasy nadrzędnej.
Przykład: Produkując różne warianty obuwia możemy wprowadzać innowacje, jednak pewne elementy procesu pozostają bez zmian. Choć produkujemy obuwie sportowe z poszyciem oddychającym, możemy nadpisać tę regułę i wypuścić obuwie nieprzemakalne, stosując do tego dodatkowe instrukcje.
Odwiedzający (Visitor)
Ostatnim ze wzorców, który nas interesuje jest Odwiedzający. To klasa zbudowana obok istniejącej architektury, która wykorzystuje potencjał już napisanego kodu. Nie jest sztuką ciągła zmiana tego, co już napisaliśmy. Sztuką jest takie wykorzystanie istniejącego kodu, by implementować go na różne sposoby.
Visitor, czyli obiekt odwiedzający inne obiekty ma za zadanie „odwiedzić” inny obiekt zgodny z interfejsem odwiedzin i wykonać w nim operacje specyficzne dla jego natury. Przypomina to nieco pracę z bazą danych, gdzie łączymy tabele, by otrzymać zależne dane i wykonać na nich działania ciągle posługując się głównym obiektem.
Przykład: rutynowe kontrole w naszej fabryce wymagają konkretnego dostępu. Jeśli w każdym dziale istnieje taka sama polityka dostępu do dokumentacji, osoba wydelegowana do zarządzania tą dokumentacją przygotuje nam konkretny, spójny raport.
Omówiliśmy powyżej najczęściej stosowane wzorce projektowe w PHP. Może się z początku wydawać, że wiele z nich ma podobną charakterystykę, lub strukturę. Warto przećwiczyć na realnych przykładach każdy z tych wzorców by umieć je odróżnić i prawidłowo zastosować w naszym kodzie.
Celowo pominęliśmy kilka wzorców, które są używane rzadko i nie przesądzają o naszej znajomości wzorców. Oto one w skrótowej formie:
- Wzorce strukturalne: Pyłek – jeśli w wielu obiektach powtarza się jakaś cecha, wydzielamy ją do osobnej klasy dla optymalizacji pamięci.
- Wzorce strukturalne: Pełnomocnik – gdy nie chcemy odrywać oryginalnego obiektu możemy wystosować reprezentanta, który komunikuje się z aplikacją.
- Wzorce behawioralne: Mediator – to taka wieża kontroli lotów, która komunikuje między sobą obiekty, by wyprodukować określone zadania (np. powiadomienia, walidację itp.)
- Wzorce behawioralne: Pamiątka – wydzielenie migawki stanu obiektu do osobnej klasy podobnie jak w wersjonowaniu plików.