SOLID 是由 Robert C. Martin 所提倡的物件導向程式設計原則。當設計出現臭味時,常常是因為違反了 SOLID 所導致。學習 SOLID 有助於開發人員消除設計中的臭味,讓設計盡可能的乾淨、簡單並富有表達力。
Table of Contents
設計臭味(Design Smells)— 腐化軟體的氣味
當軟體出現以下任何一種氣味時,就代表著軟體正在腐化。
僵化性(Rigidity)— 設計難以改變
難以對系統進行修改,因為每一個修改都會迫使要對其他部分進行修改。
脆弱性(Fragility)— 設計易遭到破壞
進行一個修改導致系統出現問題,而且出現的問題和修改的地方沒有概念上的關聯。
頑固性(Immobility)— 設計難以重複使用
難以將系統中的某個部分分離出來成一個元件,來再次使用在其他系統。
粘滯性(Viscosity)— 難以做正確的事情
正確地修改比錯誤地修改更難以實現。
當面臨一個修改時,開發人員發現有不只一種修改方式。有一些方式可以保留目前的設計;其他的方式則無法(也就是 hacks)。當可以保留目前設計的方法比 hacks 難以實現時,就表示目前的設計具有高的粘滯性。所以做錯誤的事(hacks)簡單,但做正確的事(保留原設計)很難。
不必要的複雜性(Needless Complexity)— 過分設計
設計中包含了目前沒有用的部分。
這常常發生在開發人員預測了需求的變化,而在開發時就預先放置了處理潛在變化的程式碼。
不必要的重複(Needless Repetition)— 濫用滑鼠
設計中包含了重複的結構,而這些重複的結構可透過一個抽象(abstraction)來去除。
這常常發生在開發者複製某段程式碼,再貼上到其他地方。
晦澀性(Opacity)— 混亂的表達
難以閱讀和理解的程式碼。程式碼無法清楚地表達它的意圖。
SRP (The Single-Responsibility Principle):單一職責原則
「凝聚(cohesive)」一詞意味著 SRP。凝聚力是種力量,可以將程式碼綁定在一起,以對角色負責。
下圖中的 Employee class 有三個方法:
- calculatePay():會計部指定,他們向 CFO 報告。
- reportHours():人資部指定,他們向 COO 報告。
- save():DBA 指定,他們向 CTO 報告。
開發人員將這三個方法放到 Employee class,所以 Employee class 對三個角色負責。這違反了 SRP 原則。
假設 calculatePay() 和 reportHours() 共用計算非加班工作時間的方法 regularHours()。現在假設 CFO 團隊想要調整非加班工作時間的計算方法,但是 COO 團隊沒有調整計算方法。開發人員開始進行修改 calculatePay(),也發現是由 regularHours() 計算非加班工作時間,因而對 regularHours() 進行修改。但是開發人員沒有注意到 reportHours() 也有使用 regularHours()。
修改完成後,CFO 團隊對系統進行測試,確認新功能正常。但是 COO 團隊完全沒有意識到計算非加班工作時間的方法被調整過了。最終,這將使公司造成損失。
解決方式是將這三個方法移到個別的 class。每一個 class 只對一個角色負責——遵循 SRP 原則。共用的 Employee Data 是一個沒有方法的簡單資料結構。
OCP (The Open-Closed Principle):開放-封閉原則
OCP 指導了設計類別和模組的原則。它的目標是使系統易於擴展而不會因修改而產生較大的影響。這個目標是透過將系統劃分為元件,並將這些元件安排到依賴階層中而實現的。
如何才能使「不變動模組原始程式碼但改變它的行為」成為可能呢?答案是抽象(abstraction)。
下圖中的設計符合 OCP 原則。假設一開始只有 ScreenPresenter。我們想要擴充功能,可以輸出至 PDF。我們只需要實作 PrintPresenter,無須對 FinancialReportController 做任何修改。FinancialReportController 中會定義 Presenter 的介面或抽象類別。ScreenPresenter 和 PrintPresenter 會實作或繼承 Presenter 來擴充功能,並且不變動 FinancialReportController。
LSP (The Liskov Substitution Principle):Liskov 替換原則
下圖遵循 LSP 原則,因為 Billing 的行為不依賴於任何 License 的子型態。這兩個子型態(PersonalLicense 和 BusinessLicense)可以替換 License 型態。
下圖違反 LSP 原則,因為 User 必須依賴於 Square 型態。
如下程式碼,在 User.draw() 中,它必須要先知道 rectangle 的型態,才決定要呼叫哪個方法。
class User { private void draw(Rectangle rectangle) { if (rectangle instanceof Square) { ((Square) rectangle).setSide(10); } else { rectangle.setWidth(10); rectangle.setHeight(10); } } }
ISP (The Interface-Segregation Principle):介面隔離原則
下圖中,UI interface 包含了四個方法。DepositTransaction 只需要 requestDepostAmount(),但是它必須要實作其他三個方法。DepositTransaction 的這三個方法不會有任何程式碼。當 requestWithdrawalAmount() 的名稱或是參數有變更時,DepositTransaction 也要跟著修改。這就很奇怪了,因為 DepositTransaction 沒有使用到 requestWithdrawalAmount(),當它有變更時,DepositTransaction 卻也要跟著修改。
解決方法是利用 LSP 原則來將 UI interface 分離成數個介面,如下圖。
DIP (The Dependency-Inversion Principle):依賴反向原則
下圖顯示傳統的開發方法。高層的 Policy Layer 依賴於低層的 Mechanism Layer,而其又依賴於更低層的 Utility Layer。依賴關係是遞移的,所以 Policy Layer 依賴於 Utility Layer。
下圖是更為合適的模型,其遵循 DIP 原則。每個較高層都為它所需要的服務宣告一個抽象介面,而較低層實現了這些抽象介面。這樣高層就不會依賴於低層。低層反而依賴於高層中的抽象介面。這也就解除了 Policy Layer 對 Utility Layer 的依賴關係。
結語
非常推薦進一步地閱讀 Robert C. Martin 的 Agile Principles, Patters, and Practices in C# 和 Clean Architecture。書中他除了詳細地探討 SOLID 之外,還包含很多主題,如 agile design、UML、design patters、architectures 等。對於作為一位開發人員,這些都是重要的知識。