流程式控制制引擎組件化

来源:https://www.cnblogs.com/cdap/archive/2018/08/10/9454835.html
-Advertisement-
Play Games

在較大規模的業務系統中經常會有這樣的模塊,它按照一定的業務流程調用其它模塊來實現一定的業務邏輯,我們姑且稱之為流程引擎。這裡稱之為引擎有兩層含義,一、突顯其在業務系統的核心重要位置。二、它又是複雜不好維護的,通常由資深程式員把持。這樣的引擎不僅代碼繁多,與各個模塊的介面複雜,並且一定程度對外是不透明... ...


                 流程式控制制引擎組件化

  在較大規模的業務系統中經常會有這樣的模塊,它按照一定的業務流程調用其它模塊來實現一定的業務邏輯,我們姑且稱之為流程引擎。這裡稱之為引擎有兩層含義,一、突顯其在業務系統的核心重要位置。二、它又是複雜不好維護的,通常由資深程式員把持。這樣的引擎不僅代碼繁多,與各個模塊的介面複雜,並且一定程度對外是不透明的,就像一個黑盒模塊。當一個新手想對其進行哪怕是一點點修改的時候都將會是一個災難。本文就來討論如何將這樣一個龐大的引擎進行組件化改造,使其擁有代碼級的流程圖,使代碼的維護難度降低一個量級,讓新手也可以很快知道要如何修改代碼,給引擎增加新的功能。

  我們來看兩張圖,分別是數控機床和機器人流水線。他們都是自動化的,就像我們的業務流程引擎一樣。數控機床簡潔、高效。但維護的時候需要專業人員,想增加新功能幾乎不可能。機器人流水線,透明,看著有點複雜。但可以隨時對流水線上任意部分進行升級,改造,替換。回到我們的業務流程引擎,我們到底希望是上面的哪一種?不難想到,由於業務需求的易變性,我們更傾向於第二種方案。但往往我們的代碼是第一種情況,並且代碼的組織是混亂的,並不是數控機床那種優雅。

 

 

數控機床

機器人流水線

   

  當我們面對一個不熟悉的引擎的時候,我們看到的是下麵這樣子。

  當我們花了一個月時間,仔細研讀了幾萬行的代碼後,我們看到的是下麵這樣子。

 

 

  雖然我們已經瞭解了引擎內部的情況,但就像圖中所描繪的樣子,當要給引擎增加一個新的功能的時候,新增的功能和代碼很容易影響原來正確的系統,一個小局部(零件)的問題,都會影響到整個系統。那麼,我們如何來改變現有的流程引擎,以實現方便地增加新的功能?

   

   

 

 

  如上圖所示,如果我們的流程是一個流水線的話,我們就可以輕易地在流水線中增加新的組件,以實現新的功能。流程組件具有這樣的特征:每個組件都是一個獨立的個體,不受其它組件的影響。組件具有統一的介面,新增的組件可以方便和原有的組件一起工作。可以根據業務需要挑選合適的組件,組成一個新的業務流程。當把這些組件組合到一起,就形成了組件鏈表的概念。一個個獨立的組件,就像鏈表裡面的一個個節點,組件節點通過組件指針連接起來。一個流程的執行過程就像是對這個鏈表的遍歷。並且這個鏈表不僅可以只是個單鏈表,而且可以是多分支鏈表,以應對流程中多條件分支的情況。

 

  

  下麵我們來看看代碼是如何來實現的。

組件節點:

 

 例子:

 

  這裡的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;
}

最終複雜的流程圖:

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 何為滾動視差 視差滾動(Parallax Scrolling)是指讓多層背景以不同的速度移動,形成立體的運動效果,帶來非常出色的視覺體驗。 作為網頁設計的熱點趨勢,越來越多的網站應用了這項技術。 通常而言,滾動視差在前端需要輔助 Javascript 才能實現。當然,其實 CSS 在實現滾動視差效果 ...
  • 一、使用文檔自帶的原生API rich-text, nodes屬性直接綁定需要渲染的html內容即可,文檔參見這裡:https://developers.weixin.qq.com/miniprogram/dev/component/rich-text.html 二、使用WxParseData插件, ...
  • 如何實現進度條效果呢 ? 效果:點擊頁面的某個按鈕,彈出一個進度條,然後實時顯示進度,直到任務完成。 思路:頁面裡面有個隱藏的進度條,點擊按鈕後彈出。ajax迴圈請求數據,直到全部完成 難點:ajax的同步請求問題 1、首先引入頁面樣式: 2、頁面 進度條 HTML 元素 3、JS 實現 定義全局的 ...
  • 規則就是,調用函數,放兩個參數,第一個參數,是設計稿的寬度,第二個參數是px與rem的轉換比例,通常會寫100(因為好算);當然了,要把這段js代碼最好封裝在一個單獨的js文件里,並且放在所有的css文件引入之前載入。 其中 var n=t.clientWidth||320;n>720&&(n=72 ...
  • 初學前端js經常搞不清楚null與undefined的區別,他們是js中的原始數據類型 1、undefined數據類型只有一個值undefined,當聲明的變數未初始化時,變數的預設值是undefined. 2、null也只有一個值null,用來表示尚未存在的對象,常用來表示函數企圖返回一個不存在的 ...
  • obj =["34", "3", "34#add"] 數組,id 與樹形的所有id 一致 ...
  • 1、DOM操作: DOM操作分為三類: ●DOM Core:任何一種支持DOM的編程語言都可以使用它,如getElementById()、getElementsByName; ●HTML-DOM:用於處理HTML文檔,如document.forms; ●CSS-DOM:用於操作CSS(獲取和設置st ...
  • MVC框架 介紹: MVC全名Model View Controller Model:模型的意思,代表業務模型 View:視圖的意思,代表用戶界面 Controller:控制器的意思,控制器接受用戶的輸入並調用模型和視圖去完成用戶的需求。 MVC把各個層次需要關註的內容分離了開來。 MVC將負責顯示 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...