Перейти к содержанию

Template Method (Шаблонный метод)

Категория: паттерн поведения.

Проблема

Несколько алгоритмов почти одинаковы по структуре: одна и та же последовательность шагов, но одна или несколько шагов реализованы по-разному в разных вариантах. Если писать каждый вариант отдельным независимым методом/классом, общая часть логики дублируется и её приходится синхронно поддерживать сразу в нескольких местах.

Решение

  1. Общий алгоритм оформляется как метод в базовом классе - шаблонный метод. Он вызывает набор более мелких шагов в фиксированном порядке.
  2. Шаги, одинаковые для всех вариантов, реализуются прямо в базовом классе.
  3. Шаги, которые различаются, объявляются abstract (обязательны к переопределению) либо virtual с реализацией по умолчанию (хук - подкласс может переопределить его, а может оставить как есть).
  4. Сам шаблонный метод обычно не делают virtual, чтобы подклассы не могли случайно изменить порядок шагов или пропустить часть логики.

Структура

  • AbstractClass - объявляет шаблонный метод и набор шагов (абстрактных и/или виртуальных).
  • ConcreteClass - переопределяет нужные шаги, не трогая сам шаблонный метод.

Когда применять

  • Несколько классов реализуют вариации одного и того же алгоритма, отличающиеся только частью шагов.
  • Нужно контролировать структуру алгоритма в одном месте (в базовом классе), позволяя подклассам настраивать только определённые точки расширения.
  • Общая логика должна быть переиспользована без копипасты между вариантами.

Плюсы

  • Устраняет дублирование общей части алгоритма.
  • Гарантирует единый порядок шагов во всех вариантах - его невозможно случайно нарушить в подклассе.
  • Хуки дают точечные точки расширения без необходимости переопределять весь алгоритм целиком.

Минусы

  • Основан на наследовании, а не на композиции - подкласс жёстко привязан к структуре базового класса и не может легко "собрать" алгоритм из частей разных базовых классов.
  • Чем больше шагов и хуков, тем сложнее читать итоговое поведение конкретного подкласса - нужно смотреть и в базовый класс, и в переопределения.
  • В runtime нельзя поменять поведение отдельного шага без создания нового подкласса (в отличие от Strategy, где реализация подставляется динамически).

Стратегия vs Шаблонный метод

Оба паттерна выделяют "изменяемую часть" поведения, но делают это по-разному: - Template Method использует наследование: изменяемая часть - это переопределённый метод в подклассе, порядок шагов зафиксирован в базовом классе. - Strategy использует композицию: изменяемая часть - это отдельный объект, подставляемый в контекст, который можно поменять в рантайме без создания нового класса.

Как правило, если нужно менять поведение динамически или без построения иерархии наследования - выбирают Strategy; если общей логики много, а меняется только один-два шага - компактнее выходит Template Method.

Реализация в C# через делегаты и методы расширения

Классический вариант через наследование - не единственный в .NET. Шаблонный метод можно выразить и без иерархии классов: - локальный шаблонный метод на основе делегатов - метод принимает делегаты на изменяемые шаги как параметры, вместо того чтобы требовать создания подкласса; - методы расширения - общий алгоритм пишется как метод расширения над интерфейсом с несколькими абстрактными операциями, что позволяет обойтись без базового класса вовсе.

Оба варианта дают тот же эффект (общая логика в одном месте, различающиеся шаги - подключаемые), но без накладных расходов на построение иерархии наследования - что часто удобнее с точки зрения тестируемости.

Пример в .NET Framework / BCL

  • Stream.CopyTo - общий алгоритм копирования, использующий переопределяемые Read/Write у конкретных наследников Stream.
  • Жизненный цикл ASP.NET Web Forms (Page_Init, Page_Load, Page_PreRender, ...) - классический (хоть и устаревший) пример шаблонного метода на уровне целого фреймворка.
  • Базовые классы IEnumerator/абстрактные коллекции, где общий алгоритм обхода фиксирован, а конкретное получение элемента переопределяется.

Пример реализации на C#

TemplateMethod.cs
using System;

namespace DesignPatterns.Behavioral.TemplateMethod
{
    // Базовый класс задаёт общий скелет алгоритма импорта данных.
    // Порядок шагов и общая логика (открыть, разобрать, сохранить, закрыть) не меняются,
    // а вот "разбор" данных отличается для каждого формата.
    public abstract class DataImporter
    {
        // Это и есть шаблонный метод: он не виртуальный, чтобы подклассы
        // не могли случайно изменить порядок или пропустить шаги.
        public void Import(string filePath)
        {
            Console.WriteLine($"Открываем файл {filePath}");
            string rawData = ReadFile(filePath);

            var records = Parse(rawData); // <- переменный шаг, реализуется подклассами

            Validate(records);
            Save(records);

            Console.WriteLine("Импорт завершён");
        }

        protected virtual string ReadFile(string filePath) => $"raw-content-of({filePath})";

        // Абстрактный шаг - обязателен для переопределения
        protected abstract string[] Parse(string rawData);

        // "Хук" - шаг с реализацией по умолчанию, который подкласс может переопределить,
        // но не обязан этого делать.
        protected virtual void Validate(string[] records)
        {
            Console.WriteLine($"Базовая проверка: записей {records.Length}");
        }

        protected virtual void Save(string[] records)
        {
            Console.WriteLine($"Сохраняем {records.Length} записей в базу");
        }
    }

    public sealed class CsvImporter : DataImporter
    {
        protected override string[] Parse(string rawData) =>
            rawData.Split(',');
    }

    public sealed class JsonImporter : DataImporter
    {
        protected override string[] Parse(string rawData) =>
            new[] { rawData }; // упрощённо, для примера

        // Переопределяем хук - для JSON нужна дополнительная проверка схемы
        protected override void Validate(string[] records)
        {
            base.Validate(records);
            Console.WriteLine("Дополнительно проверяем JSON-схему");
        }
    }

    public static class Demo
    {
        public static void Run()
        {
            DataImporter importer = new CsvImporter();
            importer.Import("data.csv");

            Console.WriteLine();

            importer = new JsonImporter();
            importer.Import("data.json");
        }
    }
}

Открыть TemplateMethod.cs отдельно Скачать TemplateMethod.cs