Przegląd

Ta implementacja pokazuje wzorzec na metodach wirtualnych i dziedziczeniu. Klasa bazowa definiuje metodę szablonową, która organizuje algorytm, a konkretne podklasy zastępują kroki, które mają się różnić.

Zalety tego podejścia

  • Jawna struktura algorytmu: metoda szablonowa od razu pokazuje przepływ.
  • Bezpieczeństwo typów w czasie kompilacji: kompilator sprawdza wszystkie niestandardowe kroki.
  • Bez dodatkowej warstwy: nie ma opakowania funkcji, więc nie dochodzi związany z nim narzut.
  • Jasne umowy: metody abstrakcyjne mówią dokładnie, co musi zaimplementować podklasa.

Tradycyjne podejście: metody wirtualne

Klasyczna wersja wzorca opiera się na dziedziczeniu i funkcjach wirtualnych.

Klasa bazowa z metodami wirtualnymi

#include <iostream>
#include <string>

class DataProcessorBase {
public:
    virtual ~DataProcessorBase() = default;

    // Metoda szablonowa: definiuje szkielet algorytmu
    void execute(const std::string& input) {
        std::cout << "Rozpoczynanie potoku przetwarzania danych...\n";

        // Krok 1: Walidacja
        std::string validated = validate(input);
        if (validated.empty()) {
            std::cout << "Walidacja nie powiodła się. Przerywanie.\n";
            return;
        }

        // Krok 2: Przetwarzanie
        std::string processed = process(validated);
        std::cout << "Przetwarzanie ukończone.\n";

        // Krok 3: Zapis
        save(processed);

        std::cout << "Potok zakończony pomyślnie.\n";
    }

protected:
    // Metody wirtualne: podklasy je zastępują
    virtual std::string validate(const std::string& data) = 0;
    virtual std::string process(const std::string& data) = 0;
    virtual void save(const std::string& data) = 0;
};

Konkretne implementacje

class CSVProcessor : public DataProcessorBase {
protected:
    std::string validate(const std::string& data) override {
        std::cout << "  [CSV Walidacja] Sprawdzanie formatu CSV...\n";
        return data.empty() ? "" : data;
    }

    std::string process(const std::string& data) override {
        std::cout << "  [CSV Przetwarzanie] Parsowanie i czyszczenie CSV...\n";
        return data + " (wyczyszczone)";
    }

    void save(const std::string& data) override {
        std::cout << "  [CSV Zapis] Pisanie do pliku CSV: " << data << "\n";
    }
};

class JSONProcessor : public DataProcessorBase {
protected:
    std::string validate(const std::string& data) override {
        std::cout << "  [JSON Walidacja] Walidowanie składni JSON...\n";
        if (data.empty() || data[0] != '{') return "";
        return data;
    }

    std::string process(const std::string& data) override {
        std::cout << "  [JSON Przetwarzanie] Parsowanie i formatowanie JSON...\n";
        return data + " (sformatowane)";
    }

    void save(const std::string& data) override {
        std::cout << "  [JSON Zapis] Pisanie do pliku JSON: " << data << "\n";
    }
};

Przykład tradycyjnego użycia

int main() {
    std::cout << "=== Wzorzec Metoda Szablonowa (Metody Wirtualne) ===\n\n";

    // Potok CSV
    std::cout << "1. Potok CSV:\n";
    CSVProcessor csvProc;
    csvProc.execute("imię,wiek,email");

    std::cout << "\n2. Potok JSON:\n";
    JSONProcessor jsonProc;
    jsonProc.execute("{\"klucz\": \"wartość\"}");

    return 0;
}

Wyjście

=== Wzorzec Metoda Szablonowa (Metody Wirtualne) ===

1. Potok CSV:
Rozpoczynanie potoku przetwarzania danych...
  [CSV Walidacja] Sprawdzanie formatu CSV...
  [CSV Przetwarzanie] Parsowanie i czyszczenie CSV...
Przetwarzanie ukończone.
  [CSV Zapis] Pisanie do pliku CSV: imię,wiek,email (wyczyszczone)
Potok zakończony pomyślnie.

2. Potok JSON:
Rozpoczynanie potoku przetwarzania danych...
  [JSON Walidacja] Walidowanie składni JSON...
  [JSON Przetwarzanie] Parsowanie i formatowanie JSON...
Przetwarzanie ukończone.
  [JSON Zapis] Pisanie do pliku JSON: {"klucz": "wartość"} (sformatowane)
Potok zakończony pomyślnie.

Kluczowe punkty: metody wirtualne

Metoda szablonowa

execute() jest metodą szablonową i definiuje szkielet algorytmu. W określonych punktach wywołuje metody wirtualne, w których podklasy wstrzykują własne zachowanie.

Metody abstrakcyjne

