Appearance
模板方法与命令模式
本章讲两个行为型模式:模板方法(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 的模板方法有三种实现思路:
- 接口 + 具体类型:定义算法步骤为接口,模板方法在具体类型里编排。
- 嵌入 + 函数字段:把可变步骤定义为函数字段,默认实现可被覆盖。
- 纯函数组合:把骨架写成函数,可变步骤作为参数传入。
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. 实战示例:撤销/重做
撤销/重做是命令模式的招牌应用。命令需要同时实现 Execute 和 Undo:
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 中间件如何体现职责链,以及用状态机管理订单流转。