一.命令模式原理
1.家电自动化遥控器API项目的问题
假设有一家家电自动化的公司,在智能家庭、智能家居里每样电器都是可以互相联通的,都是可以通过一个统一的终端来控制,这项目模拟的例子就是遥控器,可以控制家里所有的家电。这个遥控器简化以后就放成这样五排按钮,每排理解为一个为关、一个为开(或者是音响的话,一个是把声音调大,一个是把声音调小,是电视机的话,一个是加频道,一个是减频道)。
项目要做的是通过在遥控器上加载好我们设计好的软件或项目,通过这上面的键可以控制电灯、电视、音响等家电,项目的要求是各种家电的API给你后,要可以灵活的把它加上去,控制家电的功能。
家电公司提供的API可以认为是SDK、API接口,我们获取到这个接口以后,去控制电灯的开还是关、空调的加温度还是减温度;遥控器本身也应该是一个接口或模板,因为按照这个模板做change按钮,就会调用下面提供的函数或者方法,在这里就是我们要设计的自动化遥控器API,change按钮调用遥控器API以后去控制家电API去控制家电。
Light.java
package com.bijian.study.device;
/**
* 可以认为是各种家电公司提供的不同SDK或类对象
* 灯类
* @author bijian
*
*/
public class Light {
String loc = "";
//loc表示是哪个灯,是客厅的还是卧室的还是厨房的
public Light(String loc) {
this.loc = loc;
}
//开灯
public void On() {
System.out.println(loc + " On");
}
//关灯
public void Off() {
System.out.println(loc + " Off");
}
}
Stereo.java
package com.bijian.study.device;
/**
* CD类
* @author bijian
*
*/
public class Stereo {
static int volume = 0;
public void On() {
System.out.println("Stereo On");
}
public void Off() {
System.out.println("Stereo Off");
}
public void SetCd() {
System.out.println("Stereo SetCd");
}
//设置音量
public void SetVol(int vol) {
volume = vol;
System.out.println("Stereo volume=" + volume);
}
//获得音量值
public int GetVol() {
return volume;
}
//开始
public void Start() {
System.out.println("Stereo Start");
}
}
Control.java
package com.bijian.study.oo;
/**
* 控制器接口
* @author bijian
*/
public interface Control {
public void onButton(int slot);
public void offButton(int slot);
public void undoButton();
}
TraditionControl.java
package com.bijian.study.oo;
import com.bijian.study.device.Light;
import com.bijian.study.device.Stereo;
/**
* 传统的作法是把灯的对象或音响的对象直接传到遥控器上,遥控器change时根据灯和音响放到哪个插槽上on或off时调用相应的功能
* @author bijian
*/
public class TraditionControl implements Control {
Light light;
Stereo stereo;
//构造函数中就传入灯和音响
public TraditionControl(Light light, Stereo stereo) {
this.light = light;
this.stereo = stereo;
}
//在on时我们假设第一个插槽控制的是灯,第二、三个插槽是对音响的控制
@Override
public void onButton(int slot) {
// TODO Auto-generated method stub
switch (slot) {
case 0:
light.On();//灯亮
break;
case 1:
stereo.On();//音响打开
break;
case 2:
int vol = stereo.GetVol();
if (vol < 11) {
stereo.SetVol(++vol);//音响音量调高
}
break;
}
}
@Override
public void offButton(int slot) {
// TODO Auto-generated method stub
switch (slot) {
case 0:
light.Off();//灯关
break;
case 1:
stereo.Off();//音响关掉
break;
case 2:
int vol = stereo.GetVol();
if (vol > 0) {
stereo.SetVol(--vol);//音响音量调低
}
break;
}
}
@Override
public void undoButton() {
// TODO Auto-generated method stub
}
}
ControlTest.java
package com.bijian.study.oo;
import com.bijian.study.device.Light;
import com.bijian.study.device.Stereo;
/**
* 主程序
* @author bijian
*/
public class ControlTest {
public static void main(String[] args) {
Control ctl;
Light light = new Light("Bedroom");//卧室的灯
Stereo stereo = new Stereo();
ctl = new TraditionControl(light, stereo);
ctl.onButton(0);//把灯打开
ctl.offButton(0);//把灯关掉
ctl.onButton(1);//把音响打开
ctl.onButton(2);//把音量调大
ctl.offButton(2);//把音量调小
ctl.offButton(1);//把音响关掉
}
}
运行结果:
Bedroom On Bedroom Off Stereo On Stereo volume=1 Stereo volume=0 Stereo Off
但项目对自动化遥控器要求扩展性好、维护性好。假设有新的设备进来,要加载新的东西,要能加进来且遥控器本身的变化要小,出现故障的概率要小,扩展过程中不能带入BUG。也就是要对功能扩展开放,对代码修改关闭。
但上述的代码,要加新的设备,必须在TraditionControl.java中增加本地变地,构造函数里也要加进来,同时控制的onButton、offButton也要加case分支及逻辑。这种设计对功能开放,但对代码的修改也是开放的,相悖对功能扩展开放,对代码修改关闭原则。这种设计的问题也可简单理解为耦合度高,即遥控器具体使用或调用过程中的功能是设备里面具体的执行函数,也就是说控制器和设备之间是强相关的,所以有一方变动,另一方也要变动。
2.命令模式的原理
命令模式就是把原来的命令抽取成一个对象,然后把这个对象放到我们的控制器里面。如下我们把所有命令抽象成接口,接口里有execute、undo两个方法,我们把所有对原子的执行都封装成命令对象。在控制器Invoker中有一个setCommand,把这里所有的具体的命令放到setCommand里,把它和插槽关联起来,在控制器里面,插槽的调用只是调用接口,因为它只是接口,和具体设备不相关了,就减耦合了,具体执行这个命令是实现什么功能由具体设备决定的。
命令模式:将请求、命令、动作等封装成对象,这样可以让项目使用这些对象来参数化其他对象。使得命令的请求者和执行者解耦。
3.设计方案类图
setCommand函数就是把命令接口和具体的插槽里面的按钮关联起来,而具体命令接口实现和扩展以后,我们把所有家电的执行函数都包装成对象。比如电灯的开是一个命令对象LightOnCommand,音响的开也是一个命令对象StereoOnCommand。在遥控器上按下一个按钮以后就会调用命令的执行函数,执行函数具体化以后就是不同的命令,不同的命令再去调相应的物理设备,执行相应的开、关等等。这样就保证了遥控器上面的代码在调用具体实现时和原来的设备就解耦了。如增加设备,只要增加命令并将其通过setCommand方法设置到控制器上即可。
二.命令模式项目代码
把灯的打开、关闭抽取为两个命令类,分别是LightOnCommand、LightOffCommand,同理音响也是一样,拆分成StereoOnCommand、StereoOffCommand、StereoAddVolCommand、StereoSubVolCommand。
LightOffCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
import com.bijian.study.device.Light;
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.Off();
}
@Override
public void undo() {
light.On();
}
}
LightOnCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
import com.bijian.study.device.Light;
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.On();
}
@Override
public void undo() {
light.Off();
}
}
StereoAddVolCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
import com.bijian.study.device.Stereo;
public class StereoAddVolCommand implements Command {
private Stereo stereo;
public StereoAddVolCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
int vol = stereo.GetVol();
if (vol < 11) {
stereo.SetVol(++vol);//音响音量调高
}
}
@Override
public void undo() {
int vol = stereo.GetVol();
if (vol > 0) {
stereo.SetVol(--vol);//音响音量调低
}
}
}
StereoSubVolCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
import com.bijian.study.device.Stereo;
public class StereoSubVolCommand implements Command {
private Stereo stereo;
public StereoSubVolCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
int vol = stereo.GetVol();
if (vol > 0) {
stereo.SetVol(--vol);//音响音量调低
}
}
@Override
public void undo() {
int vol = stereo.GetVol();
if (vol < 11) {
stereo.SetVol(++vol);//音响音量调高
}
}
}
StereoOnCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
import com.bijian.study.device.Stereo;
public class StereoOnCommand implements Command {
private Stereo stereo;
public StereoOnCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.On();
stereo.SetCd();
}
@Override
public void undo() {
stereo.Off();
}
}
StereoOffCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
import com.bijian.study.device.Stereo;
public class StereoOffCommand implements Command {
private Stereo stereo;
public StereoOffCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.Off();
}
@Override
public void undo() {
stereo.On();
stereo.SetCd();
}
}
NoCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
public class NoCommand implements Command {
@Override
public void execute() {
}
@Override
public void undo() {
}
}
MarcoCommand.java
package com.bijian.study.command.impl;
import com.bijian.study.command.Command;
/**
* 宏命令
* @author bijian
*
*/
public class MarcoCommand implements Command {
private Command[] commands;
public MarcoCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for(int i=0,len=commands.length;i<len;i++) {
commands[i].execute();
}
}
@Override
public void undo() {
for (int i = commands.length - 1; i >= 0; i--) {
commands[i].undo();
}
}
}
Command.java
package com.bijian.study.command;
public interface Command {
public void execute();
public void undo();
}
CommandModeControl.java
package com.bijian.study.command;
import java.util.Stack;
import com.bijian.study.command.impl.NoCommand;
import com.bijian.study.oo.Control;
/**
* 命令模式实现的摇控器
*/
public class CommandModeControl implements Control {
//on命令数组
private Command[] onCommands;
//off命令数组
private Command[] offCommands;
private Stack<Command> stack = new Stack<Command>();
public CommandModeControl() {
onCommands = new Command[5];
offCommands = new Command[5];
Command noCommand = new NoCommand();
//onButton、offButton方法中无需判断onCommands[slot]是否为空
for(int i=0,len = onCommands.length;i<len;i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
@Override
public void onButton(int slot) {
onCommands[slot].execute();
stack.push(onCommands[slot]);
}
@Override
public void offButton(int slot) {
offCommands[slot].execute();
stack.push(offCommands[slot]);
}
@Override
public void undoButton() {
stack.pop().undo();
}
}
ControlTest.java
package com.bijian.study.command;
import com.bijian.study.command.impl.LightOffCommand;
import com.bijian.study.command.impl.LightOnCommand;
import com.bijian.study.command.impl.MarcoCommand;
import com.bijian.study.command.impl.StereoAddVolCommand;
import com.bijian.study.command.impl.StereoOffCommand;
import com.bijian.study.command.impl.StereoOnCommand;
import com.bijian.study.command.impl.StereoSubVolCommand;
import com.bijian.study.device.Light;
import com.bijian.study.device.Stereo;
public class ControlTest {
public static void main(String[] args) {
CommandModeControl control = new CommandModeControl();
MarcoCommand onmarco,offmarco;
Light bedroomlight = new Light("BedRoom");
Light kitchlight = new Light("Kitch");
Stereo stereo = new Stereo();
LightOnCommand bedroomlighton = new LightOnCommand(bedroomlight);
LightOffCommand bedroomlightoff = new LightOffCommand(bedroomlight);
LightOnCommand kitchlighton = new LightOnCommand(kitchlight);
LightOffCommand kitchlightoff = new LightOffCommand(kitchlight);
Command[] oncommands = {bedroomlighton, kitchlighton};
Command[] offcommands = {bedroomlightoff, kitchlightoff};
onmarco = new MarcoCommand(oncommands);
offmarco = new MarcoCommand(offcommands);
StereoOnCommand stereoOn = new StereoOnCommand(stereo);
StereoOffCommand stereoOff = new StereoOffCommand(stereo);
StereoAddVolCommand stereoaddvol = new StereoAddVolCommand(stereo);
StereoSubVolCommand stereosubvol = new StereoSubVolCommand(stereo);
control.setCommand(0, bedroomlighton, bedroomlightoff);
control.setCommand(1, kitchlighton, kitchlightoff);
control.setCommand(2, stereoOn, stereoOff);
control.setCommand(3, stereoaddvol, stereosubvol);
control.setCommand(4, onmarco, offmarco);
//不带回退功能的应用
System.out.println("------------不带回退功能的应用-------------");
control.onButton(0);
control.offButton(0);
control.onButton(1);
control.offButton(1);
control.onButton(2);
control.onButton(3);
control.offButton(3);
control.offButton(2);
//带回退功能的应用
System.out.println("------------带回退功能的应用-------------");
control.onButton(0);
control.undoButton();
control.onButton(1);
control.offButton(1);
control.onButton(2);
control.onButton(3);
control.offButton(3);
control.undoButton();
control.offButton(2);
control.undoButton();
//宏命令应用
System.out.println("------------宏命令应用-------------");
control.onButton(4);
control.offButton(4);
control.onButton(4);
control.undoButton();
}
}
运行结果:
------------不带回退功能的应用------------- BedRoom On BedRoom Off Kitch On Kitch Off Stereo On Stereo SetCd Stereo volume=1 Stereo volume=0 Stereo Off ------------带回退功能的应用------------- BedRoom On BedRoom Off Kitch On Kitch Off Stereo On Stereo SetCd Stereo volume=1 Stereo volume=0 Stereo volume=1 Stereo Off Stereo On Stereo SetCd ------------宏命令应用------------- BedRoom On Kitch On BedRoom Off Kitch Off BedRoom On Kitch On Kitch Off BedRoom Off
三.命令模式关键点
1.命令模式的意义
传统设计耦合在一起,不易扩展、升级、维护,同时也违背了对功能扩展开放,对代码修改关闭原则。命令模式把外围设备、API或命令封装组合成一个对象,具体某个按钮按下去的时候,调的是这个抽象的命令接口,这个抽象的命令接口在运行时会赋值一个具体的命令对象,摇控器按钮并不知道具体执行的是哪个对象或设备,因为运行时是通过setCommand把具体的对象或具体的命令执行放到按钮下面,按钮只知道调用一个接口的execute方法,这样就达到解耦的目的。
2.解耦的意义
摇控器的升级和外围设备不相干了,同样外围设备的升级和摇控器也不相干了。层次结构清晰,扩展、升级、维护相当容易,而且不易引入新的BUG。