iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
AI Engineering

AI 寫 Code 之後,我們還需要 Software Architecture 嗎?系列 第 5

Day 05:介面切分——為什麼「型別提示介面 vs 具體類別」是架構決策

  • 分享至 

  • xImage
  •  

前言:只是換一行程式碼,能有多大差別?

「建構子參數的型別提示,寫介面還是寫具體類別,不就是打幾個字的差別嗎?」

如果只看語法層面,確實是這樣——兩種寫法都能編譯、都能執行,型別檢查也都會過。但這個系列從 Day 03 開始講的核心命題是:架構不是規範「怎麼寫這段程式碼」,是限制「這個改動能碰到哪裡」。今天要說的是,「型別提示介面還是具體類別」正好落在這條命題的核心——它看起來只是一行語法選擇,實際上決定了呼叫端跟這段程式碼之間,到底綁死到什麼程度。

今日目標

  • 理解型別提示介面跟型別提示具體類別,各自替呼叫端承諾了什麼
  • 看清楚這個選擇如何直接決定「未來能不能安全替換底層實作」
  • 認識依賴反轉(Dependency Inversion)原則跟這個系列講的「架構邊界」是同一件事的不同說法
  • 建立一組判斷準則:什麼時候該提示介面、什麼時候提示具體類別反而是對的

型別提示的兩種承諾

一個建構子的型別提示,本質上是呼叫端跟被依賴對象之間的一份契約。這份契約寫成介面還是具體類別,代表了兩種完全不同的承諾:

型別提示介面,承諾的是「我只依賴這組行為,不管背後是誰實作」。呼叫端拿到的物件,只要滿足這組方法簽章就能用——今天是 A 實作,明天換成 B 實作,呼叫端一行程式碼都不用改。

型別提示具體類別,承諾的是「我依賴這一個特定的實作,包含它建構子需要的每一個參數、它內部可能有的每一個副作用」。呼叫端跟這個實作直接綁死,換掉底層實作,代表呼叫端的程式碼也要跟著動。

這不是「介面比較先進、具體類別比較落後」的優劣問題——兩種承諾都有各自成立的情境,重點是這個決定往往是無意識做出來的:寫程式碼的當下,通常只是「剛好手邊有這個類別,就把它塞進建構子」,沒有意識到這個瞬間的選擇,已經替未來的改動範圍畫了一條線。

案例對照:呼叫端跟哪個東西綁在一起

❌ 型別提示具體類別:
class ReportExporter
{
    public function __construct(
        private CsvFileWriter $writer
    ) {}

    public function export(array $rows): void
    {
        $this->writer->write($rows);
    }
}
→ ReportExporter 綁死在 CsvFileWriter 這個具體實作上。
  想加一個「匯出成 JSON」的需求,不能靠替換依賴達成,
  只能修改 ReportExporter 本身,或者在呼叫端到處判斷
  「這次要用哪個 Writer」。
  想寫測試也綁手綁腳——沒辦法簡單替換成一個
  不落地寫檔的假實作。

✅ 型別提示介面:
interface RowWriter
{
    public function write(array $rows): void;
}

class ReportExporter
{
    public function __construct(
        private RowWriter $writer
    ) {}

    public function export(array $rows): void
    {
        $this->writer->write($rows);
    }
}
→ ReportExporter 只依賴「能寫入一批資料列」這個行為。
  要支援 JSON,寫一個新的 JsonRowWriter 實作介面即可,
  ReportExporter 本身完全不用改。
  測試時换成一個把資料寫進記憶體陣列的假實作,
  不需要真的碰檔案系統。

這正是這個系列反覆強調的邊界問題的另一種樣貌:型別提示介面,把「這個功能怎麼實作」的改動範圍,限制在新寫一個實作類別;型別提示具體類別,則讓改動範圍直接波及到每一個依賴它的呼叫端。

這件事對 AI 協作的具體意義

把視角換到 AI 重構的情境,這個差異會被放大。AI 面對一個要替換底層實作的需求時(例如「這裡的匯出邏輯要改成非同步處理」「這個第三方服務要換供應商」),第一步永遠是先確認:呼叫端依賴的是介面還是具體類別。

