命令模式

什么是命令模式

命令模式是将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

命令模式可以用来解决什么问题

在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

命令模式的结构

基础结构

  • 发布者 invoker(发出命令,调用命令对象,不知道如何执行与谁执行);
  • 接收者 receiver (提供对应接口处理请求,不知道谁发起请求);
  • 命令对象 command(接收命令,调用接收者对应接口处理发布者的请求)。

image.png
发布者 invoker 和接收者 receiver 各自独立,将请求封装成命令对象 command ,请求的具体执行由命令对象 command 调用接收者 receiver 对应接口执行。

代码实现

  1. // 接收者类
  2. class Receiver {
  3. execute(action) {
  4. console.log(`接收者执行命令:${action}`)
  5. }
  6. }
  7. // 命令对象
  8. class Command {
  9. constructor(receiver) {
  10. this.receiver = receiver
  11. }
  12. execute (action) {
  13. console.log(`命令对象接收到命令:${action}`);
  14. this.receiver.execute(action)
  15. }
  16. }
  17. // 发布者
  18. class Invoker {
  19. constructor(command) {
  20. this.command = command
  21. }
  22. invoke(action) {
  23. console.log(`发布者发布命令:${action}`)
  24. this.command.execute(action)
  25. }
  26. }
  27. // 仓库
  28. const warehouse = new Receiver();
  29. // 订单
  30. const order = new Command(warehouse);
  31. // 客户
  32. const client = new Invoker(order);
  33. client.invoke('生产一张书桌')

image.png

命令对象 command 充当发布者 invoker 与接收者 receiver 之间的连接桥梁(中间对象介入)。实现发布者与接收之间的解耦,对比过程化请求调用,命令对象 command 拥有更长的生命周期,接收者 receiver 属性方法被封装在命令对象 command 属性中,使得程序执行时可任意时刻调用接收者对象 receiver 。因此 command 可对请求进行进一步管控处理,如实现延时、预定、排队、撤销等功能。

  1. // 接收者类
  2. class Receiver {
  3. execute(action) {
  4. console.log(`接收者执行命令: 生产${action}`);
  5. switch (action) {
  6. case "书桌":
  7. this.outputDesk();
  8. break;
  9. case "椅子":
  10. this.outputChair();
  11. break;
  12. default:
  13. console.log(`工厂没有生产${action}的能力,请确认订单`);
  14. break;
  15. }
  16. }
  17. // 生产书桌的方法
  18. outputDesk() {
  19. setTimeout(() => {
  20. console.log('生产了一张书桌');
  21. console.log('书桌已经生产好了,可以交给客户了');
  22. }, 1000);
  23. }
  24. // 生产椅子的方法
  25. outputChair() {
  26. setTimeout(() => {
  27. console.log('生产了一把椅子');
  28. console.log('椅子已经生产好了,可以交给客户了');
  29. }, 1000);
  30. }
  31. }
  32. // 命令对象
  33. class Command {
  34. constructor(receiver) {
  35. this.receiver = receiver;
  36. }
  37. execute(action) {
  38. console.log(`命令对象接收到命令:${action}`);
  39. this.receiver.execute(action)
  40. }
  41. }
  42. class DeskCommand extends Command {
  43. constructor(receiver) {
  44. super(receiver);
  45. }
  46. execute() {
  47. console.log(`接到一张书桌的订单,给工厂下命令生产一张书桌`);
  48. this.receiver.execute('书桌');
  49. }
  50. }
  51. class ChairCommand extends Command {
  52. constructor(receiver) {
  53. super(receiver);
  54. }
  55. execute() {
  56. console.log(`接到一把椅子的订单,给工厂下命令生产一把椅子`);
  57. this.receiver.execute('椅子');
  58. }
  59. }
  60. // 发布者
  61. class Invoker {
  62. constructor() {
  63. this.orderList = [];
  64. }
  65. // 下订单
  66. setOrder(orderType, order) {
  67. console.log(`接到一个订单:${orderType}`)
  68. this.orderList.push(order);
  69. }
  70. // 取消订单
  71. cancelOrder(orderType, order) {
  72. let index = this.orderList.indexOf(order);
  73. this.orderList.splice(index, 1);
  74. console.log(`取消一个订单:${orderType}`);
  75. }
  76. // 发布命令
  77. invoke() {
  78. console.log(`发布者发布命令,给工厂下订单`)
  79. this.orderList.forEach((item) => { item.execute(); });
  80. }
  81. }
  82. // 仓库
  83. const warehouse = new Receiver();
  84. // 订单
  85. const desk = new DeskCommand(warehouse);
  86. const chair = new ChairCommand(warehouse);
  87. // 客户
  88. const client = new Invoker();
  89. client.setOrder("书桌", desk);
  90. client.setOrder("书桌", desk);
  91. client.setOrder("书桌", desk);
  92. client.setOrder("书桌", desk);
  93. client.setOrder("椅子", chair);
  94. client.setOrder("椅子", chair);
  95. client.setOrder("椅子", chair);
  96. client.setOrder("椅子", chair);
  97. client.setOrder("椅子", chair);
  98. client.setOrder("椅子", chair);
  99. client.cancelOrder("书桌", desk);
  100. setTimeout(() => {
  101. client.invoke();
  102. }, 1000);

