Lapa Knowledge Base
Поведенческие паттерны

Template Method

Паттерн Template Method

Template Method — это поведенческий паттерн проектирования, который определяет скелет алгоритма в базовом классе, позволяя подклассам переопределять определенные шаги алгоритма, не изменяя его общую структуру.

Проблема

Представьте, что вы создаете приложение для анализа данных. Вам нужно обрабатывать различные типы документов: PDF, Word, Excel. Каждый тип документа требует своего способа извлечения данных, но общий процесс анализа одинаков:

  1. Открыть документ
  2. Извлечь данные
  3. Обработать данные
  4. Создать отчет
  5. Закрыть документ

Если вы реализуете это для каждого типа документа отдельно, вы получите много дублированного кода.

Решение

Паттерн 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 — это скелет алгоритма с настраиваемыми шагами!

Copyright © 2026