用 TypeScript 解釋 SOLID 原則裡的「S」

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):依賴反轉原則

首先說說第一個,單一職責原則,原文的定義是

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 類別,其實有兩個理由會使其改變

  1. 改變計算卡洛里的方式,trackCalories 方法需要動刀
  2. 改變卡洛里超標通知的方式,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 的內容

單一職責原則最大的目的,就是要限制改變所帶來的影響
為了減少這個影響,最好的方式就是增加一段程式碼的內聚力
不相關的東西就拆分出來,在其他地方實踐(使用類別或是介面)
 

參考資料
讓 TypeScript 成為你全端開發的 ACE !
使人瘋狂的 SOLID 原則:單一職責原則 (Single Responsibility Principle)

參考影片

sharkHead 後端工程師,稍微擅長 Laravel、Python 與 Google
對於前端有興趣,無奈沒什麼慧根