命令模式:它通过将请求封装为一个独立的对象即命令对象,来解耦命令的调用者和接收者,使得调用者和接收者不直接交互。在命令对象里会包含请求相关的全部信息,每一个命令都是一个操作的请求: 请求方发出请求要求执行一个操作; 接收方收到请求,并执行操作。
命令模式结构:
- Receiver:命令的接收方,唯一包含业务逻辑的类,命令对象会将请求传递给它,它是请求的最终处理者
- Command:命令对象,组装了一个Receiver成员,并绑定实现了Receiver的一个特定行为的调用
- Invoker:请求的发送者,组装了Command成员,通过调用Command实例的execute()方法来触发对应的指令
- Client:通过将Receiver实例和请求信息传递给Command构造器来创建Command对象,之后会将创建的对象同Invoker绑定。
假设PS5的CPU支持A、B、C三个命令操作,
type CPU struct{}
func (CPU) ADoSomething() {
fmt.Println("a do something")
}
func (CPU) BDoSomething() {
fmt.Println("b do something")
}
type PS5 struct {
cpu CPU
}
func (p PS5) ACommand() {
p.cpu.ADoSomething()
}
func (p PS5) BCommand() {
p.cpu.ADoSomething()
}
func main() {
cpu := CPU{}
ps5 := PS5{cpu}
ps5.ACommand()
ps5.BCommand()
}
后续还可能会给CPU增加其他命令操作,以及需要支持命令宏(即命令组合操作)。如果每次都修改PS5的类定义,显然不符合面向对象开闭原则(Open close principle)的设计理念。
通过命令模式,我们把PS5抽象成命令发送者、CPU对象作为执行业务逻辑的命令接收者,然后引入引入Command 接口把两者做解耦,来满足开闭原则。
type CPU struct{}
func (CPU) ADoSomething(param int) {
fmt.Printf("a do something with param %v\n", param)
}
func (CPU) BDoSomething(param1 string, param2 int) {
fmt.Printf("b do something with params %v and %v \n", param1, param2)
}
func (CPU) CDoSomething() {
fmt.Println("c do something with no params")
}
// 接口中仅声明一个执行命令的方法 Execute()
type Command interface {
Execute()
}
// 命令对象持有一个指向接收者的引用,以及请求中的所有参数,
type ACommand struct {
cpu *CPU
param int
}
// 命令不会进行逻辑处理,调用Execute方法会将发送者的请求委派给接收者对象。
func (a ACommand) Execute() {
a.cpu.ADoSomething(a.param)
a.cpu.CDoSomething()// 可以执行多个接收者的操作完成命令宏
}
func NewACommand(cpu *CPU, param int) Command {
return ACommand{cpu, param}
}
type BCommand struct {
state bool // Command 里可以添加些状态用作逻辑判断
cpu *CPU
param1 string
param2 int
}
func (b BCommand) Execute() {
if b.state {
return
}
b.cpu.BDoSomething(b.param1, b.param2)
b.state = true
b.cpu.CDoSomething()
}
func NewBCommand(cpu *CPU, param1 string, param2 int) Command {
return BCommand{false,cpu, param1, param2}
}
type PS5 struct {
commands map[string]Command
}
// SetCommand方法来将 Command 指令设定给PS5。
func (p *PS5) SetCommand(name string, command Command) {
p.commands[name] = command
}
// DoCommand方法选择要执行的命令
func (p *PS5) DoCommand(name string) {
p.commands[name].Execute()
}
func main() {
cpu := CPU{}
// main方法充当客户端,创建并配置具体命令对象, 完成命令与执行操作的接收者的关联。
ps5 := PS5{make(map[string]Command)}
ps5.SetCommand("a", NewACommand(&cpu, 1))
ps5.SetCommand("b", NewBCommand(&cpu, "hello", 2))
ps5.DoCommand("a")
ps5.DoCommand("b")
}
命令模式作用可以类比于一个场景—在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。 服务员来到厨房, 把订单贴在墙上。 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。 厨师将做好的食物和订单一起放在托盘上。 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。 命令中包含与烹饪这些食物相关的所有信息。 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。
如果你需要通过操作来参数化对象, 可使用命令模式。
- 命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。
如果你想要将操作放入队列中、 操作的执行或者远程执行操作,可使用命令模式。
- 同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。
如果你想要实现操作回滚功能, 可使用命令模式。
- 为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。
可以同时使用 命令 和 备忘录模式 来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
可以将 访问者模式 视为 命令模式 的加强版本, 其对象可对不同类的多种对象执行操作。