命令模式
万能遥控板
前方售前团队带来了好消息,拿到了巴斯特家电自动化公司的业务。
需求是这样的,这家公司正在设计一款遥控板,和普通的遥控板不同,它提供了若干按键,可以通过编程控制不同的家电,但这些家电是由不同的厂商开发出来的,比如小米的电视,格力的空调等,他们提供了不同的API。同时,按键可以扩展以便支持未来可能合作的家电。
V1版
我们一步一步来逐步分析实现。先实现第一版,如果要让遥控板控制电视开关,我们怎么处理?
先把电视类实现出来(这个类由厂商提供)
/**
* 电视
*/
public class TelevisionV1 {
public void on(){
System.out.println("打开电视....");
System.out.println("正在放映<<哪吒>>");
}
public void off() {
System.out.println("关闭电视....");
}
}
遥控板实现
/**
* V1版遥控板
*/
public class TVRemoteControlV1 {
TelevisionV1 television;
public TVRemoteControlV1() {
television = new TelevisionV1();
}
public void powerOn(){
System.out.println("按下遥控板的电源打开按钮....");
television.on();
}
public void powerOff(){
System.out.println("按下遥控板的电源关闭按钮....");
television.off();
}
}
测试
/**
* 测试
*/
public class RemoteControlMainV1 {
public static void main(String[] args) {
TVRemoteControlV1 remoteControl = new TVRemoteControlV1();
remoteControl.powerOn();
System.out.println();
remoteControl.powerOff();
}
}
恩,通过这种简单自然的方式,遥控板类中组合电视,就可以使用遥控板来控制电视了。
V2版
接下来,我们让我们的遥控板既能控制电视,又可以控制空调。按照V1一样的思路,实现V2版
/**
* 电视-小米
*/
public class TelevisionV2 {
public void on(){
System.out.println("打开电视....");
System.out.println("正在放映<<哪吒>>");
}
public void off() {
System.out.println("关闭电视....");
}
}
/**
* 空调-格力
*/
public class AirConditionerV2 {
public void turnOn(){
System.out.println("打开空调....");
System.out.println("吹出26度的舒适冷风....");
}
public void turnOff() {
System.out.println("关闭空调....");
}
}
V2版本的遥控板实现
/**
* V2版遥控板
*/
public class RemoteControlV2 {
TelevisionV2 television;
AirConditionerV2 airConditioner;
public RemoteControlV2() {
television = new TelevisionV2();
airConditioner = new AirConditionerV2();
}
public void tvPowerOn(){
System.out.println("按下遥控板的电视电源打开按钮....");
television.on();
}
public void tvPowerOff(){
System.out.println("按下遥控板的电视电源关闭按钮....");
television.off();
}
public void airConditionerPowerOn(){
System.out.println("按下遥控板的空调电源打开按钮....");
airConditioner.turnOn();
}
public void airConditionerPowerOff(){
System.out.println("按下遥控板的空调电源关闭按钮....");
airConditioner.turnOff();
}
}
测试
/**
* 测试
*/
public class RemoteControlMainV2 {
public static void main(String[] args) {
RemoteControlV2 remoteControl = new RemoteControlV2();
remoteControl.tvPowerOn();
System.out.println();
remoteControl.airConditionerPowerOn();
System.out.println();
remoteControl.tvPowerOff();
System.out.println();
remoteControl.airConditionerPowerOff();
}
}
好,V2版遥控板通过组合多个厂商的家电,调用厂商家电类的API,初步实现了同一个遥控板控制不同家电的功能。
不过,分析上面的实现,我们不难发现,遥控板和具体的厂商家电类是紧耦合的,换言之,厂商家电类API发生变化,遥控板要跟着变化;
另外,要新增控制的家电时,也要修改遥控板类。
这里,我们称遥控板为发送请求者,厂商家电类为执行请求者,前两个版本的实现中,发送请求者和执行请求者耦合在一起,有没有一种方法,将它们解耦,这样一来,厂商家电类API发生变化不会影响遥控器类,而增加控制的家电时,也仅仅是通过扩展类的方式来实现?
V3版
带着这种思路,我们在请求者和执行者中间加了一层,称这一层为指令,即遥控器只管发送指令,而不用关心指令是被谁,以什么样的方式执行的。
来看看V3版本的代码
首先厂商家电类,没变化,复用V2的
/**
* 电视-小米
*/
public class TelevisionV3 {
public void on(){
System.out.println("打开电视....");
System.out.println("正在放映<<哪吒>>");
}
public void off() {
System.out.println("关闭电视....");
}
}
/**
* 空调-格力
*/
public class AirConditionerV3 {
public void turnOn(){
System.out.println("打开空调....");
System.out.println("吹出26度的舒适冷风....");
}
public void turnOff() {
System.out.println("关闭空调....");
}
}
接下来,我们抽象出指令接口
/**
* 遥控指令
*/
public interface ControlCommandV3 {
void execute();
}
然后,针对遥控器上的每个按钮操作,实现具体的指令
/**
* 打开电视指令
*/
public class TVPowerOnCommandV3 implements ControlCommandV3 {
private TelevisionV3 television;
public TVPowerOnCommandV3(TelevisionV3 television) {
this.television = television;
}
@Override
public void execute() {
System.out.println("执行【打开电视指令】...");
television.on();
}
}
/**
* 关闭电视指令
*/
public class TVPowerOffCommandV3 implements ControlCommandV3 {
private TelevisionV3 television;
public TVPowerOffCommandV3(TelevisionV3 television) {
this.television = television;
}
@Override
public void execute() {
System.out.println("执行【关闭电视指令】...");
television.off();
}
}
/**
* 打开空调指令
*/
public class AirConditionerPowerOnCommandV3 implements ControlCommandV3 {
private AirConditionerV3 airConditionerV3;
public AirConditionerPowerOnCommandV3(AirConditionerV3 airConditionerV3) {
this.airConditionerV3 = airConditionerV3;
}
@Override
public void execute() {
System.out.println("执行【打开空调指令】...");
airConditionerV3.turnOn();
}
}
/**
* 关闭空调指令
*/
public class AirConditionerPowerOffCommandV3 implements ControlCommandV3 {
private AirConditionerV3 airConditionerV3;
public AirConditionerPowerOffCommandV3(AirConditionerV3 airConditionerV3) {
this.airConditionerV3 = airConditionerV3;
}
@Override
public void execute() {
System.out.println("执行【关闭空调指令】...");
airConditionerV3.turnOff();
}
}
然后,我们在遥控器中,将这些指令初始化并存储起来
/**
* V3版遥控板
*/
public class RemoteControlV3 {
private TVPowerOnCommandV3 tvPowerOnCommand;
private TVPowerOffCommandV3 tvPowerOffCommand;
private AirConditionerPowerOnCommandV3 airConditionerPowerOnCommand;
private AirConditionerPowerOffCommandV3 airConditionerPowerOffCommand;
public RemoteControlV3() {
TelevisionV3 televisionV3 = new TelevisionV3();
tvPowerOnCommand = new TVPowerOnCommandV3(televisionV3);
tvPowerOffCommand = new TVPowerOffCommandV3(televisionV3);
AirConditionerV3 airConditionerV3 = new AirConditionerV3();
airConditionerPowerOnCommand = new AirConditionerPowerOnCommandV3(airConditionerV3);
airConditionerPowerOffCommand = new AirConditionerPowerOffCommandV3(airConditionerV3);
}
public void tvPowerOn(){
System.out.println("按下遥控板的电视电源打开按钮....");
tvPowerOnCommand.execute();
}
public void tvPowerOff(){
System.out.println("按下遥控板的电视电源关闭按钮....");
tvPowerOffCommand.execute();
}
public void airConditionerPowerOn(){
System.out.println("按下遥控板的空调电源打开按钮....");
airConditionerPowerOnCommand.execute();
}
public void airConditionerPowerOff(){
System.out.println("按下遥控板的空调电源关闭按钮....");
airConditionerPowerOffCommand.execute();
}
}
测试
/**
* 测试
*/
public class RemoteControlMainV3 {
public static void main(String[] args) {
RemoteControlV3 remoteControl = new RemoteControlV3();
remoteControl.tvPowerOn();
System.out.println();
remoteControl.airConditionerPowerOn();
System.out.println();
remoteControl.tvPowerOff();
System.out.println();
remoteControl.airConditionerPowerOff();
}
}
在这一版中,通过指令类解耦了遥控板类和厂商类的调用,当厂商类的API发生变更时,只需要修改对应的指令类,而不用修改遥控器类了。
不过,这一版遥控器和具体厂商类还是有耦合,那么新加入控制的厂商类时,还是要修改。
原来我们讲过,要分离和封装变化,V3班的遥控板类中,容易扩展和变化的就是存储的指令类了,那么有什么方法解决。
V4版
我们来看V4版的实现,厂商类同前一样。忽略
指令类和V3版没有本质区别,添加了描述方法,便于测试时观察。
/**
* 遥控指令
*/
public interface ControlCommandV4 {
void execute();
String desc();
}
/**
* 打开电视指令
*/
public class TVPowerOnCommandV4 implements ControlCommandV4 {
private TelevisionV4 television;
public TVPowerOnCommandV4(TelevisionV4 television) {
this.television = television;
}
@Override
public void execute() {
System.out.println("执行【打开电视指令】...");
television.on();
}
@Override
public String desc() {
return "打开电视指令";
}
}
其他3个指令实现类似。
关键我们来看遥控板类,它是如何将变化的东西刨出去的
/**
* V4版遥控板 - 4个按键的遥控板
*/
public class RemoteControlV4 {
//4个按钮
private ControlCommandV4[] commands = new ControlCommandV4[4];
public void setCommand(int index, ControlCommandV4 command){
System.out.println("设置遥控板的第"+index+"个按钮,功能是"+command.desc());
commands[index] = command;
}
public void pressDownButton(int index){
System.out.println("按下遥控板的第"+index+"个按钮....");
System.out.println("执行的功能是"+commands[index].desc());
commands[index].execute();
}
}
这一版的遥控板,只和抽象指令耦合,所以厂商类API变化,以及扩展控制新的家电都不用在修改。
不过,V4版本的遥控板使用时需要注意,它要先配置相应的指令才能使用,比如
/**
* 测试
*/
public class RemoteControlMainV4 {
public static void main(String[] args) {
TelevisionV4 television = new TelevisionV4();
AirConditionerV4 airConditioner = new AirConditionerV4();
RemoteControlV4 remoteControl = new RemoteControlV4();
remoteControl.setCommand(0, new TVPowerOnCommandV4(television));
remoteControl.setCommand(1, new TVPowerOffCommandV4(television));
remoteControl.setCommand(2, new AirConditionerPowerOnCommandV4(airConditioner));
remoteControl.setCommand(3, new AirConditionerPowerOffCommandV4(airConditioner));
System.out.println();
remoteControl.pressDownButton(0);
System.out.println();
remoteControl.pressDownButton(1);
System.out.println();
remoteControl.pressDownButton(2);
System.out.println();
remoteControl.pressDownButton(3);
}
}
当然,客户不会直接使用裸遥控板的,出厂前会预植入每个按键相应的指令。不过,如果遥控板够高级的话,用户拿到遥控板后,可以自定义设置按键的指令。
扩展技巧,当我们希望按下按键时,什么都不做,我们可以使用空指令对象来初始化遥控板,从而避免空检查。
/**
* 空指令-什么都不做
*/
public class NoneCommandV4 implements ControlCommandV4 {
@Override
public void execute() {
}
@Override
public String desc() {
return "空指令";
}
}
很好,反过来看之前的需求,我们发现V4版的设计完美的满足了需求,大吉大利,今晚吃鸡。
感受下UML
好了,到这里,你可能也明白了啥是命令模式了,V4版就是利用命令模式将发送请求者和执行请求者解耦的。
定义
可以看出,命令模式中核心的就是有个叫命令的东西(我们的例子叫指令,都是一个意思),它们有统一的接口,
这样就可以统一方式调用命令;同时命令封装了具体的执行者,将请求转发到具体执行者上执行。
我们使用代码,来说明下命令模式
首先,来一个接受者,真正执行请求
/**
* 接受者-也就是执行请求者
*/
public class Receiver {
public void doSpecial(){
System.out.println("接受者,实际执行请求....");
}
}
接下来,定义命令接口
/**
* 命令接口
*/
public interface Command {
void execute();
}
实现具体的命令,也就是包装接受者
/**
* 具体的命令
*/
public class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.doSpecial();
}
}
有一个调用者,用它来调用命令
/**
* 调用者-发起请求者
*/
public class Invoker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void doSomething(){
command.execute();
}
}
测试走一波
/**
* 客户
*/
public class CommandMain {
public static void main(String[] args) {
Invoker invoker = new Invoker();
invoker.setCommand(new ConcreteCommand(new Receiver()));
invoker.doSomething();
invoker.setCommand(() -> {
System.out.println("直接在命令里面就把事情做了...");
});
invoker.doSomething();
}
}
把UML图拉出来看看
如果大家写过回调的话,可以感觉到invoker.setCommand()相当于注册回调,而调用invoker.doSomething()相当于触发回调。
撤销
正在吃鸡的过程中,客户告诉你说,他们对遥控板做了升级,加了一个撤销按钮,按下它可以撤销上一步的操作。
因为在V4版中使用的命名模式,那么实现撤销的功能就轻松了,来看看V5实现
厂商类一样,忽略。
指令接口中,加入撤销方法
/**
* 遥控指令
*/
public interface ControlCommandV5 {
void execute();//执行
String desc();//描述
void undo(); //撤销
}
接下来,每个指令都实现撤销方法。对于我们当前的指令来说,撤销的实现一点不复杂,做反操作即可
/**
* 打开电视指令
*/
public class TVPowerOnCommandV5 implements ControlCommandV5 {
private TelevisionV5 television;
public TVPowerOnCommandV5(TelevisionV5 television) {
this.television = television;
}
@Override
public void execute() {
System.out.println("执行【打开电视指令】...");
television.on();
}
@Override
public String desc() {
return "打开电视指令";
}
@Override
public void undo() {
television.off();
}
}
/**
* 关闭电视指令
*/
public class TVPowerOffCommandV5 implements ControlCommandV5 {
private TelevisionV5 television;
public TVPowerOffCommandV5(TelevisionV5 television) {
this.television = television;
}
@Override
public void execute() {
System.out.println("执行【关闭电视指令】...");
television.off();
}
@Override
public String desc() {
return "关闭电视指令";
}
@Override
public void undo() {
television.on();
}
}
/**
* 打开空调指令
*/
public class AirConditionerPowerOnCommandV5 implements ControlCommandV5 {
private AirConditionerV5 airConditionerV4;
public AirConditionerPowerOnCommandV5(AirConditionerV5 airConditionerV4) {
this.airConditionerV4 = airConditionerV4;
}
@Override
public void execute() {
System.out.println("执行【打开空调指令】...");
airConditionerV4.turnOn();
}
@Override
public String desc() {
return "打开空调指令";
}
@Override
public void undo() {
airConditionerV4.turnOff();
}
}
/**
* 关闭空调指令
*/
public class AirConditionerPowerOffCommandV5 implements ControlCommandV5 {
private AirConditionerV5 airConditionerV4;
public AirConditionerPowerOffCommandV5(AirConditionerV5 airConditionerV4) {
this.airConditionerV4 = airConditionerV4;
}
@Override
public void execute() {
System.out.println("执行【关闭空调指令】...");
airConditionerV4.turnOff();
}
@Override
public String desc() {
return "关闭空调指令";
}
@Override
public void undo() {
airConditionerV4.turnOn();
}
}
现在,我们来实现新版遥控版上的撤销按钮代码,仅仅需要加一个属性来记录上次操作的指令,当按下撤销按钮时,调用上次命令的撤销方法即可。
/**
* V5版遥控板 - 4个功能按键和1个撤销按键遥控板
*/
public class RemoteControlV5 {
//4个按钮
private ControlCommandV5[] commands = new ControlCommandV5[4];
//撤销按钮
private ControlCommandV5 undoCommand;
public void setCommand(int index, ControlCommandV5 command){
System.out.println("设置遥控板的第"+index+"个按钮,功能是"+command.desc());
commands[index] = command;
}
public void pressDownButton(int index){
System.out.println("按下遥控板的第"+index+"个按钮....");
System.out.println("执行的功能是"+commands[index].desc());
commands[index].execute();
undoCommand = commands[index];
}
public void pressDownUndoButton(){
System.out.println("按下遥控板的撤销按钮....");
undoCommand.undo();
}
}
测试
/**
* 测试
*/
public class RemoteControlMainV5 {
public static void main(String[] args) {
TelevisionV5 television = new TelevisionV5();
AirConditionerV5 airConditioner = new AirConditionerV5();
RemoteControlV5 remoteControl = new RemoteControlV5();
remoteControl.setCommand(0, new TVPowerOnCommandV5(television));
remoteControl.setCommand(1, new TVPowerOffCommandV5(television));
remoteControl.setCommand(2, new AirConditionerPowerOnCommandV5(airConditioner));
remoteControl.setCommand(3, new AirConditionerPowerOffCommandV5(airConditioner));
System.out.println();
remoteControl.pressDownButton(0);
System.out.println();
remoteControl.pressDownButton(1);
System.out.println();
remoteControl.pressDownUndoButton();
System.out.println();
remoteControl.pressDownButton(2);
System.out.println();
remoteControl.pressDownButton(3);
System.out.println();
remoteControl.pressDownUndoButton();
}
}
不要太完美,你将代码修改完后,还能接着吃鸡,在老板眼中,这就是人才啊。
宏命令
过了不久,有来新的需求,最新的遥控版上要添加一种新的模式,即按某一个按钮,可以同时控制多个家电。
比如,你下班回到家,只需要按一次按钮,就能打开空调和电视,这又要如何实现?
其实这时候只需要创建一个新的命令实现,它来组合要控制的其他命令即可,命令模式中称这种命令为 宏命令。
这儿,我实现下宏命令,剩下的由你来试试。
/**
* 宏命令,组合其他命令
*/
public class MacroCommandV6 implements ControlCommandV5{
private List<ControlCommandV5> commandV5s;
public MacroCommandV6(List<ControlCommandV5> commandV5s) {
this.commandV5s = commandV5s;
}
@Override
public void execute() {
commandV5s.forEach(command -> command.execute());
}
@Override
public String desc() {
return commandV5s.stream().map(controlCommandV5 -> controlCommandV5.desc())
.reduce((s1, s2) -> s1 + "," + s2).get();
}
@Override
public void undo() {
commandV5s.forEach(command -> command.undo());
}
}
扩展示例
示例来自https://www.journaldev.com/1624/command-design-pattern.
代码是自解释的,就不用单独说明。直接上代码。
先定义Receiver和它的实现。
/**
* 文件系统操作接口, 文件行为者
*/
public interface FileSystemReceiver {
void openFile();
void writeFile();
void closeFile();
}
/**
* Unix文件系统 具体行为实现
*/
public class UnixFileSystemReceiver implements FileSystemReceiver{
@Override
public void openFile() {
System.out.println("Opening file in unix OS");
}
@Override
public void writeFile() {
System.out.println("Writing file in unix OS");
}
@Override
public void closeFile() {
System.out.println("Closing file in unix OS");
}
}
/**
* Windows文件系统 具体行为实现
*/
public class WindowsFileSystemReceiver implements FileSystemReceiver{
@Override
public void openFile() {
System.out.println("Opening file in Windows OS");
}
@Override
public void writeFile() {
System.out.println("Writing file in Windows OS");
}
@Override
public void closeFile() {
System.out.println("Closing file in Windows OS");
}
}
定义command和其实现
/**
* 命令抽象
*/
public interface Command {
void execute();
}
/**
* 打开文件具体命令
*/
public class OpenFileCommand implements Command{
private FileSystemReceiver fileSystemReceiver;
public OpenFileCommand(FileSystemReceiver fileSystemReceiver) {
this.fileSystemReceiver = fileSystemReceiver;
}
@Override
public void execute() {
fileSystemReceiver.openFile();
}
}
/**
* 打开文件具体命令
*/
public class CloseFileCommand implements Command{
private FileSystemReceiver fileSystemReceiver;
public CloseFileCommand(FileSystemReceiver fileSystemReceiver) {
this.fileSystemReceiver = fileSystemReceiver;
}
@Override
public void execute() {
fileSystemReceiver.closeFile();
}
}
/**
* 打开文件具体命令
*/
public class WriteFileCommand implements Command{
private FileSystemReceiver fileSystemReceiver;
public WriteFileCommand(FileSystemReceiver fileSystemReceiver) {
this.fileSystemReceiver = fileSystemReceiver;
}
@Override
public void execute() {
fileSystemReceiver.writeFile();
}
}
然后是invoker
/**
* 调用者
*/
public class FileInvoker {
public Command command;
public FileInvoker(Command command) {
this.command = command;
}
public void execute(){
command.execute();
}
}
最后,客户端使用
/**
* 客户端
*/
public class FileSystemClient {
public static void main(String[] args) {
//Creating the receiver object
FileSystemReceiver fs = FileSystemReceiverUtil.getUnderlyingFileSystem();
//creating command and associating with receiver
OpenFileCommand openFileCommand = new OpenFileCommand(fs);
//Creating invoker and associating with Command
FileInvoker file = new FileInvoker(openFileCommand);
//perform action on invoker object
file.execute();
WriteFileCommand writeFileCommand = new WriteFileCommand(fs);
file = new FileInvoker(writeFileCommand);
file.execute();
CloseFileCommand closeFileCommand = new CloseFileCommand(fs);
file = new FileInvoker(closeFileCommand);
file.execute();
}
}
其中使用到一个工具类 FileSystemReceiverUtil, 它通过os.name属性获取底层操作系统的Receiver类型。
/**
* 文件系统工具类
*/
public class FileSystemReceiverUtil {
public static FileSystemReceiver getUnderlyingFileSystem(){
String osName = System.getProperty("os.name");
System.out.println("Underlying OS is:"+osName);
if(osName.contains("Windows")){
return new WindowsFileSystemReceiver();
}else{
return new UnixFileSystemReceiver();
}
}
}
输出结果
Underlying OS is:Mac OS X
Opening file in unix OS
Writing file in unix OS
Closing file in unix OS