本文将解释设计模式中的命令模式,在哪里使用它,以及如何在 Go 中实现它。
顾名思义,命令模式用于创建和执行“命令”。不同的命令有自己的实现,但是执行的步骤是一样的。
命令接口
实现命令模式的基本单元是Command接口:
type Command interface {
execute()
}
如果命令可以输出错误,接口也可以包含一个错误返回值:
type Command interface {
execute() error
}
👉 命令接口提供任何其他类型都可以实现的通用签名
让我们看一个例子来说明 Go 中的命令模式。
假设有一家餐厅,它有一定数量的厨师,厨房里有一些菜肴。每个厨师一次可以执行以下一项任务:
- 做披萨
- 做沙拉
- 洗盘子(每次做披萨或沙拉,就会有一个盘子被用掉。洗盘子会重置盘子的总数。)
创建命令
餐厅的三个任务都可以表示为命令。让我们看看如何构建餐厅和三个命令:
// 餐厅包含盘子总数和已清洗的盘子数
type Restaurant struct {
TotalDishes int
CleanedDishes int
}
// `NewRestaurant` 构造一个新的餐厅实例,里面有 10 个盘子,都是干净的
func NewResteraunt() *Restaurant {
const totalDishes = 10
return &Restaurant{
TotalDishes: totalDishes,
CleanedDishes: totalDishes,
}
}
// MakePizzaCommand 是一个结构体,它包含要制作的比萨饼的数量,以及餐厅作为其属性
type MakePizzaCommand struct {
n int
restaurant *Restaurant
}
func (c *MakePizzaCommand) execute() {
// 减少餐厅的干净盘子数,并在完成后打印一条消息
c.restaurant.CleanedDishes -= c.n
fmt.Println("made", c.n, "pizzas")
}
// MakeSaladCommand 类似于 MakePizza 命令
type MakeSaladCommand struct {
n int
restaurant *Restaurant
}
func (c *MakeSaladCommand) execute() {
c.restaurant.CleanedDishes -= c.n
fmt.Println("made", c.n, "salads")
}
type CleanDishesCommand struct {
restaurant *Restaurant
}
func (c *CleanDishesCommand) execute() {
// 将已清洗的盘子数重置为盘子总数,并在完成后打印一条消息
c.restaurant.CleanedDishes = c.restaurant.TotalDishes
fmt.Println("dishes cleaned")
}
👉 MakePizzaCommand
、MakeSaladCommand
和 CleanDishesCommand
都实现了 Command
接口的 execute
方法。
我们现在可以向 Restaurant
添加方法以创建这些命令的实例:
func (r *Restaurant) MakePizza(n int) Command {
return &MakePizzaCommand{
restaurant: r,
n: n,
}
}
func (r *Restaurant) MakeSalad(n int) Command {
return &MakeSaladCommand{
restaurant: r,
n: n,
}
}
func (r *Restaurant) CleanDishes() Command {
return &CleanDishesCommand{
restaurant: r,
}
}
通过这种方式,Restaurant
充当了一种命令工厂。
执行命令
创建命令后,可以通过调用 execute
方法来执行它。虽然这看起来没什么特别之处,但是当我们需要执行多个不同的命令时,它的优势就体现出来了。
为了证明这一点,让我们为我们的餐厅添加一些厨师:
// 厨师将他们的命令列表作为属性
type Cook struct {
Commands []Command
}
// executeCommands 方法将所有命令一一执行
func (c *Cook) executeCommands() {
for _, c := range c.Commands {
c.execute()
}
}
Cook
是我们餐厅的执行者,接受命令并一个接一个地执行。
👉 让 Cook
获取一组 Command
对象,将它们与命令的实际执行分开,因为厨师不需要知道每个命令的内部实现。
开始营业
到目前为止,我们的示例中有三个实体:
- 餐厅,所有命令在其上执行
- 厨师,执行命令
- 命令本身
使用这三个实体,我们可以为每个厨师构建一个作业队列,以在餐厅执行他们各自的命令:
func main() {
// 初始化一个新的餐厅
r := NewResteraunt()
// 创建要执行的任务列表
tasks := []Command{
r.MakePizza(2),
r.MakeSalad(1),
r.MakePizza(3),
r.CleanDishes(),
r.MakePizza(4),
r.CleanDishes(),
}
// 创建将执行任务的厨师
cooks := []*Cook{
&Cook{},
&Cook{},
}
// 将任务交替的分配给在现有的厨师
for i, task := range tasks {
// 我们使用当前任务索引的模数来给厨师交替分配任务
cook := cooks[i%len(cooks)]
cook.Commands = append(cook.Commands, task)
}
// 现在所有的厨师都有他们的命令,我们可以调用 `executeCommands` 方法让每个厨师执行他们各自的命令
for i, c := range cooks {
fmt.Println("cook", i, ":")
c.executeCommands()
}
}
您可以在此处运行完整示例,将输出:
cook 0 :
made 2 pizzas
made 3 pizzas
made 4 pizzas
cook 1 :
made 1 salads
dishes cleaned
dishes cleaned
何时使用命令模式
当你需要执行任务,但又想将任务管理与任务本身的执行分开时,命令模式很有用。
在本文中的示例中,通过将每个任务封装在一个公共接口中,将执行者(厨师)与任务分开。