25. Design Principles - SOLID


Posted by WayneCheng on 2021-02-02

關於 Design Principle 本篇將討論以下幾個問題

1. 關於 Design Principles

2. 關於 SOLID

3. Single-responsibility principle

4. Open–closed principle

5. Liskov substitution principle

6. Interface segregation principle

7. Dependency inversion principle


1. 關於 Design Principles

即便知道封裝、繼承、多型三大特性,也無法一下子就將程式寫得符合物件導向設計(Object-Oriented Programming, OOP),而設計原則可以協助我們以明確的定義作為標準,評斷程式中那些部分可以優化,以達到提高可讀性、維護性與擴充性的目的。


2. 關於 SOLID

SOLID 為設計原則中最廣為人知的五個原則,由大神 Robert C. Martin 在 2000 年的 paper 「Design Principles and Design Patterns」中介紹的(並非全都由 Robert C. Martin 提出),中文版可以參考無瑕的程式碼 敏捷完整篇

SOLID 是由五個設計原則的字首(藏頭詩?)拼成,分別是

名稱(縮寫) 中文
Single-Responsibility Principle(SRP) 單一職責原則
Open-Closed Principle(OCP) 開閉原則
Liskov Substitution Principle(LSP) 里氏替換原則
Interface Segregation Principles(ISP) 介面隔離原則
Dependency Inversion Principle(DIP) 依賴反轉原則

3. Single-responsibility principle

單一職責原則,一個 class 或模組應該只有一個職責

不同的層面來看同一個 class 或模組的職責是否足夠單一會有不同的結果(e.g. 從整個購物流程的層面來看,包含加入購物車 & 付款很合理,但從結帳流程來看,加入購物車卻顯得格格不入),而我們還是可以從一些比較客觀的方式來判斷

  • class 或模組本身很龐大(e.g. 屬性或方法數量很多)
  • class 或模組本身難以命名
  • 依賴過多

4. Open–closed principle

開閉原則,對擴展開放,對修改封閉

要達到 OCP 最簡單的例子就是 abstract class 讓子類別 override,但其它時候呢?對擴展開發聽起來很合理,但對修改封閉卻很抽象,什麼樣的程度算是修改?是否有異動到物件本身就算是違反 OCP 呢?

實務上很難完全不會異動到物件本身,所以個人比較偏向這個看法

  • 對擴展開放是為了因應變化
  • 對修改封閉是為了保持原有程式的穩定

在這個前提之下,儘管我們在物件中新增了屬性、方法,但沒有修改到原有的程式,因應變化並保持原有程式穩定性,故沒有違反 OCP


5. Liskov substitution principle

里氏替換原則,子類別繼承父類別,可以修改實作,但不能改變父類別原有的約定(e.g. 回傳型別、參數、Exception的處理等)

有個簡單的驗證方式,將父類別的單元測試套用於子類別上,能通過單元測試通常都符合 LSP。違反 LSP 時未必都是子類別不遵守合約,也可能是抽象類別或介面設計上的錯誤,我們來看看實際的例子

  • (O) 父類別定義方法GetStartDate(),子類別實作並回傳開始日期
  • (X) 父類別定義方法GetStartDate(),子類別實作改為回傳結束日期
  • (O) 父類別哺乳類,定義方法Walk(),子類別人類實作走路
  • (X) 父類別哺乳類,定義方法Walk(),子類別鯨魚無法實作走路

6. Interface segregation principle

介面隔離原則,呼叫端不應該被強迫依賴它不需要的介面

ISP 跟 SRP 有點像,但角度有些不同,ISP 是由呼叫端的角度來判斷,SRP 則是關注在自己本身,而這邊說到的介面可以比較廣泛的解釋,像是

  • interface 中有部分方法沒被呼叫端使用到,就應該拆分出來
  • 呼叫端只需要一個UserName,但呼叫的方法 / API 卻回傳User所有欄位回來

7. Dependency inversion principle

依賴反轉原則

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

  • 高階模組不應依賴於低階模組,兩個模組都應依賴抽象
  • 抽像不應依賴細節,細節依賴於抽象

單看命名滿難理解 DIP 在說什麼,透過原文的說明可以知道,高階模組在呼叫低階模組時,應透過抽象,而不是直接呼叫低階模組的實作,藉此降低高低階模組間的耦合。

  1. 高階模組DataTask & 低階模組LogService皆依賴 interface ILogService
  2. 低階模組LogService就算沒有 interface 也不會反過來依賴高階模組DataTask
  3. 高階模組DataTask與低階模組LogService之間為鬆耦合
public interface ILogService
{
    void Info(DateTime time, StatusType status);
}

public class LogService : ILogService
{
    public void Info(DateTime time, StatusType status)
    {
        // 實作寫 log
    }
}

public class DataTask
{
    private ILogService _log { get; }

    public DataTask(ILogService logService)
    {
        _log = logService;
    }

    public void 處理資料排程()
    {
        _log.Info(DateTime.Now, StatusType.Start);

        // 處理資料實作

        _log.Info(DateTime.Now, StatusType.End);
    }
}

總結

Design Principles 可能會因為從不同的角度(層面)而有不同的解讀,而原則是死的,很多時候並非絲毫不差的遵守原則就是好的設計,過度從更細的層面來檢視是否符合 Design Principles 可能會造成過度設計且讓整體程式變得非常複雜,但什麼才是剛好的設計就只能從經驗來拿捏了。


參考資料

  1. Wikipedia
  2. 無瑕的程式碼 敏捷完整篇

新手上路,若有錯誤還請告知,謝謝


#DesignPrinciples







Related Posts

新版 Python 在 PyCharm 無法正確判讀與除錯處理

新版 Python 在 PyCharm 無法正確判讀與除錯處理

JS30 Day 13 筆記

JS30 Day 13 筆記

大腦理解需要時間 先看影片學習輸入腦袋~

大腦理解需要時間 先看影片學習輸入腦袋~


Comments