Template Method to jeden z prostszych wzorców behawioralnych GoF, ale bardzo skuteczny przy eliminowaniu duplikacji kodu. Definiuje szkielet algorytmu w klasie bazowej i pozwala podklasom nadpisać wybrane kroki bez zmiany ogólnej struktury. Jeśli masz kilka klas które robią „to samo, ale trochę inaczej” – Template Method jest kandydatem do refaktoringu.
Problem bez Template Method
<?php
// Duplikacja logiki w kilku klasach importerów
class CsvProductImporter
{
public function import(string $filePath): array
{
// 1. Walidacja pliku
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
// 2. Odczyt danych - specyficzny dla CSV
$rows = array_map('str_getcsv', file($filePath));
$headers = array_shift($rows);
$data = array_map(fn($row) => array_combine($headers, $row), $rows);
// 3. Transformacja danych - specyficzna dla CSV
$products = array_map(fn($row) => [
'sku' => $row['sku'],
'name' => $row['name'],
'price' => (float) $row['price'],
], $data);
// 4. Zapis - identyczny w każdym importerze
$saved = 0;
foreach ($products as $product) {
$this->saveProduct($product);
$saved++;
}
// 5. Raport - identyczny w każdym importerze
echo "Zaimportowano: {$saved} produktów\n";
return $products;
}
}
class JsonProductImporter
{
public function import(string $filePath): array
{
// 1. Walidacja pliku - skopiowane z CsvProductImporter
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
// 2. Odczyt danych - specyficzny dla JSON
$data = json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR);
// 3. Transformacja danych - specyficzna dla JSON
$products = array_map(fn($row) => [
'sku' => $row['product_code'], // inne nazwy pól!
'name' => $row['title'],
'price' => (float) $row['base_price'],
], $data['products'] ?? $data);
// 4. Zapis - identyczny, skopiowane
$saved = 0;
foreach ($products as $product) {
$this->saveProduct($product);
$saved++;
}
// 5. Raport - identyczny, skopiowane
echo "Zaimportowano: {$saved} produktów\n";
return $products;
}
}
// Kroki 1, 4 i 5 są identyczne - klasyczny kandydat na Template Method
Implementacja wzorca
<?php
declare(strict_types=1);
// Klasa bazowa - definiuje szkielet algorytmu
abstract class AbstractProductImporter
{
// Template Method - final żeby nikt nie nadpisał szkieletu
final public function import(string $filePath): array
{
// Kroki algorytmu - kolejność ustalona na zawsze
$this->validateFile($filePath); // hook - można nadpisać
$rawData = $this->readData($filePath); // abstract - musi być nadpisane
$products = $this->transformData($rawData); // abstract - musi być nadpisane
$saved = $this->saveAll($products); // konkretna - nie trzeba nadpisywać
$this->generateReport($saved, $filePath); // hook - można nadpisać
return $products;
}
// Kroki konkretne - wspólna logika dla wszystkich podklas
protected function validateFile(string $filePath): void
{
if (!file_exists($filePath)) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
if (!is_readable($filePath)) {
throw new \RuntimeException("File is not readable: {$filePath}");
}
}
private function saveAll(array $products): int
{
$saved = 0;
foreach ($products as $product) {
$this->saveProduct($product);
$saved++;
}
return $saved;
}
protected function generateReport(int $count, string $source): void
{
echo "Import zakończony: {$count} produktów z {$source}\n";
}
protected function saveProduct(array $product): void
{
// Wspólna logika zapisu - podklasy mogą nadpisać jeśli potrzeba
echo "Zapisuję: {$product['sku']} - {$product['name']}\n";
}
// Kroki abstrakcyjne - każda podklasa MUSI je zaimplementować
abstract protected function readData(string $filePath): array;
abstract protected function transformData(array $rawData): array;
}
<?php
declare(strict_types=1);
// Konkretna implementacja dla CSV
class CsvProductImporter extends AbstractProductImporter
{
public function __construct(
private string $delimiter = ',',
private string $enclosure = '"'
) {}
protected function readData(string $filePath): array
{
$handle = fopen($filePath, 'r');
$headers = fgetcsv($handle, 0, $this->delimiter, $this->enclosure);
$rows = [];
while (($row = fgetcsv($handle, 0, $this->delimiter, $this->enclosure)) !== false) {
$rows[] = array_combine($headers, $row);
}
fclose($handle);
return $rows;
}
protected function transformData(array $rawData): array
{
return array_map(fn($row) => [
'sku' => trim($row['sku']),
'name' => trim($row['name']),
'price' => (float) str_replace(',', '.', $row['price']),
], $rawData);
}
}
// Konkretna implementacja dla JSON
class JsonProductImporter extends AbstractProductImporter
{
protected function readData(string $filePath): array
{
$content = file_get_contents($filePath);
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
// Obsłuż różne struktury JSON
return $data['products'] ?? $data['items'] ?? $data;
}
protected function transformData(array $rawData): array
{
return array_map(fn($row) => [
'sku' => $row['product_code'] ?? $row['sku'],
'name' => $row['title'] ?? $row['name'],
'price' => (float) ($row['base_price'] ?? $row['price'] ?? 0),
], $rawData);
}
// Nadpisanie hooka - inny format raportu dla JSON
protected function generateReport(int $count, string $source): void
{
echo json_encode([
'status' => 'success',
'count' => $count,
'source' => basename($source),
]) . "\n";
}
}
// Implementacja dla XML
class XmlProductImporter extends AbstractProductImporter
{
protected function readData(string $filePath): array
{
$xml = simplexml_load_file($filePath);
$items = [];
foreach ($xml->product as $product) {
$items[] = (array) $product;
}
return $items;
}
protected function transformData(array $rawData): array
{
return array_map(fn($row) => [
'sku' => (string) ($row['sku'] ?? ''),
'name' => (string) ($row['name'] ?? ''),
'price' => (float) ($row['price'] ?? 0),
], $rawData);
}
// Nadpisanie walidacji - XML ma dodatkowe wymagania
protected function validateFile(string $filePath): void
{
parent::validateFile($filePath); // wywołaj logikę bazową
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
if (strtolower($extension) !== 'xml') {
throw new \InvalidArgumentException("Expected .xml file, got .{$extension}");
}
}
}
// Użycie - wszystkie importery mają ten sam interfejs
function runImport(AbstractProductImporter $importer, string $file): void
{
try {
$products = $importer->import($file);
echo "Sukces: " . count($products) . " produktów\n";
} catch (\Exception $e) {
echo "Błąd: " . $e->getMessage() . "\n";
}
}
runImport(new CsvProductImporter(), 'products.csv');
runImport(new JsonProductImporter(), 'products.json');
runImport(new XmlProductImporter(), 'products.xml');
Hooks – opcjonalne kroki
Wzorzec rozróżnia dwa rodzaje metod w klasie bazowej: abstract (podklasa MUSI zaimplementować) i hooks (podklasa MOŻE nadpisać, ale nie musi). Hooks mają domyślną implementację lub są puste:
<?php
abstract class AbstractReportGenerator
{
// Template Method
final public function generate(): string
{
$this->beforeGenerate(); // hook - domyślnie nic nie robi
$data = $this->fetchData(); // abstract
$report = $this->formatData($data); // abstract
$this->afterGenerate($report); // hook - domyślnie nic nie robi
return $report;
}
// Hooks - puste domyślne implementacje
protected function beforeGenerate(): void {}
protected function afterGenerate(string $report): void {}
// Abstract - obowiązkowe
abstract protected function fetchData(): array;
abstract protected function formatData(array $data): string;
}
class SalesReport extends AbstractReportGenerator
{
// Nadpisuje tylko hook który potrzebuje
protected function beforeGenerate(): void
{
echo "Przygotowuję dane sprzedażowe...\n";
}
protected function fetchData(): array
{
return [['product' => 'Widget', 'qty' => 100, 'revenue' => 2999.0]];
}
protected function formatData(array $data): string
{
$lines = array_map(
fn($row) => "{$row['product']}: {$row['qty']} szt., {$row['revenue']} PLN",
$data
);
return implode("\n", $lines);
}
}
Template Method vs Strategy
| Aspekt | Template Method | Strategy |
|---|---|---|
| Mechanizm | Dziedziczenie | Kompozycja |
| Zmiana algorytmu | Podklasa nadpisuje kroki | Podmiana obiektu strategii |
| Szkielet algorytmu | Zdefiniowany w klasie bazowej | Brak wspólnego szkieletu |
| Zmiana w runtime | Niemożliwa (dziedziczenie) | Możliwa (setStrategy) |
| Duplikacja kodu | Eliminowana przez klasę bazową | Może pozostać w kontekście |
Podsumowanie
Template Method to wzorzec który rozwiązuje bardzo konkretny problem: masz algorytm z niezmienną strukturą, ale zmiennymi detalami. Klasa bazowa definiuje „co” i „w jakiej kolejności”, podklasy definiują „jak”. Użyj final na metodzie template żeby zagwarantować że nikt nie naruszy szkieletu. Hooks dają elastyczność tam gdzie podklasy mogą ale nie muszą dostarczyć własnej implementacji.
