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.