流程控制引擎组件化
在较大规模的业务系统中经常会有这样的模块,它按照一定的业务流程调用其它模块来实现一定的业务逻辑,我们姑且称之为流程引擎。这里称之为引擎有两层含义,一、突显其在业务系统的核心重要位置。二、它又是复杂不好维护的,通常由资深程序员把持。这样的引擎不仅代码繁多,与各个模块的接口复杂,并且一定程度对外是不透明的,就像一个黑盒模块。当一个新手想对其进行哪怕是一点点修改的时候都将会是一个灾难。本文就来讨论如何将这样一个庞大的引擎进行组件化改造,使其拥有代码级的流程图,使代码的维护难度降低一个量级,让新手也可以很快知道要如何修改代码,给引擎增加新的功能。
流程控制引擎组件化
在较大规模的业务系统中经常会有这样的模块,它按照一定的业务流程调用其它模块来实现一定的业务逻辑,我们姑且称之为流程引擎。这里称之为引擎有两层含义,一、突显其在业务系统的核心重要位置。二、它又是复杂不好维护的,通常由资深程序员把持。这样的引擎不仅代码繁多,与各个模块的接口复杂,并且一定程度对外是不透明的,就像一个黑盒模块。当一个新手想对其进行哪怕是一点点修改的时候都将会是一个灾难。本文就来讨论如何将这样一个庞大的引擎进行组件化改造,使其拥有代码级的流程图,使代码的维护难度降低一个量级,让新手也可以很快知道要如何修改代码,给引擎增加新的功能。
我们来看两张图,分别是数控机床和机器人流水线。他们都是自动化的,就像我们的业务流程引擎一样。数控机床简洁、高效。但维护的时候需要专业人员,想增加新功能几乎不可能。机器人流水线,透明,看着有点复杂。但可以随时对流水线上任意部分进行升级,改造,替换。回到我们的业务流程引擎,我们到底希望是上面的哪一种?不难想到,由于业务需求的易变性,我们更倾向于第二种方案。但往往我们的代码是第一种情况,并且代码的组织是混乱的,并不是数控机床那种优雅。
数控机床
机器人流水线
当我们面对一个不熟悉的引擎的时候,我们看到的是下面这样子。
当我们花了一个月时间,仔细研读了几万行的代码后,我们看到的是下面这样子。
虽然我们已经了解了引擎内部的情况,但就像图中所描绘的样子,当要给引擎增加一个新的功能的时候,新增的功能和代码很容易影响原来正确的系统,一个小局部(零件)的问题,都会影响到整个系统。那么,我们如何来改变现有的流程引擎,以实现方便地增加新的功能?
如上图所示,如果我们的流程是一个流水线的话,我们就可以轻易地在流水线中增加新的组件,以实现新的功能。流程组件具有这样的特征:每个组件都是一个独立的个体,不受其它组件的影响。组件具有统一的接口,新增的组件可以方便和原有的组件一起工作。可以根据业务需要挑选合适的组件,组成一个新的业务流程。当把这些组件组合到一起,就形成了组件链表的概念。一个个独立的组件,就像链表里面的一个个节点,组件节点通过组件指针连接起来。一个流程的执行过程就像是对这个链表的遍历。并且这个链表不仅可以只是个单链表,而且可以是多分支链表,以应对流程中多条件分支的情况。
下面我们来看看代码是如何来实现的。
组件节点:
例子:
这里的Execute()函数就是实现每个业务组件具体功能的地方,在这个函数里面可能调用其它模块。
空组件节点:
业务组件节点:
开闭原则:每个组件都是一个独立体(类),组件之间是一种松耦合的关系。流程中某个组件的改动,或者新增加一个组件,并不会影响到其它组件。支持以一种“对扩展开放,对修改封闭”的方式来为流程增加新的功能。从而不会将错误引入原有的稳定、正确的代码中。组件就像流水线上的每个节点,独立可替换。
流程组装:每个组件都有一个统一的编号ID。初始化时,通过按一定顺序读入这些ID,来进行流程的组装。流程中每个功能模块的调用顺序不再是固定写死的,而是在初始化时动态配置生成的。从而使流程功能的调整(增加,删除)变得很容易,不再用修改代码,而是调整流程组装配置列表的参数。
由于流程可能是复杂的,具有复杂的分支情况,这里我们设计了一套编码方法来应对这个情况。
流程编码方法:
当我们有如下流程图需要实现的时候,下面图片可另外窗口打开。
就可以按上面的编码方案生成相应的流程代码,注意这里面说的是代码。这里的流程组装配置列表与流程图是一一对应的。从流程组装配置列表就可以轻松看到计费控制的整个流程图,而不用再发很多时间去了解代码,进而再画流程图。代码化的流程图是自维护的,即跟实际的情况是同步的,不再需要另外地去维护。想像一下,当你接到一个需要维护的复杂引擎的时候,这个引擎的流程图就在你面前,而不再需要你花费一个月时间,阅读数万行代码,自己一点点去画出来。这是何等的进步——The source code is the design
流程组装配置列表
组件化的优点:
a)将流程和被流程调用的子模块进行解耦。例如:批价函数,m_pPriceGuiding->ExecuteRating();在现有的在线流程中共有38处地方进行了调用,而按新的设计方案只会在一个组件(ExecuteRating)中进行调用,要用的批价的地方,只要将其加到流程组装配置列表即可。可以想象,如果ExecuteRating()函数接口改变了,对于原有的在线流程将会是一个灾难,而新的设计将可以轻松应对。注意:这并不是一个特例,在流程中几乎所有对子模块的调用都存在这种情况。
b)组件的复用和移植。新的设计改变了原来以整个流程为最小单位的方式,而是一个组件为系统的最小单位。每个组件都是功能独立的,互不影响的方法集。不仅可以在不同流程间进行复用,甚至于可以跨版本地复用。包括组件移植时,成本也是很低的,只要将组件的代码文件进行移植即可,移植的是文件而不是代码。
c)一些组件会趋于稳定。原来以流程为最小单位设计,任何的改变都会影响到整个流程,导致每次升版时流程的代码总是有很大的变化。而新的设计以组件为系统最小的单位,有一些组件会趋于稳定,一定程度上保持不变。从而分隔了系统中变化和不变的部分。使系统从整体上更加健壮。
下面介绍一些代码的细节。
RatingCtrlComponent.h
#ifndef RATINGCTRLCOMPONENT_H_ #define RATINGCTRLCOMPONENT_H_ class RatingCtrlComponent { public: virtual ~RatingCtrlComponent() { } /** * @brief 组件流程执行接口 * * @param pRatableEvent [in/out] * @return bool */ virtual bool Execute(TRatableEvent& pRatableEvent) = 0; }; #endif
RatingCtrlDectorator .h
#ifndef RATINGCTRLDECTORATOR_H_ #define RATINGCTRLDECTORATOR_H_ #include "RatingCtrlComponent.h" class RatingCtrlDectorator : public RatingCtrlComponent { public: RatingCtrlDectorator(RatingCtrlComponent* pComponentA) : m_pComponentA(pComponentA) { } virtual ~RatingCtrlDectorator() { } /** * @brief 组件流程执行接口 * * @param pRatableEvent [in/out] * @return bool */ virtual bool Execute(TRatableEvent& pRatableEvent) = 0; protected: RatingCtrlComponent* m_pComponentA; ///< 该组件的下一个组件(A分支) }; #endif
ComponentFactory.h
#ifndef COMPONENTFACTORY_H_ #define COMPONENTFACTORY_H_ #include <list> #include <vector> #include "ComponentDefine.h" #include "RatingCtrlComponent.h" class ComponentFactory { public: ComponentFactory() { } virtual ~ComponentFactory() { ClearComponent(); } /** * @brief 装配组件 * * 根据编码[一个数组]装配组件。 * 将各个组件构成一个流程分支链表。 * * @param viComponentID [in] * @return RatingCtrlComponent 流程的头结点 */ RatingCtrlComponent* BuildComponent(vector<int>& viComponentID); void ClearComponent(); protected: virtual RatingCtrlComponent* CreateComponent(int iComponentID, RatingCtrlComponent* pComponent, list<RatingCtrlComponent*>& lpComponent) = 0; private: list<RatingCtrlComponent*> m_lpComponent; vector<RatingCtrlComponent*> m_vpComponent; }; #endif
ComponentFactory.cpp
#include "ComponentFactory.h" void ComponentFactory::ClearComponent() { vector<RatingCtrlComponent*>::iterator itr; for (itr = m_vpComponent.begin(); itr != m_vpComponent.end(); ++itr) { delete * itr; *itr = NULL; } } /* 下面的代码很短,却完了一件精妙的工作.组件以一种近似网状的结构组织起来, 就像所有的网络流算法一样,程序的执行过程就是选择一条路径遍历的过程. 不同的是这边的分支是根据业务不同的而进行选择的。如此复杂的想法却以 这么简单的方式来实现,甚为精妙!-----by liang.shishi */ RatingCtrlComponent* ComponentFactory::BuildComponent(vector<int>& viComponentID) { if (viComponentID.empty()) return NULL; m_lpComponent.clear(); RatingCtrlComponent* pComponent = NULL; vector<int>::iterator itr = viComponentID.end() - 1; // 增加默认基础类 if (*itr != NONE_COMPONENT) { viComponentID.push_back(NONE_COMPONENT); } for (itr = viComponentID.end() - 1; itr >= viComponentID.begin(); --itr) { if (*itr < 0) { if (*itr == -1) { // 正常分支结束后,放在容器后面 m_lpComponent.push_back(pComponent); } else { for (int i = 0; i < (*itr)*(-1); i++) { // 多分支情况,放容器前面 m_lpComponent.push_front(pComponent); } } // 处理以0结束的分支 if ((itr - 1) >= viComponentID.begin() && *(itr - 1) == 0) { pComponent = NULL; } else { pComponent = m_lpComponent.front(); m_lpComponent.pop_front(); } continue; } pComponent = CreateComponent(*itr, pComponent, m_lpComponent); // 组装失败 if (pComponent == NULL) { ClearComponent(); // LOG return NULL; } m_vpComponent.push_back(pComponent); } return pComponent; }
RatingCtrlFactory .h
#ifndef RATINGCTRLFACTORY_H_ #define RATINGCTRLFACTORY_H_ #include "ComponentFactory.h" class RatingCtrlFactory : public ComponentFactory { public: /** * @brief 创建组件 * * 根据iComponentID创建对应的组件,用pComponent初始化新组件,最后返回新组件给pComponent。 * 对于分支组件,需要根据lpComponent做特殊处理~ * * @param iComponentID [in] * @param pComponent [in/out] * @param lpComponent [in/out] */ virtual RatingCtrlComponent* CreateComponent(int iComponentID, RatingCtrlComponent* pComponent, list<RatingCtrlComponent*>& lpComponent); private: RatingCtrlComponent* Back(list<RatingCtrlComponent*>& lpComponent); }; #endif
#include "RatingCtrlFactory.h" #include "NoneComponent.h" RatingCtrlComponent* RatingCtrlFactory::Back(list<RatingCtrlComponent*>& lpComponent) { RatingCtrlComponent* pComponent = lpComponent.back(); lpComponent.pop_back(); return pComponent; } // 当遇到多分支的时候,应该先将pComponent放回链表首,再从链表尾取出相应数量的组件指针进行组装. RatingCtrlComponent* RatingCtrlFactory::CreateComponent(int iComponentID, RatingCtrlComponent* pComponent, list<RatingCtrlComponent*>& lpComponent) { ADD_TRACE("%s RatingCtrlFactory::CreateComponent IN: iComponentID: %d, pComponent: %p.\n", LOG_PREFIX, iComponentID, pComponent); if (iComponentID != NONE_COMPONENT && pComponent == NULL) { ADD_TRACE("%s RatingCtrlFactory::CreateComponent OUT: iComponentID: %d, pComponent: %p.\n", LOG_PREFIX, iComponentID, pComponent); return NULL; } switch (iComponentID) { case NONE_COMPONENT: { if (pComponent != NULL) { return NULL; } pComponent = new NoneComponent(); break; } case EXECUTE_INITIALIZE: { pComponent = new ExecuteInitialize(pComponent); break; } case EXECUTE_PROCESS_IN: { lpComponent.push_front(pComponent); if (lpComponent.size() < 4) { return NULL; } pComponent = new ExecuteProcessIN(Back(lpComponent), Back(lpComponent), Back(lpComponent), Back(lpComponent)); break; } default: { return NULL; } } return pComponent; }
复杂分机的代码实现:
#ifndef EXECUTEPROCESSIN_H_ #define EXECUTEPROCESSIN_H_ #include "RatingCtrlDectorator.h" class ExecuteProcessIN : public RatingCtrlDectorator { public: ExecuteProcessIN(RatingCtrlComponent* pComponentA, RatingCtrlComponent* pComponentB, RatingCtrlComponent* pComponentC, RatingCtrlComponent* pComponentD) : RatingCtrlDectorator(pComponentA), m_pComponentB(pComponentB), m_pComponentC(pComponentC), m_pComponentD(pComponentD) { } virtual ~ExecuteProcessIN() { } /** * @brief IN业务流程选择 * * 根据 ExecuteId 选择流程 * A:初始包流程; B:更新包流程; C:结束包鉴权; D:异常流程 * * @param pRatableEvent [in] * @return bool */ virtual bool Execute(TRatableEvent& pRatableEvent); private: RatingCtrlComponent* m_pComponentB; RatingCtrlComponent* m_pComponentC; RatingCtrlComponent* m_pComponentD; }; #endif
#include "ExecuteProcessIN.h" bool ExecuteProcessIN::Execute(TRatableEvent& pRatableEvent) { ADD_TRACE("%s ExecuteProcessIN::Execute() Begin.\n", LOG_PREFIX); int iExecuteId = pRatableEvent.GetAttrEx(EA::EXECUTE_ID)->AsInteger(); switch (iExecuteId) { case EXECUTE_ID_INIT: { ADD_TRACE("%s ExecuteProcessIN::Execute() End Goto EXECUTE_ID_INIT Process.\n", LOG_PREFIX); return m_pComponentA->Execute(pRatableEvent); } case EXECUTE_ID_UPDATE: { ADD_TRACE("%s ExecuteProcessIN::Execute() End Goto EXECUTE_ID_UPDATE Process.\n", LOG_PREFIX); return m_pComponentB->Execute(pRatableEvent); } case EXECUTE_ID_FINISH: { ADD_TRACE("%s ExecuteProcessIN::Execute() End Goto EXECUTE_ID_FINISH Process.\n", LOG_PREFIX); return m_pComponentC->Execute(pRatableEvent); } case EXECUTE_ID_EMCDR: { ADD_TRACE("%s ExecuteProcessIN::Execute() End Goto EXECUTE_ID_EMCDR Process.\n", LOG_PREFIX); return m_pComponentD->Execute(pRatableEvent); } default: { pRatableEvent.SetAttr(EA::OCS_RESULT_CODE, CCA::DIAMETER_RATING_FAILED); ADD_ERROR("ZSmart-Charging-Online-1015", "%s ExecuteProcessIN::ExecuteRatableEvent() GetExecuteId Failed. iExecuteId = [%d], ReturnCode = [%d]\n", LOG_PREFIX, iExecuteId, pRatableEvent.GetAttrEx(EA::OCS_RESULT_CODE)->AsInteger()); } } ADD_TRACE("%s ExecuteProcessIN::Execute() Fail.\n", LOG_PREFIX); return false; }
最终复杂的流程图: