模板方法模式(Template Method Pattern)
模板方法模式(Template Method Pattern)
1. 知识点总结
模板方法模式是一种行为型设计模式,核心思想是:
在父类(或模板结构体)中定义算法的骨架,把某些步骤的具体实现延迟到子类中。子类可以在不改变算法结构的前提下,重新定义特定步骤。
典型的执行流程像一条流水线:
步骤 1:数据校验(Validate)
步骤 2:数据转换(Transform)
步骤 3:加载入库(Load)
这三个步骤的顺序和异常处理是固定的,但每一步的具体逻辑因数据源不同而不同。
适用场景
- 多个类拥有相同的算法流程,只有部分步骤实现不同;
- 希望控制扩展点,只允许子类重写特定步骤而不是整个流程;
- 框架中定义处理流程,由使用者提供具体实现(例如 Web 框架的生命周期钩子)。
优缺点
| 优点 | 缺点 |
|---|---|
| 复用公共流程,减少重复代码 | 每增加一种实现就要新增一个子类,类数量可能膨胀 |
| 扩展点明确,子类不能破坏整体流程 | 子类必须了解模板方法流程,父类和子类存在一定耦合 |
| 符合开闭原则:新增子类无需修改模板 | Go 没有原生继承,需要用匿名组合 + 接口注入模拟 |
2. Go 中的实现要点
Go 没有类继承,但有 匿名组合 + 接口注入 的组合拳:
- 定义一个接口(
Importer),把算法中可变的步骤声明为方法; - 定义一个“父类”结构体(
BaseImporter),持有Impl Importer引用; - 在父类的模板方法
Run()中按固定顺序调用b.Impl.Validate()、b.Impl.Transform()、b.Impl.Load(); - 具体子类通过匿名组合
BaseImporter,并实现接口中的可变步骤; - 初始化时把子类自身回注给
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. 关键点复盘
- 算法骨架 vs 可变步骤:模板方法把“流程控制”这类公共逻辑固化,把“具体实现”这类变化点开放出去。
- 匿名组合模拟继承:Go 中
CSVImporter通过嵌入BaseImporter获得Run()方法,表现上类似继承。 - 接口注入实现多态:
BaseImporter不直接调用自己的方法,而是通过Impl接口调用子类实现,保证可变步骤在运行时被正确分发。 - 与策略模式的区别:模板方法强调“固定骨架 + 重写步骤”,策略模式强调“整个算法可替换”。如果流程本身经常变,用策略更合适。

浙公网安备 33010602011771号