物件導向設計的五原則 SOLID

程式進階架構知識 Coding Structure 物件導向設計的五原則 SOLID

SOLID 原則對專案的影響很大,當專案一點一滴的導入 SOLID 原則的程式碼,不少複雜的程式碼慢慢被簡化,被簡化的程式碼可以降低複雜度,讀懂程式碼的時間從原本需要花 20 分鐘閱讀,到只需要花費 2 分鐘閱讀。

縮短閱讀時間對專案來說是一件好事,一般來說工程師「閱讀程式碼」的時間常常大於「新增/修改程式碼」的時間,畢竟要先讀懂才能動手嘛,因此 縮短閱讀程式碼時間 等於 縮短「新增/修改程式碼」 程式碼的時間。

遵循 SOLID 原則的程式碼具有以下特性

  • 降低程式碼複雜度
  • 縮短閱讀程式碼的時間
  • 減少維護專案程式碼的時間
  • 能夠容忍變化
  • 容易擴展新邏輯
  • 容易理解
  • 容易複用

SOLID 原則說明

S: 單一職責原則 (Single responsibility principle, SRP)

目的

每個物件,不管是類別、函數,負責的功能,都應該 只做一件事

原因

對函數而言,一個函數內,同時做了兩件以上的事情。當發生錯誤時,很難快速定位錯誤的原因。也容易間接導至程式碼的可閱讀性降低。

O: 開放封閉原則 (Open-Close principle, OCP)

目的

當需求有異動時,要如何在不變動現在正常運行的程式碼,藉由繼承相依性注入等方式,增加新的程式碼,以實作新的需求。

原因

假若為了新需求,去修改了原本的程式中的某一個函數,可能會造成其他呼叫使用該函數的的功能,出現非預期的錯誤

L: 里氏替換原則 (Liskov substitution principle, LSP)

目的

子類別可以擴充套件父類別的功能,但 不改變 父類別原有的功能

原因

繼承的特性導致高耦合,子類別對於方法修改(Override, Overload) 必須依照父類別行為方向,否則會對整體的繼承體系照成破壞,會有產生不可預測的行為不好察覺 Bug

假設你繼承實作並且 override 的時候,必須先思考修改內容的行為方向是否符合父類別,如果行為不符合就意味著其實一開始就不屬於該父類別

範例

一般正常 繼承(is-a),老鷹是鳥的一種,鳥為父類別,老鷹為子類別。而子類別也繼承父類別的行為,比如說鳥會飛,老鷹為鳥的子類別,所以老鷹也會飛。

class Bird {
    int flySpeed;
    public void setFlySpeed(int flySpeed) {
        this.flySpeed = flySpeed;
    }
    public int getFlyTime(int distance) {
        return(distance/flySpeed);
    }
}

class Eagle extends Bird {}

public class LSP {
    public static void main(String[] args) {
        Bird eagle=new Eagle();
        eagle.setFlySpeed(120);
        System.out.println("路程300公里:");
        System.out.println("老鷹花了" + eagle.getFlyTime(300) + "小時.");
    }
}

// 路程300公里:
// 老鷹花了2.5小時.

不會飛企鵝 繼承 鳥子類別

class Bird {
    int flySpeed;
    public void setFlySpeed(int flySpeed) {
        this.flySpeed = flySpeed;
    }
    public int getFlyTime(int distance) {
        return(distance/flySpeed);
    }

}

class Eagle extends Bird {}

class Penguin extends Bird {
    public void setFlySpeed(int flySpeed) {
        this.flySpeed = 0;
    }
}

public class LSP {
    public static void main(String[] args) {
        Bird eagle=new Eagle();
        Bird penguin=new Penguin();
        eagle.setFlySpeed(120);
        penguin.setFlySpeed(20);
        System.out.println("路程300公里:");
        System.out.println("老鷹花了" + eagle.getFlyTime(300) + "小時");
        System.out.println("企鵝花了" + penguin.getFlyTime(300) + "小時");
    }
}

// 飛行300公里:
// 老鷹花了2.5小時
//
// Exception in thread "main" java.lang.ArithmeticException: / by zero
// 	at Bird.getFlyTime(LSP.java:7)
// 	at LSP.main(LSP.java:28)

沒有辦法計算出企鵝飛行的時間,因為企鵝根本不會飛,違反了 LSP 原則,所以 企鵝 根本不應該繼承 ,兩者的屬性不相同會導致發生意外的錯誤

Liskov Substitution Principle 的4個繼承規範

  • 子類別必須 完全實現 父類別的方法
  • 子類別 可以有自己的特性
  • 重載 (Overload) 或者實現父類別的方法時 輸入參數可以被放大
  • 覆蓋或者實現父類別的方法時 輸出結果可以被縮小

LSP 的優缺點

優點

  1. 提高程式碼的 重用性
  2. 提高類別的 擴充性

缺點

  1. 因為繼承關係,耦合性增高 (修改父類別常數時需要思考是否會影響到其他繼承的子類別)
  2. 降低程式碼靈活性 (必須實作父類別所有方法)

I: 接口隔離原則 (Interface segregation principle, ISP)

目的

針對不同需求的用戶,開放其對應需求的介面,提拱使用。

原因

避免不相關的需求介面異動,造成被強迫一同面對異動的情況

假設你有一個擁有10個函數的類別,現在有 3 個不同的客戶端都需要使用這個類別其中的 4、7、5 個函數。

如果直接把這個類別傳給三個不同的客戶端,則這些客戶端很可能會因為它所沒使用到的函數改變了,也被迫跟著改變(因為原本類別的介面改變了)。

另一種作法,則是針對三個不同的客戶端,提供三個不同的 Adapter。透過 Adapter,只開放客戶端所需的函數給它們,以隔離因為不相關介面改變所造成的客戶端改變。

D: 依賴反轉原則 (Dependency inversion principle, DIP)

目的

當 A 模組在內部使用 B 模組的情況下,我們稱 A 為上層高階模組 (caller)B 為底層低階模組 (callee)

上層高階模組 (caller) 不應該 依賴於 底層低階模組 (callee),兩者都該 依賴抽象介面

原因

避免 上層高階模組 (caller) 因為 底層低階模組 (callee) 改變而被迫改變,導致更多 bug

參考資料