Также известен как Template Method

Шаблонный метод

Суть паттерна

Определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Позволяет подклассам переопределять шаги алгоритма, не меняя его общей структуры.

Проблема

Вы пишете программу для дата-майнинга в офисных документах. Пользователи будут загружать в неё документы в разных форматах (PDF, DOC, CSV), а программа должна извлекать из них полезную информацию.

В первой версии вы ограничились только парсингом DOC файлов. В следующей версии, добавили поддержку CSV. А через месяц прикрутили работу с PDF документами.

В какой-то момент вы заметили что код всех трёх классов парсинга документов хоть и отличается в части работы с файлами, но содержат довольно много общего в части самого извлечения данных. Было бы здорово избавиться от дублирования кода, оставив структуру алгоритмов без изменений.

К тому же остальной код, работающий с объектами этих классов наполнен условиями, проверяющими тип объектов перед началом работы. Весь этот код можно упростить, если объединить классы парсеров, либо свести их к общему интерфейсу.

Решение

Паттерн Шаблонный метод предлагает разбить алгоритм на последовательность шагов, описать шаги в отдельных методах и вызывать их в одном «шаблонном» методе друг за другом.

Это позволит подклассам переопределять некоторые шаги алгоритма, оставляя без изменений его структуру и остальные шаги, которые для этого подкласса не так важны.

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

method templateMethod() is
    openDocument();
    extractRawData();
    parseRawData();
    analyzeData();
    closeDocument();

Для начала, шаги шаблонного метода можно сделать абстрактными — все подклассы должны будут предоставить свою реализацию.

Но бывает, что в базовый класс можно вынести общее для всех подклассов поведение. В нашем случае, шаги открытия и закрытия документов будут отличаться для всех подклассов, поэтому останутся абстрактными. А вот одинаковый для всех типов документов код обработки данных переедет в базовый класс.

Есть и третий тип шагов — хуки. Хук — это «опциональный» шаг, метод с пустой реализацией, определённый в базовом классе. Шаблонный метод будет работать, даже если ни один подкласс не переопределит хук.

Аналогия из жизни

Строительство типовых домов

Строители используют подход, похожий на шаблонный метод при строительстве типовых домов. У них есть основной архитектурный проект, в котором расписаны шаги строительства — заливка фундамента, постройка стен, постановка крыши, установка окон, обивка и так далее.

Но несмотря на стандартизацию каждого этапа, строители могут делать небольшие изменения на любом из этапов, чтобы сделать дом чуточку непохожим на другие.

Структура

Схема структуры классов паттерна Шаблонный Метод
  1. Абстрактный класс определяет шаги алгоритма и содержит шаблонный метод, состоящий из вызовов этих шагов.

    Шаги могут быть как абстрактными, так и содержать реализацию по умолчанию.

  2. Конкретный класс переопределяет некоторые (или все) шаги алгоритма.

    Конкретные классы не переопределяют сам шаблонный метод.

Псевдокод

В этом примере Шаблонный метод используется как заготовка для стандартных действий искусственного интеллекта в простой игре. Для введения в игру новой расы, достаточно создать подкласс и реализовать в нём недостающие методы.

class GameAI is
    // Шаблонный метод должен быть задан в базовом классе. Он состоит из вызовов
    // методов в определённом порядке. Чаще всего эти методы являются шагами
    // некоего алгоритма.
    method turn() is
        collectResources()
        buildStructures()
        buildUnits()
        attack()

    // Некоторые из этих методов могут быть реализованы прямо в базовом классе.
    method collectResources() is
        foreach this.structures
            structure.collect()

    // А некоторые могут быть полностью абстрактными.
    abstract method buildStructures()
    abstract method buildUnits()

    // Кстати, шаблонных методов в классе может быть несколько.
    method attack() is
        enemy = closestEnemy()
        if (enemy == null)
            sendScouts(map.center)
        else
            sendWarriors(enemy.position)

    abstract method sendScouts(position)
    abstract method sendWarriors(position)

// Подклассы могут предоставлять свою реализацию шагов алгоритма, не изменяя сам
// шаблонный метод.
class OrcsAI extends GameAI is
    method buildStructures() is
        If enough resources then
            Build farms, then barracks, then stronghold.

    method buildUnits() is
        If enough resources
            If scouts not exist, build 1 peon.
            Else build grunt.

    // ...

    method sendScouts(position) is
        If scouts exists, send scouts to position.

    method sendWarriors(position) is
        If grunts are more than 5, then send warriors to position.

// Подклассы могут не только реализовывать абстрактные шаги, но и переопределять
// шаги, уже реализованные в базовом классе.
class MonstersAI extends GameAI is
    method collectResources() is
        Do nothing.

    method buildStructures() is
        Do nothing.

    method buildUnits() is
        Do nothing.

Применимость

Если подклассы должны расширять базовый алгоритм, не меняя его структуры.

Шаблонный метод позволяет подклассам расширять определённые шаги алгоритма через наследование, не меняя при этом структуру алгоритмов, объявленную в родительском классе.

У вас есть несколько классов, делающих одно и то же с незначительными отличиями. Если вы редактируете один класс, то приходится вносить такие же правки и в остальные классы.

Паттерн шаблонный метод предлагает создать для похожих классов общий суперкласс и оформить в нём главный алгоритма в виде шагов. Отличающиеся шаги можно переопределить в подклассах.

Это позволит убрать дублирование кода в нескольких классах с похожим поведением, но отличающихся в деталях.

Шаги реализации

  1. Изучите алгоритм и подумайте можно ли его разбить на шаги. Прикиньте какие шаги будут стандартными для всех вариаций алгоритма, а какие изменчивыми.

  2. Создайте абстрактный базовый класс. Опишите в нём шаблонный метод. Этот метод должен состоять из вызовов шагов алгоритма. Имеет смысл сделать шаблонный метод финальным, чтобы подклассы не могли переопределить его (если ваш язык это позволяет).

  3. Добавьте в абстрактный класс методы для каждого из шагов алгоритма. Стандартные шаги должны иметь реализацию по умолчанию. Изменяемые шаги должны быть объявлены абстрактными. Их нужно будет реализовать в подклассах.

  4. Подумайте о введении в алгоритм хуков. Чаще всего, хуки располагают между основными шагами алгоритма, а также до и после всех шагов.

  5. Создайте конкретные классы наследниками абстрактного класса. Реализуйте в них все недостающие шаги и хуки.

Преимущества и недостатки

  • Облегчает повторное использование кода.
  • Вы жёстко ограничены скелетом существующего алгоритма.
  • Вы можете нарушить Принцип подстановки Барбары Лисков, изменяя базовое поведение одного из шагов алгоритма через подкласс.
  • С ростом количества шагов, шаблонный метод становится слишком сложно поддерживать.

Отношения с другими паттернами

  • Фабричный метод можно рассматривать как частный случай Шаблонного метода. Кроме того, Фабричный метод нередко бывает частью большого класса с Шаблонными методами.

  • Шаблонный метод использует наследование, чтобы расширять части алгоритма. Стратегия использует делегирование, чтобы изменять алгоритм. Шаблонный метод работает на уровне классов. Стратегия позволяет менять логику отдельных объектов.

Реализация в различных языках программирования

Java