命令模式:将一个请求封装为一个对象,从而使得你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
比如我们去吃烧烤的时候,可以把每次点单当作一个请求,我们向服务员点单,然后服务员再通知厨师,下面用代码实现这一过程:
烤肉串者类:
public class Barbecuer {
public void bakeMutton() {
System.out.println("烤羊肉串");
}
public void bakeChickenWing() {
System.out.println("烤鸡翅");
}
}
将每次点单都当作一个命令,抽象出一个命令类:
public abstract class Command {
protected Barbecuer barbecuer;
/**
* 确定烤肉串者是谁
* @param barbecuer
*/
public Command(Barbecuer barbecuer) {
this.barbecuer = barbecuer;
}
/**
* 执行烤肉串者命令
*/
public abstract void executeCommand();
}
烤羊肉串,烤鸡翅等为具体命令类,执行命令时执行具体的行为,需要继承命令类并实现相应方法:
public class BakeMuttonCommand extends Command {
public BakeMuttonCommand(Barbecuer barbecuer) {
super(barbecuer);
}
@Override
public void executeCommand() {
barbecuer.bakeMutton();
}
}
public class BakeChickenWingCommand extends Command {
public BakeChickenWingCommand(Barbecuer barbecuer) {
super(barbecuer);
}
@Override
public void executeCommand() {
barbecuer.bakeChickenWing();
}
}
服务员类负责接收命令并通知厨师,不管客户想要什么烤肉,都是命令,只管记录订单然后通知烤肉串者执行即可:
public class Waiter {
private Command command;
public void setOrder(Command command) {
this.command = command;
}
public void notifyBarbecuer() {
command.executeCommand();
}
}
客户端代码:
public class CommandTest {
public static void main(String[] args) {
// 烧烤店事先找好了烤肉厨师、服务员和烤肉菜单,就等顾客上门。
Barbecuer barbecuer = new Barbecuer();
Command command1 = new BakeMuttonCommand(barbecuer);
Command command2 = new BakeMuttonCommand(barbecuer);
Command command3 = new BakeChickenWingCommand(barbecuer);
// 服务员根据用户要求,通知厨房开始制作
Waiter waiter = new Waiter();
waiter.setOrder(command1);
waiter.notifyBarbecuer();
waiter.setOrder(command2);
waiter.notifyBarbecuer();
waiter.setOrder(command3);
waiter.notifyBarbecuer();
}
}
执行结果:
烤羊肉串
烤羊肉串
烤鸡翅
上面代码把基本要求都实现了,但还有几个问题。第一,真实的情况并不是用户点一个菜服务员就通知厨房做一个,应该是点完之后一起通知厨房制作;第二,如果此时鸡翅没了,不应该是客户来判断是否还有,应该是服务员或者烤肉串者来否决这个请求;第三,客户到底点了哪些烧烤是要记录日志的,以备收费,也包括后期统计;第四,客户可能因为某种原因取消一些没有制作的肉串,这种情况也要考虑。
下面是改进后的服务员类:
public class Waiter {
private List<Command> orders = new ArrayList<>();
public void setOrder(Command command) {
if (command instanceof BakeChickenWingCommand) {
System.out.println("鸡翅没有了");
} else {
orders.add(command);
System.out.println("增加订单:" + command.toString() + ",时间:" + LocalDateTime.now().toString());
}
}
public void cancelCommand(Command command) {
orders.remove(command);
System.out.println("取消订单:" + command.toString() + ",时间:" + LocalDateTime.now().toString());
}
public void notifyBarbecuer() {
for (Command command : orders) {
command.executeCommand();
}
}
}
客户端代码:
public class CommandTest {
public static void main(String[] args) {
Barbecuer barbecuer = new Barbecuer();
Command command1 = new BakeMuttonCommand(barbecuer);
Command command2 = new BakeMuttonCommand(barbecuer);
Command command3 = new BakeChickenWingCommand(barbecuer);
Waiter waiter = new Waiter();
waiter.setOrder(command1);
waiter.setOrder(command2);
waiter.setOrder(command3);
waiter.notifyBarbecuer();
}
}
输出:
增加订单:烤羊肉串,时间:2019-07-15T22:06:35.133
增加订单:烤羊肉串,时间:2019-07-15T22:06:35.133
鸡翅没有了
烤羊肉串
烤羊肉串
这样就是一个比较完整的命令模式了。
命令模式的作用
优点:第一,它能较容易地设计一个命令队列;第二,在需要的情况下,可以容易地将命令记入日志;第三,允许接收请求的一方决定是否要否决请求;第四,可以很容易地实现对请求的撤销和重做;第五,由于加进新的命令类不影响其他的类,因此增加新的具体命令类很容易。最关键的优点是命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分隔开(上面例子中就是把顾客和厨师分隔开)。
那么是否碰到类似的情况就一定要实现命令模式呢?比如命令模式支持撤销/恢复功能,但当你还不清楚是否需要这个功能时,就先不要实现命令模式。
敏捷开发的原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现这个模式并不困难,只有在真正需要加如撤销/恢复操作等功能时,把原来的代码重构为命令模式才有意义。