iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

AI時代下的軟體工程系列 第 17 篇

Day17: Dependency:不要太依賴我了 (Composition vs Inheritance)

  • 分享至 

  • xImage
  •  

第2階段我們很大的程度都在講 程式碼與程式碼 之間的關係,像是 boundary / contract / abstract ...

有所謂的關係 就存在 Dependency

Dependency 是什麼

前面幾天已經數次提到「依賴」這個詞。

如果一段 code 必須知道另一段 code 的某些資訊,才能正確工作,我們就可以說它依賴那些資訊。

例如:

def show_entries(storage: SQLiteStorage) -> None:
    for entry in storage.list():
        print(entry)

show_entries() 至少知道兩件事:

  1. 有一個叫做 SQLiteStorage 的型別。
  2. 它提供 list()。

因此這兩件事只要改變,show_entries() 就可能需要跟著修改。

從 change 的角度來看,dependency 就是一條 change 可能傳播的路徑:

SQLiteStorage 改變
        ↓
show_entries() 可能需要跟著改

Dependency 本身不是問題。程式不可能完全沒有 dependency;show_entries() 想取得帳目,本來就必須依賴某些東西。

真正值得注意的是:

client 到底依賴了 implementer 的哪些資訊。

如果 client 只依賴穩定、必要的 contract,implementer 裡面的改動大多可以停在 boundary 裡;反之 client 依賴了具體實作的細節,那些細節一變,change 就容易沿著 dependency 傳出去。

依賴越多,對方改變時越容易受到影響;這種彼此綁定的程度,通常稱為 coupling。

而 object 之間建立 dependency,常見的方式有兩種:inheritance 與 composition。

Inheritance 繼承:我是你的一種

昨天的 Storage 可以這樣實作:

class SQLiteStorage(Storage):
    def save(self, entry: Entry) -> None: ...
    def list(self) -> list[Entry]: ...

這裡表達的是 is-a:

SQLiteStorage is a Storage

概念大概是 Storage 能做到的事情 SQLiteStorage 都也要能做到。

Inheritance 不是單純取得幾個 method,而是讓子類(SQLiteStorage)加入父類(Storage)的 type hierarchy。

因此父類提供的 interface、行為與規則,都會成為子類的一部分;子類對父類形成的 dependency 也比較廣。
由於是 is a 的關係,當今天父類改變,子類就很有可能需要做出對應的改動

如果只是為了使用某個功能而繼承,就容易出現奇怪的關係:

class ReportService(Logger): ...

這等於宣告 ReportService is a Logger,但它真正想做的可能只是使用 Logger。

Composition 組合:我需要你

另一種方式,是直接持有需要的 object:

class ReportService:
    def __init__(self, logger: Logger):
        self.logger = logger

    def generate(self) -> None:
        self.logger.log("report generated")

這裡表達的是 has-a / uses-a:

ReportService has a Logger

ReportService 不需要成為 Logger,只需要知道完成工作所需的那部分 contract。

而只要 contract 相同,也可以替換 implementation:

ReportService(ConsoleLogger())
ReportService(FileLogger())

因此 composition 通常把 dependency 限制在比較小的範圍。

由於是 has a 的關係,當今天工具改變,使用者只需要變動用到工具的地方即可

Inheritance vs Composition

Inheritance Composition
關係 A is-a B A has-a B
Dependency A 的型別建立在 B 上 A 只使用 B 的部分能力
Coupling 通常較高 通常較低
替換 implementation 較困難 較容易
適合情況 A 真的是一種 B A 只是需要 B 幫忙完成工作

兩者都會產生 dependency,差別在於 A 必須知道 B 多少事情。

Favor composition over inheritance

因此軟體設計裡常看到一句:

Favor composition over inheritance.

意思不是不要使用 inheritance,而是不要只是為了 reuse code,就把兩個 class 綁進同一個 hierarchy。

可以先問:

A 真的是一種 B,還是 A 只是需要 B?

這也能重新看昨天的 abstraction:

class SQLiteStorage(Storage): ...

SQLiteStorage is a Storage,所以 inheritance 合理。

但真正使用 storage 的 client:

class LedgerService:
    def __init__(self, storage: Storage):
        self.storage = storage

則是:

LedgerService has a Storage

LedgerService 不需要知道是 SQLite、File 還是其他 implementation,只需要依賴 Storage 這份 contract。

所以好的設計並不是消除 dependency,而是讓 dependency 停在真正需要的地方:

只依賴完成工作所需要的資訊,不要因為方便而知道更多。

對 Coding Agent 的幫助

較少的 dependency,代表完成一個修改時,需要理解的 context 也比較少。

如果 LedgerService 只依賴 Storage 的 contract,coding agent 要修改 service 時,只需要知道 Storage 提供哪些能力,不必再讀 SQLiteStorage 裡的 SQL、connection 或 schema。

這和前面談 boundary 的目標是一樣的:讓 change 只需要理解局部資訊,也盡量只影響局部。

對 coding agent 來說,dependency 越少、越明確,就越容易:

  • 用較少的 context 理解目前的工作。
  • 降低修改時誤碰其他 implementation detail 的機率。
  • 在替換 implementation 時,減少需要一起修改的檔案。

因此降低 coupling 的價值,不只是讓 class diagram 比較漂亮,而是:

讓一次 change 所需要理解的範圍,以及可能影響的範圍,都盡可能縮小。


上一篇
Day16: Abstraction:把 contract 從實作中抽出來
下一篇
Day18: State & Side Effect:隱藏在 function 底下的魔鬼
系列文
AI時代下的軟體工程 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言