热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[01][01][19]命令模式详解

文章目录1.定义2.应用场景3.在业务场景中的应用4.在源码中的体现5.优缺点5.1优点5.2缺点1.定义命令模式(CommandPattern)是对命令的封装,每一个命令都是

文章目录

  • 1. 定义
  • 2. 应用场景
  • 3. 在业务场景中的应用
  • 4. 在源码中的体现
  • 5. 优缺点
    • 5.1 优点
    • 5.2 缺点


1. 定义

命令模式(Command Pattern) 是对命令的封装,每一个命令都是一个操作∶请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作.命令模式解耦了请求方和接收方,请求方只需请求执行命令,不用关心命令是怎样被接收,怎样被操作以及是否被执行·等.命令模式属于行为型模式

原文∶Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests, and support undoable operations.
解释∶将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能

在软件系统中,行为请求者与行为实现者通常是一种紧耦合关系,因为这样的实现简单明了.担紧耦合关系缺乏扩展性,在某些场合中,当需要为行为进行记录,撤销或重做等处理时,只
修改源码.而命令模式通过为请求与实现间引入一个抽象命令接口,解耦了请求与实现,并中间件是抽象的,它可以有不同的子类实现,因此其具备扩展性.所以,命令模式的本质是解耦命令请求与处理


2. 应用场景

当系统的某项操作具备命令语义时,且命令实现不稳定(变化),那么可以通过命令模式解耦请求与实现,利用抽象命令接口使请求方代码架构稳定,封装接收方具体命令实现细节.接收方与抽象命令接口呈现弱耦合(内部方法无需一致),具备良好的扩展性.命令模式适用于以下应用场景



  • 现实语义中具备"命令"的操作(如命令菜单,shell 命令…)

  • 请求调用者和请求的接收者需要解耦,使得调用者和接收者不直接交互

  • 需要抽象出等待执行的行为,比如撤销((Undo) 操作和恢复(Redo) 等操作

  • 需要支持命令宏(即命令组合操作)

首先看下命令模式的通用 UML 类图

从 UML 类图中,我们可以看到,命令模式主要包含四种角色



  • 接收者角色(Receiver)∶该类负责具体实施或执行一个请求;命令角色(Command)∶定义需要执行的所有命令行为

  • 具体命令角色(ConcreteCommand) 该类内部维护一个接收者(Receiver) 在其 execute() 方法中调用 Receiver 的相关方法

  • 请求者角色(Invoker)∶接收客户端的命令,并执行命令

从命令模式的 UML 类图中,其实可以很清晰地看出∶Command 的出现就是作为 Receiver 和 Invoker 的中间件,解耦了彼此.而之所以引入 Command 中间件,我觉得是以下两方面原因

解耦请求与实现∶即解耦了 Invoker 和 Receiver,因为在 UML 类图中,Invoker 是一个具体的实现,等待接收客户端传入命令(即 Invoker 与客户端耦合 ),Invoker 处于业务逻辑区域,应当是一个稳定的结构.而 Receiver 是属于业务功能模块是经常变动的如果没有 Command,则 Invoker 紧耦合 Receiver,一个稳定的结构依赖了一个不稳定的结构,就会导致整个结构都不稳定了.这也就是 Command 引入的原因∶不仅仅是解耦请求与实现,同时稳定(Invoker) 依赖稳定(Command),结构还是稳定的

扩展性增强∶扩展性体现在两个方面



  • Receiver 属于底层细节,可以通过更换不同的 Receiver 达到不同的细节实现

  • Command 接口本身就是抽象的,本身就具备扩展性;而且由于命令对象本身就具备抽象,如果结合装饰器模式,功能扩展简直如鱼得水

注∶在一个系统中,不同的命令对应不同的请求,也就是说无法把请求抽象化,因此命令模式中的 Receiver 是具体写现;但是如果在某一个模块中,可以对 Receiver 进行抽象,其实这就变相使用到了桥接模式(Command 类具备两个变化的维度∶Command 和 Receiver),这样子的扩展性会更加优秀

举个生活中的例子,相信80后的小伙伴应该都经历过普及黑白电视机的那个年代.黑白电视机要换台那简直不容易,需要人跑上前去用力掰动电视机上那个切换频道的旋钮,一顿"啪啪啪"折腾下来才能完成一次换台.如今时代好了,我们只需躺沙发上按一下遥控器就完成了换台.这就是用到了命令模式,将换台命令和换台处理进行了分离

另外,就是餐厅的点菜单,一般是后厨先把所有的原材料组合配置好了,客户用餐前只需要点菜即可,将需求和处理进行了解耦


3. 在业务场景中的应用

假如我们自己开发一个播放器,播放器有播放功能,有拖动进度条功能,停止播放功能,暂停功能,我们自己去操作播放器的时候并不是直接调用播放器的方法,而是通过一个控制条去传达指令给播放器内核,那么具体传达什么指令,会被封装为一个一个的按钮.那么每个按钮的就相当于是对一条命令的封装.用控制条实现了用户发送指令与播放器内核接收指令的解耦.下面来看代码,首先创建播放器内核 GPlayer 类

public class GPlayer {public void play(){System.out.println("正常播放");}public void speed(){System.out.println("拖动进度条");}public void stop(){System.out.println("停止播放");}public void pause(){System.out.println("暂停播放");}
}

创建命令接口 IAction 类

public interface IAction {void execute();
}

然后分别创建操作播放器可以接受的指令,播放指令 PlayAction 类

private GPlayer gplayer;public PlayAction(GPlayer gplayer) {this.gplayer = gplayer;}public void execute() {gplayer.play();}
}

