首页 > 其他分享 >【设计模式】命令模式Command:在一次请求中封装多个参数

【设计模式】命令模式Command:在一次请求中封装多个参数

时间:2023-09-08 19:01:54浏览次数:38  
标签:封装 void 接收者 模式 命令 Command 操作 设计模式 public

(目录)


命令模式使用频率不算太高。如果熟悉函数式编程的话,会发现命令模式完全没有使用的必要,甚至在业务开发的场景中也很少使用到。 不过对于想要找到正确抽象的设计者来说,命令模式的设计思想却非常值得借鉴。

命令和查询的区别:

  • 查询,获取一个不可变的结果;
  • 命令,改变状态,不一定获取结果

模式原理 命令模式的原始定义是:将一个请求封装为一个对象,从而可以参数化具有不同请求、队列或日志请求的其他对象,并支持可撤销的操作。 从这个定义能看出,命令模式是为了将一组操作封装在对象中而设计,简单说,就是为了将函数方法封装为对象以方便传输。但是,实际的大部分编程语言中,函数不能作为参数进行传递,比如,Java 直到 8 以后才真正支持将函数作为参数传递。 所以说,在实际开发中,如果用到的编程语言不支持函数作为参数来传递,就可以借助命令模式将函数封装为对象来使用。

命令模式的 UML 图:

image.png

从这个 UML 图中,看出命令模式包含五个关键角色:

  1. 抽象命令类(Command)。声明需要做的操作有哪些;
  2. 具体命令类(Command1、2等):实现 Command 接口,其中存储一个接收者类,并在 execute 调用具体命令时,委托给接收者来执行具体方法;
  3. 调用者(Invoker):客户端通过与调用者交互,操作不同命令对象;
  4. 抽象接收者(Receiver):声明需要执行的命令操作,同时提供给客户端使用;
  5. 具体接收者(Receiver1、2等):实现抽象接收者,用于接收命令并执行真实的代码逻辑。

这个 UML 图对应的代码实现:

public interface Command {
    void excute();
}

public class Command1 implements Command{

    private final Receiver receiver;

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

    @Override
    public void excute() {
        receiver.operationA();
    }
}

public class Command2 implements Command{

    private final Receiver receiver;

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

    @Override
    public void excute() {
        receiver.operationB();
    }
}


public class Command3 implements Command{

    private final Receiver receiver;

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

    @Override
    public void excute() {
        receiver.operationC();
    }
}

public interface Receiver {

    void operationA();

    void operationB();

    void operationC();

}

public class Receiver1 implements Receiver{

    @Override
    public void operationA() {
        System.out.println("操作 A");
    }

    @Override
    public void operationB() {
        System.out.println("操作 B");
    }

    @Override
    public void operationC() {
        System.out.println("操作 C");

    }
}

public class Demo {

    public static void main(String[] args) {
        Receiver receiver1 = new Receiver1();
        Invoker invoker = new Invoker();
        invoker.setCommands(new Command1(receiver1));
        invoker.setCommands(new Command2(receiver1));
        invoker.setCommands(new OperationA(receiver1));
        invoker.run();
    }
}


//输出结果
// 操作 A
// 操作 B
// 操作 C

上面的代码实现非常简单,其中 Command 就和通常理解的“命令”相一致,比如,点击按钮打开文件、点击按钮关闭窗口等命令,只不过这个命令是告诉计算机的。接收者则类似不同的处理对象,比如,浏览器可以是一个接收者来接收打开关闭文件的命令,文件编辑器也可以是接收者来接收打开关闭文件的命令。 命令模式的核心关键点就在于围绕着命令来展开,通过抽象不同的命令,并封装到对象,让不同的接收者针对同一个命令都能做出自身的反应。


使用场景

