写在最开始的话
为什么要有设计模式?设计模式是用来做什么的?写了那么多年的代码,我认为用最简单通俗的话来说,设计模式是用来解耦代码,使得工程代码具有低耦合、高内聚的特性,模块化程度更高,进而提高软件模块的复用率以及扩展性,使得软件维护成本大大降低的一种技术。
所有设计模式相关文章都分为两个个部分,第一个部分:设计模式的定义;第二个部分:如何使用设计模式,什么时候使用这种设计模式。
- 命令模式的定义是什么?
命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
命令模式的类图:
命令模式允许将“发出请求的对象”和“接受与执行这些请求的对象”分隔开来。在上面的类图中,谁是“发出请求的对象”?Invoker。谁是“接受与执行这些请求的对象”?Receiver和ConcreateCommand。
2. 如何使用设计模式,什么时候使用这种设计模式?
命令模式是如何解耦代码以及体现代码的良好的扩展性的呢?请看上面的类图。在Command这个接口上是不是可以实现很多个ConcreateCommand? 同时,每个ConcnreateCommand对应一个Receiver。 当需要扩展代码的时候,新的ConcreateCommand只需要实现Command接口,然后再添加一个新的Receiver就行了,以前的旧代码完全不用修改。
下面来再来看看具体的代码如何写?借用Head First设计模式里面的例子,为了只关注于设计,删减了一些代码细节。现在有一个遥控器,遥控器上有5对按钮,每一对按钮管理一种家电的开关。
遥控器是invoker类,这个类的代码为:
public class Invoker{
Command[] onCommands;//开命令
Command[] offCommands;//关命令
public Invoker(){
onCommands = new Command[7];
offCommands = new Command[7];
}
public void setCommand(int slot, Command onCommand, Command offCommand){
onCommand[slot] = onCommand;
offCommand[slot] = offCommand;
} //将执行命令的对象设置到请求对象中
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
}//打开电器
public void offButtonWasPushed(int slot){
offCommands[slot].execute();
}//关掉电器
}
Invoker就是发出请求的对象,在Invoker中设置了需要具体执行的命令的对象Command之后,调用Command对象的execute方法,执行命令的具体内容。
public interface Command{
public void execute();
}
下面来添加具体的开关家电的功能:
(1)添加遥控器的第一组开关功能:开关灯
假设现在需要控制电灯的开关,就要实现一个电灯的实体类Light和开关电灯的命令类LightOnCommand、 LightOffCommand。
public class Light(){
public void on();
public void off();
}
public class LightOnCommand implements Command{
Light light;
public LightOnCommand(Light light){
this.light = light;
}
Public void execute(){
light.on();
}
}
public class LightOffCommand implements Command{
Light light;
public LightOffCommand(Light light){
this.light = light;
}
Public void execute(){
light.off();
}
}
最后,我们在Client类中,完整的执行“打开电灯、关闭电灯”这个命令,看看Invoker、command以及Light是如何协同工作的。
public class Client{
public static void main(String[] args){
Invoker invoker = new Invoker();//创建请求对象
Light light = new Light();//创建请求接收对象
LightOnCommand lightOn = new LightOnCommand(light); //创建执行开灯请求对象
LightOffCommand lightOff = new LightOffCommand(light);//创建执行关灯请求对象
invoker.setCommand(0, lightOn,lightOff );//将执行请求的对象设置到请求对象中
invoker.onButtonWasPushed(0);//执行开灯请求
invoker.offButtonWasPushed(0);//执行关灯请求
}
}
(2)添加遥控器的第二组开关功能:开关音响
添加这个功能的时候,我们就可以看到代码是如何在不影响旧的代码情况下,轻松的增加扩展代码的。
public class Stereo{
public void on();
public void off();
public void setCd();
public void setDvd();
public void setRadio();
public void setVolume();
}
Public class StereoOnWithCDCommand implements Command{
Stereo stereo;
Public StereoOnWithCDCommand(Stereo stereo){
this.stereo = stereo;
}
Public void execute(){
stereo.on();
stereo.setCD();
stereo.setVolume(11);
}
}
Public class StereoOffWithCDCommand implements Command{
Stereo stereo;
Public StereoOffWithCDCommand(Stereo stereo){
this.stereo = stereo;
}
Public void execute(){
stereo.off();
}
}
最后,如何在Client中开关音响呢?只需要把light相关的类换成stereo相关的类就可以了。
public class Client{
public static void main(String[] args){
Invoker invoker = new Invoker();//创建请求对象
Stereo stereo = new Stereo();//创建请求接收对象
StereoOnCommand stereoOn = new StereoOnCommand(stereo);//创建执行开灯请求对象
StereoOffCommand stereoOff = new StereoOffCommand(stereo);//创建执行关灯请求对象
invoker.setCommand(1, stereoOn,stereoOff );//将执行请求的对象设置到请求对象中
invoker.onButtonWasPushed(1);//执行开灯请求
invoker.offButtonWasPushed(1);//执行关灯请求
}
}
在这里我们就可以看到,整个过程只添加了新的Stereo、StereoOnCommand、StereoOffCommand类,在Client中添加调用语句即可,旧有的代码完全没有受到任何影响。
由此,我们也可以得出展开的命令模式的类图:
在上面这张类图中,可以看出在“区域1”和“区域2”中,是随意增加新的类和功能的,而Invoker类以及Command接口可以保持不变,这就是命令模式的扩展性。
另外,Client连接Light、Stereo、LighOnCommand、StereoOCommand的箭头是表示创建对象,而Invoker与Command之间的箭头以及LightOnCommand与Light之间、StereoOnCommand与Stereo之间的箭头是表示依赖关系,也就是说Invoker中有实现了Command接口的对象stereoOnCommand、LightOnCommand作为成员;StereoOnCommand中有Stereo作为成员;LightOnCommand中有Light作为成员。
(3)什么时候使用命令模式
命令模式除了像上面描述的那样使用外,还可以用于队列请求和日志请求。
命令可以将运算快(一个接收者和一组动作)打包,放到一个工作队列中。想象有一个工作队列:你在某一端添加命令,然后另一端则是线程。线程从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再去除下一个命令........
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的状态。因此,我们只需要在command接口中增加store()、load()两个方法,然后在store()方法中存储死机前所做的操作,死机后调用load()方法将这些操作以及对象重新恢复就行了。