Template Method (Шаблонный метод)
Проблема
Несколько алгоритмов имеют одинаковую структуру, но отличаются в деталях. Например: импорт данных из CSV, JSON, XML -- все проходят этапы: открытие, парсинг, валидация, сохранение. Template Method определяет каркас алгоритма, позволяя подклассам переопределять отдельные шаги.
В Go нет наследования! Template Method реализуется через интерфейс + функцию, а не через abstract class.
Диаграмма
Template function:
func ProcessData(p DataProcessor) error {
data := p.Read() // step 1: varies
parsed := p.Parse(data) // step 2: varies
validate(parsed) // step 3: fixed
save(parsed) // step 4: fixed
}
+------------------+
| DataProcessor | <-- interface (variable steps)
+------------------+
| Read() []byte |
| Parse() []Record |
+------------------+
^
+----+-----+
| |
CSVProc JSONProc
Template function provides the fixed skeleton.
Interface provides the variable steps.
Реализация
Интерфейс для вариативных шагов
package dataminer
import (
"context"
"fmt"
"log/slog"
"time"
)
// Record represents a parsed data record.
type Record struct {
Fields map[string]string
}
// DataProcessor defines the variable steps of data processing.
type DataProcessor interface {
// Read retrieves raw data from the source.
Read(ctx context.Context) ([]byte, error)
// Parse converts raw data into structured records.
Parse(data []byte) ([]Record, error)
// Name returns the processor name (for logging).
Name() string
}
// ProcessData is the template method.
// It defines the fixed algorithm skeleton.
func ProcessData(ctx context.Context, p DataProcessor, logger *slog.Logger) ([]Record, error) {
start := time.Now()
logger.InfoContext(ctx, "starting data processing",
"processor", p.Name(),
)
// Step 1: Read (varies by processor)
data, err := p.Read(ctx)
if err != nil {
return nil, fmt.Errorf("read %s: %w", p.Name(), err)
}
logger.InfoContext(ctx, "data read",
"processor", p.Name(),
"bytes", len(data),
)
// Step 2: Parse (varies by processor)
records, err := p.Parse(data)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", p.Name(), err)
}
// Step 3: Validate (fixed step -- same for all)
valid := make([]Record, 0, len(records))
for _, r := range records {
if validateRecord(r) {
valid = append(valid, r)
}
}
// Step 4: Log results (fixed step)
logger.InfoContext(ctx, "processing complete",
"processor", p.Name(),
"total", len(records),
"valid", len(valid),
"duration", time.Since(start),
)
return valid, nil
}
// validateRecord is a fixed step -- not customizable.
func validateRecord(r Record) bool {
// Must have at least one non-empty field
for _, v := range r.Fields {
if v != "" {
return true
}
}
return false
}
Конкретные процессоры
package dataminer
import (
"context"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"os"
"strings"
)
// --- CSV Processor ---
type CSVProcessor struct {
filePath string
delimiter rune
}
func NewCSVProcessor(filePath string, delimiter rune) *CSVProcessor {
return &CSVProcessor{filePath: filePath, delimiter: delimiter}
}
func (p *CSVProcessor) Name() string { return "CSV" }
func (p *CSVProcessor) Read(ctx context.Context) ([]byte, error) {
return os.ReadFile(p.filePath)
}
func (p *CSVProcessor) Parse(data []byte) ([]Record, error) {
reader := csv.NewReader(strings.NewReader(string(data)))
reader.Comma = p.delimiter
// First row is headers
headers, err := reader.Read()
if err != nil {
return nil, fmt.Errorf("read headers: %w", err)
}
var records []Record
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("read row: %w", err)
}
fields := make(map[string]string, len(headers))
for i, header := range headers {
if i < len(row) {
fields[header] = row[i]
}
}
records = append(records, Record{Fields: fields})
}
return records, nil
}
// --- JSON Processor ---
type JSONProcessor struct {
filePath string
}
func NewJSONProcessor(filePath string) *JSONProcessor {
return &JSONProcessor{filePath: filePath}
}
func (p *JSONProcessor) Name() string { return "JSON" }
func (p *JSONProcessor) Read(ctx context.Context) ([]byte, error) {
return os.ReadFile(p.filePath)
}
func (p *JSONProcessor) Parse(data []byte) ([]Record, error) {
var items []map[string]string
if err := json.Unmarshal(data, &items); err != nil {
return nil, fmt.Errorf("unmarshal JSON: %w", err)
}
records := make([]Record, len(items))
for i, item := range items {
records[i] = Record{Fields: item}
}
return records, nil
}
// --- HTTP API Processor ---
type APIProcessor struct {
url string
client *http.Client
}
func NewAPIProcessor(url string) *APIProcessor {
return &APIProcessor{
url: url,
client: &http.Client{Timeout: 10 * time.Second},
}
}
func (p *APIProcessor) Name() string { return "API" }
func (p *APIProcessor) Read(ctx context.Context) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.url, nil)
if err != nil {
return nil, err
}
resp, err := p.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func (p *APIProcessor) Parse(data []byte) ([]Record, error) {
// Same as JSON
var items []map[string]string
if err := json.Unmarshal(data, &items); err != nil {
return nil, err
}
records := make([]Record, len(items))
for i, item := range items {
records[i] = Record{Fields: item}
}
return records, nil
}
Использование
func main() {
ctx := context.Background()
logger := slog.Default()
// Same algorithm, different data sources
processors := []dataminer.DataProcessor{
dataminer.NewCSVProcessor("users.csv", ','),
dataminer.NewJSONProcessor("users.json"),
dataminer.NewAPIProcessor("https://api.example.com/users"),
}
for _, p := range processors {
records, err := dataminer.ProcessData(ctx, p, logger)
if err != nil {
logger.Error("processing failed", "processor", p.Name(), "error", err)
continue
}
fmt.Printf("%s: %d valid records\n", p.Name(), len(records))
}
}
Template Method с хуками
Хуки -- опциональные шаги, которые процессор может переопределить:
// ProcessorWithHooks extends DataProcessor with optional hooks.
type ProcessorWithHooks interface {
DataProcessor
// BeforeRead is called before reading (optional hook).
BeforeRead(ctx context.Context) error
// AfterParse is called after parsing (optional hook).
AfterParse(records []Record) []Record
}
// DefaultHooks provides no-op implementations.
type DefaultHooks struct{}
func (h DefaultHooks) BeforeRead(_ context.Context) error { return nil }
func (h DefaultHooks) AfterParse(r []Record) []Record { return r }
// CSVProcessor with hooks
type CSVProcessorWithHooks struct {
CSVProcessor
DefaultHooks // embed default no-op hooks
}
Go vs Java: Template Method
| Аспект | Java | Go |
|---|---|---|
| Механизм | Abstract class + наследование | Interface + функция |
| Фиксированные шаги | Protected final methods | Обычные функции (не экспортируемые) |
| Вариативные шаги | Abstract methods | Методы интерфейса |
| Хуки | Protected methods с default impl | Отдельный интерфейс + embedding |
Когда использовать
Используйте, когда:
- Несколько алгоритмов с одинаковой структурой
- Фиксированный каркас с вариативными шагами
- Нужно избежать дублирования общей логики
- ETL-конвейеры, импорт/экспорт данных
Не используйте, когда:
- Алгоритмы полностью различаются (используйте Strategy)
- Структура часто меняется
- Только один "шаблон" без вариаций
Сравнение с похожими паттернами
| Паттерн | Отличие |
|---|---|
| Strategy | Полная замена алгоритма; Template Method -- замена шагов |
| Factory Method | Создание объекта; Template Method -- шаги алгоритма |
| Builder | Пошаговое создание объекта; Template Method -- пошаговое выполнение |