日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

软件系统解耦:理解依赖关系

發布時間:2024/6/21 综合教程 34 生活家
生活随笔 收集整理的這篇文章主要介紹了 软件系统解耦:理解依赖关系 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

轉自:https://zhuanlan.zhihu.com/p/31391535

在實際工作中,復雜度上來后,各模塊之間錯綜復雜,調用關系網千頭萬緒。即使有各種設計模式做指導,做出合理的設計也并不容易。程序員天天疲于應對層出不窮的變化,在不斷緊逼的deadline壓力下,面對巨大的重構工作量往往感到心有余而力不足。

系統復雜度的根源除了業務本身的復雜度,就是設計了不恰當的耦合關系。本文試圖探討依賴關系的主要類型,并總結應對依賴的編程范式。

耦合:依賴和變化

耦合是一個有歧義的詞語(為什么“耦合”概念該要摒棄)。當我們說“A和B耦合”的時候,我們是想表達A和B之間有緊密的聯系。具體是什么,不容易講清楚。

在我看來,耦合至少包含了兩個方面的含義:依賴和變化。

業務邏輯固有的復雜度決定了,模塊之間必然存在著依賴。規范模塊間的依賴關系,就是梳理業務復雜度的過程。最終的成果反映在代碼中,代表了對業務復雜度的一種認識。這種認識隨著業務需求的變化而演化,隨著設計者的能力提升而深化。依賴不能被消除,但是可以被優化。探討一些應對的范式有助于規避已知的陷阱。

變化則來源于兩個方面:發展中的用戶需求,完善中的系統模型。用戶的需求是我們努力的方向。系統模型則代表了我們對需求的理解,是經驗和智慧的結晶。一個完善的系統模型,表達能力要足夠強,對業務的適應能力要足夠強。變化,意味著工作量,意味著成本,應該盡量降低。如果我們把“系統變更”和“業務需求變更”寫成函數:

我們希望自變量不變的情況下,“系統變更”這個函數值越小越好。特別是“業務需求變更”在當前系統設計假設條件下產生調整的時候,“系統變更”應該局限在很小的范圍內。

依賴的種類

在UML類圖中,依賴關系被標記為<<use>>。A依賴B意味著,A模塊可以調用B模塊暴露的API,但B模塊絕不允許調用A模塊的API(IBM Knowledge Center)。

在類圖中,依賴關系是指更改一個類(供應者)可能會導致更改另一個類(客戶)。供應者是獨立的,這是因為更改使用者并不會影響供應者。

例如,Cart 類依賴于 Product 類,因為 Product 類被用作 Cart 類中的“添加”操作的參數。在類圖中,依賴關系是從 Cart 類指向 Product 類。換句話說,Cart 類是使用者元素,而 Product 類是供應者元素。更改 Product 類可能會導致更改 Cart 類。

在類圖中,C/C++ 應用程序中的依賴關系將兩個類連接起來,以指示這兩個類之間存在連接,并且該連接比關聯關系更加具有臨時性。依賴關系是指使用者類執行下列其中一項操作:

臨時使用具有全局作用域的供應者類,
將供應者類臨時用作它的某個操作的參數,
將供應者類臨時用作它的某個操作的局部變量,
將消息發送至供應者類。

模塊之間產生依賴的主要方式是數據引用和函數調用。檢驗模塊依賴程度是否合理,則主要看“變更”的容易程度。軟件模塊之間的調用方式可以分為三種:同步調用、回調和異步調用(異步消息的傳遞-回調機制)。同步調用是一種單向依賴關系。回調是一種雙向依賴關系。異步調用往往伴隨著消息注冊操作,所以本質上也是一種雙向依賴。

三種調用方式

有一種觀點將“依賴”直接總結為人腦中的依賴(為什么“耦合”概念該要摒棄),我非常認同。文中提到:

只要程序員編寫模塊A時,需要知道模塊B的存在,需要知道模塊B提供哪些功能,A對B依賴就存在。甚至就算通過所謂的依賴注入、命名查找之類的“解耦”手段,讓模塊A不需要import B或者include "B.h",人腦中的依賴仍舊一點都沒有變化。唯一的作用是會騙過后文會提到的代碼打分工具,讓工具誤以為兩個模塊間沒有依賴。

代碼的復雜度更主要的體現在閱讀和理解,如果只是糾結于編譯器所看到的依賴,實在是分錯了主次,誤入了歧途。

單向依賴與單一職責原則(SRP)

單向依賴是最簡單的依賴。

單向依賴

上述都是單向依賴的例子。其中,(1)是最理想的情況。當邏輯變復雜后,單個模塊往往承擔了過多的責任。即使模塊之間可以保持簡單的單向關系,模塊內部各行為之間卻形成高強度的耦合整體。根據單一職責原則(SRP),這樣的模塊也是難以維護的,我們需要對模塊做拆分。

