Menu
CoddyTech

命令模式

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 91 课,共 107 课。

命令模式将请求封装为对象,让你可以对操作进行参数化、将操作排队,或支持撤销功能。策略模式封装算法,而命令模式将整个操作及其参数封装起来。

在 Go 中,我们定义一个带有 Execute 方法的 Command 接口,然后创建具体命令,其中包含执行操作所需的全部信息:

type Command interface {
    Execute() string
}

type Light struct {
    IsOn bool
}

type TurnOnCommand struct {
    light *Light
}

func (c *TurnOnCommand) Execute() string {
    c.light.IsOn = true
    return "Light turned on"
}

type TurnOffCommand struct {
    light *Light
}

func (c *TurnOffCommand) Execute() string {
    c.light.IsOn = false
    return "Light turned off"
}

调用者可以存储并执行命令,而无需知道这些命令的作用:

type RemoteControl struct {
    command Command
}

func (r *RemoteControl) SetCommand(c Command) {
    r.command = c
}

func (r *RemoteControl) PressButton() string {
    return r.command.Execute()
}

调用者与接收者(Light)完全解耦:

light := &Light{}
remote := &RemoteControl{}

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // 灯已打开

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // 灯已关闭

当你需要存储、延迟或重放操作时,Command 非常适合用于实现撤销/重做系统、任务队列或宏录制。

challenge icon

挑战

简单

让我们使用命令模式构建一个支持撤销功能的文本编辑器吧!你将创建封装文本操作的命令,让编辑器能够执行和撤销操作。这是一个经典用例,能展示命令模式为何如此强大。

你将把代码分成三个文件:

  • command.go:定义命令接口和具体的文本编辑命令。

    创建一个 Command 接口,包含两个方法:Execute() string 用于执行操作,Undo() string 用于撤销操作。

    实现两个操作 *TextEditor 的命令类型:

    • InsertCommand:保存编辑器指针和要插入的文本。执行时,将文本追加到编辑器内容末尾,并返回 Inserted: [text]。撤销时,从末尾移除这段文本,并返回 Undone insert: [text]
    • DeleteCommand:保存编辑器指针,以及要从末尾删除的字符数。执行时,移除这些字符(并保存它们以便撤销),然后返回 Deleted: [removed text]。撤销时,恢复这些字符,然后返回 Undone delete: [restored text]
  • editor.go:创建文本编辑器(接收者)以及管理命令历史记录的调用者。

    构建一个 TextEditor 结构体,包含 Content 字段(字符串),以及 Append(text string) 和 DeleteLast(count int) string 方法(返回已删除的文本)。

    构建一个 EditorInvoker 结构体,用切片保存已执行的命令作为历史记录。添加以下方法:

    • ExecuteCommand(cmd Command) string:执行命令,将其添加到历史记录中,并返回结果
    • UndoLast() string:从历史记录中移除最后一条命令,调用它的 Undo 方法,并返回结果。如果历史记录为空,则返回 Nothing to undo
  • main.go:通过一系列操作演示命令系统。

    读取操作数量。对于每个操作,读取操作类型(insert、delete 或 undo)。对于插入操作,还要读取要插入的文本。对于删除操作,读取字符数量。通过调用者执行每个操作并打印结果。所有操作完成后,将编辑器的最终内容打印为 Final: [content]。

输入格式如下:

  • 第 1 行:操作数量
  • 每个操作:操作类型,以及按需提供的额外数据(插入操作提供文本,删除操作提供数量)

例如,给定以下输入:

5
insert
Hello
insert
 World
delete
3
undo
undo

输出应为:

Inserted: Hello
Inserted:  World
Deleted: rld
Undone delete: rld
Undone insert:  World
Final: Hello

再例如,给定以下输入:

4
insert
Go
insert
Lang
undo
insert
!

输出应为:

Inserted: Go
Inserted: Lang
Undone insert: Lang
Inserted: !
Final: Go!

再例如,给定以下输入:

2
undo
insert
Test

输出应为:

Nothing to undo
Inserted: Test
Final: Test

注意,每条命令都封装了执行和撤销操作所需的一切内容。调用者不需要知道命令具体做什么,只需执行命令并维护历史记录,以支持撤销操作!

亲手试试

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// 读取操作次数
	line, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(line))
	
	// 创建编辑器和调用器
	editor := &TextEditor{}
	invoker := &EditorInvoker{}
	
	for i := 0; i < numOps; i++ {
		// 读取操作类型
		opLine, _ := reader.ReadString('\n')
		opType := strings.TrimSpace(opLine)
		
		var result string
		
		switch opType {
		case "insert":
			// 读取要插入的文本
			textLine, _ := reader.ReadString('\n')
			text := strings.TrimSuffix(textLine, "\n")
			
			// TODO:创建 InsertCommand,并通过调用器执行它
			_ = text
			_ = editor
			
		case "delete":
			// 读取要删除的字符数
			countLine, _ := reader.ReadString('\n')
			count, _ := strconv.Atoi(strings.TrimSpace(countLine))
			
			// TODO:创建 DeleteCommand,并通过调用器执行它
			_ = count
			
		case "undo":
			// TODO:在调用器上调用 UndoLast
			
		}
		
		fmt.Println(result)
	}
	
	// 输出最终内容
	fmt.Printf("Final: %s\n", editor.Content)
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器