命令模式(别名:动作,事务)

概念

将一个请求封装为一个对象,从而是用户可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销操作。

概述

例如:在军队作战时中,指挥官请求三连偷袭人,但是指挥官不希望或无法直接与三连取得联系,那么可以将该请求:”三连偷袭敌人”形成一个“作战命令”,该作战命令的核心就是“三连偷袭敌人”。只要能让该“作战命令”被执行(即使指挥官已经不存在),就会实现三连偷袭敌人的目的。如图所示:
军官要求三连偷袭敌人

结构与使用

命令模式的结构中包括四种角色。

  • 接收者(Receiver):接收者是一个类的实例,该实例负责执行与请求相关的操作。

  • 命令(Command)接口:命令是一个接口,规定了用来封装“请求”的若干方法,比如: execute() undo()等方法。

  • 具体命令(ConcreteCommand)具体命令是实现命令接口的类的实例。具体命令必须实现命令接口中的方法,比如execute()方法,使该方法封装一个“请求”。

  • 请求者(Invoker),请求者是一个包含Command接口变量的类的实例,请求者中的Command接口的变量可以存放任何具体命令的引用,请求者负责调用具体命令,让具体命令执行那些封装了“请求”的方法,比如execute()方法。

UML类图

类图

结构描述

下面通过前面举的例子,来对命令模式中涉及的各个角色进行简单的描述

1.接收者(Receiver)

接收者是下列TripleArmy类的一个实例,该实例的sneakAttack()方法可以实现如何偷袭敌人。

1
2
3
4
5
public class TripleArmy {
public void sneakAttack() {
System.out.println("我们知道如何偷袭敌人,保证完成任务。");
}
}

2.命令(Command)接口

1
2
3
public interface Command {
public abstract void execute();
}

3.具体命令(ConcreteCommand)

指挥官想调动三连sneakAttack()方法偷袭敌人,但是指挥官不想或无法直接与三连打交道。在这种情况下,可以把指挥官的请求:“三连的实例调用sneakAttack()”封装到一个具体命令对象的execute()方法中。

1
2
3
4
5
6
7
8
9
public class ConcreteCommand implements Command {
TripleArmy army; //含有接收者的应用
public ConcreteCommand(TripleArmy army) {
this.army = army;
}
public void execute() {
army.sneakAttack();
}
}

4.请求者(Invoker)

请求者含有Command接口声明的变量。请求者通过执行具体的命令,实现自己的请求。

1
2
3
4
5
6
7
8
9
public class ArmySuperior {
Command command;
public void setCommand(Command command) {
this.command = command;
}
public void executeCommand() {
command.execute();
}
}

5.模式的使用

1
2
3
4
5
6
7
public static void main(String[] args) {
TripleArmy tripleArmy = new TripleArmy(); //创建接收者
Command command = new ConcreteCommand(tripleArmy); // 创建具体命令并指定接收者
ArmySuperior superior = new ArmySuperior(); //创建请求者
superior.setCommand(command);
superior.executeCommand();
}

命令接口中的撤销方法

现在有如下问题:请求者请求在硬盘建立目录,请求成功后,还可以撤销请求。这就要求接收者不仅可以在硬盘上建立目录,也可以删除目录。针对该问题所设计的类图如图:

命令接口中的撤销方法

####1. 接收者

接收者是MakeDir类的一个实例。

1
2
3
4
5
6
7
8
9
10
public class MakeDir {
public void createDir(String name) {
File dir = new File(name);
dir.mkdir();
}
public void deleteDir(String name) {
File dir = new File(name);
dir.delete();
}
}

####2. 命令接口

1
2
3
4
5
6
7
8
9
10
public class MakeDir {
public void createDir(String name) {
File dir = new File(name);
dir.mkdir();
}
public void deleteDir(String name) {
File dir = new File(name);
dir.delete();
}
}

####3. 具体命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConcreteCommand implements Command {
ArrayList<String> dirNameList;
MakeDir makeDir;
public ConcreteCommand(MakeDir makeDir) {
dirNameList = new ArrayList<String>();
this.makeDir = makeDir;
}
@Override
public void execute(String name) {
makeDir.createDir(name);
dirNameList.add(name);
}
@Override
public void undo() {
if (dirNameList.size() > 0) {
int m = dirNameList.size();
String str = dirNameList.get(m-1);
makeDir.deleteDir(str);
dirNameList.remove(m-1);
}else
System.out.println("没有需要撤销的操作");
}
}

####4. 请求者

1
2
3
4
5
6
7
8
9
10
11
12
public class RequestMakeDir {
Command command;
public void setCommand(Command command) {
this.command = command;
}
public void executeCommand(String name) {
command.execute(name);
}
public void undoCommand() {
command.undo();
}
}

####5. 测试程序

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
MakeDir makeDir = new MakeDir(); //创建接收者
Command command = new ConcreteCommand(makeDir); // 创建具体命令并指定接收者
RequestMakeDir askMakeDir = new RequestMakeDir(); //创建请求者
askMakeDir.setCommand(command);
askMakeDir.executeCommand("corly");
askMakeDir.executeCommand("meng");
askMakeDir.undoCommand();
askMakeDir.undoCommand();
}

优点

  • 在命令模式中,请求者(Invoker)不直接与接收者(Receiver)交互,即请求者(Invoker)不包含接收者(Receiver)的引用,因此彻底消除了彼此之间的耦合。

  • 命令模式满足“开-闭原则”。如果增加新的具体命令和该命令的接收者,不必修改调用者的代码调用者就可以使用新的命令对象;反之,如果增加新的调用者,不必修改现有的具体命令和接收者,新增加的调用者就可以使用已有的具体命令。

  • 由于请求者的请求被封装到了具体命令中,那么就可以将具体命令保存到持久化的媒介中,在需要的时候,重新执行这个具体命令因此,因此,使用命令模式可以记录日志。

  • 使用命令模式可以对请求者的“请求”进行排队。每个请求都各自对应一个具体命令。因此可以按一定顺序执行这些具体命令。


举例:

模拟小电器

欢迎关注我的其它发布渠道