命令模式常见的使用场景有以下几种情况:

  1. 需要通过操作来参数化对象时。比如,当将鼠标移动到网页上的下拉菜单时,获取下拉列表的同时还能点菜单项;
  2. 想要将操作放入队列、按顺序执行脚本操作或者执行一些远程操作命令时。比如,先 SSH 登录远程服务器,再 tail 查询某个目录下的日志文件,并将日志打印回显到网页的某个窗口中;
  3. 实现操作回滚功能的场景时。虽然备忘录模式也能够实现,但是命令模式能够更好地记录命令操作的顺序和相关的上下文信息。

对于命令模式的使用场景,一个经典的类比例子就是 Shell 脚本。一个 Shell 脚本其实就是这里的 Invoker 调用者,脚本里各式各样的 ps、cat、sed 等命令就是 Command,而 bash shell 或 z shell 就是作为接收者来具体实现执行命令的。 当然,命令模式并不仅限于操作系统的命令,在实际的业务开发中,可能是对应的一组复杂的代码调用逻辑,比如:触发数据统计、日志记录、链路跟踪等。

下面通过一个简单例子来为你演示。 假设正在开发一款网页的文字编辑器,对于文本格式我们期望支持 HTML 和 Markdown 的格式,编辑器需要有打开、保存和关闭的功能。于是,先来创建一个抽象命令类 Command,其中定义一个无返回的方法 execute。

public interface Command {
    void excute();
}

再来依次实现打开(Open)、保存(Save)、关闭(Close)三个操作,每个操作中都存有一个 Editor(抽象接收者类),在实现方法 execute 时,会调用 Editor 对应的 open、save 和 close 方法。

public class Open implements Command{

    private Editor editor;

    public Open(Editor editor) {
        this.editor = editor;
    }

    @Override
    public void excute() {
        editor.open();
    }
    
}

public class Save implements Command{

    private Editor editor;

    public Save(Editor editor) {
        this.editor = editor;
    }

    @Override
    public void excute() {
        editor.save();
    }
    
}

public class Close implements Command{

    private Editor editor;

    public Close(Editor editor) {
        this.editor = editor;
    }

    @Override
    public void excute() {
        editor.close();
    }
    
}

然后,我们需要定义 Editor 的三个操作方法:打开、保存和关闭。

public interface Editor {

    void open();

    void save();

    void close();
    
}

接着我们再来实现支持 HTML5 的编辑器 Html5Editor(具体接收者),分别实现打开、保存和关闭三个方法,这里具体只是打印了三种不同的操作。

public class HtmlEditor implements Editor{
    @Override
    public void open() {
        System.out.println("=== Html5Editor 执行 open 操作");
    }

    @Override
    public void save() {
        System.out.println("=== Html5Editor 执行 save 操作");
    }

    @Override
    public void close() {
        System.out.println("=== Html5Editor 执行 close 操作");
    }
}

同样,再实现支持 Markdown 的编辑器 MarkDownEditor,功能和 Html5Editor 相同。

public class MarkDownEditor implements Editor{
    @Override
    public void open() {
        System.out.println("=== MarkDownEditor 执行 open 操作");
    }

    @Override
    public void save() {
        System.out.println("=== MarkDownEditor 执行 save 操作");
    }

    @Override
    public void close() {
        System.out.println("=== MarkDownEditor 执行 close 操作");
    }
}

最后,定义一个 Web 编辑器来模拟执行编辑器的操作,并自定义一个简单的编辑流程来模拟运行编辑器的使用。

public class WebEditFlow {

    private final List<Command> commands;

    public WebEditFlow() {
        this.commands = new ArrayList<>();
    }

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

    public void run() {
        commands.forEach(Command::excute);
    }
}

public class Client {
    public static void main(String[] args) {

        HtmlEditor htmlEditor = new HtmlEditor();
        Open htmlOpen = new Open(htmlEditor);
        Save htmlSave = new Save(htmlEditor);
        Close htmlClose = new Close(htmlEditor);

        MarkDownEditor markDownEditor = new MarkDownEditor();
        Open markDownOpen = new Open(markDownEditor);
        Save markDownSave = new Save(markDownEditor);
        Close marDownClose = new Close(markDownEditor);

        WebEditFlow webEditFlow = new WebEditFlow();
        webEditFlow.setCommands(htmlOpen);
        webEditFlow.setCommands(htmlSave);
        webEditFlow.setCommands(htmlClose);

        webEditFlow.setCommands(markDownOpen);
        webEditFlow.setCommands(markDownSave);
        webEditFlow.setCommands(marDownClose);
        webEditFlow.run();
    }
}