暂停指令 PauseAction 类

public class PauseAction implements IAction {private GPlayer gplayer;public PauseAction(GPlayer gplayer) {this.gplayer = gplayer;}public void execute() {gplayer.pause();}
}

拖动进度条指令 SpeedAction 类

public class SpeedAction implements IAction {private GPlayer gplayer;public SpeedAction(GPlayer gplayer) {this.gplayer = gplayer;}public void execute() {gplayer.speed();}
}

停止播放指令 StopAction 类

public class StopAction implements IAction {private GPlayer gplayer;public StopAction(GPlayer gplayer) {this.gplayer = gplayer;}public void execute() {gplayer.stop();}
}

最后创建控制条 Controller 类

public class Controller {private List<IAction> actions = new ArrayList<IAction>();public void addAction(IAction action){actions.add(action);}public void execute(IAction action){action.execute();}public void executes(){for (IAction action:actions) {action.execute();}actions.clear();}
}

从上面代码来看,控制条可以执行单条命令,也可以批量执行多条命令,下面来看客户端测试代码

public class Test {public static void main(String[] args) {GPlayer player = new GPlayer();Controller controller = new Controller();controller.execute(new PlayAction(player));controller.addAction(new PauseAction(player));controller.addAction(new PlayAction(player));controller.addAction(new StopAction(player));controller.addAction(new SpeedAction(player));controller.executes();}
}

运行结果

正常播放
暂停播放
正常播放
停止播放
拖动进度条

由于控制条已经与播放器内核解耦了,以后如果想扩展新命令,只需增加命令即可,控制条的结构无需改动


4. 在源码中的体现

首先来看 JDK 中的 Runnable 接口,实际上 Runnable 就相当于是命令的抽象,只要是实现了 Runnable 接口的类都被认为是一个线程

public interface Runnable {public abstract void run();
}

实际上调用线程的 start() 方法之后,就有资格去抢 CPU 资源,而不需要我们自己编写获得 CPU 资源的逻辑.而线程抢到 CPU 资源后,就会执行 run() 方法中的内容,用 Runnable 接口把用户请求和 CPU 执行进行了解耦

然后,再看一个大家非常熟悉的 junit.framework.Test 接口

public interface Test {public abstract int countTestCases();public abstract void run(TestResult result);
}

