有一個最基本、但也是物件導向設計初學者最常犯的一個錯誤,就是未封裝類別的資料成員。在這樣的錯誤下,某個類別直接存取另一個類別的資料成員,這就造成了「Content Coupling」,而它是強度最高的Coupling類型。

資訊隱藏是物件導向設計最基本的功課之一,假如讓一個類別的資料成員直接暴露,使外界可直接存取,便大大破壞了資訊隱藏及封裝的大原則。

尤其當多個資料成員的更改,必須依據一定的規則,而且在修改時由於具有連動的性質,因而在更動它們時,必須保有不可分割的性質時;暴露資料成員於外,很容易在客戶端程式未能完整掌握更動邏輯的情況下,造成此類別進入不一致的狀態。

更危險的是,資料成員通常是內部實作的表現特徵,展現資料成員等於揭示內部實作。而且,讓客戶端程式相依於內部實作,是件高度危險的事情。物件導向程式語言幾乎都提供存取權限修飾詞,例如Java提供了Public、Protected、Private、Package Access等權限,讓你得以設定類別成員的存取權限。對資料成員而言,在沒有特殊的考量下,應設為Private權限,完全阻擋外界的存取。

Public函式越多,表示與外界接觸的面積越廣
對於存取權限,物件導向程式設計初學者尚有另一個常見的問題,就是沒有妥善的控制函式成員的存取權限。有些設計者或許利用存取權限修飾詞封鎖了外界對資料成員的存取,但並未仔細思考函式成員的權限。有些設計者幾乎將所有的函式成員皆設為Public。

可是宣告為Public的函式成員,對類別的意義為何呢?事實上,它代表著類別對外的介面,而所謂的「介面」,是類別和類別之間的銜接處,在這個介面中的函式,代表這個類別開放對外的入口及通道。當這個介面中的函式越多時,就代表外界與類別接觸的面積更廣,有可能招致更多的相依關係,造成更多的類別相依於此類別。

事實上,許多被宣告為Public的函式,僅做為內部使用,應該宣告為Private。因為它們代表的是內部的實作方式,主要作用是供被宣告為Public的函式使用。原則很清楚:Public函式做為類別的公開介面,而Private函式則做為類別的內部實作。

類別的介面應當要盡可能地窄化,也就是宣告為Public的函式應當要設法減少。當你發現一個類別有著過於廣泛的介面時,意謂著在它介面中的函式,相關度(內聚力)可能不夠高。過於廣泛的介面暗示此類別可能被賦予過多責任,已經成了一個超級類別(Super Class)。此時,應該要拆解此類別,成為若干個較具內聚力的類別,一來可以提升每個類別的內聚力,同時也可以降低每個類別對外的耦合程度。

相識即相依。當A認識了C,免不了建立相依關係
要降低類別間的相依性,最基本的原則,就是盡量避免某個類別的名稱出現在另一個類別的原始碼中(Hardcode Class Names)。

「相識即相依」,當一個類別認識了另一個類別,無論相依關係是強或弱,仍免不了建立某種程度的相依關係。

程式人應該要避免不必要的相依關係。舉個例子,類別A的函式接收客戶端的3種資訊,另一個類別B欲呼叫A的此一函式,而B所擁有的這3種資訊,事實上是封裝在類別C裡。

有一種設計是直接傳入C的物件,而在A的函式中再由C的物件取出所需的資訊。但這麼一來,不僅B原先就認識C,而且還進一步讓A也認識C,可是A僅需要C的3項資訊,而C所包含的資訊可能多於此,這使得我們為了已封裝為C的方式取得資訊,付出了讓A認識C的代價。事實上,倘若擴展A函式的引數列表,明確地展開3項資訊做為引數,便可以消除A對C認識的需求。

為什麼我舉「3」項資訊為例呢?在重構的方法中,提到了「過長的參數列(Long Parameter List)」這一種壞味道,便是因為傳入過多的資訊進入函式,因而造成程式碼的問題。

對此,重構建議程式人使用「引入參數物件(Introduce Parameter Object)」的方式,將引數列中的多個引數集結包裝成為單一物件,縮短引數列的長度。有趣的是,這恰好和我的建議背道而馳。

事實上,此處的確是需要取捨的地方。當引數列在拆解資訊後不致太長時(3個引數不算太長),拆解資訊既不會造成過長參數列的問題,又能降低額外認識另一類別的相依性。但倘若拆解後會形成過長參數列,你就必須思考,是否需要引數參數物件、增加相依性來做為投資了。

如果分不清楚應當開放多少函式,就開放最少數量
我反覆提到「針對介面來撰寫程式,而不要針對實作(Program to interface, not an implementation)」這個原則。針對可能會產生變化的實作而言,利用抽象化的介面築起一道或多道阻隔變化的防火牆,是相當有效的方法。可是,實務上究竟應該如何設計扮演緩衝角色的介面呢?基本的大原則是讓介面保有「最小相依性、最大穩定性」。

最小相依性,意指盡可能讓介面中所含的成員越少越好。或許此介面抽象化的類別,具有很多函式成員,但並非每個函式都具備對外公開的特性。僅有具備對外公開特性的函式,才適合放到介面中。

即使你從類別的角度,思考該類別應具備某些Public函式,但不意謂這些Public函式都適合放入介面之中,介面中的函式應該要盡可能地保持很少,才能避免此介面和其他類別相介接的機會。

如果你在一開始還分不清楚應當在介面上開放多少函式,那麼,就開放最少數量的函式--只開放有明確客戶端程式需要的函式,讓介面去隱藏類別內其餘可用的函式。等到有客戶端程式對某函式的需求浮現,而且此函式做為此類別對外的操作具有意義時,才開放此一函式於介面。

之所以要在一開始讓介面中的函式盡量少,是因為日後再開放函式,主要是對介面的擴充,並不會影響到既有的程式碼。倘若一開始便開放函式多過於需求,日後想要收回,便有更高的困難度。

設計介面夠一般化,才能包容變化
除了保持介面小之外,我們也應該評量介面中的函式及它的外貌式(Signature)是否相對穩定。我們運用介面的目的,是希望透過抽象化的過程,抽取具象類別的抽象化層面。

而不夠穩定的介面,會在背後的具象類別發生變化時,跟著發生連動,使得介面本身也必須因應做調整。這麼一來,我們嘗試著透過介面阻擋類別變化的想法,形同失敗。

設計每個介面的長相時,應該設想日後可能會有的變化類型以及趨向,試著讓介面在發生設想的變化之後,仍然毋需更動函式及外貌式。也就是說,應該讓介面一般化。越一般化的介面,越能包容變化,也就越能為背後的類別阻隔變化,當然是具有更高的穩定性。

倘若一層的介面不夠,應該考慮設計多層、位於不同抽象層級上的介面,以保護你的類別。對客戶端程式碼的介面,要保持足夠的穩定性,才能在背後的類別發生變化時,不致引發介面跟著變化。

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

相關連結:
漫談程式碼的相依性(1)時時關注類別的相依程度、持續改善
漫談程式碼的相依性(2)低耦合與高聚合才是好的軟體設計

熱門新聞

Advertisement