Hura! Mamy wreszcie przyjemność udostępnić wam polską wersję! Zapraszamy do przesyłania wiadomości z waszymi uwagami i informacjami o zauważonych błędach.

Metoda szablonowa

Znany też jako: Template Method

Cel

Metoda szablonowa to behawioralny wzorzec projektowy definiujący szkielet algorytmu w klasie bazowej, ale pozwalający podklasom nadpisać pewne etapy tego algorytmu bez konieczności zmiany jego struktury.

Wzorzec projektowy Metoda szablonowa

Problem

Wyobraź sobie, że tworzysz aplikację zbierającą dane, która analizuje dokumenty w korporacji. Użytkownicy przesyłają do niej dokumenty w różnych formatach (PDF, DOC, CSV), a ta próbuje wydobyć z nich istotne dane i przedstawić je w jednym formacie.

Pierwsza wersja aplikacji mogła współpracować tylko z plikami DOC, kolejna wersja dodała obsługę plików CSV. Miesiąc później aplikacja “umiała” już ekstrahować dane z PDF.

Klasy eksploracji danych zawierały sporo powtarzającego się kodu

Klasy eksploracji danych zawierały sporo powtarzającego się kodu.

W jakimś momencie zauważasz, że wszystkie trzy klasy mają sporo podobnego kodu. Fragmenty odpowiedzialne za pracę z różnymi formatami danych są bardzo odmienne, ale kod odpowiedzialny za obróbkę i analizę danych jest niemal identyczny. Świetnie byłoby się tych powtórzeń pozbyć nie naruszając przy tym struktury algorytmów, prawda?

Istniał jeszcze jeden problem, dotyczący kodu klienckiego który korzystał z tych klas. Miał pełno instrukcji warunkowych służących wybieraniu odpowiedniego sposobu działania zależnie od klasy obiektu przetwarzającego. Jeśli wszystkie trzy klasy przetwarzające miałyby jeden wspólny interfejs lub wspólną klasę bazową, byłoby możliwe usunięcie instrukcji warunkowych w kodzie klienckim i zastosowanie polimorfizmu podczas wywoływania metod obiektu przetwarzającego.

Rozwiązanie

Rozwiązanie zawarte we wzorcu Metody szablonowej zakłada rozdzielenie algorytmu na kolejne etapy, utworzenie z tych etapów metod i umieszczenie ciągu wywołań poszczególnych metod w jednej metodzie szablonowej. Etapy mogą być albo abstrakcyjne, albo posiadać jakąś domyślną implementację. Aby skorzystać z algorytmu, klient powinien dostarczyć swoją podklasę implementującą wszystkie etapy abstrakcyjne i nadpisać opcjonalne etapy jeśli jest taka potrzeba (oprócz samej metody szablonowej).

Zobaczmy jak się to sprawdzi w naszej aplikacji do eksploracji danych. Możemy stworzyć klasę bazową dla wszystkich trzech algorytmów parsowania. Ta klasa definiuje metodę szablonową składającą się z ciągu wywołań różnych etapów przetwarzania dokumentu.

Metoda szablonowa definiuje szkielet algorytmu

Metoda szablonowa dzieli algorytm na etapy, umożliwiając podklasom ich nadpisywanie, ale nie samą metodę szablonową.

Na początek możemy zadeklarować wszystkie etapy jako abstrakcyjne, zmuszając podklasy do ich zaimplementowania. W naszym przypadku podklasy już posiadają konieczne implementacje, więc jedyną rzeczą jaka pozostaje do zrobienia jest dostosowanie sygnatur metod tak, aby zgadzały się z metodami klasy bazowej.

A teraz zobaczmy, co da się zrobić by zlikwidować duplikacje kodu. Kod otwierania/zamykania pliku oraz ekstrakcji/parsowania wydaje się różnić pomiędzy formatami danych, więc nie ma sensu się tymi metodami zajmować. Jednakże implementacja pozostałych etapów, jak analiza surowych danych i układania raportów, jest bardzo podobna. Można więc przenieść te fragmenty do klasy bazowej, dzięki czemu podklasy będą mogły je współdzielić.

Jak widać, mamy dwa rodzaje etapów:

  • etapy abstrakcyjne, które trzeba zaimplementować w każdej podklasie
  • etapy opcjonalne, które mają już jakąś domyślną implementację, ale nadal mogą być nadpisane, jeśli zaistnieje taka potrzeba

