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的函数式特性完全可以将命令模式的这一缺点给优化掉!因此,我们只需要保留服务员类,剩余的行为命令包装类使用尝试使用函数接口来代替,这样既保持了优点,又规避了缺点!