在前一回中,我們介紹了「相依倒置原則(Dependency Inversion Principle,DIP)」,它是在物件導向的設計領域中,十分廣為人知的原則。基於這個原則,可以透過反轉程式模組間相互依賴的關係,降低程式碼中變動所造成的衝擊。

事實上,「相依倒置原則」和另一個在軟體架構設計時的概念高度相關,這個概念就是所謂的「控制反轉(Inversion of control,IoC)」原則。

程式模組之間的相依關係
為什麼會有一個這樣的名稱呢?你可以想想,在傳統的控制下,我們會把可以重複使用的程式碼寫成副程式,或是可重複運用的類別,讓各種需要運用的應用程式去加以應用。這個控制的方式,是由應用程式扮演一個集中控管的角色,由它來決定程式的執行流程,控制各個副程式執行的順序及流程。

正如在前一回中我們所提到的,Robert C. Martin分析了不佳的軟體設計的特性,找出了幾項會影響到變動對程式碼所造成衝擊的原因,追根究底,就是程式模組間相依關係所造成的。

他認為,讓穩定的程式去依賴不穩定的程式,這會因為不穩定的程式容易起變化,因而波及穩定的程式。這麼一來,整個系統就會傾向於容易因為變化而跟著變化,這當然會引起不少的問題。所以,他認為應該要讓不穩定的部份去依賴穩定的部份,這麼一來,即使不穩定的部份有所變化,也不會影響到穩定的部份,因為相依關係已經倒轉了。

程式的流程控制亦同。在傳統的設計方式下,副程式其實是設計者將程式中可被重複使用、但卻是執行特定工作的部份給抽離出來的組成,而主程式控制著各個副程式執行的順序及流程。因為主程式呼叫副程式,所以主程式對副程式產生了依賴的關係,當副程式需要有所改變時,就會將這個改變透過相依關係傳遞到主程式,因而有可能連帶的造成主程式的改變。當我們在畫分主副程式之間的關係時,若副程式負責的是容易變動的實作,那麼就會造成副程式比主程式更不穩定的情況,而這種不佳的相依關係,就會使得變動所引起的影響變大。

而「控制反轉」的設計原則,就是反轉這種在控制上的關係,讓通用的程式碼來控制應用特定的程式碼,不讓相較而言較多變的應用特定程式碼,去影響到通用的程式碼。

從既有系統的設計看控制反轉的實現
雖然「控制反轉」這個名詞提出的時間並不長,但事實上,在過去的一些軟體架構中,我們不難看出這種精神。例如,在作業系統的架構設計中,我們就很容易可以看到這種設計的精神。

舉例來說,作業系統在設計時,可能必須考慮到各種可能的輸入、輸出設備。若是這些可能的輸入、輸出設備受作業系統核心本身所直接控制,那麼便意謂著作業系統核心必須「認識」這些輸入、輸出設備,而這便使得核心相依於這些輸入、輸出設備。若是輸入、輸出設備的特性有所變化或是想要增加新的輸入、輸出設備,那麼就會造成核心必須連帶著做修改。試想,一個作業系統的核心若是因為這樣子的變動就必須做修改,豈不是太不穩定了?

我記得在當年的DOS作業系統時代,有一陣子還沒有所謂CD-ROM光碟機。不過,當光碟機問世之後,我們這些電腦的使用者想要連接CD-ROM,並在DOS下使用,並不需要換上更新版本、支援CD-ROM的DOS就可以使用,這說明了,作業系統的核心並不會因為要支援新的設備就必須連帶修改。這就是倚靠控制反轉的概念來做到。

回想DOS是怎麼支援CD-ROM這個新設備的呢?即使DOS的核心在開發時,對CD-ROM這種新設備一無所知,但是,DOS允許你外掛所謂的驅動程式,透過在你的DOS上安裝支援CD-ROM的驅動程式,就可以讓DOS作業系統對CD-ROM執行讀取的動作──即使它對CD-ROM一無所知。而這個控制反轉其實便建立在這個「一無所知」的基礎之上。因為根本不知道,所以也不會依賴於它。

