Skip to main content

Command Palette

Search for a command to run...

Lesson 14: 物件導向的靈魂-介面與抽象類別

Updated
3 min read

很多剛轉職或自學的工程師,最討厭看到的兩個關鍵字就是 interfaceabstract

心中一定有過這個疑問:「為什麼要寫一個『空的』函式放在那邊?直接把程式碼寫在 Class 裡面不好嗎?」

這堂課我們要來解開這個誤會。這不是為了增加程式碼行數,而是為了 「多型 (Polymorphism)」 與 「抽換 (Swap)」 的彈性。

繼承的盲點:為什麼不能只用 extends

假設我們在做一個「物流系統」,有「黑貓 (BlackCat)」和「新竹物流 (HCT)」。 直覺寫法通常是這樣:

class BlackCat {
    public function ship($parcel) {
        return "黑貓已運送包裹: " . $parcel;
    }
}

class HCT {
    public function ship($parcel) {
        return "新竹物流已運送包裹: " . $parcel;
    }
}

Service中可能就長這樣

// 如果今天是黑貓
$logistic = new BlackCat();
$logistic->ship('iPhone 15');

// 如果明天老闆說要換成新竹物流
// 你得把所有用到 BlackCat 的地方,全部改成 HCT
// 萬一 HCT 的方法不叫 ship() 叫做 send() 怎麼辦?這就是災難。

這時候,我們需要「規範」。

介面 :定義「行為的合約」

Interface 像是一個 USB 插孔的規格。 電腦不在乎插進來的是滑鼠、鍵盤還是隨身碟,它只在乎「你插得進去」且「你可以運作」。

  • Interface 只管 「你能做什麼」,不管你 「你是誰」。

  • Interface是一種型別契約,他只包含方法的簽名,沒有實現邏輯。

  • 一個類別可以實現多個Interface,但必須實現所有Interface中定義的方法

  • Interface會強制實現某些方式,確保類別具有特定的行為,使用Interface可以更加清晰的定義類別的職責

注:方法的簽名:指方法的 名稱、參數列表,以及(若有)回傳型別,不包含具體實作邏輯。

實作 Interface

我們定義一個物流介面,規定大家都必須有一個 ship 方法:

interface LogisticInterface {
    // 介面裡的方法不能有內容,只能定義名稱跟參數
    public function ship($parcel);
}

接著讓兩家物流公司都 遵守implements

class BlackCat implements LogisticInterface {
    public function ship($parcel) {
        // 實作黑貓的邏輯
        return "黑貓運送: " . $parcel;
    }
}

class HCT implements LogisticInterface {
    public function ship($parcel) {
        // 實作新竹物流的邏輯
        return "新竹物流運送: " . $parcel;
    }
}
function processOrder(LogisticInterface $logistic, $parcel) {
    // 我不在乎 $logistic 是黑貓還是新竹物流
    // 我只知道它一定有 ship() 方法,因為它簽了合約 (Interface)
    $logistic->ship($parcel);
}

這就是 多型 (Polymorphism) 的威力。

抽象類別 Abstract Class:提取「共同的邏輯」

Abstract Class 又是什麼?

如果 Interface 是為了規範「外部行為」,那 Abstract Class 就是為了 「減少內部重複的程式碼」

  • 抽象類別無法被實例化,只能用於繼承

  • 抽象類別可以包含一般function跟抽象function

  • 抽象方法不包含具體邏輯,只包含方法的簽名,如果抽象類別宣告了抽象方法,則子類別必須實作出該方法(或一樣使用方法簽名)

假設黑貓和新竹物流,雖然運送方式不同,但 「計算運費」 的公式是一樣的(例如都是 重量 * 10)。我們不想在兩個 Class 裡寫兩遍一樣的公式。

實作 Abstract Class

Abstract Class 就像是一個 「未完成的藍圖」它不能被直接 new 出來,只能被繼承。

abstract class BaseLogistic implements LogisticInterface {
    // 1. 具體的方法:大家共用的邏輯寫在這裡
    public function calculateFee($weight) {
        return $weight * 10;
    }

    // 2. 抽象的方法:強迫子類別一定要去實作 (跟 Interface 很像)
    // 因為每家送貨方式不同,這裡先空著留給子類別去寫
    abstract public function ship($parcel);
}

