文章出處

很多軟件工程師都多少在處理 "Bad Design" 時有一些痛苦的經歷。如果發現這些 "Bad Design" 的始作俑者就是我們自己時,那感覺就更糟糕了。那么,到底是什么讓我做出一個能稱為 "Bad Design" 的設計呢?

絕大多數軟件工程師不會在設計之初就打算設計一個 "Bad Design"。許多軟件也在不斷地演化中逐漸地降級到了一個點,而從這個點開始,有人開始說這個設計已經腐爛到一定程度了。為什么會發生這些事情呢?是因為最初設計的匱乏嗎,還是設計逐步降級到像塊腐爛的肉一樣?實際上,尋找這些答案得先從確定 "Bad Design" 的準確定義開始。

"Bad Design" 的定義

你可能曾經提出過一個讓你倍感自豪的軟件設計,然后讓你的一個同事來做 Design Review?你能感覺到你同事臉上隱含的抱怨與嘲弄,他會冷笑的問道:"為什么你要這么設計?" 反正這事兒在我身上肯定是發生過,并且我也看到在我身邊的很多工程師身上也發生過。確切的說,那些持不同想法的同事是沒有采用與你相同的評判標準來斷定 "Bad Design"。我見過最常使用的標準是 "TNNTWI-WHDI" ,也就是 "That's not the way I would have done it(要是我就不會這么干)" 標準。

但有一些標準是所有工程師都會贊同的。如果軟件在滿足客戶需求的情況下,其呈現出了下述中的一個或多個特點,則就可稱其為 "Bad Design":

  1. 難以修改,因為每次修改都影響系統中的多個部分。(僵化性Rigidity)
  2. 當修改時,難以預期系統中哪些地方會被影響。(脆弱性Fragility)
  3. 難以在其他應用中重用,因為它不能從當前系統中解耦。(復用性差Immobility)

此外,還有一些較難斷定的 "Bad Design",比如:靈活性(Flexible)、魯棒性(Robust)、可重用性(Reusable)等方面。我們可以僅使用上面明確的三點作為判定一個設計的好與壞的標準。

"Bad Design" 的根源

那到底是什么讓設計變得僵化、脆弱和難以復用呢?答案是模塊間的相互依賴

如果一個設計不能很容易被修改,則設計就是僵化的。這種僵化性體現在,如果對相互依賴嚴重的軟件做一處改動,將會導致所有依賴的模塊發生級聯式的修改。當設計師或代碼維護者無法預期這種級聯式的修改所產生的影響時,那么這種蔓延的結果也就無法估計了。這導致軟件變更的代價無法被準確的預測。而管理人員在面對這種無法預測的情況時,通常是不會對變更進行授權,然后僵化的設計也就得到了官方的保護。

脆弱性是指一處變更將破壞程序中多個位置的功能。而通常新產生的問題所涉及的模塊與該變更所涉及的模塊在概念上并沒有直接的關聯關系。這種脆弱性極大地削弱了設計與維護團隊對軟件的信任度。同時軟件使用者和管理人員都不能預測產品的質量,因為對應用程序某一部分簡單的修改導致了其他多個位置的錯誤,而且看起來還是完全無關的位置。而解決這些問題將可能導致更多的問題,使得維護過程陷進了 "狗咬尾巴" 的怪圈。

如果設計中實現需求的部分對一些與該需求無關的部分產生了很強的依賴,則該設計陷入了死板區域。設計師可能會被要求去調查是否能夠將該設計應用到不同的應用程序,要能夠預知該設計在新的應用中是否可以完好的工作。然而,如果設計的模塊間是高度依賴的,而從一個功能模塊中隔離另一個功能模塊的工作量足以嚇到設計師時,設計師就會放棄這種重用,因為隔離重用的代價已經高于重新設計的代價

示例:一個拷貝程序(Copy)

通過一個簡單的例子來描述這些問題可能會對我們有所幫助。設想有一個簡單的 "Copy" 程序,它負責將鍵盤上輸入的字符拷貝到一個打印機上。假設設備獨立而且是與平臺無關的。我們可以構思這個程序的結構,類似于圖 1 中的描述:

圖 1 拷貝程序

