SOLID 原則:開放封閉原則 (OCP)
8 分鐘
SOLID 原則:開放封閉原則 (OCP)
開放封閉原則 (Open/Closed Principle,OCP) 是 SOLID 的第二條:
軟體實體應該對擴展開放,對修改封閉。
對擴展開放:可以新增行為。 對修改封閉:不需要修改現有程式碼。
實際情境下,這代表欲對系統加入新功能,應該是「新增程式碼」,而不是「修改現有程式碼」。
什麼是開放封閉原則
OCP 的核心目標是:當需求變化時,不需要動到現有已測試、已驗證的程式碼。
這樣有兩個好處:
- 減少引入錯誤的風險 (沒有動到的地方,就沒有機會改壞它)
- 新的功能可以獨立開發和測試
經典範例:形狀面積計算
一個違反 OCP 的常見寫法:
TypeScript
type Shape = { type: 'circle'; radius: number }
| { type: 'rectangle'; width: number; height: number }
| { type: 'triangle'; base: number; height: number };
function calculateArea(shape: Shape): number {
if (shape.type === 'circle') {
return Math.PI * shape.radius 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
} else if (shape.type === 'triangle') {
return (shape.base * shape.height) / 2;
}
throw new Error('Unknown shape');
}每次新增一種形狀,就必須修改 calculateArea。這個函式對擴展是「封閉」的,對修改卻是「開放」的,完全違反 OCP。
問題會越來越明顯:隨著形狀種類增加,if-else 不斷變長,而且每次改動的影響範圍也越來越大。
用多型解決
把面積計算的職責移進形狀本身:
TypeScript
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius 2;
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number {
return this.width * this.height;
}
}
class Triangle implements Shape {
constructor(private base: number, private height: number) {}
area(): number {
return (this.base * this.height) / 2;
}
}
// 這個函式不需要再改了
function calculateArea(shape: Shape): number {
return shape.area();
}現在要新增一種形狀,只需新增一個實作 Shape 介面的類別就好,calculateArea 完全不需要碰。
這就是對擴展開放、對修改封閉。
實務應用:支付方式
這個模式在實務中非常常見:
TypeScript
interface PaymentMethod {
pay(amount: number): void;
}
class CreditCard implements PaymentMethod {
pay(amount: number): void {
console.log(`信用卡付款 $${amount}`);
}
}
class LinePay implements PaymentMethod {
pay(amount: number): void {
console.log(`Line Pay 付款 $${amount}`);
}
}
class ApplePay implements PaymentMethod {
pay(amount: number): void {
console.log(`Apple Pay 付款 $${amount}`);
}
}
class Checkout {
constructor(private paymentMethod: PaymentMethod) {}
complete(amount: number): void {
this.paymentMethod.pay(amount);
}
}
// 新增支付方式,只需新增類別
// Checkout 和其他程式碼完全不需要修改
const checkout = new Checkout(new ApplePay());
checkout.complete(1200);日後新增第四、第五種支付方式,Checkout 完全不需要動。
總結
OCP 的實踐方式是:
- 介面 / 抽象類別:定義封閉的行為合約,具體實作程式碼對它開放
- 策略模式:將可替換的演算法或行為封裝成獨立的類別
- 多型:透過繼承或實作介面,讓新的行為透過新增類別而不是修改現有程式碼
實務上的判斷標準:如果新增功能需要修改現有且已測試的程式碼,就應該思考是否可以改成「新增程式碼」就讓新功能運作。