Scott's world.

设计模式-命令模式

Word count: 2.6kReading time: 9 min
2019/08/12 Share

设计模式-命令模式

模式定义

命令模式(Command Pattern):将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

模式动机

在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。

命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。

简单的来说

将请求封装成对象,将动作请求者和动作执行者解耦。

模式结构

  • Command类:是一个抽象类,类中对需要执行的命令进行声明,一般来说要对外公布一个execute方法用来执行命令。
  • ConcreteCommand类:Command类的实现类,对抽象类中声明的方法进行实现。
  • Client类:最终的客户端调用类.

  • Invoker类:调用者,负责调用命令。

  • Receiver类:接收者,负责接收命令并且执行命令.

模式实现

  • 假想一个用万能遥控器控制家电中灯,电脑等的场景

    下面是用go写的具体代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package commendpattern

import "fmt"

type light struct{
name string
}

type computer struct{
name string
}

type commend interface {
execute()
}

type lightOnCommend struct{
light
}
type lightOffCommend struct{
light
}
type computerOnCommend struct{
computer
}
type computerOffCommend struct{
computer
}

func (l *lightOnCommend)execute(){
l.name="灯"
fmt.Println(l.name,"开")
}
func (l *lightOffCommend)execute(){
l.name="灯"
fmt.Println(l.name,"关")
}
func (computerOnCommend)execute(){
l:=computer{name:"电脑"}
fmt.Println(l.name,"开")
}
func (computerOffCommend)execute(){
l:=computer{name:"电脑"}
fmt.Println(l.name,"关")
}

type controller struct{
commends []commend
}

func (c *controller)setCommend(m commend){
c.commends=append(c.commends,m)
}

func (c *controller)actionCommend(i int){
c.commends[i].execute()
}

func TestCommendPattern(){
c:=controller{commends:nil}
c.setCommend(&lightOnCommend{light{"灯"},})
c.setCommend(&lightOffCommend{light{"灯"},})
c.setCommend(&computerOffCommend{computer{name:"电脑"}})
c.setCommend(&computerOnCommend{computer{name:"电脑"}})
c.actionCommend(1)
c.actionCommend(2)
c.actionCommend(0)
c.actionCommend(3)
}

  • 这里也给出此命令模式的通用代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class Invoker {
    private Command command;
    public void setCommand(Command command) {
    this.command = command;
    }
    public void action(){
    this.command.execute();
    }
    }

    abstract class Command {
    public abstract void execute();
    }

    class ConcreteCommand extends Command {
    private Receiver receiver;
    public ConcreteCommand(Receiver receiver){
    this.receiver = receiver;
    }
    public void execute() {
    this.receiver.doSomething();
    }
    }

    class Receiver {
    public void doSomething(){
    System.out.println("接受者-业务逻辑处理");
    }
    }

    public class Client {
    public static void main(String[] args){
    Receiver receiver = new Receiver();
    Command command = new ConcreteCommand(receiver);
    //客户端直接执行具体命令方式(此方式与类图相符)
    command.execute();

    //客户端通过调用者来执行命令
    Invoker invoker = new Invoker();
    invoker.setCommand(command);
    invoker.action();
    }
    }

    通过代码我们可以看到,当我们调用时,执行的时序首先是调用者类,然后是命令类,最后是接收者类。也就是说一条命令的执行被分成了三步,它的耦合度要比把所有的操作都封装到一个类中要低的多,而这也正是命令模式的精髓所在:把命令的调用者与执行者分开,使双方不必关心对方是如何操作的。

模式分析

命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。

  • 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。
  • 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
  • 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
  • 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。

模式优缺点

首先,命令模式的封装性很好:每个命令都被封装起来,对于客户端来说,需要什么功能就去调用相应的命令,而无需知道命令具体是怎么执行的。比如有一组文件操作的命令:新建文件、复制文件、删除文件。如果把这三个操作都封装成一个命令类,客户端只需要知道有这三个命令类即可,至于命令类中封装好的逻辑,客户端则无需知道。
其次,命令模式的扩展性很好,在命令模式中,在接收者类中一般会对操作进行最基本的封装,命令类则通过对这些基本的操作进行二次封装,当增加新命令的时候,对命令类的编写一般不是从零开始的,有大量的接收者类可供调用,也有大量的命令类可供调用,代码的复用性很好。比如,文件的操作中,我们需要增加一个剪切文件的命令,则只需要把复制文件和删除文件这两个命令组合一下就行了,非常方便。
最后说一下命令模式的缺点,那就是命令如果很多,开发起来就要头疼了。特别是很多简单的命令,实现起来就几行代码的事,而使用命令模式的话,不用管命令多简单,都需要写一个命令类来封装。

适用场景

在以下情况下可以使用命令模式:

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
  • 系统需要将一组操作组合在一起,即支持宏命令

模式应用

  • 宏命令功能

    如UNIX平台下的Shell编程,可以将多条命令封装在一个命令对象中,只需要一条简单的命令即可执行一个命令序列

  • 日志

    日志一般用于记录用户行为,或者在异常时恢复时用的,比如每个命令现在包含两个方法,一个执行execute,一个undo(上例中为了方便大家理解,没有写undo),我们可以把用户所有命令调用保存到日志中,比如用户操作不当了,电器异常了,只需要把日志中所有的命令拿出来执行一遍undo就完全恢复了,是吧,就是这么个意思。

  • 队列

    设置命令和执行命令就有点类似于队列的数据结构

模式总结

  • 在命令模式中,将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作模式或事务模式。
  • 命令模式包含四个角色:抽象命令类中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作;具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中;调用者即请求的发送者,又称为请求者,它通过命令对象来执行请求;接收者执行与请求相关的操作,它具体实现对请求的业务处理。
  • 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
  • 命令模式的主要优点在于降低系统的耦合度,增加新的命令很方便,而且可以比较容易地设计一个命令队列和宏命令,并方便地实现对请求的撤销和恢复;其主要缺点在于可能会导致某些系统有过多的具体命令类。
  • 命令模式适用情况包括:需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互;需要在不同的时间指定请求、将请求排队和执行请求;需要支持命令的撤销操作和恢复操作,需要将一组操作组合在一起,即支持宏命令。

我们开发中,请求-响应模式的功能非常常见,一般来说,我们会把对请求的响应操作封装到一个方法中,这个封装的方法可以称之为命令,但不是命令模式。到底要不要把这种设计上升到模式的高度就要另行考虑了,因为,如果使用命令模式,就要引入调用者、接收者两个角色,原本放在一处的逻辑分散到了三个类中,设计时,必须考虑这样的代价是否值得。

参考

https://blog.csdn.net/zhengzhb/article/details/7550895

https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/command.html

CATALOG
  1. 1. 设计模式-命令模式
    1. 1.1. 模式定义
    2. 1.2. 模式动机
    3. 1.3. 模式结构
    4. 1.4. 模式实现
    5. 1.5. 模式分析
    6. 1.6. 模式优缺点
    7. 1.7. 适用场景
    8. 1.8. 模式应用
    9. 1.9. 模式总结
    10. 1.10. 参考