13、命令模式.md

命令模式

目录

  • 命令模式
      • 命令模式
    • 分析
  • 应用场景
  • 代码实现
  • 实例
    • 将函数封装为对象
      • 代码
      • 测试
    • 将函数直接作为参数
      • 代码
      • 测试
  • 总结

*

命令模式很多同学可能不会用到,但这个模式还是蛮有意思的。命令模式能够将操作和数据打包成对象,便于系统对命令进行管理、维护。

命令模式

命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

UML

*

分析

上面的定义和UML比较复杂,大家可能比较难理解。

首先我们需要明白什么是命令。命令包括指令和数据。指令是行为,数据影响到指令。如前进3米,前进是指令,3米是数据。

然后我们再看一下各个类的含义。

Command和ConcreteCommand是命令,有Excute函数,代表要做的行为。

ConcreteCommand调用Excute(),最终调用Receiver的Action。这意味ConcreteCommand只是一个容器,真正的操作逻辑在Receiver中。

Invoker包含了所有Command,控制Command何时执行Excute()。

现在我们将UML简化,把Invoker、Receiver去掉,看看是否容易理解了。

*

通过这个简洁版UML,我们来看一下为什么要用命令模式。

命令包括指令和数据,指令其实对应着操作,操作在代码中对应着函数。

命令模式其实是把函数封装成对象,系统能对对象进行各种操作,如排队执行、记录日志、撤销等。

为什么要将函数包装成对象呢?C、C++、Go支持函数指针,但并不是所有语言都有这种特性,这时命令模式就起作用了。而且即使语言支持函数指针,命令的数据部分怎么存放仍是一个问题。

所以简单理解,命令模式就是把请求打包成一个一个Command对象,存储起来,系统根据实际需求进行处理。

应用场景

大家可能感觉命令模式与MQ、工厂模式一样,其实在细节上是有区别的:

  • MQ只包含数据,不包含行为,命令模式两者都包含
  • 工厂模式需要实时执行,但命令模式可以进行存储,延后执行

代码实现

package main

import "fmt"

/**
 * @Description: 命令接口
 */
type Command interface {
   
     
   Execute()
}

/**
 * @Description: 移动命令
 */
type MoveCommand struct {
   
     
   x, y int64
}

/**
 * @Description: 如何移动
 * @receiver m
 */
func (m *MoveCommand) Execute() {
   
     
   fmt.Printf("向右移动%d,向上移动%d \n", m.x, m.y)
}

/**
 * @Description: 攻击命令
 */
type AttackCommand struct {
   
     
   skill string
}

/**
 * @Description: 如何攻击
 * @receiver a
 */
func (a *AttackCommand) Execute() {
   
     
   fmt.Printf("使用技能%s\n", a.skill)
}

/**
 * @Description: 记录命令
 * @param action
 * @return Command
 */
func AddCommand(action string) Command {
   
     
   if action == "attack" {
   
     
      return &AttackCommand{
   
     
         skill: "野蛮冲撞",
      }
   } else {
   
      //默认是移动
      return &MoveCommand{
   
     
         x: 10,
         y: 20,
      }
   }
}

func main() {
   
     
   //将命令记录
   lc := make([]Command, 0)
   lc = append(lc, AddCommand("attack"))
   lc = append(lc, AddCommand("move"))
   lc = append(lc, AddCommand("move"))
   lc = append(lc, AddCommand("attack"))

   //执行命令
   for _, c := range lc {
   
     
      c.Execute()
   }
}

输出:

*myproject go run main.go

使用技能野蛮冲撞

向右移动10,向上移动20

向右移动10,向上移动20

使用技能野蛮冲撞

通过上面的代码,大家应该能够理解命令模式了。可以看出,对不同请求,生成不同的Command,Command中包含对应的数据与操作。这也是模式定义中说到的”对请求排队或记录请求日志,以及支持可撤销的操作“。

实例

接下来会有两个例子,第一个是按照原文定义的方式,将函数封装成对象,第二个例子我们直接将函数作为参数传递。

将函数封装为对象

代码
// Package command 命令模式
// Blog: https://lailin.xyz/post/command.html
// 这是示例一,采用将函数封装为对象的方式实现,
// 示例说明:
// 假设现在有一个游戏服务,我们正在实现一个游戏后端
// 使用一个 goroutine 不断接收来自客户端请求的命令,并且将它放置到一个队列当中
// 然后我们在另外一个 goroutine 中来执行它
package command