現在,我們的物流公司可以這樣寫:

class BlackCat extends BaseLogistic {
    public function ship($parcel) {
        return "黑貓運送";
    }
}

BlackCat 現在自動擁有了 calculateFee 的能力,同時被迫實作了 ship 的行為。

特性Interface (介面)Abstract Class (抽象類別)
核心概念行為規範 (Contract)部分實作與共用邏輯
比喻USB 插槽、合約車子的底盤、未完成的藍圖
實作內容完全不能寫程式碼邏輯 (純定義)可以寫共用的程式碼,也可以留空 (abstract method)
繼承數量可以多重實作 (implements A, B, C)只能單一繼承 (extends A)
使用時機當你希望不同的類別都有相同的方法名稱時 (如:Log, Cache, Mail)當你有多個類別長得很像,有很多重複程式碼要共用時

實戰中的選擇

在「系統架構設計」與「依賴反轉(DIP)」層面,Interface 的重要性通常高於 Abstract Class。

為什麼?因為我們通常更在乎「這個物件能不能被抽換」,而不是「繼承層級」。

  • 優先考慮 Interface:為了未來的擴充性 (例如 Lesson 15 要講的依賴注入)。

  • 輔助使用 Abstract:當你發現多個實作類別有大量重複程式碼時,再用 Abstract Class 來重構 (Refactor)。

為什麼不要濫用 Abstract Class

Abstract Class 看起來很美好:

  • 可以寫共用邏輯

  • 可以少寫重複程式碼

  • 子類別看起來「很整齊」

但在真實專案中,濫用 Abstract Class 往往是系統開始變硬、變脆的起點

下面是三個最常見、也最容易被忽略的風險。

抽象類別會「過早凍結設計」

當你建立一個 Abstract Class 時,你其實是在對未來下這樣的假設:

「這些子類別永遠都會長得差不多。」

這個假設在一開始幾乎一定是錯的。

問題場景

abstract class BaseLogistic {
    public function calculateFee($weight) {
        return $weight * 10;
    }

    abstract public function ship($parcel);
}

一開始很合理,但半年後需求來了:

  • DHL:計費方式不同(體積重)

  • Uber Eats:即時動態加價

  • 自取:沒有運費

你會開始在 BaseLogistic 裡面看到:

if ($this instanceof DHL) { ... }
if ($this instanceof UberEats) { ... }

這是一個明確的設計退化訊號。

Abstract Class 讓我們在還不了解變化之前,就把邏輯定死了。

單一繼承會快速限制擴充性(PHP 的現實)

在 PHP 中:

  • Class 只能繼承一個 Abstract Class

  • 但可以實作 多個 Interface

這代表什麼?

如果用了 Abstract Class

class BlackCat extends BaseLogistic

就失去了:

class BlackCat extends BaseLogistic, Trackable, Refundable // 不能

只能選擇:

  • 再塞更多功能進 BaseLogistic

  • 或開始用 Trait 補洞(通常會更亂)

相對地,Interface 不會有這個問題

class BlackCat implements LogisticInterface, TrackableInterface, RefundableInterface

IInterface 通常搭配「組合(Composition)」使用,而 Abstract Class 屬於「繼承(Inheritance)」模型。

而現代設計更偏好前者。

Abstract 容易變成「上帝類別」(God Class)

當團隊開始「為了共用而共用」:

「反正大家都會用到,放在 Base 裡面就好。」

Abstract Class 很容易變成這樣:

abstract class BaseLogistic {
    public function calculateFee() {}
    public function logShipment() {}
    public function notifyUser() {}
    public function retryPolicy() {}
    public function audit() {}
}

結果是:

  • 子類別被迫「繼承一堆用不到的東西」

  • 修改 Base Class 影響所有子類別

  • 測試成本指數型上升

這直接違反了:

  • SRP(單一職責原則)

  • OCP(開放封閉原則)

實戰建議

設計初期:

  • 先用 Interface 定義「能做什麼」

  • 用組合(Service / Strategy)解決差異

重構階段:

  • 當看到「多個實作類別真的有穩定重複邏輯」

  • 再抽出一個「小而薄」的 Abstract Class

總結

