【设计模式】命令模式

173 阅读6分钟

本文主要介绍命令模式的概念和用法,以及一些针对特定需要的变种形式。

模式背景

在面向对象的程序中,调用的过程即耦合的过程,这不可避免。但是存在特殊的情况,调用方(我们称为请求者)和被调方(称为接收者)并不是调用关系很明确!又或者说一个请求者对应多个接收者,并且需要根据不同的环境选择不同的接受者。比如我们自己定义快捷键:我们的快捷键就是请求者,对应要实现的功能(如:最小化窗口,关闭页面)就是接受者,这些快捷键需要依据用户配置的功能的映射来选择对应的功能。

在正常的思路中,我们一般都直接将调用者和接受者耦合在一起。即快捷键类组合一个功能的引用。代码即:

class Invoker{
  private Receiver rev;
  public void invoke(){
    rev.execute();
  }
}

这么做,在一般的情况还行。但是如果Receiver太多了,并且Receiver无法提取共同的抽象!(比如rev1是execute方法,rev2是process方法,rev3要调用一连串方法)这就很糟糕了:

  1. 如果需要修改接收者,需要修改这个类,用户也可定制化设置。

  2. 这个调用关系是被绑死了,如果添加新的接收者,需要创建一个新的Invoker类,并且格式还差不多。这会造成大量的类。

  3. 总之,就是因为这2个关系的耦合度高,导致了系统不易扩展,会违背开闭原则。

    所以我们需要一种松耦合的方式来解决这个扩展性的问题,这就是命令模式的目的。

定义&概念

命令模式(Command):将一个请求封装为一个对象,从而可以使用不同的请求对象对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,别名有行为模式或者事务模式。

原理

定义说的很复杂,其实思想也很简单。既然希望调用者和接收者接口,那么调用者中引用的必然是要一个抽象层的东西。我们让调用者全部面向抽象编程,就能完全解耦了。但是上面也说了,接受者的具体执行方法逻辑五花八门,是没有办法提取共同抽象的。命令模式所以就在中间添加了一层:命令层。这就是定义所说的,将一个请求封装为一个对象,这个对象就是命令类对象。通过对每个接收者包装一层命令类,我们就可以通过命令类再去调用接收者方法。

组成要素

  • 抽象命令类
    • 一个抽象类或者接口,提供统一的命令的抽象,目的让调用者面向抽象编程。
  • 具体命令类
    • 抽象命令类的子类,即使封装的命令对象,该命令对象内部依赖具体的执行者。
    • 我的理解就是在每个接受者上面包了一层。让他们具有统一的对外访问入口。
  • 调用者
    • 请求的发送者,它通过命令对象来执行请求。该类设计的时候并不会确定其接受者是谁。而是在程序运行中将一个具体的命令对象注入其中。动态的来指定调用者和接收者的关系。
  • 接受者
    • 具体的功能,请求的具体业务处理逻辑。

因为我们的实际的需求可能是多变的,命令模式也有很多变种:

  • 命令队列

    如果请求者需要一连串的接受者来处理,我们可以使用命令队列的形式。就是添加一个命令队列类,命令队列中持有一个命令的List。

  • 宏命令

    是组合模式和命令模式的联用,添加一个宏命令类,这个命令类里面也是一个命令的list,和命令队列差不多。(我感觉一样就是叫法不同,不知道书上作者为啥分开讲)

  • 增加撤销操作

    执行命令,当然也可以撤销,我们可以在抽象命令类中,添加一个撤销的方法,用来undo执行的命令。

UML

实现

一般命令模式

调用者

public class Invoker {
    /**
     * 需要被注入给该调用者的命令
     */
    Command command;

    public Invoker(Command command) {
        this.command = command;
    }

    public void call() {
        command.execute();
    }
}

接受者

public class Receiver {
    public void action() {
        System.out.println("processing...");
    }
}

抽象命令类

public interface Command {
    /**
     * 所有命令都走这个方法来执行 接收者的调用
     */
    void execute();
}

具体命令类

public class ConcreteCommand implements Command {

    /**
     * 一个命令绑定一个接受者
     */
    private Receiver receiver;

    public ConcreteCommand() {
        this.receiver = new Receiver();
    }

    /**
     * 通过execute调用接受者
     */
    @Override
    public void execute() {
        receiver.action();
    }
}

客户端

Command command = new ConcreteCommand();
Invoker invoker = new Invoker(command);
invoker.call();

命令队列

如果调用者需要完成的功能是需要一些列的命令来执行,那么久需要一个队列命令。我们可以使用list来存放一个命令列表,但是最常用,灵活性最好的是加一个CommandQueue类。

public class CommandQueue {
    private ArrayList<Command> commands = new ArrayList<>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    public void removeCommand(Command command) {
        commands.remove(command);
    }

    /**
     * 循环调用每一个命令
     */
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }
}

优缺点

优点

  • 降低系统的耦合度,请求者和接收者之间的解耦。
  • 方便扩展新命令,而不需要修改代码。
  • 可以方便的实现一个命令队列或者宏命令。

缺点

  • 没有解决会造成系统中大量的类的问题。

适用场景

那些希望使用者和接收者不直接交互,在前期设计的时候,这2者的关系无法确定,后面可能需要动态的去配置的时候尝试使用命令模式。还有某个使用者如果想要执行一系列的操作的时候也考虑使用命令队列来操作。

总结

命令模式的主要目的就是为了让请求者和接收者解耦,做法就是添加一个命令层,每个命令类对应着一个接受者,从而让接收者有抽象命令类的统一外观。然后请求者只需要面向抽象命令类编程即可。

相关代码:github.com/zhaohaoren/…

如有代码和文章问题,还请指正!感谢!