2007年9月28日 星期五

相依倒轉原則

from http://javaworld.com.tw/roller/page/qing/20070924

類別物件的產生(4)相依倒轉原則

在前文中,我們介紹了如何透過reflection的技巧來降低”Simple Factory Method”與它所產生之類別物件的相依性。在本文中,我們要轉向討論一個和產生物件息息相關的設計原則。

在 套用各式以工廠為中心的生成模式時,你會發現,基本上我們有一個重要的中心思想,就是隔離用戶端程式碼對實際類別的認識,我們只允許用戶端程式碼認識某個 介面,不允許它認識某個實際的類別。在之前的實例中,你應該會慢慢了解到當我們採用這樣子的設計方式時,我們的設計對於「變更」的抵抗力提昇了。

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

這個設計原則,其實本質上就是告訴我們,如果設計上非得存在相依關係時,那麼就相依在比較抽象的部份吧!為什麼要這麼做呢?在系統的設計中,愈是抽象的事物,被改變的機會就愈小。例如,我們把系統中的登入功能分成三個層次來探討:(a)系統提供認證使用者身份的功能(b)系統提供讓使用者輸入帳號及密碼來認證的功能(c)系統提供讓使用者輸入帳號及密碼以比對資料庫中的帳號資料來認證的功能。很容易便可以理解到,這分別是依照抽象程度的高低,由高至低排列下來,但可以說是在描述同一樣事。

立足在愈抽象的點來看系統的功能,就愈不會被可能的改變所影響。怎麼說呢?倘若上述的三個層次是系統目前設計的寫照,那麼最有可能被改變所波及的,正是抽象程度最低的(c)。因為我們的系統很有可能被要求要改採別的帳號資料來源來認證使用者,例如LDAP。儘管如此,在大多數的情況下,我們只會遇上(c)改變的情況,但比較不會遭遇到(b)必須要改變的情況,因為基於帳號和密碼的認證模式,是廣泛被採用的認證方式-姑且不論系統要到那邊對比對帳號和密碼。(b)比較不會需要改變的原因正是因為(b)的抽象程度比較高,它將比較具體的實作細目(也就是具體的帳號密碼來源)予以抽離,所以受到變動而影響的威脅就小的多了。不過,它的抽象程度終究不夠高,倘若有朝一日,這個系統被要求要使用IC卡來認證使用者時,建立在(b)之上的概念,就會被影響到,因為從(b)的角度來看,認證使用者的基準是一組帳號和密碼,但IC卡的認證模式卻非如此。但即使(b)被影響到了,但(a)仍是高枕無憂,因為就算是系統要改用IC卡來認證使用者,從(a)來看系統的觀點-系統提供認證使用者身份的功能,依然適用。這又再次的說明,愈是抽象的概念,愈不會被改變所影響。

讓我們回到DIPDIP告 訴我們如果非得存在相依關係時,選擇相依於較為抽象的部份,因為愈抽象愈不會被改變所影響,愈是抽象的部份,就愈會是系統中宏觀的部份。倘若我們讓抽象相 依於具象,便會因為具象的部份容易變動,引發抽象的部份也頻繁的跟著變動,這使得星星之火時常燎原,然後設計者就會像疲於奔命的救火隊一樣的四處救火。而DIP原則便希望設計者將錯誤設計中時常看到的抽象相依於具象的情況予以倒轉,讓抽象不再相依於具象。

前文我們已經寫過相依於具象的程式碼了:

LDAPAuthenticator authLDAP = new LDAPAuthenticator ();

authLDAP.authenticate(id, passowrd);

前文我們已經寫過相依於具象的程式碼了:

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

DBAuthenticator authDB = new DBAuthenticator ();

fAuthResult = authDB.authenticate(id, passowrd);

在本文中已經提到:是抽象的概念,愈不會被改變所影響。相反的,愈是具象的事物,就愈會被改變所影響。當我們直接產生一個類別時,我們幾乎都相依於某個相當具象的事物(因為代表抽象觀念的介面及抽象類別是無法透過new語 法去產生其物件的),這是我們之所以要透過以工廠為中心的生成模式來產生物件實體的原因。當我們應用以工廠為中心的生成模式來產生物件實體時,用戶端程式 並不會碰觸到具象的部份,反而只會接觸到抽象的部份,因為它只會接觸到所有可能實際類別的共通介面,也就是產品介面,如此一來,便降低了相依於具象事物的 程度,達到了DIP希望我們依循的原則。

基本上,在程式中直接利用new語法來產生類別物件,其實就違反了DIP。因為直接產生類別物件的寫法,使得程式和具象類別有了相依性。因此,倘若要完全的遵守DIP,勢必得利用各種生成模式才有可能辦到,好比我們在本系列文中,使用”Simple Factory Method”來產生符合同一介面的各式具象類別的實體一樣。大量的採用生成模式會面臨實務上的考量,因為這使得系統在設計上,得為每種可能會採用到的類別,都套用生成模式。即使採用最簡單的”Simple Factory Method”,每需要產生一種具象的類別,就得為系統添加一個相對應的介面或抽類別,以及工廠類別或是method,這使得系統的類別數會大增,同時因為加上一層間接層,也提高了系統的複雜度。

實務上要完全遵守DIP,我覺得是有困難。DIP的 基礎假設是所有的具象類別都有可能會變化,因此才會希望程式碼相依於抽象的介面,而不要相依於會變化的具象類別。不過,這樣的假設並不一定符合我們所開發 之系統的實況。在設計時,不應該無條件的假設萬事萬物都會改變。倘若我們在設計的同時,對於系統未來可能的變化假設過了頭,想要提供無止盡的彈性及擴充 性,很容易會落入過度工程化(over engineering)的陷阱之中。對彈性及擴充性的假設,還是需要框在一個合理的範圍之中,至於多大的範圍算是合理的範圍,顯然是對設計者功力的考驗。

許多類別在系統中其實是保持相對穩定的狀態。這些類別可以被預期系統中不致於有什麼變化,這類的類別其實就算違反DIP去直接產生它們的物件,也不致於對系統產生什麼負面的效應。設計者應該關注的,是那些明知日後會變化或預期日後可能變動的類別。設計者在設計時,應該針對這類變化可能性高的類別,事先利用生成模式來封裝其產生的邏輯,將所產生的具象類別予以隱藏。

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

沒有留言: