这篇文章呢,我们来学习一下命令模式,同样地我们会从一个例子入手(对《Head First 设计模式》这本书上的例子进行了稍微地修改),通过三个版本的迭代演进,让我们能更好地理解命令模式。
现在有一个装修公司,在装修房子时会安装一个家用电器的总控制器,例如有电灯、空调、热水器、电脑等电器,这个控制器上的每一对 ON/OFF 开关就对应了一个具体的设备,可以对该设备进行操作。
另外,有些用户家中可能没有热水器,不需要对其进行控制,而有些用户家中可能还有电视,又需要对电视进行控制。所以,具体对哪些设备进行控制,需要由用户自己决定。试想一下,这个系统该如何设计呢?
我们先来尝试一下。例如,现在需要对电灯、空调、电脑进行控制,这三个实体类定义如下(注意它们是由不同的厂家制造,其接口不同):
public class Lamp { // 接口不同,也就是开关的方法不同 public void turnOn() { System.out.println("打开电灯"); } public void turnOff() { System.out.println("关闭电灯"); } } public class AirConditioner { public void on() { System.out.println("打开空调"); } public void off() { System.out.println("关闭空调"); } } public class Computer { public void powerOn() { System.out.println("打开电脑"); } public void powerOff() { System.out.println("关闭电脑"); } } 复制代码
对于控制器呢,由于我们事先不知道具体的槽上,对应的是什么设备。所以,我们只能一个一个地进行判断,然后才能执行开关操作。
public class SimpleController1 { // Object 类型的数组 private Object[] control = new Object[3]; public void setControlSlot(int slot, Object controller) { control[slot - 1] = controller; } // 使用 instanceOf 判断类型 public void onButtonWasPressed(int slot) { if (control[slot - 1] instanceof Lamp) { Lamp lamp = (Lamp) control[slot - 1]; lamp.turnOn(); } else if (control[slot - 1] instanceof AirConditioner) { AirConditioner airConditioner = (AirConditioner) control[slot - 1]; airConditioner.on(); } else if (control[slot - 1] instanceof Computer) { Computer computer = (Computer) control[slot - 1]; computer.powerOn(); } } public void offButtonWasPushed(int slot) { if (control[slot - 1] instanceof Lamp) { Lamp lamp = (Lamp) control[slot - 1]; lamp.turnOff(); } else if (control[slot - 1] instanceof AirConditioner) { AirConditioner airConditioner = (AirConditioner) control[slot - 1]; airConditioner.off(); } else if (control[slot - 1] instanceof Computer) { Computer computer = (Computer) control[slot - 1]; computer.powerOff(); } } } 复制代码
下面写个类来测试一下:
public class Test { public static void main(String[] args) { // 三种家电 Lamp lamp = new Lamp(); AirConditioner airConditioner = new AirConditioner(); Computer computer = new Computer(); // 设置到相应的控制槽上 SimpleController1 simpleController1 = new SimpleController1(); simpleController1.setControlSlot(1, lamp); simpleController1.setControlSlot(2, airConditioner); simpleController1.setControlSlot(3, computer); // 对 1 号槽对应的设备进行开关操作 simpleController1.onButtonWasPressed(1); simpleController1.offButtonWasPushed(1); } } // 打开电灯 // 关闭电灯 复制代码
对于上面的这种方式,由于无法预先知道控制器上的槽对应的什么设备,所以控制器的实现中使用了大量的类型判断语句,我们可以看到,这样的设计很不好。
另外,如果有别的用户想要控制其他设备,就需要去修改控制器的代码,这明显不符合开闭原则,并且会造成很大的工作量。
那该如何进行改进呢?我们想着要是这些设备的接口可以修改就好了,我们将它们的接口修改成统一的,也就不需要再去一个一个地判断了。
来看一下它如何实现,我们定义一个家电接口,其中包含开关操作,然后让不同的家电设备去实现它。
public interface HomeAppliance { void on(); void off(); } public class Lamp implements HomeAppliance { @Override public void on() { System.out.println("打开电灯"); } @Override public void off() { System.out.println("关闭电灯"); } } public class AirConditioner implements HomeAppliance { @Override public void on() { System.out.println("打开空调"); } @Override public void off() { System.out.println("关闭空调"); } } public class Computer implements HomeAppliance { @Override public void on() { System.out.println("打开电脑"); } @Override public void off() { System.out.println("关闭电脑"); } } 复制代码
如此,控制器就可以这样设计:
public class SimpleController2 { // 三种家电,统一的接口 private HomeAppliance[] control = new HomeAppliance[3]; public void setControlSlot(int slot, HomeAppliance controller) { control[slot - 1] = controller; } // 不需要再进行判断 public void onButtonWasPressed(int slot) { control[slot - 1].on(); } public void offButtonWasPushed(int slot) { control[slot - 1].off(); } } 复制代码
下面写段代码来测试一下:
public class Test { public static void main(String[] args) { HomeAppliance lamp = new Lamp(); HomeAppliance airConditioner = new AirConditioner(); HomeAppliance computer = new Computer(); SimpleController2 simpleController2 = new SimpleController2(); simpleController2.setControlSlot(1, lamp); simpleController2.setControlSlot(2, airConditioner); simpleController2.setControlSlot(3, computer); simpleController2.onButtonWasPressed(1); simpleController2.offButtonWasPushed(1); } } 复制代码
可以看到,我们不需要再写大量的类型判断语句,并且有用户想要控制别的设备时,只需要让该设备实现 HomeAppliance 接口,就可以了。
但理想很丰满,显示很苦干。可惜的是这些家电设备的接口从出厂时就已经固定了,无法再改变,这种方式只是看起来不错,我们还需要另寻出路。
我们继续进行改进。那我们能否将这些设备包装一下,让其对外提供统一的开关方法,如此控制器就不需要去判断是什么类型,而是只管去调用包装后的开关方法就好了。
也就是说重新定义一个统一的接口,它包含了开关操作的方法,然后让不同的设备,都创建一个与它自己对应的类,用来操作它本身。
对于三个实体类,我们仍然使用第一次尝试时使用的类。而这个统一的接口可以这样定义:
public interface OnOff { void on(); void off(); } 复制代码
然后,让不同的设备,都创建一个与它自己对应的类,其内部封装了它自己。在对外提供的统一方法 on/off 实现中,再去调用自己的开关方法:
public class LampOnOff implements OnOff { private Lamp lamp; public Lamp_OnOff(Lamp lamp) { this.lamp = lamp; } @Override public void on() { lamp.turnOn(); } @Override public void off() { lamp.turnOff(); } } public class AirConditionerOnOff implements OnOff { private AirConditioner airConditioner; public AirConditioner_OnOff(AirConditioner airConditioner) { this.airConditioner = airConditioner; } @Override public void on() { airConditioner.on(); } @Override public void off() { airConditioner.off(); } } public class ComputerOnOff implements OnOff { private Computer computer; public Computer_OnOff(Computer computer) { this.computer = computer; } @Override public void on() { computer.powerOn(); } @Override public void off() { computer.powerOff(); } } 复制代码
这时控制器就可以这样写,和版本 2 很类似:
public class SimpleController3 { private OnOff[] onOff = new OnOff[3]; public void setControlSlot(int slot, OnOff controller) { onOff[slot - 1] = controller; } public void onButtonWasPressed(int slot) { onOff[slot - 1].on(); } public void offButtonWasPushed(int slot) { onOff[slot - 1].off(); } } 复制代码
下面写段代码来测试一下:
public class Test { public static void main(String[] args) { Lamp lamp = new Lamp(); AirConditioner airConditioner = new AirConditioner(); Computer computer = new Computer(); // 三种设备封装成统一的接口 // 也就是三种命令对象 OnOff lampOnOff = new LampOnOff(lamp); OnOff airConditionerOnOff = new AirConditionerOnOff(airConditioner); OnOff computerOnOff = new ComputerOnOff(computer); SimpleController3 simpleController3 = new SimpleController3(); simpleController3.setControlSlot(1, lampOnOff); simpleController3.setControlSlot(2, airConditionerOnOff); simpleController3.setControlSlot(3, computerOnOff); simpleController3.onButtonWasPressed(1); simpleController3.offButtonWasPushed(1); } } 复制代码
上面这种做法呢,既没有了大量的判断语句,而且用户想要控制其他设备时,只需要创建一个实现 OnOff 接口的类,在这个类的 on、off 方法中,调用设备的具体实现即可。
其实上面的版本三就是命令模式,我们这就来看一下在 《Head First 设计模式》中对它的定义:它将“请求”封装成命令对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销操作。
对于这个定义如何理解呢?我们以上面的例子来说明。
在接收者(电灯)上绑定一组开关动作(turnOn/turnOff 方法)就是请求,然后将请求封装成一个命令对象(OnOff 对象),它对外只暴露 on/off 方法。
当命令对象(OnOff 对象)的 on/off 方法被调用时,接收者(电灯)就会执行相应的动作(turnOn/turnOff 方法)。对于外界来说,其他对象不知道究竟哪个接收者执行了动作,而是只知道调用了命令对象的 on/off 方法。
在将请求封装成命令对象后,就可以用命令来参数化其他对象,这里就是控制器的插槽(OnOff[])用不用的命令(OnOff 对象)当参数。
它的 UML 图如下:
下面总结一下命令模式的优点:
缺点:
对于线程池(这里我们先不考虑线程数小于核心线程数的情况),我们将任务(命令)添加到阻塞队列(工作队列)的某一端,然后线程从另一端获取一个命令,调用它的 run 方法执行,等待这个调用完成后,再取出下一个命令,继续执行。
命令(任务)接口的定义如下。而具体的任务由我们自己实现:
public interface Runnable { public abstract void run(); } 复制代码
在线程池 ThreadPoolExecutor 中有一个阻塞队列,用于存放任务,它的部分源码如下:
public class ThreadPoolExecutor extends AbstractExecutorService { // 存放命令 private final BlockingQueue<Runnable> workQueue; // 注意:这里与上面说的例子中 execute 方法不同 public void execute(Runnable command) { ··· // 线程数大于核心线程数,将命令加入到阻塞队列 if (isRunning(c) && workQueue.offer(command)) { ··· // 创建 worker addWorker(null, false); } ··· } } 复制代码
在调用 ThreadPoolExecutor 的 execute 方法时,会将实现命令接口的任务添加到阻塞队列中。
最终线程在执行 Worker 的 run 方法时,又会调用外部的 runWorker 方法,它会循环从阻塞队列中一个一个地获取命令对象,然后调用命令对象的 run 方法执行,一旦完成后,就会再去处理下一个命令对象:
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); try { // 循环调用 getTask 获取命令对象 while (task != null || (task = getTask()) != null) { w.lock(); try { try { // 调用命令对象的 run 方法执行 task.run(); } ··· } finally { task = null; w.unlock(); } } } ··· } 复制代码
这里简单地说了一下,具体线程池的实现,感兴趣的小伙伴可以自己研究一下。