前言

个人感觉,该模式主要还是在多线程程序的设计中比较常用,尤其是一些异步任务执行的过程。但是本文还是打算先在单线程程序里总结它的用法,至于多线程环境中命令模式的用法,还是想在多线程的设计模式里重点总结。

实现思路

其实思路很简单,就是把方法的请求调用和具体执行过程分开,让客户端不知道该请求是如何、何时执行的。那么如何分开呢?

其实没什么复杂的,就是使用 OO 思想,把对方法的请求封装为对象即可,然后在设计一个请求的接受者对象,当然还要有一个请求的发送者对象,请求本身也是一个对象。最后,请求要如何执行呢?

故,除了请求对象,请求发送者,请求接受者,还要一个请求执行者——这里可以看成是客户端,而请求(其实叫命令、或者请求都是一样的意思,后文就用请求这个术语)最好设计为抽象的(或者接口)。

也可得知,命令模式是对象的行为型的设计模式。

简单的命令模式

模拟场景:在线教育平台售卖一些培训的视频课程,规定必须付费后才能观看,故管理员需要有开放课程观看和关闭课程观看权限的操作

首先需要一个抽象的命令(请求)接口

public interface ICommand { // 抽象的命令(请求)接口
    void execute();
}

然后设计一个课程类——Lesson,它代表课程本身,也是命令(请求)的接受者,因为是对课程这个实体下命令

public class Lesson { // 代表课程本身,也是命令(请求)的接受者,因为是对课程这个实体下命令
    private String name;

    public Lesson(String name) {
        this.name = name;
    }

    public void openLesson() {
        System.out.println("可以观看课程:" + name);
    }

    public void closeLesson() {
        System.out.println("不可以观看课程:" + name);
    }
}

下面是两个具体的命令类,分别实现命令接口,里面是有聚合关系,把课程 Lesson 的引用聚合到命令类,哪一个命令要对哪一个实体,不能写错,比如关闭对关闭。

public class CloseCommand implements ICommand {
    private Lesson lesson;

    public CloseCommand(Lesson lesson) {
        this.lesson = lesson;
    }

    @Override
    public void execute() {
        this.lesson.closeLesson();
    }
}
//////////////////////////////////////////////////////
public class OpenCommand implements ICommand {
    private Lesson lesson;

    public OpenCommand(Lesson lesson) {
        this.lesson = lesson;
    }

    @Override
    public void execute() {
        this.lesson.openLesson();
    }
}

设计一个管理员类,作为命令(请求)的调用者,用来发出请求(命令),而命令的实际执行,交给了命令(请求)的接受者——Lesson

public class Admin2 {
    private ICommand commond;

    public void setCommond(ICommand commond) {
        this.commond = commond;
    }

    public void executeCommond() {
        this.commond.execute();
    }
}

客户端

        Lesson lesson1 = new Lesson("c++"); // 请求(命令)的接受者
        CloseCommand closeCommand1 = new CloseCommand(lesson1); // 命令封装为对象
        OpenCommand openCommand1 = new OpenCommand(lesson1);
        Admin2 admin2 = new Admin2(); // 请求(命令)的调用者:用来发出请求
        admin2.setCommond(openCommand1); // 将命令传给调用者
        admin2.executeCommond(); // 发出请求(命令),但是admin 并不知道这个请求(命令)发给了谁,是谁在执行这个请求(命令)
        admin2.setCommond(closeCommand1);
        admin2.executeCommond();

如上就实现了请求调用和具体执行的分离(解耦)

一次执行多个命令

下面是一次执行多个命令的写法,也可以作为宏命令的实现

命令接口和具体命令都不变,admin 变化如下:

public class Admin {
    private List<ICommand> commondList = new ArrayList<>(); // 使用 ArrayList 还能保证命令的顺序执行

    public void addCommond(ICommand commond) {
        commondList.add(commond);
    }

    public void executeCommond() {
        for (ICommand commond : commondList) {
            commond.execute();
        }
        commondList.clear();
    }
}

