23.烤羊肉串引来的思考 — 命令模式(Command Pattern)

23.1 吃烤羊肉串!

  • 时间: 6月23日17点   地点: 小区门口   人物: 小菜, 大鸟

     “小菜, 肚子饿了, 走, 我请你去吃羊肉串.”

     “好呀, 小区门口的那新疆人烤的就很不错的.”
     小菜和 大鸟来到了小区门口.
     “啊, 这么多人, 都围了十几个了.” 小菜感叹道.
     “现在读大学, 进公司, 做白领, 其实未必有人家烤羊肉串的挣得多.”
     “这是两回事, 人家也是很辛苦的”
     “…..”
     旁边等着拿肉的人七嘴八舌的叫开了. 场面有些混乱, 由于人实在太多, 烤羊肉串的老板已经分不清谁是谁了, 造成分发错误, 收钱错误, 烤肉质量不过关等等.
     “小菜, 我们换一家吧, 这里实在是太混乱了, 过去不远有一家烤肉店是有店面的.”
     “恩, 他这样子生意是做不好. 咱们去那一家吧.”

  • 时间: 6月23日18点   地点: 烤肉店   人物: 小菜, 大鸟

     小菜和大鸟走到了那家烤肉店.
     “服务员, 我们要十串羊肉串, 两串鸡翅, 两瓶啤酒.” 大鸟根本没有看菜单.
     “鸡翅没有了, 你点点别的烧烤吧.” 服务员答道.
     “那来四串牛板筋, 烤肉要辣的.” 大鸟很轻车熟路.
     “大鸟常来这里吃吗? 很熟悉嘛!” 小菜问道.
     “太熟悉了, 这年头, 单身在外混, 哪有不熟悉家门口附近的吃饭的地儿. 不然每天晚上的肚皮问题怎么解决?”
     “你说, 在外面打游击烤羊肉串和这种开门店做烤肉, 哪个更赚钱?” 小菜问道.
     “哈, 这很难讲, 毕竟各有各的好, 在外面打游击, 好处是不用租房, 不用上税, 最多就是交点儿保护费, 但下雨天不行, 大白天不行, 太晚也不行, 一般都是傍晚几个钟头, 顾客也不固定, 像刚才那个, 由于人多造成混乱, 于是就放跑了我们这两条大鱼, 其实他的生意是不稳定的.”
     “大白天不行? 太晚不行?”
     “大白天, 城管没下班呢, 怎能容他如此安逸. 超过晚上11点, 夜深人静, 谁还愿意在路边吃烤肉. 单开门店就不一样了, 不管什么时间做生意, 由于环境相对较好, 所以固定的顾客就多, 看似房租好像是交出去了, 但其实由于顾客多, 而且是正经做生意, 所以最终可以赚到大钱.”
     “大鸟研究得很透嘛!”
     “其实这门店好过马路游击队, 还可以对应一个很重要的设计模式呢!”
     “哦, 此话怎讲?”

