首页 > 其他分享 >命令模式:如何在一次请求中封装多个参数?

命令模式:如何在一次请求中封装多个参数?

时间:2024-09-22 16:22:38浏览次数:10  
标签:封装 请求 void 接收者 模式 命令 参数 操作 public

命令模式使用频率不算太高,这里大家可能会问:命令和查询有什么区别?简单来说,查询是获取一个不可变的结果,而命令是改变状态,不一定获取结果

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

一、模式原理分析

命令模式的原始定义是:将一个请求封装为一个对象,从而让我们可以参数化具有不同请求、队列或日志请求的其他对象,并支持可撤销的操作。

从这个定义中我们能看出,命令模式是为了将一组操作封装在对象中而设计的,简单来说,就是为了将函数方法封装为对象以方便传输。但我们知道,实际上在大部分的编程语言中,函数并不能作为参数来进行传递,比如,Java 直到 8 以后才真正支持将函数作为参数传递的。

所以说,在实际的开发中,如果用到的编程语言并不支持用函数作为参数来传递,那么就可以借助命令模式将函数封装为对象来使用。

我们先来看看命令模式的UML图:

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

  • 抽象命令类(Command):用于声明需要做的操作有哪些。

  • 具体命令类(Command1、2等):实现 Command 接口,其中存储一个接收者类,并在 execute 调用具体命令时,委托给接收者来执行具体的方法。

  • 调用者(Invoker):客户端通过与调用者交互来操作不同的命令对象。

  • 抽象接收者(Receiver):声明需要执行的命令操作,同时提供给客户端使用。

  • 具体接收者(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.setCommand(new Command1(receiver1));
        invoker.setCommand(new Command2(receiver1));
        invoker.setCommand(new OperationA(receiver1));
        invoker.run();
    }
}
//输出结果
操作 A
操作 B
操作 C

上面的代码实现非常简单,其中 Command 就和我们通常理解的“命令”相一致,比如,点击按钮打开文件、点击按钮关闭窗口等命令,只不过这个命令是告诉计算机的。接收者则类似不同的处理对象,比如,浏览器可以是一个接收者来接收打开关闭文件的命令,文件编辑器也可以是接收者来接收打开关闭文件的命令。

命令模式的核心关键点就在于围绕着命令来展开,通过抽象不同的命令,并封装到对象,让不同的接收者针对同一个命令都能做出自身的反应。

二、使用场景分析

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

  • 需要通过操作来参数化对象时。比如,当我们将鼠标移动到网页上的下拉菜单时,获取下拉列表的同时还能点菜单项。

  • 想要将操作放入队列、按顺序执行脚本操作或者执行一些远程操作命令时。比如,先 SSH 登录远程服务器,再 tail 查询某个目录下的日志文件,并将日志打印回显到网页的某个窗口中。

实现操作回滚功能的场景时。虽然备忘录模式也能够实现,但是命令模式能够更好地记录命令操作的顺序和相关的上下文信息。

对于命令模式的使用场景,一个经典的类比例子就是 Shell 脚本。如果你熟悉 Shell 脚本的话,就会发现一个 Shell 脚本其实就是这里的 Invoker 调用者,脚本里各式各样的 ps、cat、sed 等命令就是 Command,而 bash shell 或 z shell 就是作为接收者来具体实现执行命令的。

当然,命令模式并不仅限于操作系统的命令,在实际的业务开发中,可能是对应的一组复杂的代码调用逻辑,比如,触发数据统计、日志记录、链路跟踪等。

为了更好地理解命令模式的使用场景,下面我们通过一个简单例子来演示。

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

public interface Command {
    void execute();
}

再来依次实现打开(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 execute() {
        editor.open();
    }
}
public class Save implements Command {
    private Editor editor;
    public Save(Editor editor) {
        this.editor = editor;
    }
    @Override
    public void execute() {
        editor.save();
    }
}
public class Close implements Command{
    private Editor editor;
    public Close(Editor editor) {
        this.editor = editor;
    }
    @Override
    public void execute() {
        editor.close();
    }
}

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

public interface Editor {
    void open();
    void save();
    void close();
}

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

public class Html5Editor 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() {
        commands = new ArrayList<>();
    }
    public void setCommand(Command command) {
        commands.add(command);
    }
    public void run() {
        commands.forEach(Command::execute);
    }
}
public class Client {
    public static void main(String[] args) {
        Html5Editor html5Editor = new Html5Editor();
        MarkDownEditor markDownEditor = new MarkDownEditor();
        Open html5Open = new Open(html5Editor);
        Save html5Save = new Save(html5Editor);
        Close html5Close = new Close(html5Editor);
        Open markDownOpen = new Open(markDownEditor);
        Save markDownSave = new Save(markDownEditor);
        Close markDownClose = new Close(markDownEditor);
        WebEditFlow webEditFlow = new WebEditFlow();
        webEditFlow.setCommand(html5Open);
        webEditFlow.setCommand(html5Save);
        webEditFlow.setCommand(html5Close);
        webEditFlow.setCommand(markDownOpen);
        webEditFlow.setCommand(markDownSave);
        webEditFlow.setCommand(markDownClose);
        webEditFlow.run();
    }
}
//输出结果
=== Html5Editor 执行 open 操作
=== Html5Editor 执行 save 操作
=== Html5Editor 执行 close 操作
=== MarkDownEditor 执行 open 操作
=== MarkDownEditor 执行 save 操作
=== MarkDownEditor 执行 close 操作

从上面的例子中可以看出,命令模式的使用还是非常简单的,使用关键点就在于按照顺序来组合不同的命令

因此,命令模式的使用场景也非常局限,只能针对命令顺序执行的场景,而对于需要多种组合的场景来说,命令模式并不是很适合。

三、为什么使用命令模式?

分析完命令模式的原理和使用场景后,我们再来说说使用命令模式的原因,可总结为以下三个。

第一个,只关心具体的命令和动作,不想知道具体的接收者是谁以及如何操作。在实际的开发中,有时我们经常需要向某些对象发送请求,但又不知道请求的接收者是谁。比如,在 Linux 下查看操作系统的日志打印,通常我们知道执行 tail 命令就可以查看日志打印,但是对于 tail 命令如何将文件内容转化为窗口中的显示,实际上我们并不关心。命令模式通过将发送者和接收者解耦开,也就去除了它们之间的直接引用的关系,就能让发送者只提供命令而不必知道命令到底是如何完成的。

第二个,为了围绕命令的维度来构建功能。比如,可以构建上传、下载、打开、关闭这样的命令,这样更符合人类自然的思考逻辑,同时避免了使用者需要了解大量的代码实现逻辑,起到隐藏代码逻辑的作用。同时,还能自由组合相关的命令,完成一系列的组合功能,比如,登录到服务器自动下载日志文件,并保存关键数据到数据库。

第三个,为了方便统计跟踪行为操作。比如,对于数据的排序、序列化、跟踪、日志记录等操作。使用命令模式能够便捷地记录相关操作,例如,执行撤销和重做操作时,可以从执行的命令列表中快速找到相关操作进行重置,弥补了备忘录模式的缺点。像一些需要读取大量数据的场景中,使用命令模式来读取上下文信息,还能避免内存溢出的风险。

四、命令模式的优缺点是什么?

通过上述分析,我们也可以总结出使用命令模式的优点。

  • 降低对象之间的耦合度。比如,发出请求的对象和执行请求的对象是通过命令对象来间接耦合的,避免了直接引用。

  • 扩展更容易。由于命令模式是以命令作为统一的使用维度,新增逻辑可以放入一个新的命令中来执行。同时,对于同一个命令,还可以使用不同的接收者来进行实现。

  • 可以快捷地设计组合命令。对于调用系统命令或远程命令时,使用命令模式能够方便地进行自由组合。比如,登录到远程服务器执行一组命令操作。

同样,命令模式也有一些缺点。

  • 不同的接收者需要实现重复的命令。比如,Markdown 编辑器需要实现编辑器里的打开、保存、关闭等操作,新增一个富文本编辑器时也需要实现打开、保存、关闭等操作。

  • 当命令中涉及对象状态变化时,可能导致不同的结果出现。比如,在执行某条命令时调用异步方法获取结果,这时如果异步执行未完成,那么接下来执行的命令可能就会出现与预期不符的情况。

  • 增加了代码的数量和修改难度。每新增一个命令都需要在对应的接收者那里新增命令的实现,代码量会增加很多。同样,一旦代码发生修改,也会需要修改所有接收者的实现,不利于代码的维护。

命令模式将一个或一组命令封装为一个对象,从而能够将函数方法作为参数进行传输,同时还能够解耦客户端和服务端的直接耦合,适用场景有:做简单的请求排队,记录请求日志,以及支持可撤销的操作。

简单来说,命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分离开

命令模式的原理稍微有点复杂,在使用时也容易区分不开接收者和命令,好在适用的场景比较有限,对于大多数开发人员来说,并不是高频使用的模式。