在有多個模塊的情況下,(2)的依賴關系顯然要好于(3),因為在(2)中模塊的依賴關系要比(3)少。這樣的解釋過于抽象,我們用游戲中比較典型的一個應用場景來說明一下。

場景對象管理器GameObjectManager,管理著場景對象GameObjectInstance,而場景對象的構造需要資源AssetStore的支持。他們的調用關系,用(2)和(3)的模式分別實現一遍:

//(2) GameObjectManager從AssetStore取資源數據,然后調用GameObjectInstnce的初始化流程
class GameObjectManager{
public:
    AssetForGameObject* GetAsset(DWORD dwID){m_Asset.GetAsset(dwID);}
    GameObjectInstance* Create(DWORD dwAssetID){
        AssetForGameObject* pAsset = GetAsset(dwAssetID);
        return m_GameObjects[dwNewID] = new GameObjectInstance(pAsset);
    }
    void TickGameObject(){foreach(auto go = m_GameObjects) go.Tick();}
private:
    AssetStore m_Asset;
    map<DWORD, GameObjectInstance*> m_GameObjects;
};

//(3) GameObjectInstance自己調用AssetStore的方法取資源數據,做初始化
class GameObjectManager{
public:
    GameObjectInstance* Create(AssetStore* pAssets, DWORD dwAssetID){
        GameObjectInstance* pGo = new GameObjectInstance();
        pGo->Init(pAssets, dwAssetID);
        return m_GameObjects[dwNewID] = pGo;
    }
private:
    AssetStore m_Asset;
    map<DWORD, GameObjectInstance*> m_GameObjects;
};

class GameObjectInstance{
public:
    void Init(AssetStore* pAssets, DWORD dwAssetID){
        m_Data = pAssets->GetAsset(dwAssetID);
    }
};

GameObjectInstance只需要依賴于AssetForGameObject,但是在依賴關系(3)中,卻要依賴于一個范圍更大的概念AssetStore。

將雙向依賴轉換為單向依賴

雙向依賴關系在網絡游戲中也是比比皆是。我們來看一個雙向依賴的典型例子:網絡數據包的收發。如果把“上層業務邏輯”和“底層網絡連接”看作兩個模塊。在發數據包的過程中,業務邏輯調用底層發送接口發送數據。業務邏輯依賴于底層網絡連接。而在收數據包的時候,數據首先在網絡連接模塊接收,再分派到不同的業務邏輯。上層業務邏輯和底層網絡連接形成了一種天然的雙向依賴關系。

class Logic{
public:
    void SendMessage(byte* pbyBuffer, size_t uLen){
        m_pConnection->Send(pbyBuffer, uLen);
    }
    void HandleMessage(byte* pbyBuffer, size_t uLen){/*...*/}
private:
    Connection* m_pConnection;
};

class Connection{
public:
    void SetLogic(Logic* pLogic){m_pLogic = pLogic;}
    void SendMessage(byte* pbyBuffer, size_t uLen){/*...*/}
    void RecvMessage(byte* pbyBuffer, size_t uLen){
        m_pLogic->HandleMessage(pbyBuffer, uLen);
    }
private:
    Logic* m_pLogic;
};

用最自然的方式,我們寫出了上面的代碼。這其實是用“依賴注入”實現的回調。容易發現,當Logic增減成員變量或成員函數,Connection就需要重新編譯,甚至重新調整代碼。這樣的耦合度是無法接受的。

我們可以嘗試用"Don't call us, we will call you"把雙向依賴轉換為單向依賴。簡單來說,當網絡連接收到數據包后,可以先放到一個存儲區。等調度到業務邏輯的時候,業務邏輯主動去取數據并處理。在存儲區存儲一個數據,就相當于存儲一個對業務邏輯的調用請求。這樣就演變為了單向依賴關系(3),模塊C就相當于存儲區。需要說明的是,存儲區并不一定必須要獨立出來一個模塊,完全可以維護在模塊B中。此種情形,A可以直接向B要數據。

并不是所有的雙向依賴關系都可以很容易的轉換為單向依賴。上述例子中,如果業務邏輯來不及處理數據包,網絡連接層就要維護一個數據列表。這增加了存儲開銷。而且有時候把數據延遲處理是不合適的。代碼也因此變得晦澀難懂,難以維護。如果導致這種結果,那就與我們轉換依賴關系的初衷背道而馳了。

弱化雙向依賴:回調與中間層

一般情況下,為了弱化雙向依賴的影響,我們可以增加一個中間層。雖然調用鏈路是從“網絡連接”又回到了“業務邏輯”,但是由于中間層的存在,變化被隔離,原先很強的依賴關系變弱了。以下介紹四種典型的中間層。