//输出结果
// === Html5Editor 执行 open 操作
// === Html5Editor 执行 save 操作
// === Html5Editor 执行 close 操作
// === MarkDownEditor 执行 open 操作
// === MarkDownEditor 执行 save 操作
// === MarkDownEditor 执行 close 操作

从上面的例子中可以看出,命令模式的使用还是非常简单的,使用关键点就在于按照顺序来组合不同的命令。 因此,命令模式的使用场景也非常局限,只能针对命令顺序执行的场景,而对于需要多种组合的场景来说,命令模式并不是很适合


使用命令模式的原因

使用命令模式的原因,可总结为以下三个:

  1. 只关心具体的命令和动作,不想知道具体的接收者是谁以及如何操作。在实际的开发中,有时我们经常需要向某些对象发送请求,但又不知道请求的接收者是谁。比如,在 Linux 下查看操作系统的日志打印,通常我们知道执行 tail 命令就可以查看日志打印,但是对于 tail 命令如何将文件内容转化为窗口中的显示,实际上我们并不关心。命令模式通过将发送者和接收者解耦开,也就去除了它们之间的直接引用的关系,就能让发送者只提供命令而不必知道命令到底是如何完成的;
  2. 为了围绕命令的维度来构建功能。比如,可以构建上传、下载、打开、关闭这样的命令,这样更符合人类自然的思考逻辑,同时避免了使用者需要了解大量的代码实现逻辑,起到隐藏代码逻辑的作用。同时,还能自由组合相关的命令,完成一系列的组合功能,比如,登录到服务器自动下载日志文件,并保存关键数据到数据库;
  3. 为了方便统计跟踪行为操作。比如,对于数据的排序、序列化、跟踪、日志记录等操作。使用命令模式能够便捷地记录相关操作,例如,执行撤销和重做操作时,可以从执行的命令列表中快速找到相关操作进行重置,弥补了备忘录模式的缺点。像一些需要读取大量数据的场景中,使用命令模式来读取上下文信息,还能避免内存溢出的风险。

优缺点

通过上述分析,使用命令模式的优点:

  1. 降低对象之间的耦合度。比如,发出请求的对象和执行请求的对象是通过命令对象来间接耦合的,避免了直接引用;
  2. 扩展更容易。由于命令模式是以命令作为统一的使用维度,新增逻辑可以放入一个新的命令中来执行。同时,对于同一个命令,还可以使用不同的接收者来进行实现;
  3. 可以快捷地设计组合命令。对于调用系统命令或远程命令时,使用命令模式能够方便地进行自由组合。比如,登录到远程服务器执行一组命令操作。

命令模式的缺点:

  1. 不同的接收者需要实现重复的命令。比如,Markdown 编辑器需要实现编辑器里的打开、保存、关闭等操作,新增一个富文本编辑器时也需要实现打开、保存、关闭等操作;
  2. 当命令中涉及对象状态变化时,可能导致不同的结果出现。比如,在执行某条命令时调用异步方法获取结果,这时如果异步执行未完成,那么接下来执行的命令可能就会出现与预期不符的情况;
  3. 增加了代码的数量和修改难度。每新增一个命令都需要在对应的接收者那里新增命令的实现,代码量会增加很多。同样,一旦代码发生修改,也会需要修改所有接收者的实现,不利于代码的维护。

总结

命令模式将一个或一组命令封装为一个对象,从而能够将函数方法作为参数进行传输,同时还能够解耦客户端和服务端的直接耦合,适用场景有:做简单的请求排队,记录请求日志,以及支持可撤销的操作。 简单来说,命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分离开。 命令模式的原理稍微有点复杂,在使用时也容易区分不开接收者和命令,好在适用的场景比较有限,大多数场景,并不是高频使用的模式。


