用 TypeScript 解釋 SOLID 原則裡的 S
此為 SOLID 原則介紹的系列文章之一,所有文章的連結如下。
- 用 TypeScript 解釋 SOLID 原則裡的 S
- 用 PHP 解釋 SOLID 原則裡的 O
- 用 TypeScript 簡單介紹 SOLID 原則裡面的 L
- 用 PHP 簡單介紹 SOLID 原則裡面的 I
- 用 TypeScript 簡單介紹 SOLID 原則裡面的 D
什麼是 SOLID 原則
SOLID 原則,是物件導向程式設計 (Object-oriented programming,OOP) 中的 5 大基本原則,這些原則的目標,就是希望能以優雅且漂亮的方式,去建構良好的軟體架構,以便後續維護與開發。
當我們在程式中寫好一個又一個的模組,這些模組之間該如何相互關聯,這就是 SOLID 原則想告訴我們的事情。
SOLID 原則指的 5 大原則分別是。
- Single-responsibility principle (SRP):單一職責原則。
- Open–closed principle (OCP):開放封閉原則。
- Liskov substitution principle (LSP):里氏替換原則。
- Interface segregation principle (ISP):介面隔離原則。
- Dependency inversion principle (DIP):依賴反轉原則。
SOLID 原則中的 S
首先說說第一個,單一職責原則,原文的定義是。
A class should have only one reason to change.
一個類別應有且只有一個理由會使其改變。
非常詭異…
從名字上來看,你會覺得意思是類別 (或函式、方法與資料等) 只要能夠簡單地完成一件事情就可以了。但從原文定義來看,好像又不是那麼簡單。
舉一個例子,假設我們有一個追蹤卡洛里的小程式 calorie-tracker.ts
如下。
class CalorieTracker {
public maxCalories: number = 0;
public currentCalories: number = 0;
constructor(maxCalories: number) {
// 設定最大卡洛里
this.maxCalories = maxCalories;
// 設定當前的卡洛里
this.currentCalories = 0;
}
// 紀錄當前卡洛里的變化
public trackCalories(calorieCount: number) {
this.currentCalories += calorieCount;
// 如果當前卡洛里超過最大卡洛里
if (this.currentCalories > this.maxCalories) {
this.logCalorieSurplus();
}
}
// 卡洛里超標通知
public logCalorieSurplus() {
console.log('Max calories exceeded !');
}
}
// 設定最大卡洛里為 2000
const calorieTracker = new CalorieTracker(2000);
// 持續追蹤卡洛里
calorieTracker.trackCalories(500);
calorieTracker.trackCalories(1000);
calorieTracker.trackCalories(700);
因為我們持續更新卡洛里為 500 + 1000 + 700,已超過最大卡洛里所設定的 2000。
因此上述程式碼的執行結果為如下所示。
Max calories exceeded !
這個小程式看起來沒有問題,但凡事總有個 BUT,單一職責原則所提到的,一個類別應有且只有一個理由會使其改變,其實可以用其他方式來理解。
- 一個類別應該只有一個職責。
- 一個類別應該只有一位服務對象 (Consumer)。
這邊的 CalorieTracker
類別,其實有兩個理由會使其改變,也就是服務對象不止一位。
- 改變計算卡洛里的方式,
trackCalories
方法需要動刀。 - 改變卡洛里超標通知的方式,
logCalorieSurplus
方法需要動刀,又因為trackCalories
方法中含有logCalorieSurplus
方法,trackCalories
方法可能也要一起動。
這樣等於整個類別都要大改了,這是我們在設計軟體架構時,最不希望遇到的事情,耦合度太高。此時我們可以將卡洛里超標通知獨立成一個 Log 出來,多寫一個模組 logger.ts
。
export default function logMessage(message: string) {
console.log(message);
}
然後修改 calorie-tracker.ts
的內容。
// 引入 logger
import logMessage from './logger';
class CalorieTracker {
public maxCalories: number = 0;
public currentCalories: number = 0;
constructor(maxCalories: number) {
// 設定最大卡洛里
this.maxCalories = maxCalories;
// 設定當前的卡洛里
this.currentCalories = 0;
}
// 紀錄當前卡洛里的變化
public trackCalories(calorieCount: number) {
this.currentCalories += calorieCount;
// 如果當前卡洛里超過最大卡洛里
if (this.currentCalories > this.maxCalories) {
// 卡洛里超標通知
logMessage('Max calories exceeded');
}
}
}
// 以下省略
這樣當我們想修改卡洛里超標的方式,例如改用寄信的方式通知,我們就完全不需要修改 calorie-tracker.ts
的內容。
單一職責原則最大的目的,就是要限制改變所帶來的影響,為了減少這個影響,最好的方式就是增加一段程式碼的內聚力。不相關的東西就拆分出來,在其他地方實踐 (使用類別或是介面) 。