两位大侠均系出名门,适配器大侠身为结构教长老,而策略大侠位居行为派护法。二侠虽然门派不同,素昧平生,却也一见如故,把酒言欢之余,心意相通,准备合奏一曲助兴。只见适配器大侠使出乾坤大挪移,无论何种乐器,或吹、或拉、或弹、或敲,不管音调如何,或轻、或重、或缓、或急,都被他一一化解为一种和音,无不处处落在策略大侠的旋律上。丝丝入扣,不差毫厘,冥冥之中自有天意,“适配器”大侠与“策略”大侠竟然合成了一首《命令模式曲》。曲终之后,二位大侠自是喜不待言,在场的各大门派的编程名家、架构大师更是赞叹不已,在击节叫好之余,都不由地暗下决心:是时候修炼《命令模式72绝技》了!于是大家纷纷打开真经,首先映入眼帘的就是它的定义和结构图。
将一个请求封装为一个对象,从而使你用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
基本结构图:
该图中包括如下一些基本的类:
- Command(命令接口类):定义命令的接口,声明执行操作的方法。
- ConcreteCommand(具体的命令类):定义具体命令的类。通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- Invoker(命令调用者):使用命令的类,该类的职责是请求某个命令对象执行待定命令,通常会持有命令,真正使用命令功能的地方。
- Receiver(命令接收者):命令接收者,知道在接收到一个命令后,如何完成具体的处理操作,是真正执行命令功能的地方。任何一个类都可以成为命令接受者。
- Client:创建具体的命令对象,设置命令的接收者,并把命令对象传递给命令调用者,它通过Invoker来触发命令的执行。
它是典型的行为型模式,任何一个对象中的任何一个方法(在这里是请求),可以说是林林总总,什么形式的方法都有,要让Invoker使用固定的形式来调用一个对象中的方法,就得找到变化并封装之,在这里,方法是变化的根源,那就用Command类来封装这个方法。
总之,Client把接收者的方法创建成一个命令对象,交给调用者,调用者调用命令来让接收者处理操作。从结构图中可以看出Invoker和Command是策略模式,在这里Invoker是Context角色,而Command是策略对象,显然Invoker和Command解耦了。那么,具体Command对象委托引用Receiver的操作方法,也被封装在了具体Command中,Invoker与Command解耦也就是Invoker与Receiver解耦,无论是处理什么样的操作,调用者都不会修改,操作与调用者解耦了。也就是说,在实际业务中Receiver提供的操作方法尽管接口形式各异,没法提供一个共同的抽象基类来实现面向接口编程(不同的Receiver提供的方法没有共同点,不可能把它们都继承在同一个基类中,因为对Receiver没有规定,任何一个类都可以用作Receiver,显然它们之间是不可能抽象出基类的),那就使用Command这个中间类来过渡:需要操作Receiver中的一个方法,就创建一个新的Command类来对应,来封装它(封装变化)。与策略模式相比,比策略模式多了一层适配包装:Command作为适配器来适配Receiver与Invoker。因此,为了便于这个模式的理解,可以把它看作是策略模式+适配器模式的组团应用,核心思想就是把一个对象的方法封装成对象。
该模式的特点:
- Receiver集中精力做好自己的功能,可以根据自己的意愿随意定义方法,其它模块使用它时,按照它的方法来调用。
- Invoker的对外调用的接口是Command,Command是一个通用的接口,面向接口编程;Command通过适配器来转换Receiver的接口方法为对象,封装变化。
- Receiver的变化只影响到Command的变化,而Command对Invoker而言,又是面向接口的,对Invoker没有任何影响,也即Invoker通过Command接口封装了Receiver的变化!
- 当Receiver扩展功能时,新建一个新Command子类即可,Invoker没有任何变化,符合对扩展开放,对修改关闭的原则。
- Command把调用者(Invoker)和接收者(Receiver)隔离开了,解耦了调用者和接收者,尽管接收者形形色色,对调用者没有任何影响。
显然命令模式也可以代替回调函数,实现IOC机制,比如Android中的Handler机制。
好,我们先看一下在开发实践中是如何使用线程的:假设MyBusiness是我们的一个业务类,有一个doSomthing()方法用来处理业务逻辑,如果想要doSomthing()异步运行,通过线程来实现,非常简单,代码片段如下:
public void client() {
MyBusiness business = new MyBusines();// 创建Receiver对象
Runnable concreteRunnable = new Runnable() {// 创建具体的Command对象
publicvoid run() {
business.doSomthing();// 在具体命令中封装Receiver的操作
}
};
Thread thread = new Thread(concreteRunnable);// 创建Invoker对象
thread.start();// 向调用者Invoker发送命令请求
}
有没有发现这就是命令模式的典型用法?Thread类就是Invoker,Runnable接口就是抽象Command类,ConcreteRunnable就是具体Command对象,而MyBusiness就是Receiver类,它里面的方法doSomthing()就是Invoker最终调用的操作。尽管MyBusiness形式各异,里面的方法也千变万化,但是它的任何方法,比如doSomthing()、doAnything()、doEverything()…都可以放在Thread中异步运行,并且Thread还不会做任何修改,怎么做到的呢?就是通过Runnable这个Command抽象接口(适配器)给封装了变化,解耦了Thread和MyBusiness,命令模式居然如此简单。我们在使用时有没有意识到这就是命令模式得场景?
再者,在Android应用开发中,也有一个常见的命令模式应用场景:使用Handler向一个HandlerThread去发送一个Message或者Runnable对象。创建和发送命令的client角色是就是使用Handler的应用程序,包含Looper对象的线程就是命令的调用者Invoker角色,Message或Runnable就是命令Command角色,而Handler就是命令的接收者Receiver角色。如果要Undo一个命令,可以调用removeCallbacks()、removeMessages()、removeCallbacksAndMessages()等方法,经典的命令模式应用场景,大家可以分析一下。
使用该模式需要注意的地方:
- 既然把对象的操作方法封装成了对象,那么扩展一个新操作就得定义一个对应的ConcreteCommand类来封装它,这会引起ConcreteCommand类的数量增多,线程中常见的方式是匿名类,也是新增的类。
- 向Invoker请求一个操作时,就得创建一个Command对象实例,调用一次就创建一个,如果请求的次数比较多,就会产生大量的一次性对象。因此,为了防止内存的剧烈抖动,可以使用对象池来复用命令对象。如在Android应用中,当向Handler发送Message对象时,可以使用Message.obtain(),从Message对象池中获取一个闲置Message对象,当使用完毕后,再把Message对象归还到Message对象池中。
- Invoker调用Receiver的操作,可以使用同步方式,也可以使用异步方式,完全由Invoker根据自己的业务逻辑决定,在Handler与线程的例子中,都是异步方式的例子。
- 由于命令Command可以储存在Invoker中,如果长时间不处理的话,它就迟迟得不到释放,而Command对象又委托引用了一个Receiver,这样Receiver对象也迟迟得不到释放,如果Receiver是重量级对象的话,对内存的占用开销也很大。比如,在Android中,如果Handler是Activity中的一个内部类的实现对象,在它的内部类中重写了handleMesage(),那么它就隐含引用了Activity对象,这样即使Activity失去生命周期了,也无法被释放回收,这在使用这种模式应用时需要注意的一点。
实现一个文件回收站的例子:当删除文件时,可以把文件存放在回收站中,这样可以在后来把文件从中恢复回来。借助于这个回收站,我们可以实现一个文件操作的undo功能,如,可以对文件的copy、create、delete、move等操作进行undo操作,既然命令模式有撤销操作的功能,就使用命令模式实现再合适不过了。
假设回收站的目录为:“/recycler/”,代码如下:
public class CommandPattern {
private final static String RECYCLER = "/recycler";
// 命令接口类
interface ICommand {
void execute();
}
// 命令调用者,负责保存命令,并执行命令
static class Invoker {
private Stack<ICommand> mStack; // undo命令要放在堆栈中,按照先进后出的顺序undo
public Invoker() {
mStack = new Stack();
}
public void backupCommand(ICommand command) { //保存Undo命令
mStack.push(command);
}
public void undo() { //执行undo操作
try {
ICommand command = mStack.pop();
command.execute();
} catch (EmptyStackException e) {
e.printStackTrace();
}
}
}
// 文件操作接口类
interface IFileOperation {
void create(String file);
void copy(String src, String dst);
void move(String src, String dst);
void delete(String file);
void undo();
}
// 一个文件操作具体类
static class FileOperation implements IFileOperation {
public void create(String file) {
System.out.println("create:"+file);
}
public void copy(String src, String dst) {
System.out.println("copy:"+src+" to "+dst);
}
public void move(String src, String dst) {
System.out.println("move:"+src+" to "+dst);
}
public void delete(String file) {
System.out.println("delete:"+file);
}
public void undo() {
}
}
// 文件操作的代理类,代理模式
static class FileOperationProxy extends FileOperation {
private FileOperation mOperation;
private Invoker mInvoker;
public FileOperationProxy(FileOperation operation) {
mOperation = operation;
mInvoker = new Invoker();
}
@Override
public void create(final String newFile) {
mOperation.create(newFile);
mInvoker.backupCommand(new ICommand() {
@Override
public void execute() {
mOperation.delete(newFile); // undo操作相当于删除文件
}
});
}
@Override
public void copy(final String file1, final String file2) {
mOperation.copy(file1, file2);
mInvoker.backupCommand(new ICommand() {
@Override
public void execute() {
mOperation.delete(file2); // undo操作相当于删除目的文件
}
});
}
@Override
public void delete(final String newFile) {
mOperation.move(newFile, RECYCLER+newFile);//删除操作相当于移动到回收站
mInvoker.backupCommand(new ICommand() {
@Override
public void execute() {
mOperation.move(RECYCLER+newFile, newFile);// undo操作相当于从回收站移动回来
}
});
}
@Override
public void move(final String file1, final String file2) {
mOperation.move(file1, file2);
mInvoker.backupCommand(new ICommand() {
@Override
public void execute() {
mOperation.move(file2, file1); //undo操作相当于反方向移动
}
});
}
@Override
public void undo() {
mInvoker.undo();
}
}
// 模拟一个文件操作场景
public static void testUndo(boolean undoFlag) {
final String newFile = "/abc/120.txt";
final String file1 = "/abc/121.txt";
final String file2 = "/abc/122.txt";
final String file3 = "/abc/123.txt";
final String file4 = "/abc/124.txt";
// 如果不需要undo功能,直接使用FileOperation,否使用FileOperationProxy
FileOperation operation = new FileOperation();
if (undoFlag) { // undo设置标记打开
operation = new FileOperationProxy(operation);
}
operation.create(newFile);
operation.copy(file1, file2);
operation.move(file3, file4);
operation.delete(file2);
operation.undo();
operation.undo();
operation.undo();
operation.undo();
}
// 测试
public static void main(String[] argv) {
// testUndo(false);
testUndo(true);
}