Skip to content

模板方法与命令模式

本章讲两个行为型模式:模板方法(Template Method)和命令(Command)。模板方法在 Java 里高度依赖抽象类 + 继承 + 钩子方法,Go 没有继承,必须变通——我们用「接口 + 嵌入 + 函数字段」模拟。命令模式在 Go 里则可以简化为「函数」,但复杂场景(撤销/重做、队列)仍需要接口封装。

一、模板方法模式

1. 意图

模板方法模式:在一个方法中定义算法的骨架,把某些步骤延迟到子类中实现。模板方法让子类在不改变算法结构的前提下,重新定义算法中的某些步骤。

2. Java 实现回顾:抽象类 + 钩子方法

Java 经典写法:

java
// Java 伪代码
abstract class HttpRequestHandler {
    // 模板方法:定义算法骨架,final 防止子类改
    public final void handle(Request req) {
        authenticate(req);
        validate(req);
        process(req);
        log(req);
    }
    protected abstract void authenticate(Request req);
    protected abstract void validate(Request req);
    protected abstract void process(Request req);
    // 钩子:默认空实现,子类可选覆盖
    protected void log(Request req) {}
}

关键点:

  • handle 是模板方法,用 final 锁定骨架。
  • authenticate 等是抽象方法,子类必须实现。
  • log 是钩子,默认空,子类可选覆盖。

3. Go 没有「final 方法」和「抽象类」

Go 没有 abstract、没有 final、没有 protected 访问级别。这意味着:

  • 无法强制子类实现某个方法——只能靠接口约定。
  • 无法禁止重写模板方法——只能靠约定(文档说明「不要重写」)。
  • 没有继承层级——用「嵌入 + 接口」模拟。

Go 的模板方法有三种实现思路:

  1. 接口 + 具体类型:定义算法步骤为接口,模板方法在具体类型里编排。
  2. 嵌入 + 函数字段:把可变步骤定义为函数字段,默认实现可被覆盖。
  3. 纯函数组合:把骨架写成函数,可变步骤作为参数传入。

4. Go 实现:接口 + 嵌入 + 函数字段

下面用一个 HTTP 请求处理模板演示三种思路。

思路 A:接口 + 模板方法

go
package main

import "fmt"

// 定义算法步骤的接口
type RequestHandler interface {
	Authenticate(token string) error
	Validate(data string) error
	Process(data string) (string, error)
}

// 模板方法:固定骨架,调用接口方法
func HandleRequest(h RequestHandler, token, data string) {
	if err := h.Authenticate(token); err != nil {
		fmt.Println("鉴权失败:", err)
		return
	}
	if err := h.Validate(data); err != nil {
		fmt.Println("校验失败:", err)
		return
	}
	result, err := h.Process(data)
	if err != nil {
		fmt.Println("处理失败:", err)
		return
	}
	fmt.Println("处理成功:", result)
}

// 具体实现:订单处理器
type OrderHandler struct{}

func (OrderHandler) Authenticate(token string) error {
	if token != "secret" {
		return fmt.Errorf("token 无效")
	}
	return nil
}

func (OrderHandler) Validate(data string) error {
	if data == "" {
		return fmt.Errorf("数据为空")
	}
	return nil
}

func (OrderHandler) Process(data string) (string, error) {
	return "订单已创建: " + data, nil
}

func main() {
	HandleRequest(OrderHandler{}, "secret", "ORD-001")
	HandleRequest(OrderHandler{}, "wrong", "ORD-002")
	HandleRequest(OrderHandler{}, "secret", "")
}

HandleRequest 是模板方法,骨架固定(鉴权→校验→处理→输出),各步骤由 RequestHandler 接口的实现决定。新增处理器(如 UserHandler)只要实现接口,骨架不变——这正是模板方法的精髓。

思路 B:嵌入 + 函数字段(模拟钩子)

go
package main

import "fmt"

// Template 用嵌入的「默认实现」+ 函数字段模拟钩子
type RequestTemplate struct {
	// 函数字段:可被外部替换
	Authenticate func(token string) error
	Validate     func(data string) error
	Process      func(data string) (string, error)
	Log          func(stage, info string) // 钩子:默认不做事
}

// 默认实现(可作为「基类」)
func NewDefaultTemplate() *RequestTemplate {
	return &RequestTemplate{
		Authenticate: defaultAuthenticate,
		Validate:     defaultValidate,
		Process:      defaultProcess,
		Log:          func(string, string) {}, // 默认空实现
	}
}

func defaultAuthenticate(token string) error {
	if token != "secret" {
		return fmt.Errorf("token 无效")
	}
	return nil
}

