程序畅通执行-命令模式

模式介绍

命令模式指的是将每一种请求封装成一个对象,把客户端参数化,当用户使用不同的请求时,对请求进行排队或者记录请求日志,以及支持可撤销的操作。

我们平时接触最多的命令模式就是菜单命令了,比如操作系统中的关机操作,我们点击“关机”后,系统会执行一系列的操作,这一系列的操作,对于用户而言,用户根本不了解点击“关机”后,程序执行了什么操作。用户要知道的,只是想要关闭电脑时,去点击一下“关机”就可以。

命令模式相对于其他模式而言,没有那么多的条条框框,不过正是因为这一点,命令模式相对于其他的设计模式更为灵活多变。

使用场景

  • 需要抽象出待执行的操作,然后以参数的形式提供出来。
  • 在不同的时刻指定、排列和执行请求。一个命令对象可以有与初始请求无关的生存期。
  • 需要支持取消操作。
  • 支持修改日志功能。
  • 支持事务操作。

模式构成

  • Receiver:真正执行具体命令的核心类。
  • Command:抽象的命令接口。
  • ConcreateCommand:命令接口的具体实现类,持有Receiver。
  • Invoker:请求者类,持有Command。
  • Client:客户端角色,发出命令的地方,比如“关机”一例中,客户端就是我们自己。

模式示例

命令模式总体来说并不难,只是比较繁琐,一个简单的调用关系,被解耦成多个部分,必定会增加类的复杂度,但是即便如此,命令模式的结构也是合理并清晰的。

大家应该都玩过《俄罗斯方块》,这款游戏中有四个核心指令,那就是上下左右,左右控制左右平移,上控制变形,下控制快速下落,这次我们就模拟这款小游戏。

首先是Receiver,也就是执行具体命令的核心类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TetrisReceiver {

public String left(){
return "左移";
}

public String right(){
return "右移";
}

public String top(){
return "变形!";
}

public String bottom(){
return "加速下落!";
}
}

接下来我们定义一个接口,作为《俄罗斯方块》命令角色的抽象:

1
2
3
4
public interface TetrisCommand {

String execute();
}

有了命令抽象,就开始创建具体的命令类,它将持有Receiver:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandLeft implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandLeft(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.left();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandRight implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandRight(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.right();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandBottom implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandBottom(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.bottom();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TetrisCommandTop implements TetrisCommand {

private TetrisReceiver receiver;

public TetrisCommandTop(TetrisReceiver receiver) {
this.receiver = receiver;
}

@Override
public String execute() {
return receiver.top();
}
}

对于请求者Invoker,我们这里用一个Button类来表示,命令由按钮执行:

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
public class TetrisButtons {
private TetrisCommand commandLeft;
private TetrisCommand commandRight;
private TetrisCommand commandTop;
private TetrisCommand commandBottom;


public void setCommandLeft(TetrisCommand commandLeft) {
this.commandLeft = commandLeft;
}

public void setCommandRight(TetrisCommand commandRight) {
this.commandRight = commandRight;
}

public void setCommandTop(TetrisCommand commandTop) {
this.commandTop = commandTop;
}

public void setCommandBottom(TetrisCommand commandBottom) {
this.commandBottom = commandBottom;
}


public String toLeft() {
return commandLeft.execute();
}


public String toRight() {
return commandRight.execute();
}


public String toBottom() {
return commandBottom.execute();
}


public String toTop() {
return commandTop.execute();
}
}

最后,由客户端来决定,调用哪些命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//首先要有《俄罗斯方块游戏》
TetrisReceiver receiver = new TetrisReceiver();
//我们创建好定义好的命令
TetrisCommand commandLeft = new TetrisCommandLeft(receiver);
TetrisCommand commandRight = new TetrisCommandRight(receiver);
TetrisCommand commandBottom = new TetrisCommandBottom(receiver);
TetrisCommand commandTop = new TetrisCommandTop(receiver);
//将命令统一封装到按钮中
TetrisButtons buttons = new TetrisButtons();
buttons.setCommandLeft(commandLeft);
buttons.setCommandRight(commandRight);
buttons.setCommandBottom(commandBottom);
buttons.setCommandTop(commandTop);
//具体按下哪个按钮,由用户决定
buttons.toTop();
buttons.toTop();
buttons.toTop();
buttons.toTop();
buttons.toLeft();
buttons.toRight();
buttons.toRight();
buttons.toBottom();

总结

看了上述的案例,大家肯定觉得是一篇长篇代码文,明明是一个很简单的调用逻辑,为何要做的如此复杂?对于大部分开发者来说,可能更愿意接受这样的代码:

1
2
3
4
5
6
7
8
9
//创建游戏
TetrisReceiver receiver = new TetrisReceiver();
//实现什么操作,直接调用相关函数
receiver.top();
receiver.top();
receiver.top();
receiver.top();
receiver.left();
receiver.right();

上面这样写,确实会很方便,但是这样的逻辑留给后来者,没有人会觉得方便,日后维护修改,也会有许多不可控的隐患。

命令模式调用逻辑做的如此复杂,主要是遵循了设计模式当中重要的原则:对修改关闭,对扩展开放。

除此之外,使用命令模式的另一个好处是可以实现命令的记录功能。比如在上面的例子中,我们在请求者Invoker中使用一个数据结构来存储执行过程中的命令对象,以此可以方便地知道刚刚执行过那些命令,并可在必要时恢复、撤销,具体代码大家可以自行尝试。

命令模式充分体现了几乎是所有设计模式的通病:类数量的膨胀,大量衍生类的创建

其实这是一个不可避免的问题,因为这样做带给我们的好处非常多:更弱的耦合性、更灵活的控制性、更好的扩展性。

不过,在实际开发中,是不是采用命令模式还是需要斟酌。


感谢

《Android源码设计模式解析与实战》 何红辉、关爱民 著