完整结构

Command:定义命令的接口,声明执行的方法。

ConcreteCommand:命令接口实现对象,是”虚”的实现。
通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。

Receiver:接收者,真正执行命令的对象。
任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。

Invoker:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。
这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。

Client:创建具体的命令对象,并且设置命令对象的接收者。
注意这个不是我们常规意义上的客户端,而是在组装命令对象和接收者,或许,把这个 Client 称为装配者会更好理解,因为真正使用命令的客户端是从 Invoker 来触发执行。

代码实现

  1. // 烧烤店的实现
  2. // Receiver 接收者类 厨师,拥有烤羊肉串,烤鸡翅等能力
  3. class Cooker {
  4. execute(action) {
  5. console.log(`厨房收到订单: 开始${action}`)
  6. switch (action) {
  7. case "烤羊肉串":
  8. this.bakeMutton();
  9. break;
  10. case "烤鸡翅":
  11. this.bakeChickenWing();
  12. break;
  13. default:
  14. console.log(`厨房没有${action}了,请点其他的菜`);
  15. break;
  16. }
  17. }
  18. // 烤羊肉串
  19. bakeMutton() {
  20. setTimeout(() => {
  21. console.log('羊肉串烤好了,可以给客人上菜了');
  22. }, 500);
  23. }
  24. // 烤鸡翅
  25. bakeChickenWing() {
  26. setTimeout(() => {
  27. console.log('鸡翅烤好了,可以给客人上菜了');
  28. }, 1000);
  29. }
  30. }
  31. // command 命令对象类
  32. class Command {
  33. constructor(receiver) {
  34. this.receiver = receiver;
  35. }
  36. execute(action) {
  37. this.receiver.execute(action);
  38. }
  39. }
  40. // ConcreteCommand
  41. // 烤羊肉串的命令
  42. class BakeMuttonCommand extends Command {
  43. constructor(receiver) {
  44. super(receiver);
  45. }
  46. execute() {
  47. this.receiver.execute('烤羊肉串');
  48. }
  49. }
  50. // 烤牛肉串的命令
  51. class BakeBeefCommand extends Command {
  52. constructor(receiver) {
  53. super(receiver);
  54. }
  55. execute() {
  56. this.receiver.execute('烤牛肉串');
  57. }
  58. }
  59. // 烤鸡翅的命令
  60. class BakeChickenWingCommand extends Command {
  61. constructor(receiver) {
  62. super(receiver);
  63. }
  64. execute() {
  65. this.receiver.execute('烤鸡翅');
  66. }
  67. }
  68. // Invoker 发布者 服务员
  69. class Invoker {
  70. constructor() {
  71. this.orders = [];
  72. }
  73. // 添加菜品
  74. setOrder(type, command) {
  75. this.orders.push(command);
  76. console.log(`${Date.now()} 下单 ${type}`);
  77. }
  78. // 取消菜品
  79. cancelOrder(type, command) {
  80. let index = this.orders.indexOf(command);
  81. this.orders.splice(index, 1);
  82. console.log(`${Date.now()} 取消下单 ${type}`)
  83. }
  84. // 向后厨下单
  85. notify() {
  86. console.log('向后厨下单');
  87. this.orders.forEach(element => {
  88. element.execute();
  89. });
  90. }
  91. }
  92. let cooker = new Cooker();
  93. let bakeMuttonCommand = new BakeMuttonCommand(cooker);
  94. let bakeBeefCommand = new BakeBeefCommand(cooker);
  95. let bakeChickenWingCommand = new BakeChickenWingCommand(cooker);
  96. let waiter = new Invoker();
  97. waiter.setOrder('烤羊肉串', bakeMuttonCommand);
  98. waiter.setOrder('烤羊肉串', bakeMuttonCommand);
  99. waiter.setOrder('烤牛肉串', bakeBeefCommand);
  100. waiter.setOrder('烤鸡翅', bakeChickenWingCommand);
  101. waiter.cancelOrder('烤羊肉串', bakeMuttonCommand);
  102. setTimeout(() => {
  103. waiter.notify();
  104. }, 1000);

