命令设计模式
什么是命令设计模式
通过将“请求”封装成命令对象,来将操作的请求者与操作的执行者解耦,以便使用不同的请求队列或者日志来参数化其他对象,命令模式也支持可撤销的操作。
为什么要使用命令设计模式
试想下面一种业务场景。
现在拥有一个中间件,名称为 cache。这个 cache 承担了异步向用户外发送邮件与短信的功能。
系统中已经有设计好的独立的邮件与短信对象,如下:
屏幕快照 2016-12-27 下午2.33.36.png-16.4kB
发送一封邮件要比发送短信复杂的多。所以,MAIL 对象相对于 SMS 对象要复杂不少。除了 title 与 body 以外,还可以设置邮件的附件。
现在让我们开始设计中间件的具体逻辑。我的思路很清晰:首先设计一个中间件。为了能够快速的从容器中获取和添加对象,我们采用 LinkedList 对象。
然后将发邮件与发短信的任务逐个添加进 cache 中,最后循环 cache,做出相应处理。
/**
* 发送邮件
*/
public class MAIL {
public void setMailTitle() {
System.out.println("设置邮件标题");
}
public void setMailBody() {
System.out.println("设置邮件内容");
}
public void setMailAttachment() {
System.out.println("设置邮件附件");
}
public void sendMail() {
System.out.println("发送邮件");
}
}
/**
*
*
*/
public class SMS {
public void setMsg() {
System.out.println("设置短信内容");
}
public void sendSms() {
System.out.println("发送短信");
}
}
import java.util.LinkedList;
import java.util.List;
/**
* 命令设计模式
* 中间件任务执行器
*/
public class JobExecuter {
final static private List<Object> caches = new LinkedList<>();
/**
* 执行中间件任务
*/
private static void executeJobs() {
for (Object cache : caches) {
if (cache instanceof MAIL) {
((MAIL) cache).setMailTitle();
((MAIL) cache).setMailBody();
((MAIL) cache).setMailAttachment();
((MAIL) cache).sendMail();
} else if (cache instanceof SMS) {
((SMS) cache).setMsg();
((SMS) cache).sendSms();
}
}
}
public static void main(String[] args) {
caches.add(new MAIL());
caches.add(new SMS());
executeJobs();
}
}
为了简明扼要的说明设计模式,我们省略了各种线程任务的创建执行代码。
任务已经顺利完成了,中间件能够根据任务类型自动判断不同的业务,并顺利完成需求。
遗憾的是,这样的设计拥有一些“重大缺陷”。
- 请求与执行的耦合度很高。假设需要再增加一个推送 app 消息的业务,那么你的代码很快就会陷入 if else 的海洋。
- 类没有对扩展开发,对修改关闭。当需求变化时我们仅仅能够通过修改代码来应对。
- executeJobs() 是一个典型的依赖具体对象,而不是接口或抽象的失败案例。
- 如果我们想要将发送给某1个用户的消息一起执行,应该怎么办?
好消息是,命令设计模式能够帮助我们解决上面的所有问题。
在学习命令设计模式之前,我们先试着用餐厅就餐这个日常生活中的行为,来接近命令设计模式。
当我们在餐厅就餐时,我们往往会先向服务员点餐,然后通过服务员把点好的餐点送往后厨,并通知后厨根据新订单准备餐点。
client=>start: 客户
waiter=>operation: 服务员
createOrders=>operation: 点餐(createOrders)
notifyChef=>operation: 通知后厨
chef=>end: 准备餐点
client->createOrders->waiter->notifyChef->chef
通过点餐创建的订单,屏蔽了不同的客户的不同需求。对于 waiter 来说,不需要知道订单的具体内容。只需要将客户的点餐订单传递给后厨,并通知后厨任务就算完成了。后厨通过订单内容,着手准备工作。
从餐厅到命令设计模式
将餐厅想象成 OO 设计模式的一种模型。客户就相当于 client、点餐的动作就相当于创建请求、服务员就相当于异步中间件、后厨则是真正 action 的执行者。
好了,让我们根据这样的思路重新设计之前的需求。
屏幕快照 2016-12-27 下午4.37.55.png-44.8kB
/**
* 命令设计模式
*/
public interface Command {
void execute();
}
/**
* 发送邮件
*/
public class MAIL implements Command {
public void setMailTitle() {
System.out.println("设置邮件标题");
}
public void setMailBody() {
System.out.println("设置邮件内容");
}
public void setMailAttachment() {
System.out.println("设置邮件附件");
}
public void sendMail() {
System.out.println("发送邮件");
}
@Override
public void execute() {
setMailAttachment();
setMailBody();
setMailTitle();
sendMail();
}
}
/**
* 命令设计模式
*/
public class SMS implements Command {
public void setMsg() {
System.out.println("设置短信内容");
}
public void sendSms() {
System.out.println("发送短信");
}
@Override
public void execute() {
setMsg();
sendSms();
}
}
import java.util.LinkedList;
import java.util.List;
/**
* 命令设计模式
*/
public class CommandInvoker {
private List<Command> caches = new LinkedList<>();
/**
* 设置Invoker所需要的命令
*
* @param e
*/
public void addCommand(Command e) {
caches.add(e);
}
/**
* 执行中间件任务
*/
public void executeJobs() {
for (Command cache : caches) {
cache.execute();
}
}
}
/**
* 命令客户端
*/
public class CommandClient {
public static void main(String[] args) {
CommandInvoker commandInvoker = new CommandInvoker();
Command commandMail = new MAIL();
Command commandSms = new SMS();
commandInvoker.addCommand(commandMail);
commandInvoker.addCommand(commandSms);
//TODO add command in futrue on pg run
commandInvoker.executeJobs();
}
}
设置邮件附件
设置邮件内容
设置邮件标题
发送邮件
设置短信内容
发送短信
问题回顾
总算完成了。最后再回顾一下之前出现的重大问题,看看是否这些问题都通过命令设计模式都得到了解决
请求与执行的耦合度很高。假设需要再增加一个推送 app
消息的业务,那么你的代码很快就会陷入 if else 的海洋。
通过将请求封装为命令,解耦了请求者与执行者。当有新需求时,新的请求只需要实现命令接口,就能被我们的代码所复用。
类没有对扩展开发,对修改关闭。当需求变化时我们仅仅能够通过修改代码来应对。
需求有变化时,通过 setCommand() / addCommand() 方法实现了对扩展开放,对修改关闭。
- executeJobs() 是一个典型的依赖具体对象,而不是接口或抽象的失败案例。
现在依赖的是Command接口对象。
如果我们想要将发送给某1个用户的消息一起执行,应该怎么办?
将所需要的任务组合成为一个新的 commandMacro 宏对象。宏对象持有一个 command 对象的数组。并在 execute 中调用数组中所有对象的 execute 方法,就能实现这样的需求。
总结
命令设计模式通过一个中间人/件将请求封装为统一的命令,达到了与命令的执行者之间实现解耦的目的。这样的设计使得中间人的代码尽可能的简单,并且能够与任意的对象组合。完整的命令设计模式还拥有 undo,撤销上一次命令的功能。
你可以试着自己来实现这个功能来加深对命令设计模式的理解。