说明
在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,实现二者之间的松耦合。这就是命令模式(Command)。
UML
角色:
Command:
定义命令的接口,声明执行的方法。
ConcreteCommand:
命令接口实现对象,是“虚”的实现;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
Receiver:
接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
Invoker:
要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
Client:
创建具体的命令对象,并且设置命令对象的接收者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接收者,或许,把这个Client称为装配者会更好理解,因为真正使用命令的客户端是从Invoker来触发执行。
代码
以客人到店点餐为例,服务员记录订单后下单,后厨做菜。
此例取自《大话设计模式》,代码如下。
Command角色,抽象命令
/**
* @author ctl
* @date 2021/1/28
* 抽象命令,Command
*/
public abstract class Command {
protected Barbecuer barbecuer;
protected String type;
public Command(Barbecuer barbecuer) {
this.barbecuer = barbecuer;
}
public String getType() {
return this.type;
}
abstract public void executeCommand();
}
ConcreteCommand角色,烤翅
/**
* @author ctl
* @date 2021/1/28
* 烤翅命令,ConcreteCommand
*/
public class BakeChickenWingCommand extends Command {
public BakeChickenWingCommand(Barbecuer barbecuer) {
super(barbecuer);
type = "烤翅";
}
@Override
public void executeCommand() {
barbecuer.bakeChickenWing();
}
}
ConcreteCommand角色,烤串
/**
* @author ctl
* @date 2021/1/28
* 烤串命令,ConcreteCommand
*/
public class BakeMuttonCommand extends Command {
public BakeMuttonCommand(Barbecuer barbecuer) {
super(barbecuer);
type = "烤串";
}
@Override
public void executeCommand() {
barbecuer.bakeMutton();
}
}
Receiver角色,后厨烧烤师傅
/**
* @author ctl
* @date 2021/1/28
* 后厨烧烤师傅,Receiver
*/
public class Barbecuer {
public void bakeMutton() {
System.out.println("烤肉串");
}
public void bakeChickenWing() {
System.out.println("烤鸡翅");
}
}
Invoker角色,服务员
/**
* @author ctl
* @date 2021/1/28
* 服务员,Invoker
*/
public class Waiter {
private List<Command> list = new ArrayList<>();
public void setOrder(Command command) {
if ("烤翅".equals(command.getType())) {
System.out.println("服务员:没有鸡翅了,请点别的烧烤");
} else {
list.add(command);
System.out.println("增加订单:" + command.getType() +
" 时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
public void cancelOrder(Command command) {
list.remove(command);
System.out.println("取消订单:" + command.getType() +
" 时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
public void notifyCommand() {
for (Command command : list) {
command.executeCommand();
}
}
}
Client角色,测试类
/**
* @author ctl
* @date 2021/1/28
* Client
*/
public class CommandMain {
public static void main(String[] args) {
// 准备工作
Barbecuer barbecuer = new Barbecuer();
Command backMutton = new BakeMuttonCommand(barbecuer);
Command backChickenWing = new BakeChickenWingCommand(barbecuer);
Waiter waiter = new Waiter();
// 下单
waiter.setOrder(backMutton);
waiter.setOrder(backMutton);
waiter.setOrder(backMutton);
waiter.setOrder(backChickenWing);
// 取消订单
waiter.cancelOrder(backMutton);
// 执行
waiter.notifyCommand();
}
}
结果
可以看到,行为请求者waiter与行为实现者Barbecuer没有直接耦合在一起,并且可以对行为进行记录,撤销等。
总结
命令模式有以下几个优点:
1、命令模式比较容易实现一个命令队列。
2、在需要的情况下,命令模式可以较容易地将命令记入日志。
3、允许接收请求的一方决定是否要否决请求。
4、容易对请求实现撤销和重做。
5、新增加命令类不会影响其他命令,易于扩展。
6、行为请求者与行为实现者解耦合。
适用场景:
1、系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
2、系统需要在不同的时间指定请求、将请求排队和执行请求。
3、系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
4、系统需要将一组操作组合在一起,即支持宏命令。
敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构来实现这个模式并不难,只有在真正需要上述场景中的功能时,把原来的代码重构为命令模式才有意义。