命令模式例子——菜单命令

现在页面中有三个button元素和一个菜单程序界面,这三个button元素的作用是:当用户点击时,他们分别会执行“刷新菜单”、“添加子菜单”和“删除子菜单”这三个功能。如果由一个程序员来完成这个功能就非常的简单了,因为他非常清楚那个按钮对应那个功能。但是如果实在一个分工很细致的团队里就不是这样了,例如一个人负责写html和css样式布局,另一个人负责写js,他们两个人同时进行各自的工作。在这种情况下,负责写js的那个人就很难确定哪个按钮对应哪个功能了。我们回想一下命令模式的使用场景:

有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

我们发现这种情况非常符合使用命令模式。
HTML按钮结构:

  1. <button class="ref">刷新菜单</button>
  2. <button class="add">添加子菜单</button>
  3. <button class="del">删除子菜单</button>

首先我们对功能进行封装,将三个功能分别封装到Menu和SubMenu两个对象中:

  1. //菜单对象
  2. var Menu = {
  3. refresh: function(){
  4. console.log("刷新菜单");
  5. }
  6. };
  7. //子菜单
  8. var SubMenu = {
  9. add: function(){
  10. console.log('增加子菜单');
  11. },
  12. del: function(){
  13. console.log('删除子菜单');
  14. }
  15. };

然后封装三个功能调用的命令:

  1. //封装刷新菜单命令
  2. var refreshMenuCommand = function(receiver){
  3. this.receiver = receiver;
  4. }
  5. refreshMenuCommand.prototype.execute = function(){
  6. this.receiver.refresh();
  7. }
  8. //封装添加子菜单命令
  9. var addSubMenuCommand = function(receiver){
  10. this.receiver = receiver;
  11. }
  12. addSubMenuCommand.prototype.execute = function(){
  13. this.receiver.add();
  14. }
  15. //封装删除子菜单命令
  16. var delSubMenuCommand = function(receiver){
  17. this.receiver = receiver;
  18. }
  19. delSubMenuCommand.prototype.execute = function(){
  20. this.receiver.del();
  21. }

在然后是封装设置命令函数:

  1. //设置命令函数
  2. function setCommand(btn, command){
  3. btn.addEventListener("click", function(){
  4. command.execute();
  5. })
  6. };

最后是客户端(client)的调用:

  1. //client客户调用
  2. var refreshCommand = new refreshMenuCommand(Menu);
  3. var addCommand = new addSubMenuCommand(SubMenu);
  4. var delCommand = new delSubMenuCommand(SubMenu);
  5. var btn = document.querySelectorAll("button");
  6. setCommand(btn[0], refreshCommand);
  7. setCommand(btn[1], addCommand);
  8. setCommand(btn[2], delCommand);

JavaScript中的命令模式:

大家细看上面菜单的例子,会发现实现一个这么简单的功能,竟然弄得代码这么复杂难懂。即使不用什么模式,用下面几行代码就可以实现相同的功能:

  1. //菜单对象
  2. var Menu = {
  3. refresh: function(){
  4. console.log("刷新菜单");
  5. }
  6. };
  7. //子菜单
  8. var SubMenu = {
  9. add: function(){
  10. console.log('增加子菜单');
  11. },
  12. del: function(){
  13. console.log('删除子菜单');
  14. }
  15. };
  16. //事件绑定函数
  17. function addEvent(dom, fn, Capture){
  18. dom.addEventListener("click", fn, !!Capture);
  19. };
  20. var btn = document.querySelectorAll("button");
  21. addEvent(btn[0], Menu.refresh);
  22. addEvent(btn[1], SubMenu.add);
  23. addEvent(btn[2], SubMenu.del);

这就是JavaScript语言中的命令模式,在JavaScript语言中函数是一等对象,它可以作为一个参数传递到函数内部去执行。所以在JavaScript这门语言中,命令模式和策略模式一样是JavaScript这门语言的天赋(生来即具有的属性)或者是隐性模式。命令模式其实就是回调函数一个面向对象的替代品,在JavaScript中命令模式和策略模式一样依赖回调函数实现,使用起来也更简单、更便捷。但有些时候这会成为一种缺点,因为他无法执行撤销操作,所以在实现撤销操作时,我们最好还是使用命令对象的execute方法为好。

撤销操作的实例

