Template Method
Template Method — это поведенческий паттерн проектирования, который определяет скелет алгоритма в базовом классе, позволяя подклассам переопределять определенные шаги алгоритма, не изменяя его общую структуру.
Проблема
Представьте, что вы создаете приложение для анализа данных. Вам нужно обрабатывать различные типы документов: PDF, Word, Excel. Каждый тип документа требует своего способа извлечения данных, но общий процесс анализа одинаков:
- Открыть документ
- Извлечь данные
- Обработать данные
- Создать отчет
- Закрыть документ
Если вы реализуете это для каждого типа документа отдельно, вы получите много дублированного кода.
Решение
Паттерн Template Method предлагает разбить алгоритм на последовательность шагов, превратить эти шаги в методы и поместить вызовы этих методов в один "шаблонный" метод.
Структура
AbstractClass
├── templateMethod(): void
├── primitiveOperation1(): void
├── primitiveOperation2(): void
└── primitiveOperation3(): void
ConcreteClass
├── primitiveOperation1(): void
├── primitiveOperation2(): void
└── primitiveOperation3(): void
Пример реализации
JavaScript
// Абстрактный класс с шаблонным методом
class DataProcessor {
// Шаблонный метод - определяет скелет алгоритма
processData(data) {
console.log('=== Начало обработки данных ===');
this.validateData(data);
const extractedData = this.extractData(data);
const processedData = this.processExtractedData(extractedData);
const result = this.generateResult(processedData);
console.log('=== Обработка данных завершена ===');
return result;
}
// Конкретные шаги (могут быть переопределены)
validateData(data) {
console.log('Базовая валидация данных');
if (!data || data.length === 0) {
throw new Error('Данные не могут быть пустыми');
}
}
extractData(data) {
throw new Error('Метод extractData должен быть переопределен');
}
processExtractedData(data) {
console.log('Базовая обработка извлеченных данных');
return data;
}
generateResult(data) {
console.log('Генерация результата');
return {
processedAt: new Date().toISOString(),
data: data,
status: 'success'
};
}
}
// Конкретная реализация для CSV файлов
class CSVProcessor extends DataProcessor {
extractData(data) {
console.log('Извлечение данных из CSV');
const lines = data.split('\n');
const headers = lines[0].split(',');
const rows = lines.slice(1).map(line => {
const values = line.split(',');
const row = {};
headers.forEach((header, index) => {
row[header.trim()] = values[index] ? values[index].trim() : '';
});
return row;
});
return rows;
}
processExtractedData(data) {
console.log('Обработка CSV данных');
return data.map(row => ({
...row,
processed: true,
timestamp: new Date().toISOString()
}));
}
}
// Конкретная реализация для JSON файлов
class JSONProcessor extends DataProcessor {
extractData(data) {
console.log('Извлечение данных из JSON');
try {
return JSON.parse(data);
} catch (error) {
throw new Error('Неверный формат JSON');
}
}
processExtractedData(data) {
console.log('Обработка JSON данных');
if (Array.isArray(data)) {
return data.map(item => ({
...item,
processed: true,
timestamp: new Date().toISOString()
}));
} else {
return {
...data,
processed: true,
timestamp: new Date().toISOString()
};
}
}
}
// Конкретная реализация для XML файлов
class XMLProcessor extends DataProcessor {
extractData(data) {
console.log('Извлечение данных из XML');
// Упрощенный парсинг XML (в реальности используйте библиотеки)
const items = [];
const itemRegex = /<item>(.*?)<\/item>/gs;
let match;
while ((match = itemRegex.exec(data)) !== null) {
const itemContent = match[1];
const item = {};
const fieldRegex = /<(\w+)>(.*?)<\/\1>/g;
let fieldMatch;
while ((fieldMatch = fieldRegex.exec(itemContent)) !== null) {
item[fieldMatch[1]] = fieldMatch[2];
}
items.push(item);
}
return items;
}
processExtractedData(data) {
console.log('Обработка XML данных');
return data.map(item => ({
...item,
processed: true,
timestamp: new Date().toISOString(),
source: 'XML'
}));
}
}
// Использование
const csvData = `name,age,city
John,25,New York
Jane,30,London
Bob,35,Paris`;
const jsonData = JSON.stringify([
{ name: 'Alice', age: 28, city: 'Berlin' },
{ name: 'Charlie', age: 32, city: 'Madrid' }
]);
const xmlData = `<items>
<item>
<name>David</name>
<age>27</age>
<city>Rome</city>
</item>
<item>
<name>Eve</name>
<age>29</age>
<city>Amsterdam</city>
</item>
</items>`;
console.log('=== Обработка CSV данных ===');
const csvProcessor = new CSVProcessor();
const csvResult = csvProcessor.processData(csvData);
console.log('Результат:', csvResult);
console.log('\n=== Обработка JSON данных ===');
const jsonProcessor = new JSONProcessor();
const jsonResult = jsonProcessor.processData(jsonData);
console.log('Результат:', jsonResult);
console.log('\n=== Обработка XML данных ===');
const xmlProcessor = new XMLProcessor();
const xmlResult = xmlProcessor.processData(xmlData);
console.log('Результат:', xmlResult);
TypeScript с интерфейсами
// Абстрактный класс для построения домов
abstract class HouseBuilder {
// Шаблонный метод
buildHouse(): void {
console.log('=== Начало строительства дома ===');
this.prepareFoundation();
this.buildWalls();
this.installRoof();
this.installWindows();
this.installDoors();
this.addFinishingTouches();
console.log('=== Строительство дома завершено ===');
}
// Общие шаги
prepareFoundation(): void {
console.log('Подготовка фундамента');
}
buildWalls(): void {
console.log('Возведение стен');
}
installRoof(): void {
console.log('Установка крыши');
}
// Шаги, которые должны быть переопределены
abstract installWindows(): void;
abstract installDoors(): void;
abstract addFinishingTouches(): void;
}
// Конкретная реализация для деревянного дома
class WoodenHouseBuilder extends HouseBuilder {
installWindows(): void {
console.log('Установка деревянных окон');
}
installDoors(): void {
console.log('Установка деревянных дверей');
}
addFinishingTouches(): void {
console.log('Добавление деревянных элементов декора');
console.log('Покрытие лаком');
}
}
// Конкретная реализация для кирпичного дома
class BrickHouseBuilder extends HouseBuilder {
installWindows(): void {
console.log('Установка пластиковых окон');
}
installDoors(): void {
console.log('Установка металлических дверей');
}
addFinishingTouches(): void {
console.log('Штукатурка стен');
console.log('Покраска фасада');
}
}
// Конкретная реализация для современного дома
class ModernHouseBuilder extends HouseBuilder {
installWindows(): void {
console.log('Установка панорамных окон');
}
installDoors(): void {
console.log('Установка стеклянных дверей');
}
addFinishingTouches(): void {
console.log('Установка умных систем');
console.log('Добавление LED освещения');
console.log('Установка солнечных панелей');
}
}
// Использование
console.log('=== Строительство деревянного дома ===');
const woodenBuilder = new WoodenHouseBuilder();
woodenBuilder.buildHouse();
console.log('\n=== Строительство кирпичного дома ===');
const brickBuilder = new BrickHouseBuilder();
brickBuilder.buildHouse();
console.log('\n=== Строительство современного дома ===');
const modernBuilder = new ModernHouseBuilder();
modernBuilder.buildHouse();
Когда использовать
Используйте Template Method, когда:
- Есть несколько классов с похожими алгоритмами
- Нужно избежать дублирования кода
- Хотите контролировать расширение алгоритма
- Нужно определить скелет алгоритма, но позволить подклассам переопределять некоторые шаги
Преимущества
- Переиспользование кода
- Контроль над алгоритмом
- Принцип открытости/закрытости: можно добавлять новые варианты алгоритма
- Устраняет дублирование кода
Недостатки
- Может усложнить код из-за наследования
- Нарушает принцип подстановки Лисков
- Сложно изменять структуру алгоритма
Отличия от других паттернов
- Strategy — выбирает алгоритм во время выполнения
- Command — инкапсулирует запросы как объекты
- State — изменяет поведение при изменении состояния
Реальный пример
// Template Method для генерации отчетов
class ReportGenerator {
generateReport(data) {
console.log('=== Генерация отчета ===');
const processedData = this.processData(data);
const formattedData = this.formatData(processedData);
const report = this.createReport(formattedData);
this.saveReport(report);
console.log('=== Отчет сгенерирован ===');
return report;
}
processData(data) {
console.log('Базовая обработка данных');
return data;
}
formatData(data) {
throw new Error('Метод formatData должен быть переопределен');
}
createReport(data) {
console.log('Создание отчета');
return {
data: data,
generatedAt: new Date().toISOString(),
type: this.getReportType()
};
}
saveReport(report) {
console.log(`Сохранение отчета типа: ${report.type}`);
}
getReportType() {
throw new Error('Метод getReportType должен быть переопределен');
}
}
class PDFReportGenerator extends ReportGenerator {
formatData(data) {
console.log('Форматирование данных для PDF');
return data.map(item => ({
...item,
formatted: true,
format: 'PDF'
}));
}
getReportType() {
return 'PDF';
}
}
class ExcelReportGenerator extends ReportGenerator {
formatData(data) {
console.log('Форматирование данных для Excel');
return data.map(item => ({
...item,
formatted: true,
format: 'Excel'
}));
}
getReportType() {
return 'Excel';
}
}
// Использование
const salesData = [
{ product: 'Laptop', sales: 100, revenue: 50000 },
{ product: 'Mouse', sales: 200, revenue: 2000 }
];
const pdfGenerator = new PDFReportGenerator();
const excelGenerator = new ExcelReportGenerator();
pdfGenerator.generateReport(salesData);
excelGenerator.generateReport(salesData);
Template Method — это скелет алгоритма с настраиваемыми шагами!