Istnieje jednak jeszcze jeden rodzaj etapów, zwane hookami. Hook to opcjonalny, pusty etap. Metoda szablonowa będzie działać nawet jeśli nie nadpisze się hooków. Zazwyczaj umieszcza się je przed i po istotnych etapach algorytmu, dając tym samym podklasom dogodne punkty zaczepienia, mogące służyć rozbudowie algorytmu.

Analogia do prawdziwego życia

Seryjna produkcja domów

Typowy projekt architektoniczny można nieco zmienić by zaspokoić potrzeby klienta.

Opisywane tu podejście można zastosować w seryjnej budowie domów. Projekt architektoniczny standardowego domu może mieć wiele punktów zaczepienia, które pozwolą potencjalnemu właścicielowi dostosować finalną budowlę do swoich potrzeb.

Każdy etap budowy, jak kładzenie fundamentów, szkielet, wznoszenie ścian, instalację wodno-kanalizacyjna i elektryczną, itd., można nieco zmodyfikować, czyniąc dom odmiennym od reszty.

Struktura

Struktura wzorca projektowego Metoda szablonowaStruktura wzorca projektowego Metoda szablonowa
  1. Klasa Abstrakcyjna deklaruje metody stanowiące etapy algorytmu oraz faktyczną metodę szablonową która wywołuje poszczególne etapy w określonej kolejności. Etapy mogą być zadeklarowane jako abstrakcyjne, albo posiadać domyślną implementację.

  2. Konkretne Klasy mogą nadpisać każdy z etapów, oprócz samej metody szablonowej.

Pseudokod

W poniższym przykładzie, wzorzec Metoda Szablonowa daje “szkielet” dla różnorakich gałęzi sztucznej inteligencji w prostej grze strategicznej.

Struktura przykładu użycia wzorca Metoda szablonowa

Klasy SI prostej gry komputerowej.

Wszystkie rasy mają niemal takie same typy jednostek i budynków. Możesz więc ponownie wykorzystać tę samą strukturę SI w poszczególnych rasach, nadpisując pewne szczegóły. Dzięki takiemu podejściu SI orków czyni je agresywniejszymi, zaś ludzie charakteryzują się bardziej defensywną postawą. Ponadto, potwory nie potrafią wznosić budynków. Dodanie kolejnej rasy do gry wymagałoby stworzenia nowej podklasy SI i nadpisania domyślnych metod zadeklarowanych w klasie bazowej SI.

// Klasa abstrakcyjna definiuje metodę szablonową która zawiera
// szkielet jakiegoś algorytmu skomponowany w formie zestawu
// wywołań prostych, abstrakcyjnych działań. Konkretne podklasy
// implementują te działania, ale pozostawiają samą metodę
// szablonową nietkniętą.
class GameAI is
    // Metoda szablonowa definiuje szkielet algorytmu.
    method turn() is
        collectResources()
        buildStructures()
        buildUnits()
        attack()

    // Niektóre etapy mogą zostać zaimplementowane już w klasie
    // bazowej.
    method collectResources() is
        foreach (s in this.builtStructures) do
            s.collect()

    // A inne mogą być zdefiniowane jako abstrakcyjne.
    abstract method buildStructures()
    abstract method buildUnits()

    // Klasa może mieć wiele metod szablonowych.
    method attack() is
        enemy = closestEnemy()
        if (enemy == null)
            sendScouts(map.center)
        else
            sendWarriors(enemy.position)

    abstract method sendScouts(position)
    abstract method sendWarriors(position)

// Konkretne klasy muszą zaimplementować wszystkie abstrakcyjne
// działania klasy bazowej, ale nie mogą nadpisać samej metody
// szablonowej.
class OrcsAI extends GameAI is
    method buildStructures() is
        if (there are some resources) then
            // Buduj farmy, potem koszary, potem twierdzę.

    method buildUnits() is
        if (there are plenty of resources) then
            if (there are no scouts)
                // Buduj piechura, dodaj go do grupy zwiadowców.
            else
                // Buduj żołnierza, dodaj go do grupy
                // wojowników.

    // ...

    method sendScouts(position) is
        if (scouts.length > 0) then
            // Wyślij zwiadowców na pozycję.

    method sendWarriors(position) is
        if (warriors.length > 5) then
            // Wyślij wojowników na pozycję.

