一步一步理解命令模式

栏目: 后端 · 发布时间: 5年前

内容简介:这篇文章呢,我们来学习一下命令模式,同样地我们会从一个例子入手(对《Head First 设计模式》这本书上的例子进行了稍微地修改),通过三个版本的迭代演进,让我们能更好地理解命令模式。现在有一个装修公司,在装修房子时会安装一个家用电器的总控制器,例如有电灯、空调、热水器、电脑等电器,这个控制器上的每一对 ON/OFF 开关就对应了一个具体的设备,可以对该设备进行操作。另外,有些用户家中可能没有热水器,不需要对其进行控制,而有些用户家中可能还有电视,又需要对电视进行控制。所以,具体对哪些设备进行控制,需要由

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

PHP for the World Wide Web, Second Edition (Visual QuickStart Gu

PHP for the World Wide Web, Second Edition (Visual QuickStart Gu

Larry Ullman / Peachpit Press / 2004-02-02 / USD 29.99

So you know HTML, even JavaScript, but the idea of learning an actual programming language like PHP terrifies you? Well, stop quaking and get going with this easy task-based guide! Aimed at beginning ......一起来看看 《PHP for the World Wide Web, Second Edition (Visual QuickStart Gu》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具