Паттерн Шаблонный метод в Java

Введение
Предположим, нам поставили задачу написать программу, которая должна нарисовать трехцветный флаг России. При этом она будет действовать по следующему алгоритму:
  • нарисовать флагшток
  • нарисовать первую полосу белого цвета
  • нарисовать вторую полосу синего цвета
  • нарисовать третью полосу красного цвета
1. Идем напролом
Затея выглядит просто:
class Main {

    public static void main(String[] args) {
        System.out.println("Рисуем флаг России:");
        System.out.println("Флагшток нарисован");
        System.out.println("Полоса белого цвета нарисована");
        System.out.println("Полоса синего цвета нарисована");
        System.out.println("Полоса красного цвета нарисована");
    }
}
Результат выполнения программы:
Наша программа делает то, что требуется. Далее заказчик попросил написать код для рисования еще одного трехцветного флага — флага Нидерландов.
Добавим несколько строк кода:
class Main {

    public static void main(String[] args) {
        System.out.println("Рисуем флаг России:");
        System.out.println("Флагшток нарисован");
        System.out.println("Полоса белого цвета нарисована");
        System.out.println("Полоса синего цвета нарисована");
        System.out.println("Полоса красного цвета нарисована");

        System.out.println("\nРисуем флаг Нидерландов:");
        System.out.println("Флагшток нарисован");
        System.out.println("Полоса красного цвета нарисована");
        System.out.println("Полоса белого цвета нарисована");
        System.out.println("Полоса синего цвета нарисована");
    }
}
Результат выполнения программы:
Мы выполнили и это задание — программа рисует необходимые заказчику флаги.
Но, наш код ужасен! Мы не используем объектно-ориентированный подход, и у нас нарушен DRY-принцип — код дублируется.
2. Используем ООП
Давайте создадим для каждого флага свой класс: RussianFlag и NetherlandsFlag. Рисование конкретным цветом вынесем в методы, которые поместим в класс Colors, что позволит избавиться от дублирования кода.
Методы рисования красками сделаем статическими (чтобы не создавать экземпляр):
class Colors {

    static void paintWhiteColor() {
        System.out.println("Полоса белого цвета нарисована");
    }

    static void paintBlueColor() {
        System.out.println("Полоса синего цвета нарисована");
    }

    static void paintRedColor() {
        System.out.println("Полоса красного цвета нарисована");
    }
}
Классы для рисования флага России и Нидерландов выглядят так:
class RussianFlag {

    void drawFlag() {
        drawFlagpole();
        Colors.paintWhiteColor();
        Colors.paintBlueColor();
        Colors.paintRedColor();
    }

    private void drawFlagpole() {
        System.out.println("Флагшток нарисован");
    }
}
class NetherlandsFlag {

    void drawFlag() {
        drawFlagpole();
        Colors.paintRedColor();
        Colors.paintWhiteColor();
        Colors.paintBlueColor();
    }

    private void drawFlagpole() {
        System.out.println("Флагшток нарисован");
    }
}
Класс Main теперь выглядит так:
class Main {

    public static void main(String[] args) {
        System.out.println("Рисуем флаг России:");
        RussianFlag russianFlag = new RussianFlag();
        russianFlag.drawFlag();

        System.out.println("\nРисуем флаг Нидерландов:");
        NetherlandsFlag netherlandsFlag = new NetherlandsFlag();
        netherlandsFlag.drawFlag();
    }
}
Запустив это код, мы получим тот же самый результат, что и ранее — будут нарисованы два флага.
Код уже стал лучше, но мы можем вынести рисование флагштока в отдельный класс Flagpole, чтобы метод не дублировался. А уже от этого класса будут наследоваться классы для рисования конкретных флагов.
Класс Flagpole выглядит так:
class Flagpole {

