《Java Design Patterns》第十五章 命令模式 - 请求发送者和接收者解耦

第十五章 命令模式 - 请求发送者和接收者解耦

一、命令模式

命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。

命令模式是一种对象行为型模式,又称为动作模式或事务模式。

命令模式中包含的几个角色:

  1. Command - 抽象命令类
    1. 声明了用于执行请求的 execute() 等方法,通过这些方法可以调用请求接收者的相关操作
  2. ConcreteCommand - 具体命令类
    1. 实现抽象命令类中声明的方法
    2. 对应具体的接收者对象,将接收者对象的动作绑定其中
    3. 在实现execute() 方法时,调用接收者对象的相关操作
  3. Invoker - 调用者
    1. 请求的发送者,通过命令对象来执行请求
    2. 调用者并不需要在设计时确定其接收者,因此它只与抽象命令类保持关联
    3. 在程序运行时,注入一个具体命令对象,再调用具体命令对象的execute() 方法
    4. 从而间接调用请求接收者的相关操作
  4. Receiver - 接收者
    1. 接收者执行与请求相关的操作,它具体实现对请求的业务处理

命令模式的本质是对请求进行封装,一个请求对应一个命令,将发出命令的责任和执行命令的责任分隔开。

二、命令模式的关键 - 抽象命令类

请求发送者只需要针对抽象命令类编程即可。

// 抽象命令类
abstract class Command {
    // 声明公共的执行方法
    public abstract void execute();
}
// 请求发送者 - 调用者
class Invoker {
    // 请求发送者只需要针对 Command 编程即可,具体命令类在运行时指定
    private Command command;

    // 通过构造函数或者setter方法注入Command
    public Invoker(Command command){
        this.command = command;
    }

    // 用于调用命令类的 execute 方法
    public void call(){
        command.execute();
    }
}
// 具体命令类
class ConcreteCommand extends Command {
    // 维持一个对请求接收者对象的引用
    private Receiver receiver; // 如果需要多个接收者,可以将该对象改为List等集合对象

    public void execute(){
        // 调用请求接收者的业务处理方法
        receiver.action();
    }
}
class Receiver {
    public void action(){
        // 具体操作
    }
}

三、完整实现

// 功能键设计窗口类
class FBSettingWindow{
    // 窗口标题
    private String title;
    // List集合,存储所有的功能按键
    private List<FunctionButton> functionButtons = new ArrayList<>();

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void addFunctionButton(FunctionButton fb){
        functionButtons.add(fb);
    }

    public void removeFunctionButton(FunctionButton fb){
        functionButtons.remove(fb);
    }

    // 显示窗口及功能
    public void display(){
        System.out.println("显示窗口:"+this.title);
        System.out.println("显示功能键");
        for (FunctionButton functionButton : functionButtons) {
            System.out.println(functionButton.getName());
        }
    }
}

// 功能按键 - 请求发送者
class FunctionButton {
    // 按键名称
    private String name;
    // 维持一个抽象命令的引用
    private Command command;

    public String getName() {
        return name;
    }

    public Command getCommand() {
        return command;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setCommand(Command command) {
        this.command = command;
    }

    public void onClick(){
        System.out.println("点击按键:");
        command.execute();
    }
}

// 抽象命令类
abstract class Command {
    public abstract void execute();
}

// 帮助命令类 - 具体命令类
class HelpCommand extends Command {
    private HelpHandler helpHandler;

    public HelpCommand() {
        helpHandler = new HelpHandler();
    }

    @Override
    public void execute() {
        helpHandler.action();
    }
}

// 最小化命令类 - 具体命令类
class MinimizeCommand extends Command {
    private MinimizeHandler handler;

    public MinimizeCommand() {
        handler = new MinimizeHandler();
    }