如果依賴的是介面,AI 可以有信心地說「我只需要新增一個實作類別,呼叫端不用動」——這個結論的查證範圍跟結論本身是對得上的,因為介面已經把可以變動跟不能變動的邊界劃清楚了。

如果依賴的是具體類別,AI 要做的判斷就複雜得多:它必須先窮舉「所有依賴這個具體類別的呼叫端」,逐一確認換掉實作後,這些呼叫端會不會受影響——而這正是這個系列從 Day 01 開始反覆出現的風險模式:AI 給出的「已確認可以安全替換」,如果查證範圍只涵蓋了它當下看到的那幾個呼叫端,這個結論的可信度就跟它實際窮舉的範圍綁在一起,範圍不夠,結論就不成立。

型別提示介面的價值,不是讓程式碼「看起來比較專業」,而是先把這個窮舉問題,在寫程式碼的當下就解決掉——呼叫端只依賴一組行為,AI 完全不需要去追蹤「這個具體類別還有誰在用」,因為那件事本來就跟它能不能安全替換無關。

這不是「所有地方都該用介面」的萬用規則

需要澄清一件事:這不代表每一處依賴都該無腦提示介面。當一個類別在整個系統裡只有、也永遠只會有一種實作,而且沒有測試替換的需求時,硬加一層介面反而是在製造不必要的間接層,讓程式碼變得更難讀,卻沒有換來任何實際的彈性。 這個判斷準則,跟後面幾天會講到的「過度設計」是同一個問題的兩面——這裡先點出來:型別提示要不要用介面,取決於「這個依賴未來有沒有可能需要替換、有沒有需要在測試裡隔離掉」,不是一條無條件套用的規則。

真正該問的問題是:如果明天這個依賴的實作要換掉,或者要在測試裡把它抽掉,現在的型別提示能不能讓這件事只影響一個新檔案,而不是散落在系統裡的每一個呼叫端?

依賴反轉:一個更正式的說法

軟體工程領域早就有一個更正式的名字在描述這件事——依賴反轉原則(Dependency Inversion Principle,SOLID 的最後一個字母),核心主張是:高層模組不該依賴低層模組的具體實作,兩者都該依賴抽象。這個原則背後的動機,跟這個系列講的架構邊界完全是同一件事:讓改動的影響範圍,不因為底層實作換掉而波及到高層呼叫端。

只是這個系列想強調的重點不在於「要遵守這條 SOLID 原則」這個規範性的說法,而在於它對 AI 協作的實際效益:一個依賴反轉做得徹底的系統,AI 在替換、擴充底層實作時,需要查證的範圍是可預期的;一個沒有做依賴反轉的系統,AI 每次都得先花力氣搞清楚「這個具體類別背後藏著多少個呼叫端」,而這件事沒有任何機制保證它查得全。

今日思考題

回想你手上專案裡某個建構子的型別提示:它提示的是介面還是具體類別?如果是具體類別,這個決定是有意識選的(例如這個依賴永遠只有一種實作),還是純粹因為「剛好手邊有這個類別」的隨手之作?

今日重點回顧

  • 型別提示介面承諾「只依賴一組行為」,型別提示具體類別承諾「跟這個實作綁死」——這是兩種不同的契約,不是語法偏好
  • AI 面對「能不能安全替換底層實作」這個問題時,型別提示介面讓查證範圍是可預期的;型別提示具體類別,讓 AI 必須先窮舉所有呼叫端才能給出可信的答案
  • 不是所有依賴都該無腦提示介面——只有在依賴未來可能替換、或需要在測試裡隔離時,這層間接才有實際價值
  • 依賴反轉原則是這件事更正式的說法,核心動機是同一個:讓改動的影響範圍不因為底層實作換掉而擴散出去

明日預告

第一部還剩兩天。明天要看「依賴方向」——AI 在沒有明確規則約束時,很容易寫出反向依賴(低層模組反過來依賴高層模組),這件事會怎麼發生、又該用什麼架構規則防止。


上一篇
Day 04:Repository 分層——把 AI 的改動範圍收斂到一個資料夾
系列文
AI 寫 Code 之後,我們還需要 Software Architecture 嗎?5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言