定义
Encapsulate a request as an object, thereby letting you parametrize clients with different requests, queue or log requests, and support undoable operations.
将请求封装成一个对象,因此可以让你用不同的请求参数化发送者,入队或记录请求,以及支持可撤销的操作。
情景
我记得,小时候如果一个家庭有一台黑白电视,那是件非常稀罕的事;
但现在,如果一个家庭没有各种品牌的家电,那反到成了稀罕的事。
家电多了也会带来幸福的烦恼——各种遥控总是在需要的时候找不到,不需要的时候天天见。
因此,你决定为自己开发一款手机版的遥控器,它只有两个按键但可以控制所有家电的"开"、"关"操作,伪代码如下:
public class RemoteControl {
public void turnOn(String deviceName){
System.out.println("deviceName:"+deviceName);
if(deviceName.equals("TV")){
tv.setVolume(11);
tv.open();
}else if(deviceName.equals("fans")){
fans.setLevel(3);
fans.start();
}
System.out.println("status:ON");
}
public void turnOff(String deviceName){...}
}
当你按下"打开"按钮时,手机会触发遥控的turnOn方法并将当前佩戴的电器名作为参数传入。
遥控除了将当前电器的名称和状态显示在手机上外,还需向对应的电器发起请求,过程中会涉及一些初始化的配置。
问题
在上面的代码中,RemoteControl是请求的发送者,tv、fans是请求的接收者,调用电器的过程如:tv.setVolume(11)和tv.open()即是一个请求。
假设,你以后想为遥控陆续增加各种接收者,那么你就需要不停的增加if...else;
再假设,你希望通过手机侧面的快捷键进行开关控制,那么"快捷键"的代码很有可能和RemoteControl非常相似。
所以,在接收者数量不固定并且相同的请求存在多个发送者时,如果发送者直接耦合接收者,那么会导致代码不易扩展以及不复用。
因此,此时应该使用更合适的方式——命令模式。
方案
命令模式将请求过程封装成了一个对象即命令(如下所示),使其可以作为参数传递给发送者,发送者收到命令后只需执行该命令而无需关心请求的细节以及接收者是谁。
public class FansTurnOnCommand implements Command{
private final Fans fans;
public FansTurnOnCommand(Fans fans){
this.fans = fans;
}
@Override
public void execute() {
fans.setLevel(3);
fans.start();
}
}
将请求命令化后,命令便切断了发送者和接收者之间的直接依赖关系。如下所示:
因此,如若要让发送者支持新的接收者,无需改动发送者只需新增一个对应的命令,将其传递给发送者就可以;如若要新增发送者,也无需直接请求接收者只需复用已有的命令,便可间接调用接收者。
命令模式不仅支持请求的灵活扩展和复用,而且还能让你对请求进行增强操作如:延迟发送、记录日志、撤销。但是,它也有缺点就是命令的数量太多,会导致难以维护。
应用
接下来,我们使用命令模式重构一下"手机遥控器",将请求封装成命令使其可以在对象之间传递。
首先,创建一个抽象的命令接口,用于统一请求标准。
public interface Command {
public void execute();
}
然后,创建具体的命令FansTurnOnCommand、TVTurnOnCommand。
public class FansTurnOnCommand implements Command {
private final Fans fans;
public FansTurnOnCommand(Fans fans){
this.fans = fans;
}
@Override
public void execute() {
fans.setLevel(3);
fans.start();
}
}
public class TVTurnOnCommand implements Command {
private final TV tv;
public TVTurnOnCommand(TV tv){
this.tv = tv;
}
@Override
public void execute() {
tv.setVolume(3);
tv.open();
}
}
现在,我们重构一下RemoteControl,将命令作为参数传给它。
public class RemoteControl {
private Command turnOnCommand;
private Command turnOffCommand;
public void turnOff(){...}
public void setTurnOffCommand(Command command){...}
public void setTurnOnCommand(Command command){
this.turnOnCommand = command;
}
public void turnOn(){
System.out.println("command:"+turnOnCommand.getClass());
turnOnCommand.execute();
System.out.println("status:ON");
}
}
最后,我们在看看客户端如何使用命令模式。
public class Client {
public static void main(String[] args) {
RemoteControl remoteControl = new RemoteControl();
//打开电视
TV tv = new TV();
TVTurnOnCommand tvTurnOnCommand = new TVTurnOnCommand(tv);
remoteControl.setTurnOnCommand(tvTurnOnCommand);
remoteControl.turnOn();
//打开风扇
Fans fans = new Fans();
FansTurnOnCommand fansTurnOnCommand = new FansTurnOnCommand(fans);
remoteControl.setTurnOnCommand(fansTurnOnCommand);
remoteControl.turnOn();
}
}
结构
抽象命令角色(Command) :声明了一个调用请求的方法,这个方法通常没有任何入参;
具体命令角色(ConcreteCommand) :封装了接收者、请求参数、请求方法等具体的请求细节,它是发送者向接收者发送请求的媒介;
发送者角色(Invoker) :它通常不负责创建命令,而是对外提供设置命令的方法,这些命令可以让它间接地向接收者发送请求;
接收者角色(Receiver) :它们实现的接口不尽相同,但可以被同一个发送者关联。
客户端(Client) :负责创建具体命令,并为发送者设置命令。
总结
当一个对象的请求过程可以被多个发送者复用,或者发送者的一个行为关联多个对象的请求过程时,通常可以将请求过程封装到一个对象中,来解藕发送者和接收者之间的直接依赖关系。
这样,可以使你复用请求过程以及插拔式地扩展请求。