23.2 烧烤摊 vs. 烤肉店

     “你在回忆刚才我们在小区门口烤肉摊看到的情景.”
     “因为要吃烤肉的人太多, 都希望能最快吃到肉串, 烤肉老板一个人, 所以有些混乱.”
     “还不止这些, 老板一个人, 来的人一多, 他就未必记得住谁交没交过钱, 要几串, 需不需要放辣椒等等.”
     “是呀, 大家都站在这里, 没什么事, 于是都盯着烤肉去了, 那一串多, 那一串少, 那一串烤的好, 那一串烤的焦都看的清清楚楚, 于是挑剔也就接踵而至.”
     “这其实就是我们在编程中说的什么?”
     “我想想, 你是想说 ‘紧耦合’?”
     “哈, 不错, 不枉我的精心栽培.”
     “由于客户和烤羊肉串老板的 ‘紧耦合’ 所以是的容易出错, 容易混乱, 也容易挑剔.”
     “说得对, 这其实就是 ‘行为请求者’ 与 ‘行为实现者’ 的紧耦合. 我们需要记录那个人要几串羊肉串, 有没有特殊要求(放辣不放辣), 付没付过钱, 谁先谁后, 这其实都相当于请求做什么?”
     “对请求做记录, 啊, 应该是做日志.”
     “很好, 那么如果有人需要退回请求, 或者要求烤肉重考, 这其实就是?”
     “相当于撤销和重做吧.”
     “OK, 所以对请求排队或记录请求日志, 以及支持可撤销的操作等行为, ‘行为请求者’ 与 ‘行为实现者’ 的紧耦合是不太适合的. 你说怎么办?”
     “开家门店.”
     “哈, 这是最终结果, 不是这个意思, 我们是烤肉请求者, 烤肉的师傅是烤肉的的实现者, 对于开门店来说, 我们用得着去看烤肉的实现过程吗? 现实是怎么做的呢?”
     “哦, 我明白你的意思了, 我们不用去认识烤肉者是谁, 连他的面都不用见到, 我们只需要给接待我们的服务员说我们要什么就可以了. 他可以记录请求, 然后在有他去通知烤肉师傅做.”
     “而且, 由于我们所做的请求, 其实也就是我们点肉的订单, 上面与很详细的我们的要求, 所有的客户都有着一份订单, 烤肉师傅可以按照先后顺序操作 不会混乱, 也不会遗忘.”
     “收钱的时候, 也不会多收或少收.”
     “优点还不止这些, 比如说,” 大鸟突然大声叫道, “服务员, 我们那十串羊肉串太多了, 改成六串就可以了.”
     “好的!” 服务员答道.
     大鸟接着说: “你注意看他接下来接着做什么了”
     “他好像在一个小本子上划了一下, 然后去通知烤肉师傅了.”
     “对呀, 这其实是在做撤销行为的操作. 由于有了记录, 所以最终算账还是不会错的.”
     “对对对, 这里用一个服务员来解除客户和烤肉师傅的处理好处真的是很多.”
     “好了, 这里有纸和笔, 你把刚才的想法写成代码吧?”
     “啊, 在这?”
     “这才叫让编程融入生活. 来吧, 不写出来, 你是不能完全理解的.”
     “好吧, 我试试看.”

23.3 紧耦合设计

     边吃着烤肉串, 编写代码, 小才完成了第一版.
     代码结构图



     路边烤羊肉串的实现

1
2
3
4
5
6
7
8
9
10
11
// 烤羊肉串者
public class Barbecuer {
// 烤羊肉
public void backMutton() {
System.out.println("烤羊肉串!");
}
// 烤鸡翅
public void backChickenWing() {
System.out.println("烤鸡翅!");
}
}

     客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Barbecuer boy = new Barbecuer();
// 客户端程序与 '烤肉串者' 紧耦合, 尽管简单
// 但却极为僵化, 有许许多多的隐患
boy.backMutton();
boy.backMutton();
boy.backMutton();
boy.backChickenWing();
boy.backMutton();
boy.backMutton();
boy.backChickenWing();
}

     “很好, 这就是路边烤肉的对应, 如果用户多了, 请求多了, 就容易乱了. 那你在尝试用门店的方式来实现它.”
     “我知道一定要增加服务员类, 但怎么做有些不太明白.”
     “恩, 这里的却是难点, 要知道, 不管是烤羊肉串, 还是烤鸡翅, 还是其他烧烤, 这些都是 ‘烤肉串者类’ 的行为, 也就是他的方法, 具体怎么做都是有方法内部来实现的, 我们不用去管它. 但是由于 ‘服务员’ 类来说, 他其实就是根据用户的需要, 发个命令, 说: ‘有人要是个羊肉串, 有人要两个鸡翅’, 这些都是命令….”
     “我明白了, 你的意思是, 把 ‘烤肉串者’ 类当中的方法, 分别写成多个命令类, 那么他们就可以被服务员类请求了?”
     “是的, 说的没错, 这些命令其实差不多都是同一个样式, 于是你就可以放画出一个抽象类, 让 ‘服务员’ 只管对抽象类的 ‘命令’ 发号施令就可以了. 具体是什么命令, 即是烤什么, 有客户来决定吧.”
     “我大概明白了.”

23.4 松耦合的设计

     接着, 小菜经过思考, 把第二个版本的代码写了出来.
     代码结构图



     抽象命令类