    static void drawFlagpole() {
        System.out.println("Флагшток нарисован");
    }
}
Внесем изменения в классы RussianFlag и NetherlandsFlag: добавим extends Flagpole и уберем метод drawFlagpole:
class RussianFlag extends Flagpole {

    void drawFlag() {
        drawFlagpole();
        Colors.paintWhiteColor();
        Colors.paintBlueColor();
        Colors.paintRedColor();
   }
}
class NetherlandsFlag extends Flagpole {

    void drawFlag() {
        drawFlagpole();
        Colors.paintRedColor();
        Colors.paintWhiteColor();
        Colors.paintBlueColor();
   }
}
Снова запустив класс Main, мы получим тот же самый результат — два требуемых флага.
3. Реализуем Шаблонный метод
Внезапно мы понимаем, что каждый раз действуем по одному и тому же алгоритму: рисуем флагшток, верхнюю часть флага, среднюю и нижнюю. Этот алгоритм неизменен для рисования флагов с тремя полосами. А что, если нам сделать методы для рисования полос флага абстрактными, а их реализацию делегировать (поручить) классам конкретных флагов? И даже сам алгоритм рисования флага поместить в абстрактный класс в метод drawFlag. А чтобы его нельзя было переопределить (изменить) при наследовании, пометить его как final.
Класс Flagpole становится ненужным, поскольку рисование флагштока также можно перенести в абстрактный класс. Итак, класс AbstractThreeRowsFlag будет выглядеть следующим образом:
abstract class AbstractThreeRowsFlag {

    abstract void drawUpperLevel();
    abstract void drawMiddleLevel();
    abstract void drawBottomLevel();

    final void drawFlag() {
        drawFlagpole();
        drawUpperLevel();
        drawMiddleLevel();
        drawBottomLevel();
    }

    private void drawFlagpole() {
        System.out.println("Флагшток нарисован");
    }
}
Метод drawFlag(), содержащий алгоритм рисования флага, является шаблонным методом. Он так называется потому, что задает скелет в виде последовательности шагов, которые будут переопределять (реализовывать), каждый по своему, наследники AbstractThreeRowsFlag. Его подклассы не смогут изменить последовательность шагов, т.к. шаблонный метод помечен, как final и не наследуется.
Как вы уже поняли, для рисования флагов мы только что использовали паттерн под названием «Шаблонный метод» (Template Method), благодаря которому мы избавились от дублирования кода, а также повысили его универсальность и переиспользуемость.
Обратите внимание, не все методы в шаблонном методе являются абстрактными, например drawFlagpole (рисование флагштока). Из этого можно сделать вывод, что шаблонный метод не обязательно должен состоять только из абстрактных методов, которые необходимо переопределять в подклассах. Он может содержать переменные, какую-то внутреннюю логику и т. д. Но очень часто новички, читая материал по данному паттерну, ошибочно полагают, что шаблонный метод — это метод, который состоит только из абстрактных методов и ничего другого содержать не может. Это и не удивительно, в интернете полно статей с примерами таких методов, что сбивает с толку.
Вернемся к нашим флагам, классы для рисования которых уже выглядят так (про @Override можно прочитать в нашей прошлой статье):
class RussianFlag extends AbstractThreeRowsFlag {

    @Override
    void drawUpperLevel() {
        Colors.paintWhiteColor();
    }

    @Override
    void drawMiddleLevel() {
        Colors.paintBlueColor();
    }

    @Override
    void drawBottomLevel() {
        Colors.paintRedColor();
    }
}
class NetherlandsFlag extends AbstractThreeRowsFlag {

    @Override
    void drawUpperLevel() {
        Colors.paintRedColor();
    }

    @Override
    void drawMiddleLevel() {
        Colors.paintWhiteColor();
    }

