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

Введение

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

1. Идем напролом

Реализуем простой класс для отрисовки флага в консоли с помощью псевдографики. Используем ANSI escape-коды для цветов и символ блока. Длину полотна флага зададим в 10 блоков, используя метод String.repeat(), а ширину — 3 блока:
class Main {

    static final String WHITE = "\u001B[97m";
    static final String BLUE = "\u001B[94m";
    static final String RED = "\u001B[91m";
    static final String BLOCK = "█";
    static final int LEN = 10;

    public static void main(String[] args) {
        System.out.println("Флаг России:");

        System.out.println(WHITE + BLOCK.repeat(LEN));
        System.out.println(BLUE + BLOCK.repeat(LEN));
        System.out.println(RED + BLOCK.repeat(LEN));
        System.out.println(WHITE + BLOCK);
        System.out.println(BLOCK);
        System.out.println(BLOCK);
    }
}
Вывод результата на консоль:
Далее заказчик попросил доработать программу, чтобы она могла рисовать еще один трехцветный флаг — флаг Нидерландов.
Добавим новый код:
class Main {

    static final String WHITE = "\u001B[97m";
    static final String BLUE = "\u001B[94m";
    static final String RED = "\u001B[91m";
    static final String BLOCK = "█";
    static final int LEN = 10;

    public static void main(String[] args) {
        System.out.println("Флаг России:");

        System.out.println(WHITE + BLOCK.repeat(LEN));
        System.out.println(BLUE + BLOCK.repeat(LEN));
        System.out.println(RED + BLOCK.repeat(LEN));
        System.out.println(WHITE + BLOCK);
        System.out.println(BLOCK);
        System.out.println(BLOCK);

        System.out.println("\nФлаг Нидерландов:");

        System.out.println(RED + BLOCK.repeat(LEN));
        System.out.println(WHITE + BLOCK.repeat(LEN));
        System.out.println(BLUE + BLOCK.repeat(LEN));
        System.out.println(WHITE + BLOCK);
        System.out.println(BLOCK);
        System.out.println(BLOCK);
    }
}
Результат выполнения программы:
Мы выполнили и это задание — программа рисует необходимые заказчику флаги. Но, наш код ужасен: в нем не используется ООП, а также нарушен DRY-принцип — код дублируется.
Что если заказчик попросит добавить вывод флага Франции, Словении или Парагвая? Придется дублировать одни и те же 6 строк снова и снова, меняя только порядок цветов.
Исправлением этой проблемы мы займемся в последующих главах.

2. Используем ООП

Создадим для каждого флага свой класс: RussianFlag и NetherlandsFlag. Рисование полос конкретным цветом вынесем в методы класса Colors, что позволит избавиться от дублирования кода. При этом методы сделаем статическими (чтобы не создавать экземпляр):
class Colors {

    static final String WHITE = "\u001B[97m";
    static final String BLUE = "\u001B[94m";
    static final String RED = "\u001B[91m";
    static final String BLOCK = "█";
    static final int LEN = 10;

    public static void paintWhiteStripe() {
        System.out.println(WHITE + BLOCK.repeat(LEN));
    }

    public static void paintBlueStripe() {
        System.out.println(BLUE + BLOCK.repeat(LEN));
    }

    public static void paintRedStripe() {
        System.out.println(RED + BLOCK.repeat(LEN));
    }
}
Классы для рисования флага России и Нидерландов выглядят так:
class RussianFlag {

    void drawFlag() {
        Colors.paintWhiteStripe();
        Colors.paintBlueStripe();
        Colors.paintRedStripe();
        paintFlagpole();
    }

    private void paintFlagpole() {
        System.out.println(Colors.WHITE + Colors.BLOCK);
        System.out.println(Colors.BLOCK);
        System.out.println(Colors.BLOCK);
    }
}
class NetherlandsFlag {

    void drawFlag() {
        Colors.paintRedStripe();
        Colors.paintWhiteStripe();
        Colors.paintBlueStripe();
        paintFlagpole();
    }

    private void paintFlagpole() {
        System.out.println(Colors.WHITE + Colors.BLOCK);
        System.out.println(Colors.BLOCK);
        System.out.println(Colors.BLOCK);
    }
}
Изменим код класса Main:
class Main {

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

        System.out.println("\nФлаг Нидерландов:");
        NetherlandsFlag nf = new NetherlandsFlag();
        nf.drawFlag();
    }
}
Запустив данный класс, мы получим тот же самый результат, что и ранее — будут нарисованы два флага.
Код уже стал лучше. Но чтобы окончательно избавиться от дублирования, вынесем метод paintFlagpole() в абстрактный класс AbstractThreeStripeFlag. А уже от этого класса будут наследоваться классы для рисования конкретных флагов.
Это позволит:
  • убрать повторяющийся код из всех классов-флагов
  • централизовать логику отрисовки флагштока в одном месте
  • сделать добавление новых флагов проще — не придется копировать метод флагштока
