概述
命令模式(Command Pattern),将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
该模式是一种对象行为型模式,别名为动作(Action)模式或事务(Transaction)模式。
命令模式是一种数据驱动的设计模式,请求以命令的形式包裹在对象中,并传给调用对象,调用对象寻找可处理该命令的对象,将命令传给该对象,再由该对象执行命令。
软件开发当中,经常需要向某些对象发送请求(调用其中的某个或某些方法),但并不知道请求的具体接收者是谁。这时,希望能用一种松耦合的方式设计软件,使得请求发送者与请求接收者消除彼此间的耦合,使对象间的调用关系更加灵活,可灵活指定请求接收者和被请求的操作(命令)之间的关系。命令模式为此类问题提供了较为完美的解决方案。
命令模式可将请求发送者和请求接收者完全解耦,使两者没有直接的依赖关系,发送请求的对象只需知道如何发送请求,无需知道如何接收请求以及如何对请求进行处理。
命令模式的核心在于引入了命令类,通过命令类来降低请求发送者和请求接收者之间的耦合度,请求发送者只需指定一个命令对象,通过命令对象来调用请求接收者的处理方法。
命令模式主要包含下面几个角色
Command(抽象命令类),一般是抽象类或接口,其中声明用于执行请求的execute等方法,通过这些方法可调用请求接收者的相应处理方法。
ConcreteCommand(具体命令类),抽象命令类的子类,实现了在抽象命令类中声明的方法。它对应具体的接收者对象,将接收者对象绑定在该类(作为该类的一个对象型变量)。实现execute()方法时,调用接收者对象的处理方法。
Invoker(请求发送者),调用者,通过命令对象类执行请求。一个调用者在设计时不需要知道其接收者,因此它只与抽象命令类间存在关系。在程序运行时可将一个具体的命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关处理方法。
Receiver(请求接收者),执行与请求相关的操作,具体实现处理方法。
命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发送命令的操作和执行命令的操作分隔开来。每个命令都是一个操作:请求一方发出请求要求执行某个操作,接收方收到请求,具体执行该操作。命令模式使得请求者不必知道接收者,也不必知道请求是如何被接收者接收、是否会被接收以及接收后对命令具体是如何执行的。
命令模式关键在于引入抽象命令类,请求发送者针对抽象命令类进行编程,只有实现了抽象命令类的具体命令类才与请求接收者相关联(解除了请求发送者和接收者的关联关系)。
实例
cs可是一款经典的射击游戏,记得以前刚接触电脑,鼠标还不会用的时候就开始玩这个游戏,游戏默认的WSAD键分别对应着控制人物前进后退左移和右移,在Esc设置界面可以对键盘按键进行重新绑定,现在我想把前进后退的两个按键颠倒过来,W后退,S前进:)
命令模式实现,在抽象命令类中只定义一个execute()方法,每个具体命令类中引用一个请求接收者类型的变量,不同的具体命令类提供了execute()方法不同的实现,也就是调用不同的接收者去实现命令。
定义抽象命令类
public abstract class Command {
public abstract void execute();
}
定义具体命令类
前进命令类
public class GoForwardCommand extends Command {
//引用前进命令执行类类型的对象
private GoForwardHandler handler;
public GoForwardCommand() {
handler = new GoForwardHandler();
}
//命令执行方法,调用请求接受者的处理方法
@Override
public void execute() {
handler.goForward();
}
}
后退命令类
public class GoBackCommand extends Command {
//引用后退命令执行类类型的对象
private GoBackHandler handler;
public GoBackCommand() {
handler = new GoBackHandler();
}
//命令执行方法,调用请求接收者的处理方法
@Override
public void execute() {
handler.goBack();
}
}
在上面的代码中,具体命令类继承了抽象命令类,它与请求接收者相关联(引用请求接收者类型的变量),实现了在抽象命令类中声明的execute()方法,在实现方法中调用请求接收者的处理方法。
请求发送者
功能按键类
按下键盘上的某个键,发送某个命令
//功能键类
public class FunctionKey {
//功能键名称
private String name;
//引用抽象命令类类型对象作为变量
private Command command;
public FunctionKey(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
//为按键注入命令
public void setCommand(Command command) {
this.command = command;
}
//按下按键,发送请求(比如想要使人物后退按下W键)
public void onClick() {
System.out.println("点击" + name);
//发送命令
command.execute();
System.out.println("------------------------------");
}
}
请求接收者
前进命令执行类
public class GoForwardHandler {
public void goForward() {
System.out.println("正在前进!!");
}
}
后退命令执行类
public class GoBackHandler {
public void goBack() {
System.out.println("正在后退!!");
}
}
请求接收者类具体实现对请求命令的处理,它提供了实现方法,用于执行与请求相关的操作。
按键绑定设置界面类
public class KeyBindingWindow {
//窗口标题
private String title;
//定义一个集合用来存储按键
private ArrayList<FunctionKey> list = new ArrayList<FunctionKey>();
public KeyBindingWindow(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
//向该设置窗口添加按键设置
public void addFunctionKey(FunctionKey key) {
list.add(key);
}
//显示窗口名称及该窗口中可以设置的按键
public void display() {
System.out.println("窗口名称:" + title);
System.out.println("可以设置的功能按键:");
for(FunctionKey key : list) {
System.out.println(key.getName());
}
}
}
为提高系统灵活性和扩展性,将具体命令类的路径存储在配置文件中,并通过工具类XMLUtil来读取配置文件并通过反射生成对象
XMLUtil类代码如下
//该类需要的jar包有org.dom4j包和jaxen包
public class XMLUtil {
//从配置文件中提取具体类类名,并返回一个实例对象
public static Object getBean(String name) throws Exception {
SAXReader reader = new SAXReader();
String path = XMLUtil.class.getClassLoader().getResource("com/env/command/config.xml").getPath();
Document document = reader.read(new File(path));
//类名
String cName = null;
if(name.equals("goForwardCommand")) {
cName = document.selectSingleNode("/config/goForwardCommand").getText();
} else if (name.equals("goBackCommand")) {
cName = document.selectSingleNode("/config/goBackCommand").getText();
}
//通过类名生成实例对象并将其返回
Class<?> c = Class.forName(cName);
Object object = c.getInstance();
return object;
}
}
配置文件代码如下
<?xml version="1.0" encoding="UTF-8"?>
<config>
<goForwardCommand>com.env.command.GoForwardCommand</goForwardCommand>
<goBackCommand>com.env.command.GoBackCommand</goBackCommand>
</config>
客户端测试类
public class CommandPatternDemo {
public static void main(String[] args) {
//新建按键绑定设置窗口
KeyBindingWindow window = new KeyBindingWindow("按键绑定设置窗口");
//前进键
FunctionKey goForwardKey = new FunctionKey("前进键(S)");
//后退键
FunctionKey goBackKey = new FunctionKey("后退键(W)");
//通过读取配置文件使用反射生成命令对象
Command command1 = (Command)XMLUtil.getBean("goForwardCommand");
Command command2 = (Command)XMLUtil.getBean("goBackCommand");
//将命令对象注入功能键(为按键绑定命令)
goForwardKey.setCommand(command1);
goBackKey.setCommand(command2);
//在按键绑定设置窗口中添加前进和后退按键设置
window.addFunctionKey(goForwardKey);
window.addFunctionKey(goBackKey);
//调用显示窗口名称和可以设置的按键方法
window.display();
System.out.println("============================");
//调用功能按键的业务方法(按下按键)
goForwardKey.onClick();
goBackKey.onClick();
}
}
输出结果
倘若我想把前进后退键改回来,按W键前进,按S键后退,只需在客户端中修改命令对象注入功能键的代码即可
goForwardKey.setCommand(command2);
goBackKey.setCommand(command1);
这时,我又想修改一个按键动作之间的绑定,默认的按Ctrl下蹲我用不习惯,通常要改成Shift键
只需如下几步
新建一个具体命令类下蹲命令类
public class SquatCommand extends Command {
//引用下蹲命令执行类类型的对象
private SquatHandler handler;
public SquatCommand(SquatHandler handler) {
this.handler = handler;
}
//命令执行方法,调用请求接收者的处理方法
@Override
public void execute() {
handler.squat();
}
}
新建一个请求接收者下蹲命令执行类
public class SquatHandler {
public void squat() {
System.out.println("已蹲下!!");
}
}
config.xml配置文件中新建一个节点
<squatCommand>com.env.command.SquatCommand</squatCommand>
XMLUtil类中新加一个else if判断
else if (name.equals("squatCommand")) {
cName = document.selectSingleNode("/config/squatCommand").getText();
}
客户端测试类添加如下几句代码
//新建一个功能键下蹲键
FunctionKey squatKey = new FunctionKey("下蹲键(Shift)");
//通过读取配置文件利用反射生成命令对象
Command command3 = (Command)XMLUtil.getBean("squatCommand");
//将命令绑定到按键上
squatKey.setCommand(command3);
//在按键绑定设置窗口添加下蹲按键设置
window.addFunctionKey(squatKey);
//按下下蹲键
squatKey.onClick();
执行测试方法,打印结果变成
在此过程中,每个具体命令类对应一个接收者,通过向请求发送者(键盘按键)注入不同的具体命令对象,使得同一个发送者可对应不同的接收者,从而实现将一个请求封装为一个对象,用不同的请求对客户进行参数化。
客户端只需将具体命令对象作为参数注入请求发送者(为按键绑定命令),无需直接操作请求接收者。
总结
优点
降低系统耦合度,将请求发送者和接收者解耦,调用者实现功能时只需调用Command抽象类的execute()方法即可,不需知道具体是哪个接收者执行;具体命令类可像其他对象一样被操纵和扩展;增加一个新的具体命令类无需修改已有的代码,符合"开闭原则"。
缺点
使用命令模式可能会导致系统存在过多的具体命令类,造成资源占用。
使用场景
系统需将请求调用者和接收者解耦,使得两者不直接交互;系统需在不同时间指定请求,将请求进行排队(如线程池+工作队列)和执行请求;系统需支持命令的撤销(Undo)操作和恢复(Redo)操作;系统需将一组操作结合在一起,也就是支持宏命令。
更多用途
队列请求:将命令排成一个队列打包,逐个调用execute()方法,如线程池的任务队列,线程不关心任务队列中是读取IO还是进行计算,亦或是其他操作,只取出命令后执行,接着进行下一个。
日志请求:某些应用需将所有动作记录在日志中,然后在系统死机或是出现其他状况后,重新调用这些动作恢复到之前的状态,如数据库事务。
撤销操作:命令模式中,可通过调用一个命令对象的execute()方法实现对请求的处理,若需撤销请求,可通过在命令类中增加一个逆向操作来实现。