SOLID 物件導向程式設計原則

Photo by Guillaume Meurice on Unsplash
Photo by Guillaume Meurice on Unsplash
SOLID 是由 Robert C. Martin 所提倡的物件導向程式設計原則。當設計出現臭味時,常常是因為違反了 SOLID 所導致。學習 SOLID 有助於開發人員消除設計中的臭味,讓設計盡可能的乾淨、簡單並富有表達力。

SOLID 是由 Robert C. Martin 所提倡的物件導向程式設計原則。當設計出現臭味時,常常是因為違反了 SOLID 所導致。學習 SOLID 有助於開發人員消除設計中的臭味,讓設計盡可能的乾淨、簡單並富有表達力。

設計臭味(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 團隊完全沒有意識到計算非加班工作時間的方法被調整過了。最終,這將使公司造成損失。

The Employee class, from Clean Architecture.
The Employee class, from Clean Architecture.

解決方式是將這三個方法移到個別的 class。每一個 class 只對一個角色負責——遵循 SRP 原則。共用的 Employee Data 是一個沒有方法的簡單資料結構。

The three classes do not know about each other, from Clean Architecture.
The three classes do not know about each other, from Clean Architecture.

OCP (The Open-Closed Principle):開放-封閉原則

一個軟體製品應該對於擴展是開放的,但對於修改是封閉的。

OCP 指導了設計類別和模組的原則。它的目標是使系統易於擴展而不會因修改而產生較大的影響。這個目標是透過將系統劃分為元件,並將這些元件安排到依賴階層中而實現的。

如何才能使「不變動模組原始程式碼但改變它的行為」成為可能呢?答案是抽象(abstraction)。

下圖中的設計符合 OCP 原則。假設一開始只有 ScreenPresenter。我們想要擴充功能,可以輸出至 PDF。我們只需要實作 PrintPresenter,無須對 FinancialReportController 做任何修改。FinancialReportController 中會定義 Presenter 的介面或抽象類別。ScreenPresenter 和 PrintPresenter 會實作或繼承 Presenter 來擴充功能,並且不變動 FinancialReportController。

The component relationships are unidirectional, from Clean Architecture.
The component relationships are unidirectional, from Clean Architecture.

LSP (The Liskov Substitution Principle):Liskov 替換原則

子型態(subtype)必須能夠替換掉它們的基底型態(base type)。

下圖遵循 LSP 原則,因為 Billing 的行為不依賴於任何 License 的子型態。這兩個子型態(PersonalLicense 和 BusinessLicense)可以替換 License 型態。

License, and its derivatives, conform to LSP, from Clean Architecture.
License, and its derivatives, conform to LSP, from Clean Architecture.

下圖違反 LSP 原則,因為 User 必須依賴於 Square 型態。

The infamous square/rectangle problem, from Clean Architecture.
The infamous square/rectangle problem, from Clean Architecture.

如下程式碼,在 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 卻也要跟著修改。

ATM Transaction Hierarchy, from Agile Software Development.
ATM Transaction Hierarchy, from Agile Software Development.

解決方法是利用 LSP 原則來將 UI interface 分離成數個介面,如下圖。

Segregated ATM UI Interface, from Agile Software Development.
Segregated ATM UI Interface, from Agile Software Development.

DIP (The Dependency-Inversion Principle):依賴反向原則

a. 高層模組不應該依賴於低層模組。二者都應該依賴於抽象。
b. 抽象不應該依賴於細節。細節應該依賴於抽象。

下圖顯示傳統的開發方法。高層的 Policy Layer 依賴於低層的 Mechanism Layer,而其又依賴於更低層的 Utility Layer。依賴關係是遞移的,所以 Policy Layer 依賴於 Utility Layer。

Naive layering scheme, from Agile Software Development.
Naive layering scheme, from Agile Software Development.

下圖是更為合適的模型,其遵循 DIP 原則。每個較高層都為它所需要的服務宣告一個抽象介面,而較低層實現了這些抽象介面。這樣高層就不會依賴於低層。低層反而依賴於高層中的抽象介面。這也就解除了 Policy Layer 對 Utility Layer 的依賴關係。

Inverted layers, from Agile Software Development.
Inverted layers, from Agile Software Development.

結語

非常推薦進一步地閱讀 Robert C. Martin 的 Agile Principles, Patters, and Practices in C#Clean Architecture。書中他除了詳細地探討 SOLID 之外,還包含很多主題,如 agile design、UML、design patters、architectures 等。對於作為一位開發人員,這些都是重要的知識。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *