為什麼需要 Connascence#

Chapter 5 介紹的 module coupling 是在程序式(procedural)語言時代被提出的。物件導向(object-oriented)的興起讓我們需要更細緻的模型,能描述物件互動中各種微妙差異。Meilir Page-Jones (1996) 提出的 connascence(共生)正是回應這個需求。

「Connascence」源自拉丁文,意為「同生」。在軟體設計中,兩個模組「共生」表示它們的生命週期糾纏在一起:其中一個改動,另一個就要跟著改、或至少要仔細審查可能的破壞性變更。

Connascence 比 module coupling 更細緻,分成兩大類:

  • 靜態共生(Static connascence):原始碼層級、編譯期的關係
  • 動態共生(Dynamic connascence):執行期的行為依賴

靜態共生#

依強度由弱到強:name → type → meaning → algorithm → position。越靠後越隱晦、共享越多知識。

Figure 6.1: 不同層級靜態共生所需共享知識的對應關係

Connascence of Name(名稱共生)#

最弱層級。模組必須對「名稱」達成共識——變數、方法、服務、欄位的名稱。

def greet(name):           # 01
    message = f'Hello, {name}!'   # 02
    print(message)              # 03

greet('world')             # 05

可觀察到三組共生關係:

  • 第 1、2 行 → 共享參數名稱 name
  • 第 2、3 行 → 共享變數名稱 message
  • 第 1、5 行 → 共享方法名稱 greet

任何一個名稱變更,就有兩處需要同步修改。

Connascence of Type(型別共生)#

模組必須對「型別」達成共識:

private static void Greet(string name) {
    string message = $"Hello, {name}";
    Console.WriteLine(message);
}

static void Main() {
    Greet("world");
}

強型別把原本的 name 共生升級成 type 共生。即使是動態語言,型別假設仍然存在——只是延後到執行期才暴露。

Type 比 name 略強,但兩者通常成對出現。

Connascence of Meaning(意義共生)#

模組對「特定值的意義」達成共識——也就是傳遞 magic value

fun processEmail(msg: EmailMessage, caseId: CaseId) {
    val supportCase = repository.load(caseId)
    supportCase.appendResponse(msg.body, newStatus = 7)   // 7 是什麼?
}

Magic value 無法被編譯器驗證,介面顯式程度比 name / type 更低。

可以透過提取常數或引入列舉,把 connascence of meaning 降級成 type 或 name:

enum class Status {
    Open, FollowUp, OnHold, Escalated, Closed, Resolved, Reopened
}

supportCase.appendResponse(msg.body, newStatus = Status.FollowUp)

Connascence of Algorithm(演算法共生)#

模組必須對「使用同一個演算法」達成共識,才能解讀彼此交換的值:

  • 雙方加解密必須用同一套演算法
  • 計算 checksum 用 MD5,遠端儲存若用其他雜湊就對不上
fun uploadFile(filePath: String) {
    val data = readFile(filePath)
    val checksum = calculateMD5(data)
    storage.upload(data, checksum)
}

Connascence of algorithm 不是「程式碼重複」的問題。重點是「需要同一個演算法才能彼此理解」,演算法存在第三方函式庫或重複實作都一樣。重複的業務邏輯則是更強的層級。

Connascence of Position(位置共生)#

最強的靜態層級。模組必須對「元素的順序」達成共識:

fun sendEmail(data: Array<String>) {
    val from = data[0]
    val to = data[1]
    val subject = data[2]
    val body = data[3]
}

或是「同型別多個未命名參數」:

fun sendEmail(from: String, to: String, subject: String, body: String) { ... }

或是「未命名 tuple 回傳」:

fun getCurrentDateTime(): Pair<DateTime, DateTime> {
    return Pair(DateTime.Now, DateTime.UtcNow)
}

Position 共生讓整合介面非常脆弱:簡單地調換順序就會破壞所有整合者。看起來與 connascence of name 差不多,但兩者一個顯式、一個隱式,可靠性差距巨大。

動態共生#

動態共生描述執行期的行為依賴,比所有靜態共生都強。由弱到強:execution → timing → value → identity。

Figure 6.2: 不同層級動態共生所需共享知識的對應關係

Connascence of Execution(執行共生)#

模組必須以特定順序執行(靜態 position 的動態對應):

interface DbConnection {
    fun openConnection()
    fun beginTransaction(transactionId: UUID)
    fun executeQuery(sql: String): QueryResult
    fun commit(transactionId: UUID)
    fun rollback(transactionId: UUID)
    fun closeConnection()
}

