iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

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

Day16: Abstraction:把 contract 從實作中抽出來

  • 分享至 

  • xImage
  •  

Day15 的 contract 是寫給人看的:precondition、postcondition 寫在 docstring 裡,靠 client 自己讀、自己遵守。

不過,code 裡本來就有一部分 contract 不必靠人記得。
method 的名稱、參數與回傳型別,都是 client 可以直接遵守的承諾,對於動態型別語言 e.g., python Day9 提到的 type checker 就是為此而生。而在 C++、Java 這類靜態型別語言裡,這項檢查更直接由 compiler 負責,不符合的程式根本編譯不過。

換句話說,每一個 class 本身就是一份 contract。既然如此,為什麼許多語言還要另外提供 interface、abstract class?

依賴具體實作所產生的問題

假設帳目可以存進 SQLite,也可以存成一般的檔案,於是有了 SQLiteStorage 與 FileStorage 兩個 class。它們都有 save() 與 list(),做法卻完全不同。

如果 client 直接使用其中一個:

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

show_entries() 依賴的就不是「儲存」這件事,而是綁定在 SQLite 上的做法。即使它只用到 list(),FileStorage 也傳不進來:

show_entries(FileStorage("entries/"))  # Pyright:"FileStorage" 無法指派給 "SQLiteStorage"

所以對於 client 來說,都必須實作對應的方法去使用每種不同的 storage。
而這樣 implementer 跟 client 之間就不是一個好的 boundary 了,

問題也不只在型別上。想知道 SQLiteStorage 承諾了什麼,client 得打開這個 class,而裡面的 contract 與實作是寫在一起的:

class SQLiteStorage:
    def list(self) -> list[Entry]:
        """回傳所有已儲存的帳目"""
        rows = self._db.execute("SELECT * FROM entries ORDER BY rowid")

        ... n rows ...
        
    def save()

當今天實作簡短時還好,但只要實作繁瑣,client 就被迫閱讀大量他不需要理解的實作細節,這對於 client 也是一種負擔。

由此可見

若依賴一個具體實作,client 使用起來不靈活,並且增加負擔

What is abstraction

Abstraction,中文是「抽象」,泛指忽略細節、只留下重點。並且透過這些特性,解決了上面的問題。

抽象在 code 裡的形式:一個只有 contract、沒有實作的型別。 它不必真的做任何事,只負責說明「能做什麼」,「怎麼做」則交給其他 class。
Java、Go 稱它為 interface;Python 則可以用 abstract class 表達:

from abc import ABC, abstractmethod


class Storage(ABC):
    @abstractmethod
    def save(self, entry: Entry) -> None:
        """儲存一筆帳目。

        Precondition:  entry.id 不可為空字串。
        Postcondition: 儲存後,list() 的結果會包含這筆帳目。
        """

    @abstractmethod
    def list(self) -> list[Entry]:
        """回傳所有已儲存的帳目,不保證順序。"""

ABC 與 @abstractmethod 單純宣告 storage 具有哪些 method 不在這裡實作。
後續再由 SQLiteStorage 與 FileStorage 各自繼承 Storage,補上自己的做法。

client 則改為標註 Storage:

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

show_entries() 不再需要知道帳目存在哪裡、怎麼存,兩種 storage 都能傳進來。它需要讀的也只剩 Storage 中定義的 method 即可。

Python 也可以用 typing.Protocol 寫出同樣的 contract,差別在於 implementer 不必繼承,只要具備相同的 method 即可。

讓工具守住 contract

寫成 abstract class 之後,type checker 就能替邊界的兩側把關。

這裡舉個例子:
在 SQLiteStorage 中或許還有 SQLite 才有的 method,例如整理資料庫檔案的 vacuum()。

但由於這不存在於 Storage 的 contract 上,所以今天 client 想直接使用,會出現以下問題:

def show_entries(storage: Storage) -> None:
    storage.vacuum()  # Pyright:無法存取類別 "Storage" 的屬性 "vacuum"

另一側也是如此:
如果 implementer 漏實作了 list(),Pyright 會在建立物件的地方指出來,執行時 Python 也會直接報錯。

Client 只能使用 contract 寫明的東西,implementer 必須提供 contract 寫明的全部東西。

而對 coding agent 而言,這條界線特別有用。
Day9 提到,型別資訊能在寫之前提供介面資訊,在寫之後提供修正線索。寫之前,agent 只需讀進 Storage 這十幾行,不必把 SQL 或檔案格式都讀進 context。寫之後,它若呼叫了 contract 沒有的 method,type checker 會當場擋下。

而 contract 固定之後,boundary 兩側也能分開進行:依照 Storage 寫 client 的 agent,不必等 FileStorage 完成,也不必讀它的 code。

型別能寫下多少 contract

不過,工具能檢查的,只有寫進型別的部分。那麼,型別能寫下多少 contract?

比想像中多。Day15 的 get_today(),precondition 是「tz 是合法的時區名稱」。參數型別是 str 時,這個條件只能寫在文件裡;如果換成標準庫的 ZoneInfo:

from zoneinfo import ZoneInfo


def get_today(tz: ZoneInfo) -> str: ...


get_today(ZoneInfo("Asia/Taipei"))  # OK
get_today("Asia/Taipei")  # Pyright:str 無法指派給 ZoneInfo
ZoneInfo("Asia/Taipe")  # 執行時丟出 ZoneInfoNotFoundError

不合法的時區名稱,在建立 ZoneInfo 時就會失敗,根本到不了 get_today();type checker 則確保傳進來的一定是 ZoneInfo。這條 precondition,就從文件移進了型別。

但行為寫不進去。list() 不保證順序、儲存後查得到這筆帳目、儲存失敗時丟出 StorageError,-> list[Entry] 一樣也說不出來;Python 的函式簽名,也沒有地方宣告會丟出哪些 exception。所以 Storage 裡的 docstring 一行都不能少,這些承諾仍要靠 Day15 的 spec 寫下,再由 test 驗證。

寫得進型別的條件,交給工具檢查;寫不進型別的行為,仍要靠 spec 與 test。

什麼時候該抽出 abstraction

既然一般的 class 就已經是 contract,也就不是每個 class 都需要再抽出一個 abstract class。只有一種做法、短期內也不會有第二種時,client 直接依賴那個 class 就夠了;硬是多加一層,只是多了一個名稱要維護。

值得抽出來的,是同一件事已經有、或很快會有不只一種做法,像 SQLite 與一般檔案;或是實作的細節多到 client 讀了反而容易誤會。兩者的共同點是:client 不需要知道,這件事是怎麼做出來的。

至於 abstraction 本身的成本,以及抽得太早、太多會發生什麼事,留給階段三討論。

Reference


上一篇
Day15: Contract:連接 boundary 兩側的橋樑
下一篇
Day17: Dependency:不要太依賴我了 (Composition vs Inheritance)
系列文
AI時代下的軟體工程 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言