PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Wzorzec Template Method – szkielet algorytmu, hooks, abstract vs hook, porównanie ze Strategy

by Henryk Tews / wtorek, 08 listopada 2022 / Opublikowano w Wzorce projektowe

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.

About Henryk Tews

Co możesz przeczytać następne

Factory Method + Abstract Factory – implementacje od zera, tabela różnic, Simple Factory jako alternatywa
CQRS – Command Bus, Query Bus, read models, integracja z Magento 2
Strategy w PHP – i jak Magento 2 używa go w cenach
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}