1
2
3
4
5
6
7
8
9
10
11
12
//抽象命令
public abstract class Command {
protected Barbecuer receiver;
// 抽象命令类, 只需要确定 '烤肉串者' 是谁
public Command(Barbecuer receiver) {
this.receiver = receiver;
}
// 执行命令
abstract public void executeCommand();
}

     具体命令类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 烤羊肉命令
public class BackMuttonCommand extends Command {
public BackMuttonCommand(Barbecuer receiver) {
super(receiver);
}
// 具体命令类, 执行命令时, 执行具体行为
@Override
public void executeCommand() {
receiver.backMutton();
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}
// 烤鸡翅命令
public class BackChickenWingCommand extends Command {
public BackChickenWingCommand(Barbecuer receiver) {
super(receiver);
}
@Override
public void executeCommand() {
receiver.backChickenWing();
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}

     服务员类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 服务员类
public class Waiter {
private Command command;
// 设置订单
public void setOrder(Command command) {
// 不管用户想要什么烤肉, 反正都是 '命令'
// 只管记录订单, 然后通知 '烤肉串者' 执行即可
this.command = command;
}
// 通知执行
public void notifyOrder() {
command.executeCommand();
}
}

     烤肉者类与之前相同, 略
     客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// 开店前的准备
Barbecuer boy = new Barbecuer();
Command backMuttonCommand1 = new BackMuttonCommand(boy);
Command backMuttonCommand2 = new BackMuttonCommand(boy);
Command backChickenWindCommand1 = new BackChickenWingCommand(boy);
Waiter girl = new Waiter();
// 开门营业
girl.setOrder(backMuttonCommand1);
girl.notifyOrder();
girl.setOrder(backMuttonCommand2);
girl.notifyOrder();
girl.setOrder(backChickenWindCommand1);
girl.notifyOrder();
}

     “大鸟, 我这样写如何?”
     “很好很好, 基本把代码都实现了. 但有几个问题, 第一, 真实的情况其实并不是用户点一个菜, 服务员就通知厨房去做一个, 那样不科学, 应该是点完烤肉后, 服务员依次通知制作; 第二, 如果此时鸡翅没了, 不应该是客户来判断是否还有, 客户哪知道有没有呀, 应该是服务员或烤肉串者来否决这个请求; 第三, 客户到地点了哪些烧烤或饮料, 这是需要记录日志的, 以备收费, 也包括后期的统计; 第四, 客户完全有可能因为点的肉串太多而烤炉取消一些还没有制作的肉串. 这些问题都需要解决.”
     “你说的这些好像现在都不难办到了, 你看着…”

23.5 松耦合后

     小才开始了第三版的代码编写.
     服务员类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 服务员类
public class Waiter {
// 增加存放具体命令的容器
private List<Command> orders = new LinkedList<>();
// 设置订单
public void setOrder(Command command) {
// 在客户提出请求时, 对没货的烧烤进行拒绝
if (Objects.equals(command.toString(), BackChickenWingCommand.class.getSimpleName())) {
System.out.println("服务员: 鸡翅没有了, 请点别的烧烤");
} else {
orders.add(command);
// 记录客户所点的烧烤的日志,
// 以备算账收钱
System.out.println("增加订单: " + command.toString() +
"时间: " + new Date().toString());
}
}
// 取消订单
public void cancelOrder(Command command) {
orders.remove(command);
System.out.println("取消订单: " + command.toString() +
"时间: " + new Date().toString());
}
// 通知全部执行
public void notifyOrder() {
for (Command order : orders) {
order.executeCommand();
}
}
}

     客户端代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// 开店前的准备
Barbecuer boy = new Barbecuer();
Command backMuttonCommand1 = new BackMuttonCommand(boy);
Command backMuttonCommand2 = new BackMuttonCommand(boy);
Command backChickenWindCommand1 = new BackChickenWingCommand(boy);
Waiter girl = new Waiter();
// 开门营业
girl.setOrder(backMuttonCommand1);
girl.setOrder(backMuttonCommand2);
girl.setOrder(backChickenWindCommand1);
// 点菜完毕, 一次通知厨房
girl.notifyOrder();
}

     执行结果