    @Override
    void drawBottomLevel() {
        Colors.paintBlueColor();
    }
}
Класс Main теперь выглядит так:
class Main {
    public static void main(String[] args) {
        System.out.println("Рисуем флаг России:");
        AbstractThreeRowsFlag russianFlag = new RussianFlag();
        russianFlag.drawFlag();

        System.out.println("\nРисуем флаг Нидерландов:");
        AbstractThreeRowsFlag netherlandsFlag = new NetherlandsFlag();
        netherlandsFlag.drawFlag();
    }
}
Теперь можно нарисовать флаг Югославии, Германии (добавив к нашему набору цветов необходимые: черный и желтый) и любые другие подобные трехцветные флаги, применяя ООП и максимально переиспользуя существующий код.
4. Что нужно знать про паттерн Шаблонный метод
В соответствии с классификацией паттернов GoF, паттерн Шаблонный метод (Template Method) относится к паттернам поведения.
4.1 Классическая диаграмма Шаблонного метода
AbstractClass — абстрактный класс, реализующий шаблонные методы, которые содержат скелет какого-то алгоритма (шаблон).
ConcreteClass — конкретный класс, реализующий абстрактные методы.
Предложенная выше диаграмма классов демонстрируется в каждом учебнике по паттернам. Благодаря этому, очень часто, новички думают, что шаблонный метод — это метод, который состоит исключительно из абстрактных шагов алгоритма, но это не так. Еще раз акцент на том, что в абстрактном классе кроме не абстрактного шаблонного метода могут быть и другие не абстрактные методы, используемые в шаблонном методе. Вам не нужно делегировать абсолютно все шаги алгоритма подклассам, если в этом нет необходимости. Делегируйте только то, что будет изменяться. Шаблонный метод также может включать в себя переменные, циклы, ветвление и т. п., не забывайте про это.
4.2 Назначение паттерна
Использование паттерна Шаблонный метод позволяет:
  • устранить дублирования кода
  • повысить переиспользуемость кода за счет наследования
  • сделать код более универсальным и легко изменяемым под новые условия
Например, мы хотим, чтобы в коде была предусмотрена возможность опционально рисовать на флаге герб страны. В этом случае, в абстрактный класс и в шаблонный метод нужно добавить метод-пустышку drawBlazon(). В подклассе, при необходимости, мы можем переопределить этот метод требуемым нам поведением, в данном случае рисованием герба на флаге.
В таком случае, абстрактный класс AbstractThreeRowsFlag будет выглядеть следующим образом:
abstract class AbstractThreeRowsFlag {

    abstract void drawUpperLevel();
    abstract void drawMiddleLevel();
    abstract void drawBottomLevel();

    final void drawFlag() {
        drawFlagpole();
        drawUpperLevel();
        drawMiddleLevel();
        drawBottomLevel();
        drawBlazon();
    }

    void drawFlagpole() {
        System.out.println("Флагшток нарисован");
    }

    void drawBlazon() {}
}
Класс для рисования флага России с гербом в этом случае примет следующий вид:
class RussianFlag extends AbstractThreeRowsFlag {

    @Override
    void drawUpperLevel() {
        Colors.paintWhiteColor();
    }

    @Override
    void drawMiddleLevel() {
        Colors.paintBlueColor();
    }

    @Override
    void drawBottomLevel() {
        Colors.paintRedColor();
    }

    @Override
    void drawBlazon() {
        System.out.println("Герб нарисован");
    }
}
Запустим код:
  • Также шаблонный метод определяет основу алгоритма и позволяет подклассам переопределить некоторые его шаги, не изменяя структуру в целом.
Обратите внимание на «… не изменяя структуру в целом.» Таким образом, подклассы абстрактного класса не должны иметь возможность переопределить шаблонный метод с целью измения логики его работы. По этой причине шаблонные методы объявляются как final и не могут использовать интерфейсы с их методами по умолчанию (появились в Java 8) вместо абстрактных классов. Вся идея паттерна в этом случае будет нарушена.
Проиллюстрируем это на примере. Давайте воспользуемся вместо абстрактного класса интерфейсом:
interface ThreeRowsFlag {

