缘起

最近复习设计模式
拜读谭勇德的<<设计模式就该这样学>>
本系列笔记拟采用golang练习之

命令模式

  1. 命令模式(Command Pattern)是对命令的封装,
  2. 每一个命令都是一个操作:
  3. 请求方发出请求要求执行一个操作;
  4. 接收方收到请求,并执行操作。
  5. 命令模式主要适用于以下应用场景。
  6. 1)现实语义中具备“命令”的操作(如命令菜单、Shell命令等)。
  7. 2)请求的调用者和接收者需要解耦,使得调用者和接收者不直接交互。
  8. 3)需要抽象出等待执行的行为,比如撤销(Undo)操作和恢复(Redo)等操作。
  9. 4)需要支持命令宏(即命令组合操作)。
  10. (摘自 谭勇德 <<设计模式就该这样学>>)

场景

  • 某线上学校, 提供在线美术课, 需要为小朋友提供在线画板
  • 画板的基本功能有: 点选颜色, 画点, 画线, 撤销上一步等操作
  • 码农王二狗接到Leader张阿蛋下达的开发任务, 加班加点, 很快把绘图功能做好了, 但是撤销功能不知道该咋实现, 于是只好请教Leader:
    • 王二狗: 张哥, 这个撤销功能, 百思不得姐啊
    • 张阿蛋: 我看看你咋弄的…我晕, 你的画板简直就是裸奔啊, 暴露那么多绘图函数
    • 王二狗: 难道…可是不暴露函数, 上层咋个调用啊?
    • 张阿蛋: 整个命令模式啊, 任何绘图操作都封装为一个带参数的命令
    • 王二狗: 不明觉厉…还是不懂 -_-|||
    • 张阿蛋: 画板内部整个堆栈, 绘图就是命令入栈, 撤销就是命令出栈, 刷新就是把栈里的命令遍历重做.
    • 王二狗: Got it! 张哥, 强!

设计

  • ICanvas: 定义画板接口, 供上层调用
  • IDrawCommand: 定义绘图命令接口
  • IGraphics: 定义绘图上下文接口
  • tMockGraphics: 虚拟的绘图上下文, 实现IGraphics接口
  • tMockCanvas: 虚拟画板类, 实现ICanvas接口
  • tColorCmd: 切换颜色的绘图命令, 实现IDrawCommand接口
  • tDotCmd: 画点的绘图命令, 实现IDrawCommand接口
  • tLineCmd: 画线的绘图命令, 实现IDrawCommand接口

单元测试

command_pattern_test.go

  1. package behavioral_patterns
  2. import (
  3. "image/color"
  4. "learning/gooop/behavioral_patterns/command"
  5. "testing"
  6. )
  7. func Test_CommandPattern(t *testing.T) {
  8. canvas := command.NewMockCanvas()
  9. canvas.Command(command.NewColorCmd(color.Black))
  10. canvas.Command(command.NewDotCmd(1, 1))
  11. canvas.Command(command.NewLineCmd(0, 0, 100, 100))
  12. canvas.Undo()
  13. canvas.Undo()
  14. }

测试输出

  1. $ go test -v command_pattern_test.go
  2. === RUN Test_CommandPattern
  3. tMockGraphics.Clear
  4. tMockGraphics.Color, (0,0,0,65535)
  5. tMockGraphics.Clear
  6. tMockGraphics.Color, (0,0,0,65535)
  7. tMockGraphics.DrawDot, (1,1)
  8. tMockGraphics.Clear
  9. tMockGraphics.Color, (0,0,0,65535)
  10. tMockGraphics.DrawDot, (1,1)
  11. tMockGraphics.DrawLine, (0,0) -> (100,100)
  12. tMockGraphics.Clear
  13. tMockGraphics.Color, (0,0,0,65535)
  14. tMockGraphics.DrawDot, (1,1)
  15. tMockGraphics.Clear
  16. tMockGraphics.Color, (0,0,0,65535)
  17. --- PASS: Test_CommandPattern (0.00s)
  18. PASS
  19. ok command-line-arguments 0.001s

ICanvas.go

定义画板接口, 供上层调用

  1. package command
  2. type ICanvas interface {
  3. Command(cmd IDrawCommand)
  4. Undo()
  5. }

IDrawCommand.go

定义绘图命令接口

  1. package command
  2. type IDrawCommand interface {
  3. Draw(g IGraphics)
  4. }

IGraphics.go

定义绘图上下文接口

  1. package command
  2. import "image/color"
  3. type IGraphics interface {
  4. Color(color color.Color)
  5. Clear()
  6. DrawDot(x int, y int)
  7. DrawLine(x0 int, y0 int, x1 int, y1 int)
  8. }

tMockGraphics.go