当然这里用栈等数据结构去包装命令也是可以的

        Lesson lesson = new Lesson("java"); // 请求(命令)的接受者
        CloseCommand closeCommand = new CloseCommand(lesson); // 命令
        OpenCommand openCommand = new OpenCommand(lesson);
        Admin admin = new Admin(); // 请求(命令)的调用者:用来发出请求
        admin.addCommond(openCommand); // 将命令传给调用者
        admin.addCommond(closeCommand);
        admin.executeCommond();

引申:空类型模式

再比如,使用静态数组去包装命令,这里引申一个空类型模式,就是说有一个类,这个类什么都不做,就是占位或者初始化用的,代替 null 类型。

下面举一个例子,设计一个控制器,控制电灯的开关,闪烁,变暗,变亮等操作

public interface ICommand2 {
    void execute(); // 命令接口
}
//////////////////////////////////
public class LightOffCommand implements ICommand2 {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        this.light.off();
    }
}
//////////////////////////////////
public class LightOnCommand implements ICommand2 {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        this.light.on();
        this.light.zoomin();
        this.light.blink();
    }
}
//////////////////////////////////
public class EmptyCommand implements ICommand2 { // 空类型模式的体现
    @Override
    public void execute() {
        System.out.println("什么都不做");
    }
}
//////////////////////////////////
public class Light {
    public Light() {
    }

    public void on() {
        System.out.println("电灯打开");
    }

    public void off() {
        System.out.println("电灯关闭");
    }

    public void zoomin() {
        System.out.println("灯光变强");
    }

    public void zoomout() {
        System.out.println("灯光变弱");
    }

    public void blink() {
        System.out.println("灯光闪烁");
    }

    public void noBlink() {
        System.out.println("灯光停止闪烁");
    }
}

下面是一个控制器类,setCommand 方法可以设置某个命令和某个操作的对应关系,初始化时,使用空类型模式

public class MainController {
    private ICommand2[] onCommands;
    private ICommand2[] offCommands;

    public MainController() {
        this.onCommands = new ICommand2[3];
        this.offCommands = new ICommand2[2];
        ICommand2 emptyCommand = new EmptyCommand();
        for (int i = 0; i < 3; i++) {
            this.onCommands[i] = emptyCommand;
        }
        for (int i = 0; i < 2; i++) {
            this.offCommands[i] = emptyCommand;
        }
    }

    public void setCommand(int idx, ICommand2 onCommand, ICommand2 offCommand) {
        this.onCommands[idx] = onCommand;
        this.offCommands[idx] = offCommand;
    }

    public void executeOnCommand(int idx) {
        this.onCommands[idx].execute();
    }

    public void executeOffCommand(int idx) {
        this.offCommands[idx].execute();
    }
}

客户端

        MainController mainController = new MainController();
        Light roomLight = new Light();
        Light doorLight = new Light();
        LightOnCommand roomLightOnCommand = new LightOnCommand(roomLight);
        LightOffCommand roomLightOffCommand = new LightOffCommand(roomLight);
        LightOnCommand doorLightOnCommand = new LightOnCommand(doorLight);
        LightOffCommand doorLightOffCommand = new LightOffCommand(doorLight);

        mainController.setCommand(0, roomLightOnCommand, roomLightOffCommand);
        mainController.setCommand(1, doorLightOnCommand, doorLightOffCommand);

        mainController.executeOnCommand(0);
        mainController.executeOffCommand(0);
        mainController.executeOnCommand(1);
        mainController.executeOffCommand(1);
        mainController.executeOnCommand(2);

命令模式在单线程环境下的优点(使用场景)

通过封装对方法的请求调用和方法执行过程,并将其分离,也就是所谓的完全解耦了。

故可以对方法的调用执行实现一些额外操作,比如记录日志,撤销某个方法的请求调用,或者实现一次请求,N 次执行某个方法等。

在架构上,可以让程序易于扩展新的请求(命令)。

命令模式在多线程程序中的优点

这样做,在多线程环境下的好处是:

1、避免算法(策略)模块执行缓慢拖累调用方——抽象了需要等待的操作

2、控制执行顺序,因为请求调用和具体执行分离,故执行顺序和调用顺序没有关系

