簡單說明SOLID法則
作為一名後端工程師,我們常聽過不少開發上的規範,比方說常見的
- DRY原則──指的是Don’t repeat yourself.,不要重複寫同樣的內容。
- KISS原則──指開發的產品程式碼應該要越簡單越好,簡單的好處不僅可以降低錯誤的發生率,也帶來較好維護、好理解、好測試等等優點。
但我們這次要提到的不是以上這兩個,而是SOLID。
SOLID是一名後端工程師在成長的過程中一定會聽到的單字,特別指物件導向設計的設計原則,SOLID的目的是使軟體設計更為便於理解、更便於修改、而且更好維護的五項設計原則。我們就一起來看看他們吧。
S 單一職責原則(Single Responsibility Principle)
SRP主張一個類別應該只有一個職責,也就是說一個類別應該只有一個改變的原因。也有人這樣解釋:一個類別應只對唯一的一個角色負責。
這聽起來很難不讓人不茫然,我的理解是這樣的,SRP是指一個類別應該只做一件事,舉例來說:
假設有一個物件同時負責了將物件回傳給前端,以及物件存入資料庫這兩件事。當今天規格需要調整時,就可能造成,新增某欄位後前端壞掉後端也存不進去的問題,因為同一個模組負責了兩件事情 (回傳給前端的物件 + 存入資料庫的物件)。
比較好的做法是切割這兩個功能,使其成為一個獨立的類別。
實務上來說,這並不好實現,在設計時,雖然我們希望一個類別相當單純,但往往又會希望這個功能是可以好重複利用的,我們會很想要把一些重複的、不屬於該類別的處理加入該類別中,這最終導致了單一職責原則遭破壞。
所以SRP未必是一定得遵守的原則,反倒像是一種理想形式,只能透過經驗去學習如何切出一個乾淨的、職責分明的類別。
O 開放封閉原則 (Open-Closed Principle)
OCP主張一個軟體在面對擴展時是開放的,且擴充時不應修改到原有的程式。它必須是好修改的,就算要改,也不該影響現有程式功能。 舉例來說:
我們有一個車輛介面(Vehicle),並有一個make()製造的方法,所有的汽車(Toyota、Honda)都是透過CarFactory的assemble()方法去組裝一台汽車。今天,假設我們有一個新的類別Mazda,他加入時, 不應該要更改CarFactory的內容。
所有的調整、商業邏輯要集中在Mazda,並且沒有資格參加這個會議。
1 | |
通常定義介面、或是一個繼承關係,可以實現OCP,定義中提到的擴展開放──就是指加入新的類別時不應影響到基底的功能。如果真的要加入的話,定義一個新的類別,透過繼承原先類別來完成它。
對修改封閉的意思剛好反過來,假設你想要調整CarFactory、就變得相當困難,因為一旦修改CarFactory,所有的汽車類別都會受到影響,因此對修改是封閉的。
聽起來是不是理所當然?但實際上也不太容易看到這樣的程式碼,通常的情況是,大家常常覺得在基底類別中加入一個if做特例處理就好,這導致了現實常常是一堆if if if在共用的類別中。要乾淨地拆開,既不違反KISS又要能符合OCP往往並不簡單。
最好的情況是,當下在設計初期,就考慮到抽象化類別的可能性,但這部分也需要經驗去判斷。
L 里氏替換原則 (Liskov Substitution Principle)
LSP主張,子類別必須能夠替換父類別,並且行為不受影響。原則有三:
- 子類別的先決條件 (Preconditions) 不應被加強。────子類別應該要可以完全取代父類別、簡單來說就是不可以限制父類的定義,如果父類別的方法是接收Vehicle、子類別不可以覆寫並限制只能放入Mazda。
- 子類別的後置條件 (Postconditions) 不應被削弱。────子類別不可以削弱父類別的定義、與剛才相反,如果父類別的方法是接收Mazda,子類別覆寫時不可以改為Vehicle
- 父類別的不變條件 (Invariants) 必須被子型態所保留。────子類別不可以修改父類別的定義。
簡單來說呢,當你子類別想要做與父類別截然不同的事,你應該要思考,這個物件是否真的需要的是繼承嗎?還是應該拆開、或是套用Interface。
讓我們看一個壞例子
1 | |
I 介面隔離原則 (Interface Segregation Principle)
ISP主張,因為模組之間的依賴不應有用不到的功能。
這是為了避免Code smell(程式碼異味)的一個原則,有一種設計上的問題要做God Object,這個God Object就跟上帝一樣什麼都能做,這種物件存在最終會導致,今天God Object需要調整時,你將會感受痛苦與絕望。因為它被使用到的地方太多,而你極有可能無法去確保修改之後系統會發生什麼不可預料的事情。腐敗就此開始。
當出現這樣的問題時,應該適當予以分割,我們可以透過介面來進行分割,把模組分得更符合本身的角色,也讓使用介面的角色只能分別接觸到應有的功能。
多使用介面來進行解藕、把實作隱藏起來、保持抽象,有助我們程式的彈性。
D 依賴反轉原則 (Dependency Inversion Principle)
DIP主張,
- 高層模組不應依賴低層模組,它們都應依賴於抽象介面。
- 抽象介面不應該依賴於具體實作,具體實作應依賴抽象介面。
我當初看到就覺得,這什麼鬼?可不可以講人話?再說反轉是什麼,新的遊戲王陷阱卡嗎?
為了更好地說明,讓我們一步步解釋這概念吧。
高層、低層的概念是指抽象化的程度,比方說
1 | |
當中ExportService是一個高層模組,exportDoc()負責輸出文件,而他不需要管文件到底是.txt、.doc、.docx,因為它並不直接依賴低階模組的內容,而是透過介面去操作它。
低階模組就像是Txt、Word等等實作Document的類別,內部定義了實作的內容。
所以,ExportService(高階模組)不應該依賴Word(低階模組),而是依賴Document這個介面。
第二點,可以參考以下程式
1 | |
這個介面依賴了實際的內容,應該抽象化,比方說
1 | |
透過抽象化,讓介面可以更靈活。
透過實現DIP的原則,有以下好處
- 提高靈活性:系統可以更輕鬆地替換具體實作,例如更換資料庫或支付服務。
- 提升可測試性:高層模組依賴抽象介面,可以通過模擬物件(Mock)來進行單元測試。
- 降低耦合:高層與低層模組相互隔離,修改低層模組時不需要修改高層模組。
以上就是我整理的內容有關SOLID的一些概念解說,那今天就講到這裡了。