Czyste metody wirtualne w sekcji protected (validate(), process(), save()) zmuszają podklasy do implementacji wszystkich dostosowywanych kroków.

Hierarchia dziedziczenia

Każdy konkretny procesor dziedziczy z DataProcessorBase i implementuje metody wirtualne. Struktura algorytmu jest identyczna we wszystkich implementacjach.

Bezpieczeństwo typów

Kompilator sprawdza typy wszystkich dostosowań. Pilnuje też, żeby klasy konkretne implementowały każdą metodę abstrakcyjną.

Nowoczesne podejście: std::function (kompozycja)

Dziedziczenie sprawdza się w ściśle powiązanych hierarchiach, ale nowoczesny C++ daje alternatywę: std::function i kompozycję. Zyskujesz elastyczność bez dziedziczenia.

Klasa algorytmu ze std::function

#include <iostream>
#include <functional>
#include <string>

class DataProcessor {
private:
    // Kroki metody szablonowej jako elementy std::function
    std::function<std::string(const std::string&)> validate;
    std::function<std::string(const std::string&)> process;
    std::function<void(const std::string&)> save;

public:
    // Konstruktor akceptuje wszystkie dostosowywalne kroki
    DataProcessor(
        std::function<std::string(const std::string&)> validateFn,
        std::function<std::string(const std::string&)> processFn,
        std::function<void(const std::string&)> saveFn
    )
        : validate(validateFn), process(processFn), save(saveFn) {}

    // Metoda szablonowa: definiuje szkielet algorytmu
    void execute(const std::string& input) {
        std::cout << "Rozpoczynanie potoku przetwarzania danych...\n";

        // Krok 1: Walidacja
        std::string validated = validate(input);
        if (validated.empty()) {
            std::cout << "Walidacja nie powiodła się. Przerywanie.\n";
            return;
        }

        // Krok 2: Przetwarzanie
        std::string processed = process(validated);
        std::cout << "Przetwarzanie ukończone.\n";

        // Krok 3: Zapis
        save(processed);

        std::cout << "Potok zakończony pomyślnie.\n";
    }
};

Implementacje oparte na funkcjach

class CSVProcessorFn {
public:
    static std::string validateCSV(const std::string& data) {
        std::cout << "  [CSV Walidacja] Sprawdzanie formatu CSV...\n";
        if (data.empty()) return "";
        return data;
    }

    static std::string processCSV(const std::string& data) {
        std::cout << "  [CSV Przetwarzanie] Parsowanie i czyszczenie CSV...\n";
        return data + " (wyczyszczone)";
    }

    static void saveCSV(const std::string& data) {
        std::cout << "  [CSV Zapis] Pisanie do pliku CSV: " << data << "\n";
    }
};

class JSONProcessorFn {
public:
    static std::string validateJSON(const std::string& data) {
        std::cout << "  [JSON Walidacja] Walidowanie składni JSON...\n";
        if (data.empty() || data[0] != '{') return "";
        return data;
    }

    static std::string processJSON(const std::string& data) {
        std::cout << "  [JSON Przetwarzanie] Parsowanie i formatowanie JSON...\n";
        return data + " (sformatowane)";
    }

    static void saveJSON(const std::string& data) {
        std::cout << "  [JSON Zapis] Pisanie do pliku JSON: " << data << "\n";
    }
};

Użycie funkcji lambda

class CustomProcessor {
public:
    static DataProcessor createWithLambdas() {
        return DataProcessor(
            // Walidacja: odrzuć jeśli zawiera znaki specjalne
            [](const std::string& data) -> std::string {
                std::cout << "  [Lambda Walidacja] Sprawdzanie znaków specjalnych...\n";
                if (data.find('!') != std::string::npos) return "";
                return data;
            },
            // Przetwarzanie: konwersja na wielkie litery
            [](const std::string& data) -> std::string {
                std::cout << "  [Lambda Przetwarzanie] Konwersja na wielkie litery...\n";
                std::string result = data;
                for (char& c : result) c = std::toupper(c);
                return result;
            },
            // Zapis: wydruk na konsoli
            [](const std::string& data) {
                std::cout << "  [Lambda Zapis] Wynik: " << data << "\n";
            }
        );
    }
};

Przykład użycia kompozycji

int main() {
    std::cout << "=== Wzorzec Metoda Szablonowa (std::function) ===\n\n";

    // Potok CSV
    std::cout << "1. Potok CSV:\n";
    DataProcessor csvPipeline(
        CSVProcessorFn::validateCSV,
        CSVProcessorFn::processCSV,
        CSVProcessorFn::saveCSV
    );
    csvPipeline.execute("imię,wiek,email");

    std::cout << "\n2. Potok JSON:\n";
    DataProcessor jsonPipeline(
        JSONProcessorFn::validateJSON,
        JSONProcessorFn::processJSON,
        JSONProcessorFn::saveJSON
    );
    jsonPipeline.execute("{\"klucz\": \"wartość\"}");

    std::cout << "\n3. Potok Oparty na Lambda:\n";
    DataProcessor customPipeline = CustomProcessor::createWithLambdas();
    customPipeline.execute("cześć");

    std::cout << "\n4. Przykład Nieudanej Walidacji:\n";
    customPipeline.execute("niepoprawne!");

    return 0;
}

Wyjście (std::function)

=== Wzorzec Metoda Szablonowa (std::function) ===

1. Potok CSV:
Rozpoczynanie potoku przetwarzania danych...
  [CSV Walidacja] Sprawdzanie formatu CSV...
  [CSV Przetwarzanie] Parsowanie i czyszczenie CSV...
Przetwarzanie ukończone.
  [CSV Zapis] Pisanie do pliku CSV: imię,wiek,email (wyczyszczone)
Potok zakończony pomyślnie.

2. Potok JSON:
Rozpoczynanie potoku przetwarzania danych...
  [JSON Walidacja] Walidowanie składni JSON...
  [JSON Przetwarzanie] Parsowanie i formatowanie JSON...
Przetwarzanie ukończone.
  [JSON Zapis] Pisanie do pliku JSON: {"klucz": "wartość"} (sformatowane)
Potok zakończony pomyślnie.

3. Potok Oparty na Lambda:
Rozpoczynanie potoku przetwarzania danych...
  [Lambda Walidacja] Sprawdzanie znaków specjalnych...
  [Lambda Przetwarzanie] Konwersja na wielkie litery...
Przetwarzanie ukończone.
  [Lambda Zapis] Wynik: CZEŚĆ
Potok zakończony pomyślnie.

4. Przykład Nieudanej Walidacji:
Rozpoczynanie potoku przetwarzania danych...
  [Lambda Walidacja] Sprawdzanie znaków specjalnych...
Walidacja nie powiodła się. Przerywanie.

Kluczowe punkty: std::function

Wstrzykiwanie zależności przez konstruktor

Konstruktor DataProcessor przyjmuje wszystkie dostosowywalne kroki od razu:

DataProcessor(validateFn, processFn, saveFn);

Dzięki temu zależności są jawne, a testowanie z funkcjami mock prostsze.

Elastyczność std::function

Typ std::function może przechowywać: - wskaźniki funkcji, - wyrażenia lambda, - wskaźniki funkcji składowych (z wiązaniem), - obiekty funkcji (funktory).

Bezpieczeństwo typów

Każdy krok ma jasną sygnaturę: - std::function<std::string(const std::string&)> dla walidacji i przetwarzania. - std::function<void(const std::string&)> dla operacji zapisu.

Podział odpowiedzialności

  • Metoda szablonowa definiuje przepływ algorytmu.
  • Każda implementacja procesora skupia się na swojej logice domenowej.
  • Kod główny tylko łączy wszystko razem.

Porównanie: metody wirtualne i std::function

Aspekt Metody wirtualne std::function (kompozycja)
Powiązanie Ścisłe, hierarchiczne Luźne, elastyczne
Dziedziczenie Wymagane Nie wymagane
Testowanie Wymaga podklas mock Łatwe z lambdami i mockami
Zmiany w czasie wykonania Nie Tak, trywialne
Narzut pamięci Tablica wirtualna na klasę Opakowanie std::function
Złożoność Hierarchie klas Prosta kompozycja funkcji
Wielokrotne użycie Poprzez podklasy Poprzez kompozycję funkcji
Styl nowoczesnego C++ Tradycyjny OOP Programowanie funkcyjne
Bezpieczeństwo typów Sprawdzane w czasie kompilacji Wymazanie typu w czasie wykonania
Wydajność Bezpośrednie wywołania wirtualne Mały narzut wywołania funkcji

Kiedy używać

Podejście metod wirtualnych:

  • Masz ściśle zintegrowane hierarchie klas.
  • Potrzebujesz bezpieczeństwa typów w czasie kompilacji.
  • Wydajność jest krytyczna, a narzut opakowania funkcji ma znaczenie.
  • Piszesz tradycyjny projekt OOP.

Podejście std::function:

  • Budujesz elastyczną architekturę z wtyczkami.
  • Potrzebujesz zmieniać zachowanie w czasie wykonania.
  • Testujesz z funkcjami mock.
  • Wolisz styl programowania funkcyjnego.

Podsumowanie

Użyj metod wirtualnych, gdy kroki należą do typu, a hierarchia jest stabilna. Użyj std::function, gdy wywołujący ma podmienić krok w czasie wykonania albo gdy podklasa powstałaby tylko po to, żeby dostarczyć trzy funkcje. Szkielet algorytmu zostaje ten sam w obu wersjach i o to chodzi w metodzie szablonowej.