Java设计模式-命令模式

首先,我们来看下命令模式的定义:
将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
命令模式是一种行为模式,通俗点说 可以把它看成分离的关注点,可将“动作请求者”从“动作执行者”对象中解耦;
对于请求者(客户端)只需要知道调用某个方法就能达到相应的效果,并不需要执行者具体是怎么做的,它们之间用一个命令对象来连接

命令模式结构

命令模式涉及到五个角色,它们分别是:

  • 客户端(Client)角色:
    创建一个具体命令(ConcreteCommand)对象并确定其接收者。
    通俗理解我们写的测试类就是一个客户端角色,也就是要负责创建那些所需要的对象
  • 命令(Command)角色:
    声明了一个给所有具体命令类的抽象接口。一般会定义一个 execute 和 undo 方法
  • 具体命令(ConcreteCommand)角色:
    定义一个接收者和行为之间的弱耦合;实现 execute() 方法,负责调用接收者的相应操作。
    execute() 方法通常叫做执行方法。
  • 请求者(Invoker)角色:
    负责调用命令对象执行请求,相关的方法叫做行动方法。
  • 接收者(Receiver)角色:
    负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。

一个简单的栗子

还是通过代码来看比较好,首先从命令角色开始写吧,就是定义一个接口,所有的命令都要实现它

1
2
3
4
5
public interface Command {
void execute();

void undo();
}

下面是接收者角色 ;就是具体做事情的类了,这种栗子大概只有我会看懂….emmm,下面会改的

1
2
3
4
5
public class LovelyLoli {
public void action() {
System.out.println("啦啦啦o(*≧▽≦)ツ.....dance");
}
}

然后定义具体的命令角色,大部分情况下是一种具体的命令对应一种“行为”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LoliDanceCommand implements Command {
// 命令角色中会持有一个接受者角色
private LovelyLoli loli;

// 使用构造函数来取得
LoliDanceCommand(LovelyLoli loli) {
this.loli = loli;
}

@Override
public void execute() {
System.out.println("准备中.....");
loli.action();
System.out.println("谢谢~~");
}

@Override
public void undo() {
System.out.println("这个,,,没法撤销....");
}
}

请求者角色 ,这个怎么说….就是执行命令的那个类??可理解为控制器,其实还是蛮重要的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LoliconControl {
// 首先里面包含有一个命令,这个命令控制着一个“功能”
private Command command;

// 提供一个 set 方法可以随时更改命令
public void setCommand(Command command) {
this.command = command;
}

// 调用命令相关的方法称为行动方法
public void action(){
command.execute();
}
}

准备的差不多多了,下面就可以进行测试了,测试类也可以说是客户端角色,随意就好,反正在真正用的时候我感觉不会管是什么角色…..知道怎么用就行…额

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
LoliconControl control = new LoliconControl(); // 调用者
LovelyLoli loli = new LovelyLoli(); // 具体执行者
// 创建命令
LoliDanceCommand command = new LoliDanceCommand(loli);

control.setCommand(command); // 设置命令
control.action(); // 调用命令
}

然后就应该看到效果了,以上就是命令模式的栗子了,可以看出其实还是非常简单的

小总结

可对比定义,一个命令对象通过在特定的接受者上绑定一组动作来封装一个请求。
要达到这一点,需要命令对象将动作和接收者包进对象中;这个对象(命令对象)只暴露出一个 execute 方法,当这个方法被调用的时候就会执行这些动作。
从外面看来,其他对象不知道究竟是那个接收者进行了那些动作,只知道如果调用 execute 方法请求的目的就能达到
最终的主要目的之一就是让请求者和接收者之间进行解耦

更全面的栗子

这里就参考 HeadFirst 中的栗子了,感觉还是非常好的,还是那几个角色,就是加入了一些别的功能,比如撤销、宏命令
命令角色我就不写了,因为是一样的,都是统一的 execute 和 undo 方法

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/********************* 接收者角色 **********************/
public class Light {
private String local;

public Light(String local) {
this.local = local;
}

public void on() {
System.out.println(local + "灯已经打开!");
}

public void off() {
System.out.println(local + "灯已经关闭!");
}
}
/********************* 具体命令角色 **********************/
public class LightOffCommand implements Command {
private Light light;

public LightOffCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
// 这里其实可以执行一系列的方法
// 如果次对象的状态存在多种,可以在执行之前记录当前的状态;以便给后面的撤销操作用
light.off();
}

@Override
public void undo() {
// 如果存在多个状态,在 exec 中记录状态,在这里获取记录的状态,然后再相应的处理
light.on();
}
}
public class LightOnCommand implements Command {
private Light light;

public LightOnCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.on();
}

@Override
public void undo() {
light.off();
}
}
/********************* 请求者角色 **********************/
public class RemoteControl {
private Command[] onCommand;
private Command[] offCommand;
// 如果想撤销多次,需要使用一个堆桟进行记录了
private Command undoCommand; // 撤销命令

public RemoteControl() {
// 在构造器中进行初始化,具体几个按键就不用太纠结了
onCommand = new Command[4];
offCommand = new Command[4];

// 默认所有的按键都是空的,这样执行的时候就省了判断 null 了
NoCommand noCommand = new NoCommand();
for (int i = 0; i < 4; i++) {
onCommand[i] = noCommand;
offCommand[i] = noCommand;
}

undoCommand = noCommand;
}

public void setCommand(int slot, Command onCommand, Command offCommand) {
this.onCommand[slot] =onCommand;
this.offCommand[slot] = offCommand;
}

// 打开
public void onButton(int slot) {
onCommand[slot].execute();
// 记录当前的命令
undoCommand = onCommand[slot];
}

// 关闭
public void offButton(int slot) {
offCommand[slot].execute();
// 记录当前的命令
undoCommand = offCommand[slot];
}

public void undoButton() {
undoCommand.undo();
}
}

