首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau
百百 BLOG
首頁
學習紀錄
遊戲心得影視Life書單案件檔案
Side Projects委託作品與二創互動實驗場
Kurau

Kurau Blog

「隨心而寫,真真假假,都是我」

一個記錄生活、輸出興趣的個人空間。
遊戲、影視、閱讀、學習……每一段體驗都值得留下文字。

頁面導覽

  • 學習紀錄
  • 遊戲心得
  • 影視Life
  • 書單
  • 委託作品與二創
  • Kurau
  • 合作邀請

找到我

歡迎來 Discord 找我聊天!

“曾經發生的事不可能忘記,只是暫時想不起來而已。”-《神隱少女》

© 2026 Kurau All rights reserved

軟體工程

物件導向程式設計基本原則 - SOLID

By Kurau·2023-02-07·Updated 2026-05-09·9 分鐘閱讀

SOLID — OOP 五大原則

TL;DR
SOLID 是 OOP 設計的 5 條心法,讓程式碼好維護、易擴充。
  • SRP: 一個類別只做一件事
  • OCP: 開放擴充,封閉修改
  • LSP: 子類能無痛取代父類
  • ISP: 介面細分,別塞太多
  • DIP: 依賴抽象,不依賴實作
範例靈感
借用 Skyyen999 的設計模式書 的「阿文跟車子」比喻,加上 TypeScript 程式碼範例。

為什麼要 SOLID

軟體開發過程中,程式設計師最常抱怨的話:

「什麼,又要改,是要改幾次啊!?」

不論事前設計多周到,需求變更、需求理解錯誤、bug 修改總會逼你不斷調整 code。SOLID 是前人累積的經驗心法,讓你下次改 code 時,工作順利一點、痛苦少一點。


S: Single Responsibility Principle(單一職責)

一個類別只負責一件事。

反例:阿文的超級汽車

阿文 18 歲生日,爸爸送一台 又能飛、又能上路、又能潛水 的車。聽起來很酷?

問題:

  • 阿文要 機師駕照 + 汽車駕照 + 潛艇證照 才能開
  • 故障時要修飛機 + 修汽車 + 修潛艇 三種技師 同時來

這就是把太多職責塞給一個類別 — 使用、維護都痛苦。

// ❌ 違反 SRP
class SuperCar {
  drive() { /* 路上跑 */ }
  fly() { /* 天空飛 */ }
  dive() { /* 水下游 */ }
  repairEngine() { /* 修引擎 */ }
  refuel() { /* 加油 */ }
  charge() { /* 充電 */ }
  getCustomerSupport() { /* 客服 */ }
  // ...
}
typescript

正例:職責拆開

// ✅ 一個類別管一件事
class Car {
  drive() { /* 路上跑 */ }
}
class Plane {
  fly() { /* 天空飛 */ }
}
class Submarine {
  dive() { /* 水下游 */ }
}

class CarMaintenance {
  repair(car: Car) { /* ... */ }
}
typescript
不要過度細分(over-design)
SRP ≠ 一個類別只能有一個方法。 一台車的「行駛」是 前進 / 後退 / 左轉 / 右轉 / 剎車 組合而成,這些都該在 Car 內。 關鍵是「對使用者來說是一致的責任」,不是看方法數量。

O: Open/Closed Principle(開放/封閉原則)

對擴充開放、對修改封閉。這是 SOLID 最重要的一條。

比喻:換輪胎不該動引擎

阿文要 把大燈改亮一點,技師只換燈泡 → 不能也不需要動引擎。 要 上山賞雪,直接 綁雪鏈 → 不必整個輪胎換掉。

反例:每次新增功能都改舊 code

// ❌ 加新付款方式 = 改舊 if-else
class Checkout {
  pay(method: string, amount: number) {
    if (method === 'credit_card') {
      // Stripe 邏輯
    } else if (method === 'paypal') {
      // PayPal 邏輯
    } else if (method === 'linepay') {
      // 新增就要改這裡 → 容易破壞其他分支
    }
  }
}
typescript

正例:新增功能 = 新加一個類別

// ✅ 抽象介面 + 多個實作
interface PaymentMethod {
  process(amount: number): Promise<Result>;
}

class StripePayment implements PaymentMethod { /* ... */ }
class PayPalPayment implements PaymentMethod { /* ... */ }
class LinePayPayment implements PaymentMethod { /* ... */ }   // ⭐ 新增不動舊 code

class Checkout {
  constructor(private method: PaymentMethod) {}
  pay(amount: number) { return this.method.process(amount); }
}
typescript

新增功能只是 add new file,不用改 Checkout class。零風險動到既有功能。


L: Liskov Substitution Principle(Liskov 替換原則)

子類別應該能無痛取代父類別,使用者完全察覺不到差異。

反例:阿文的樂高車

阿文要開車去外婆家,從車庫挑了樂高車。結果發現是模型,不能跑 — 違反父類 Car 的「能上路」假設。

// ❌ 違反 LSP
class Car {
  drive() { /* 路上跑 */ }
}
class LegoCar extends Car {
  drive() {
    throw new Error('我是模型啦,不能跑!');   // ⭐ 子類不能滿足父類契約
  }
}

// 使用者拿到 Car 物件,理應能呼叫 drive()
function goHome(car: Car) {
  car.drive();   // 拿到 LegoCar 就炸
}
typescript

正例:不該繼承就不繼承

// ✅ LegoCar 不是 Car,應該分開
class Car {
  drive() { /* ... */ }
}
class LegoModel {   // ⭐ 純粹的展示模型,不繼承 Car
  display() { /* ... */ }
}
typescript
LSP 判斷準則
「if it walks like a duck, swims like a duck, quacks like a duck — then it's a duck」 子類應該滿足父類「所有可被觀察的行為契約」。 不能滿足 → 不該用繼承,改用 組合(composition)。

