程式者的胡言亂語
漫談程式碼的相依性 - 3
在前文中提到,我們在進行設計時,若想要降低相依性所造成的影響,除了可以試著降低相依關係的個數之外,也可以透過調整相依性連結的位置(將模組與模組間的連結改變成為模組內的連結)以及轉換相依性的類型(將強相依性的關係改變成為弱相依性的關係)。而這些正是處理相依性時的大原則。接下來,就讓我們探討在實際的設計工作中的一些議題,包括了應該避免那些會造成高相依性的動作,以及應該如何改善。
一個最基本、但也是物件導向設計初學者最常犯的一個錯誤就是未對類別的資料成員進行封裝。在這樣的錯誤下,某個類別直接存取了另一個類別的資料成員,而這就造成了所謂的 ”content coupling”,而content coupling是強度最高的coupling類型。資訊隱藏是物件導向設計最基本的功課之一,而讓一個類別的資料成員直接曝露於外,使其可受外界直接存取,便大大的破壞了資訊隱藏及封裝的大原則。尤其當多個資料成員的更改,必須依據一定的規則,而且在修改時具有連動的性質,因而在更動它們時必須保有不可分割的性質時,將資料成員曝露於外,很容易在客戶端程式未能完整的掌握更動邏輯的情況下,造成此類別物件狀態進入不一致的狀態。除了這個風險之外,更危險的是,資料成員通常是內部實作的表現特徵,曝露資料成員等於曝露內部實作。讓客戶端程式相依於內部實作,是件高度危險的事情。物件導向程式語言幾乎都提供存取權限修飾詞,例如Java提供了public、protected、private、package access等權限,讓你得以設定類別成員的存取權限。對資料成員而言,在沒有特殊的考量下,應設為private權限,完全阻擋外界對其資料成員的存取。
物件導向程式設計初學者對於存取權限尚有另一個常見的問題,也就是沒有妥善的控制函式成員的存取權限。有些設計者或許利用存取權限修飾詞封鎖了外界對資料成員的存取,但缺乏思考其函式成員的權限。有些設計者幾乎將所有的函式成員皆設為public。可是宣告為public的函式成員對一個類別來說,其意義為何呢?事實上,它代表著該類別對外的介面,所謂的「介面」即為類別和類別之間相交界的銜接處,在這個介面中的函式代表著這個類別開放對外的入口及通道。當這個介面中的函式愈多時,也就代表外界與其相接觸的面積更廣,進而有可能招致更多的相依關係,造成更多的類別相依於此類別。事實上,許多被宣告為public的函式僅僅只做為內部使用,它們應該被宣告為private,因為它們代表的是內部的實作方式,其主要作用是供那些被宣告為public的函式使用。所以原則就很清楚了,public函式做為類別本身的公開介面,而private函式則做為類別的內部實作。
類別的介面應當要盡可能的窄化,也就是宣告為public的函式應當要盡可能的少。當你發現一個類別有著過於廣泛的介面時,意謂著於其介面中的函式其相關度(表示內聚力)可能不夠高。過於廣泛的介面暗示此類別可能被賦予過多責任,已經成了一個超級類別(super class)。此時,應該要將此類別進行拆解,使其成為若干個較具內聚力的類別,一來可以提昇每個類別內的內聚力,同時也可以降低每個類別對外的耦合程度。
想要降低類別間的相依性,一個最基本的原則,就是盡量避免某個類別的名稱出現在另一個類別的原始碼中(hardcode class names)。「相識即相依」,當一個類別認識了另一個類別後,,無論這相依關係是強或是弱,仍免不了建立某種程度的相依關係。而你應該要避免不必要的相依關係。舉個例子來說吧,類別A的函式接收來自客戶端的三種資訊以便進行處理,而另一個類別B欲呼叫A的此一函式,而B所擁有的這三種資訊事實上是被封裝在類別C裡。有一種設計是直接傳入C的物件,而在A的函式中再由C的物件取出所需的資訊。但這麼一來,不僅B原先就認識C,而且還進一步讓A也認識了C,可是A僅僅只需要C中所含的三項資訊,而C所包含的資訊可能更多於此,這使得我們為了以封裝為C的方式來取得這三項資訊,付出了讓A認識了C的代價。事實上,倘若擴展A函式的引數列表,明確的展開三項資訊做為引數,便可以消除A對C的認識需求。
在這個例子中,為什麼我會舉「三」項資訊為例呢?在重構的方法中提到了「過長的參數列(Long Parameter List)」這一種壞味道,便是因為傳入過多的資訊進入函式,因而造成程式碼的問題。對此,重構的方式建議我們使用「引入參數物件(Introduce Parameter Object)」的方式,將引數列中的多個引數集結包裝成為單一物件,進而縮短引數列的長度。很有趣的是,這恰好和我們的建議呈現背道而馳的情況。事實上,此處是需要取捨的地方。當引數列在拆解資訊後不致於太長時(例如三個引數就不致於太長),拆解資訊不致於造成過長參數列的問題,又能降低額外認識另一類別的相依性。但倘若拆解後會形成過長參數列,你就必須思考是否需要引數參數物件、增加相依性來做為投資了。
在本系列中,我們反覆的提到「針對介面來撰寫程式,而不要針對實作(Program to interface, not an implementation)」這個原則。針對可能會產生變化的實作而言,利用抽象化的介面築起一道或多道阻隔變化的防火牆,是相當有效的方法。可是,實務上究竟應該如何設計扮演緩衝角色的介面呢?基本的大原則是讓此介面保有「最小相依性、最大穩定性」。
何謂最小相依性?意指盡可能的讓介面中所含的成員愈少愈好。或許此介面所抽象化的類別具有相當多的函式成員,但並非每個函式成員都具備對外公開的特性。僅有具備對外公開特性的函式才適合放到這個介面中。而且,即使你從類別的角度思考該類別應具備某些public函式,但不意謂著這些public函式都適合放入介面之中,介面中的函式應該要保持盡可能的少,才能減少此介面和其他類別相界接的機會。如果你在一開始還分不清楚應當在介面上開放多少函式,那麼,就開放最少數量的函式吧 - 只開放那些有明確客戶端程式碼需要的函式,讓介面隱藏住類別的其餘可用函式。等到有明確的客戶端程式對某函式的需求浮現,而且此函式做為此類別對外的操作是具有意義時,才開放此一函式於介面之中。之所以要在一開始讓介面中的函式保持盡可能的少,是因為日後再開放函式是對介面的擴充,並不會影響到既有的程式碼。倘若一開始便開放多過於需求的函式,日後想要再收回便會有更高的困難度。
除了保持介面盡可能小之外,我們也應該評量介面中的函式及其外貌式(signature)是否是相對穩定的。我們運用介面的目的,是希望透過抽象化的過程,將具象類別的抽象化層面抽取出來。因為抽象化的層面其變化的機會遠小於具象的層面,所以當客戶端程式碼相依於抽象的介面時,便不致於受到介面背後具象類別的變化所影響。而不夠穩定的介面,會在背後的具象類別發生變化時,跟著發生連動,使得介面本身也必須因應做調整。這麼一來,我們嘗試著透過介面來阻擋類別變化的想法,形同失敗。設計介面的長相時,應該在心中設想日後可能會有的變化類型以及趨向,試著讓此介面在即使發生你所設想的變化之後,仍然毋需更動其中的函式及外貌式。也就是說,你應該讓介面足夠一般化。愈是一般化的介面,愈能包容可能的變化,也就愈能為背後的類別阻隔變化,這當然是具有更高的穩定性。倘若一層的介面不夠,你應該考慮設計多層、位於不同抽象層級上的介面來保護你的類別。對客戶端程式碼的介面,要保持足夠的穩定性,才能在背後的類別發生變化時,不致於也引發介面本身跟著變化。
Blogged with Flock
Posted at 11:21上午 六月 24, 2008 by Chien-Hsing Wang in General | 迴響[0]
星期二 六月 24, 2008