執行順序的隱藏規則:

  • 所有方法都必須在 openConnection 之後
  • closeConnection 之後不可再執行任何方法
  • commit / rollback 必須在 beginTransaction 之後
  • executeQuery 必須在交易開始後、提交或回滾前

Connascence of Timing(時序共生)#

不只順序,還要在「特定時間間隔」內:

  • DB 連線開了 30 秒沒動作就 timeout
  • 車門解鎖後若一段時間沒開門就自動上鎖
  • X 光機啟動後 N 秒自動關閉

隱晦的例子:依賴系統時鐘多次取值

fun getTime(): Pair<Int, Int> {
    val hour = DateTime.Now.Hour
    val minute = DateTime.Now.Minute
    return Pair(hour, minute)
}

兩行之間若被作業系統 delay 或剛好遇到 daylight saving time 切換,會回傳錯誤結果。可以重構成只取一次:

fun getTime(): Pair<Int, Int> {
    val now = DateTime.Now
    return Pair(now.Hour, now.Minute)
}

但 DB timeout 這種就無法重構掉,因為它本身就是業務需求。

Connascence of Value(值共生)#

不同欄位的「值」必須一起變動,才能維持系統處於正確狀態。

  • 算術約束:例如三角形三邊長必須符合「兩邊和大於第三邊」
  • 業務不變量:例如「客戶必須先 verified 才能開啟 priority shipping」

Figure 6.3: 三角形邊長必須同時滿足的數學約束

class Customer {
    var isVerified: Boolean = false
    var priorityShippingEnabled: Boolean = false

    fun clearVerification() {
        isVerified = false
        priorityShippingEnabled = false   // 必須一起改
    }

    fun allowPriorityShipping() {
        if (isVerified) {
            priorityShippingEnabled = true
        }
    }
}

Connascence of Identity(身分共生)#

最強的層級。多個模組必須引用「同一個」第三方物件實例才能正常運作。

  • Object:多個物件必須共用同一個 DB connection pool
  • 分散式系統:兩個服務透過讀寫同一個資料庫整合,且都期待立即看到對方的變更

Figure 6.4: 兩個服務透過共用資料庫整合所形成的身分共生

透過 message bus 整合、不要求事務一致性 → 不算 connascence of identity。

Connascence 的整體層級#

兩個模組之間若同時存在多種 connascence,整體層級取「最高」的那一個。

範例:

(res, balance, tran_id) = accounting.process_payment(
    account_id='LVG141028',
    transaction_type=3,
    credit_card='S5hDn175mPiDL4D5ftbtMw=='
)

可同時觀察到:

  • Name:方法與參數名稱
  • Typeaccount_idcredit_card 是字串、transaction_type 是數字
  • Meaningtransaction_type=3 是 magic value
  • Algorithmcredit_card 是 AES 加密過的,雙方須用同一演算法
  • Position:回傳的 tuple 順序

整體層級取最高 → connascence of position

管理 Connascence#

簡單重構就能降級:

  • 用 enum / 命名常數 → meaning 降為 type
  • 用 named arguments → position 降為 name

不要把「降到 name / type」當成設計目標。許多高層級共生是業務本質決定的,無法被重構消除:timing、algorithm、execution、value、identity 都可能屬於這類。

高層級共生的元件「真的是同生的」,不該被分開——它們應該被放在彼此附近。這是 Chapter 8(距離)與 Chapter 10(平衡)的伏筆。

Connascence 與 Module Coupling 的對照#

Module Coupling對應 Connascence
Data任何靜態共生(含 position)皆可
Stamp至少 type;同樣不超出靜態共生
Control沒有對應的 connascence 層級
ExternalIdentity(共享狀態)
CommonIdentity(共享狀態 + 結構知識)
Content name(讀私有欄位只需要欄位名)

Content coupling 在 module coupling 是最強,但在 connascence 是最弱——這代表兩個模型描述的根本是不同面向的耦合!多數 dynamic connascence 在 module coupling 中也找不到對應。

重點整理#

Connascence 的兩大主題:

  • Static:要編譯通過、要溝通成功,雙方介面層面要協調的事——name、type、meaning、algorithm、position
  • Dynamic:要在執行期運作正確,雙方行為要協調的事——execution、timing、value、identity

下一章將直面 module coupling 與 connascence 的衝突,把兩者整合成更實用的 Integration Strength 模型。