1
2
3
4
5
增加订单: BackMuttonCommand时间: Sun Feb 12 15:18:37 CST 2017
增加订单: BackMuttonCommand时间: Sun Feb 12 15:18:37 CST 2017
服务员: 鸡翅没有了, 请点别的烧烤
烤羊肉串!
烤羊肉串!

     “哈, 这就比较完整了.” 大鸟满意地点点头.
     “你还没有说这是什么设计模式呢.”
     “哈, 你猜也应该猜得出, 你的那个抽象类叫什么?”
     “命令, 哦, 这就是大名鼎鼎的命令模式啊.”

23.6 命令模式

命令模式(Command), 将一个请求封装成一个对象, 从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志, 以及支持可撤销操作.[DP]



     Command 类, 用来声明执行操作的接口.

1
2
3
4
5
6
7
8
9
public abstract class Command {
protected Receiver receiver;
public Command(Receiver receiver) {
this.receiver = receiver;
}
abstract public void executeCommand();
}

     ConcreteCommand 类, 将一个接受者对象绑定于一个动作, 调用接受者的相应操作, 以实现 execute() .

1
2
3
4
5
6
7
8
9
10
public class ConcreteCommand extends Command {
public ConcreteCommand(Receiver receiver) {
super(receiver);
}
@Override
public void executeCommand() {
receiver.action();
}
}

     Invoker 类, 要求该命令执行这个请求

1
2
3
4
5
6
7
8
9
10
11
public class Invoker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void executeCommand() {
command.executeCommand();
}
}

     Receiver 类, 知道如何实施与执行一个与请求相关的操作, 任何类都可能作为一个接受者

1
2
3
4
5
public class Receiver {
public void action() {
System.out.println("执行请求");
}
}

     客户端代码, 创建一个具体命令对象并设定他的接受者.

1
2
3
4
5
6
7
public static void main(String[] args) {
Receiver r = new Receiver();
Command c = new ConcreteCommand(r);
Invoker i = new Invoker();
i.setCommand(c);
i.executeCommand();
}

23.7 命令模式的作用

     “来来来, 小菜你来总结一下命令模式的优点.”
     “我觉得第一, 他能较容易的设计一个命令队列; 第二, 在需要的情况下, 可以较容易地将命令计入日志; 第三, 允许接受请求的一方决定是否否决请求.
     “还有就是第四, 可以容易的实现对请求的撤销和重做; 第五, 由于加进新的具体命令类不影响其他的类, 因此增加新的具体命令类很容易. 其实还有最关键的优点就是命令模式把请求一个操作的对象, 和知道怎么执行一个操作的对象分开.[DP] “ 大鸟接着总结说.
     “但是是否碰到类似的情况就一定要使用命令模式呢?”
     “这就不一定了, 比如命令模式支持撤销/回复操作功能, 但你还不清楚是否需要这个功能时, 你要不要实现命令模式?”
     “要, 万一以后需要就不好办了.”
     “其实应该是不要实现. 敏捷开发原则告诉我们, 不要为代码添加基于猜测, 实际不需要的功能. 如果不清楚一个系统是否需要命令模式, 一般就不要着急去实现它, 事实上, 在需要的时候通过重构实现这个模式并不困难, 只有在真正需要如撤销/恢复操作等功能时, 把原来的代码重构为命令模式才有意义.[R2P]
     “明白. 这一顿我请了.” 小菜很开心, 大叫了一声, “服务员, 埋单.”
     “先生, 你们一共吃了28元.” 服务员递过来一个收费单.
     小菜正准备付钱.
     “慢!” 大鸟按住小菜的手, “不对呀, 我们没有吃10串羊肉串, 后来改成6串了. 应该是24元.”
     服务员去查了查账本, 回来很抱歉地说, “真对不起, 我们算错了, 应该是24元.”
     “小菜, 你看到了吧, 如果不是服务员做了记录, 也就是日志, 单就烤肉串的人, 哪记得住烤了多少串, 后果就是大家都说不清楚了.”
     “还是大鸟精明啊.”

~感谢捧场,您的支持将鼓励我继续创作~