func defaultValidate(data string) error {
	if data == "" {
		return fmt.Errorf("数据为空")
	}
	return nil
}

func defaultProcess(data string) (string, error) {
	return "默认处理: " + data, nil
}

// 模板方法:固定骨架
func (t *RequestTemplate) Handle(token, data string) {
	t.Log("auth", "开始")
	if err := t.Authenticate(token); err != nil {
		t.Log("auth", "失败")
		fmt.Println("鉴权失败:", err)
		return
	}
	t.Log("auth", "成功")

	if err := t.Validate(data); err != nil {
		t.Log("validate", "失败")
		fmt.Println("校验失败:", err)
		return
	}

	result, err := t.Process(data)
	if err != nil {
		fmt.Println("处理失败:", err)
		return
	}
	t.Log("process", result)
	fmt.Println("处理成功:", result)
}

func main() {
	t := NewDefaultTemplate()
	// 覆盖 Log 钩子(类似 Java 的钩子方法)
	t.Log = func(stage, info string) {
		fmt.Printf("[HOOK] %s: %s\n", stage, info)
	}
	t.Handle("secret", "hello")
}

Log 是「钩子」——默认空实现,使用方可以替换。这模拟了 Java 的 protected 钩子方法。函数字段是 Go 实现「可覆盖步骤」的常用手段。

思路 C:纯函数组合

go
package main

import "fmt"

type Step func(string) error

// 模板方法就是普通函数,把步骤作为参数
func Pipeline(auth, validate Step, process func(string) (string, error), token, data string) {
	if err := auth(token); err != nil {
		fmt.Println("鉴权失败:", err)
		return
	}
	if err := validate(data); err != nil {
		fmt.Println("校验失败:", err)
		return
	}
	result, err := process(data)
	if err != nil {
		fmt.Println("处理失败:", err)
		return
	}
	fmt.Println("成功:", result)
}

func main() {
	auth := func(token string) error {
		if token != "secret" {
			return fmt.Errorf("无效")
		}
		return nil
	}
	validate := func(data string) error {
		if data == "" {
			return fmt.Errorf("空")
		}
		return nil
	}
	process := func(data string) (string, error) {
		return "处理: " + data, nil
	}
	Pipeline(auth, validate, process, "secret", "data")
}

这是最 Go 风格的写法:没有接口、没有结构体,纯粹「函数 + 闭包」。代价是类型不明确(一堆 func 签名),适合简单场景。

5. 实战示例:HTTP 请求处理模板

综合思路 A,写一个更贴近实战的 HTTP 请求处理模板,包含错误响应、日志、超时:

go
package main

import (
	"fmt"
	"net/http"
	"time"
)

// 算法步骤接口
type HTTPHandler interface {
	Authenticate(r *http.Request) error
	Validate(r *http.Request) error
	Execute(r *http.Request) (any, error)
}

