一、 程序员老陈的困境
铃铃铃,一阵吵闹的手机铃把昨晚通宵升级睡的迷迷糊糊的老陈惊醒。
“喂,我刚睡下,什么事?”
“紧急bug,现在赶紧修复一下,领导在旁边盯着,你昨晚升级的代码有问题,现在真实环境中部分客户开通订单出错,客户投诉激烈,扬言1小时内不处理完就要投诉到12315!”
“我昨晚就改了点退订相关代码,怎么会影响到订购呢,又是谁把我坑惨了!”
万般无奈的老陈耷拉着浆糊的脑袋,打开电脑紧急修复bug,他昨晚修改的是订单系统将订单信息完整同步至某计费系统的代码。
组装接口报文并调用过程:依据与某计费系统的接口规范,报文结构一共分为四大部分,参数多达几十个;接口对应的订单业务流程有六种:订购/退订/变更/审批/试用/续订订单,每种订单业务流程下每个part中报文参数最终的值不尽相同,报文组装完发送给某计费系统处理。
他看到的两个接口调用步骤代码(伪代码代替)是这样的:接口调用步骤一伪代码
真实的代码可能有几千行,老陈只注意到退订分支客户不欠费情况下的处理,却忽视了part2中订购流程客户不欠费情况下有部分代码也有关联。
几个小时过去了,老陈从头到尾研究了一遍代码,因为事态的重要性,他把组装报文步骤的所有if else分支都过了好几遍,一再确认对其他代码无影响后,一共提交了10行代码,然后到测试环境重新部署验证,验证后提交了宝贵的10行代码。经过一层层审批,老陈获得了操作真实环境的权限,更新完成后,老陈本着负责任的心态,亲自在真实环境验证。
真实环境验证无误后,他要处理因为自己的失误造成的客户订单异常数据,想尽一切办法修复错误数据,真实环境运行稳定后,此时已日落西山,他已连续工作36小时。老陈很累很辛苦;在外人看来,第二天他工作的价值也就那10行代码,如果他够仔细,多花半小时检查一遍代码,也许就可以避免。
真的是他不够仔细才造成的严重后果吗?您可以先思考一分钟。二、问题分析
细心的您可能已经发现问题,组装报文的代码看上去实在是复杂,几个方法中满屏幕都是ifelse分支,几百上千行代码看上去眼花缭乱,理解透彻所有业务逻辑也比较困难。
还有个严重的问题:如果某需求方提了个很合理且看上去不复杂的需求,例如某客户类型在退订某产品时,需要增加特殊分支处理。此时,您已经预见到,这看上去很简单的需求在如此臃肿的代码结构中实现起来是多么头疼的一件事,您必须要从头到尾理解一遍if else分支,考虑在所有退订订单业务流程的判断分支下,此特殊处理会造成什么样的影响,此时您心里可能是崩溃的。
如果您存在侥幸心理:测试同事应该能帮我测完所有测试用例,有问题会及时发现。您想多了,由于此部分代码为所有产品、所有订单业务流程和所有客户类型下共用部分,测试用例可能有8*4*20=640个。因为新增了一个不复杂的需求,您需要单独占用640个测试用例的测试资源,测试同事会不会感到很崩溃。
三、解决问题思路
示例中的代码充斥着if -else判断订单业务流程的代码,所以是不是可以把每种订单业务流程的代码隔离开,即订购/退订/续订/变更等均有自己独立处理的类,互不干扰。进一步考虑,若某需求方提出需求,新增一种特殊操作流程,按照原来的实现,又得新增一些if else分支判断;而在拆分各自流程的类后,只需新增一个类即可,完全不担心影响到其他代码。
进一步研究代码,报文由4个部分组成,如果以后某需求方改动规范,要求新增一个部分呢?好吧,那我们又要对以上的几个类一个个修改了。
可是,您真的打算这样做吗?真实环境中的代码经过千锤百炼,运行得很稳定,如果新增一个需求,就要修改一遍以前的代码,那以前的测试用例全部需要重新测一遍,否则无法保证质量。这样不仅产生大量重复劳动,且毫无价值。
思路渐渐清晰,要设计软件结构,就要请出强大的明星设计原则了: 开闭原则(OpenClosed Principle)开闭原则的定义:Softeware entities like classes,modules and functions should be open for extension but closed formodifications. 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
四、选择设计模式
基于开闭原则,老陈希望把订购/退订/退款/变更等流程代码完全独立,互不影响;且希望以后接口规范修改,报文增加一个part时,不修改原有代码,只做扩展。
要解决这个问题,先引入一个概念:产品族,所谓产品族,是指位于不同产品等级结构,功能相关联的产品组成的家族。如图所示:
图中一共有四个产品族,分布于三个不同的产品等级结构中。只要指明一个产品所处的产品族以及它所属的等级结构,就可以唯一确定这个产品。
再引入一个概念: 抽象工厂, 所谓的抽象工厂是指一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象,这些具体工厂具有共同的特性,抽象出共性,就是抽象工厂。用图来描述:
有请抽象工厂模式闪亮登场:
正式定义:抽象工厂模式是提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体类。抽象工厂有以下几个角色:
对应到老陈的困境,可以做如下设计:将目标报文抽象成一个产品族组成的具体产品,报文各part抽象成各个产品族,各订单业务流程抽象成各产品等级结构。要生成一个完整报文,必须在某产品等级结构下,生成各part的报文,并组装,示意图如下:
五、设计模式的实现
对应上文做的设计,UML类图如下,由于图大小有限,故只画了订购/退订两个流程和报文中的Part1和Part2:
类说明如下:
代码实现如下:
Package类结构展示:
/*抽象工厂接口,包含生成抽象part的成员方法*/
/*生成目标报文的客户端类,初始化时指定具体的工厂类型,调用此类生成目标报文*/
/*生成具体工厂的静态工厂*/
/*静态工厂配套使用的枚举类型*/
/*生成part1报文抽象出的接口,part2,part3,part4同理,不再列举*/
/*退订业务流程对应的具体工厂,同理,创建/变更/退款等流程也有自己的具体工厂*/
运行结果,表明随着具体工厂的替换,生成了相应业务流程的结构体:
六、总结
用抽象工厂实现了组装报文过程,有如下优点:
1. 易横向扩展:若要新增一个流程,比如说退款,也就是增加一个产品等级结构,只要增加一个具体工厂类负责新增加出的产品族即可,轻松扩展,示意图如下:
2. 易纵向扩展:若某需求方提出接口规范要修改,新增报文part5,也就是增加了一个产品族,可新增具体实现,不会影响到旧的组装报文具体实现,比较容易扩展,示意图如下:
3. 封装性好,解耦:抽象工厂模式基于面向接口编程思想,具体的创建实例与客户端分离,客户端是通过它们的抽象接口操纵实例,组装报文的具体类名也被具体工厂的实现分离,不会出现在客户端代码中。高层模块只关心抽象的接口,不关心对象是如何创建出来的,这些都由具体工厂类负责。
优化后的代码在真实环境中已稳定运行大半年,老陈再也不用担心升级维护或者新增功能而影响到旧代码了,极大提高了生产效率,使用抽象工厂模式达到了插件式编程的目的,他终于可以抽一点时间出来陪陪女朋友了。
等等,真正结束了吗? 远远不止这些,老陈只是解决了庞大软件系统的一小部分而已。调用某计费系统的接口分为两个步骤,即:异步调用,回调反馈。本篇只解决了异步调用组装报文过程,而回调反馈要根据业务流程的不同,区分不同的产品,每个具体产品有不同实现,需要使用另一种设计模式,请看下回分析…