Przedstawiam Wam kolejny artykuł o wzorcach projektowych, w którym tym razem zajmiemy się wzorcem Metoda Szablonowa (ang. Template Method).

Oczywiście zacznę standardowo od definicji :

Wzorzec Metoda Szablonowa definiuje szkielet danego algorytmu w określonej metodzie, przekazując realizację niektórych jego kroków do klas podrzędnych. Wzorzec ten pozwala klasom podrzędnym na redefiniowanie pewnych kroków algorytmu, ale jednocześnie uniemożliwia zmianę jego struktury.Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra,

„Rusz głową! Wzorce Projektowe”

W skrócie: korzystając ze wzorca Metoda Szablonowa definiuje się kolejne kroki danego algorytmu pozwalając klasom podrzędnym na dostarczanie implementacji dla jednego lub więcej z nich.

Rola tego wzorca polega na utworzeniu szkieletu do implementacji danego algorytmu. Szkielet ten (czy, jak kto woli, szablon) jest specjalną metodą definiującą dany algorytm jako zestaw kolejnych kroków, przy czym co najmniej jeden z nich musi zostać zadeklarowany jako metoda abstrakcyjna, więc implementacją zajmują się klasy podrzędne (jak wspomniałem wyżej).

Dzięki zastosowaniu takiego rozwiązania możemy mieć pewność, że struktura algorytmu nie zostanie naruszona, a klasy pochodne będą miały możliwość przesłaniania określonych kroków implementacji.

Przejdźmy teraz do praktycznego przykładu, który podobnie, jak w poprzednim artykule, przedstawię w języku Java .

Metoda Szablonowa — praktyczny przykład w języku Java

W naszym przykładzie obszarami zainteresowania będą automaty do produkcji kanapek. Pierwszy z nich wytwarza kanapki z grillowanym kurczakiem, drugi natomiast zajmuje się kanapkami z grillowanym serem.

W tym przypadku metodą szablonową będzie metoda makeSandwich(), która jak sama nazwa wskazuje, służy do utworzenia kanapki. Jest ona zadeklarowana jako final, dzięki czemu klasy pochodne nie mają możliwości jej przesłonięcia, jak również modyfikacji sekwencji poszczególnych kroków algorytmu.

public abstract class SandwichMachine {
    public final void makeSandwich() {
        getBreadstuff();
        getIngredients();
        if (doesClientWantSauce()) {
            addSauce();
        }
        bake();
    }

    protected abstract void getBreadstuff();

    protected abstract void getIngredients();

    protected abstract void addSauce();

    protected final void bake() {
        System.out.println("Podpiekanie kanapki (2-3 min.)");
    }

    protected boolean doesClientWantSauce() {
        return false;
    }
}
 

Poszczególne kroki algorytmu są określane mianem operacji elementarnych. Według definicji, minimum jedna z nich musi być abstrakcyjna. Tutaj mamy trzy takie operacje. Jest też rzeczywista operacja elementarna (metoda bake()), którą podobnie, jak metodę szablonową, zadeklarowałem jako final również po to, aby uniemożliwić podklasom jej przesłanianie.

Klasa abstrakcyjna zawiera także tzw. „hook”, nazywany również „haczykiem”. Jest nim metoda doesClientWantSauce(), która, jak można się domyślić, pyta klienta, czy chce, aby maszyna dodała sos do kanapki. Głównym przeznaczeniem haczyka jest implementacja opcjonalnej części algorytmu. Klasy podrzędne nie mają żadnych ograniczeń w ich przesłanianiu. Metoda ta może być wykorzystywana zarówno przez metodę szablonową, jak i klasy dziedziczące.

Hook posiada albo pustą, albo inną, domyślną implementację, dzięki czemu klasy pochodne mogą „podwiesić się” do realizowanego algorytmu w różnych jego punktach, o ile sobie tego życzą.

Pora na implementacje naszego szablonu. Zacznijmy od kanapek z kurczakiem.

import java.util.Scanner;

public class ChickenSandwichMachine extends SandwichMachine {
    @Override
    protected void getBreadstuff() {
        System.out.println("\nPrzygotowanie chleba...");
    }

    @Override
    protected void getIngredients() {
        System.out.println("\nDodawanie składników: grillowany kurczak, sałata, pomidor, oliwki, cebula...");
    }

    @Override
    protected void addSauce() {
        chooseSauce();
        System.out.println("\nDodawanie sosu...");
    }

    @Override
    protected boolean doesClientWantSauce() {
        String answer = AuxiliaryMethods.getAnswer();

        if (answer.toLowerCase().startsWith("t")) {
            return true;
        } else {
            return false;
        }
    }

    private void chooseSauce() {
        int choice;

        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("\nWybierz sos z poniższej listy:");
            System.out.println("1. Łagodny");
            System.out.println("2. Ostry");
            choice = scanner.nextInt();

            if (choice == 1) {
                System.out.println("Wybrano łagodny sos");
                break;
            } else if (choice == 2) {
                System.out.println("Wybrano ostry sos");
                break;
            } else {
                System.out.println("Nie ma takiej opcji!");
            }
        }
    }
}
 

A teraz spójrzmy na maszynę produkującą kanapki z serem.

import java.util.Scanner;

public class CheeseSandwichMachine extends SandwichMachine {
    @Override
    protected void getBreadstuff() {
        System.out.println("\nPrzygotowanie bułki...");
    }

    @Override
    protected void getIngredients() {
        System.out.println("\nDodawanie składników: grillowany ser, ogórek, oliwki, rzodkiewka...");
    }

    @Override
    protected void addSauce() {
        chooseSauce();
        System.out.println("\nDodawanie sosu...");
    }

    @Override
    protected boolean doesClientWantSauce() {
        String answer = AuxiliaryMethods.getAnswer();

        if (answer.toLowerCase().startsWith("t")) {
            return true;
        } else {
            return false;
        }
    }

    private void chooseSauce() {
        int choice;

        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("\nWybierz sos z poniższej listy:");
            System.out.println("1. Łagodny");
            System.out.println("2. Ostry");
            System.out.println("3. Mieszany");
            choice = scanner.nextInt();

            if (choice == 1) {
                System.out.println("Wybrano łagodny sos");
                break;
            } else if (choice == 2) {
                System.out.println("Wybrano ostry sos");
                break;
            } else if (choice == 3) {
                System.out.println("Wybrano mieszany sos");
                break;
            } else {
                System.out.println("Nie ma takiej opcji!");
            }
        }
    }
}
 

We wnętrzu haczyka skorzystałem z metody pomocniczej, która jest metodą statyczną klasy AuxiliaryMethods i ma za zadanie pobrać od użytkownika odpowiedź na pytanie, czy życzy sobie sos do kanapki.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class AuxiliaryMethods {
    public static String getAnswer() {
        String answer = null;

        System.out.println("Czy dodać sos do kanapki (t/n)?");

        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        try {
            answer = in.readLine();
        } catch (IOException e) {
            System.err.println("Błąd wejścia-wyjścia podczas odczytywania odpowiedzi");
            e.printStackTrace();
        }

        if (answer == null) {
            return "nie";
        }
        return answer;
    }
}
 

Czas na przetestowanie naszych maszyn.

Test przykładowego programu

Utworzymy tutaj obiekty odwołujące się do naszych maszyn, a następnie każdy z nich wywoła metodę makeSandwich(). I tylko tyle… albo aż tyle .
public class SandwichMachineTestDrive {
    public static void main(String[] args) {
        SandwichMachine chickenSandwichMachine = new ChickenSandwichMachine();
        SandwichMachine cheeseSandwichMachine = new CheeseSandwichMachine();

        System.out.println("Drogie maszyny, zróbcie dla mnie kanapki...");

        System.out.println("\nNajpierw kanapka z kurczakiem:");
        chickenSandwichMachine.makeSandwich();

        System.out.println("\nTeraz poproszę kanapkę z serem:");
        cheeseSandwichMachine.makeSandwich();
    }
}
 

Jaki wynik otrzymamy w konsoli?

