在套用各式以工廠(Factory)為中心的生成模式時,基本上有一個重要的中心思想,就是隔離用戶端程式碼對實際類別的認識,只允許用戶端程式碼認識某個介面,卻不允許它認識實際的類別。當我們採用這樣的設計方式,我們的設計對於「變更」的抵抗力提升了。

相依倒轉原則
這便是物件導向類別設計裡的一個重要的原則:相依倒轉原則(Dependency Inversion Principle,DIP)。DIP是Robert C. Martin所提出重要的一個設計原則,這個設計原則有兩個核心:首先,高階模組不應相依於低階模組,二者皆應相依於抽象概念,其次,抽象概念不應該相依於實作細目,實作細目應相依於抽象概念。

這個設計原則,本質上就是告訴我們,如果設計上非得存在相依關係時,那麼就相依在比較抽象的部分吧!因為在系統的設計中,愈是抽象的事物,被改變的機會就愈小。

例如,我們把系統中的登入功能分成三種層次探討:A是指系統提供認證使用者身份的功能;B代表系統提供使用者輸入帳號及密碼來認證的功能;C意思是系統提供使用者輸入帳號及密碼,以比對資料庫的帳號資料來認證的功能。這種分法很容易便可以理解,這是依照抽象程度,由高至低排列下來,是在描述同一件事。

立足在愈抽象的點看系統的功能,就愈不會被可能的改變所影響。倘若上述的三個層次是系統目前設計的寫照,那麼最有可能被改變所波及的,正是抽象程度最低的C。

在大多數的情況下,我們只會遇上C改變的情況,但比較不會遭遇到B必須要改變的情況,因為基於帳號和密碼的認證模式,是廣泛被採用的認證方式──姑且不論系統要到那邊對比對帳號和密碼。

B比較不會需要改變的原因,正是因為抽象程度比較高,它將更具體的實作細目(也就是具體的帳號密碼來源)抽離,所以受到變動而影響的威脅就小多了。不過,它的抽象程度終究不夠高,倘若有朝一日,這個系統被要求要使用IC卡認證時,建立在B之上的概念,就會被影響到。

但此時A仍是高枕無憂的,因為就算是系統要改用IC卡認證,從A來看系統的觀點──系統提供認證使用者身份的功能,依然適用。這又再次說明,愈是抽象的概念,愈不會被改變所影響。

星星之火,足以讓你疲於奔命
回到DIP,我們如果非得存在相依關係,選擇相依於較為抽象的部分,因為愈抽象愈不會被改變所影響,而愈是抽象的部分,就愈會是系統中宏觀的部分。倘若讓抽象相依於具象,便會因為具象的部分容易變動,引發抽象的部分也頻繁地跟著變動。

這使得星星之火時常燎原,然後設計者就會像疲於奔命的救火隊,四處救火。DIP原則希望引導設計者將錯誤設計中「抽象相依於具象」的情況倒轉。

舉例說明所謂抽象相依於具象的程式碼如下:


LDAPAuthenticator authLDAP =
new LDAPAuthenticator ();
authLDAP.authenticate(id, passowrd);


這段程式碼是系統中認證使用者的部分,當程式碼中直接寫下new LDAPAuthenticator ()時,意指它認識了一個相當具象的類別(也就是在前段文中所提到的(C)的抽象層級上),所以它也相依於LDAPAuthenticator這個相當具象(因為它的抽象層級低)的類別之上。當這具象的事實有所變動(帳號密碼來源改變),這段用戶端程式碼立即會被影響,例如改寫成以資料庫的帳號密碼認證:


DBAuthenticator authDB =
new DBAuthenticator ();
fAuthResult =
authDB.authenticate(id, passowrd);


既然愈抽象的概念,愈不會被改變所影響。相反的,愈是具象的事物,就愈會被改變所影響。當我們直接產生一個類別時,我們幾乎都相依於某個相當具象的事物(代表抽象觀念的介面及抽象類別無法透過new語法產生),這是我們之所以要透過以工廠為中心的生成模式,產生物件實體的原因。

直接New一個物件,已違反DIP
當我們應用以工廠為中心的生成模式產生物件實體時,用戶端程式並不會碰觸到具象的部分,只會接觸到抽象的部分,因為它只會接觸到所有實際類別的共通介面,也就是產品介面。如此一來,便降低相依於具象事物的程度,達到了DIP希望我們依循的原則。

基本上,在程式中直接利用new語法產生類別物件,已經違反了DIP。因為直接產生類別物件的寫法,使得程式和具象類別有了相依性。因此,倘若要完全遵守DIP,勢必得利用各種生成模式才有可能辦到,好比以「Simple Factory Method」產生符合同一介面的各式具象類別的實體。

大量採用生成模式會面臨實務上的考量,因為這使得系統在設計上,得為每種可能會採用到的類別,都套用生成模式。即使採用最簡單的「Simple Factory Method」,每當要產生一種具象的類別,就得為系統添加一個相對應的介面或抽類別,以及工廠類別或是Method,這使得系統的類別數會大增,同時也會因為加了上一層間接層,而提高了系統的複雜度。

不要落入過度工程化的陷阱
要完全遵守DIP,我個人認為實務上是有困難的。DIP的基礎假設,是認為所有的具象類別都有可能會變化,因此才會希望程式碼能夠相依於抽象的介面,而不要相依於會變化的具象類別。

不過,這樣的假設並不一定符合開發的實況。在設計時,不應該無條件地假設萬事萬物都會改變。倘若在設計的同時,對於系統未來可能的變化假設過了頭,想要提供無止盡的彈性及擴充性,很容易會落入過度工程化(Over Engineering)的陷阱之中。對彈性及擴充性的假設,還是需要框在一個合理的範圍之中,至於多大的範圍算是合理的範圍,顯然是對設計者功力的考驗。

許多類別在系統中其實是保持相對穩定的狀態。這些類別可以被預期不致於有什麼變化,其實就算違反DIP,直接產生物件,也不致於對系統產生負面的效應。

設計者應該關注的,是那些明知日後會變化或預期日後可能變動的類別。在設計時,應該針對這類變化可能性高的類別,設法利用生成模式封裝產生的邏輯,將所產生的具象類別隱藏。

DIP是物件導向類別設計原則中和產生物件息息相關的。因為產生物件是使用物件的源頭,倘若在產生物件時便相依於具象,那麼便等於在源頭就製造了不正確的相依性方向。套用了適當的生成模式,便是在源頭處建立起正確的相依性方向。

《作者簡介》王建興
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC Game到P2P網路電話都在他的涉獵範圍之內。

相關閱讀:
探索產生物件的技巧(1)當心隨手New一下引發的衝擊效應
探索產生物件的技巧(2)生成模式的初階應用
探索產生物件的技巧(3)鬆綁程式內工廠與產品的關聯性
探索產生物件的技巧(5)維持類別僅存在一份實例的設計方法
探索產生物件的技巧(6)受夠重新開機?試試自行管理記憶體
探索產生物件的技巧(7)避免時效性不高的重複性動作
探索產生物件的技巧(終)間接產生物件的訴求:降低相依性

熱門新聞

Advertisement