import "fmt"

// ICommand 命令
type ICommand interface {
   
     
	Execute() error
}

// StartCommand 游戏开始运行
type StartCommand struct{
   
     }

// NewStartCommand NewStartCommand
func NewStartCommand( /*正常情况下这里会有一些参数*/ ) *StartCommand {
   
     
	return &StartCommand{
   
     }
}

// Execute Execute
func (c *StartCommand) Execute() error {
   
     
	fmt.Println("game start")
	return nil
}

// ArchiveCommand 游戏存档
type ArchiveCommand struct{
   
     }

// NewArchiveCommand NewArchiveCommand
func NewArchiveCommand( /*正常情况下这里会有一些参数*/ ) *ArchiveCommand {
   
     
	return &ArchiveCommand{
   
     }
}

// Execute Execute
func (c *ArchiveCommand) Execute() error {
   
     
	fmt.Println("game archive")
	return nil
}

测试
package command

import (
	"fmt"
	"testing"
	"time"
)

func TestDemo(t *testing.T) {
   
     
	// 用于测试,模拟来自客户端的事件
	eventChan := make(chan string)
	go func() {
   
     
		events := []string{
   
     "start", "archive", "start", "archive", "start", "start"}
		for _, e := range events {
   
     
			eventChan <- e
		}
	}()
	defer close(eventChan)

	// 使用命令队列缓存命令
	commands := make(chan ICommand, 1000)
	defer close(commands)

	go func() {
   
     
		for {
   
     
			// 从请求或者其他地方获取相关事件参数
			event, ok := <-eventChan
			if !ok {
   
     
				return
			}

			var command ICommand
			switch event {
   
     
			case "start":
				command = NewStartCommand()
			case "archive":
				command = NewArchiveCommand()
			}

			// 将命令入队
			commands <- command
		}
	}()

	for {
   
     
		select {
   
     
		case c := <-commands:
			c.Execute()
		case <-time.After(1 * time.Second):
			fmt.Println("timeout 1s")
			return
		}
	}
}

将函数直接作为参数

代码
// Package command 命令模式
// Blog: https://lailin.xyz/post/command.html
// 这是示例二,采用将直接返回一个函数,不用对象
// 示例说明:
// 假设现在有一个游戏服务,我们正在实现一个游戏后端
// 使用一个 goroutine 不断接收来自客户端请求的命令,并且将它放置到一个队列当中
// 然后我们在另外一个 goroutine 中来执行它
package command

import "fmt"

// Command 命令
type Command func() error

// StartCommandFunc 返回一个 Command 命令
// 是因为正常情况下不会是这么简单的函数
// 一般都会有一些参数
func StartCommandFunc() Command {
   
     
	return func() error {
   
     
		fmt.Println("game start")
		return nil
	}
}

// ArchiveCommandFunc ArchiveCommandFunc
func ArchiveCommandFunc() Command {
   
     
	return func() error {
   
     
		fmt.Println("game archive")
		return nil
	}
}

测试
package command

import (
	"fmt"
	"testing"
	"time"
)

func TestDemoFunc(t *testing.T) {
   
     
	// 用于测试,模拟来自客户端的事件
	eventChan := make(chan string)
	go func() {
   
     
		events := []string{
   
     "start", "archive", "start", "archive", "start", "start"}
		for _, e := range events {
   
     
			eventChan <- e
		}

	}()
	defer close(eventChan)

	// 使用命令队列缓存命令
	commands := make(chan Command, 1000)
	defer close(commands)

	go func() {
   
     
		for {
   
     
			// 从请求或者其他地方获取相关事件参数
			event, ok := <-eventChan
			if !ok {
   
     
				return
			}

			var command Command
			switch event {
   
     
			case "start":
				command = StartCommandFunc()
			case "archive":
				command = ArchiveCommandFunc()
			}

			// 将命令入队
			commands <- command
		}
	}()

	for {
   
     
		select {
   
     
		case c := <-commands:
			c()
		case <-time.After(1 * time.Second):
			fmt.Println("timeout 1s")
			return
		}
	}
}

总结

设计模式是为了解决现实中的问题,我们需要和具体场景相绑定。在解决问题的时候,采用的是不是标准的设计模式并不重要,模式只是手段,手段需要为达成目的服务。

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: