Metoda szablonowa – czym jest i do czego można ją wykorzystać?

Wzorzec metoda szablonowa (ang. template method) jest wzorcem z rodziny wzorców behawioralnych. Polega on na stworzeniu szkieletu (szablonu), którego elementy można zmieniać zależnie od kontekstu wykorzystania.

Metoda szablonowa idealnie się sprawdza w sytuacji, gdy w aplikacji jest wykorzystywanych kilka podobnych procesów do siebie. Procesy te różnią się od siebie nieznacznie, jednak ich szkielet wygląda identycznie. Na tym etapie może to brzmieć jeszcze dość abstrakcyjnie, dlatego przejdźmy do przykładu opartego na świecie realnym.

Prosty przykład użycia metody szablonowej

Wyobraźmy sobie, że chcemy opisać proces budowania domu. Bez różnicy jaki to będzie dom – chcemy posiadać dosyć uniwersalny sposób budowania bez względu na wykorzystane materiały, klimat czy też teren. W większości przypadków proces będzie wyglądać tak samo – i to nam wystarczy, aby stworzyć szablon.

Do zobrazowania Ci takiego procesu użyłem bardzo prostej klasy – jeszcze bez żadnej implementacji. Same nazwy metod powinny Ci wystarczyć, aby zrozumieć jak z grubsza przebiega budowa domu.

public abstract class HomeBuildingProcess {
    public final void build() {
        digHole();

        putFoundations();

        buildWallsAndFloors();

        buildRoof();

        putDoors();

        putWindows();
    }

    protected abstract void putWindows();

    protected abstract void putDoors();

    protected abstract void buildRoof();

    protected abstract void buildWallsAndFloors();

    protected abstract void putFoundations();

    protected abstract void digHole();
}

Czyli w skrócie: kopiemy dół, murujemy fundamenty, stawiamy ściany i stropy, kładziemy dach, a na koniec montujemy drzwi oraz okna. Nie ma sensu wchodzić w szczegóły.

Jak pewnie zauważyłeś nadal nie mamy żadnych szczegółów co do sposobu wykonania kroków. Na razie są tylko wywowałania konkretnych etapów w odpowiedniej kolejności. Czyli mamy już gotowy szablon procesu.

Za definicję szablonu odpowiada metoda w klasie abstrakcyjnej. Wywołuje ona po kolei odpowiednie kroki algorytmu. Każdy z tych kroków jest zdefiniowany jako metoda abstrakcyjna. Na tym etapie znamy tylko nazwę kroku, a sposób wykonania danego kroku jest nam jeszcze nieznany. Istotne żeby metoda z procesem została oznaczona jako finalna, tak aby żaden z klientów klasy nie mógł nadpisać samego procesu.

Skoro mamy już szablon to postarajmy się go teraz wykorzystać. Wyobraź sobie sytuację, gdy chcemy aby powyżej stworzony proces zastosować dla budowy dwóch różnych rodzajów domu:

  • murowanego,
  • drewnianego.

W takim przypadku wystarczy utworzyć klasę, która będzie rozszerzać proces i to jej zadaniem będzie implementacja konkretnych kroków. Klasa bazowa wie jak cały proces należy wykonać, zaś szczegóły każdego z kroków zna już konkretna implementacja.

public class WoodenHomeBuildingProcess extends HomeBuildingProcess{
    @Override
    protected void putWindows() {
        System.out.println("Wstaw drewniane okna");
    }

    @Override
    protected void putDoors() {
        System.out.println("Wstaw drewniane drzwi");
    }

    @Override
    protected void buildRoof() {
        System.out.println("Dach ze strzechy");
    }

    @Override
    protected void buildWallsAndFloors() {
    }

    @Override
    protected void putFoundations() {
    }

    @Override
    protected void digHole() {
    }
}

I analogicznie dla domu murowanego.

package blog.templatemethod.home;

public class BrickHomeBuildingProcess extends HomeBuildingProcess{
    @Override
    protected void putWindows() {

    }

    @Override
    protected void putDoors() {

    }

    @Override
    protected void buildRoof() {

    }

    @Override
    protected void buildWallsAndFloors() {

    }

    @Override
    protected void putFoundations() {

    }

    @Override
    protected void digHole() {

    }
}

Podsumowując klasa bazowa opisuje nam szablon poprzez zdefiniowanie elementów. Jeśli jakiś element jest uniwersalny to klasa bazowa go implementuje. Elementy zmieniające się zależnie od konktestu użycia muszą zostać zaimplementowane w klasie dziedziczącej. Spróbujmy teraz zrobić trochę „poważniejsze” i trudniejsze użycie tego wzorca.

Szablon gerowania raportów

Do lepszego pokazania Ci wzorca metoda szablonowa w akcji wybrałem przykład z generowaniem raportów. Na samym początku zdefiniowałem jak ma wyglądać proces generowanie każdego możliwego raportu:

  1. Szukamy dokumentu po podanym id
  2. Rozpoczynamy generowanie raportu
  3. Sprawdzamy czy użytkownik może wygenerować raport
  4. Rozpoczynamy generowanie treści
  5. Generujemy treść raportu
  6. Rozpoczynamy zapis do pliku
  7. Zapisujemy treść raportu do pliku
  8. Kończymy generowanie raportu z odpowiednim statusem

Starałem się stworzyć bardziej skomplikowany proces, aby było na czym pracować. W ramach wyjaśnienia:

  • Odkładamy statusy raportu, aby nasz potencjalny użytkownik mógł śledzić proces generowania,
  • Dokument jest nic nieznaczącym w tym przykładzie obiektem.

Opisany wyżej proces został zaimplementowany w ten sposób:

package blog.templatemethod.report;

import blog.templatemethod.document.Document;
import blog.templatemethod.document.DocumentRepository;

