AIGC标识 模板方法模式(Template Method Pattern)

模板方法模式(Template Method Pattern)

1. 知识点总结

模板方法模式是一种行为型设计模式,核心思想是:

在父类(或模板结构体)中定义算法的骨架,把某些步骤的具体实现延迟到子类中。子类可以在不改变算法结构的前提下,重新定义特定步骤。

典型的执行流程像一条流水线:

步骤 1:数据校验(Validate)
步骤 2:数据转换(Transform)
步骤 3:加载入库(Load)

这三个步骤的顺序和异常处理是固定的,但每一步的具体逻辑因数据源不同而不同。

适用场景

  • 多个类拥有相同的算法流程,只有部分步骤实现不同;
  • 希望控制扩展点,只允许子类重写特定步骤而不是整个流程;
  • 框架中定义处理流程,由使用者提供具体实现(例如 Web 框架的生命周期钩子)。

优缺点

优点 缺点
复用公共流程,减少重复代码 每增加一种实现就要新增一个子类,类数量可能膨胀
扩展点明确,子类不能破坏整体流程 子类必须了解模板方法流程,父类和子类存在一定耦合
符合开闭原则:新增子类无需修改模板 Go 没有原生继承,需要用匿名组合 + 接口注入模拟

2. Go 中的实现要点

Go 没有类继承,但有 匿名组合 + 接口注入 的组合拳:

  1. 定义一个接口(Importer),把算法中可变的步骤声明为方法;
  2. 定义一个“父类”结构体(BaseImporter),持有 Impl Importer 引用;
  3. 在父类的模板方法 Run() 中按固定顺序调用 b.Impl.Validate()b.Impl.Transform()b.Impl.Load()
  4. 具体子类通过匿名组合 BaseImporter,并实现接口中的可变步骤;
  5. 初始化时把子类自身回注给 BaseImporter.Impl,实现运行时多态。

3. 示例代码

下面以“数据导入器”为例:模板定义校验 -> 转换 -> 加载的流程,CSV 导入器负责 CSV 文件的具体实现。

package main

import (
	"bytes"
	"encoding/csv"
	"fmt"
	"io"
	"os"
	"strconv"
)

// Record 表示最终要入库的数据记录。
type Record struct {
	Name string
	Age  int
}

// Importer 定义了数据导入器的全部行为。
// BaseImporter 使用这个接口来调用具体子类实现的“可变步骤”。
type Importer interface {
	Validate() error
	Transform() ([]Record, error)
	Load([]Record) error
	Run() error
}

// BaseImporter 是模板,定义了导入流程的骨架:
// 校验 -> 转换 -> 加载。
type BaseImporter struct {
	Impl Importer
}

func (b *BaseImporter) Run() error {
	fmt.Println("[模板] 1. 开始数据校验")
	if err := b.Impl.Validate(); err != nil {
		return fmt.Errorf("校验失败: %w", err)
	}

	fmt.Println("[模板] 2. 执行数据转换")
	records, err := b.Impl.Transform()
	if err != nil {
		return fmt.Errorf("转换失败: %w", err)
	}

	fmt.Println("[模板] 3. 加载到目标存储")
	if err := b.Impl.Load(records); err != nil {
		return fmt.Errorf("加载失败: %w", err)
	}

	fmt.Println("[模板] 4. 导入流程结束")
	return nil
}

// CSVImporter 只负责 CSV 文件相关的具体步骤。
type CSVImporter struct {
	BaseImporter
	path    string
	raw     []byte
	records []Record
}

func NewCSVImporter(path string) *CSVImporter {
	c := &CSVImporter{path: path}
	// 把具体实现回注给模板,让模板方法在运行时多态地调用子类方法。
	c.BaseImporter = BaseImporter{Impl: c}
	return c
}

func (c *CSVImporter) Validate() error {
	data, err := os.ReadFile(c.path)
	if err != nil {
		return err
	}
	if len(data) == 0 {
		return fmt.Errorf("文件为空")
	}
	c.raw = data
	fmt.Println("[CSV] 文件读取并校验通过")
	return nil
}

func (c *CSVImporter) Transform() ([]Record, error) {
	r := csv.NewReader(bytes.NewReader(c.raw))
	var out []Record
	for {
		row, err := r.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}
		if len(row) < 2 {
			continue
		}
		age, err := strconv.Atoi(row[1])
		if err != nil {
			return nil, fmt.Errorf("年龄解析失败 %q: %w", row[1], err)
		}
		out = append(out, Record{Name: row[0], Age: age})
	}
	fmt.Printf("[CSV] 解析得到 %d 条记录\n", len(out))
	return out, nil
}

func (c *CSVImporter) Load(records []Record) error {
	c.records = append(c.records, records...)
	fmt.Printf("[CSV] 成功加载 %d 条记录到内存存储\n", len(records))
	return nil
}

func main() {
	// 构造一个临时 CSV 文件用于演示。
	path := "/tmp/study_template.csv"
	content := "Alice,30\nBob,25\nCharlie,35\n"
	if err := os.WriteFile(path, []byte(content), 0644); err != nil {
		panic(err)
	}
	defer os.Remove(path)

	importer := NewCSVImporter(path)
	if err := importer.Run(); err != nil {
		panic(err)
	}

	fmt.Println("最终记录:", importer.records)
}

4. 运行结果

[模板] 1. 开始数据校验
[CSV] 文件读取并校验通过
[模板] 2. 执行数据转换
[CSV] 解析得到 3 条记录
[模板] 3. 加载到目标存储
[CSV] 成功加载 3 条记录到内存存储
[模板] 4. 导入流程结束
最终记录: [{Alice 30} {Bob 25} {Charlie 35}]

5. 关键点复盘

  1. 算法骨架 vs 可变步骤:模板方法把“流程控制”这类公共逻辑固化,把“具体实现”这类变化点开放出去。
  2. 匿名组合模拟继承:Go 中 CSVImporter 通过嵌入 BaseImporter 获得 Run() 方法,表现上类似继承。
  3. 接口注入实现多态BaseImporter 不直接调用自己的方法,而是通过 Impl 接口调用子类实现,保证可变步骤在运行时被正确分发。
  4. 与策略模式的区别:模板方法强调“固定骨架 + 重写步骤”,策略模式强调“整个算法可替换”。如果流程本身经常变,用策略更合适。
posted @ 2026-08-07 09:05  FfHUCisI  阅读(6)  评论(0)    收藏  举报