虚拟的绘图上下文, 实现IGraphics接口

  1. package command
  2. import (
  3. "fmt"
  4. "image/color"
  5. )
  6. type tMockGraphics struct {
  7. color color.Color
  8. }
  9. func newMockGraphics() IGraphics {
  10. return &tMockGraphics{
  11. }
  12. }
  13. func (me *tMockGraphics) Clear() {
  14. fmt.Printf("\ntMockGraphics.Clear\n")
  15. }
  16. func (me *tMockGraphics) Color(color color.Color) {
  17. me.color = color
  18. r,g,b,a := color.RGBA()
  19. fmt.Printf("tMockGraphics.Color, (%v,%v,%v,%v)\n", r, g, b, a)
  20. }
  21. func (me *tMockGraphics) DrawDot(x int, y int) {
  22. fmt.Printf("tMockGraphics.DrawDot, (%v,%v)\n", x, y)
  23. }
  24. func (me *tMockGraphics) DrawLine(x0 int, y0 int, x1 int , y1 int) {
  25. fmt.Printf("tMockGraphics.DrawLine, (%v,%v) -> (%v,%v)\n", x0, y0, x1, y1)
  26. }

tMockCanvas.go

虚拟画板类, 实现ICanvas接口

  1. package command
  2. import "errors"
  3. type tMockCanvas struct {
  4. commands []IDrawCommand
  5. graphics IGraphics
  6. }
  7. func NewMockCanvas() ICanvas {
  8. return &tMockCanvas{
  9. commands: make([]IDrawCommand, 0),
  10. graphics: newMockGraphics(),
  11. }
  12. }
  13. func (me *tMockCanvas) Command(cmd IDrawCommand) {
  14. me.push(cmd)
  15. me.update()
  16. }
  17. func (me *tMockCanvas) Undo() {
  18. e,_ := me.pop()
  19. if e != nil {
  20. return
  21. }
  22. me.update()
  23. }
  24. func (me *tMockCanvas) update() {
  25. me.graphics.Clear()
  26. for _,it := range me.commands {
  27. it.Draw(me.graphics)
  28. }
  29. }
  30. func (me *tMockCanvas) push(cmd IDrawCommand) {
  31. me.commands = append(me.commands, cmd)
  32. }
  33. func (me *tMockCanvas) pop() (error, IDrawCommand) {
  34. size := len(me.commands)
  35. if size <= 0 {
  36. return errors.New("no more commands"), nil
  37. }
  38. it := me.commands[size - 1]
  39. me.commands = me.commands[:size - 1]
  40. return nil,it
  41. }

tColorCmd.go

切换颜色的绘图命令, 实现IDrawCommand接口

  1. package command
  2. import "image/color"
  3. type tColorCmd struct {
  4. color color.Color
  5. }
  6. func NewColorCmd(c color.Color) IDrawCommand {
  7. return &tColorCmd{
  8. c,
  9. }
  10. }
  11. func (me *tColorCmd) Draw(g IGraphics) {
  12. g.Color(me.color)
  13. }

tDotCmd.go

画点的绘图命令, 实现IDrawCommand接口

  1. package command
  2. type tDotCmd struct {
  3. x int
  4. y int
  5. }
  6. func NewDotCmd(x int, y int) IDrawCommand {
  7. return &tDotCmd{
  8. x, y,
  9. }
  10. }
  11. func (me *tDotCmd) Draw(g IGraphics) {
  12. g.DrawDot(me.x, me.y)
  13. }

tLineCmd.go

画线的绘图命令, 实现IDrawCommand接口

  1. package command
  2. type tLineCmd struct {
  3. x0 int
  4. y0 int
  5. x1 int
  6. y1 int
  7. }
  8. func NewLineCmd(x0 int, y0 int, x1 int, y1 int) IDrawCommand {
  9. return &tLineCmd{
  10. x0, y0, x1, y1,
  11. }
  12. }
  13. func (me *tLineCmd) Draw(g IGraphics) {
  14. g.DrawLine(me.x0, me.y0, me.x1, me.y1)
  15. }

命令模式小结

  1. 命令模式的优点
  2. 1)通过引入中间件(抽象接口),解耦了命令请求与实现。
  3. 2)扩展性良好,可以很容易地增加新命令。
  4. 3)支持组合命令,支持命令队列。
  5. 4)可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。
  6. 命令模式的缺点
  7. 1)具体命令类可能过多。
  8. 2)命令模式的结果其实就是接收方的执行结果,
  9. 但是为了以命令的形式进行架构、解耦请求与实现,
  10. 引入了额外类型结构(引入了请求方与抽象命令接口),
  11. 增加了理解上的困难。
  12. (摘自 谭勇德 <<设计模式就该这样学>>)