Java设计模式-命令模式
命令(Command)模式,有时也被称为动作(Action)模式或者事务(Transaction)模式,在23种设计模式中属于对象行为型模式。
命令模式将请求封装成对象,以便使用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
模式介绍
在开发中,请求响应的场景非常常见,一般会把请求的响应操作封装到一个方法中,比如在Servlet中可以把这种操作放在doGet()或者doPost()方法中,但是这种实现请求响应的操作并不是命令模式。
命令模式是要解决这样一类问题的,某些对象需要发送请求(调用其中的某个或者某些方法),但是并不知道请求的执行者是谁,也不知道被请求的操作的是哪个,如何可以将请求的发送与请求的执行者以一种低耦合的方式设计,让对象的调用关系更加灵活,命令模式提供了一种比较完美的解决方式。
一般命令模式的请求响应会涉及三个操作类,这些类都是封装对象行为的类,而非简单的属性类(比方说JavaBean、Domain或者Model)。请求类会将自己的行为通知到命令类中,然后命令类将具体的命令转发到请求的执行类中,最后由执行类完成本次的调用过程。
适用性
- 系统需要将发出请求的对象和执行请求的对象进行解耦时,调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要将一组操作组合在一起,即支持宏命令。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 可以作为面向对象回调机制Callback的一个替代。
示例介绍一
这里以一个点灯开关的示例做介绍,实现一个打开点灯的命令。先抽象一个包含共有方法的命令接口。
public interface Command { /** * 执行方法 */ void execute(); }
然后实现一个LightOnCommand的命令。
public class LightOnCommand implements Command { private Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void execute() { light.lightOn(); } }
假设有这样一个开关,它可以控制电灯的开启操作。
public class SimpleRemoteControl { private Command command; public SimpleRemoteControl(Command command) { this.command = command; } public void buttonWasPressed() { command.execute(); } } public static void main(String[] args) { // 创建接收者 Light light = new Light(); // 创建命令对象,设定其接收者 Command command = new LightOnCommand(light); // 创建请求者,把命令对象设置进去 SimpleRemoteControl controll = new SimpleRemoteControl(command); // 执行方法 controll.buttonWasPressed(); } //Light is on
示例介绍二
将上面的示例抽象为命令模式的一般模式。
// 电灯对象 public class Receiver { /** * 真正执行命令请求的操作 */ public void action() { System.out.println("执行操作"); } } // LightOnCommand命令对象 public class ConcreteCommand implements Command { /** * 持有相应的接收者对象 */ private Receiver receiver; /** * 构造方法 * * @param receiver */ public ConcreteCommand(Receiver receiver) { this.receiver = receiver; } @Override public void execute() { // 通常会转调接收者的形影方法,让接收者来真正执行功能 receiver.action(); } } // 电源开关SimpleRemoteControl public class Invoker { /** * 持有命令对象 */ private Command command; /** * 构造方法 * * @param command */ public Invoker(Command command) { this.command = command; } /** * 行动方法 */ public void action() { command.execute(); } } public static void main(String[] args) { // 创建接收者 Receiver receiver = new Receiver(); // 创建命令对象,设定其接收者 Command command = new ConcreteCommand(receiver); // 创建请求者,把命令对象设置进去 Invoker invoker = new Invoker(command); // 执行方法 invoker.action(); }
其它示例
在多名命令或者说宏命令的场景中,在请求操作中持有的命令对象有可能会为null,那么每一个操作之前都需要对命令对象进行判空,代码如下:
public void onButtonWasPushed(int slot) { if (onCommands[slot] != null) { onCommands[slot].execute(); } }
其实上面这种代码实现方式在平常开发中很常见,为了避免每一个请求者都对所持有的命令对象进行判空操作,一般开发中会提供一个什么都不执行的默认空命令对象。
public class NoCommand implements Command { @Override public void execute() { System.out.println("no action"); } } public RemoteControl() { onCommands = new Command[2]; offCommands = new Command[2]; Command noCommand = new NoCommand(); for (int i = 0; i < 2; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } }
NoCommand就是一个空对象,当不想返回任何操作,或者想提供默认操作时,这种空对象的实现方式就很有用。在许多设计模式中,都可以看到空对象的使用,甚至有些时候,空对象本身就被看做是一种设计模式。
模式分析
命令模式的一般类图如下:
- Command:抽象命令,一般设计成接口的形式,顶层执行操作的接口。
- ConcreteCommand:实现execute()方法,负责调用接收者的相应操作。
- Invoker:负责调用命令对象执行请求操作。
- Receiver:负责具体的执行请求的类。
- Client:创建一个具体的命令对象ConcreteCommand,并确定其的接收者。
命令模式的执行请求流程一般为:Invoker->Command->Receiver。
命令模式中,一般请求对象(Invoker)持有一个Command对象引用,Command持有一个Receiver对象的引用,执行对象Receiver并不会持有Command或者Invoker。
命令模式通过增加了一个中间层(Command),将请求的对象(Invoker)和执行对象(Receiver)进行了解耦。被解耦的两者之间是通过命令对象进行通信的。命令对象封装了接收者的一个或者一组动作。
// 一个动作 开灯 public void execute() { light.lightOn(); } // 一组动作 音响播放CD public void execute() { stereo.on(); stereo.setDVD(); stereo.setVolume(11); }命令模式中Command封装的是操作,即动作方法,而不是属性字段。一般来说一个命令对象只对应一个接收者。
命令模式中接收者(Receiver)不是必需的,即直接使用命令完成请求执行的的操作。只是这样实现的解耦程度不如使用接收者的结构。
命令的调用者(Invoker)通过调用命令对象的execute()方法发出请求,最终会使接收者的动作被调用。
命令的调用者可以通过接收命令当做参数,甚至可以在运行时动态进行。
命令可以支持撤销操作,做法是实现一个undo()方法来回到execute()被执行之前的状态,这里的undo操作是在命令对象中操作的,并不是在接受者中实现的。
命令模式支持宏命令,宏命令时命令的一种延伸,运行调用多个方法。宏方法也支持撤销。
模式优缺点
模式优点
命令模式将发出请求的对象与执行请求的对象进行了解耦。
可以新增Command类,而无需更改已有的类。
可以将多个命令组合成一个复合命令,如命令队列或者宏命令。
可以很方便地对请求执行取消操作。
模式缺点
命令模式可能导致系统会有许多的命令类。由于每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
与其它模式对比
命令模式通过命令对象将请求者与执行者绑定在了一起,如下图所示:
从Command模式的时序图中也可以很明显看出,Command是作为一个中间者的身份,请求和响应并不是直接相互调用,而是通过Command建立联系。
在《重构改善既有代码的设计》一书中有这样一句描述。
计算机科学是这样一门科学:它相信所有的问题都可以通过一个间接层来解决。
在很多代码重构中都会引入一个间接层,其实间接层的引入就是为了降低请求与相应之间的耦合。
在Command设计模式中,Command提供的是一个提交请求的接口。
在观察者模式中时序图如下:
观察者和被观察者其实是直接相互关联调用的,并且一个观察者可以同时关联多个观察者。
在观察者模式中,接口是为了处理被观察者的变化而设计的,耦合性相比命令模式也更高一些。
小结
在命令模式中也充分体现了面向对象的设计模式的通病,类的膨胀,大量衍生类的创建,这是一个不可避免的问题。但是,它带来的好处也是显而易见的,更低的耦合性、更灵活的控制性以及更好的扩展性。
在学习设计模式时,其实见到的例子都是很简单的,基本就是为了介绍某种设计模式而想出来的,而实际工作的的场景都是相当复杂的,有时候可能还是需要兼顾老代码的实现。所以平常在开发时,某些设计模式很难与书上的例子匹配开来,这也是为什么找不出应该使用哪种设计模式的原因。
最后了,一句话,不能为了学习设计模式而学习设计模式。学习设计模式不仅是为了更好的理解别人代码的实现意图,更多的也是让自己的代码更容易被别人理解。
示例源代码下载 提取码:hfng