圖 1 是一個結構圖。它顯示在應用程序中一共有三個模塊,或者叫子程序。"Copy" 模塊負責調用其他兩個模塊。可以簡單的想象成在 "Copy" 中有一個 while 循環,在循環體內調用 "Read Keyboard" 模塊來嘗試從鍵盤讀取一個字符,然后將字符發送到 "Write Printer" 模塊來打印字符。

1 void Copy()
2 {
3     int c;
4     while ((c = ReadKeyboard()) != EOF)
5         WritePrinter(c);
6 }

這個兩層的模塊設計是可以很好地被重用的,它們可以被使用到許多不同的應用程序中來控制對鍵盤和打印機的訪問。

然而,"Copy" 模塊在那些不使用鍵盤和打印機的條件下是無法被重用的。這太可惜了,因為系統所呈現的智能化就是體現在了這個模塊里。"Copy" 模塊封裝了我們所感興趣并且希望重用的部分。

例如,假設我們有一個新的程序,它需要將鍵盤字符拷貝到磁盤文件。我們顯然希望復用 "Copy" 模塊,因為它所做的高層封裝正是我們需要的。而這個封裝所做的就是描述將字符從源拷貝到目的地的過程。但很不幸,由于 "Copy" 模塊直接依賴了 "Write Printer" 模塊,所以這種新的需求情況下無法被重用。

當然,我們可以直接修改 "Copy" 模塊來增加新的功能。通過增加 "if" 語句來檢查一個標志位,判斷到底是寫到打印機還是寫到磁盤,這樣就可以分別使用 "Write Printer" 模塊或 "Write Disk" 模塊。然后,這樣做之后我們就又在系統中增加了一個依賴模塊。

 1 enum OutputDevice {printer, disk};
 2 void Copy(outputDevice dev)
 3 {
 4     int c;
 5     while ((c = ReadKeyboard()) != EOF)
 6         if (dev == printer)
 7             WritePrinter(c);
 8         else
 9             WriteDisk(c);
10 }

隨時時間的推移,越來越多的設備可以支持 "Copy" 功能,"Copy" 模塊也將陷入凌亂的 "if/else" 判斷中。這顯然使應用變得僵化和脆弱。

依賴倒置(Dependency Inversion)

上述問題的主要特征是包含高層邏輯的模塊依賴于低層模塊的細節,例如 "Copy" 模塊依賴于 "Read Keyboard" 模塊和 "Write Printer" 模塊。如果我們想辦法使 "Copy" 模塊不依賴于這些細節,則就會很容易地被復用。可以將其用于任何其他負責從輸入設備將字符拷貝到輸出設備的應用程序。OOD 為我們提供了一種機制,叫做依賴倒置(Dependency Inversion)。

圖 2

設想如圖 2 中的類圖結構。類 "Copy" 包含了一個抽象類 "Reader" 和另一個抽象類 "Writer"。可以想象在 "Copy" 中的循環結構不斷的從 "Reader" 讀取字符,然后將字符發送至 "Writer"。

 1 class Reader
 2 {
 3     public:
 4         virtual int Read() = 0;
 5 };
 6 class Writer
 7 {
 8     public:
 9         virtual void Write(char) = 0;
10 };
11 void Copy(Reader& r, Writer& w)
12 {
13     int c;
14     while((c=r.Read()) != EOF)
15         w.Write(c);
16 }

此時類 "Copy" 既沒有依賴 "Keyboard Reader" 也沒有依賴 "Printer Writer"。因此,這些依賴已經被反轉了(Inverted)。"Copy" 類依賴于抽象,而真正的 "Reader" 和 "Writer" 的具體實現也依賴于抽象。

此時,我們就可以重用 "Copy" 類,而不需要具體的 "Keyboard Reader" 和 "Printer Writer"。我們可以通過創造新的 "Reader" 和 "Writer" 衍生類然后替換到 "Copy" 中。而且,無論有多少種 "Reader" 和 "Writer" 被創建,"Copy" 都不會依賴于它們。因為沒有這些模塊間的相互依賴,也使得程序不會變的僵化和脆弱。并且 "Copy" 類也可以被復用到多種不同的情況中。它不再是固定的。

依賴倒置原則(The Dependency Inversion Principle)

A. High level modules should not depend upon low level modules. Both should depend upon abstractions.

B. Abstractions should not depend upon details. Details should depend upon abstraction.

A. 高層模塊不應該依賴于低層模塊,二者都應該依賴于抽象。