3、可以轻松实现请求的取消,或者反复执行某个请求

4、请求调用和具体执行分离后,进一步把负责调用的机器和负责执行的机器分开,可以基于网络,实现分布式程序

命令的撤销实现

前面,无论在什么环境下,都提到了能撤销命令(请求),故命令模式经常和备忘录模式搭配使用。参考:保存快照和撤销功能的实现方案——备忘录模式总结

这里举一个很简单的例子,还是电灯开关的例子

public interface ICommand3 {
    void execute();
    void undo(); // 和 execute 执行相反的操作
}
//////////////////////////////////
public class EmptyCommand implements ICommand3 {
    @Override
    public void execute() {
        System.out.println("什么都不做");
    }

    @Override
    public void undo() {
        System.out.println("什么都不做");
    }
}
/////////////////////////////////
public class LightOnCommand implements ICommand3 {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        this.light.on();
        this.light.zoomin();
        this.light.blink();
    }

    @Override
    public void undo() {
        this.light.noBlink();
        this.light.zoomout();
        this.light.off();
    }
}
///////////////////////////////////
public class LightOffCommand implements ICommand3 {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        this.light.off();
    }

    @Override
    public void undo() {
        this.light.on();
    }
}

控制器也要变化,初始化命令的同时,也要初始化 undo 命令

public class MainController {
    private ICommand3[] onCommands;
    private ICommand3[] offCommands;
    private ICommand3 undoCommand; // 记录上一个命令

    public MainController() {
        this.onCommands = new ICommand3[3];
        this.offCommands = new ICommand3[2];
        ICommand3 emptyCommand = new EmptyCommand();
        for (int i = 0; i < 3; i++) {
            this.onCommands[i] = emptyCommand;
        }
        for (int i = 0; i < 2; i++) {
            this.offCommands[i] = emptyCommand;
        }
        this.undoCommand = emptyCommand; // 初始化 undo 命令
    }

    public void setCommand(int idx, ICommand3 onCommand, ICommand3 offCommand) {
        this.onCommands[idx] = onCommand;
        this.offCommands[idx] = offCommand;
    }

    public void executeOnCommand(int idx) {
        this.onCommands[idx].execute();
        this.undoCommand = this.onCommands[idx];
    }

    public void executeOffCommand(int idx) {
        this.offCommands[idx].execute();
        this.undoCommand = this.offCommands[idx];
    }

    public void undoCommand() {
        this.undoCommand.undo();
    }
}

客户端

        MainController mainController = new MainController();
        Light roomLight = new Light();
        LightOffCommand offCommand = new LightOffCommand(roomLight);
        LightOnCommand onCommand = new LightOnCommand(roomLight);
        mainController.setCommand(0, onCommand, offCommand);
        mainController.executeOnCommand(0);
        System.out.println();
        mainController.executeOffCommand(0);
        System.out.println();
        mainController.undoCommand();
        System.out.println();
        mainController.executeOffCommand(0);
        System.out.println();
        mainController.executeOnCommand(0);
        System.out.println();
        mainController.undoCommand();

打印如下

电灯打开
灯光变强
灯光闪烁

电灯关闭

电灯打开

电灯关闭

电灯打开
灯光变强
灯光闪烁

灯光停止闪烁
灯光变弱
电灯关闭

命令模式的缺陷

个人觉得,唯一的缺点就是会使得程序复杂性提高,但是我认为微不足道,基础扎实的 RD 应该无压力阅读和使用才对,因为在多线程程序里,该模式大量出现,比如 Netty 等框架就大量使用了该思想。 

命令模式和策略模式的区别 

策略是不同的算法做同一件事情。不同的策略之间可以相互替换。比如实现一个支付功能,有微信支付,支付宝支付,各自渠道的支付。。。

命令是不同的命令做不同的事情。对外隐藏了具体的执行细节。比如菜单中的复制,移动和压缩

JDK 中的命令模式

最最常见的就是 lang 包里的 Runnable 接口,这就是一个命令接口,将对线程启动的请求和具体的执行分离了。实现该接口,也是启动线程推荐的写法

02-06 22:07