标签:封装,void,接收者,模式,命令,Command,操作,设计模式,public
From: https://blog.51cto.com/panyujie/7412361

相关文章

  • Java泛型对象在http请求和响应对象中的封装
    Java泛型对象在http请求和响应对象中的封装publicclassMySystemBaseResVo<T>{//注意:类的后面需要带上<T>,否则数据无法封装privateStringerr_no;privateStringerr_tips;privateTdata;publicStringgetErr_no(){returnerr_no;}......
  • React项目笔记-环境搭建、路由封装(跳转Navigate、懒加载lazy)、模块化样式引入、状态管
    环境准备nodev16.15.0npm8.5.5AntDesignofReact:https://ant.design/docs/react/introduce-cn一,创建项目npminitvite√Projectname:...vite-project-react√Selectaframework:»React√Selectavariant:»TypeScript然后使用vscode打开项目,由于......
  • java设计模式,简单工厂和抽象工厂有什么区别?
    java设计模式,简单工厂和抽象工厂有什么区别?简单工厂模式:这个模式本身很简单而且使用在业务较简单的情况下。一般用于小项目或者具体产品很少扩展的情况(这样工厂类才不用经常更改)。它由三种角色组成:工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑,根据逻辑不同,产生具体的工......
  • Mac 终端登陆MySQL出现“zsh:command not found: mysql”的问题
    mysql明明安装好了,而且也登陆使用过了,但是这次在终端登陆却报错这个问题。其实上次也报错这个一样的问题,我觉得可能是环境配置没有弄好,重新检查和source生效了下(source~/.bash_profile),此时mysql能正常登陆了;但是退出终端后再次登陆又会报同样的问题。感觉不对劲了,只能生效一次......
  • 设计模式-单例模式
    保证在整个软件系统中,对某个类只能存在一个对象实例。饿汉式(类加载时创建,没用到也创建)1、构造器私有化(防止new对象)。2、类内部创建私有的静态对象。3、用一个公共的getInstance()静态方法返回该对象。如Runtime类懒汉式(使用才创建)1、仍然使构造器私有化。2、类内部定义静......
  • 软件设计模式系列之二———抽象工厂模式
    1抽象工厂模式的定义抽象工厂模式是一种创建型设计模式,它提供了一种创建一组相关或相互依赖对象的方式,而无需指定它们的具体类。该模式以一组抽象接口为核心,包括抽象工厂接口和一组抽象产品接口,每个具体工厂类负责创建特定产品家族,保证这些产品之间的兼容性。客户端代码通过与抽......
  • 超全60000多字详解 14 种设计模式 (多图+代码+总结+Demo)
    超全60000多字详解14种设计模式(多图+代码+总结+Demo)之前读耗子叔文章时,看到过有句话没有实践,再多的理论都是扯淡,个人很赞同。你觉得自己学会了,但实践与学会之间有着很大差别。单例模式(SingletonPattern)定义或概念单例模式:保证一个类仅有一个实例,并提供一个访问的全局访......
  • 设计模式—职责链模式(Chain of Responsibility)
    目录思维导图什么是职责链模式?有什么优点呢?有什么缺点呢?什么场景使用呢?代码展示①、职责链模式②、加薪代码重构思维导图什么是职责链模式?使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它......
  • 设计模式—原型模式(Prototype)
    目录一、什么是原型模式?二、原型模式具有什么优缺点吗?三、有什么缺点?四、什么时候用原型模式?五、代码展示①、简历代码初步实现②、原型模式③、简历的原型实现④、深复制⑤、浅复制一、什么是原型模式?用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式其实......
  • 封装
    封装-提高程序安全性,保护数据-隐藏代码的实现细节-统一接口-系统可维护性增加publicclassAPPlication{publicstaticvoidmain(String[]args){Students1=newStudent();s1.setName("Duncan");System.out.println(s1.g......