菜雞與物件導向 (8): 內聚、耦合
做為前後段落的分水嶺,這篇文章我將紀錄一下 「內聚」(Cohesion) 和 「耦合」(Coupling),這兩者是評估一個類別或元件的重要概念。
在實務上,為了提升擴展性,降低維護成本等因素,我們對於單個類別或元件,會有著 「低耦合」 及 「高內聚」 的期待。例如我們在 菜雞與物件導向 (3): 封裝 中,我們就有提到封裝的好壞相當重要,其中也包含了「提高類別內的內聚性,降低對外的耦合性」。那麼,到底什麼是內聚,什麼又是耦合呢?
內聚
「把需要的程式和資料都包裝在同一個模組內,使得該模組能夠做為一個單獨的個體執行」
白話一點說,就是就是把用到的東西都打包到一處,該有的自己都有了,所以即使單獨一個人也能完成工作的能力、可以自己 Carry 整場不用看豬隊友臉色的能力。越能自己單幹,越不需要依賴其他類別的時候,內聚力也就越高。
也就是說:如果你的類別什麼都要依賴其他類別,像小嬰兒一樣需要呵護照顧,那內聚力就很低。反之,如果像野外求生大師,啥都靠自己,那內聚力就超高。
內聚代表的是該模組的獨立性,當這個模組可以獨力完成工作,就代表我們能夠重複使用它,且不需要擔心影響到其他模組。
並且也基於這點,我們不用擔心變動這個模組時需要先處理其他的模組,因為這個工作所需的都包含在模組內了,這樣就可以單獨修改該模組,減少維護成本。
例如你的筆已經包含了所有寫字工具的條件,具有墨水跟筆芯等等,可以只使用筆就完成寫字這個工作。那麼我們就可以隨身帶著,在任何需要的時候重複使用它,而不用擔心我們會不會漏了什麼必要零件沒有帶出門。同時,如果我們需要換筆芯或墨水,我們也知道要更換的部份就在筆裡面,不需要去找鉛筆盒中別的地方。
而我們在物件導向的世界中,是將不同的邏輯和功能,封裝成不同的物件,藉由這些物件的互動來構築我們抽象化的世界和想法。為了隱藏這些物件內部的複雜性,同時又保持物件的整體性,讓物件能真的符合我們概念中的「一個」物件,那麼追求高內聚就是必然的。內聚,是物件的一種美德。
然而,盲目地追求高內聚是很危險的。只要你希望,當然可以寫出一個超級內聚的類別,但這代表什麼呢?
為了提高內聚,把所有相關的東西都一股腦塞進同一個類別,越塞越多越塞越多,沒那麼相關的東西也硬塞在一起。從模組變成義大利麵,再從義大利麵變成大補帖,最後終於變成神。這樣實際上根本就不內聚,類別裡面就是一堆散沙,功能一大堆動輒數千行,改個 Bug 先看三千行程式碼,維護者莫不痛哭流涕…
又或是為了避免上面的狀況,限制了功能範圍。但卻又為了能獨立作業,為了不依賴別人,硬是把別的地方已經有的功能複製一份過來,用到的東西都複製複製複製進來,人人都有一份。最後遇到修改時,要改這又要改那,等著改的地方遍地開花,維護成本暴增,維護者再度痛哭流涕…
到這邊應該能瞭解到完全內聚是不可能也不應該的,過於執著就會走火入魔。那麼怎樣的內聚算是剛剛好呢?或是說,一個良好的高內聚?這就牽涉到這段程式碼的意圖了。
良好的內聚應該只關注在一件事情上,並適時地將不屬於自身職責的工作交給別人,達到所謂「該內聚而內聚,該耦合而耦合」。
所謂「只關注一件事情」、「不屬於自身職責」云云,我們在之後的 單一職責原則 會更進一步地說明。且先按下不表。
為了減少重複程式碼,和降低維護的困難,不管怎樣互動都是不可避免的。那既然我們的物件多多少少都得依賴別人,就不能不提到耦合了。
耦合
「如果模組和另一個模組有關聯,那這兩者之間就耦合」
耦合的定義就是這麼寬廣。不管是接收另一個物件傳入的值,或者是共用同個全域變數,更何況我中有你你中有我,都是耦合。
當兩者之間的關聯越緊密,越無法分離,其耦合度就越高。例如說繼承關係就是強耦合的代表。
當我們的目標放在減少重複的程式碼時,就會有多個模組共用同一段程式碼的情形發生,也會造成這些模組和這段重複使用的程式碼彼此耦合。
那當我們為了其中一個使用者修改了這段程式碼,就會連帶影響其他用到的地方。變成改了這裡壞那裡,修了那裡壞這裡的詭異情況。這也就是我們追求降低耦合的最大原因。
彼此關聯就會彼此牽連,因此我們要讓彼此之間保持一個舒適的距離。
注意,是舒適的距離,而不是不相往來,從這點來看,健康的內聚就是健康的耦合。
內聚與耦合
內聚是模組的獨立性,耦合則是模組的關聯性
「低內聚高耦合」的組合,牽一髮動全身,改個一行程式碼動輒就是大規模傷害,我們甚至不能切分模組,完全和物件的精神背道而馳,這是萬不能接受的。
「高內聚低耦合」則是大家所追求的目標。為了讓每個物件各自獨立又能彼此互動,從物件導向中封裝的角度出發,這個方向絕對是正確的。
但所謂過猶不及,若是太過火變成「超高內聚無耦合」,又會變成可怕的 All in one 融合怪物或是 Ctrl C VVVVV 的複製大軍……
不健康的內聚和不健康的耦合都是問題,內聚和耦合這兩者就像天秤的兩端,我們的目標就是找到那個合適的平衡點,也就是健康的高內聚低耦合才是我們所追求的。
同時也可以注意到內聚和耦合會發生的問題,例如修改時影響其他物件導致壞一整片,又或是修改時太多地方要改成本過高,總是圍繞在擴展和維護,基本上就是面對改變時會發生的問題。
就像基因的優劣在於適應環境並生存下去的能力,程式碼也是如此。為了協助我們追求健康的高內聚低耦合目標,也為了讓我們面對改變(遭遇災難)時有個方針,因此才有了一些原則。
就像我們前面敘述內聚時一直迴避的這些問題:
- 怎樣才是適合的內聚?怎樣才是健康的耦合?
- 如果說過高的內聚會塞太多功能或複製重複功能而變成怪物,過低的內聚則會四處拈花惹草,那我們要怎麼知道這個類別或元件的功能範圍剛剛好?
這些問題的參考準則,就在於我們之後要介紹的「單一職責原則」!
小結
過了兩個月再度接續這個系列,一回來就是一篇碎碎念充當預告片,總之就先交代一下內聚和耦合的概念。
但要真的達到健康的內聚和健康的耦合,不造神、不亂依賴、物件裡面高內聚、物件彼此低耦合,就必須要有一些原則。
所以下回開始就要進入物件導向五大原則的段落了,那麼,我們下次見!
註:內聚跟耦合算是相當重要又基礎的觀念,我個人也還在摸索,只聞其聲不見其影。想要更了解這兩個概念的朋友,可以將參考資料的文章都看過一遍,我個人覺得頗有幫助,尤其是實務上的高內聚與低耦合、搞笑談軟工兩篇,值得特別推薦。
本系列下一篇:菜雞與物件導向 (9): SOLID
參考資料
- 實務上的高內聚與低耦合
- 亂談軟體設計(1):Cohesion and Coupling - 搞笑談軟工
- 斷開鎖鏈! 低耦合、高內聚
- 如何寫高品質 function (內聚性篇)
- Object Oriented物件導向-5:內聚(Cohesion)、耦合(Coupling) - Sian
- Fred 聊聊 SOLID 設計原則
同系列文章
- 菜雞與物件導向 (0): 前言
- 菜雞與物件導向 (1): 類別、物件
- 菜雞與物件導向 (2): 建構式、多載
- 菜雞與物件導向 (3): 封裝
- 菜雞與物件導向 (4): 繼承
- 菜雞與物件導向 (5): 多型
- 菜雞與物件導向 (6): 抽象、覆寫
- 菜雞與物件導向 (7): 介面
- 菜雞與物件導向 (8): 內聚、耦合
- 菜雞與物件導向 (9): SOLID
- 菜雞與物件導向 (10): 單一職責原則
- 菜雞與物件導向 (11): 開放封閉原則
- 菜雞與物件導向 (12): 里氏替換原則
- 菜雞與物件導向 (13): 介面隔離原則
- 菜雞與物件導向 (14): 依賴反轉原則
- 菜雞與物件導向 (15): 最少知識原則
- 菜雞與物件導向 (Ex1): 小結
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)