一、背景
本内容是来自书籍《Head First 设计模式》的第五章,命令模式(将方法调用封装起来)
二、OO基础
- 封装
- 继承
- 多态
- 抽象
三、OO设计原则
- 封装变化,将变化的部分与不变的部分分开
- 面向接口编程,而非面向实现编程
- 组合优于继承
- 对扩展开放,对修改关闭
- 为交互对象之间的松耦合而努力
- 高组件不应该依赖低组件,他们都应该依赖其抽象
四、认识命令模式
命令模式——将请求封装成对象,这可以让你使用不同的请求、队列,或者日志请求来参数化其他对象。命令模式也可以支持撤销操作
0.命令模式解决了什么?
以一个家庭遥控器举例:现在有一个遥控器,它控制着房间里不同电器的开关,例如吊灯、卧室灯、空调、电视等,它们之间的场景有可能不同,所以提供的接口就有可能不同。
例如吊灯提供了一个lightOn()的开接口与一个lightOff()的关接口,卧室灯却提供了一个on()的开接口与一个off()的关接口,诸如此场景的其他电器很多。那么遥控器怎么设计才能提供控制不同电器开关的功能呢?
首先,给每个电器提供一个接口来控制开关是不可行的,因为此种方案会对系统的可维护性和可扩展性大打折扣。所以前辈们总结出了命令模式,它封装了内部复杂多样的接口,对外部提供一个统一的接口来进行调用。也就是说,客户在使用遥控器的时候不关心它内部是怎么执行的,客户只需要知道我按某一个按钮对应这一种电器的开或关即可。
1.遥控器的设计(开&关&撤销按钮)
下面我们就来看下遥控器是如何通过使用命令模式来实现的。
a.设计
在学习之前,我们先来看下遥控器的执行流程:
将上面的演示图转换成类图来看就是这样的:
client就相当于用户,client–>invoker相当于用户创建了一批请求对象并设置进invoker然后调用invoker里的某一个接口方法来实现某种功能。而这个接口方法内部就是调用某个Command的execute()方法也就相当于遥控器执行内部逻辑,最后具体的CommandObject再调用内部receiver的一系列action动作来完成功能。
b.实现
最后,我们来看下代码实现:
(1)、Receiver
public interface Light {
public void on();
public void off();
}
public class LivingRoomLight implements Light{
@Override
public void on() {
System.out.println("Living Room Light is On!");
}
@Override
public void off() {
System.out.println("Living Room Light is Off!");
}
}
(2)、CommandObject
public interface Command {
public void execute();
public void undo();
}
public class NoCommand implements Command{
@Override
public void execute() {
}
@Override
public void undo() {
}
}
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command{
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
(3)、Invoker
public class RemoteControl {
private Command[] onCommands;
private Command[] offCommands;
private Stack<Command> historyCommands;
public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
for (int i = 0; i < 7; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
historyCommands = new Stack<>();
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButtonWasPressed(int slot) {
onCommands[slot].execute();
historyCommands.push(onCommands[slot]);
}
public void offButtonWasPressed(int slot) {
offCommands[slot].execute();
historyCommands.push(offCommands[slot]);
}
public void undo() {
if (!historyCommands.isEmpty()){
Command command = historyCommands.pop();
if (null != command) {
command.undo();
}
}else{
System.out.println("无历史操作!");
}
}
public String toString() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("\n------Remote Control------\n");
for (int i = 0; i < onCommands.length; i++) {
stringBuffer.append("[slot " + i + "]" + onCommands[i].getClass().getName() + " " + offCommands[i].getClass().getName() + "\n");
}
return stringBuffer.toString();
}
}
(4)、Client
上面的command对象只实现了灯的开关指令,其他的电器同理,实现Command接口的execute、undo方法,调用其具体的receiver的action即可
public class ClientDemo {
public static void main(String[] args) {
RemoteControl remoteControl = new RemoteControl();
Light light = new LivingRoomLight();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
remoteControl.setCommand(0,lightOnCommand,lightOffCommand);
remoteControl.onButtonWasPressed(0);//开
remoteControl.undo();//关
remoteControl.offButtonWasPressed(0);//关闭
remoteControl.onButtonWasPressed(0);//开
remoteControl.undo();//关
remoteControl.undo();//开
remoteControl.undo();//无操作
}
}
测试结果图:
c.实现过程中的总结
可以看到上面有个NoCommand对象,它提供的空实现。在我们日常开发的时候也需要这样,当我们不想返回一个有意义的对象时,空对象就很有用。使用者可以将处理null的责任转移给空对象。
在看完上面简单实现之后,我们就可以通过设计Command对象来支持不同的功能。例如吊扇基于状态的不同去创建不同的命令对象来执行不同的命令,再例如宏命令(一堆命令的集合),创建多个命令,将这些命令放到宏命令对象中,然后将宏命令对象设置进Invoker中,当我们调用Invoker里面的这个方法时就可以执行多个命令了。
d.命令模式的拓展
命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像一般的对象一样。现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,它甚至可以在不同的线程中被调用。我们可以利用这样的特性衍生一些应用,例如:日程安排(Scheduler)、线程池、工作队列等。
再者,像某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的状态。通过新增两个方法( store()、load() ),命令模式就能够支持这一点。
五、总结
对于命令模式,书中介绍了一些要点:
- 命令模式将发出请求的对象和执行请求的对象解耦。
- 在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作。
- 调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用。
- 调用者可以接受命令当做参数,甚至在运行时动态的进行。
- 命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态。
- 宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销。
- 实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接收者。
- 命令也可以用来实现日志和事务系统。