Interface 是為了「未來的變化」, Abstract Class 是為了「已經確定不會變的部分」。

從Rookie到Junior,一個後端成長的30堂課

Part 14 of 31

“從 Rookie 到 Junior:一個後端成長的 30 堂課” 是一套專為後端新手所設計的成長型技術系列文章。內容以實務為導向,逐步拆解後端工程的核心能力,包括程式語言基礎、架構思維、框架運作原理、業務邏輯設計、資料庫操作、以及常見的開發模式。 本系列的目標是協助讀者從零散的學習堆疊,建立成體系的後端知識框架。讀者能夠理解各語言背後不變的工程思維與設計原則。這套內容旨在讓學習者從「會寫程式」進入「能理解系統設計」的階段,逐步具備勝任 Junior Backend Engineer 的能力。

Up next

Module 3 提升程式碼品質與架構

序 在前兩個模組,我們學會了如何設計資料庫與 API,讓系統成功地「動起來」。但隨著需求不斷變更與功能堆疊,你的程式碼是否正逐漸變成一團難以維護的「Spaghetti Code」?只要改一個小功能,就會讓整個系統壞光光? 這就是「能動的程式碼」與「好品質的程式碼」之間的巨大鴻溝。 在 Module 3 中,我們將暫時放下新技術的追求,回頭檢視程式碼的「架構體質」。我們將從現代框架的核心——依賴注入 (Dependency Injection) 與 IoC 開始,理解如何透過解耦(Decoupli...

More from this blog

Lesson 26 : 系統韌性的守護者-限流、熔斷與背壓的設計模式

當這幾個名詞出現後,代表我們進到了一個高併發/大流量的系統了。在這個章節中,我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。 限流(Rate Limiting) 相信大家對這個名詞並不陌生,限流其實就是字面上的含意,限制流量。 限流的目的是保護「接收方」,確保系統不會因為瞬間的高併發請求而癱瘓。 限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全

Mar 26, 20262 min read

Lesson 25: 淺談 單體架構、微服務架構與單/多租戶架構

過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目,接下來我們來看看「如何服務不同客戶」,這些架構反映了軟體開發在擴充性與複雜度之間的權衡。 軟體架構深度解析:從系統拆分到商業規模化 在軟體工程的演進中,架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構:系統如何運行(單體 vs. 分散式) 以及 如何服務客戶(單租戶 vs. 多租戶)。

Mar 26, 20262 min read

Lesson 24: 資料庫擴展術-讀寫分離、複寫機制與快取一致性挑戰

為什麼要讀寫分離? 大多數的 Web 應用都是 「讀多寫少」(例如:看文的人多,發文的人少,Heavy Read System)。當所有的請求都塞給同一台資料庫時,磁碟 I/O 和連線數會成為瓶頸。 Master (主庫): 負責寫入 (Insert/Update/Delete),確保數據一致性。 Slave (從庫): 負責讀取 (Select),可以有多個從庫來分擔讀取壓力。 為什麼讀

Mar 25, 20262 min read

面試經驗談 2025-2026

從2025年3月開始,我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試,其中有不乏 尊重面試者、展現高度專業的企業(公司),當然也有遇到幾場面試鬼故事,這篇文章主要分享我對於軟體工程師面試的方向分享,以及部分鬼故事,以此警惕自己不要成為這樣的面試官。 AI的洪流,改變了SWE的生態 LLM的發展確確實實的影響到了軟體工程師的生態。 過去受限於算力與資料規模,深度學

Mar 23, 20262 min read

Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)

佇列 佇列的實作工具非常多,舉凡AWS SQS、RabbitMQ、Kafka…等。 佇列的特性,其實是一個非常強大的系統緩衝區,應用層面非常廣。 什麼是佇列? 佇列可以想像成,在既有流程中外,有另一個”水管”,來連接原有的資料流(或邏輯過程),其中 呼叫方將資料 推(Push)到水管中,接受方(監聽) 從水管中將資料拉(Pull)出處理 為什麼佇列是「強大的緩衝區」? 在同步處理中,系統像是一

Mar 23, 20262 min read

Bennett's Tech Blog | 後端架構、系統設計

32 posts

來自台灣的軟體工程師,相信軟體可以改變世界