这篇文章呢,我们来学习一下命令模式,同样地我们会从一个例子入手(对《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 图如下:
- 这里将 SimpleController3 称为调用者,它会持有一个或一组命令,并在某个时间调用命令对象的 on/off 方法,执行请求。
- 这里将 Lamp 称为接收者,它知道如何进行具体的工作。
- 而调用者调用 on/off 发出请求,然后由 ConcreteCommand 来调用接收者的一个或多个动作。
下面总结一下命令模式的优点:
- 降低了调用者和请求接收者的耦合度,使得调用者和请求接收者之间不需要直接交互。
- 在扩展新的命令时非常容易,只需要实现抽象命令的接口即可。
缺点:
- 命令的扩展会导致系统含有太多的类,增加了系统的复杂度。
命令模式的具体实践
JDK#线程池
对于线程池(这里我们先不考虑线程数小于核心线程数的情况),我们将任务(命令)添加到阻塞队列(工作队列)的某一端,然后线程从另一端获取一个命令,调用它的 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();
}
}
} ···
}
复制代码
这里简单地说了一下,具体线程池的实现,感兴趣的小伙伴可以自己研究一下。
参考资料
- 《Head First 设计模式》