Coupling(耦合)
Coupling(耦合)是模組之間互相依賴的程度:一個模組改變時,另一個模組要跟著改的可能性與範圍。這裡的模組泛指任何有對外介面的程式單位:函式、類別、檔案或套件。這個詞與 Cohesion(內聚) 一起由 Larry Constantine 在 1960 年代後期的結構化設計裡提出,1974 年 Stevens、Myers 與 Constantine 的〈Structured Design〉把它們寫成模組設計的兩個衡量標準:內聚量模組內部,耦合量模組之間。低耦合與高內聚常一起出現,原因是它們來自同一個切法:把會一起改的元素放進同一個模組,元素之間的讀寫就留在模組內部,模組之間要交換的資料與呼叫隨之變少。
概念位置
耦合依兩個模組共享的內容分成幾種,由強到弱是:
- 內容耦合(content):一個模組直接使用另一個模組內部的程式碼或資料,例如跳進對方的內部、改對方的私有欄位。
- 共用耦合(common):好幾個模組存取同一份全域資料。
- 外部耦合(external):模組共用一個外部規定的資料格式、通訊協定或裝置介面。
- 控制耦合(control):一個模組傳一個旗標給另一個模組,由旗標決定接收的模組要走哪一條邏輯。
- 戳記耦合(stamp,其他中文資料也寫作「標記耦合」):模組之間傳遞一整個複合資料結構,而接收的一方只用到其中一部分欄位。
- 資料耦合(data):模組之間只透過參數傳遞需要的資料。
耦合越強,一個模組的修改越可能要求另一個模組跟著改。Open-Closed Principle(開放封閉原則)用來降低耦合的手段是讓使用端只依賴一個固定的抽象:使用端與各個實作之間只剩下對那個抽象的依賴,實作的內部改變不會傳到使用端。
可觀察訊號與例子
耦合過強的訊號,是修改一個模組時發現要同時改另一個模組,而這次修改的需求只跟被修改的那個模組有關:
- 改一個模組的內部欄位,另一個模組編譯失敗或行為改變:另一個模組直接讀寫了這些欄位,是內容耦合。
- 改一份全域設定,幾個看似無關的模組行為都變了:它們共用同一份全域資料,是共用耦合。Martin 對全域變數的說明在 Open-Closed Principle 卡片。
- 函式收一個
mode或is_xxx參數,依它切換成完全不同的兩套行為:呼叫端在控制被呼叫端的流程,是控制耦合;這種函式通常也代表兩套行為各自有不同的改變理由,拆成兩個函式之後,呼叫端直接呼叫它要的那一個。 - 函式收一整個物件、只讀其中一兩個欄位:例如計算運費的函式收整個訂單物件,只讀了寄送地址。訂單物件的結構一改,運費函式就要重新確認,是戳記耦合;測試這個函式時也得先造出一整個訂單物件。改成只收寄送地址,就降成資料耦合。
兩個模組之間有耦合本身不是錯誤,模組要合作就一定要交換資料或呼叫對方。要確認的是交換的內容是否只有完成這次呼叫實際用到的資料。
設計責任
降低耦合的設計責任是讓模組之間只交換合作需要的資料,而且依賴的方向穩定。判斷問句是:改這個模組時,有哪些別的模組要跟著改,原因是什麼;原因是那些模組讀了這個模組的內部、共用了同一份可變資料、或用旗標控制這個模組的流程,就把那份依賴改成更弱的形式,依情形選一種:只傳它實際用到的資料(降成資料耦合),或改成依賴一個抽象(做法見開放封閉原則)。降低耦合之前先確認那兩個模組是否本來就該是同一個:兩者過去的修改幾乎每次都一起發生,或一方大量讀寫另一方的內部資料,拆成兩個模組只會讓每次修改都要改兩處,形成 Shotgun Surgery(散彈式修改)。這時把兩者合併成一個模組比降低它們之間的耦合更合適,合併之後它們放在一起的理由相同,內聚也隨之提高。
#design-principles #knowledge-card #coupling #structured-design