public abstract class ReportGenerator {
    private final DocumentRepository documentRepository;
    private final ReportStatusService reportStatusService;

    public ReportGenerator(DocumentRepository documentRepository, ReportStatusService reportStatusService) {
        this.documentRepository = documentRepository;
        this.reportStatusService = reportStatusService;
    }

    public final void generateReport(String documentId) {
        Document document = documentRepository.findDocumentById(documentId)
                .orElseThrow(() -> new IllegalArgumentException("Cannot find document for id=" + documentId));

        String reportId = reportStatusService.startReportGenerating(documentId);

        if (!isUserCanGenerateReport()) {
            reportStatusService.finishWithFailure(reportId, "Current user is not authorized to generate report");
            return;
        }

        reportStatusService.startGeneratingProgress(reportId);

        String reportContent = generateReportContent(document);

        reportStatusService.startStoringContent(reportId);

        GeneratingReportProcessStatus status = storeReportContent(reportContent);

        if (status.isSucceed()) {
            reportStatusService.finishWithSuccess(reportId);
        } else if (status.isFailed()) {
            reportStatusService.finishWithFailure(reportId, "Report could not be stored properly");
        }
    }

    protected abstract boolean isUserCanGenerateReport();

    protected abstract GeneratingReportProcessStatus storeReportContent(String reportContent);

    protected abstract String generateReportContent(Document document);

}

Jest tego sporo, ale tak naprawdę nie musimy się w to zbytnio wczytywać, bo tak naprawdę użytkownika (osoby dodającej implementację procesu) takiej klasy interesuje tylko to co mamy dostarczyć. Po dodaniu wymaganych elementów, algorytm powinien zadziałać poprawnie. Na tym w końcu polega metoda szablonowa – otrzymujemy szkielet i uzupełniamy tylko to co nam każą.

W ramach wyjaśnienia DocumentService oraz ReportStatusService są tylko interfejsami, które są używane jako przykład pobierania dokumentu i zarządzania stanem raportu. Nie chcę utrudniać przykładu zbędną implementacją.

Wróćmy do sedna – czyli mamy szablon, czas na dodanie konkretnych przypadków. Na początek mamy przykład z generowaniem raportu do CSV – jak widzisz musimy tylko zdefiniować trzy wymagane przez szablon elementy:

  • weryfikację czy dany użytkownik ma prawo wygenerowania tego typu raportu,
  • generownie treści raportu,
  • zapis raportu w odpowiednie miejsce.
package blog.templatemethod.report;

import blog.templatemethod.document.Document;
import blog.templatemethod.document.DocumentRepository;

import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class CsvReportGenerator extends ReportGenerator {

    public CsvReportGenerator(DocumentRepository documentRepository,
                              ReportStatusService reportStatusService) {
        super(documentRepository, reportStatusService);
    }

    @Override
    protected boolean isUserCanGenerateReport() {
        return true;
    }

    @Override
    protected GeneratingReportProcessStatus storeReportContent(String reportContent) {
        try (PrintWriter printWriter = new PrintWriter("report.csv", StandardCharsets.UTF_8)) {
            printWriter.write(reportContent);
            return GeneratingReportProcessStatus.SUCCEED;
        } catch (Exception e) {
            return GeneratingReportProcessStatus.FAILED;
        }
    }

    @Override
    protected String generateReportContent(Document document) {
        return "column1,column2\nvalue1,value";
    }
}

Jako kolejny przykład utworzyłem generowanie raportu HTML.

package blog.templatemethod.report;

import blog.templatemethod.document.Document;
import blog.templatemethod.document.DocumentRepository;
import blog.templatemethod.user.UserService;

import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class HtmlReportGenerator extends ReportGenerator {
    private final UserService userService;


    public HtmlReportGenerator(DocumentRepository documentRepository,
                               ReportStatusService reportStatusService,
                               UserService userService) {
        super(documentRepository, reportStatusService);
        this.userService = userService;
    }

    @Override
    protected boolean isUserCanGenerateReport() {
        return userService.getCurrentUser().isPremiumUser();
    }

    @Override
    protected GeneratingReportProcessStatus storeReportContent(String reportContent) {
        try (PrintWriter printWriter = new PrintWriter("report.html", StandardCharsets.UTF_8)) {
            printWriter.write(reportContent);
            return GeneratingReportProcessStatus.SUCCEED;
        } catch (Exception e) {
            return GeneratingReportProcessStatus.FAILED;
        }
    }

    @Override
    protected String generateReportContent(Document document) {
        return "<html><table>...</table></html>";
    }
}

Na podstawie tych dwóch przykładów powinieneś już dostrzec zaletę wykorzystania metody szablonowej. Teraz możemy dodawać kolejne sposobu generowania raportu np. zapis do PDF lub do bazy danych mając pewność, że każdy sposób generowania raportu będzie przebiegał identycznie.

Wady i zalety

Najważniejszą zaletą tego wzorca jest możliwość zdefiniowana algorytmu w jednym miejscu. Dzięki temu tak zdefiniowany proces może być reużywany i unikamy niepotrzebnej duplikacji kodu. Co więcej, taki algorytm bardzo łatwo jest teraz zmodyfikować – wystarczy to zrobić w jednym miejscu.

Jak każdy wzorzec ma też i swoje wady. Jedną z nich może być ciężkość w utrzymaniu jeżeli utworzony algorytm ma wiele kroków i posiadamy kilka implementacji tego procesu.

Podsumowanie

Mam nadzieję, że choć trochę przybliżyłem Ci wzorzec metody szablonowej i od tej pory będziesz wiedział kiedy go użyć. Czy jesteś w stanie podać jakiś przykład, w którym metoda szablonowa sprawdziłaby się idealnie?

Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x