abstract class AbstractThreeStripeFlag {

    void paintFlagpole() {
        System.out.println(Colors.WHITE + Colors.BLOCK);
        System.out.println(Colors.BLOCK);
        System.out.println(Colors.BLOCK);
    }
}
Внесем изменения в классы: добавим extends AbstractThreeStripeFlag и уберем реализацию метода paintFlagpole():
class RussianFlag extends AbstractThreeStripeFlag {

    void drawFlag() {
        Colors.paintWhiteStripe();
        Colors.paintBlueStripe();
        Colors.paintRedStripe();
        paintFlagpole();
    }
}
class NetherlandsFlag extends AbstractThreeStripeFlag {

    void drawFlag() {
        Colors.paintRedStripe();
        Colors.paintWhiteStripe();
        Colors.paintBlueStripe();
        paintFlagpole();
    }
}
Снова запустив класс Main, мы получим тот же самый результат — два требуемых флага.
Код стал значительно лучше, однако дублирование всё ещё осталось — метод drawFlag() по-прежнему содержит одинаковую структуру в обоих классах. В идеале необходимо описать общую структуру алгоритма один раз, а вариативные части (порядок цветов) делегировать дочерним классам. Об этом пойдет речь в следующей главе.

3. Реализуем Шаблонный метод

Взглянув еще раз на имеющийся код, можно заметить закономерность, выраженную в том, что для вывода флагов каждый раз использует одни и те же шаги: рисуется верхняя часть флага, средняя, нижняя и флагшток. Этот алгоритм неизменен для триколоров.
А что, если нам сделать методы для рисования полос флага абстрактными, а их реализацию делегировать (поручить) классам конкретных флагов? И даже сам алгоритм рисования флага поместить в абстрактный класс в метод drawFlag(). А чтобы его нельзя было переопределить (override) и изменить в классах наследниках, пометить его как final.
В итоге AbstractThreeStripeFlag будет выглядеть следующим образом:
abstract class AbstractThreeStripeFlag {

    abstract void paintUpperStripe();
    abstract void paintMiddleStripe();
    abstract void paintBottomStripe();

    final void drawFlag(String flagName) {
        System.out.println("\n" + flagName);
        paintUpperStripe();
        paintMiddleStripe();
        paintBottomStripe();
        paintFlagpole();
    }

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

    @Override
    void paintUpperStripe() {
        Colors.paintWhiteStripe();
    }

    @Override
    void paintMiddleStripe() {
        Colors.paintBlueStripe();
    }

    @Override
    void paintBottomStripe() {
        Colors.paintRedStripe();
    }
}
class NetherlandsFlag extends AbstractThreeStripeFlag {

    @Override
    void paintUpperStripe() {
        Colors.paintRedStripe();
    }

    @Override
    void paintMiddleStripe() {
        Colors.paintWhiteStripe();
    }

    @Override
    void paintBottomStripe() {
        Colors.paintBlueStripe();
    }
}
Окончательная версия класса Main:
class Main {

    public static void main(String[] args) {
        AbstractThreeStripeFlag rf = new RussianFlag();
        rf.drawFlag("Флаг России:");

        AbstractThreeStripeFlag nf = new NetherlandsFlag();
        nf.drawFlag("Флаг Нидерландов:");
    }
}
Теперь, когда наш код стал универсальным, вы можете самостоятельно изменить его для рисования других флагов: создать подходящие классы, добавить новые цвета и проч.

4. Что нужно знать про Шаблонный метод

В соответствии с классификацией паттернов GoF, «Шаблонный метод» относится к поведенческим паттернам.
Для каждого паттерна существует диаграмма, демонстрирующая его суть. Приведем такую диаграмму для обсуждаемого паттерна:
Диаграмма классов паттерна «Шаблонный метод»
AbstractClass — абстрактный класс, реализующий шаблонные методы, которые содержат скелет (шаблон) какого-то алгоритма.
Скелет алгоритма — это инвариантная (неизменяемая) часть, которая определяет последовательность шагов, но позволяет подклассам переопределять отдельные шаги без изменения общей структуры.
ConcreteClass — конкретный класс, реализующий абстрактные методы.
В нашем коде класс AbstractThreeStripeFlag соответствует AbstractClass, а RussianFlag и NetherlandsFlagConcreteClass.
В абстрактном классе кроме шаблонного метода могут быть и другие не абстрактные методы, используемые в шаблонном методе. Также вам не нужно делегировать абсолютно все шаги алгоритма подклассам, если в этом нет необходимости. Делегируйте только то, что будет изменяться.
В примере с флагами:
  • делегировали: paintUpperStripe(), paintMiddleStripe(), paintBottomStripe() (порядок цветов менялся)
  • не делегировали: paintFlagpole() (всегда одинаков)
