作者:范灿华 阿里同城履约物流技术团队
命令模式是一种设计模式,总结了在特定场景下的最佳设计实践。本文将为大家介绍命令模式的模式本质及灵活运用,并通过一个真实的电商履约系统中的库存调用需求为案例,分享其在高复杂调用中的实践。
一、前言
本文是一篇基于同城履约业务中台与库存系统的协同设计过程中使用到命令模式并获得很好成果而撰写的技术分享文章。命令模式是一种设计模式,总结了在特定场景下的最佳设计实践,它是一种间接经验。为了将这种间接经验变为我们可以使用的直接经验,我们需要做到两点:看清模式本质和灵活运用。
1)模式本质:掌握一个设计模式的关键在于发现其核心关注点。每个模式都有一个关注点,例如命令模式的关注点是调用过程,而策略模式和状态模式的类图看起来相似,但它们的运作机制却完全不同:前者关注外部引起的算法变化,后者则关注内部状态的转变变化。通过找到模式的本质关注点,才能真正掌握它。
2)灵活运用:在实际应用中,设计模式的实现方式可能会与教科书上的类图略有不同甚至完全不同。因此,我们不能一味地套用模式,而应该根据实际需求进行量身定制和改进。这意味着我们需要深入了解模式本质,然后根据自己的需求来适当调整模式的用法。有时,你可能会意外地创造出一种新的模式。
针对以上两点,本文首先特别地会从OOA(面向对象分析)的角度去介绍命令模式的模式本质(2.1. 封装调用),然后列举命令模式的不同玩法及其中的原理用作展示该模式的运用灵活性(3. 灵活运用)。最后一个真实的电商履约系统中的库存调用需求为案例(4. 应用案例),这个案例刚需要隔离的变化点是调用,非常适合命令模式。
二、模式本质
2.1 封装调用
直接的调用:命令模式在设计模式的分类中属于行为型模式,它关注的是一种对象之间的调用行为,不管如何,调用行为必然涉及两个角色,他们就是:调用者(Invoker) 和 被调用者(Receiver),基本上他们对应的就是两个对象类,并且Invoker类静态编码依赖Receiver类方法进行调用。如果在不考虑其他因素的情况下,这两者的行为关系可以描述为下图:
然后我们来说一下上面的调用特点:
-
上面的调用只有2个对象,调用者(Invoker)和被调用者(Receiver);
-
其中调用者(Invoker) 是代码静态依赖被调用者(Receiver)的;
-
调用在上面是一个请求过程,这个过程在图中用虚线圈表示;
对象化调用:命令模式就是在以上场景下,把上图中虚线圈调用这个调用过程给显式化、抽象化、实例化。本来只是一个调用的过程,我们把它(这个过程)刻画出来封装为一个具体的对象(Concrete Command),这个对象就是命令对象。调用方将会利用命令对象这个代理来帮忙调用被调用方,所以以上的调用过程就变为了下图所示:
这样一来,调用方和被调用方就没有了直接的耦合关系,也就是说他们 “解耦” 了,概括一下这几对象协作的特点:
-
我们多了一个新的具体命令对象(Concrete Command) 对象的职责就是完成调用请求;
-
命令对象持有被调用方Receiver的引用和请求参数,并描述了如何执行请求,被调用方可以被参数化设置到命令对象中;
-
静态代码依赖变成了调用者(Invoker)依赖命令对象(Concrete Command),命令对象(Concrete Command)依赖被调用者(Receiver)
-
调用者(Invoker)可以完全不知道被调用者接口以及执行请求的具体方式和细节,把这些委托打包给命令对象,从而可以少写很多无谓的执行细节代码。
隔离调用变化:把调用封装起来后,我们解开了调用者(Invoker)和被调用者(Receiver)依赖,那么就可以轻易的允许调用发生变化,这一点很重要,我们很多时候封装调用都因为调用本身在未来容易发生变化,下面列举一些常见的变化:
-
不同场景下,调用者(Invoker)需要调用不同的 被调用者(Receiver);
-
被调用对象的方法发生变化,例如换了一个新版本API;
-
请求需要延迟执行,或者一次调用突然需要调用2个被调用者(Receiver);
使得调用的变化可以做到开闭原则的方式很简单,我们给命令对象实现一个命令接口 (Command),让调用者代码只依赖命令接口,这个接口的每一个具体的命令对象代表了一种变化,真正执行的具体命令是可以在运行态确定的,调用方不再依赖具体的调用命令:
提出变化的角色:如果读者对控制反转比较熟悉的话,那么就很自然知道上述中要实现隔离变化的代码编写,其实就是面向超类型编程。面向超类型编程是把设置实例的控制权交给了依赖关系中的最外层(也就是细节层),在命令模式中,我们把这一层称之为客户端(Client)。换句话说,把调用者(Invoker)、 被调用者(Receiver)和命令对象(Concrete Command)隔离开为互相独立的组件后,自然也需要一个角色去组织起来,道理很简单,积木也是需要有人搭才能千变万化,想变成怎样就是客户端(Client)的需求了。
现在我们又多了一个角色客户端(Client),整个命令模式的基本参与者都全了,可以整体看一下命令模式的类图。
上面是一个比较标准的命令模式的类图,上图中可能大家会比较疑惑的一个点就是被调用者(Receiver)能不能直接实现Command接口,这当然是可以的,但是解耦程度会比较低,我们尽量参数化被调用者,然后做一个傻瓜式的命令。要掌握命令模式,我们还得懂得如何灵活变化去运用它,下面我们看看命令模式的一些基本玩法。
三、灵活运用
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
—— From GoF
设计模式提出者“四人帮”对命令模式的总结非常精辟:参数化请求、记录请求、队列化请求,还可以支持撤销操作。另外还有一种说法是:回调请求就是命令模式的面向对象版。可见在命令对象上面做文章,可以衍生出多种玩法,本章将会详细列举常见的几种。
3.1 组装命令
命令模式最大优点是解开调用者和被调用者(接收方)的耦合,因此我们可以轻易在一个调用中更换调用请求的接收方,而解开后的组装方式有两种:
-
第一是在Client角色中进行静态代码编写;
-
第二种是实现配置化在运行时动态组装;
其中第二种动态组装和拓展点的实现原理是一样的,会根据动态的参数来决定具体执行的命令。我把这种组装的原理描述为下图:
目前有不少框架有类似拓展点的功能的实现,如TMF,IFBF,COLA等。利用拓展点的功能帮忙,我们可以轻松实现以上的方案,当然我建议自己在项目种开发一套,因为你随时都有可能面对框架做不到的变态需求。
3.2 记录命令
如果我们把调用记录下来,我们就能完成很多不可思议的事情。调用能被记录下的信息主要有:调用的顺序、调用的出入参数、调用的状态等。有了历史记录,就可以对调用的历史执行进行回放或者倒退。下面分别介绍 命令撤销 和 命令日志 的两种回放玩法。
命令撤销:命令撤销是发生在命令簇(4.3节有介绍,表示关联关系的命令组)中的一种需求。其实它的原理非常简单,因为命令基本会改变状态,我们给命令接口一个反向的恢复状态的方法(如下面的undo),并且把调用过的命令对象都记录下来,就可以在一个撤销按钮中完成状态恢复操作。(读者可以参考Head First Design Parrtern命令模式的撤销操作)
-
调用者具有多个命令插槽,每个插槽可以设置一个命令实例,共同形成一个命令簇(是个List);
-
调用者维护一个栈,其实是命令插槽的调用历史栈,每一次调用者执行命令就把该命令入栈顶;
-
用户需要倒退命令的时候,将栈顶命令退栈,并执行undo方法,利用栈后进先出的特点完成了历史状态回退功能;
命令日志:我们看一个利用函数式编程的不变性来恢复数据的原理,即在不同的时间或者空间节点中只要执行相同的事件函数调用,就一定会达到一个相同的状态。这个原理被广泛应用于很多系统中,例如数据库系统使用binLog事件和数据快照进行备份恢复,Redis使用RDB快照和AOF事件进行数据恢复,Excel文档数据保存等都是经典的应用场景。
具体来说,对于大型的数据结构而言,我们难以每时每刻都快速存储下它的状态,为了完成记录每时每刻的状态,我们可以通过上次检查点(CheckPoint)之后的所有操作命令(包括参数)都记录下来,当我们需要恢复数据结构的状态的时候,只需从检查点开始按顺序应用这些操作即可。
3.3 组合命令
如果一个命令设计的本身,就是用作封装其他不同命令的组织,我们称这个命令为宏命令。宏命令是一个通用的命令实现类,它类似一种组合模式,持有其他命令的引用,并依次执行他们。因此宏命令最大的作用是我们可以在一次调用中,调用多个命令,宏命令代码如下:
public class MacroCommand implements Command {
Command[] commands;
public MacroCommand(Command[] commands){
this.commands = commands;
}
public void execute(){
for(int i = 0; i < commands.length; i++){
commands[i].execute();
}
}
}
特别注意:特别注意这个模式的特点是宏命令完全是不需要特殊开发的,它是一个可以组合其他命令的容器命令,也等同于普通的命令。只要对3.1节中命令组装工厂进行适当改造,我们也可以把宏命令用在动态的命令组装上,他可以让调用者在一次调用中完成多个调用请求。4.1 节,我们将会把宏命令用到实际的例子中。
3.4 异步命令
异步命令:如果场景允许,我们可以把同步调用设计为异步调用。异步调用对象化后的异步命令对象(Concrete Command)打包了整个请求过程细节,对象本身具有状态,所以可以存储,也可以被传递。
对于调用者和被调用者而言,一般同步的都是强依赖,而异步则是弱依赖,因此这个解耦很大程度上是一个设计问题。同时我们也应该注意到,异步调用设计有着极大的好处也有自身的缺点。
-
同步调用需要阻塞流程,异步调用不阻塞,并立即返回对性能友好;
-
异步具有削峰填谷的能力,可以堆积命令调用,用时间换计算资源;
-
同步编程简单,异步编程比较复杂;
-
同步的程序状态追求强一致性,异步的程序状态追求最终一致性;
队列调度:异步命令对象本质上打包了运算块(参数+接受者+一组操作),所以它可以被不同调用者执行放在任意地址,任意时间执行。例如客户端/调用者可以设置好命令submit到某个队列中(具体实现产品可能是一种中间件,或者定时器),让其他调度者(不同线程)从队列中获取命令执行,从而可以实现请求堆积、请求被按需调度执行(定时调度)等需求。
回调通知:在异步编程中,如果我们想要得到异步的调用结果,我们可以设置一个回调函数,让异步过程执行完毕后通知发起异步的一方。如果这个回调函数设计为接口,并在接口的实现中封装真正的接受者和其操作,这正好就是命令对象的用法,只是看问题的角度不同,所以可以认为命令模式是回调的面向对象的版本。
四、应用案例
介绍完上面的命令模式本质和基础玩法后,这节将介绍一个同城履约中台域的真实案例。该案例应用到了以上提到的组装命令、宏命令、队列化请求等玩法。同城履约域主要负责近场零售商品的配送调度管理,它需要承接多种商家多种配送玩法(业态)的配送需求。其中对商品的物流操作的调度管理有多个通用的节点,包括创单、出库、运输、揽收和妥投等,而这些节点都可能需要去驱动库存的信息流变化,下面从出库节点的「仓单打包出库」服务作为例子介绍履约域与库存的关系。
4.1 识别调用变化
例子:我们用一个设计良好的领域服务组件开始介绍,如下图所示:假设我们有一个「仓单打包出库」的通用领域服务,完成仓单出库履约系统要做的事情有:
1)记录数据:记录真实的出库数量;
2)设置状态:设置仓单为出库状态;
3)操作库存:把占用的业务库存正式扣减掉;
4)推进履约单;推动对应的履约单状态;
库存调用的变化:现在有两个不同的业务身份(淘系商家、外部商家),他们在「仓单打包出库」操作库存是调用不同的库存系统的,如果是直接硬代码编写(大家熟知的if大法),就需要每一次不同的业务身份变更,都要变更这个领域服务。这样的系统是无法维护的,因为履约域具有数百个业务身份,而且所对接的库存系统也是多种多样。
结合上图所示,一个类似这种「仓单打包出库」服务的库存操作步骤可能存在哪些变化点呢?
-
被调用者变化:针对一个履约域的领域服务(如上面的【仓单打包出库】),不同的业务身份都可能调用不同的库存系统;
-
调用者变化:针对同一个库存系统,它可能会被履约域中的其他领域服务调用,如【缺货处理】、【妥投处理】等;
-
调用参数变化:针对同一个库存系统,不止有扣减,还可以有其他调用类型和参数,例如占用、释放、加在途等等;
-
调用过程变化:上面库存的调用,可是需要一次性调用不同的库存系统组合,2个或者3个都有可能;
4.2 实现命令组装
我们意识到了调用的变化,就需要完全的解开库存系统和履约系统的耦合。首先设计一个库存系统代理接口Inventory Receiver,接口的每一个实例都是被调用者(Receiver),它用作封装库存系统提供的API接口,并代表着一个库存系统的一种调用类型。而调用者就是履约域的各个节点,然后,我们再把调用的过程实例化为命令(Command),整体做成了一个完整的命令模式。
调用配置化实现:设计为命令模式后,【仓单打包出库】领域服务成为一个通用的服务,每一步都抽象出一个稳定的步骤,远离具体细节的变化。其中第三步:操作库存,则调用的是库存命令组装服务。这个组装服务实现了根据不同业务身份创建拥有不同Receiver的库存命令实例,整体一个调用变化为下图所示:
正如3.1节提到,目前也有很多框架都能轻松做到这种动态组装,当然手写一个这样的机制也不是什么难事。具体的运行机制很简单,简单描述如下:
-
右边一个通用的领域服务有4个步骤,其中第三步「操作库存」调用的是一个通用的库存组装服务;
-
左边的实现A、B、C是封装了具体库存系统API的Receiver,他们都实现了接口:Inventory Receiver;
-
config.xml是bean的装配配置文件,库存组装服务在运行时根据不同的商家身份,获取对应的Receiver实例设置到Concret Command中成为库存操作命令,并执行该命令;
组合调用:注意,上面的案例中,淘宝商家不仅要调用AIC系统,还需要调用IPM系统,这要求我们在一个Command接口的一次调用中实现AIC和IPM2个系统的调用,我们有两种实现方式:
-
方式1:在Command的接口实现中,把调用AIC Receiver和调用IPM Receiver的代码都写完,提供一个臃肿的接口实现;
-
方式2:设计一个宏命令模式实现(机制见3.2节介绍):
-
把AIC Receiver和IPM Receiver 添加到到宏命令容器中;
-
宏命令接口的逻辑就是依次调用宏命令容器里面的命令;
-
针对组合调用的需求,上面的方式1的解耦程度是不如方式2的,因为如果出现某个业务身份在该领域服务下仅仅只需调用AIC或者IPM的情况,或者又有需要在一个Command接口实现中调用3个库存系统,方式1都需要开发代码,而方式2仅仅配置即可,所以上面的实现2不仅可以代替方式1,还更具有灵活性,这就是松耦合的魅力。
4.3 识别命令簇
在大家熟知的《Head First Design Parttern》书籍中,里面例子重点介绍了一种「遥控器」的设计,遥控器具有多个插槽,也就是可以在一个调用者里面设置多个命令,形成一个命令簇。如果我们能在实际应用中发现这种调用组,而且他们具有关联关系,那么命令模式就可以把这种命令簇及其关联逻辑封装起来,用作应对软件变化。
库存命令簇:上面只给到一个领域服务,但是一个完整的履约域是具有多个状态节点的,而其中在一次履约过程中就有不少节点中的领域服务需要和库存系统交互,例如【履约单取消】、【仓单打包出库】、【缺货处理】、【妥投处理】、【退货回仓】等等,他们不仅有顺序关系,而且通常在设计编码中都有很多共同逻辑,这些有相关性的调用集合,就是一个命令簇。而且,更深一层考虑,他们本身就可以形成一个子领域,配合下图,我们把相关特点列举如下:
-
业务身份:库存调用子领域应该有自己独立的业务身份,并以业务身份为维度组织命令的配置,组装出关联命令簇;
-
库存流程:一个业务订单完整的生命周期中,库存的命令簇必然按顺序调用,有顺序关系的命令簇形成库存调用流程;
-
库存跟踪:库存跟踪是综合了库存流程的实例化和数据化的结果,它刻画的是整个库存调用的生命周期;
-
监控运维:考虑命令簇和多业务身份,调用量会变得极大,那么系统异常问题、库存不足问题、库存查询问题等就需要用到系统自动化级别的监控运维手段,实现这些需求需要库存跟踪的基础;
配置命令簇:如下表所示,每个业务身份都有自己的调用节点组,一个组形成了完整的调用流程。作为一个履约中台系统,经常会新加或者减少业务身份,因此配置化方式组织命令簇及库存调用流程就显得非常必要,当开展新业务的时候,只需一个新的组合配置即可支持,符合软件开发开闭原则,配置化具有以下特点:
-
针对履约域的一个节点(如【履约单创建】),不同的业务身份都可能调用不同的库存系统(下面的AIC\TIC\GSI);
-
不同业务身份即使使用同一个库存系统(如AIC系统),他们的库存协调所需要的命令组合也可能是不一样的;
-
针对同一个节点(如【履约单创建)和同一个库存系统,不同业务身份调用的参数也可能不一样,因此参数也可以配置化;
空命令(NoCommand):上表所示,一行代表一个命令簇,一个命令簇可能在某个节点(如「缺货处理」)是不做任何事的,这个时候我们可以用到NoCommand。NoCommand 对象是一个空对象的例子,当你不想返回一个对象的时候,空对象就很有用,如上图所示,「外单」的业务身份在「缺货处理」这个节点上,是不需要执行库存调用的,但返回null给调用者就会出现异常,所以这个时候我们可以把不调用任何库存系统的NoCommand实现返回。
库存调用跟踪:我们以业务身份为维度组织了命令簇,实现了命令簇组织的配置化,并且以命令簇的顺序执行特性绘制了库存执行流程。然而命令簇配置和库存流程都是静态的,为了运维和管控好库存调用,我们还需要关注每个订单的执行情况,做到对每个调用都精确跟踪。
想要跟踪履约单的库存调用,我们需要履约单当前库存状态和历史的库存执行流水。我们可以用库存跟踪单刻画一个订单实例当前状态,把命令的每次执行结果作为库存的执行流水,并用库存跟踪单把业务履约单和命令对象、命令流水等库存的串联关联起来,串联的ER图如下:
库存跟踪单可以告诉我们的信息大概有以下几点:
-
该订单命中了哪个业务身份配置;
-
该订单库存生命周期有哪些调用组,调用的顺序是怎样的;
-
该订单当前在执行哪个调用,该调用是被什么履约域事件触发的;
-
该订单历史上执行过哪些调用(命令),调用的出入参是什么,是否成功等;
命令簇顺序管理:另一方面,库存的跟踪还包括保证正确的库存调用顺序,例如某个业务的调用组的执行流程如下:
假设以上的调用都是异步调用,通过接收履约域的标准异步消息进行,因为是异步,所以就有可能「退货」消息先到,然后「妥投」消息后到,发生这种情况,就会导致库存调用出现各种可能的问题。解决该方法也很简单,利用一个可以处理当前状态和事件的库存状态机+延迟执行(Scheduler的队列命令)就可以保证流程的正确执行。
4.4 识别调用边界
这一节中,我们围绕库存调用把范围拓展到了命令簇,并且为命令簇为基础刻画了一个业务的完整库存调用生命周期,实现了从静态的库存流程设计到动态的库存跟踪掌控。这个过程我们发现库存的调用内容完全和履约主业务没有强关联,所以我们可以考虑把这些内容从履约业务系统中隔离出来,给他们划分一个清晰的边界。
库存界限上下文:库存调用作为履约系统的子域属于一个小分支,并不在核心流程内,即使不做库存操作,也不会影响一个履约主流程的运行,所以他属于弱依赖,因此,我们可以出于组件独立性的考虑,把库存调用和核心域(履约主系统)划分一个明确系统边界,让库存独立形成一个库存界限上下文。
如上图所示,除了为库存调用划分边界,成立库存界限上下文外,核心域(履约主系统)在调用上还可以划分其他边界和上下文。假设履约域还要驱动计费的流转,那么我们还可以划分出一个计费界限上下文,不过这就是另一个话题了。
库存调用协同系统:综合上面上下文中的大规模的配置化管理、库存的流程管理、库存的跟踪、监控、运维后,我们完全有理由为这个库存调用上下文建立一个独立部署的系统,其作用类似中间件的作用。
现在,我们所有的库存需求都可以在该系统中实现,而且部署、变更都不会影响履约主系统,这是一个很好的实践。下面我们从监控运维、查询可视化、新业务接入分别看看这个围绕库存调用的系统所能做的事情。
调用监控运维:方法调用都需要面临一个调用失败的问题,失败的原因可能有业务异常、系统异常,这些异常可能是可以重试成功的,有些则无法重试成功,只能人工插入管理。在一些比较小的系统,或者调用量不大的系统,发生这样的问题次数不多,我们可以通过系统日志简单运维,但像履约域每日上百万,超千万级别的调用,量表到质变,管理运维就是一个新的问题了。
-
调用状态记录:那么这些调用异常的状态应该记录在哪里?现在我们有一个很好的答案就是用命令对象(Command),并可以把命令对象持久化到数据库中等待使用。
我们记录调用的状态包括很多,调用的出入参数、命中的业务配置,触发调用的事件等等
-
调用失败重试:对于系统宕机,网络异常等调用失败问题,我们可以通过重试一定次数来解决,而重试本身也是一个问题,从失败记录、批量发现、定时重试都可以通过一个统一的模块管理,和业务流程无关。
-
调用异常报表:对于无法重试成功的异常调用,简单的日志告警容易使人疲劳,或许这个时候定期出一个调用异常报表更适合,在数据库中的命令对象离线化后,做这种事就很简单。
-
调用数据订正:当我们发现库存调用问题后,我们很大情况下解决方案是需要订数据,然后重试,因为命令对象记录了调用参数,因此修改参数重试命令也是简单可行。
我们现在从调用的监控运维视角,看一下系统对调用监控运维自动化的运作流程:
命令调用可视化:在一个调用如此庞大的系统,即使是在日志加加了trace日志作为跟踪,可能也容易造成混乱,让运维人员在库存查询时变得很痛苦,但如果我已经用库存跟踪单刻画了所有调用的状态信息,那么我基本可以通过一个订单id,找到该库存跟踪单,并可以在数据库中查到该跟踪单的所有库存调用命令和调用流水。我们把这些数据可视化,将会节省运维和开发排查问题的大量时间。
新业务开发流程:现在我们回归业务,一个系统设计再厉害,如果在应变新需求的时候需要大动干戈的修改旧代码,那这个系统必然是一个设计失败的系统。所以我们现在讨论一下库存系统的新需求接入流程,用以评估系统的可用性,我们将分别从命令变化的几个方面的需求进行讨论:
1)新业务身份接入:履约中台系统,承接各种订单的近场履约服务,而业务总是有新模式,所以就会有新的业务身份,也可能会产生新的库存调用变化,但因为履约域、库存系统都是通用的,消息/接口也是标准的,所以新的业务身份,只需要加配置文件(配置调用组、流程、调静态用参数)即可运行。
2)新库存系统接入:如果新的业务身份需要调用的库存系统之前没接入过呢?对比新业务身份接入,我们只是缺少配置的Receiver而已,因此只需要创建新的Receiver实例,封装需要接入的库存系统API,即可完成需求。完全也是拓展化开发。
3)新调用节点的接入:一个新的业务身份要调用新的节点,或者某个旧的业务身份需要加一个新节点,因为履约域的系统是标准的消息,所以也不需要修改,配置即可。
4)新的调用系统的接入:如果除了履约域要接入外,其他系统也需要接入库存调用系统,那么就需要做一个防腐层,把新系统的消息转化为库存协同系统的标准消息即可。
库存系统的所有变化,无非就是以上几个方面,开发应对基本的新代码及新配置,做到了节点,Receiver的完全解耦复用,同时复用系统所有能力(如可视化、监控运维等)。而且最重要的是,即使新增平台的能力,也是所有业务可以享用的 。到这里,库存系统的基本功能介绍完毕,而且实践各个方面都能证明它能极大提升我们的生产力。
五、结束语
相比起一般的设计模式例子,本文的案例更系统的是把请求调用进行了多方位的管理,包括命令组织为流程、调用的可视化、调用的批量处理等等,而做这些的前提都是把请求调用封装起来,本质上并没有变化,我们的关注点一直都在调用上面。
另外,文中围绕命令模式的本质,总结出了几种经典玩法背后的形式,几乎涵盖了调用变化的主流场景。当发现一个应用场景主要变化点在调用的时候,我们就可以考虑是否利用重构工具把请求封装起来,以便在需求再次变化的时候,尝试通过记录命令、配置命令、组合命令、异步命令等方式进行拓展性开发,最大限度降低开发风险和维护成本。命令模式如此,其他设计模式也是如此。
最后,一个设计模式的应用范围也不要限制在一个独立部署的系统内,也可以拓展到系统之间的设计中。例如经典的观察者模式,在系统之间的应用就非常之广。本文案例中的命令模式也是跨系统的应用,甚至可以跨组织架构之间的应用。为什么不需要被系统所限制呢?因为系统与系统之间的调用本质依旧是组件和组件之间的调用,只是其中的边界和方式有所改变。
引用
《Head First设计模式》Eric Freeman & Elisabeth Freeman with Kathy Sierra & Bert Bates [著].O`Beilly Taiwan公司[译].2007.中国电力出版社