|
© 2026
Александр Легалов
Содержание
Шаблонный метод (Template Method)
Шаблонный метод определяет основу алгоритма и позволяет подклассам переопределить некоторые шаги алгоритма, не изменяя его структуру в целом. Его предлагается применять с следующих ситуациях:
- при однократном использовании инвариантных частей алгоритмов, при этом реализация изменяющегося поведения остается на усмотрение подклассов;
- когда необходимо вычленить и локализовать в одном классе поведение, общее для всех подклассов, чтобы избежать дублирования кода, напоминающее технику «вынесения за скобки с целью обобщения». Сначала выявляются различия в существующем коде, которые затем выносятся в отдельные операции. В конечном итоге различающиеся фрагменты кода заменяются шаблонным методом, из которого вызываются новые операции;
- если требуется управление расширениями подклассов. Шаблонный метод можно определить так, что он будет вызывать операции-зацепки (hooks, хуки) в определенных точках, разрешив тем самым расширение только в этих точках.
Графическое представление ОО структуры Шаблонного метода приведено на рисунке.
Структура паттерна Шаблонный метод
Объектно-ориентированная реализация Шаблонного метода
Организация образца в целом тривиальна. Имея в основе базового класса некоторый метод, определяющий точку входа (в общем случае не обязательно одну), он имеет множество виртуальных методов, вызываемых в этой точке, которые могут переопределяться в дочерних классах. Эти дополнительные методы базового класса могут быть как чистыми, так и реализовывать каки-нибудь алгоритмы, что зависит от нюансов использования. Вернемся к нашим животным. Пусть основной метод определяет некоторый распорядок действий и поведений любого из них, такие как: время активного поведения, время сна, выполнение обычных действий, издаваемые звуки. Наверное хватит... Пусть эти действия в базовом классе Animal образуют одинаковую для всех последовательность, а каждое животное обеспечивает свою интерпретацию. Код, определяющий базовый класс и его методы, выглядит следующим образом.
// Обобщенное животное, определяемое в базовом классе.
class Animal {
protected:
std::string name; // имя животного, чтобы отличать экземпляры
int age; // возраст
public:
Animal(std::string n, int a): name{n}, age{a} {}
virtual ~Animal() {}
void PrintNameAndAge() {
std::cout << "My name is " << name << ". I'm " << age << " years old.\n";
}
virtual void Info() = 0; // Общие сведения
virtual void StandUp() = 0; // когда встает
virtual void Action() = 0; // повседневное занятие
virtual void Voice() = 0; // какой голос
void Behavior() { // поведение - комплекс действий
Info();
StandUp();
Voice();
Action();
}
};
Хуки и прочие рюшечки, описанные бандой четырех, особой роли не играют, поэтом при сопоставлении ООП и ППП ими можно пренебречь.
В качестве животных, предоставляющих свои "режимы дня", сформируем классы Петуха и Собаки. Каждое из животных по своему реализует свои действия, которые при этом одинаков укладываются в общий регламент, предоставляемый базовым классом.
// Петух
class Rooster : public Animal {
public:
Rooster(std::string n, int a): Animal{n, a} {}
void Info() {
std::cout << "Hi! I am the Rooster. ";
PrintNameAndAge();
}
void Voice() {
std::cout << "I shout \"Cock-a-doodle-doo!\" all day long.\n";
}
void StandUp() {
std::cout << "I get up early in the morning and go to bed when the sun sets.\n";
}
void Action() {
std::cout << "I spend all day chasing chickens.\n";
}
};
// Собака
class Dog : public Animal {
public:
Dog(std::string n, int a): Animal{n, a} {}
void Info() {
std::cout << "Hello! I am the Dog. ";
PrintNameAndAge();
}
void Voice() {
std::cout << "I bark \"woof-woof\" when I see strangers.\n";
}
void StandUp() {
std::cout << "I sleep all day and wake up in the evening.\n";
}
void Action() {
std::cout << "My job is to protect the house from burglars.\n";
}
};
Клиент используется в тестовой функции и осуществляет только получение указателя на базовый класс Animal, через который подключаются конкретные животные, использующие разные стратегии. Это позволяет использовать ОО полиморфизм. Тестовая функция используется для создания пары животных и обращение к ним через клиентский код.
// Клиент использует шаблонный метод через указатель на обобщение
void ClientCode(Animal* a) {
a->Behavior();
}
// Тестовая функция, создающая животных
// и запускающая их поведение через клиента.
int main() {
Rooster rooster("Petya", 2);
ClientCode(&rooster);
std::cout << "\n";
Dog dog("Rex", 5);
ClientCode(&dog);
return 0;
}
Процедурно-параметрические отношения в Шаблонном методе
Если сопоставить ПП схему Шаблонного метода с другими вариантами, то можно заметить сходство со Строителем. Там точно также используется цепочка более простых действий, но только для порождения программных объектов. Это говорит о том, что можно комбинировать поведение обоих паттернов в одном методе, осуществляя создание объектов и обеспечивая тут же их поведение. Для 4П это непринципиально, так как в любом случае данные и функции разделены, что позволяет не обращать внимание на сочетание любых комбинаций образуемых ОО паттернами проектирования.
Процедурное представление аналога Шаблонного метода
Процедурно-параметрическая имитация Шаблонного метода
Как обычно процедурно-параметрическая версия программы непосредственно реализует эту схему, эволюционно расширяя как обобщенное животное, так и обработчики обобщений, отвечающие за отдельные поведения. Обобщенное животное и использующие его обычные и абстрактные обобщенные функции выглядят следующим образом:
// Компилировать: ppclang -xc template-method.ppc -o template-method
#include <stdio.h>
//------------------------------------------------------------------------------
// Обобщенное животное
typedef struct Animal {
const char* name;
int age;
}<> Animal;
// Начальная инициализация вместо отсутствующего конструктора.
void InitAnimal(Animal* a, const char* name, int age) {
a->name = name;
a->age = age;
}
// Вывод имени и возраста
void PrintNameAndAge(Animal* a) {
printf("My name is %s. I'm %d years old.\n", a->name, a->age);
}
// Абстрактные обобщенные функции
void Info<Animal* a>() = 0;
void Voice<Animal* a>() = 0;
void StandUp<Animal* a>() = 0;
void Action<Animal* a>() = 0;
// Имитация шаблонного метода, собирающего все воедино
void Behavior(Animal* a) { // поведение - комплекс действий
Info<a>();
StandUp<a>();
Voice<a>();
Action<a>();
}
Конкретные Петух и Собака создаются как основы специализаций (в общем случае со своими данными) и эволюционно интегрируются с обобщением, образуя специализации. Каждая абстрактная обобщенная функция расширяется соответствующими обработчиками специализаций.
//------------------------------------------------------------------------------
// Петух
typedef struct Rooster {} Rooster;
Animal + <rooster: Rooster;>;
void Info<Animal.rooster* a>() {
printf("Hi! I am the Rooster. ");
PrintNameAndAge(a);
};
void StandUp<Animal.rooster* a>() {
printf("I shout \"Cock-a-doodle-doo!\" all day long\n");
};
void Voice<Animal.rooster* a>() {
printf("I get up early in the morning and go to bed when the sun sets.\n");
};
void Action<Animal.rooster* a>() {
printf("I spend all day chasing chickens.\n");
};
//------------------------------------------------------------------------------
// Собака
typedef struct Dog {} Dog;
Animal + <dog: Dog;>;
void Info<Animal.dog* a>() {
printf("Hello! I am the Dog. ");
PrintNameAndAge(a);
};
void StandUp<Animal.dog* a>() {
printf("I bark \"woof-woof\" when I see strangers.\n");
};
void Voice<Animal.dog* a>() {
printf("I sleep all day and wake up in the evening.\n");
};
void Action<Animal.dog* a>() {
printf("My job is to protect the house from burglars.\n");
};
Внешний клиент и тестовая функция реализованы аналогично ОО коду.
// Клиент использует шаблонный метод через указатель на обобщение
void ClientCode(Animal* a) {
Behavior(a);
}
int main() {
Animal.rooster rooster;
InitAnimal(&rooster, "Petya", 2);
ClientCode(&rooster);
printf("\n");
Animal.dog dog;
InitAnimal(&dog, "Rex", 5);
ClientCode(&dog);
return 0;
}
Резюме
На мой взгля Шаблонный метод является одним из простых и очевидных технических приемов, обеспечивающим расширение функциональности альтернатив с применением динамического полиморфизма. В реальной жизни он достаточно часто использовался и до появления паттернов проектирования. ПП подход обеспечивает большую гибкость, позволяя при этом, за счет использования внешних функций вместо методов, добавлять разнообразную новую функциональность. При необходимости можно безболезнненно для данных сформировать обобщенные функции, расширяющие поведение, добавить новые группирующие функции, изменяющие уже существующие комбинации поведения или использующие вновь добавляемое поведение. В целом при этом используются простые технические приемы расширения функциональности программы, которые уже были многократно описаны при сравнении элементарных расширений.
Содержание
|