在較大規模的業務系統中經常會有這樣的模塊,它按照一定的業務流程調用其它模塊來實現一定的業務邏輯,我們姑且稱之為流程引擎。這裡稱之為引擎有兩層含義,一、突顯其在業務系統的核心重要位置。二、它又是複雜不好維護的,通常由資深程式員把持。這樣的引擎不僅代碼繁多,與各個模塊的介面複雜,並且一定程度對外是不透明... ...
流程式控制制引擎組件化
在較大規模的業務系統中經常會有這樣的模塊,它按照一定的業務流程調用其它模塊來實現一定的業務邏輯,我們姑且稱之為流程引擎。這裡稱之為引擎有兩層含義,一、突顯其在業務系統的核心重要位置。二、它又是複雜不好維護的,通常由資深程式員把持。這樣的引擎不僅代碼繁多,與各個模塊的介面複雜,並且一定程度對外是不透明的,就像一個黑盒模塊。當一個新手想對其進行哪怕是一點點修改的時候都將會是一個災難。本文就來討論如何將這樣一個龐大的引擎進行組件化改造,使其擁有代碼級的流程圖,使代碼的維護難度降低一個量級,讓新手也可以很快知道要如何修改代碼,給引擎增加新的功能。
我們來看兩張圖,分別是數控機床和機器人流水線。他們都是自動化的,就像我們的業務流程引擎一樣。數控機床簡潔、高效。但維護的時候需要專業人員,想增加新功能幾乎不可能。機器人流水線,透明,看著有點複雜。但可以隨時對流水線上任意部分進行升級,改造,替換。回到我們的業務流程引擎,我們到底希望是上面的哪一種?不難想到,由於業務需求的易變性,我們更傾向於第二種方案。但往往我們的代碼是第一種情況,並且代碼的組織是混亂的,並不是數控機床那種優雅。
數控機床
機器人流水線
當我們面對一個不熟悉的引擎的時候,我們看到的是下麵這樣子。
當我們花了一個月時間,仔細研讀了幾萬行的代碼後,我們看到的是下麵這樣子。
雖然我們已經瞭解了引擎內部的情況,但就像圖中所描繪的樣子,當要給引擎增加一個新的功能的時候,新增的功能和代碼很容易影響原來正確的系統,一個小局部(零件)的問題,都會影響到整個系統。那麼,我們如何來改變現有的流程引擎,以實現方便地增加新的功能?
如上圖所示,如果我們的流程是一個流水線的話,我們就可以輕易地在流水線中增加新的組件,以實現新的功能。流程組件具有這樣的特征:每個組件都是一個獨立的個體,不受其它組件的影響。組件具有統一的介面,新增的組件可以方便和原有的組件一起工作。可以根據業務需要挑選合適的組件,組成一個新的業務流程。當把這些組件組合到一起,就形成了組件鏈表的概念。一個個獨立的組件,就像鏈表裡面的一個個節點,組件節點通過組件指針連接起來。一個流程的執行過程就像是對這個鏈表的遍歷。並且這個鏈表不僅可以只是個單鏈表,而且可以是多分支鏈表,以應對流程中多條件分支的情況。
下麵我們來看看代碼是如何來實現的。
組件節點:
例子:
這裡的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; }
最終複雜的流程圖: