六、命令模式(Command Pattern)《HeadFirst设计模式》读书笔记
命令模式就是将一系列执行操作封装成一个命令对象,这样这个命令对象就可以被传递,在需要执行的时候执行,比如当作队列或者日志的参数,同时命令模式也支持操作的撤销。
比如有一个需求,要求写一个遥控器类,用遥控器去控制不同电器的开关,下面是遥控器控制打开电灯的代码示例,遥控器按下打开按钮时电灯就会打开,按下关闭按钮时电灯就会关闭。
public class RemoteControl { public void pushTheOnButton(){ System.out.println("电灯打开了"); } public void pushTheOffButton(){ System.out.println("电灯关闭了"); } }
在上面的代码中,用简单的输出语句代表了电灯打开和关闭的操作,可以发现具体的操作都耦合在了遥控器类里面了,因为遥控器不仅仅可以控制电灯,它还可以控制很多其它电器如空调、风扇等,以后如果我们想把按下遥控器打开按钮的功能改成打开空调,就不得不修改遥控器代码了。
怎么改进呢?如果能把操作过程通过参数传递进来,比如在我们想把按下遥控器打开按钮的功能改成打开空调的时候,首先把打开空调的操作传递到pushTheOnButton()方法内,问题就好办了。因此我们可以将打开空调的操作封装到一个命令对象的一个方法内(这个方法统一命名为execute()),再通过传递这个对象实现这样一个功能,具体来看一下:
//所有命令类的接口 public interface Command { void execute(); } //打开空调的命令 public class TurnOnAirConditionerCommand implements Command { @Override public void execute(){ System.out.println("空调打开了"); } } //关闭空调的命令 public class TurnOffAirConditionerCommand implements Command { @Override public void execute(){ System.out.println("空调关闭了"); } } //遥控器 public class RemoteControl { //控制打开的命令对象 private Command onCommand; //控制关闭的命令对象 private Command offCommand; //遥控器按下打开按钮 public void pushTheOnButton(){ onCommand.execute(); } //遥控器按下关闭按钮 public void pushTheOffButton(){ offCommand.execute(); } //打开命令的setter方法 public void setOnCommand(Command onCommand) { this.onCommand = onCommand; } //关闭命令的setter方法 public void setOffCommand(Command offCommand) { this.offCommand = offCommand; } } //测试 public class testCommand { public static void main(String[] args) { //创建遥控器 RemoteControl remoteControl = new RemoteControl(); //创建打开和关闭空调的命令 TurnOnAirConditionerCommand onCommand = new TurnOnAirConditionerCommand(); TurnOffAirConditionerCommand offCommand = new TurnOffAirConditionerCommand(); //通过setter方法注入遥控器内 remoteControl.setOnCommand(onCommand); remoteControl.setOffCommand(offCommand); //调用遥控器的开关按钮方法 remoteControl.pushTheOnButton(); remoteControl.pushTheOffButton(); } }
测试结果:
首先运用面向接口编程的思想,在遥控器内声明Command接口类型的变量,再在内部的方法中去调用它的excute()方法。这看起来和策略模式有点相似;另一方面,把打开空调的操作单独封装到了一个命令对象当中,这是不是又有点像是工厂模式呢。关于它们的不同在最后的总结时再来说明。
通过上面的例子,我们简单了解了什么是命令模式还有它的代码实现,在《HeadFirst设计模式》中局的例子比上面的稍复杂一些,它在excute()的方法内部又调用了一个命令接收者的具体方法,比如又调用了空调对象的turnOn()方法,这种做法又进一步将空调的具体操作从命令对象中解耦了,只是一下子理解起来会容易混淆。另外书中的遥控器有7个插槽,可以通过数组的方式存储命令对象,还有所谓的宏命令,就是可以执行一连串的命令,也是通过在一个命令对象内用数组接收一连串命令对象,然后在execute()方法中遍历数组执行每个命令对象的execute()方法,如果看懂了上面的例子,这些都很好理解。
下面简单说一下撤销功能以及它在队列请求和日志请求方面的使用场景。
撤销功能,要想实现撤销首先要保存之前的状态才能恢复,也就是要保存上一次操作的命令对象,然后调用它的undo()方法就可以了,undo()方法其实就是和execute()的执行逻辑相反,
首先在Command接口声明抽象方法undo();
//所有命令类的接口 public interface Command { void execute(); void undo(); }
然后在具体的命令中实现undo()方法
//关闭空调的命令 public class TurnOnAirConditionerCommand implements Command { @Override public void execute(){ System.out.println("空调打开了"); } @Override public void undo() { System.out.println("撤销打开,空调又关闭了"); } }
最后在遥控器内记录上一次执行操作的的命令
public class RemoteControl { //控制打开的命令对象 private Command onCommand; //控制关闭的命令对象 private Command offCommand; //记录上一次操作的命令来实现撤销功能 private Command undoCommand; //遥控器按下打开按钮 public void pushTheOnButton(){ onCommand.execute(); //当命令执行时将当前命令保存 undoCommand = onCommand; } //遥控器按下关闭按钮 public void pushTheOffButton(){ offCommand.execute(); //当命令执行时将当前命令保存 undoCommand = offCommand; } //撤销按钮 public void pushUndoButton(){ undoCommand.undo(); } //打开命令的setter方法 public void setOnCommand(Command onCommand) { this.onCommand = onCommand; } //关闭命令的setter方法 public void setOffCommand(Command offCommand) { this.offCommand = offCommand; } }
测试一下:
public class testCommand { public static void main(String[] args) { //创建遥控器 RemoteControl remoteControl = new RemoteControl(); //创建打开和关闭空调的命令 TurnOnAirConditionerCommand onCommand = new TurnOnAirConditionerCommand(); TurnOffAirConditionerCommand offCommand = new TurnOffAirConditionerCommand(); //通过setter方法注入遥控器内 remoteControl.setOnCommand(onCommand); remoteControl.setOffCommand(offCommand); //调用遥控器的开关按钮方法 remoteControl.pushTheOffButton(); remoteControl.pushTheOnButton(); //调用遥控器的撤销方法 remoteControl.pushUndoButton(); } }
撤销成功,这里只能撤销一次,如果需要撤销多次,那就需要保存多个命令的状态。
队列请求:将命令对象放到队列中进行排队,在另一边可以通过线程取出命令并调用它的execute()方法,这里队列和命令对象是解耦的。
日志请求:将命令对象记录在日志中,如果系统死机,可以重新加载命令对象,调用它们的execute()方法,恢复系统。
总结:
1.命令模式就是将一段操作逻辑或方法封装到一个命令对象中,命令对象可以作为队列、日志等的参数进行传递,实现命令的调用者和具体命令的实现解耦。
2.其实命令模式和策略模式以及工厂模式都有一些相似之处,但是不同点主要在于他们的使用场景不同,策略模式主要是组合一族算法簇,实现功能的扩展;命令模式用于将具体的命令封装成对象,就可以传递和存储,灵活的被调用执行,可以被记录在日志中用于系统恢复,也可以用在队列中,排队执行,它还支持撤销功能;而工厂模式则用于创建对象。其实很多设计模式他们在代码实现和一些思路上确实有一些相似之处,因为他们都是充分的应用了java语言的特性(封装、继承、多态、抽象等),用于解决特定类型问题的模式,因此我们应该更关心他们的使用场景,细心体会那一丝略微的不同和巧妙之处。
3.如同上面说的,任何模式都不能脱离具体的使用场景随意使用,有时要结合实际情况选择合适的设计模式,甚至有时无模式胜过有模式,代码简洁同样重要。