1. Definicja
Metoda Szablonowa (Template Method) to wzorzec behawioralny, który definiuje szkielet algorytmu w metodzie klasy bazowej, pozostawiając konkretne kroki do zaimplementowania klasom pochodnym.
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure. — Gang of Four, Design Patterns
Najprościej rzecz ujmując: klasa bazowa mówi „co" i „w jakiej kolejności", a klasy pochodne mówią „jak".
Oznacza to, że niezmienną strukturę — kolejność kroków, ich powiązania — definiujemy raz, w jednym miejscu. Tylko te fragmenty, które różnią się w zależności od kontekstu, oddajemy do nadpisania podklasom.
2. Przykład z życia: zobaczmy jak to zrozumieć?
Wyobraźmy sobie, że uruchamiamy sieć kawiarni. Każda filia serwuje kawę inaczej — inna kawa ziarnista, inny dodatek do mleka, inny sposób podania. Mimo to w każdej kawiarni proces parzenia kawy wygląda identycznie pod względem kolejności kroków:
- Zagotuj wodę.
- Zaparz kawę (tu każda kawiarnia robi to inaczej).
- Wlej do kubka.
- Dodaj dodatki (tu też każda kawiarnia różni się).
Nie ma sensu, żeby każda filia wymyślała od nowa kolejność tych kroków — to przepis na chaos i błędy. Zamiast tego właściciel sieci drukuje instrukcję szablonową, która opisuje niezmienną sekwencję. Każda filia wypełnia tylko te miejsca, w których naprawdę się różni.
Właśnie tak działa Metoda Szablonowa.
Ćwiczenie myślowe: wyobraź sobie, że musisz napisać trzy różne eksportery danych — do CSV, do JSON i do XML. Każdy z nich robi po kolei: otwiera plik, przetwarza każdy rekord, zamyka plik. Różnią się tylko formatem, w jakim zapisują rekord. Czy warto pisać całą logikę otwierania i zamykania pliku osobno w każdym eksporterze? Odpowiedź na to pytanie zaprowadzi nas prosto do kodu.
3. Przykład — naruszenie Template Method
Wyobraźmy sobie, że budujemy system raportowania dla aplikacji e-commerce. Mamy trzy rodzaje raportów: PDF, HTML i CSV. Każdy z nich realizuje ten sam algorytm generowania raportu.
Programista bez znajomości wzorca zaimplementuje to tak:
// Klasa generująca raport PDF - cały algorytm wpleciony w jedną klasę
class PdfReportGenerator {
public void generate(List<Order> orders) {
// Krok 1: inicjalizacja - identyczny w każdym generatorze
System.out.println("Inicjalizacja generatora PDF...");
openDocument();
// Krok 2: nagłówek - różny dla PDF i HTML, identyczny dla CSV
writePdfHeader();
// Krok 3: przetwarzanie danych - logika identyczna w każdym generatorze
for (Order order : orders) {
formatAsPdf(order);
}
// Krok 4: stopka - różna w zależności od formatu
writePdfFooter();
// Krok 5: zamknięcie - identyczne w każdym generatorze
closeDocument();
System.out.println("Raport PDF gotowy.");
}
private void openDocument() { /* ... */ }
private void writePdfHeader() { /* ... */ }
private void formatAsPdf(Order o) { /* ... */ }
private void writePdfFooter() { /* ... */ }
private void closeDocument() { /* ... */ }
}
// Klasa generująca raport HTML - zduplikowana struktura algorytmu
class HtmlReportGenerator {
public void generate(List<Order> orders) {
// Duplikat: te same kroki inicjalizacji co w PDF
System.out.println("Inicjalizacja generatora HTML...");
openDocument();
// Duplikat kroku nagłówka — różni się tylko implementacją
writeHtmlHeader();
// Duplikat: ta sama pętla przetwarzająca dane
for (Order order : orders) {
formatAsHtml(order);
}
// Duplikat kroku stopki
writeHtmlFooter();
// Duplikat: to samo zamknięcie dokumentu
closeDocument();
System.out.println("Raport HTML gotowy.");
}
private void openDocument() { /* ... */ }
private void writeHtmlHeader() { /* ... */ }
private void formatAsHtml(Order o) { /* ... */ }
private void writeHtmlFooter() { /* ... */ }
private void closeDocument() { /* ... */ }
}
Co jest tutaj złe? Algorytm — czyli sekwencja: otwórz → nagłówek → dane → stopka → zamknij — powtarza się w każdej klasie. Struktura zduplikowana, a konsekwencje tego są bolesne:
- Ryzyko rozbieżności: jeśli właściciel produktu powie „dodajmy krok walidacji danych przed generowaniem", musimy zmienić każdą klasę osobno. Łatwo o pominięcie jednej z nich.
- Trudność testowania: nie ma jednego miejsca, które możemy pokryć testem sprawdzającym poprawność kolejności kroków.
- Duplikacja kodu:
openDocument()icloseDocument()są identyczne — piszemy je trzy razy. - Brak konwencji: nowy programista nie wie, że CSV też powinien działać w tym samym schemacie. Może dodać etap walidacji tylko do dwóch generatorów.
4. Przykład — zgodny z Template Method
Jak podeszlibyśmy do tego lepiej? Wyciągamy niezmienną strukturę algorytmu do klasy bazowej i zostawiamy w niej tylko „białe plamy" — metody abstrakcyjne, które każda podklasa wypełnia po swojemu.
// Klasa bazowa definiuje szkielet algorytmu
// Metoda generate() jest finalna — podklasy nie mogą zmienić kolejności kroków
abstract class ReportGenerator {
// Metoda szablonowa — serce wzorca.
// final gwarantuje, że żadna podklasa nie nadpisze kolejności kroków.
public final void generate(List<Order> orders) {
initialize(); // krok wspólny — zaimplementowany tutaj
writeHeader(); // krok zmienny — każda podklasa nadpisuje
processOrders(orders); // krok wspólny — zaimplementowany tutaj
writeFooter(); // krok zmienny — każda podklasa nadpisuje
finalize(); // krok wspólny — zaimplementowany tutaj
}
// Kroki wspólne — zaimplementowane raz, tutaj
private void initialize() {
System.out.println("Inicjalizacja generatora " + getFormatName() + "...");
openDocument();
}
private void processOrders(List<Order> orders) {
// Pętla zawsze wygląda tak samo — nie duplikujemy jej w podklasach
for (Order order : orders) {
formatOrder(order);
}
}
private void finalize() {
closeDocument();
System.out.println("Raport " + getFormatName() + " gotowy.");
}
// Kroki zmienne — podklasy MUSZĄ je zaimplementować
protected abstract void writeHeader();
protected abstract void writeFooter();
protected abstract void formatOrder(Order order);
protected abstract String getFormatName();
// Metody pomocnicze wspólne dla wszystkich formatów
private void openDocument() { /* otwórz strumień pliku */ }
private void closeDocument() { /* zamknij strumień pliku */ }
}
// Konkretna implementacja dla PDF — nadpisuje tylko to, co różni PDF od innych
class PdfReportGenerator extends ReportGenerator {
@Override
protected void writeHeader() {
// Nagłówek specyficzny dla PDF: fonty, marginesy, logo
System.out.println("<PDF header>");
}
@Override
protected void writeFooter() {
System.out.println("<PDF footer z numeracją stron>");
}
@Override
protected void formatOrder(Order order) {
System.out.println("PDF wiersz: " + order.getId());
}
@Override
protected String getFormatName() { return "PDF"; }
}
// Konkretna implementacja dla HTML — takie same metody, inna treść
class HtmlReportGenerator extends ReportGenerator {
@Override
protected void writeHeader() {
System.out.println("<html><body><table>");
}
@Override
protected void writeFooter() {
System.out.println("</table></body></html>");
}
@Override
protected void formatOrder(Order order) {
System.out.println("<tr><td>" + order.getId() + "</td></tr>");
}
@Override
protected String getFormatName() { return "HTML"; }
}
Co zyskujemy?
- Algorytm istnieje w dokładnie jednym miejscu — w metodzie
generate(). Gdy product owner poprosi o dodanie kroku walidacji, zmieniamy jeden plik. - Słowo kluczowe
finalna metodzie szablonowej jest istotnym sygnałem: ta kolejność jest nienaruszalna. Podklasy mogą zmieniać treść kroków, ale nie ich kolejność. - Każda podklasa jest skupiona wyłącznie na swojej specyfice formatowania — jest krótka i czytelna.
- Testy jednostkowe algorytmu piszemy raz, w klasie bazowej, mockując metody abstrakcyjne.
5. Diagram
ReportGenerator ┌──────────────────────────────────┐ │ + generate() : final │ ← metoda szablonowa │ ├─ initialize() │ ← krok wspólny (zaimplementowany) │ ├─ writeHeader() [abstract] │ ← krok zmienny (do nadpisania) │ ├─ processOrders() [private] │ ← krok wspólny (zaimplementowany) │ ├─ writeFooter() [abstract] │ ← krok zmienny (do nadpisania) │ └─ finalize() [private] │ ← krok wspólny (zaimplementowany) └──────────────────────────────────┘ ▲ ┌───────────┼────────────┐ │ │ │ PdfReport HtmlReport CsvReport Generator Generator Generator ┌────────┐ ┌──────────┐ ┌──────────┐ │write- │ │write- │ │write- │ │Header()│ │Header() │ │Header() │ │write- │ │write- │ │write- │ │Footer()│ │Footer() │ │Footer() │ │format- │ │format- │ │format- │ │Order() │ │Order() │ │Order() │ └────────┘ └──────────┘ └──────────┘
6. Dlaczego Template Method jest ważne?
Eliminuje duplikację struktury algorytmu. Najgroźniejszą formą duplikacji nie jest skopiowany blok kodu, ale skopiowana sekwencja operacji. Trudniej ją zauważyć podczas code review, a rozbieżności między kopiami ujawniają się dopiero przy zmianach wymagań.
Wymusza konwencję przez architekturę. Jeśli nowy programista doda czwarty eksporter — powiedzmy XML — nie może „zapomnieć" o kroku inicjalizacji, bo klasa bazowa go wykona za niego. Wzorzec wymusza poprawność struktury na poziomie kompilacji, nie dokumentacji.
Upraszcza testowanie. Algorytm strukturalny testujemy w jednym miejscu (klasa bazowa z mockami), a logikę formatowania — osobno, w izolacji od reszty. Dwa zestawy testów zamiast sześciu.
Wpływ na pracę zespołu: gdy dwie osoby rozwijają równolegle PdfReportGenerator i HtmlReportGenerator, nie wchodzą sobie w drogę. Klasa bazowa zmienia się rzadko i przez jedną osobę odpowiedzialną za algorytm. Konflikty merge'owe są minimalne.
Rzeczywiste konsekwencje zaniedbania: w dużych projektach bez tego wzorca widzimy sytuacje, gdzie jeden generator raportów ma dodatkowy krok audytu, a reszta nie — bo ktoś zapomniał go dodać. Albo gdzie pętla przetwarzająca dane jest nieznacznie różna w trzech miejscach i nikt nie wie, która wersja jest „prawidłowa".
7. Kiedy stosować?
Naturalne scenariusze:
- Parsery i eksportery — zawsze masz: otwórz źródło → przetwórz rekordy → zamknij. Format różni się, struktura nie.
- Kroki budowania / inicjalizacji — np. Spring przy tworzeniu
ApplicationContext: załaduj konfigurację → zarejestruj beany → uruchom post-procesory. Podklasy (AnnotationConfigApplicationContext,ClassPathXmlApplicationContext) nadpisują etap ładowania konfiguracji. - Algorytmy sortowania z wymiennym komparatorem — gdy szkielet algorytmu jest stały, ale kryterium porównywania zmienne.
- Frameworki testowe — JUnit używa tej idei w
@Before/@After(setUp/tearDown jako hook methods — odmiana Template Method).
Jak Spring i JDK stosują ten wzorzec:
HttpServletz Java EE: metodaservice()to metoda szablonowa;doGet(),doPost()to kroki do nadpisania.AbstractListw JDK: metodaiterator()jest zaimplementowana; podklasy dostarczająget(int index)isize().- Spring's
JdbcTemplate: szkielet operacji JDBC (otwórz połączenie → wykonaj zapytanie → obsłuż błędy → zamknij połączenie) jest stały; programista dostarcza tylkoRowMapper.
Kiedy wzorzec jest szczególnie wartościowy:
- Gdy masz co najmniej dwie klasy robiące „to samo, ale trochę inaczej" i zamazuje ci się, gdzie naprawdę się różnią.
- Gdy nowe klasy w rodzinie pojawiają się regularnie (nowy format eksportu co sprint) — każda nowa klasa automatycznie dziedziczy prawidłowy szkielet.
- Gdy zmiana wymagań dotycząca procesu (np. „dodajmy logowanie przed każdym krokiem") ma trafić do wszystkich wariantów jednocześnie.
Kiedy NIE stosować:
- Gdy kroki algorytmu mają zupełnie inną kolejność w różnych wariantach — wtedy lepiej rozważyć wzorzec Strategii (Strategy), który zamiast dziedziczenia używa kompozycji.
- Gdy hierarchia klas robi się głęboka (Generator → SpecializedGenerator → VerySpecializedGenerator) — zagnieżdżone wywołania metod szablonowych są trudne do śledzenia.
- Gdy masz tylko jeden wariant algorytmu i nie przewidujesz kolejnych — to przedwczesna abstrakcja.
„Przepis kucharza": klasa bazowa drukuje przepis i kolejność kroków. Każdy kucharz (podklasa) może wybrać własne składniki, ale kolejność kroków jest nienaruszalna.
Albo krócej:
„Ramka jest moja, obrazek jest twój." Klasa bazowa = ramka (nienaruszalna struktura). Podklasa = obrazek (wymienna zawartość).