Java命令模式以及来自lambda的优化
前言
设计模式是软件工程中一些问题的统一解决方案的模型,它的出现是为了解决一些普遍存在的,却不能被语言特性直接解决的问题,随着软件工程的发展,设计模式也会不断的进行更新,本文介绍的是经典设计模式-命令模式以及来自java8的lambda的对它的优化。
什么是命令模式
命令模式把一个请求或者操作封装到一个对象中。命令模式允许系统使用不同的请求把客户端参数化,对请求排
队或者记录请求日志,可以提供命令的撤销和恢复功能。 (摘自<大话设计模式>)
我不想把问题弄的特别复杂,我的理解是,命令模式就是对一段命令的封装,而命令,就是行为,行为在java语言里可以理解为方法,所以说命令模式就是对一些行为或者说是一些方法的封装。下面我举一个简单的例子来描述这种模式,然后再讲解它的特点。
例子
场景描述
有一家路边的小摊,做的小本经营,只有一个做饭的师傅,师傅饭做的还不错,可来吃饭的人们总是抱怨老板记性不好,有时候订单一多就给忘了,路人甲:"老板!来份牛肉饭!",老板:"好勒!马上给您做!",路人乙:"老板!来份啤酒鸭!",老板:"没问题!",路人丙:"老板!一份西红柿炒鸡蛋!",老板:"ok!",路人丁:"老板!两份啤酒鸭!",老板:"好的好的!"(内心:我晕,怎么有点记不过来了...)
基础实现
老板在这里同时扮演了做饭的角色 对于一个餐馆来说也就是厨房类
public class Kitchen {
public void beefRice(){
System.out.println("一份牛肉饭做好了!");
}
public void scrambledEggsWithTomatoes(){
System.out.println("一份西红柿炒鸡蛋做好了!");
}
public void beerDuck(){
System.out.println("一份啤酒鸭做完啦!");
}
}
客户端代码
public static void main(String[] args){
Kitchen boss = new Kitchen();
boss.beefRice();
boss.beefRice();
boss.beerDuck();
boss.beerDuck();
boss.beerDuck();
boss.beefRice();
}
代码十分的简单,可是通过观察发现,客户和厨房直接交互了,如果一旦请求多了起来,就如同上面老板说的,怎么感觉头有点晕那...从代码的角度来说,这样的耦合度也太高了,举个例子,如果现在有一个客户不想点了,那应该怎么办呢?取消订单代码写在Kitchen类里,显然不现实,写在客户端里,似乎又不太靠谱.... 再举一个例子,鸡蛋已经用完了,不能给客户提供西红柿炒鸡蛋了,那这个拒绝的代码又写在哪里呢?写在Kitchen?可以是可以,但是你把业务逻辑和基础领域模型混在一起真的好吗?写在客户端里?所有的逻辑都丢在客户端第一层显然是不明智的。
废话了半天,终于有一个顾客受不了了:"老板你这样也太累了吧!不如招个服务员给你记我们要点的饭的订单,记完了送到厨房给你,你按照这个订单做就行了,这样你全身心的投入做饭,这样能提高做饭的效率,也不会出现先点的顾客您忘记做等了半天嚷嚷着要退钱了!至于取消修改订单还是拒绝订单,都交给服务员去处理,处理好了送过来给您,这样各司其职,效率变高了,大家不是都开心嘛!"
那么很显然故事中的这个顾客就很有软件工程的天赋 😃
使用传统命令模式实现
命令模式,就是上文中的顾客所提出的建议,下面就是具体的实现类。
- 首先要添加服务员类,负责添加,拒绝,修改或者删除订单,或者增加订单的日志相关信息,其实非常简单,简单的增加逻辑即可,这里为了演示,只演示拒绝订单和输出日志
- 其次既然是命令模式,那肯定要有命令类,这里将厨房做饭的三个方法分别构造成三个命令对象,将他们的共同部分抽象成公共的抽象命令类,方便后面向上转型。
以下为代码
抽象命令类,包含一个执行命令的对象和方法,子类继承该类,对抽象的实现方法进行不同的实现
public abstract class BaseCommand {
protected Kitchen kitchen;
public BaseCommand(Kitchen kitchen) {
this.kitchen = kitchen;
}
public abstract void executeCommand();
}
下面是三个继承抽象命令类的具体命令,也就是将来客户端要添加的具体的独立的订单命令,结构完全一样
做牛肉饭命令类
public class beefRiceCommand extends BaseCommand {
public beefRiceCommand(Kitchen kitchen) {
super(kitchen);
}
@Override
public void executeCommand() {
kitchen.beefRice();
}
}
做啤酒鸭命令类
public class beerDuckCommand extends BaseCommand {
public beerDuckCommand(Kitchen kitchen) {
super(kitchen);
}
@Override
public void executeCommand() {
kitchen.beerDuck();
}
}
做西红柿炒鸡蛋命令类
public class EggsWithTomatoesCommand extends BaseCommand {
public EggsWithTomatoesCommand(Kitchen kitchen) {
super(kitchen);
}
@Override
public void executeCommand() {
kitchen.scrambledEggsWithTomatoes();
}
}
下面是服务员类,这里为了演示使用队列实现了增加订单与拒绝订单和添加日志,删除中间订单也很简单,数据结构换成linkedList或者arrayList即可,拒绝订单为了演示只是简单的通过类名来判断
public class Waiter {
/** 用于存储订单命令的队列 */
private final Queue<BaseCommand> orders ;
public Waiter() {
orders = new ArrayDeque<>();
}
/**
* 添加订单
* @param baseCommand 客户端传来的命令类,向上转型
*/
public final void setOrders(BaseCommand baseCommand){
if (baseCommand.getClass().getName().equals(EggsWithTomatoesCommand.class.getName())) {
System.out.println("啤酒鸭卖完了,换一个点点吧!");
} else {
String[] names = baseCommand.getClass().getName().split("\\.");
System.out.printf("添加订单: %s 订单时间: %s \n", names[names.length - 1], LocalDateTime.now());
orders.add(baseCommand);
}
}
/**
* 通知厨房开始做饭,遍历队列,做完了的订单移出队列
*/
public final void notifyKitchen(){
while (orders.peek() != null) {
orders.peek().executeCommand();
orders.remove();
}
}
}
客户端类
public class Client {
public static void main(String[] args) {
//准备厨房,服务员,菜单命令工作
Kitchen kitchen = new Kitchen();
Waiter waiter = new Waiter();
BaseCommand beefRiceCommand = new beefRiceCommand(kitchen);
BaseCommand beerDuckCommand = new beerDuckCommand(kitchen);
BaseCommand eggsWithTomatoesCommand = new EggsWithTomatoesCommand(kitchen);
//开始营业
System.out.println("=======================添加订单环节=======================");
// 顾客:服务员 一份牛肉饭!
waiter.setOrders(beefRiceCommand);
// 顾客:服务员 一份啤酒鸭!
waiter.setOrders(beerDuckCommand);
// 顾客:服务员 一份西红柿炒鸡蛋!
waiter.setOrders(eggsWithTomatoesCommand);
// 顾客:服务员 两份啤酒鸭!
waiter.setOrders(beerDuckCommand);
waiter.setOrders(beerDuckCommand);
System.out.println("==========服务员将订单送至厨房,厨房按照订单顺序开始做饭=========");
//服务员通知厨房按照订单顺序开始做
waiter.notifyKitchen();
}
}
运行结果
=======================添加订单环节=======================
添加订单: beefRiceCommand 订单时间: 2017-10-16T02:44:13.631
添加订单: beerDuckCommand 订单时间: 2017-10-16T02:44:13.650
啤酒鸭卖完了,换一个点点吧!
添加订单: beerDuckCommand 订单时间: 2017-10-16T02:44:13.650
添加订单: beerDuckCommand 订单时间: 2017-10-16T02:44:13.650
==========服务员将订单送至厨房,厨房按照订单顺序开始做饭=========
一份牛肉饭做好了!
一份啤酒鸭做完啦!
一份啤酒鸭做完啦!
一份啤酒鸭做完啦!
Process finished with exit code 0
总结与思考
总结
上面的例子应该并不难理解,这里列出命令模式的uml图<来源于《head first》>
命令模式涉及到五个角色,它们分别是:
- 客户端(Client)角色:具体执行程序的地方,创建一个具体命令(ConcreteCommand)对象并确定其接收者。
- 命令(Command)角色 : 声明了一个给所有具体命令类的抽象接口,在上面的例子中是BaseCommand类,这里的抽象接口只是一个概念,我使用抽象类来代替也是可以的。
- 具体命令(ConcreteCommand)角色 : 定义一个接收者和行为之间的弱耦合;实现execute()方法,负责调用接收者的相应操作。execute()方法通常叫做执行方法。在这里就是牛肉饭,啤酒鸭,番茄炒鸡蛋等命令....
- 请求者(Invoker)角色:负责调用命令对象执行请求,相关的方法叫做行动方法。在这里就是服务员,负责收集这些命令,从uml图也可以看出来他和command角色的关系是聚合关系
- 接收者(Receiver)角色:负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法叫做行动方法。在这里就是厨房了,其实我更喜欢称之为执行者,因为他是最终负责执行这些命令的人,当然反过来说,对于命令来说,他也是这些命令的接收者。
下面谈一谈命令模式的优缺点
优点
- 最大的作用是将请求者与执行者分割开,在上面的例子中就是将顾客和厨房给分开了,顾客负责向服务员点菜,服务员负责将订单交给厨房,厨房安心做饭,顾客安心点菜,这样分工明确。
- 在有需要的情况下,可以很方便的进行日志的记录,如上面的例子所示
- 对于客户端的请求,可以选择撤销请求与重新记录请求,也是十分方便的
- 可以十分容易的实现一个命令队列,例如上文所示
- 每一个命令类互不关联,添加新的命令类或者修改旧有的命令类十分容易
- 总结几点就是 解耦,复合命令,动态的控制,易于扩展
缺点
- 增加的类太多了,事实上很多设计模式都有这样的毛病,与其说是缺点,不如说是不可避免,因为程序语言特性的缺陷不得不用更多资源来变着法子完成,就拿上文的例子来说,最初的基本实现虽然问题很多,但是只有两个类,厨房类和客户端类,可使用命令模式完成了扩展之后,除开原先的两个类,新增了服务员类,抽象命令类,以及它下面具体的三个命令,那么假设这个厨房类可以做100道菜,难道我就要加100个子命令类???
思考
命令模式的优点是显而易见的,解耦复合易扩展动态控制,简直棒!可他的这个缺点似乎有时候也挺头疼的,那么具体的进行分析与思考,命令模式的优点几乎全部集中于这个服务员类,也就是invoke角色里,而剩下的1个抽象接口与它下面的n个子类只是为了将厨房类(receiver角色)里的每一个行为(方法)给抽象出来。问题来了,为什么要用类去包装这个行为才能抽象呢?事实上,行为抽象是函数式语言的特性之一,java此前并没有这个语言特性,所以没办法,只能用单独新增一个类来包裹这个行为来代替,可是这一点自从Java8出来之后就不一样了,Java8的函数式特性完全可以将命令模式的这一缺点给优化掉!因此,我们只需要保留服务员类,剩余的行为命令包装类使用尝试使用函数接口来代替,这样既保持了优点,又规避了缺点!
使用函数抽象行为进行优化
将这些做饭的命令抽象成函数接口,然后指定一个执行者,这样的接口在Java8 4大函数接口中属于Consumer接口,也就是消费者接口,下面就用Consumer接口进行行为抽象
厨房类依旧不变
public class Kitchen {
public void beefRice(){
System.out.println("一份牛肉饭做好了!");
}
public void scrambledEggsWithTomatoes(){
System.out.println("一份西红柿炒鸡蛋做好了!");
}
public void beerDuck(){
System.out.println("一份啤酒鸭做完啦!");
}
}
服务员类也很容易,原先装的是一个个命令对象,现在直接一点,直接将行为放进去
public class Waiter {
/**
* 此时队列装载的不再是命令对象了,而是更直接的厨房类的行为
*/
private final Queue<Consumer<Kitchen>> orders;
public Waiter() {
orders = new ArrayDeque<>();
}
/**
* 添加订单
* @param kitchenAction 厨房的具体行为
*/
public final void setOrders(Consumer<Kitchen> kitchenAction) {
System.out.printf("添加订单成功! 订单时间: %s \n", LocalDateTime.now());
orders.add(kitchenAction);
}
/**
* 这里增加一个执行者参数,来对队列中的行为进行操作
* @param kitchen 执行者,用于执行队列中的行为
*/
public final void notifyKitchen(Kitchen kitchen) {
while (orders.peek() != null) {
orders.peek().accept(kitchen);
orders.remove();
}
}
}
客户端代码,简单、清晰的惊人,代码自带注释效果,无论是简短性还是可阅读性,都比之前的要好上很多,中间的营业代码很直观,直接阅读代码就很清楚的看到添加了哪些行为到订单队里中
public class Client {
public static void main(String[] args) {
//准备厨房,服务员,菜单命令工作
Kitchen kitchen = new Kitchen();
Waiter waiter = new Waiter();
//开始营业
System.out.println("=======================添加订单环节=======================");
// 顾客:服务员 一份牛肉饭!
waiter.setOrders(Kitchen::beefRice);
// 顾客:服务员 一份啤酒鸭!
waiter.setOrders(Kitchen::beerDuck);
// 顾客:服务员 一份西红柿炒鸡蛋!
waiter.setOrders(Kitchen::scrambledEggsWithTomatoes);
// 顾客:服务员 两份啤酒鸭!
waiter.setOrders(Kitchen::beerDuck);
waiter.setOrders(Kitchen::beerDuck);
System.out.println("==========服务员将订单送至厨房,厨房按照订单顺序开始做饭=========");
//服务员通知厨房按照订单顺序开始做
waiter.notifyKitchen(kitchen);
}
}
输出结果
=======================添加订单环节=======================
添加订单成功! 订单时间: 2017-10-16T03:19:40.003
添加订单成功! 订单时间: 2017-10-16T03:19:40.019
添加订单成功! 订单时间: 2017-10-16T03:19:40.020
添加订单成功! 订单时间: 2017-10-16T03:19:40.020
添加订单成功! 订单时间: 2017-10-16T03:19:40.021
==========服务员将订单送至厨房,厨房按照订单顺序开始做饭=========
一份牛肉饭做好了!
一份啤酒鸭做完啦!
一份西红柿炒鸡蛋做好了!
一份啤酒鸭做完啦!
一份啤酒鸭做完啦!
Process finished with exit code 0
到这里,已经没有其他类的代码了!类的数量由原先的7个变成了3个,并且由于服务员类(Invoke角色)依然存在,原先的解耦复合控制扩展等优点,一个都没少,与此同时客户端的代码也清爽了不少。试想一下,假如现在厨房有100道做菜的方法,按照原先的方法实现的类的数量应该是3(客户端+厨房+服务员) + 1(抽象命令接口) + 100(具体命令接口) = 104个类,而采用lambda之后,依旧只需要三个类!并且原先的优点完全保留了下来。
函数补充优化(update 17.10.24)
在整理关于方法引用转换函数接口资料的时候忽然觉得命令模式优化这里似乎可以再稍微变动一下使得代码呈现链式(之前也有过类似的想法,但是没想到门路,今天想到了,就补充一下)
上文的代码其实已经成型了,这里做的优化是将多次调用设置成链式调用法。
链式调用一
只需要将waiter类的setOrders方法的返回值设置为this本身即可。
public final Waiter setOrders(Consumer<Kitchen> kitchenAction) {
System.out.printf("添加订单成功! 订单时间: %s \n", LocalDateTime.now());
orders.add(kitchenAction);
return this;
}
客户端链式调用
public class Client {
public static void main(String[] args) {
//准备厨房,服务员,菜单命令工作
Kitchen kitchen = new Kitchen();
Waiter waiter = new Waiter();
//开始营业
System.out.println("=======================添加订单环节=======================");
// 顾客:服务员 一份牛肉饭!
// 顾客:服务员 一份啤酒鸭!
// 顾客:服务员 一份西红柿炒鸡蛋!
// 顾客:服务员 两份啤酒鸭!
// 顾客:服务员 两份啤酒鸭!
//这里通过setOrders完成链式调用,和之前的效果完全一样,但是提高了可读性与间接性
waiter.setOrders(asConsumer(Kitchen::beefRice))
.setOrders(Kitchen::beerDuck)
.setOrders(Kitchen::scrambledEggsWithTomatoes)
.setOrders(Kitchen::beerDuck)
.setOrders(Kitchen::beerDuck);
System.out.println("==========服务员将订单送至厨房,厨房按照订单顺序开始做饭=========");
//服务员通知厨房按照订单顺序开始做
waiter.notifyKitchen(kitchen);
}
}
输出结果和上面一样,这里不再显示
链式调用二
这里是通过一个方法引用的包装器,然后通过function接口的andThen方法链式的讲订单存入订单队列中。
- 这里与上文的链式优化一的是有区别的,区别在于,上文中无论是普通的setOrders还是return this 链式的setOrders的 日志信息是会输出5条的(进行了5次的setOrders动作),而这里只会输出一条,原因是这里的一条动作里andThen了4个动作,这一条动作链作为一个整体传入了Setorders,因此只会有一条日志信息输出。
- 至于什么时候使用,根据实际情况,例如通知厨房做一道便当快餐,而便当快餐假设是由一份牛肉饭+一份啤酒鸭构成的,那么在这里这一个订单就可以使用这样的andThen来进行构造,这样的链式组合其实是有序的,其实这就是建造者模式的lambda表示形式。
- 使用这样的方法,你的设计里就是包含了命令模式+建造者模式,以lambda形式表现出来,当然如果不按照顺序的话,这样的模式你还可以理解成组合的形式。
lambda的方法引用固然使得代码清晰可见,但是坏处是一旦使用了方法引用,就无法进行lambda的链式调用,也就是无法使用andThen这样的链式方法,但是我们通过一个中转站,先将方法引用转换成函数接口,再链式调用,因为方法引用本质上是lambda的语法糖,下面是转换方法,十分简易。
public class FunctionCastUtil {
public static <T> Consumer<T> asConsumer(Consumer<T> consumer) {
return consumer;
}
}
客户端代码
import static com.lambda.functionutils.FunctionCastUtil.*;
public class Client {
public static void main(String[] args) {
//准备厨房,服务员,菜单命令工作
Kitchen kitchen = new Kitchen();
Waiter waiter = new Waiter();
//开始营业
System.out.println("=======================添加订单环节=======================");
// 顾客:服务员 一份牛肉饭!
// 顾客:服务员 一份啤酒鸭!
// 顾客:服务员 一份西红柿炒鸡蛋!
// 顾客:服务员 两份啤酒鸭!
// 顾客:服务员 两份啤酒鸭!
//这里使用anThen完成链式调用,注意这里是只有一条订单,所以要注意使用的场合
waiter.setOrders(asConsumer(Kitchen::beefRice)
.andThen(Kitchen::beerDuck)
.andThen(Kitchen::scrambledEggsWithTomatoes)
.andThen(Kitchen::beerDuck)
.andThen(Kitchen::beerDuck));
System.out.println("==========服务员将订单送至厨房,厨房按照订单顺序开始做饭=========");
//服务员通知厨房按照订单顺序开始做
waiter.notifyKitchen(kitchen);
}
}
因为输出结果不同,所以这里贴一下输出结果,根据输出结果就明白这里的订单只记录了一条,这一条里面包含了要做多种菜,在厨房那里实际完成的结果是一样的,大家可以根据不同的需要使用不同的链式优化
=======================添加订单环节=======================
添加订单成功! 订单时间: 2017-10-24T07:01:59.444
==========服务员将订单送至厨房,厨房按照订单顺序开始做饭=========
一份牛肉饭做好了!
一份啤酒鸭做完啦!
一份西红柿炒鸡蛋做好了!
一份啤酒鸭做完啦!
一份啤酒鸭做完啦!
Process finished with exit code 0
结尾
使用lambda优化之后的命令模式在保证优点的同时极大的减少了代码量,简直完美。这就是语言特性所带来的力量,简单的几处修改就能获得如此多的受益,这也是我为什么这么喜欢lambda的原因。
关于本文代码
本文的代码与md文章同步更新在github中的command-mode模块下,欢迎fork _