// Podklasy mogą też nadpisać niektóre działania domyślną
// implementacją.
class MonstersAI extends GameAI is
    method collectResources() is
        // Potwory nie zbierają zasobów.

    method buildStructures() is
        // Potwory nie wznoszą budowli.

    method buildUnits() is
        // Potwory nie budują jednostek.

Zastosowanie

Stosuj wzorzec Metoda szablonowa gdy chcesz pozwolić klientom na rozszerzanie niektórych tylko etapów algorytmu, ale nie całego, ani też jego struktury.

Metoda szablonowa pozwala zmienić monolityczny algorytm w ciąg pojedynczych etapów, które można następnie łatwo rozszerzać w podklasach bez naruszania struktury opisanej w klasie bazowej.

Wzorzec ten jest przydatny gdy masz wiele klas zawierających niemal identyczne algorytmy różniące się jedynie szczegółami.  W takiej sytuacji bowiem konieczność modyfikacji algorytmu skutkuje koniecznością modyfikacji wszystkich klas.

Zmieniając taki algorytm w metodę szablonową możesz także przenieść jego etapy o podobnej implementacji do klasy bazowej, eliminując tym samym duplikację kodu. Kod który różni się pomiędzy podklasami, może w nich pozostać.

Jak zaimplementować

  1. Przeanalizuj algorytm docelowy pod kątem możliwego podziału na etapy. Rozważ które z nich są wspólne dla wszystkich podklas, a które zawsze będą unikalne.

  2. Stwórz abstrakcyjną klasę bazową i zadeklaruj metodę szablonową oraz zestaw abstrakcyjnych metod reprezentujących etapy algorytmu. Nakreśl strukturę algorytmu w metodzie szablonowej poprzez uruchamianie odpowiednich etapów. Rozważ zastosowanie słowa kluczowego final w stosunku do metody szablonowej aby zapobiec nadpisaniu jej przez podklasy.

  3. Może się zdarzyć, że wszystkie etapy pozostaną abstrakcyjnymi. Jednak niektóre z nich skorzystałyby na posiadaniu domyślnej implementacji. Podklasy nie muszą implementować tych metod.

  4. Zastanów się nad dodaniem hooków pomiędzy kluczowymi etapami algorytmu.

  5. Dla każdego wariantu algorytmu stwórz nową konkretną podklasę. Musi ona implementować wszystkie abstrakcyjne etapy, ale może także nadpisać część opcjonalnych.

Zalety i wady

  • Można pozwolić klientom nadpisać tylko niektóre partie dużego algorytmu czyniąc go odporniejszym na szkody wskutek zmian poszczególnych jego części.
  • Można przenieść powtarzający się kod do klasy bazowej.
  • Dla niektórych klientów przygotowany szkielet algorytmu może stanowić ograniczenie.
  • Może prowadzić do naruszenia Zasady podstawienia Liskov wskutek stłumienia domyślnych implementacji etapów w podklasach.
  • Metody szablonowe zwykle trudniej utrzymywać w miarę jak przybywa etapów.

Powiązania z innymi wzorcami

  • Metoda wytwórcza to wyspecjalizowana Metoda szablonowa. Może stanowić także jeden z etapów większej Metody szablonowej.

  • Metoda szablonowa polega na mechanizmie dziedziczenia: pozwala zmieniać części algorytmu rozszerzając je w podklasach. Strategia bazuje na kompozycji: można zmienić część zachowania obiektu poprzez nadanie mu różnych strategii odpowiadających temu zachowaniu. Metoda szablonowa działa na poziomie klasy, więc jest statyczna. Strategia działa na poziomie obiektu, więc pozwala przełączać zachowania w trakcie działania programu.

Przykłady kodu

Wzorce projektowe: Metoda szablonowa w języku Java Wzorce projektowe: Metoda szablonowa w języku C# Wzorce projektowe: Metoda szablonowa w języku C++ Wzorce projektowe: Metoda szablonowa w języku PHP Wzorce projektowe: Metoda szablonowa w języku Python Wzorce projektowe: Metoda szablonowa w języku Ruby Wzorce projektowe: Metoda szablonowa w języku Swift Wzorce projektowe: Metoda szablonowa w języku TypeScript Wzorce projektowe: Metoda szablonowa w języku Go