    void drawUpperLevel();
    void drawMiddleLevel();
    void drawBottomLevel();

    default void drawFlag() {
        drawFlagpole();
        drawUpperLevel();
        drawMiddleLevel();
        drawBottomLevel();
    }

    default void drawFlagpole() {
        System.out.println("Флагшток нарисован");
    }
}
Методы drawUpperLevel, drawMiddleLevel и drawBottomLevel уже без ключевого слова abstract, поскольку в интерфейсах методы без реализации по умолчанию абстрактны.
Метод drawFlagpole и шаблонный метод drawFlag объявлены как методы по умолчанию, что позволяет разместить в теле интерфейса их реализацию. Как мы знаем, default-методы разрешается переопределить в классе, реализующим этот интерфейс.
Перепишем класс RussianFlag следующим образом:
class RussianFlag implements ThreeRowsFlag {

    @Override
    public void drawUpperLevel() {
        Colors.paintWhiteColor();
    }

    @Override
    public void drawMiddleLevel() {
        Colors.paintBlueColor();
    }

    @Override
    public void drawBottomLevel() {
        Colors.paintRedColor();
    }
    @Override
    public void drawFlag() {
        drawFlagpole();
        System.err.println("Мы изменили алгоритм рисования флага");
        System.err.println("Не реализуйте шаблонные методы в интерфейсе");
    }
}
В классе RussianFlag мы «случайно» смогли изменить шаблонный метод drawFlag и вместо рисования флага РФ, вывели в консоль «левый» текст красным цветом:
Таким образом, применяя вместо абстрактного метода интерфейс, мы смогли не только переопределить некоторые шаги алгоритма, но и изменить его структуру в целом, что противоречит определению паттерна Шаблонный метод. Не используйте интерфейс как основу для шаблонного метода.
5. Заблуждения
—  Можно ли для реализации шаблонного метода использовать вместо абстрактного класса интерфейс?
—  Нет ничего невозможного, но лучше этого не делать, если вы не хотите, чтобы кто-то «случайно» изменил его структуру.
—  Шаблонный метод обязательно должен состоять только из абстрактных методов?
—  Шаблонный метод — это метод, который может содержать любой код без ограничений, а не только абстрактные методы.
—  Шаблонными методами являются все не абстрактные методы абстрактного класса?
—  Не путайте шаблонность методов абстрактного класса как таковых для всех его наследников с методами, реализованными с помощью паттерна Шаблонный метод. Шаблонными являются только те, которые содержат абстрактные методы и являются final (можно и без final, но это чревато сложноуловимыми ошибками).
—  Шаблонными методами являются абстрактные методы, т.к. они задают шаблон поведения для подклассов?
— Шаблонными являются методы, в которых вызываются абстрактные методы, а не сами абстрактные методы. Шаблонность в данном случае означает некий алгоритм, последовательность шагов, которая и является неизменяемым шаблоном. Абстрактные методы не содержат никаких шагов алгоритма, т.к. не имеют реализации.
— Шаблонный метод не должен содержать более одного абстрактного метода?
— Шаблонный метод может содержать любое количество как абстрактных методов, так и любых операторов языка Java.
6. Реализация шаблонного метода в JDK
Паттерн шаблонный метод широко используется в библиотеках Java и присутствует, например, во всех не абстрактных методах библиотек java.io.InputStream, java.io.OutputStream, java.io.Reader, java.io.Writer, java.util.AbstractList, java.util.AbstractSet, java.util.AbstractMap и в других.
В классе javax.servlet.http.HttpServlet, все методы типа doXXX () по умолчанию отправляют ошибку HTTP 405 «Метод не разрешен». Вы можете реализовать какой-либо из этих методов.
Заключение
В этой статье мы рассказали на примерах про шаблонный метод и способ его реализации в Java.
Автор: Малянов Игорь
Технический редактор: Чимаев Максим
Оцените статью, если она вам понравилась!