文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发

个人小工具程序上线啦,通过公众号菜单即可体验,欢迎大家体验后提出优化意见

标签:封装,请求,void,接收者,模式,命令,参数,操作,public
From: https://blog.51cto.com/jiangyi/12080803

相关文章

  • 通过组合使用这些工具,您可以实现灵活的 WIM 备份和恢复方案。每个工具都有其特定功能,
    使用Windows的WIM(WindowsImagingFormat)备份和恢复可以通过命令行工具DISM(DeploymentImagingServiceandManagementTool)来实现。以下是一些常用的WIM备份和恢复命令参数示例:1. 备份(Capture)使用dism命令将系统映像备份为WIM文件:bashCopyCodedism/Cap......
  • 淘宝商品详情数据接口包括哪些参数
    ‌淘宝商品详情数据接口‌的参数主要包括商品ID(num_iid)、是否获取促销价(is_promotion)、返回的字段列表(fields)等。这些参数的选择可以根据开发者的需求灵活调整,以获取所需的商品详情信息。‌商品ID(num_iid)‌:这是淘宝商品详情数据接口的核心参数,用于唯一标识一个淘宝商品。通过......
  • js逆向实战之烯牛数据请求参数加密和返回数据解密
    声明:本篇文章仅用于知识分享实战网址:https://www.xiniudata.com/industry/newest?from=data请求参数加密访问网址,往下翻翻,可以看到触发了如下的数据包,请求参数进行了加密。全局搜索list_industries_by_sort地址,有四处,都位于同一个文件中。随便点一个看看,可以看到有payload......
  • python函数一:函数的概念、函数定义与调用、函数的参数、函数的返回值、说明文档以及函
    文章目录1.函数介绍1.1函数的概念1.2函数定义与调用1.2函数的参数1.3函数的返回值1.4说明文档2.函数的嵌套调用2.1嵌套调用及执行流程2.2嵌套调用的应用1.函数介绍1.1函数的概念什么是函数?函数:是一个被命名的、独立的、完成特定功能的代码段,其可能......
  • Flutter中使用Sqflite封装数据库操作与更新管理
    1.简介在Flutter中,sqflite是一个强大的插件,用于SQLite数据库操作。我们可以通过封装sqflite库的数据库操作来使代码更清晰和模块化。本篇文章将介绍如何封装增删改查、数据库版本控制与更新的处理,以及如何将查询结果映射到实体类。2.初始化数据库与数据库封装创建数据库......
  • Python 中的 HTTP 编程入门,如何使用 Requests 请求网络
    Python中的HTTP编程入门HTTP(超文本传输协议)是现代网络通信的基础,几乎所有的网络应用都依赖于HTTP协议进行数据交换。在Python中,处理HTTP请求和响应非常简单,可以通过内置的http模块或第三方库如requests来实现。本文将详细介绍Python中的HTTP编程,包括基本......
  • 软件测试笔记|web自动化测试|自动化测试中,模块封装有哪些?封装的目的是什么?
    在自动化测试中,常见的模块封装有以下几种:一、页面元素定位封装将页面上的各种元素定位方式(如通过ID、Name、XPath、CSS选择器等)进行封装,形成统一的元素定位方法。例如,可以创建一个函数,传入定位方式和定位表达式,返回定位到的元素对象。二、操作封装1.把对页面元素的常见......
  • 如何避免请求无法访问的资源
    404notfound错误表示服务器无法找到所请求的资源。对于浏览器来说,这意味着该url无法识别。404请求会让用户感到沮丧,并可能导致网站性能不佳。此外,搜索引擎会抓取不存在的页面并将其编入索引,因此,网站的排名会受到负面影响。总而言之,我们可以通过一些方法来避免搜索无法访问的......
  • js逆向实战之Bitcoin浏览器交易x-apikey参数加密逻辑
    声明:本篇文章仅用于知识分享实战网址:https://www.oklink.com/zh-hans/btc/tx-list分析过程访问网址,会触发一条数据包。看它的响应内容。就是我们想要获取的内容,找到数据了。可以先尝试直接去访问该url,看能否获取数据。importrequestsurl="https://www.oklink.com/a......
  • 函数参数的解构
    假设我在three.js中有一个项目,我需要一些几何图形,我将硬编码一个对象数组,其中包含它们的x、y和z值以及它们的宽度、高度和深度值,但是这个数组可能来自服务器或第三方api=>constgeometriesraw=[{color:0x44aa88,x:0,y:1,z:0,widt......