转载

一步一步理解命令模式

这篇文章呢,我们来学习一下命令模式,同样地我们会从一个例子入手(对《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();
            }
        }
    } ···
}
复制代码

这里简单地说了一下,具体线程池的实现,感兴趣的小伙伴可以自己研究一下。

原文  https://juejin.im/post/5cde39206fb9a07f0c464f4c
正文到此结束
Loading...