從上面的這個例子,你可以看到控制反轉和「相依倒置原則」的相似性。在「相依倒置原則」中,Robert C. Martin的解決方法是引入一個抽象的介面層,讓上層的模組相依於這個介面層,也讓下層的模組相依同一個介面層,這麼一來,上層的模組就不再相依於下層的模組了。

而這個實例中,DOS是透過「驅動程式」的抽象介面層來改變作業系統相依於輸入輸出設備的相依關係。「驅動程式」本身就是一個抽象的介面,作業系統透過它定義了所有實體設備都需要具備的共通介面,但同時又不涉及太多的實作細節。

作業系統核心所面對、所操作的,都是抽象的驅動程式介面,並不會受實作的變化而影響。核心可以利用同一個介面來操作軟碟機、硬碟機,也可以用同一個介面來操作光碟機。在現在,甚至有人開發了針對DOS提供的USB磁碟機驅動程式,讓古老的DOS也能使用最現代的USB磁碟機。

而這就是控制反轉設計的威力所在。若是少了驅動程式這個抽象層,作業系統或許就必須直接面對各種輸入輸出設備,那麼,想要讓古老始終沒有更新的DOS獲得支援USB磁碟機的可能性,或許就有相當的困難度。

這種設計為何適用於應用程式框架上?
程式中如果可以畫分成為通用和特定兩種層次,通用的部份處理所謂的政策(Policy),而應用特定的部份處理所謂的機制(Mechanism),那麼,便可以利用控制反轉的設計方式,讓通用的部份不相依於特定的部份,讓政策不相依於機制。如此一來,機制可改變、也可輕易的擴充或縮減。

這種設計的應用方式,特別適合用在所謂應用程式框架(Framework)或是所謂的應用程式引擎(Application Engine)。

舉例來說,像視窗程式的應用程式框架,有許多便是利用控制反轉的觀念來設計的。在我們常見到的視窗應用程式框架中,通常會針對它所能操作的「視窗」定義出抽象的介面,而且,通常是一個抽象介面的階層體系。例如,像Java的Swing中,JFrame類別繼承自Frame,而Frame繼承自Window、Window又繼承Container、Container繼承最原始的Component。

Swing的程式設計者若是希望在畫面上呈現一個視窗,他可能會選擇實作JFrame,並且設定對應的各種屬性,同時,處理JFrame可能會需要處理的各種事件,藉以描述他所想要的JFrame的特性。

Swing核心的開發團隊,並不會事先知道每一個基於Swing的程式設計者會寫出什麼樣的視窗程式,但是,因為定義好的抽象介面,使得他們並不會相依於任何具體的實作,但他們所寫通用的視窗系統核心,卻可以驅動各種可能的實作。同樣的情況,你可以在3D繪圖引擎,或像是線上角色扮演遊戲引擎等應用領域中看到。同一套3D繪圖引擎,可以允許應用程式開發者用以繪出各種千變萬化的3D圖像,而同一套線上角色扮演遊戲也允許遊戲開發者,藉以產生各種不同的線上角色扮演遊戲。這些所謂的「引擎」都捕捉了某一種應用領域中共通的部份,而允許使用引擎的人自行定義應用中特定的部份,而這便是憑藉著控制反轉的觀念。

控制反轉使得某個應用的通用部份(而且通常被稱為核心部份)可以由某個團隊來開發,而讓其他多個不同的團隊自行開發其應用的特定部份,藉以形成各種不同的應用,滿足特定應用之所需。善用這種設計方式,可以讓一段通用的程式碼驅動無限可能特定的程式碼,同個核心但換個不同的殼,就形成了不同的面貌。

 

專欄作者

熱門新聞

Advertisement