一、命令模式的概念
命令模式的定义:将“请求”封装成对象,以便使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作。
命令模式的目的:是解除调用类与接收者类之间的耦合。
命令模式理解引导:看电视时,我们点击遥控器的换台按钮,电视机就会转换到相应的频道。在这个最典型的例子中,我们(Client)发出一条命令(Commend)给遥控器(Invoker),然后遥控器接着通知电视机(Receiver)执行该命令,这就是命令模式的表现。让电视机转换频道这个动作的调用者本该是我们,而我们则把这个调用的动作交给了遥控器来达到调用的目的,然而无论如何都是电视机自己转换了频道,我们或遥控器只是决定了电视机转换频道的时机。我们不直接调用电视机的调频功能就能调频,这样就实现了调用者与接收者的解耦。
命令模式的构成:
1、Client做初始化,实例化所有将要执行的Commend对象,和提供对应功能的Receiver(即TV)对象。
2、Commend是命令接口,其子类负责提供请求的处理功能,但他不包含功能的具体实现。
2、Invoker是控制类,决定何时调用某功能。
3、Receiver(TV)是所有命令的功能代码的具体实现类。
命令模式的UML类图
二、命令模式的java实现
通过java代码实现遥控电视的命令模式:
具体的接收者,是命令的执行者。
/**
* 命令的接收者,实现具体的命令代码
**/
public class TV {
/**
* 具体的开机操作实现
* */
public void open() {
System.out.println("\n----开机操作----");
System.out.println("检查线路");
System.out.println("连接电源");
System.out.println("播放");
System.out.println("----开机完毕----\n");
}
/**
* 具体的关机操作实现
* */
public void close() {
System.out.println("\n----关闭操作----");
System.out.println("停止播放");
System.out.println("切断电源");
System.out.println("----关闭完毕----\n");
}
/**
* 具体的转换频道操作实现
* */
public void channel() {
System.out.println("\n----切换频道操作----");
System.out.println("正在切换频道");
System.out.println("----切换频道完成----\n");
}
}
命令接口,统一命令的规则。
/**
* <span style="font-family: Arial, Helvetica, sans-serif;">命令的接口,定义命令的规则</span>
* */
public interface Commend {
/**
* 接口函数
* */
public void exectute();
}
命令接口的具体实现类,该类持有一个具体接受者类的引用,以便调用接收者对应功能的方法。
关闭命令:
/**
* <span style="font-family: Arial, Helvetica, sans-serif;">关闭命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现</span>
* */
public class CloseCommend implements Commend {
/**
* 持有TV的引用
* */
private TV tv;
public CloseCommend(TV tv) {
this.tv = tv;
}
/**
* 调用TV中对应的操作
* */
public void exectute() {
tv.close();
}
}
调频命令:
/**
* <span style="font-family: Arial, Helvetica, sans-serif;">调频命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现</span>
* */
public class ChannelCommend implements Commend {
/**
* 持有TV的引用
* */
private TV tv;
public ChannelCommend(TV tv) {
this.tv = tv;
}
/**
* 调用TV中对应的操作
* */
public void exectute() {
tv.channel();
}
}
开机命令:
/**
* <span style="font-family: Arial, Helvetica, sans-serif;">开机命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现</span>
*/
public class OpenCommend implements Commend {
/**
* 持有TV的引用
* */
private TV tv;
public OpenCommend(TV tv) {
this.tv = tv;
}
/**
* 调用TV中对应的操作
* */
public void exectute() {
tv.open();
}
}
调用者,通过他来实现调用与执行分离。
/**
* 调用者
* */
public class Invoker {
/**
* 持有命令的父接口引用
* */
private Commend com;
public void setCom(Commend com) {
this.com = com;
}
/**
* 执行命令中的方法
* */
public Invoker(Commend c) {
this.com = c;
}
public void call() {
com.exectute();
}
}
客户类,负责创建命令,接受者,和调用者。
/**
* 客户类
* */
public class Client {
public static void main(String[] args) {
//创建TV对象
TV tv = new TV();
//创建开机命令
Commend open = new OpenCommend(tv);
Invoker iv = new Invoker(open);
iv.call();
//创建调频命令
Commend channel = new ChannelCommend(tv);
iv.setCom(channel);
iv.call();
//创建关机命令
Commend close = new CloseCommend(tv);
iv.setCom(close);
iv.call();
}
}
执行结果:
三、命令模式的进阶
在了解基本的命令模式的实现之后,我们再对命令模式进行一些功能性的扩充,实现一些高级的操作。比如在调用者中存储多个命令,实现命令的撤销等
3.1、存储多个命令
你肯定会发现这样和直接调用电视机的相应功能没什么区别,对没错,这只是演示最基本的命令模式。上面的Invoker,只能通过不断的装载新的命令来调用新的功能,并不像遥控器那样可以执行不同的命令,遥控器如果是这样就没有什么意义了。下面来添加一些功能来实现一个较为真实的遥控器。
修改后的Invoker:
/**
* 调用者
*/
public class Invoker {
/**
* 持有持有多个命令接口的引用
*/
private Commend[] coms;
/**
* 初始化Invoker
*/
public Invoker() {
// 默认可以存储三个命令
this.coms = new Commend[3];
}
/**
* 将命令设置到对应的位置上
*
* @param com
* @param index
*/
public void setCom(Commend com, int index) {
coms[index] = com;
}
/**
* 执行对应命令中的方法
*/
public void call(int index) {
coms[index].exectute();
}
}
对应的Client:
/**
* 客户类
*/
public class Client {
public static void main(String[] args) {
// 创建TV对象
TV tv = new TV();
// 创建一个遥控器Invoker
Invoker iv = new Invoker();
// 创建开机命令
Commend open = new OpenCommend(tv);
// 创建调频命令
Commend channel = new ChannelCommend(tv);
// 创建关机命令
Commend close = new CloseCommend(tv);
iv.setCom(open, 0);
iv.setCom(channel, 1);
iv.setCom(close, 2);
Scanner sc = new Scanner(System.in);
do {
System.out.print("请输入对应index的命令:");
iv.call(sc.nextInt());
} while (true);
}
}
执行Client得到:
怎么样?通过在Invoker中添加一个数组来存储多个命令来分别调用对应的命令,现在这个命令模式是不是有点像遥控器了。不仅如此,我们还可以动态的将对应的位置的命令 进行 替换,实现“按钮”与功能的动态绑定。只不过这个实现比较简陋但是已经足以表示其作用了。
3.2、实现撤销功能(undo)
在很多遥控器上都能看到撤销这个功能,点击撤销就会将上次的动作还原,这个“还原”相对来说就是与上个命令执行结果相反的操作。那么怎么做才能达到这个目的尼?很显然,这个相反的操作必须由电视机(Receiver)提供,而触发触发他则是遥控器(Invoker),而我们(Client)则需要创建这样一条支持撤销的命令(Commend)给遥控器。由此看来我必须重新处理下这个命令模式的内容,如果支持撤销,那么该命令必须提供与execute()方法相反的undo()方法,无论execute()刚做过什么,undo()都会还原。so在Commend中加入undo()方法。
修改后的Commend接口:
/**
* 命令的接口,定义命令的规则
*/
public interface Commend {
/**
* 接口函数
*/
public void exectute();
/**
* 将exectute()方法所做的事情还原
*/
public void undo();
}
Commend接口发生了变化,那么实现了该接口的具体命令也将实现该方法。对于OpenCommend来说,execute()方法是开电视,那么undo()方法则是关电视。以此类推,CloseCommend的undo()方法则是开电视,ChannelCommend的undo方法则是调到上一个频道,TV添加一个回调功能用于支持undo。在此仅贴出OpenCommend的具体修改,相信其他部分也能很随意的写出来。
修改后的OpenCommend命令:
/**
* 开机命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现
*/
public class OpenCommend implements Commend {
/**
* 持有TV的引用
*/
private TV tv;
public OpenCommend(TV tv) {
this.tv = tv;
}
/**
* 调用TV中对应的操作
*/
public void exectute() {
tv.open();
}
@Override
public void undo() {
tv.close();
}
}
接收者、命令都有了,那么现在就要让调用者(Invoker)支持undo这个功能了。那么问题来了要怎么实现?相比大家都已经知道了,对没错,就是在Invoker中用一个新的字段将记录上一次操作的命令是什么,然后在撤销时候直接执行该命令的undo方法就可以了,是不是很简单。
修改后的Invoker类:(这里的NothingCommend是Commend接口的空实现用来用其初始化操作,防止出现空指针异常,其他避免异常的操作亦可,推荐使用如下使用方法)
/**
* 调用者
*/
public class Invoker {
/**
* 持有持有多个命令接口的引用
*/
private Commend[] coms;
private Commend preCom;
/**
* 初始化Invoker
*/
public Invoker() {
// 默认可以存储三个命令
this.coms = new Commend[3];
//创建一个空的命令进行初始化 避免空指针异常
NothingCommend nCommend = new NothingCommend();
for (int i = 0; i < coms.length; i++) {
coms[i] = nCommend;
}
//刚开始也没有前一个命令,则也用命令初始化
preCom = nCommend;
}
/**
* 将命令设置到对应的位置上
*
* @param com
* @param index
*/
public void setCom(Commend com, int index) {
coms[index] = com;
}
/**
* 执行对应命令中的方法
*/
public void call(int index) {
coms[index].exectute();
preCom = coms[index];
}
/**
* 撤销上一个命令的动作
*/
public void undoCall() {
preCom.undo();
}
}
Client做出了些简单调整如下:
/**
* 客户类
*/
public class Client {
public static void main(String[] args) {
// 创建TV对象
TV tv = new TV();
// 创建一个遥控器Invoker
Invoker iv = new Invoker();
// 创建开机命令
Commend open = new OpenCommend(tv);
// 创建调频命令
Commend channel = new ChannelCommend(tv);
// 创建关机命令
Commend close = new CloseCommend(tv);
iv.setCom(open, 0);
iv.setCom(channel, 1);
iv.setCom(close, 2);
Scanner sc = new Scanner(System.in);
String com;
do {
System.out.print("请输入对应index的命令(undo为撤销操作):");
com = sc.nextLine();
if ("undo".equals(com)) {
iv.undoCall();
} else {
iv.call(Integer.parseInt(com));
}
} while (true);
}
}
执行Client后的结果如下:
是不是很简单就实现撤销操作?该例子的撤销功能还是比较简单,还以通过一个数组、队列或栈来将操作过的命令进行记录来实现多次撤销,这个实现起来也是很简单的,在此只做抛砖引玉大家可以自行实现。
3.3组合命令
现在,我们想实现打开电视后立即跳转到指定频道这个功能,用上面的命令模式实现还需要两个命令OpenCommend和ChannelCommend,然而我很懒只想动一下,那么该怎么办尼?此时组合命令就派上用场了,顾名思义,就是将多个命令组合起来使用,组合后的命令依然属于命令,所以仍然可以像其他命令一样使用。还拿上面的例子来说(呵呵,例子太简单了),我们要将OpenCommend和ChannelCommend这两个命令包装成一个命令即可。有了这个想法那么实现起来也是很简单的,我们只需要重新实现也Commend接口,该接口需要用其他一组命令来初始化,然后在execute()中顺序的执行这组命令的execute()方法,同理undo方法也是(不过undo要反过来执行,为什么?),好了是时候实现该命令了(果然懒惰是人类进步的阶梯,哈哈......)。
组合命令的实现如下:
/**
* 组合命令
*
* @author Administrator
*
*/
public class CombinatonCommend implements Commend {
/**
* 用来记录要组合执行的所以命令
*/
private Commend[] coms;
/**
* 通过构造函数对coms初始化
*
* @param coms
*/
public CombinatonCommend(Commend[] coms) {
super();
this.coms = coms;
}
/**
* 执行coms中所有命令的exectute方法
*/
@Override
public void exectute() {
for (int i = 0; i < coms.length; i++) {
coms[i].exectute();
}
}
/**
* 执行coms中所有命令的undo方法
*/
@Override
public void undo() {
for (int i = coms.length - 1; i >= 0; i--) {
coms[i].undo();
}
}
}
只需对Client进行简单的修改即可测试组合命令,此处只是将index=0处的命令改成了包含了开机与调频的组合命令,只贴出运行结果:
3.4命令模式的其他用途
命令请求队列:创建出来的命令可以立即执行,也可以延后执行,也可以不执行,更可以在其他线程中执行(需考虑线程安全的问题),这样我们可以将需要执行的命令放到一个队列当中,前面对命令进行处理,后面还可以继续添加命令。还有日志请求等。
四、命令模式的总结
命令模式的优点:
1、命令模式可以实现调用者和接受者之间的解耦。
2、命令模式很容易扩充,新增Command,并且无需改变现有的类。
3、可以组合命令
4、支持命令的撤销与重做等