一步一步理解命令模式

这篇文章呢,咱们来学习一下命令模式,一样地咱们会从一个例子入手(对《Head First 设计模式》这本书上的例子进行了稍微地修改),经过三个版本的迭代演进,让咱们能更好地理解命令模式。java

命令模式

如今有一个装修公司,在装修房子时会安装一个家用电器的总控制器,例若有电灯、空调、热水器、电脑等电器,这个控制器上的每一对 ON/OFF 开关就对应了一个具体的设备,能够对该设备进行操做。设计模式

另外,有些用户家中可能没有热水器,不须要对其进行控制,而有些用户家中可能还有电视,又须要对电视进行控制。因此,具体对哪些设备进行控制,须要由用户本身决定。试想一下,这个系统该如何设计呢?数组

版本一

咱们先来尝试一下。例如,如今须要对电灯、空调、电脑进行控制,这三个实体类定义以下(注意它们是由不一样的厂家制造,其接口不一样):ide

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);
    }
}
// 打开电灯
// 关闭电灯
复制代码

对于上面的这种方式,因为没法预先知道控制器上的槽对应的什么设备,因此控制器的实现中使用了大量的类型判断语句,咱们能够看到,这样的设计很很差。this

另外,若是有别的用户想要控制其余设备,就须要去修改控制器的代码,这明显不符合开闭原则,而且会形成很大的工做量。spa

版本二

那该如何进行改进呢?咱们想着要是这些设备的接口能够修改就行了,咱们将它们的接口修改为统一的,也就不须要再去一个一个地判断了。线程

来看一下它如何实现,咱们定义一个家电接口,其中包含开关操做,而后让不一样的家电设备去实现它。设计

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 设计模式》
相关文章
相关标签/搜索