// 模板方法
func TemplateServe(h HTTPHandler, w http.ResponseWriter, r *http.Request) {
	start := time.Now()
	defer func() {
		fmt.Printf("[LOG] %s %s 耗时 %v\n", r.Method, r.URL.Path, time.Since(start))
	}()

	if err := h.Authenticate(r); err != nil {
		http.Error(w, "鉴权失败: "+err.Error(), http.StatusUnauthorized)
		return
	}
	if err := h.Validate(r); err != nil {
		http.Error(w, "校验失败: "+err.Error(), http.StatusBadRequest)
		return
	}
	result, err := h.Execute(r)
	if err != nil {
		http.Error(w, "执行失败: "+err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "OK: %+v\n", result)
}

// 具体处理器
type OrderAPI struct{}

func (OrderAPI) Authenticate(r *http.Request) error {
	if r.Header.Get("X-Token") == "" {
		return fmt.Errorf("缺少 token")
	}
	return nil
}

func (OrderAPI) Validate(r *http.Request) error {
	if r.URL.Query().Get("id") == "" {
		return fmt.Errorf("缺少 id")
	}
	return nil
}

func (OrderAPI) Execute(r *http.Request) (any, error) {
	id := r.URL.Query().Get("id")
	return map[string]string{"order_id": id, "status": "paid"}, nil
}

func main() {
	// 真实服务可以用 http.HandleFunc("/", func(w, r){ TemplateServe(OrderAPI{}, w, r) })
	// 这里用模拟调用演示
	handler := OrderAPI{}

	// 模拟请求 1:正常
	req1, _ := http.NewRequest("GET", "/order?id=001", nil)
	req1.Header.Set("X-Token", "abc")
	TemplateServe(handler, mockWriter{}, req1)

	// 模拟请求 2:缺 token
	req2, _ := http.NewRequest("GET", "/order?id=002", nil)
	TemplateServe(handler, mockWriter{}, req2)

	// 模拟请求 3:缺 id
	req3, _ := http.NewRequest("GET", "/order", nil)
	req3.Header.Set("X-Token", "abc")
	TemplateServe(handler, mockWriter{}, req3)
}

type mockWriter struct{}

func (mockWriter) Header() http.Header         { return http.Header{} }
func (mockWriter) Write(b []byte) (int, error) { fmt.Println("  ->", string(b)); return len(b), nil }
func (mockWriter) WriteHeader(int)             {}

注意:这个模板方法 TemplateServe 是一个普通函数,不是某个类的方法。Go 里模板方法可以放在独立函数里,由调用方传入实现接口的对象。这与 Java「模板方法在抽象基类里」的写法不同,但效果一样。

二、命令模式

1. 意图

命令模式:把一个请求或操作封装成一个对象。命令模式让你可以用不同的请求对客户端进行参数化、队列化、记录日志,以及支持撤销操作。

2. Go 实现:命令接口 + 具体命令

go
package main

import "fmt"

// 命令接口
type Command interface {
	Execute() error
}

// 具体命令:带状态
type PrintCommand struct {
	Message string
}

func (c *PrintCommand) Execute() error {
	fmt.Println("打印:", c.Message)
	return nil
}

type AddCommand struct {
	A, B int
}

func (c *AddCommand) Execute() error {
	fmt.Printf("%d + %d = %d\n", c.A, c.B, c.A+c.B)
	return nil
}

// 调用者:把命令排队执行
type Invoker struct {
	commands []Command
}

func (i *Invoker) AddCommand(c Command) {
	i.commands = append(i.commands, c)
}

func (i *Invoker) Run() {
	for _, c := range i.commands {
		_ = c.Execute()
	}
	i.commands = nil
}

func main() {
	inv := &Invoker{}
	inv.AddCommand(&PrintCommand{Message: "hello"})
	inv.AddCommand(&AddCommand{A: 3, B: 5})
	inv.AddCommand(&PrintCommand{Message: "world"})
	inv.Run()
}

3. 函数作为命令(Go 特色)

如果命令不需要撤销、不需要复杂状态,用函数就够了:

go
package main

import "fmt"

// 命令就是函数
type Command func() error

type Invoker struct {
	commands []Command
}

func (i *Invoker) Add(c Command) {
	i.commands = append(i.commands, c)
}

func (i *Invoker) Run() {
	for _, c := range i.commands {
		_ = c()
	}
}

func main() {
	inv := &Invoker{}
	inv.Add(func() error {
		fmt.Println("打印 hello")
		return nil
	})
	inv.Add(func() error {
		fmt.Println("3 + 5 =", 3+5)
		return nil
	})
	inv.Add(func() error {
		fmt.Println("打印 world")
		return nil
	})
	inv.Run()
}

4. 实战示例:任务队列

go
package main

import (
	"fmt"
	"time"
)

type Task func() error

type TaskQueue struct {
	tasks []Task
}

func (q *TaskQueue) Push(t Task) {
	q.tasks = append(q.tasks, t)
}

func (q *TaskQueue) RunAll() {
	for i, t := range q.tasks {
		fmt.Printf("[task %d] 开始\n", i+1)
		if err := t(); err != nil {
			fmt.Printf("[task %d] 失败: %v\n", i+1, err)
		} else {
			fmt.Printf("[task %d] 完成\n", i+1)
		}
	}
	q.tasks = nil
}

func main() {
	q := &TaskQueue{}
	q.Push(func() error {
		time.Sleep(50 * time.Millisecond)
		fmt.Println("  下载文件")
		return nil
	})
	q.Push(func() error {
		fmt.Println("  解析文件")
		return fmt.Errorf("格式错误")
	})
	q.Push(func() error {
		fmt.Println("  存储结果")
		return nil
	})
	q.RunAll()
}

5. 实战示例:撤销/重做

撤销/重做是命令模式的招牌应用。命令需要同时实现 ExecuteUndo

go
package main

import "fmt"

// 命令接口:必须能执行也能撤销
type UndoableCommand interface {
	Execute()
	Undo()
}

// 接收者:被命令操作的对象
type TextEditor struct {
	text string
}

func (t *TextEditor) Append(s string) { t.text += s }
func (t *TextEditor) Delete(n int) {
	if n > len(t.text) {
		n = len(t.text)
	}
	t.text = t.text[:len(t.text)-n]
}
func (t *TextEditor) Text() string { return t.text }

// 具体命令:追加文本
type AppendCommand struct {
	editor  *TextEditor
	content string
}

func (c *AppendCommand) Execute() {
	c.editor.Append(c.content)
}
func (c *AppendCommand) Undo() {
	c.editor.Delete(len(c.content))
}

// 具体命令:删除文本
type DeleteCommand struct {
	editor *TextEditor
	count  int
	deleted string // 保存被删除的内容以便撤销
}

func (c *DeleteCommand) Execute() {
	start := len(c.editor.text) - c.count
	if start < 0 {
		start = 0
	}
	c.deleted = c.editor.text[start:]
	c.editor.Delete(c.count)
}
func (c *DeleteCommand) Undo() {
	c.editor.Append(c.deleted)
}

// 调用者:维护撤销栈和重做栈
type History struct {
	undoStack []UndoableCommand
	redoStack []UndoableCommand
}

func (h *History) Execute(c UndoableCommand) {
	c.Execute()
	h.undoStack = append(h.undoStack, c)
	h.redoStack = nil // 新操作清空重做栈
}

func (h *History) Undo() {
	if len(h.undoStack) == 0 {
		fmt.Println("无法撤销")
		return
	}
	c := h.undoStack[len(h.undoStack)-1]
	h.undoStack = h.undoStack[:len(h.undoStack)-1]
	c.Undo()
	h.redoStack = append(h.redoStack, c)
}

func (h *History) Redo() {
	if len(h.redoStack) == 0 {
		fmt.Println("无法重做")
		return
	}
	c := h.redoStack[len(h.redoStack)-1]
	h.redoStack = h.redoStack[:len(h.redoStack)-1]
	c.Execute()
	h.undoStack = append(h.undoStack, c)
}

func main() {
	editor := &TextEditor{}
	history := &History{}

	history.Execute(&AppendCommand{editor: editor, content: "Hello"})
	history.Execute(&AppendCommand{editor: editor, content: " World"})
	fmt.Println("当前文本:", editor.Text()) // Hello World

	history.Execute(&DeleteCommand{editor: editor, count: 5})
	fmt.Println("删除 5 字符后:", editor.Text()) // Hello

	fmt.Println("--- 撤销 ---")
	history.Undo()
	fmt.Println("撤销删除后:", editor.Text()) // Hello World

	fmt.Println("--- 重做 ---")
	history.Redo()
	fmt.Println("重做删除后:", editor.Text()) // Hello

	fmt.Println("--- 撤销两次 ---")
	history.Undo()
	history.Undo()
	fmt.Println("撤销追加后:", editor.Text()) // Hello
}

撤销/重做是命令模式少有的「必须用对象」的场景——纯函数无法记录「撤销需要的信息」,必须用结构体保存状态。

三、模板方法 vs 策略

两者都涉及「把可变部分抽出来」,区别:

维度模板方法策略
关注点算法的 骨架(步骤顺序)算法的 整体
实现机制继承(Java)/ 嵌入+接口(Go)接口 / 函数
粒度多个步骤一个完整算法
客户端通常不感知,骨架已定主动选择策略
Go 中形态一个函数编排多个接口方法一个接口字段被替换

简单说:模板方法是「我做骨架,你填空」;策略是「整个换掉」。

四、命令模式 vs 策略

维度命令策略
意图把请求封装成对象把算法封装成可互换单元
关注点请求的 生命周期(队列、撤销)算法的 可替换性
生命周期通常一次性,执行完可能保留用于撤销长期持有,可切换
典型场景任务队列、撤销重做、宏命令折扣、排序、支付

五、小结

  • 模板方法定义算法骨架,把可变步骤延迟到子类/具体实现。Java 用抽象类 + final 方法 + 钩子;Go 没有继承,用「接口 + 模板方法函数」「嵌入 + 函数字段」「纯函数组合」三种思路模拟。
  • 函数字段是 Go 实现「钩子」的常用手段:默认实现可被外部替换,无需继承。
  • 命令模式把请求封装成对象,支持队列、撤销、日志。Go 中简单场景用 func() error 即可,复杂场景(撤销/重做、宏命令)用接口封装。
  • 撤销/重做是命令模式「必须用对象」的典型场景:需要保存状态才能撤销。
  • 模板方法关注骨架,策略关注整体;命令关注请求生命周期。
  • Go 的「函数是一等公民」让这两个模式在很多场景下简化为「传函数」,但不要因此忽略接口封装的价值——复杂场景下接口更清晰、更易扩展。

下一篇讲职责链与状态模式,看 HTTP 中间件如何体现职责链,以及用状态机管理订单流转。