I: Interface Segregation Principle(介面隔離)

介面要細,不要塞太多無關功能。使用者不該被迫依賴他不用的方法。

反例:車子介面太胖

阿文家車庫有 一般車 + 樂高車。Car 介面定義了「路上跑」,但樂高車根本不能跑 → 違反 LSP。

根本問題:Car 介面把太多東西混在一起。

正例:把介面拆細

// ❌ 一個大介面
interface Car {
  display(): void;          // 展示用
  drive(): void;            // 行駛
  refuel(): void;           // 加油
  changeOil(): void;        // 換機油
}

// ✅ 拆成多個小介面
interface Displayable {
  display(): void;
}
interface Drivable {
  drive(): void;
}
interface Maintainable {
  refuel(): void;
  changeOil(): void;
}

// 真車實作多個
class Sedan implements Displayable, Drivable, Maintainable { /* ... */ }

// 樂高車只實作展示
class LegoCar implements Displayable { /* ... */ }   // ⭐ 不用假裝能跑
typescript

每個 client 只依賴自己需要的介面,bundle 也更乾淨。


D: Dependency Inversion Principle(依賴反轉)

高階模組不依賴低階模組,兩者都依賴抽象。抽象不依賴細節,細節依賴抽象。

比喻:邀請函上不要寫死車型

❌ 邀請函:「歡迎來欣賞 Ferrari Fx2020 超跑」
   → 當天爸爸開走了 → 你超糗

✅ 邀請函:「歡迎來欣賞超級跑車」
   → 當天即使換車也 OK,反正都是「超跑」這個抽象

反例:直接依賴 concrete class

// ❌ Checkout 直接依賴 Stripe
import { Stripe } from 'stripe';

class Checkout {
  private stripe = new Stripe(process.env.STRIPE_KEY!);

  pay(amount: number) {
    return this.stripe.charges.create({ amount });
  }
}
typescript

問題:

  • 想換 PayPal? 整個 Checkout 都要改
  • 寫測試? 還要 mock 整個 Stripe
  • 多個付款方式? if-else 大爆炸

正例:依賴介面(抽象)

// ✅ 高階模組(Checkout)依賴抽象
interface PaymentGateway {
  charge(amount: number): Promise<Result>;
}

class Checkout {
  constructor(private payment: PaymentGateway) {}   // ⭐ 注入抽象

  pay(amount: number) {
    return this.payment.charge(amount);
  }
}

// 細節層(實作)
class StripeGateway implements PaymentGateway {
  charge(amount: number) { /* Stripe API */ }
}
class PayPalGateway implements PaymentGateway {
  charge(amount: number) { /* PayPal API */ }
}

// 使用時注入
const checkout = new Checkout(new StripeGateway());
typescript

依賴方向反轉了:

  • 之前 Checkout → Stripe(高階依賴低階)
  • 之後 Checkout → PaymentGateway ← Stripe(都依賴抽象)

SOLID 在 React 的應用

很多人覺得「SOLID 是後端 / Java 的事」,其實 React 也適用:

SRP — 元件職責單一

// ❌ 一個 component 做太多
function UserDashboard() {
  // ... 200 行: 抓資料 + 表單 + 圖表 + 通知
}

// ✅ 拆成多個小 component
function UserDashboard() {
  return (
    <>
      <UserProfile />
      <UserActivityChart />
      <UserNotifications />
    </>
  );
}
tsx

OCP — 用 props 擴充行為

// ✅ 擴充新樣式不用改 Button
<Button variant="primary">...</Button>
<Button variant="danger">...</Button>
<Button variant="ghost">...</Button>   // 新增 variant 只加 CSS
tsx

DIP — Hook 抽象資料層

// ❌ component 直接呼叫 fetch
function PostList() {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    fetch('/api/posts').then((r) => r.json()).then(setPosts);
  }, []);
}

// ✅ component 依賴 hook 抽象
function PostList() {
  const { data: posts } = usePosts();   // ⭐ 不知道資料從哪來
}

// hook 實作可換 fetch / TanStack Query / GraphQL / mock
function usePosts() {
  return useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
}
tsx

這就是 DIP — Component 依賴 hook 介面,不依賴 fetch 細節。


SOLID 的爭議與限制

不要 cargo cult
SOLID 是指導原則,不是律法。死板套用會 過度抽象,讓簡單問題變複雜。

例如:你寫一個 hello world script,還搞 5 層介面 + DI container,完全沒必要。

判斷準則:這段 code 預期會變嗎? 會 → 用 SOLID 防止以後痛。不會 → 直接寫,以後要改再 refactor。


總結

原則一句話關鍵字
SRP一個類別一個職責拆
OCP擴充開放、修改封閉插件式
LSP子類能完美取代父類契約
ISP介面要小,不要強迫依賴細分
DIP依賴抽象不依賴實作倒過來
學會 SOLID 之後
接下來進階:設計模式(GoF) + Clean Architecture / DDD + 領域語言(Ubiquitous Language)。SOLID 是這些東西的內功心法。

目錄

    ◆ 相關文章

    • 物件導向設計(OOD Object-Oriented Design)

      2026-05-09
    • 物件導向分析(OOA Object-Oriented Analysis)

      2026-05-09
    • Memory Leak (記憶體管理)

      2026-05-09
    • 四種渲染模式

      2026-05-09
    ← 上一篇物件導向分析(OOA Object-Oriented Analysis)下一篇 →箭頭函數 和 一般函數的 差別

    ◆ 關於作者

    Kurau

    個人寫作 / 創作的 SoT,記錄遊戲、影視、學習與生活。

    更多 Kurau 的文章