模式介绍
命令模式指的是将每一种请求封装成一个对象,把客户端参数化,当用户使用不同的请求时,对请求进行排队或者记录请求日志,以及支持可撤销的操作。
我们平时接触最多的命令模式就是菜单命令了,比如操作系统中的关机操作,我们点击“关机”后,系统会执行一系列的操作,这一系列的操作,对于用户而言,用户根本不了解点击“关机”后,程序执行了什么操作。用户要知道的,只是想要关闭电脑时,去点击一下“关机”就可以。
命令模式相对于其他模式而言,没有那么多的条条框框,不过正是因为这一点,命令模式相对于其他的设计模式更为灵活多变。
使用场景
- 需要抽象出待执行的操作,然后以参数的形式提供出来。
- 在不同的时刻指定、排列和执行请求。一个命令对象可以有与初始请求无关的生存期。
- 需要支持取消操作。
- 支持修改日志功能。
- 支持事务操作。
模式构成
- 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
18public class TetrisReceiver {
public String left(){
return "左移";
}
public String right(){
return "右移";
}
public String top(){
return "变形!";
}
public String bottom(){
return "加速下落!";
}
}
接下来我们定义一个接口,作为《俄罗斯方块》命令角色的抽象:1
2
3
4public interface TetrisCommand {
String execute();
}
有了命令抽象,就开始创建具体的命令类,它将持有Receiver:1
2
3
4
5
6
7
8
9
10
11
12
13public class TetrisCommandLeft implements TetrisCommand {
private TetrisReceiver receiver;
public TetrisCommandLeft(TetrisReceiver receiver) {
this.receiver = receiver;
}
public String execute() {
return receiver.left();
}
}
1 | public class TetrisCommandRight implements TetrisCommand { |
1 | public class TetrisCommandBottom implements TetrisCommand { |
1 | public class TetrisCommandTop implements TetrisCommand { |
对于请求者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
43public 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源码设计模式解析与实战》 何红辉、关爱民 著