命令模式
在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好的解决这个问题。
在显示生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来控制电视机(命令接受者)。
1、命令模式的定义与特点
命令模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分隔开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
优点
- 通过引入中间件(抽象接口)降低系统的耦合度。
- 扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现Undo和Redo操作。命令模式可以与备忘录模式结合,实现命令的撤销与恢复。
- 可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。
缺点
- 可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这回增加系统的复杂性。
- 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求放与抽象命令接口)增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难以理解。
2.命令模式的结构与实现
1.模式的结构
- 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法execute
- 具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
- 实现者/接受者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
- 调用者/请求者(Invoker)角色:是请求的发送者,它通常用有很多命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
2.模式的实现
/**
* @author liyuan
* @date 2021年07月12日 14:03
* 接收者
*/
public class Receiver {
public void action(){
System.out.println("接收者的action()方法被调用...");
}
}
/**
* @author liyuan
* @date 2021年07月12日 14:00
* 抽象命令
*/
public interface Command {
public abstract void execute();
}
/**
* @author liyuan
* @date 2021年07月12日 14:02
* 具体的命令
*/
public class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand() {
receiver = new Receiver();
}
@Override
public void execute() {
receiver.action();
}
}
/**
* @author liyuan
* @date 2021年07月12日 13:59
* 调用者
*/
public class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void setCommand(Command command){
this.command=command;
}
public void call(){
System.out.println("调用者执行命令command...");
command.execute();
}
}
public class Test {
public static void main(String[] args) {
Command cmd = new ConcreteCommand();
Invoker ivk = new Invoker(cmd);
System.out.println("接收者的action()方法被调用...");
ivk.call();
}
}
执行结果如下:
接收者的action()方法被调用...
调用者执行命令command...
接收者的action()方法被调用...
3.命令模式的应用实例
【例】用命令模式实现客户去餐馆吃早餐的实例。
分析:客户去餐馆可选择的早餐有肠粉、河粉和馄饨等,客户可向服务员选择以上早餐中的若干种,服务员将客户的请求交给相关的厨师去做。这里的点早餐相当于“命令”,服务员相当于“调用者”,厨师相当于“接收者”,所以用命令模式实现比较合适。
- 首先,定义一个早餐类(BreakfastCommand),它是抽象命令类,有抽象方法 cooking(),说明要做什么;
- 再定义其子类肠粉类(ChangFenInvoker)、馄饨类(HunTunInvoker)和河粉类(HeFenInvoker),它们是具体命令类,实现早餐类的 cooking() 方法,但它们不会具体做,而是交给具体的厨师去做;
- 具体厨师类有肠粉厨师(ChangFenReveiver)、馄饨厨师(HunTunReveiver)和河粉厨师(HeFenReveiver),他们是命令的接收者。
代码如下:
/**
* @author liyuan
* @date 2021年07月12日 14:15
* 点早餐的命令
*/
public interface BreakfastCommand {
/**
* 早餐烹饪
**/
public abstract void cooking();
}
/**
* @author liyuan
* @date 2021年07月12日 14:18
*/
public class CangFenReceiver {
public void cookingCangFen(){
System.out.println("厨师正在做肠粉");
}
}
/**
* @author liyuan
* @date 2021年07月12日 14:37
*/
public class HeFenReceiver {
public void cookingHeFen(){
System.out.println("厨师正在做河粉");
}
}
/**
* @author liyuan
* @date 2021年07月12日 14:31
*/
public class HunTunReveiver {
public void cookingHunTun(){
System.out.println("厨师正在做馄饨");
}
}
/**
* @author liyuan
* @date 2021年07月12日 14:17
*/
public class CangFenCommand implements BreakfastCommand {
private CangFenReceiver cangFenReceiver;
public CangFenCommand() {
cangFenReceiver = new CangFenReceiver();
}
@Override
public void cooking() {
cangFenReceiver.cookingCangFen();
}
}
/**
* @author liyuan
* @date 2021年07月12日 14:36
*/
public class HeFenCommand implements BreakfastCommand{
private HeFenReceiver heFenReceiver;
public HeFenCommand() {
heFenReceiver = new HeFenReceiver();
}
@Override
public void cooking() {
heFenReceiver.cookingHeFen();
}
}
/**
* @author liyuan
* @date 2021年07月12日 14:32
*/
public class HunTunCommand implements BreakfastCommand{
private HunTunReveiver hunTunReveiver;
public HunTunCommand() {
hunTunReveiver = new HunTunReveiver();
}
@Override
public void cooking() {
hunTunReveiver.cookingHunTun();
}
}
/**
* @author liyuan
* @date 2021年07月12日 14:48
*/
public class Waiter {
private BreakfastCommand changFen,hunTun,heFen;
public void setChangFen(BreakfastCommand changFen) {
this.changFen = changFen;
}
public void setHunTun(BreakfastCommand hunTun) {
this.hunTun = hunTun;
}
public void setHeFen(BreakfastCommand heFen) {
this.heFen = heFen;
}
public void diancan(){
if (changFen!=null){
System.out.println("点餐:肠粉");
changFen.cooking();
}
if (heFen!=null){
System.out.println("点餐:河粉");
heFen.cooking();
}
if (hunTun!=null){
System.out.println("点餐:馄饨");
hunTun.cooking();
}
}
}
public class Test {
public static void main(String[] args) {
Waiter waiter = new Waiter();
BreakfastCommand command1 = new HeFenCommand();
waiter.setHeFen(command1);
BreakfastCommand command2 = new CangFenCommand();
waiter.setChangFen(command2);
BreakfastCommand command3 = new HunTunCommand();
waiter.setHunTun(command3);
waiter.diancan();
}
}
执行结果如下:
点餐:肠粉
厨师正在做肠粉
点餐:河粉
厨师正在做河粉
点餐:馄饨
厨师正在做馄饨
4.命令模式的应用场景
当系统的某项操作具备命令语义,且命令实现不稳定(变化)时,可以通过命令模式解耦请求与实现。使用抽象命令接口使请求方的代码架构稳定,封装接收方具体命令的实现细节。接收方与抽象命令呈现弱耦合,具备良好的扩展性。
命令模式通常用于以下场景:
- 请求调用者需要与请求接收者解耦时,命令模式可以使调用者和接收者不直接交互。
- 系统随机请求命令或经常增加、删除命令时,命令模式可以方便地实现这些功能。
- 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
- 当系统需要支持命令的撤销操作和回复操作时,可以将命令对象存储起来,采用备忘录模式来实现。
5.命令模式的扩展
在软件开发中,有时将命令模式与组合模式联合使用,这就构成了宏命令模式,也叫组合命令模式。宏命令包含了一组命令,它充当了具体命令与调用者的双重角色,执行它时将递归调用它所包含的所有命令(可以优化上边的例子),其具体结构图如下:
/**
* @author liyuan
* @date 2021年07月12日 15:12
* 抽象命令
*/
public interface AbstractCommand {
public abstract void execute();
}
/**
* @author liyuan
* @date 2021年07月12日 15:13
*/
public class CompositeReceiver {
public void action1() {
System.out.println("接收者的action1()方法被调用...");
}
public void action2() {
System.out.println("接收者的action2()方法被调用...");
}
}
/**
* @author liyuan
* @date 2021年07月12日 15:12
* 树叶构件:具体命令1
*/
public class ConcreteCommand1 implements AbstractCommand {
private CompositeReceiver receiver;
ConcreteCommand1() {
receiver = new CompositeReceiver();
}
@Override
public void execute() {
receiver.action1();
}
}
/**
* @author liyuan
* @date 2021年07月12日 15:12
* 树叶构件:具体命令1
*/
public class ConcreteCommand2 implements AbstractCommand {
private CompositeReceiver receiver;
ConcreteCommand2() {
receiver = new CompositeReceiver();
}
@Override
public void execute() {
receiver.action2();
}
}
/**
* @author liyuan
* @date 2021年07月12日 15:14
* 树枝构件:调用者
*/
public class CompositeInvoker {
private ArrayList<AbstractCommand> children = new ArrayList<AbstractCommand>();
public void add(AbstractCommand c) {
children.add(c);
}
public void remove(AbstractCommand c) {
children.remove(c);
}
public AbstractCommand getChild(int i) {
return children.get(i);
}
public void execute(){
for (int i = 0; i < children.size(); i++) {
((AbstractCommand)children.get(i)).execute();
}
}
}
/**
* @author liyuan
* @date 2021年07月12日 15:16
*/
public class Test {
public static void main(String[] args) {
AbstractCommand cmd1 = new ConcreteCommand1();
AbstractCommand cmd2 = new ConcreteCommand2();
CompositeInvoker ir = new CompositeInvoker();
ir.add(cmd1);
ir.add(cmd2);
System.out.println("客户访问调用者的execute()方法...");
ir.execute();
}
}
执行结果如下:
客户访问调用者的execute()方法...
接收者的action1()方法被调用...
接收者的action2()方法被调用...