Solid
其實網路上對SOLID的探討及見解文章已經多不勝數。筆者在此僅記錄自身見解與歷程。
若對該原則理解有誤,請不吝指教。
SOLID?
- 在程式設計的領域中,SOLID指物件導向編成和物件導向設計的五個基本原則。
- 當這些原則一起被應用時,可以使程式設計師開發一個容易擴充及維護的系統。
- SOLID所包含的原則是通過引發編程者進行軟體原始碼代碼重構的代碼異味清掃。從而使軟體清晰可讀及具可擴展性。
- SOLID被典型的應用在測試驅動開發上,且是敏捷開發及自適應軟體開發的基本原則重要組成部分。
S: Single responsibility principle(SRP) 單一職責
定義:
1 | 一個類別(class)/方法(method)只能負責一個職責 |
如果一個類別做了兩件職責,就必須拆成兩個類別。
- 當類別包含複數職責時,則其
內聚力
越低。 - 當類別職責越單純且清楚,則其
內聚力
越高。
舉例:
如果汽車
與飛機
皆為交通工具而實作於同一個類別(super obj)
中,汽車行為模式為 run
而飛機行為模式為 fly
。那該類別內部邏輯會變得雜亂且難以維護。
若將汽車
與飛機
個別實作,各自物件只要管理自己的權責
。即可提高內聚
。而特定邏輯修改時只要到對應物件內做調整,也降低了維護的難易度。
結論:
- 一個 class/method 只做一件事。
- 遵守 SPR 可為程式提高內聚。
Open/close principle(OCP) 開放/封閉原則
定義:
1 | 程式在擴充新功能時,不更動原程式碼或者僅以最小幅度修改程式碼的開發方式。 |
系統實作完若遇到新需求,必須回頭調整原本做好的程式代碼。而只要修改舊的代碼就可能造成
不良副作用
#1。應以最小的幅度來修改已存在的程式碼(甚至不修改
)才是最佳的情況。OCP 使系統保有彈性,可以擴充新功能。若有新需求只要進行新增,而不用修改到舊有程式碼(
對修改封閉
,對新增開放
)。進而杜絕不良副作用
的產生。OCP 只是一個原則,讓程式變得靈活的代價是需要花費額外的時間與精力將程式引入新的抽象層,還會增加程式的複雜度。所以 OCP 原則只適合被套用於經常變更的地方!
(#1)不良副作用: 修改程式內碼造成其他使用的該邏輯的地方產生錯誤,即大家口中的改 A 壞 B。
舉例:
設計車子時,因單一職責(SPR)
而對車上零件(車燈、輪胎、引擎)進行模組化。
若此時若想將一般輪胎換成雪胎。而去修改一般輪胎的模組(物件)內容,可能造成其他使用該物件的邏輯錯誤。
若遵守 OCP
原則,僅對輪胎新增雪胎模組,在車子內部進行替換組合。則只需新增
還不需要修改
。拒絕了改 A 壞 B的情況產生。
結論:
- 只有經常變更的地方需要使用 OCP 原則。
- 系統對
新增開放
,修改封閉
。
L: Liskov substitution principle(LSP) Liskov替換
定義:
1 | Subtypes must be substitutable for their base types. |
里氏替換原則原則要能夠成立,介面(interface)/抽象方法(Abstract method) 就必須要遵守定義去實做。
子類別需兌現對父類別的承諾,遵照父類別設計開發。
作為子類別的方法必須和他們父類別的方法操作一致,子類別中可以擁有父類別沒有的特殊功能,但是繼承的方法,功能應該兩者一致的。
子類不只是實現父類別的方法,而且必須名符其實,否則會發生無法預料的事情。
舉例:
雪胎
及一般輪胎
皆繼承輪胎模組(父類別)
,而組裝者可由方法取得材質(function)
取得輪胎材質來判斷是否適合組裝。若雪胎
擅自更改回傳值為適用地形
。就會造成組裝者誤判而產生無法逾期之錯誤。
若遵守 LSP
原則,取得材質(function)
方法應該遵循輪胎模組(父類別)
設計結構去回傳正確值。即可避免程式的行為變得不可預測
。
結論:
- 子類別必須遵從父類別或介面的設計理念去實作方法。
- LSP 是實現 OCP 原則的重要方式,只有當子類別能夠
完全替代它們的父類別類
時,使用父類別的函數
才能夠被安全的重用。
I: Interface Segregation Principle(ISP) 介面隔離
定義:
1 | Clients should not be forced to depend upon interfaces that they don’t use. |
1 | The dependency of one class to another one should depend on the smallest possible interface. |
類別與類別之間的關係,應只依賴彼此需要的最少介面
,介面不能太肥
,應該要細化
。
介面的目的都是提供一個讓 Client 端可以使用我們
開發模組的管道
。介面處於應用程式與模組之間、或是專案與模組之間,這種關係就如同
第三方套件提供服務 API 給一個網站使用
。
舉例:
跑車
與玩具車
皆繼承汽車模組(父類別)
,而汽車模組(父類別)
存在一方法跑(function)
。但玩具車並不具備跑(function)
這個行為,若是空實作則違反 LSP
原則。
應該設計兩介面(interface)
:模型車(interfece)
與實車(interface)
。
而於實車(interface)
內部宣告跑(function)
方法,再由跑車
與玩具車
個別去實作對應的介面(interface)
。
結論:
ISP
可降低商業邏輯(低階模組)與 Client 之間的耦合。- 設計模組時,要以 Client 需求的角度建立介面,且避免設計龐多功能的單一介面。
- 設計
介面(interface)
的時候,應該考慮單一職責原則,把有關聯的方法放在一起,分割出多個單一功能的介面。
D: Dependency Inversion Principle(DIP) 依賴反轉
定義:
1 | High-level modules should not depend on low-level modules. Both should depend on abstractions. |
1 | Abstractions should not depend on details. |
1 | Details should depend on abstractions. |
高階與低階,是相對關係,其實也就是 呼叫者 (Caller) 與 **被呼叫者 (Callee)**。
高階模組直接依賴低階模組,則為高耦合。
不應該讓任何東西直接依賴低階模組。
DIP
由DI(注入)
與IOC(控制反轉)
實作。目的為解除耦合性(解偶)
。
舉例:
實作汽車
時需組裝零件
,此時若將零件
寫死於汽車
中。若其中一項零件
停產了或要更換。則需到汽車
中修改既有內碼(違反 OCP
)。
以輪胎為例,汽車
有內碼使用 一般輪胎
。而一般輪胎
剛好停產或廢棄了。那汽車
即會出錯。
若是要修改還需要到汽車
內碼中將使用 一般輪胎
改成使用 新的一般輪胎
。
若依照 DIP
設計。將輪胎抽離於組裝者取得並組裝於車上。舉例來說汽車->組裝輪胎(一般輪胎)
。
依上述情境若一般輪胎
出事了,組裝者只要從外部替換輪胎汽車->組裝輪胎(新的一般輪胎)
。
則不用更動汽車程式內碼
,汽車
與輪胎
之間也不具備耦合性。
結論:
介面(interface)
應由高階模組去制定規範。- 使用
介面(interface)
確保低階模組
開發正確性。 - 遵守
DIP
可大大降低物件之間的耦合性(解偶)
。 - 依賴反轉原則可以幫助我們遵守其他原則,遵守依賴反轉的過程中會:
- 拆散類別的職責(
SRP
)。 - 更容易達成
OCP
。 - 避免父子類別沒有依照介面的定義實作(
LSP
)。 - 拆散介面的職責(
ISP
)。
- 拆散類別的職責(
——————-
工程師最怕聽到的就是程式又要改。
而SOLID原則是前人經過無數經驗統整出使程式具備容易維護且具可擴充性質。讓之後在更動程式或擴充需求時能降低修改既有程式難易度。
雖說SOLID原則能使程式品質有實質的躍升,但還是得注意別陷入過度設計。