阅读 323

重识设计模式-命令模式(Command Pattern)

本文已同步发表到我的技术微信公众号,扫一扫文章底部的二维码或在微信搜索 “程序员驿站”即可关注,不定期更新优质技术文章。同时,也欢迎加入QQ技术群(群号:650306310)一起交流学习!

定义

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

角色

命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如图所示:

命令模式结构图

在命令模式结构图中包含如下几个角色:
● Command( 抽象命令类) : 抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。

● ConcreteCommand( 具体命令类) : 具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。

● Invoker( 调用者) : 调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。

● Receiver( 接收者) : 接收者执行与请求相关的操作,它具体实现对请求的业务处理。

命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。

命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的execute()方法,每个具体命令类将一个Receiver类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了execute()方法的不同实现,并调用不同接收者的请求处理方法。 典型的抽象命令类代码如下所示:

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 抽象命令类
 */
public interface Command {
    void execute();
}
复制代码

对于请求发送者即调用者而言,将针对抽象命令类进行编程,可以通过构造注入或者设值注入的方式在运行时传入具体命令类对象,并在业务方法中调用命令对象的execute()方法,其典型代码如下所示:

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 */
public class Invoker {
    private Command command;

    /**
     * 构造注入
     */
    public Invoker(Command command) {
        this.command = command;
    }

    /**
     * 设值注入
     */
    public void setCommand(Command command) {
        this.command = command;
    }

    /**
     * 业务方法,用于调用命令类的execute()方法
     */
    public void call() {
        command.execute();
    }
}
复制代码

具体命令类继承了抽象命令类,它与请求接收者相关联,实现了在抽象命令类中声明的execute()方法,并在实现时调用接收者的请求响应方法action(),其典型代码如下所示:

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 */
public class ConcreteCommand implements Command {
    /** 维持一个对请求接收者对象的引用 */
    private Receiver receiver;

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

    public void execute() {
        //调用请求接收者的业务处理方法action()
        receiver.action();
    }
}
复制代码

请求接收者Receiver类具体实现对请求的业务处理,它提供了action()方法,用于执行与请求相关的操作,其典型代码如下所示:

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 */
public class Receiver {
    public void action() {
        //具体操作
    }
}
复制代码

案例回放

某软件公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。该软件公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”,为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,该窗口界面如图所示:

“功能键设置”界面效果图

通过上图所示界面,用户可以将功能键和相应功能绑定在一起,还可以根据需要来修改功能键的设置,而且系统在未来可能还会增加一些新的功能或功能键。

为了降低功能键与功能处理类之间的耦合度,让用户可以自定义每一个功能键的功能,该软件公司开发人员使用命令模式来设计“自定义功能键”模块,其核心结构如图所示:

自定义功能键核心结构图

在上图中,FBSettingWindow是“功能键设置”界面类,FunctionButton充当请求调用者,Command充当抽象命令类,MinimizeCommand和HelpCommand充当具体命令类,WindowHanlder和HelpHandler充当请求接收者。完整代码如下所示:

FBSettingWindow.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 功能键设置窗口类
 */
public class FBSettingWindow {
    //窗口标题
    private String title;
    //定义一个ArrayList来存储所有功能键
    private ArrayList<FunctionButton> functionButtons = new ArrayList<>();

    public FBSettingWindow(String title) {
        this.title = title;
    }

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

    public String getTitle() {
        return this.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 (Object obj : functionButtons) {
            System.out.println(((FunctionButton) obj).getName());
        }
        System.out.println("------------------------------");
    }
}
复制代码

FunctionButton.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 功能键类:请求发送者
 */
public class FunctionButton {
    private String name; //功能键名称
    private Command command; //维持一个抽象命令对象的引用

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

    public String getName() {
        return this.name;
    }

    //为功能键注入命令
    public void setCommand(Command command) {
        this.command = command;
    }

    //发送请求的方法
    public void onClick() {
        System.out.print("点击功能键:");
        command.execute();
    }
}
复制代码

Command.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 抽象命令类
 */
public interface Command {
    void execute();
}
复制代码

MinimizeCommand.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 *最小化命令类:具体命令类
 */
public class MinimizeCommand implements Command {
    private WindowHanlder whObj; //维持对请求接收者的引用

    public MinimizeCommand() {
        whObj = new WindowHanlder();
    }

    //命令执行方法,将调用请求接收者的业务方法
    public void execute() {
        whObj.minimize();
    }
}
复制代码

HelpCommand.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 帮助命令类:具体命令类
 */
public class HelpCommand implements Command {
    private HelpHandler hhObj; //维持对请求接收者的引用

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

    //命令执行方法,将调用请求接收者的业务方法
    public void execute() {
        hhObj.display();
    }
}
复制代码

WindowHanlder.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 窗口处理类:请求接收者
 */
public class WindowHanlder {
    public void minimize() {
        System.out.println("将窗口最小化至托盘!");
    }
}
复制代码

HelpHandler.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 帮助文档处理类:请求接收者
 */
public class HelpHandler {
    public void display() {
        System.out.println("显示帮助文档!");
    }
}
复制代码

Client.java

/**
 * Created by Coding小僧 on 2019/3/18
 *
 * @since 1.0
 * 客户端测试代码
 */
public class Client {
    public static void main(String args[]) {
        FBSettingWindow fbSettingWindow = new FBSettingWindow("功能键设置");
        FunctionButton fb1 = new FunctionButton("功能键1");
        FunctionButton fb2 = new FunctionButton("功能键1");

        //帮助文档命令
        Command helpCommand = new HelpCommand();
        //缩小窗口命令
        Command minimizeCommand = new MinimizeCommand();
   
        //将命令对象注入功能键
        fb1.setCommand(helpCommand);
        fb2.setCommand(minimizeCommand);

        fbSettingWindow.addFunctionButton(fb1);
        fbSettingWindow.addFunctionButton(fb2);
        fbSettingWindow.display();

        //调用功能键的业务方法
        fb1.onClick();
        fb2.onClick();
    }
}
复制代码

编译并运行程序,输出结果如下:

显示窗口:功能键设置
显示功能键:
功能键1
功能键1
------------------------------
点击功能键:显示帮助文档!
点击功能键:将窗口最小化至托盘!
复制代码

如果需要修改功能键的功能,例如某个功能键可以实现“自动截屏”,只需要对应增加一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象注入到某个功能键即可,原有代码无须修改,符合“开闭原则”。在此过程中,每一个具体命令类对应一个请求的处理者( 接收者) ,通过向请求发送者注入不同的具体命令对象可以使得相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者。

典型应用

我们在Android开发中使用的Runnable(在java.lang包下)也是命令模式应用之一,其结构图如下:

优点

命令模式的主要优点如下:
1.降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。

2.新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。

3.可以比较容易地设计一个命令队列或宏命令( 组合命令)。

4.为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。

缺点

命令模式的主要缺点如下:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

重点

命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。在基于GUI的软件开发,无论是在电脑桌面应用还是在移动应用中,命令模式都得到了广泛的应用。

使用场景

在以下情况下可以考虑使用命令模式:
1.系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。

2.系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。

3.系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

4.系统需要将一组操作组合在一起形成宏命令。

关注我的技术公众号"程序员驿站",不更新技术文章,微信扫一扫下方二维码即可关注: