用 TypeScript 解釋 SOLID 原則裡的 S

程式技術
sharkHead

此為 SOLID 原則介紹的系列文章之一,所有文章的連結如下。

什麼是 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 類別,其實有兩個理由會使其改變,也就是服務對象不止一位。

  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 的內容。

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

參考資料

sharkHead
written by
sharkHead

後端打工仔,在下班後喜歡研究各種不同的技術。稍微擅長 PHP,並偶爾涉獵前端開發。個性就像動態語言般隨興,但渴望做事能像囉嗦的靜態語言那樣嚴謹。

0 則留言