Drogie maszyny, zróbcie dla mnie kanapki...

Najpierw kanapka z kurczakiem:

Przygotowanie chleba...

Dodawanie składników: grillowany kurczak, sałata, pomidor, oliwki, cebula...
Czy dodać sos do kanapki (t/n)?
t

Wybierz sos z poniższej listy:
1. Łagodny
2. Ostry
2
Wybrano ostry sos

Dodawanie sosu...
Podpiekanie kanapki (2-3 min.)

Teraz poproszę kanapkę z serem:

Przygotowanie bułki...

Dodawanie składników: grillowany ser, ogórek, oliwki, rzodkiewka...
Czy dodać sos do kanapki (t/n)?
n
Podpiekanie kanapki (2-3 min.)
 

Metoda Szablonowa a reguła Hollywood

Wdrażając Metodę Szablonową mamy do czynienia z bardzo ważną zależnością zwaną Regułą Hollywood, która ułatwia nam zachowanie struktury algorytmu i którą można streścić następującymi słowami:

Nie dzwoń do nas, my zadzwonimy do CiebieReguła Hollywood

Dzięki stosowaniu się do tej reguły możemy uniknąć zjawiska „fachowo” nazywanego „butwieniem drzewa zależności”. Zjawisko to pojawia się w sytuacji, gdy w systemie można znaleźć składniki wysokiego poziomu (czyli np. klasy bazowe) uzależnione od innych składników wysokiego poziomu, które z kolei są uzależnione od pewnych składników poziomów bocznych, a te są zależne od jeszcze innych składników na niskim poziomie i tak dalej.

Jak łatwo można się domyślić, w takiej sytuacji nietrudno się pogubić, badając w jaki sposób zaprojektowano daną aplikację.

Implementując regułę Hollywood, zezwalamy składnikom na niskim poziomie istnieć gdzieś w systemie, jednak to od składników wysokiego poziomu zależy kiedy i w jaki sposób mogą z nich korzystać.

Inaczej mówiąc, składniki będące na wysokim poziomie postępują ze składnikami niskiego poziomu w taki sposób, jak zazwyczaj ma to miejsce w Hollywood, czyli: „Nie dzwoń do nas, to my zadzwonimy do Ciebie”.

Reguła ta dotyczy nie tylko wzorca Metoda Szablonowa, ale również wielu innych wzorców, np. Fabryka Abstrakcyjna, który opisałem w poprzednim artykule.

Zastosowania wzorca Metoda Szablonowa

Przede wszystkim, możemy zastosować ten wzorzec, gdy chcielibyśmy, aby klienci rozszerzali tylko niektóre etapy algorytmu, ale nie cały, ani tym bardziej jego strukturę.

Po drugie: Metoda Szablonowa jest również przydatna w sytuacji, gdy posiadamy wiele klas, które zawierają niemalże identyczne algorytmy, w których różnica polega tylko na ich szczegółach. W tym wypadku modyfikacja algorytmu powodowałaby konieczność modyfikowania wszystkich klas.

Wady i zalety wzorca Metoda Szablonowa

Metoda Szablonowa, jak każdy inny wzorzec, nie jest rozwiązaniem idealnym, więc również posiada wady obok zalet.

Pozytywy i negatywy podsumowałem w poniższej tabeli.

ZaletyWady
Klienci mają możliwość nadpisania tylko niektórych części dużego algorytmu, co czyni go bardziej odpornym na usterki spowodowane zmianami jego poszczególnych częściPrzygotowany szablon algorytmu może równać się ograniczeniu dla niektórych klientów
Powtarzający się kod może zostać przeniesiony do klasy nadrzędnejMoże zostać naruszona Zasada podstawienia Liskov z powodu przytłumienia domyślnych implementacji etapów w klasach podrzędnych
Trudność utrzymania metod szablonowych wraz z przybywaniem etapów

Kod źródłowy do pobrania

Kliknij na poniższy przycisk, jeśli chcesz pobrać kod źródłowy programu przedstawionego w tym artykule.

Źródła:

0 komentarzy

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *