程式碼可讀性之簡報介紹vol. 4: “相依關係” 篇

前言

大家好,我是通訊 App「LINE」Android 團隊的石川。

本簡報是 “關於程式碼可讀性之簡報介紹 ” 不定期連載的第四篇。上一篇簡報在這裡

本次將解說第六章與第七章,型態與型態的「相依關係」。

在設計與撰寫程式時,型態之間的相依關係是無可避免的問題。例如,繼承某個型態、把實體當作參數或作為返回值使用,或者是呼叫函式時,對該型態的相依關係就會產生。但若毫無計畫地製造相依關係,將會損及可讀性與強健性。以下將從耦合度、方向、重複、明示性四個觀點來解說如何處理相依關係。

耦合度

耦合度是相依關係強度的指標。這裡我們介紹應特別避免或減少的三種強度耦合:內容耦合、共用耦合、控制耦合。關於各種耦合的詳細解決方法,請參照簡報資料。

1.  內容耦合 (content coupling)

內容耦合是指直接相依耦合對象的內部實裝內容。可以舉出的其中一個極端例子是從外部直接跳到某個 procedure 的特定標籤。在近代的程式語言中,大多是透過限制跳躍方法,使這種內容耦合難以產生。但是若編寫了相依於某個物件的內部狀態的程式碼,就會變成與內容耦合同等的強耦合。這裡舉一個存在不當狀態的設計案例。

首先,撰寫計算數值 ‘Calculator’的class。如果使用這個 ‘Calculator’ 時,前處理或後處理需要特別的手續,或者需要前、後條件,那麼呼叫方將會相依於 `Calculator` 的內部狀態。

class Caller {
    fun callCalculator() {
        calculator.parameter = 42
 
        calculator.prepare()
        calculator.calculate()
        calculator.tearDown()
 
        val result = calculator.result
        // 呼叫 result 程式碼
    }
}

在此例子中,値的取得與送回結果都使用屬性,前處理與後處理分別為 ‘prepare()’ 與 ‘tearDown()’ 。若使用不當的使用方式,例如撰寫使用順序已經固定的class,而忘記呼叫 ‘prepare()’ 等時,將會導致程序錯誤。另外,若因為撰寫了不需要的狀態而平行呼叫這個 procedure 數次,將會導致競爭狀態發生。

這樣的耦合可以藉由刪除或隱藏內部狀態、接收或傳送値時使用參數或返回值來解決。

2.  共用耦合 (common coupling)

共用耦合是指使用全域狀態的相依關係。具體來說,此一耦合為使用全域變數、可變單例模式物件或檔案等單一外部resource並共享狀態時發生的耦合。雖然在大多數情況下,不得不使用檔案等resource,但全域變數與可變單例模式物件應盡可能避免使用。

這裡我們舉一個將 Repository class 的 instance 維持為全域變數的例子。

val USER_DATA_REPOSITORY = UserDataRepository()
 
class UserListUseCase {
    suspend fun invoke(): List <User> = withContext(...) {
        val result = USER_DATA_REPOSITORY.query()
        // snip
    }
}

若將可變物件維持為全域變數,將會使管理變困難。例如除了無法限制生命週期或參照源,也難以替換為其他物件來變更規格或做測試。

要解決上述問題,可避免使用全域變數,將對象物件作為構造函數提出。藉此,對象物件的生命週期或參照源等,可以透過構造函數調用者管理。

3.  控制耦合 (control coupling)

控制耦合是指依參數不同而使動作不同的相依關係。例如撰寫「接收真假値或列舉型的値,再依據所接收的值執行不同動作」的 procedure,即會產生控制耦合。特別是當該不同動作間共用的部分少或分歧範圍廣泛時,應減少耦合。在減少控制耦合的方法中,有以下幾項。

  • 分離procedure,刪除條件分岐
  • 不以條件而以對象區分邏輯
  • 使用策略模式

以下針對「不以條件而以對象區分邏輯」的方法說明。此方法在各條件下操作對象共用時有效。例如,有某個procedure執行以下的UI更新。