与上面的简单例子相比,这个请求者容纳更多的命令,就像遥控器一样各自都有指定的位置,并且还具备撤销操作,其实也非常简单,接收者应该最清楚撤销怎么实现了,所以我们执行某个命令后如果需要撤销操作就再执行这条命令的撤销方法就行了,也就是说只需要记录执行时的这个命令对象就可以了,下面就看下测试的代码吧(或者说客户端?):

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
private static void lightTest() {
// 先创建好控制器(请求者)
RemoteControl remoteControl = new RemoteControl();
// 创建具体的执行者(接受者)
Light corridorLight = new Light("走廊");
Light roomLight = new Light("卧室");

// 创建相关的具体命令
LightOnCommand roomOnCmd = new LightOnCommand(roomLight);
LightOffCommand rommOffCmd = new LightOffCommand(roomLight);
LightOnCommand corridorOnCmd = new LightOnCommand(corridorLight);
LightOffCommand corridorOffCmd = new LightOffCommand(corridorLight);

// 设置到指定的按键上
remoteControl.setCommand(0,roomOnCmd,rommOffCmd);
remoteControl.setCommand(1,corridorOnCmd,corridorOffCmd);

// 调用执行
remoteControl.onButton(0);
remoteControl.onButton(1);
remoteControl.offButton(1);
// 撤销
System.out.println("撤销....");
remoteControl.undoButton();
remoteControl.offButton(0);
}

就是这样,这样命令模式就应该差不多很全了;关于撤销,如果想撤销多次那么可以使用一个类似堆桟的结构来按顺序存储多个命令,然后就很容易可以实现多次撤销了

使用宏命令

所谓的宏命令就是命令的集群,就是说一次性执行一连串的命令达到某个效果,当然也是可以进行撤销的,使用宏命令我们再加两个类,嫌麻烦不用接口大概也是可以的…..就是嘛可能会….

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
public interface MacroCommand extends Command {
// 最基本的添加和删除
void add(Command command);
void remove(Command command);
}
/******************* 宏命令 **************************/
public class MacroLightCommand implements MacroCommand {
private List<Command> commands = new ArrayList<>();
private List<Command> undoCommands = new ArrayList<>();

@Override
public void add(Command command) {
commands.add(command);
}

@Override
public void remove(Command command) {
commands.remove(command);
}

@Override
public void execute() {
for (Command command : commands) {
command.execute();
undoCommands.add(command);
}
}

@Override
public void undo() {
for (int i = undoCommands.size() - 1; i >= 0; i--) {
undoCommands.get(i).undo();
}
undoCommands.clear();
}
}

并没有太大的差别,具体命令对象中只是改用 List 保存一串命令,增加了 add 和 remove 方法,在执行的时候是将集合里的每个命令按顺序执行,下面是测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {

MacroCommand macroCommand = new MacroLightCommand();
Light light = new Light("卧室");
LovelyLoli loli = new LovelyLoli();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
LoliDanceCommand loliDanceCommand = new LoliDanceCommand(loli);

// 添加到宏命令
macroCommand.add(lightOnCommand);
macroCommand.add(loliDanceCommand);
macroCommand.add(lightOffCommand);

macroCommand.execute();
System.out.println("--------------");
macroCommand.undo();
}

效果还是不错的,嗯….

关于空对象

注意,这里说的是空对象 ,比如上面所说的 NoCommand 对象,当你不想返回一个有意义的对象时,空对象就显得非常的有用了,将处理 Null 的责任转移给了空对象,这大概会避免写太多的判断 xxx != null 的语句了,并且有效避免了空指针异常

在许多模式中都会看到空对象的使用,甚至在有些时候,空对象本身也视为是一种设计模式

命令模式的应用

就像定义中所说,很多语言在设计队列请求、线程池的时候就是用的命令模式,起码我知道在 Android 中的消息机制就是这样实现的;
比如,在一个工作队列中,我们在一边进行添加命令,另一端则是线程;线程会从队列中取出一个命令然后执行它的 execute 方法,等待调用完成后就将此命令进行丢弃,再取出下一个命令….
这样的实现,工作队列和进行计算的对象之间是完全解耦的,只要实现了命令模式的对象就可以加入到队列中;当线程调用的时候,不管你是具体做什么的,只知道调用其 execute 方法


还有一种比较出名的就是日志请求,通过序列化,当每个命令被执行时全部存储到硬盘中,如果出现宕机,可以从硬盘中取出重新加载,以正确的次序执行,这就相当于是回滚操作了

或者还可以应用于事务系统

命令模式的优点

  • 更松散的耦合
    命令模式使得发起命令的对象——客户端,和具体实现命令的对象——接收者对象完全解耦,也就是说发起命令的对象完全不知道具体实现对象是谁,也不知道如何实现。
  • 更动态的控制
    命令模式把请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,从而使得系统更灵活。
  • 很自然的复合命令
    命令模式中的命令对象能够很容易地组合成复合命令,也就是宏命令,从而使系统操作更简单,功能更强大。
  • 更好的扩展性
    由于发起命令的对象和具体的实现完全解耦,因此扩展新的命令就很容易,只需要实现新的命令对象,然后在装配的时候,把具体的实现对象设置到命令对象中,然后就可以使用这个命令对象,已有的实现完全不用变化。

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~