B. 抽象不應該依賴于具體實現細節,而具體實現細節應該依賴于抽象。

有人可能會問,為什么我要使用 "Inversion" 這個詞兒。坦白的說,是因為,對于更加傳統的軟件開發方法,例如結構化的分析與設計(Structured Analysis and Design),更趨向于創建高層模塊依賴于低層模塊的軟件結構,進而使得抽象依賴了具體實現細節。而且實際上這些方法最主要的目標就是通過定義子程序的層級關系來描述高層模塊式如何調用低層模塊的。圖 1 中的示例正好描述了這樣的一個層級結構。因此,一個設計良好的面向對象程序的依賴結構是 “inverted” 倒置了相對于傳統過程化方法的依賴結構。

考慮下高層模塊依賴于低層模塊所帶來的連帶影響。高層模塊包含著應用程序中重要的業務決策信息,是這些業務模型包含了應用程序的功能特征。當這些模塊依賴于低層模塊時,對低層模塊的修改將直接影響高層模塊,也就是強制修改了它們。

這種情形是違反常理的!應該是高層模塊強制要求修改低層模塊才對。高層模塊的權重應該優先于低層模塊。高層模塊是無論如何都不應當依賴于低層模塊。

更進一步說,我們其實想重用的是高層模塊。我們已經通過子程序庫等方式很好地重用了低層模塊了。如果高層模塊依賴于低層模塊,將導致高層模塊在不同的環境中變得極難被復用。而如果高層模塊完全獨立于與低層模塊,高層模塊就可以很容易地被復用。這就是這個原則的核心所在。

分層(Layering)

依據 Grady Booch 的定義:

All well-structured object-oriented architectures have clearly-defined layers, with each layer providing some coherent set of services though a well-defined and controlled interface.

所有結構良好的面向對象架構都有著清晰明確的層級定義,每一層都通過一個定義良好和可控的接口來提供一組內聚的服務集合。

如果不加思索的來解釋這段話,可能會讓設計師創建出類似于圖3中的結構。

圖 3

在圖中高層類 "Policy" 使用了低層類 "Mechanism","Mechanism" 使用了更細粒度的 "Utility" 類。這看起來像是很合適,但其實隱藏了一個問題,就是對于 Policy Layer 的更改將對一路下降至 Utility Layer。這稱為依賴是傳遞的(Dependency is transitive)。Policy Layer 依賴一些依賴于 Utility Layer 的模塊,然后 Policy Layer 傳遞性的依賴了 Utility Layer。這顯示是非常不幸的。

圖 4 給出了一個更合適的模型。

圖 4

每一個低層都被一個抽象類所表述,而實際的層級則由這些抽象類所派生。每一個高層類通過抽象接口來使用低層類。因此,層級之間不會依賴其他的層。取而代之的是,層依賴了抽象類。這不僅打破了 Policy Layer 到 Utility Layer 的傳遞性依賴,同時也將 Policy Layer 到 Mechanism Layer 的依賴打破。

使用這個模型后,Policy Layer 不會被任何 Mechanism Layer 或 Utility Layer 的更改所影響。同時,Policy Layer 也能夠在任何情形下進行重用,只要是低層模塊符合 Mechanism Layer Interface 定義即可。因此,通過反轉依賴關系,沃恩稿件了一個更靈活、更持久的設計結構。

總結

依賴倒置原則(Dependency Inversion Principle)是很多面向對象技術的根基。它特別適合應用于構建可復用的軟件框架,其對于構建彈性地易于變化的代碼也特別重要。并且,因為抽象和細節已經彼此隔離,代碼也變得更易維護。

 

面向對象設計的原則

 SRP

 單一職責原則

 Single Responsibility Principle

 OCP

 開放封閉原則

 Open Closed Principle

 LSP

 里氏替換原則

 Liskov Substitution Principle

 ISP

 接口分離原則

 Interface Segregation Principle

 DIP

 依賴倒置原則

 Dependency Inversion Principle

 LKP

 最少知識原則

 Least Knowledge Principle

參考資料

 

本文《依賴倒置原則(Dependency Inversion Principle)》由 Dennis Gao 翻譯改編自 Robert Martin 的文章《DIP: The Dependency Inversion Principle》,未經作者本人同意禁止任何形式的轉載,任何自動或人為的爬蟲行為均為耍流氓。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()