Przedstawiam Wam kolejny artykuł o wzorcach projektowych, w którym tym razem zajmiemy się wzorcem Metoda Szablonowa (ang. Template Method).
„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.
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
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:
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”.
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.
Zalety | Wady |
---|---|
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ęści | Przygotowany szablon algorytmu może równać się ograniczeniu dla niektórych klientów |
Powtarzający się kod może zostać przeniesiony do klasy nadrzędnej | Moż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.
- https://refactoring.guru/pl/design-patterns/template-method
- Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra, Rusz głową! Wzorce Projektowe, wyd. HELION 2017 — rozdział 8.
0 komentarzy