物件導向設計的五原則 SOLID
Categories:
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 的優缺點
優點
- 提高程式碼的
重用性
- 提高類別的
擴充性
缺點
- 因為繼承關係,
耦合性增高
(修改父類別常數時需要思考是否會影響到其他繼承的子類別
) - 降低程式碼靈活性 (必須實作父類別所有方法)
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
參考資料
- 09. 物件導向設計原則—SOLID - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
- 搞笑談軟工: SOLID:五則皆變
- 淺談物件導向 SOLID 原則對工程師的好處與如何影響能力 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
- SOLID 里氏替換原則 Liskov Substitution Principle (LSP) - Finn - Medium
- S.O.L.I.D 五大原则之里氏替换原则 LSP - 深入理解 JavaScript - 极客学院Wiki
- [Day05] 里氏替換原則 | Liskov Substitution Principle - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天