一、命令模式的角色
命令模式中角色共有四类,其中
Client:模拟发出命令场景的测试类Invoker:命令的调用者,同时也是命令的接收者Command:命令的封装类,其中有指定命令具体的实现者Receiver:命令的执行者
命令模式将命令封装成一个对象,在命令发出者、调用者之间传递,在创建命令的同时已经指定了具体的执行者。所以命令发出者无需知道命令的具体执行过程,只需要创建命令、并指定命令的执行者,然后再将命令交给调用者Invoker即可完成命令的流转。
举个例子,如果你到饭馆点菜,服务员过来拿菜单给你点菜(你需要在菜单上勾画点的菜品),然后服务员将菜单交给后台对应的大厨做菜。在此过程中,菜单就是封装的Command类,不过其中已经指定好了哪个菜由哪个大厨(Receiver)来做;而服务员就是Invoker,我们只需要点好菜(创建命令),将菜单交给服务员即可。而服务员是怎么分配菜单给大厨以及大厨如何做菜都不需要关心,只要等着热腾腾、香喷喷的菜上桌即可。此处Client所在对Receiver的依赖,之后再加以讨论。
二、命令模式源码分析
package command;
public class Receiver {
public void doSomethingA() {
System.out.println("doA");
}
public void doSomethingB() {
System.out.println("doB");
}
}
package command;
public interface ICommand {
void execute();
}
package command;
public class CommandA implements ICommand {
private Receiver receiver;
public CommandA(Receiver receiver) {
super();
this.receiver = receiver;
}
@Override
public void execute() {
receiver.doSomethingA();
}
}
package command;
public class CommandB implements ICommand {
private Receiver receiver;
public CommandB(Receiver receiver) {
super();
this.receiver = receiver;
}
@Override
public void execute() {
receiver.doSomethingB();
}
}
package command;
public class Invoker {
private ICommand command;
public void setCommand(ICommand command) {
this.command = command;
}
public void action(){
command.execute();
}
}
package command;
public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker();
Receiver receiver = new Receiver();
ICommand command = new CommandA(receiver);
invoker.setCommand(command);
invoker.action();
}
}
仔细观察代码,大家也许会有些疑惑:Invoker直接调用ICommand接口的方法execute方法,ICommand的实现类再调用Receiver的具体命令实现方法,为何不直接在Client场景类中直接调用receiver的方法呢,这样是不是有点多此一举的味道?对于简单的问题确实如此,对于复杂的问题,比如命令队列、命令撤销、以及命令的扩展等就难以解决了。如同web应用需要分层一般,命令模式同样是将Invoker、Command、Receiver分离并面向接口变成来降低耦合性。Receiver类比于Dao层,做一些基本的实现功能;Command类比于Service层,处理一些具体命令的分解和整合的逻辑,可以组合多个Receiver类;Invoker则是处理Command集合。由于是面向接口编程,Command的扩展也变得容易,只需要添加实现类即可。
三、具体运用实例
这里我们举个存款/取款的例子,并扩展一下上面的基本代码,加入命令撤销以及命令队列的功能。具体代码实现如下:
package account;
public class Account {
private double amount;
public Account(double amount) {
super();
this.amount = amount;
}
public double getAmount() {
return amount;
}
public void saveMoney(double amount) {
this.amount += amount;
}
public void drawMoney(double amount) {
this.amount -= amount;
}
}
package account;
public interface ICommand {
void execute();
void cancel();
}
package account;
public class SaveCommand implements ICommand {
private Account account;
private double amount;
public SaveCommand(Account account, double amount) {
this.account = account;
this.amount = amount;
}
@Override
public void execute() {
account.saveMoney(amount);
}
@Override
public void cancel() {
account.drawMoney(amount);
}
}
package account;
public class DrawCommand implements ICommand {
private Account account;
private double amount;
public DrawCommand(Account account, double amount) {
this.account = account;
this.amount = amount;
}
@Override
public void execute() {
account.drawMoney(amount);
}
@Override
public void cancel() {
account.saveMoney(amount);
}
}
package account;
import java.util.LinkedList;
import java.util.Queue;
public class Invoker {
private Queue<ICommand> queue = new LinkedList<>();
private ICommand command;
public Invoker(Queue<ICommand> queue) {
super();
this.queue = queue;
}
public Invoker() {
super();
}
public void addCommand(ICommand command) {
queue.add(command);
}
public void action() {
command = queue.poll();
command.execute();
}
public void cancel() {
if (command != null) {
command.cancel();
}
}
}
package account;
public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker();
Account account = new Account(1000);
ICommand command = new SaveCommand(account, 1000);
invoker.addCommand(command);
invoker.action();
System.out.println(account.getAmount());
invoker.cancel();
System.out.println(account.getAmount());
}
}
正如上一节所说,Account是具体存取钱的实现者,Invoker类处理Command集合或者是在Command之上的一些逻辑操作。在这里,我们添加了命令队列来控制命令的执行顺序,并将上次执行的命令保存下来,用来撤销上次执行的命令。命令撤销是命令模式的重要运用之一,利用物理上的联系(执行和撤销操作都放置在该命令实现类下)减少逻辑上的繁复性(撤销只需要考虑做执行的相反操作即可)。此处撤销操作较为简单,存款对应的相反操作就是取款,而实际运用中则可能遇到更加复杂的情况,可能涉及日志回滚等情况就不加以讨论了。
四、对基本命令模式的改进
1.封闭Receiver类
或许有部分细心的读者发现了,在我们举的第一个点菜的例子中,菜由哪位大厨来做并不需要由我们客人来指定,是已经指定好的(默认)。但是我们观察Command实现类或者是Client场景类就知道,在创建命令的时候是需要指定其具体的实现者Receiver的。所以在某些情况下,我们需要封闭Receiver,也就是去掉Client对于Receiver的依赖。对此,拿上个例子来说,我们对Command实现类做出如下修改:
package account;
public class DrawCommand implements ICommand {
private Account account;
private double amount;
public DrawCommand(Account account, double amount) {
this.account = account;
this.amount = amount;
}
public DrawCommand(double amount) {
account = new Account(0);
this.amount = amount;
}
@Override
public void execute() {
account.drawMoney(amount);
}
@Override
public void cancel() {
account.saveMoney(amount);
}
}
2.添加Command抽象类
ICommand接口过于抽象,不同种类的命令对应的实现者是可以预见的,比如我们是一个项目组,客户提新需求给项目经理,那么工作谁去做呢?当然是需求、开发、测试、美工组啦。所以可以这些可以预见实现者(Receivers)的命令抽象出来,拿前面取款的例子来说,可以针对账号相关的命令做抽象类:
package account;
public abstract class AccountCommand implements ICommand {
protected Account account;
public AccountCommand(Account account) {
this.account = account;
}
}
package account;
public class SaveCommand extends AccountCommand{
private double amount;
public SaveCommand(Account account,double amount) {
super(account);
this.amount = amount;
}
public SaveCommand(double amount) {
super(new Account(0));
this.amount = amount;
}
@Override
public void execute() {
account.saveMoney(amount);
}
@Override
public void cancel() {
account.drawMoney(amount);
}
}
这里的AccountCommand抽象类指定了Account作为实现者,申明为protected让子类继承,并实现了ICommand接口,必须重写执行、取消命令方法。
五、后序
到这对于命令模式的解读就全部完成了,参考了<设计模式之禅>与<大话设计模式>两本书,之中包括了自己的一些理解,如有错误,欢迎指正。之后也会陆陆续续写写其余的模式,也希望大家多多支持,这是我坚持下去最大的动力,谢谢!