Шаблонный метод
Когда приходится спрашивать человека, какие паттерны проектирования ему приходилось использовать чаще всего, почему-то мало кто называет паттерн «Шаблонный метод» (Template Method). Вероятно, это связано с пробелом в знании номенклатуры паттернов, ибо лично я с трудом представляю себе, чтобы более-менее опытный программист ни разу не использовал такой удобный и полезный паттерн. Предлагаю ещё раз взглянуть на него поближе.
Итак, шаблонный метод. Никакого отношения к шаблонам c++ он не имеет. Данный паттерн примечателен тем, что он очень простой, интуитивно понятный, и крайне полезный. Относится он к категории паттернов поведения и служит одной простой цели — переопределению шагов некоторого алгоритма в семействе классов, производных от базового, определяющего структуру этого самого алгоритма.
Допустим, у мы пишем класс Crypt, который предназначен для шифрования некоторой строки текста. В классе определена функция шифрования:
void encrypt() {
// Установка начальных параметров
setupRnd();
setupAlgorithm();
// Получаем строку
std::string fContent = getString();
// Применяем шифрование
std::string enc = applyEncryption(fContent);
// Сохраняем строку
saveString(fContent);
// Подчищаем следы работы алгоритма
wipeSpace();
}
С помощью паттерна «Шаблонный метод» мы можем использовать алгоритм, представленный в функции encrypt (), чтобы работать со строками, полученными из разных источников — с клавиатуры, прочитанные с диска, полученные по сети. При этом сама структура алгоритма и неизменные шаги (установка начальных параметров, подчистка следов работы, и при желании применение шифрования) остаются неизменными. Это позволяет нам:
- Повторно использовать код, который не изменяется для различных подклассов;
- Определить общее поведение семейства подклассов, используя единожды определённый код;
- Разграничить права доступа — при реализации изменяемых шагов алгоритма мы будем использовать закрытые виртуальные функции. Это гарантирует, что такие операции будут вызываться только в качестве шагов модифицируемого алгоритма (или, скорее, не будут вызываться производными классами в неподходящих для этого местах).
Итак, дополним класс Crypt необходимыми членами:
private:
void setupRnd() {
// Некая инициализация алгоритма случайных чисел
std::cout << "setup rnd\n";
};
void setupAlgorithm() {
// Начальные установки алгоритма шифрования
std::cout << "setup algorithm\n";
};
void wipeSpace() {
// Удаление следов работы
std::cout << "wipe\n";
};
virtual std::string applyEncryption(const std::string& content) {
// Шифрование
std::string result = someStrongEncryption(content);
return result;
}
virtual std::string getString() = 0;
virtual void saveString(const std::string& content) = 0;
Обратите внимание, что функции закрытые. Это намеренное ограничение на их вызов, которое не мешает переопределить их в производном классе.
И, собственно, производный класс — шифрующий файл на диске:
class DiskFileCrypt : public Crypt {
public:
DiskFileCrypt(const std::string& fName)
: fileName(fName) {};
private:
std::string fileName;
virtual std::string getString() {
std::cout << "get disk file named \"" << fileName << "\"\n";
// Прочитать файл с диска и вернуть содержимое
return fileContent;
}
virtual void saveString(const std::string& content) {
std::cout << "save disk file named \"" << fileName << "\"\n";
// Записать файл на диск
}
};
Уже понятно, что при вызове
DiskFileCrypt d("foo.txt");
d.encrypt();
Будет выполнен алгоритм функции encrypt () и в консоли будет следующее: setup rnd
setup algorithm
get disk file named "foo.txt"
save disk file named "foo.txt"
wipe
Механизм виртуальных функций в примере выше служит исключительно для кастомизации поведения классов. Определяя их как чисто виртуальные можно задавать требования к наследующим классам. Например, если бы мы хотели, чтобы наследники определяли алгоритм шифрования, следовало бы сделать чисто виртуальной функцию-член класса applyEncryption.