多线程场景设计利器:分离方法的调用和执行——命令模式总结
前言
个人感觉,该模式主要还是在多线程程序的设计中比较常用,尤其是一些异步任务执行的过程。但是本文还是打算先在单线程程序里总结它的用法,至于多线程环境中命令模式的用法,还是想在多线程的设计模式里重点总结。
实现思路
其实思路很简单,就是把方法的请求调用和具体执行过程分开,让客户端不知道该请求是如何、何时执行的。那么如何分开呢?
其实没什么复杂的,就是使用 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 接口,这就是一个命令接口,将对线程启动的请求和具体的执行分离了。实现该接口,也是启动线程推荐的写法