文章目录
- 本节用例
- 餐厅类比
- 点餐流程
- 角色与职责
- 从餐厅到命令模式
- 命令模式
- 第一个命令对象
- 实现命令接口
- 实现一个命令
- 使用命令对象
- NoCommand与空对象
- 定义命令模式
- 支持撤销功能
- 使用状态实现撤销
- 多层次撤销
- One One One …… more things
- 宏命令
- 使用宏命令
- 队列请求
- 日志请求
- 总结
《Head First设计模式》读书笔记
相关代码:Vks-Feng/HeadFirstDesignPatternNotes: Head First设计模式读书笔记及相关代码
将封装带到一个全新的境界:把方法调用(method invoke)封装起来
本节用例
设计一个家电自动化遥控器的API。
- 遥控器有7个可编程的插槽
- 每个插槽都有对应的开关按钮
- 具备一个整体的撤销按钮
希望创建一组控制遥控器的API,让每个插槽都能控制一个或一组装置。且需要能够控制目前的装置和任何未来可能出现的装置
家电设计:种类众多,接口不一,以后还会有更多厂商,每个类还会有其他各种方法
所以分离的关注点:遥控器应该知道如何解读按钮被按下的动作,然后发出正确的请求,但是遥控器不需要知道这些家电自动化的细节
命令模式将“动作的请求者”从“动作的执行者”对象中解耦
- 此例中:遥控器是请求者、厂商类是执行者
采用“命令对象”,把请求封装成一个特定对象,请求发出时就可以让命令对象做相关工作
- 此例中:每个按钮都存储一个命令对象,当按钮按下时,命令对象做相关的工作。遥控器不需要知道工作内容是什么,只要命令对象能和正确的对象沟通并完成任务即可。实现了遥控器和具体家电的解耦
餐厅类比
点餐流程
- 顾客将订单交给招待员
createOrder()
- 招待员拿走订单
takeOrder()
,将订单传递给订单柜台并发出通知orderUp()
- 厨师根据订单备餐
cook()
角色与职责
订单:封装了准备餐点的请求
- 订单可以被传递
- 订单只包含一个方法
orderUp()
,封装了备餐动作 - 订单内有一个到“需要进行准备工作的对象”的引用(即厨师)
招待:接受订单,调用订单的orderUp()
方法 - 招待接收不同用户的不同订单,其
takeOrder()
被传入不同参数 - 招待知道订单包含
orderUp()
方法,在需要备餐时调用即可 - 招待无需知道订单内容、谁来备餐,只需知道并调用
orderUp()
厨师:具备准备餐点的只是 - 真正知道如何备餐
- 当
orderUp()
被调用时,厨师接手,实现需要创建餐点的所有方法 - 厨师与招待彻底解耦
从餐厅到命令模式
- 客户创建一个命令对象
- 客户利用
setCommand()
将命令对象存储在调用者中 - 客户要求调用者执行命令
- 客户(Client)负责创建命令对象
createCommandObject()
- 命令对象包含了接收者上的一组动作,它提供了一个
execute()
方法,封装了这些动作。- 动作和接收者在命令对象中被绑在一起
receiver.action()
- 调用
execute()
会调用接收者的这些动作
- 动作和接收者在命令对象中被绑在一起
- 客户在调用者对象(Invoker)上调用
setCommand()
方法,并把它传入命令对象。命令对象被存储其中并在之后被使用 - 某个时刻,调用者将调用命令对象
命令模式
第一个命令对象
实现命令接口
public interface Command {public void execute();
}
实现一个命令
以打开电灯命令为例
public class Light { public void on() { System.out.println("Light is On"); }
}
public class LightOnCommand implements Command{ Light light; public LightOnCommand(Light light) { this.light = light; } public void execute() { light.on(); }
}
使用命令对象
public class SimpleRemoteControl { Command slot; public SimpleRemoteControl() { } public void setCommand(Command command) { slot = command; } public void buttonWasPressed() { slot.execute(); }
}
NoCommand与空对象
NoCommand对象是一个空对象(null object)的例子。
- 当你不想返回一个有意义的对象时,空对象就很有用
- 客户可以将处理null的责任转移给空对象
- 本例中:遥控器不可能出场时就设置了有意义的命令对象,所以提供了NoCommand对象作为代用品,当调用execute方法时,这种对象什么事情都不做
许多设计模式中都会看到空对象,甚至有时空对象也被视为一种设计模式
定义命令模式
#HeadFirst设计模式7-命令模式
命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
一个命令对象通过在特定接收者上绑定一组动作来封装一个请求
- 命令对象将动作和接收者包进对象中。这个对象只暴露出一个
execute()
方法,当此方法被调用时,接收者会进行这些动作, - 从外面看,其他对象不知道接收者做了哪些具体动作,只知道调用
execute()
就能达成目标
Q:接收者一定有必要存在吗?为何命令对象不直接实现execute()方法的细节?
A:一般来说,我们尽量设计“傻瓜”命令对象。它只懂得调用一个接收者的一个行为。然而,有许多“聪明”命令对象会实现许多逻辑,直接完成一个请求。但是“聪明”命令对象的调用者和接收者之间的解耦程度不如“傻瓜”命令对象,而且你不能把接收者当成参数传给命名
支持撤销功能
- 当命令支持撤销时,该命令就必须提供和
execute()
方法相反的undo()
方法。- 不管
execute()
刚才做什么,undo()
都会倒转过来
- 不管
public interface Command {public void execute();public void undo();
}
- 以
LightOnCommand
为例,如果LightOnCommand
的execute()
方法被调用,那么最后被调用的是on()
方法,所以undo()
需要执行的即为相反的off()
public class LightOnCommand implements Command { private Light light; public LightOnCommand(Light light) { this.light = light; } public void execute() { light.on(); } public void undo() { light.off(); }
}
- 遥控器类中加入一个新的实例变量用来追踪最后被调用的命令。这样当撤销按钮被按下后,我们都可以取出这个命令并调用它的
undo()
方法
package remotecontrol; import command.Command;
import command.NoCommand; public class RemoteControlWithUndo { Command[] onCommands; Command[] offCommands; Command undoCommand; public RemoteControlWithUndo() { onCommands = new Command[7]; offCommands = new Command[7]; Command noCommand = new NoCommand(); for (int i = 0; i < 7; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } undoCommand = noCommand; } public void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } public void onButtonWasPushed(int slot) { onCommands[slot].execute(); undoCommand = onCommands[slot]; } public void offButtonWasPushed(int slot) { offCommands[slot].execute(); undoCommand = offCommands[slot]; } public void undoButtonWasPushed() { undoCommand.undo(); } public String toString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("\n------ Remote Control ------\n"); for (int i = 0; i < onCommands.length; i++) { stringBuilder.append("[slot " + i + "]" + onCommands[i].getClass().getName() + " " + offCommands[i].getClass().getName() + "\n"); } return stringBuilder.toString(); }
}
使用状态实现撤销
例如,风扇具有多种转速,当撤销时,我们就需要考虑如何恢复它的上一个转速。
方法:加入prevSpeed
变量对历史状态进行记录
多层次撤销
使用一个堆栈记录操作过程的每一个命令
当撤销时,从堆栈中取出最上层的命令,然后调用其undo()
方法
One One One …… more things
宏命令
按下一个按钮,同时完成多个任务
制造一个新的命令,用来执行其他一堆命令
public class MacroCommand implements Command{Command[] commands; public MacroCommand(Command[] commands) { this.commands = commands; } public void execute() { for (int i = 0; i < commands.length; i++) { commands[i].execute();; } }
}
Q:为什么不创建一个PartyCommand()
,在其中调用其他的命令
A:这相当于把Party模式“硬编码”到PartyCommand中。而利用宏命令,你可以动态地决定PartyCommand是由哪些命令组成,所以宏命令在使用上更灵活。一般来说,宏命令的做法更优雅,也需要较少的新代码。
使用宏命令
- 创建想要进入宏的命令集合
- 创建两个数组,其中一个用来记录开启命令,另一个用来记录关闭命令,并在数组内放入对应命令
- 将宏命令指定给我们希望的按钮
队列请求
命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。
现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,他甚至可以在不同的线程中被调用。
利用这样的特性可以衍生出一些应用:日程安排(Scheduler)、线程池、工作队列等
想象有一个工作队列:一端添加命令,另一端则是线程
- 线程进行下面的动作,从队列中取出一个命令,调用它的
execute()
方法,调用完成后将命令对象丢弃,再取出下一个命令
注意:工作队列类和进行计算的对象之间完全是解耦的。此刻线程可能在进行财务运算,下一刻却在读取网络数据。工作队列对象不在乎到底做些生命,它们只知道取出命令对象,然后调用execute()
方法。
类似的,只要是实现命令模式的对象,就可以放入队列里,当线程可用时就调用此对象的execute()
方法。
日志请求
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的装填。
命令模式能够支持这一点:新增两个方法store()
和load()
- java中,利用对象的序列化(Serialization)实现这些方法,但是一般认为序列化最好还是只用在对象的持久化上(persistence)
执行命令时,将历史记录存储在磁盘中,一旦系统司机,就可以将命令对象重新加载,并成批次地依次调用这些对象的execute()
方法
许多大型数据结构的动作的应用无法在每次改变发生时被快速地存储。通过使用记录日志,我们可以将上次检查点(checkpoint)之后的所有操作记录下来,如果系统出状况,从检查点开始应用这些操作
对更高级的应用而言,这些技巧可以被扩展应用到事务(transaction)处理中。
总结
OO基础
- 抽象
- 封装
- 多态
- 继承
OO原则
- 封装变化
- 多用组合,少用继承
- 针对接口编程,不针对实现编程
- 为交互对象之间的松耦合设计而努力
- 对扩展开放,对修改关闭
- 依赖抽象,不要依赖具体类
OO模式
- 命令模式——将请求封装成对象,这可以让你使用不同的请求、队列或者日志请求来参数化其他对象。命令模式也可以支持撤销操作。
要点:
- 命令模式将发出请求的对象和执行请求的对象解耦
- 被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作
- 调用者通过调用命令对象的
execute()
发出请求,这会使得接收者的动作被调用 - 调用者可以接受命令当做参数,甚至在运行时动态地进行
- 命令可以支持撤销,做法是实现一个
undo()
方法来回到execute()
被执行前的状态 - 宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销
- 实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接收者
- 命令也可以用来实现日志和事务系统