Test 接口中有两个方法,第一个是 countTestCases() 方法用来统计当前需要执行的测试用例总数.第二个是 run() 方法就是用来执行具体的测试逻辑,其参数 TestResult 是用来返回测试结果的.实际上我们在平时编写测试用例的时候,只需要实现 Test 接口即便认为就是一个测试用例,那
么在执行的时候就会自动识别.实际上我们平时通常做法都是继承 TestCase 类,我们不妨来看一下 TestCase 的源码

public abstract class TestCase extends Assert implements Test {public void run(TestResult result) {result. run(this);}
}

实际上 TestCase 类它也实现了 Test 接口.我们继承 TestCase 类,相当于也实现了 Test 接口,自然也就会被扫描成为一个测试用例


5. 优缺点


5.1 优点



  • 通过引入中间件(抽象接口),解耦了命令请求与实现

  • 扩展性良好,可以很容易地增加新命令

  • 支持组合命令,支持命令队列

  • 可以在现有命令的基础上,增加额外功能(比如日志记录…,结合装饰器模式更酸爽)


5.2 缺点



  • 具体命令类可能过多

  • 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构,解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难(不过这也是设计模式带来的一个通病,抽象必然会引入额外类型;抽象肯定比紧密难理解)


推荐阅读
  • 【重识云原生】第四章云网络4.8.3.2节——Open vSwitch工作原理详解
    2OpenvSwitch架构2.1OVS整体架构ovs-vswitchd:守护程序,实现交换功能,和Linux内核兼容模块一起,实现基于流的交换flow-basedswitchin ... [详细]
  • SpringBoot简单日志配置
     在生产环境中,只打印error级别的错误,在测试环境中,可以调成debugapplication.properties文件##默认使用logbacklogging.level.r ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 本文讨论了如何在codeigniter中识别来自angularjs的请求,并提供了两种方法的代码示例。作者尝试了$this->input->is_ajax_request()和自定义函数is_ajax(),但都没有成功。最后,作者展示了一个ajax请求的示例代码。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 本文介绍了如何使用python从列表中删除所有的零,并将结果以列表形式输出,同时提供了示例格式。 ... [详细]
  • iOS Swift中如何实现自动登录?
    本文介绍了在iOS Swift中如何实现自动登录的方法,包括使用故事板、SWRevealViewController等技术,以及解决用户注销后重新登录自动跳转到主页的问题。 ... [详细]
  • 本文详细介绍了git常用命令及其操作方法,包括查看、添加、提交、删除、找回等操作,以及如何重置修改文件、抛弃工作区修改、将工作文件提交到本地暂存区、从版本库中删除文件等。同时还介绍了如何从暂存区恢复到工作文件、恢复最近一次提交过的状态,以及如何合并多个操作等。 ... [详细]
  • 设计模式——模板方法模式的应用和优缺点
    本文介绍了设计模式中的模板方法模式,包括其定义、应用、优点、缺点和使用场景。模板方法模式是一种基于继承的代码复用技术,通过将复杂流程的实现步骤封装在基本方法中,并在抽象父类中定义模板方法的执行次序,子类可以覆盖某些步骤,实现相同的算法框架的不同功能。该模式在软件开发中具有广泛的应用价值。 ... [详细]
  • 本文介绍了Python语言程序设计中文件和数据格式化的操作,包括使用np.savetext保存文本文件,对文本文件和二进制文件进行统一的操作步骤,以及使用Numpy模块进行数据可视化编程的指南。同时还提供了一些关于Python的测试题。 ... [详细]
  • 本文介绍了关系型数据库和NoSQL数据库的概念和特点,列举了主流的关系型数据库和NoSQL数据库,同时描述了它们在新闻、电商抢购信息和微博热点信息等场景中的应用。此外,还提供了MySQL配置文件的相关内容。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • AFNetwork框架(零)使用NSURLSession进行网络请求
    本文介绍了AFNetwork框架中使用NSURLSession进行网络请求的方法,包括NSURLSession的配置、请求的创建和执行等步骤。同时还介绍了NSURLSessionDelegate和NSURLSessionConfiguration的相关内容。通过本文可以了解到AFNetwork框架中使用NSURLSession进行网络请求的基本流程和注意事项。 ... [详细]
author-avatar
mobiledu2502862117
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有