命令模式
1、概述
将一个请求封装为一个对象。命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
2、模式分析
在一次请求中,存在请求的调用者和执行者,一般情况下调用者会直接调用执行者的API,这样两者紧密的耦合在一起。如下图
现在在调用者和执行者之间添加一个对象,用来封装请求的命令。由该对象调用执行者的API,进而使调用者和执行者独立开来。这样,调用者只需要指定具体的请求命令即可,不需要知道请求时如何被接受、操作是否被执行,以及是如何执行的。
调用者只针对命令接口编程,不针对执行者的API编程。
3、模式角色
命令接口:定义命令的接口,声明执行方法。
具体命令:具体命令,实现命令接口,实现命令要执行的方法。成员中含有执行者,以及命令操作的相关参数,在命令执行方法中调用执行者的API。
执行者:真正实行命令的对象,含有命令执行的详细API。
调用者:持有命令对象,调用者通过调用命令对象的方法,进而实现执行者API的调用。
客户端:创建具体的命令对象,并且设置命令对象的接收者。
4、优点与缺点
优点:
- 降低了系统的耦合度;
- 新的命令可以很容易的加入到系统中;
- 很容易地设计成一个命令队列;
- 很容易地实现对请求的撤销和恢复;
- 在需要的情况下,可以容易地将命令记入日志。
缺点:
导致系统有过多的具体命令类,增加了系统的复杂度。
5、应用示例
以ATM上的操作为例,对银行卡账户上的存款进行操作,包括查询、存款、取款等,其类图如下:
Account(执行者,银行卡账户)
1 /** 2 * 银行账户实体类,可以执行查询、支取、存款等操作命令 3 * @author hjy 4 */ 5 public class Account { 6 private BigDecimal decimal; 7 8 //无参构造函数,账户金额初始为0 9 public Account(){ 10 this.decimal = new BigDecimal(0); 11 } 12 //有参构造方法,账户金额为给定值 13 public Account(int initNum){ 14 this.decimal = new BigDecimal(initNum); 15 } 16 17 //查询操作 18 public void query(){ 19 //return decimal; 20 Double out = decimal.setScale(2).doubleValue(); 21 System.out.println("当前账户余额为: " + out); 22 } 23 24 //存款操作 25 public void inMonery(double in){ 26 decimal = decimal.add(new BigDecimal(in)); 27 } 28 //取款操作 29 public void outMonery(double out){ 30 BigDecimal outMonery = new BigDecimal(out); 31 if(outMonery.compareTo(decimal) == 1){ 32 System.out.println("余额不足!!!"); 33 return; 34 } 35 decimal = decimal.subtract(outMonery); 36 } 37 }
AccountCommand命令接口
1 /** 2 * 银行账户操作命令接口 3 * @author hjy 4 */ 5 public interface AccountCommand { 6 //命令执行操作 7 public void execute(); 8 }
AccountQueryCommand查询命令
1 /** 2 * 查询操作命令的实现类 3 * @author hjy 4 */ 5 public class AccountQueryCommand implements AccountCommand { 6 private Account account; 7 8 //构造查询命令对象时,只需要指定操作的账号即可 9 public AccountQueryCommand(Account account){ 10 this.account = account; 11 } 12 //执行查询命令 13 @Override 14 public void execute() { 15 account.query(); 16 } 17 }
AccountInCommand存款命令
1 /** 2 * 存款操作命令实现类 3 * @author hjy 4 */ 5 public class AccountInCommand implements AccountCommand { 6 private Account account; 7 private double in; 8 9 //构造存款命令对象时,需要执行具体的账号及存款金额 10 public AccountInCommand(Account account, double in){ 11 this.account = account; 12 this.in = in; 13 } 14 //执行存款命令 15 @Override 16 public void execute() { 17 account.inMonery(in); 18 } 19 }
AccountOutCommand取款命令
1 /** 2 * 取款操作实现类 3 * @author hjy 4 */ 5 public class AccountOutCommand implements AccountCommand { 6 private Account account; 7 private double out; 8 9 //构造取款命令对象时,需要执行具体的账号及存款金额 10 public AccountOutCommand(Account account, double out){ 11 this.account = account; 12 this.out = out; 13 } 14 //执行取款命令 15 @Override 16 public void execute() { 17 account.outMonery(out); 18 } 19 }
AccountRequest(调用者)
1 /** 2 * 账户操作请求的实体类,每一个对象实际是最一条请求命令的封装 3 * @author hjy 4 */ 5 public class AccountRequest { 6 //封装操作的命令 7 private AccountCommand command; 8 9 public AccountRequest(){} 10 public void setCommand(AccountCommand command) { 11 this.command = command; 12 this.undoCommand = command; 13 } 14 15 //执行命令 16 public void executeCommand(){ 17 if(command == null){ 18 System.out.println("请指定具体的操作命令!!!"); 19 return; 20 } 21 command.execute(); 22 } 23 }
Client(客户端)
/** * 账户操作的客户端 * @author hjy */ public class AccountClient { public static void main(String[] args) { //定义一个账户 Account account = new Account(2000); //定义一个账户操请求的对象 AccountRequest accountRequest = new AccountRequest(); //定义一个查询命令 AccountCommand queryCommand = new AccountQueryCommand(account); //先来一次查询(必须先执行要请求的操作命令) accountRequest.setCommand(queryCommand); accountRequest.executeCommand(); //执行存款100元操作 AccountCommand moneryInCommand = new AccountInCommand(account, 100); accountRequest.setCommand(moneryInCommand); accountRequest.executeCommand(); //先来一次查询(必须先执行要请求的操作命令) accountRequest.setCommand(queryCommand); accountRequest.executeCommand(); //执行取款1000元操作 AccountCommand moneyOutCommand = new AccountOutCommand(account, 1000); accountRequest.setCommand(moneyOutCommand); accountRequest.executeCommand(); //先来一次查询(必须先执行要请求的操作命令) accountRequest.setCommand(queryCommand); accountRequest.executeCommand(); //再执行一次取款操作,取款金额为1500元 moneyOutCommand = new AccountOutCommand(account, 1500); accountRequest.setCommand(moneyOutCommand); accountRequest.executeCommand(); //先来一次查询(必须先执行要请求的操作命令) accountRequest.setCommand(queryCommand); accountRequest.executeCommand(); } }
执行结果
当前账户余额为: 2000.0 当前账户余额为: 2100.0 当前账户余额为: 1100.0 余额不足!!! 当前账户余额为: 1100.0
改进
可以在命令接口中添加撤销操作,每个具体命令类实现该方法,同时在调用者中添加一个成员变量来记录最近执行的命令即可。