Error message here!

Hide Error message here!

忘记密码?

Error message here!

请输入正确邮箱

Hide Error message here!

密码丢失?请输入您的电子邮件地址。您将收到一个重设密码链接。

Error message here!

返回登录

Close

多线程场景设计利器:分离方法的调用和执行——命令模式总结

dashuai的博客 2019-02-06 21:49:00 阅读数:150 评论数:0 点赞数:0 收藏数:0

前言

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

实现思路

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

其实没什么复杂的,就是使用 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 接口,这就是一个命令接口,将对线程启动的请求和具体的执行分离了。实现该接口,也是启动线程推荐的写法

版权声明
本文为[dashuai的博客]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/kubixuesheng/p/10353809.html

编程之旅,人生之路,不止于编程,还有诗和远方。
阅代码原理,看框架知识,学企业实践;
赏诗词,读日记,踏人生之路,观世界之行;

支付宝红包,每日可领