通過添加穩定的中間層隔離變化

需要說明的是,上述所說的中間層,偏向于概念,在代碼實現中并不一定要獨立成一個單獨的模塊。但為了方便,還是借用模塊(如上圖中的模塊C)來表述。

1)接口與繼承

我們很自然想到,依賴注入可以使用接口。當Connection依賴的是Logic的接口(假定為ILogic),雖然Logic變更,只要ILogic不變,就不會影響Connection。但是在實踐中根本不是這么回事。

我們經常聽說,只要把接口設計得“正交”“緊湊”,就能保證接口的穩定。但是,在實踐中,混亂的繼承關系隨處可見。大多數程序員都停留在利用繼承思維構造業務邏輯關系,并盡快實現功能。極少有能力有時間檢視繼承關系是否恰當。正確使用繼承對程序員的要求太高了。

當重新審視繼承的時候我們發現,繼承的父類和子類之間實際形成了一種雙向依賴。繼承和多態不僅規定了函數的名稱、參數、返回類型,還規定了類的繼承關系,是一種強耦合(https://cloud.github.com/downloads/chenshuo/documents/CppPractice.pdf, p45)。接口約定了外部調用的規范,繼承類必須按照這些規范去實現。只要規范不變,繼承類的實現可以調整而不將影響傳遞出去。糟糕的是,不管是規范還是實現,都基本上不可能一開始就確定好。當變化發生的時候,接口類和繼承類都需要做大量的修改,而這些修改也很容易影響到所有使用接口的那些模塊。

穩定的繼承關系可以提供良好的擴展性,也可以避免把相同的邏輯寫得到處都是(DRY原則)。但是濫用繼承也會是災難性的。在"Is-A"和"Has-A"的取舍中,要謹慎行事。

2)Delegation

一個對調用者和被調用者約束較小的方式是代理(Delegation)。所謂代理,就是將依賴轉移到較穩定的代理類上。通過一個仿函數,調用不同類中有相同簽名的方法。一個典型的代理類的例子如下所示(The Impossibly Fast C++ Delegates)。其最初版本需要對每種參數做不同處理。后來發展出來一種更一般的代理方式(C++ Delegates On Steroids),可以接受任意類型和任意數量的參數。

class delegate
{
public:
    delegate() : object_ptr(0), stub_ptr(0){}

    template <class T, void (T::*TMethod)(int)>
    static delegate from_method(T* object_ptr){
        delegate d;
        d.object_ptr = object_ptr;
        d.stub_ptr = &method_stub<T, TMethod>; // #1
        return d;
    }

    void operator()(int a1) const{
        return (*stub_ptr)(object_ptr, a1);
    }

private:
    typedef void (*stub_type)(void* object_ptr, int);

    void* object_ptr;
    stub_type stub_ptr;

    template <class T, void (T::*TMethod)(int)>
    static void method_stub(void* object_ptr, int a1){
        T* p = static_cast<T*>(object_ptr);
        return (p->*TMethod)(a1); // #2
    }
};

3) Bind/Function

Bind/Function機制不要求被綁定的類有任何繼承規范。其更像是C中的函數指針,比代理類要更簡單。除了和代理類一樣需要函數簽名一致,不需要程序員額外維護一個類。

現在C++11提供了很好用的bind/function(Bind illustrated,C++11: std::function and std::bind)。我們可以將上述的數據包處理回調重寫如下:

class Logic{
public:
    void Init(){
        m_pConnection->SetCallbackFunc(std::bind(HandleMessage), this);
    }
    void SendMessage(byte* pbyBuffer, size_t uLen){
        m_pConnection->Send(pbyBuffer, uLen);
    }
    void HandleMessage(byte* pbyBuffer, size_t uLen){/*...*/}
private:
    Connection* m_pConnection;
};

class Connection{
public:
    void SetCallbackFunc(Logic* pLogic){m_pLogic = pLogic;}
    void SendMessage(byte* pbyBuffer, size_t uLen){/*...*/}
    void RecvMessage(byte* pbyBuffer, size_t uLen){
        m_callbackfunc(pbyBuffer, uLen);
    }
private:
    func* m_callbackfunc;
};

4) Lambda與閉包

嚴格來說,bind/function的實現也屬于閉包。這里把Lambda/Closure單獨列出來是想強調Lambda表達式可以通過匿名函數把相同的事做的更簡潔。比起bind一個成員函數,直接bind一個在局部空間定義的lambda表達式給程序員帶來的思維負擔更小。

畢竟,修改lambda表達式時,可以清楚知道影響的范圍。而修改被bind的成員函數時,還要考慮該成員函數是不是在其他地方被用到。

總結

以上是生活随笔為你收集整理的软件系统解耦:理解依赖关系的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。