    @Override
    public void execute() {
        handler.action();
    }
}

// 帮助处理类 - 请求接收者
class HelpHandler {
    public void action(){
        System.out.println("显示文档");
    }
}

// 最小化处理类 - 请求接收者
class MinimizeHandler {
    public void action (){
        System.out.println("最小化!");
    }
}

public class CommandPattern {
    public static void main(String[] args) {
        final FBSettingWindow window = new FBSettingWindow();
        window.setTitle("功能键设置");

        final FunctionButton button1 = new FunctionButton();
        button1.setName("按钮1");
        final FunctionButton button2 = new FunctionButton();
        button2.setName("按钮2");

        final HelpCommand helpCommand = new HelpCommand();
        final MinimizeCommand minimizeCommand = new MinimizeCommand();

        button1.setCommand(helpCommand);
        button2.setCommand(minimizeCommand);

        window.addFunctionButton(button1);
        window.addFunctionButton(button2);
        window.display();

        button1.onClick();
        button2.onClick();
    }
}

如果需要增加一个新的命令,只需要增加一个具体命令类,将命令类与具体的处理类进行关联,并注入到某个功能键即可。原有代码无需修改,符合“开闭原则”。

四、命令队列

当我们点击一个按钮后,需要执行多次命令操作,且命令是可重用的。此时我们可以使用“命令队列”的方式来设计,此时,请求发送者不再维护单独的一个Command,而是一个CommandQueue,存储多个命令对象:

class FunctionButton {
    private String name;
    private CommandQueue commandQueue;
}
class CommandQueue {
    private List<Command> commands = new ArrayList<>();

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

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

    public void display(){
        for(Command cd:commands){
            cd.execute();
        }
    }
}

命令队列类似于“批处理”的概念,可以对一组对象(命令)进行批量操作。如果请求接收者没有严格的先后次序,还可以通过多线程技术来并发调用对象的execute方法,从而提高程序执行效率。

五、撤销操作的实现

在命令模式中,可以通过调用命令对象的 execute() 方法来实现对请求的处理,如果需要撤销操作,可以在命令类中增加一个逆向操作来实现。

也可以通过保存对象的历史状态来实现撤销,如“备忘录模式”。

我们可以在具体的Handler 中,保存上次执行时的状态,并提供一个方法以便恢复至保存的状态。

class AdderHandler{
    private int number;

    public int add(int i){
        number += i;
        return number;
    }
}

class AdderCommand extends Command {
    private AdderHandler handler;
    private int history;

    public AdderCommand(AdderHandler handler){
        this.handler = handler;
    }

    @Override
    public void add(int value){
        int result = handler.add(value);
        history = value;
        System.out.print("加法结果为:"+result);
    }

    @Override
    public void undo(){
        // 使用保存的状态实现撤销操作
        int result = handler.add(-history);
        System.out.print("撤销后的结果为:"+result);
    }
}

注,该撤销操作只能执行一次,如果需要执行多次,可以使用集合来保存 histroy

六、宏命令

宏命令(Macro Command)又称为组合命令,是组合模式和命令联合的产物。

宏命令是一个具体的命令类,拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。

当调用宏命令的 execute 方法时,会递归调用它的每个成员命令的 execute 方法,成员既可以是一个简单命令,也可以是宏命令。

七、总结

命令模式是一种 使用频率非常高 的设计模式,可以将请求发送者和接收者解耦。发送者通过命令对象间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。

优点

  1. 降低系统耦合,请求发送者与接收者不直接引用,相互独立
  2. 方便扩展,增加新的命令不会影响其他类,无需修改系统代码,符合“开闭原则”
  3. 可以比较简单的设计一个命令队列或组合命令(宏命令)
  4. 为请求的撤销、恢复操作提供了一种设计和实现方案

缺点

  1. 可能会导致系统中存在过多的命令类,因为每个接收者都需要设计一个具体命令类来调用

适用场景

  1. 系统需要请求发送者与接收者解耦,彼此互不影响,互不相知
  2. 系统需要使用一组命令或者宏命令
  3. 系统需要支持撤销、恢复操作
  4. 系统需要在不同的时间指定请求、将请求排队和执行请求

八、练习