fun updateView(isError: Boolean) {
    if (isError) {
        resultView.isVisible = true
        errorView.isVisible = false
        iconView.image = CROSS_MARK_IMAGE
    } else {
        resultView.isVisible = false
        errorView.isVisible = true
        iconView.image = CHECk_MARK_IMAGE
    }
}

這個 procedure 有以下兩個缺點。

  1. 為了掌握 procedure 的概要,需要看各條件的詳細動作
  2. 對象或條件增加時,程式碼將變得繁雜,且容易發生程序錯誤

這個 procedure 不管 `isError` 的値是多少, 以 ‘resultView’、‘errorView’, ‘iconView’ 三個對象共用操作。在此情況下,針對每個對象,將依據 ‘isError’ 所產生的不同分歧進行分割,將可使 procedure 變得簡潔且易於理解。以下為改善的例子。

fun updateView(isError: Boolean) {
    resultView.isVisible = isError
    errorView.isVisible = !isError
    iconView.image = getIconImage(isError)
}
 
fun getIconImage(isError: Boolean): Image =
    if (!isError) CHECk_MARK_IMAGE else CROSS_MARK_IMAGE

相依關係的方向

原則上,相依關係應為單一方向。若使循環相依,將難以追蹤 procedure 的流程,使狀態的管理變得繁雜。為了避免相依關係的循環,請檢查每個相依關係的方向是否正確。以下為正確的相依關係例子。

  • 呼叫方相依於被呼叫方
  • 具象相依於抽象
  • 複雜項目相依於單純項目
  • 演算法相依於資料類型
  • 頻繁變更的項目相依於不太變更的項目

但是,欲使用 callback 時等情況下,可能會需要相依的循環。但即使如此,請考慮刪除該 callback、拆開 callback 部分將相依變為偏序關係、或使用 Promise pattern,看看是否能減少上述情況。另外,即使產生了相依的循環,也應盡量縮小該範圍為佳。

關係的重複

相依關係不一定總是一對一的關係。例如,基本的 utility 或資料類型,相依各種type。此時,某相依對象組合可能共通出現在多個type。若製作一個層,將此共用的相依對象組合加以整合,即可能會變成更加強健且具高可讀性的設計。

例如,將提供使用者數據的 Repository class 撰寫成 Local cache 用與 Remote data 用兩個。此時,使用者的名稱顯示功能、使用者的圖像顯示功能、使用者相關的各種功能共用相依 Local cache 與 Remote data 兩者的 Repository。結果,選擇使用 Local 或 Remote 數據的 procedure 將會重複出現在各個功能中。另外,撰寫新功能或 Repository 時也將變得繁雜。

在這種情況下,建議新創建隱蔽 Local cache 與 Remote data 的層為佳。如此一來,可避免選擇數據的 procedure 重複,追加新功能或 Repository 也會變得簡單。但是有以下兩個需要注意的重點。

  • 需要隱蔽層時才撰寫 (YAGNI、KISS)
  • 不提供揭露被隱蔽的type方法

在這次的例子中,若只有Local cache就不應該撰寫隱蔽層,且隱蔽層不應該直接提供Local cache或Remote data Repository的參照。

相依的明示性

在多數情況下,藉由描繪class圖,可以明確表示type的相依關係,但裡面也會有圖中不會顯示的相依關係存在。例如,將抽象型定義為參數的type,但實際上卻撰寫期待會傳送某個具象型的 procedure ,則會產生對該具象型的隱性相依關係。其它像是將任意文字列作為參數的 procedure ,所有的實際引數為某資料類型所變換而來的文字列時,則該 procedure 可說是對資料類型隱性相依。這樣的隱性相依關係大多不會顯示在 class 圖中,以靜態分析工具也難以發現。

由於隱性相依難以追蹤 procedure 的流程,因此將會降低程式碼的可讀性。另外,變更被隱性相依的 type 時,因相依方所期待的前提被破壞,故可能導致程序錯誤,且難以查出原因。為了減少隱性相依,建議刪除不需要的繼承關係,或定義明示參數或返回值變動範圍的 type 為佳。

結語

本次「相依關係」篇從耦合度、方向、重複、明示性的四個觀點進行了解說。

下一篇「審查與總結」篇為最後一篇,將說明程式碼審查的方法與總結本連載。