Использование паттерна позволяет:
  • устранить дублирования кода
  • повысить переиспользуемость кода за счет наследования
  • сделать код более универсальным и легко изменяемым под новые условия
  • определить скелет алгоритма, отдельные шаги которого реализуются подклассами без изменения его структуры

5. Заблуждения

1
Mr. X
Можно ли для реализации шаблонного метода использовать вместо абстрактного класса интерфейс?
Max
Лучше этого не делать, если вы не хотите, чтобы кто-то «случайно» изменил структуру, заданного в методе алгоритма.
Проиллюстрируем это на примере. Воспользуемся вместо абстрактного класса интерфейсом:
interface ThreeStripeFlag {

    void drawUpperStrip();
    void drawMiddleStrip();
    void drawBottomStrip();

    default void drawFlag(String flagName) {
        System.out.println("\n" + flagName);
        drawUpperStrip();
        drawMiddleStrip();
        drawBottomStrip();
        paintFlagpole();
    }

    default void paintFlagpole() {
        System.out.println(Colors.WHITE + Colors.BLOCK);
        System.out.println(Colors.BLOCK);
        System.out.println(Colors.BLOCK);
    }
}
Обратите внимание, что мы не указываем слово abstract для методов drawUpperStrip(), drawMiddleStrip() и drawBottomStrip(), поскольку в интерфейсах методы без реализации по умолчанию абстрактны.
Метод drawFlagpole() и шаблонный drawFlag() объявлены как методы по умолчанию (default), что позволяет им иметь базовую реализацию, которую разрешается переопределить в классах, реализующих этот интерфейс.
Перепишем класс RussianFlag следующим образом:
class RussianFlag implements ThreeStripeFlag {

    @Override
    public void drawUpperStrip() {
        Colors.paintWhiteStripe();
    }

    @Override
    public void drawMiddleStrip() {
        Colors.paintBlueStripe();
    }

    @Override
    public void drawBottomStrip() {
        Colors.paintRedStripe();
    }

    @Override
    public void drawFlag(String flagName) {
        System.out.println("""
                Мы изменили алгоритм рисования RussianFlag.
                Не реализуйте шаблонные методы в интерфейсе.""");

    }
}
В классе RussianFlag мы «случайно» изменили шаблонный метод drawFlag() и вместо вывода флага РФ, отобразили в консоль посторонний текст красным цветом:
Таким образом, применяя вместо абстрактного метода интерфейс, мы смогли не только переопределить некоторые шаги алгоритма, но и изменить его структуру в целом, что противоречит идее паттерна.
Внимание: не используйте интерфейс как основу для шаблонного метода. Интерфейсы не могут его защитить ключевым словом final, поэтому для настоящего шаблонного метода используйте абстрактные классы.
2
Mr. X
Шаблонный метод обязательно должен состоять только из абстрактных методов?
Max
Шаблонный метод — это метод, который может содержать любой код без ограничений, а не только абстрактные методы.
Как было продемонстрировано и упомянуто выше в статье, шаблонный метод может содержать циклы, ветвление, переменные, вызовы неабстрактных методов и т. д.
3
Mr. X
Шаблонными методами являются все не абстрактные методы абстрактного класса?
Max
Не путайте шаблонность методов абстрактного класса как таковых для всех его наследников с методами, реализованными с помощью паттерна «Шаблонный метод». Шаблонными являются только те, которые содержат вызовы абстрактных методов и являются final (можно и без final, но это чревато трудноуловимыми ошибками).
Без final подклассы могут переопределить шаблонный метод и нарушить алгоритм, что приведет к непредсказуемому поведению системы.
4
Mr. X
Шаблонными методами являются абстрактные методы, т. к. они задают шаблон поведения для подклассов?
Max
Шаблонными являются методы, в которых вызываются абстрактные методы, а не сами абстрактные методы. Шаблонность в данном случае означает некий алгоритм, последовательность шагов, которая и является неизменяемым шаблоном. Абстрактные методы не содержат никаких шагов алгоритма, т. к. не имеют реализации.
5
Mr. X
Шаблонный метод не должен содержать более одного абстрактного метода?
Max
Шаблонный метод может содержать любое количество как абстрактных методов, так и любых операторов языка 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 «Метод не разрешен». Вы можете реализовать какой-либо из этих методов.

Заключение

Шаблонный метод — это мощный инструмент для борьбы с дублированием кода и стандартизации алгоритмов.
Как мы увидели на примере с флагами, он позволяет:
  • вынести общую структуру алгоритма в базовый класс
  • делегировать вариативные части подклассам
  • гарантировать корректность выполнения через final методы
  • упростить добавление новых вариантов алгоритма
Помните ключевые правила:
  • используйте абстрактные классы, а не интерфейсы
  • помечайте шаблонный метод final
  • делегируйте только то, что действительно меняется
Этот паттерн отлично подходит для задач, где несколько алгоритмов имеют одинаковую структуру, но отличаются в деталях реализации.
Авторы: Малянов Игорь и Чимаев Максим
Оцените статью, если она вам понравилась!