 命令模式
 Sunny软件公司欲开发一个基于Windows平台的公告板系统。
 该系统提供了一个主菜单(Menu),
 在主菜单中包含了一些菜单项(MenuItem),
 可以通过Menu类的addMenuItem()方法增加菜单项。
 菜单项的主要方法是click(),
 每一个菜单项包含一个抽象命令类,
 具体命令类包括OpenCommand(打开命令),CreateCommand(新建命令),EditCommand(编辑命令)等,
 命令类具有一个execute()方法,
 用于调用公告板系统界面类(BoardScreen)的open()、create()、edit()等方法。
 试使用命令模式设计该系统,
 以便降低MenuItem类与BoardScreen类之间的耦合度
/**
 * 命令模式
 * Sunny软件公司欲开发一个基于Windows平台的公告板系统。
 * 该系统提供了一个主菜单(Menu),
 * 在主菜单中包含了一些菜单项(MenuItem),
 * 可以通过Menu类的addMenuItem()方法增加菜单项。
 * 菜单项的主要方法是click(),
 * 每一个菜单项包含一个抽象命令类,
 * 具体命令类包括OpenCommand(打开命令),CreateCommand(新建命令),EditCommand(编辑命令)等,
 * 命令类具有一个execute()方法,
 * 用于调用公告板系统界面类(BoardScreen)的open()、create()、edit()等方法。
 * 试使用命令模式设计该系统,
 * 以便降低MenuItem类与BoardScreen类之间的耦合度
 */
// 主菜单 - 类似于之前的窗口界面,包含多个按钮(菜单项)
class Menu{
    private List<MenuItem> menuItems = new ArrayList<>();

    public void addItem(MenuItem item){
        menuItems.add(item);
    }

    public void display(){
        System.out.println("------显示菜单:-----");
        for (MenuItem menuItem : menuItems) {
            System.out.println(menuItem.getName());
        }
    }
}

// 菜单项 - 请求发送者
class MenuItem {
    private String name;
    private Command command;

    public MenuItem(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setCommand(Command command){
        this.command = command;
    }

    public void create(){
        command.create();
    }

    public void edit(){
        command.edit();
    }

    public void open(){
        command.open();
    }
}

// 抽象命令类
abstract class Command {
    abstract void create();
    abstract void edit();
    abstract void open();
}

// 宏命令 - 一组命令的集合,宏命令一般不直接操作接收者,而是通过集合中的对象属性来操作
class MacroCommand extends Command {
    private List<Command> commands = new ArrayList<>();

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

    @Override
    void create() {
        for (Command command : commands) {
            command.create();
        }
    }

    @Override
    void edit() {
        for (Command command : commands) {
            command.edit();
        }
    }

    @Override
    void open() {
        for (Command command : commands) {
            command.open();
        }
    }
}

// 具体命令类
class BoardScreenCommand extends Command{
    private BoardScreenHandler handler;

    public BoardScreenCommand(){
        handler = new BoardScreenHandler();
    }

    @Override
    void create() {
        handler.create();
    }

    @Override
    void edit() {
        handler.edit();
    }

    @Override
    void open() {
        handler.open();
    }
}

// 请求接收者,也就是具体的处理类
class BoardScreenHandler {
    public void open(){
        System.out.println("打开公告板");
    }
    public void create(){
        System.out.println("创建公告板");
    }
    public void edit(){
        System.out.println("修改公告板");
    }
}

public class CommandPattern {
    public static void main(String[] args) {
        final Menu menu = new Menu();

        final MenuItem menuItem = new MenuItem("公告板管理");
        final MenuItem menuItem1 = new MenuItem("高噶");

        final Command command = new BoardScreenCommand();

        final MacroCommand command1 = new MacroCommand();
        command1.addCommand(command);
        command1.addCommand(command);

        menuItem.setCommand(command);
        menuItem1.setCommand(command1);

        menu.addItem(menuItem);
        menu.addItem(menuItem1);

        menu.display();
        menuItem.create();
        menuItem.open();
        System.out.println("----宏命令执行------");
        menuItem1.create();
        menuItem1.edit();
        menuItem1.open();
    }
}
文章作者: koral
文章链接: http://luokaiii.github.io/2019/07/17/读书笔记/《JavaDesignPatterns》/17.命令模式/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自