简说设计模式——命令模式
一、什么是命令模式
在说命令模式前我们先来说一个小例子。很多人都有吃夜市的经历,对于那些推小车的摊位,通常只有老板一个人,既负责制作也负责收钱,我要两串烤串多放辣,旁边的人要了三串烤面筋不要辣,过了一会儿又来人要烤蔬菜……,当人多的时候记忆力不好的老板肯定就不知道谁要的啥、交没交钱了;而去有店铺的烤肉摊,点单的时候会有服务员来记录我们的菜单,然后再去通知烧烤师傅进行烧烤,这样就不会出现混乱了,当然我们也可以随时对菜单进行修改,此时只需服务员记录后去通知烤肉师傅即可,由于有了记录,最终算账还是不会出错的。
从这里讲,前者其实就是“行为请求者”和“行为实现者”的紧耦合,对于请求排队或记录请求日志,以及支持可撤销的操作来说,紧耦合是不太合适的,而命令模式恰恰解决了这点问题。
命令模式(Command),将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。UML结构图如下:
其中,Invoker是调用者角色,要求该命令执行这个请求;Command是命令角色,需要执行的所有命令都在这里声明,可以是接口或抽象类;Receiver是接收者角色,知道如何实施与执行一个请求相关的操作,任何类都可能作为一个接收者;ConcreteCommand将一个接收者对象绑定与一个动作,调用接收者相应的操作,以实现Execute。
1. Command类
用来声明执行操作的接口/抽象类。
1 public abstract class Command { 2 3 protected Receiver receiver; 4 5 public Command(Receiver receiver) { 6 this.receiver = receiver; 7 } 8 9 //执行命令的方法 10 abstract public void execute(); 11 12 }
2. ConcreteCommand类
具体的Command类,用于构造传递接收者,根据环境需求,具体的命令类也可能有n个。
1 public class ConcreteCommand extends Command { 2 3 //构造传递接收者 4 public ConcreteCommand(Receiver receiver) { 5 super(receiver); 6 } 7 8 //必须实现一个命令 9 @Override 10 public void execute() { 11 receiver.action(); 12 } 13 14 }
3. Invoker类
接收命令,并执行命令。
1 public class Invoker { 2 3 private Command command; 4 5 //接受命令 6 public void setCommand(Command command) { 7 this.command = command; 8 } 9 10 //执行命令 11 public void executeCommand() { 12 command.execute(); 13 } 14 15 }
4. Receiver类
该角色就是干活的角色, 命令传递到这里是应该被执行的。
1 public class Receiver { 2 3 public void action() { 4 System.out.println("执行请求!"); 5 } 6 7 }
5. Client类
首先定义一个接收者,然后定义一个命令用于发送给接收者,之后再声明一个调用者,即可把命令交给调用者执行。
1 public class Client { 2 3 public static void main(String[] args) { 4 //定义接收者 5 Receiver receiver = new Receiver(); 6 //定义一个发送给接收者的命令 7 Command command = new ConcreteCommand(receiver); 8 //声明调用者 9 Invoker invoker = new Invoker(); 10 11 //把命令交给调用者执行 12 invoker.setCommand(command); 13 invoker.executeCommand(); 14 } 15 16 }
运行结果如下:
二、命令模式的应用
1. 何时使用
- 在某些场合,如要对行为进行“记录、撤销/重做、事务”等处理时
2. 方法
- 通过调用者调用接收者执行命令,顺序为调用者→接收者→命令
3. 优点
- 类间耦合,调用者角色与接收者角色之间没有任何依赖关系
- 可扩展性
- 命令模式结合职责链模式可以实现命令族解析任务;结合模板方法模式可以减少Command子类的膨胀问题
4. 缺点
- 可能导致某些系统有过多的具体命令类
5. 使用场景
- 认为是命令的地方都可以使用
- 系统需要支持命令的撤销/恢复操作时
6. 应用实例
- GUI中每一个按钮都是一条命令
- 模拟CMD(DOS命令)
- 订单的撤销/恢复
- 触发-反馈机制的处理
三、命令模式的实现
下面以上面提到的烧烤店模型为例,使用命令模式编写代码实现。类图如下:
1. 调用者角色
服务员类为调用者角色,在其中定义一个订单列表用于存储客户订单信息,通过setOrder()方法设置订单、cancelOrder()方法取消订单、notifyExecute()方法下单。
1 public class Waiter { 2 3 private List<Command> orders = new LinkedList<>(); 4 5 //设置订单 6 public void setOrder(Command command) throws Exception { 7 //通过反射获得鸡翅的类 8 String s1 = Class.forName("com.adamjwh.gofex.command.BakeChickenWingCommand").toString().substring(6); 9 //获取command订单中的类 10 String s2 = command.toString().substring(0, command.toString().indexOf("@")); 11 12 //这里模拟鸡翅卖完的情况,当订单中有鸡翅时,撤销订单 13 if(s1.equals(s2)) { 14 System.out.println("【服务员:鸡翅没有了,请点别的烧烤】"); 15 cancelOrder(command);//撤销订单 16 } else { 17 orders.add(command); 18 System.out.println("添加订单:" + command.getBarbecuer() + "\t时间:" + new Date().toString()); 19 } 20 } 21 22 //取消订单 23 public void cancelOrder(Command command) { 24 orders.remove(command); 25 System.out.println("取消订单:" + command.getBarbecuer() + "\t时间:" + new Date().toString()); 26 } 27 28 //通知全部执行 29 public void notifyExecute() { 30 System.out.println("-----------------------订单-----------------------"); 31 for(Command command : orders) { 32 command.excuteCommand(); 33 } 34 } 35 }
2. 命令角色
1 public abstract class Command { 2 3 protected Barbecuer receiver; 4 5 public Command(Barbecuer receiver) { 6 this.receiver = receiver; 7 } 8 9 //执行命令 10 abstract public void excuteCommand(); 11 12 //获取名称 13 abstract public String getBarbecuer(); 14 15 }
3. 接收者角色
这里的接收者角色就是烧烤师傅,提供“烤羊肉串”和“烤鸡翅”的操作。
1 public class Barbecuer { 2 3 //烤羊肉 4 public void bakeMutton() { 5 System.out.println("烤羊肉串"); 6 } 7 8 //烤鸡翅 9 public void bakeChickenWing() { 10 System.out.println("烤鸡翅"); 11 } 12 13 }
4. 具体命令
这里以烤羊肉串类为例,提供了执行命令的方法。烤鸡翅类同理,此处不再赘述。
1 public class BakeMuttonCommand extends Command { 2 3 private String barbecuer; 4 5 public BakeMuttonCommand(Barbecuer receiver) { 6 super(receiver); 7 barbecuer = "烤羊肉串"; 8 } 9 10 @Override 11 public void excuteCommand() { 12 receiver.bakeMutton(); 13 } 14 15 //获取名称 16 public String getBarbecuer() { 17 return barbecuer; 18 } 19 20 }
5. Client客户端
开店前准备即初始化烤肉师傅、服务员及命令类,顾客点菜后将菜单信息存入服务员的订单上,假设鸡翅卖完了(参考Waiter类),则将鸡翅项从订单上删除(即“撤销”),然后使用notifyExecute()方法通知烤肉师傅。
1 public class Client { 2 3 public static void main(String[] args) throws Exception { 4 //开店前准备 5 Barbecuer barbecuer = new Barbecuer(); 6 Command bakeMuttonCommand1 = new BakeMuttonCommand(barbecuer); 7 Command bakeMuttonCommand2 = new BakeMuttonCommand(barbecuer); 8 Command bakeChickenWingCommand1 = new BakeChickenWingCommand(barbecuer); 9 Waiter waiter = new Waiter(); 10 11 //开门营业,顾客点菜 12 waiter.setOrder(bakeMuttonCommand1); 13 waiter.setOrder(bakeMuttonCommand2); 14 //这里假设鸡翅卖完了 15 waiter.setOrder(bakeChickenWingCommand1); 16 17 //点菜完毕,通知厨房 18 waiter.notifyExecute(); 19 } 20 21 }
运行结果如下:
命令模式其实是把一个操作的对象与知道怎么执行一个操作的对象分隔开。至于命令模式使用时机,敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现这个模式并不困难,只有在真正需要如撤销/恢复操作等功能时,把原来的代码重构为命令模式才有意义。