物件導向設計(OOD Object-Oriented Design)
物件導向設計(OOD, Object-Oriented Design)
TL;DROOD 把 OOA 的「世界觀」轉成「設計藍圖」:具體類別關係、介面、設計模式、時序圖。做完 OOD 才開始寫 code。OOD 階段套用 SOLID 跟 設計模式(GoF 23 種)。
OOD 是做什麼
OOA(分析:有什麼)→ ==OOD(設計:怎麼組合)== → OOP(實作:怎麼寫)
↑
本篇講這個
OOA 給你 entity 模型,OOD 把它轉成 可實作的設計:
- 介面定義(API contract)
- 類別繼承 / 組合 結構
- 模組依賴 關係
- 設計模式 套用
- 時序圖 描述執行流程
結果是一份 design document + UML,工程師看了能直接寫 code。
設計步驟
1. 描述類別 / 介面間的依賴關係
(OOA 結果) (OOD 加工後)
┌─────────────┐
User ─→ Cart ───→ │ IUserService │ ←── UserController
└─────────────┘
↑
┌─────────────┐
│ UserService │ ── depends on ─→ UserRepository
└─────────────┘
關鍵:OOA 的「User 跟 Cart 有關係」抽象,OOD 變成「User 透過 IUserService 操作 Cart」具體。
2. 套用設計模式
知名 GoF 23 種(部分常用):
| Pattern | 用途 | 範例 |
|---|---|---|
| Singleton | 全域只有一個 instance | DB 連線、config |
| Factory | 創建物件 by type | 多種 Notification 子類 |
| Strategy | 換實作不換介面 | 多種付款方式 |
| Observer | pub-sub 通知 | 事件系統 |
| Decorator | 動態加功能 | middleware |
| Adapter | 橋接不同 API | 整合舊系統 |
// Strategy pattern 範例
interface PaymentStrategy {
process(amount: number): Promise<PaymentResult>;
}
class StripeStrategy implements PaymentStrategy { /* ... */ }
class PayPalStrategy implements PaymentStrategy { /* ... */ }
class LinePayStrategy implements PaymentStrategy { /* ... */ }
class Checkout {
constructor(private strategy: PaymentStrategy) {}
pay(amount: number) {
return this.strategy.process(amount); // ⭐ 不知道實際是哪一家
}
}
3. 定義系統向方法(method)
每個類別 / 介面該暴露什麼 API:
interface IUserService {
register(input: RegisterInput): Promise<User>;
login(email: string, password: string): Promise<Session>;
getCurrentUser(): Promise<User | null>;
updateProfile(updates: Partial<User>): Promise<User>;
}
介面是 contract,把「要做什麼」(this layer)跟「怎麼做」(impl layer)分離。
4. 用時序圖描述執行流程
User Browser Server DB
│ │ │ │
│── 點 Login ──────→ │ │ │
│ │── POST /login ─→ │ │
│ │ │── SELECT ───→ │
│ │ │←── user ────│
│ │ │ │
│ │ │── set cookie│
│ │←── 200 OK ──────│ │
│←── redirect ──────│ │ │
時序圖讓 frontend / backend 對齊,知道每個 API 的時序、誰先誰後。
OOD 階段必做
1. 套用 SOLID
OOD 的核心方針:
- SRP — 一個類別只做一件事
- OCP — 開放擴充、封閉修改
- LSP — 子類能取代父類
- ISP — 介面細分,不要塞太多
- DIP — 依賴抽象不依賴實作
2. 識別 Boundaries
哪些東西該分模組?哪些該放同個 module?✅ 高內聚:相關功能放一起(User 操作 → UserService 內)
✅ 低耦合:模組間透過介面互動(UserService 不直接動 DB)
3. 設計 錯誤處理 流程
OOA 不太談錯誤,但 OOD 要:
- 哪些方法會 throw?
- 用 Result type 還是 exception?
- 失敗時誰負責 retry / log / 通知?
範例:電商付款流程的 OOD
OOA 給的:User, Order, Payment, Merchant, Inventory
OOD 加工:
// 介面層(抽象)
interface IPaymentGateway {
charge(amount: number, token: string): Promise<ChargeResult>;
}
interface IInventoryService {
reserve(productId: string, quantity: number): Promise<Reservation>;
release(reservationId: string): Promise<void>;
commit(reservationId: string): Promise<void>;
}
// 服務層(實作)
class CheckoutService {
constructor(
private payment: IPaymentGateway,
private inventory: IInventoryService,
private orderRepo: OrderRepository,
) {}
async place(input: PlaceOrderInput): Promise<Order> {
// 1. 預扣庫存
const reservations = await Promise.all(
input.items.map((it) => this.inventory.reserve(it.productId, it.quantity))
);
try {
// 2. 收款
const charge = await this.payment.charge(input.total, input.paymentToken);
if (!charge.success) throw new PaymentFailed(charge.errorCode);
// 3. 確認庫存
await Promise.all(reservations.map((r) => this.inventory.commit(r.id)));
// 4. 建立訂單
const order = await this.orderRepo.create({ ...input, paymentId: charge.id });
return order;
} catch (err) {
// 失敗 → 釋放預扣庫存
await Promise.all(reservations.map((r) => this.inventory.release(r.id)));
throw err;
}
}
}
這就是 OOD 的具體產出:介面、依賴、流程、錯誤處理都明確。
現代視角:Clean Architecture
2026 年 OOD 常跟 Clean Architecture 結合:
┌─────────────────────────────────────┐
│ Frameworks(Express / Next.js / DB) │ 最外層,最易變
├─────────────────────────────────────┤
│ Interface Adapters(Controllers) │
├─────────────────────────────────────┤
│ Use Cases(CheckoutService) │ ← 我們在 OOD 設計這層
├─────────────────────────────────────┤
│ Entities(User / Order / Product) │ ← OOA 的核心
└─────────────────────────────────────┘
依賴方向 →(內層不能依賴外層)
核心原則:依賴方向 永遠由外向內。Entity 不知道有 framework,Use Case 不知道有 DB。
OOD 給前端工程師的啟發
前端也該做 OOD,只是規模小:
// component 層級的 OOD
interface IPostListProps {
posts: Post[];
onSelect: (post: Post) => void;
}
interface IPostListBehavior {
filter(query: string): void;
sort(by: 'date' | 'title'): void;
}
// 組件
function PostList({ posts, onSelect }: IPostListProps) { /* ... */ }
// hook(behavior)
function usePostListBehavior(initial: Post[]): IPostListBehavior & { posts: Post[] } { /* ... */ }
介面分離 props vs behavior 讓 component 跟資料邏輯解耦,容易測試。