现在页面中有一个元素,有两个按钮,其中一个按钮点击时元素会往右移动一段距离,另一个按钮是撤销上一个移动操作。用命令模式实现这个功能,代码如下:
HTML结构:

  1. <div class="demo">
  2. <button class="move">移动</button>
  3. <button class="undo">撤销</button>
  4. <div class="target" style="left:0"></div>
  5. </div>

CSS样式:

  1. .demo{
  2. width:100%;
  3. height:100px;
  4. position:relative;
  5. }
  6. .target{
  7. width:50px;
  8. height:50px;
  9. position:absolute;
  10. bottom:0;
  11. background-color:red;
  12. }

js代码:

  1. //移动对象
  2. var Animate = function(dom){
  3. this.dom = dom;
  4. var self = this;
  5. //移动函数
  6. this.move = function(){
  7. var left = parseInt(self.dom.style.left);
  8. self.dom.style.left = left + 10 + 'px';
  9. };
  10. //取消移动函数
  11. this.undo = function(){
  12. var left = parseInt(self.dom.style.left);
  13. self.dom.style.left = left - 10 + 'px';
  14. };
  15. };
  16. //命令对象
  17. var Command = function(receiver){
  18. var self = this;
  19. this.receiver = receiver;
  20. this.count = 0; //记录执行命令的次数
  21. //执行命令
  22. this.execute = function(){
  23. self.receiver.move();
  24. self.count+= 1;
  25. };
  26. //撤销命令
  27. this.unexecute = function(){
  28. if(self.count === 0) return;
  29. self.receiver.undo();
  30. self.count-= 1;
  31. }
  32. };
  33. //设置命令函数
  34. function addEvent(dom, fn, Capture){
  35. dom.addEventListener("click", fn, !!Capture);
  36. };
  37. //client调用
  38. var dom = document.querySelector(".target");
  39. var moveBtn = document.querySelector(".move");
  40. var unmoveBtn = document.querySelector(".undo");
  41. var m = new Animate(dom);
  42. var c = new Command(m);
  43. addEvent(moveBtn, c.execute);
  44. addEvent(unmoveBtn, c.unexecute);

命令模式的优缺点:

优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。

缺点:使用命令模式可能会导致某些系统有过多的具体命令类。

宏命令

宏命令:一组命令集合(命令模式与组合模式的产物)

发布者发布一个请求,命令对象会遍历命令集合下的一系列子命令并执行,完成多任务。

  1. // 宏命令对象
  2. class MacroCommand {
  3. constructor() {
  4. this.commandList = []; // 缓存子命令对象
  5. }
  6. add(command) { // 向缓存中添加子命令
  7. this.commandList.push(command);
  8. }
  9. execute() { // 对外命令执行接口
  10. // 遍历自命令对象并执行其 execute 方法
  11. for (const command of this.commandList) {
  12. command.execute();
  13. }
  14. }
  15. }
  16. const openWechat = { // 命令对象
  17. execute: () => {
  18. console.log('打开微信');
  19. }
  20. };
  21. const openChrome = { // 命令对象
  22. execute: () => {
  23. console.log('打开Chrome');
  24. }
  25. };
  26. const openEmail = { // 命令对象
  27. execute: () => {
  28. console.log('打开Email');
  29. }
  30. }
  31. const macroCommand = new MacroCommand();
  32. macroCommand.add(openWechat); // 宏命令中添加子命令
  33. macroCommand.add(openChrome); // 宏命令中添加子命令
  34. macroCommand.add(openEmail); // 宏命令中添加子命令
  35. macroCommand.execute(); // 执行宏命令
  36. /* 输出:
  37. 打开微信
  38. 打开Chrome
  39. 打开Email
  40. */

傻瓜命令与智能命令

傻瓜命令:命令对象需要接收者来执行客户的请求。 智能命令:命令对象直接实现请求,不需要接收者,“聪明”的命令对象。

“傻瓜命令” 与 “智能命令” 的区别在于是否有 “接收者” 对象。

  1. // openWechat 是智能命令对象,并没有传入 receiver 接收对象
  2. const openWechat = {
  3. execute: () => { // 命令对象直接处理请求
  4. console.log('打开微信');
  5. }
  6. };

没有 “接收者” 的智能命令与策略模式很类似。代码实现类似,区别在于实现目标不同。

  1. 策略模式中实现的目标是一致的,只是实现算法不同(如目标:根据KPI计算奖金);
  2. 智能命令的解决问题更广,目标更具散发性。(如目标:计算奖金/计算出勤率等)。

PPT

https://www.yuque.com/stay_hungry_stay_foolish/language/xmgf0b#page=1

参考文档

JavaScript 设计模式之命令模式
JavaScript设计模式(七):命令模式