S.O.L.I.D:物件導向設計的頭 5 大原則

cucr發表於2015-04-23

S.O.L.I.D 是物件導向設計(OOD)的頭五大基本原則的首字母縮寫,由俗稱「鮑勃大叔」的 Robert C. Martin 提出。

這些原則,結合在一起能夠方便程式設計師開發易於維護和擴充套件的軟體,也讓開發人員輕鬆避免程式碼異味,易於重構程式碼,也是敏捷或自適應軟體開發的一部分。

注意:這只是一篇“歡迎來到S.O.L.I.D”的簡單介紹文章,它只是揭示了S.O.L.I.D是什麼。

S.O.L.I.D 代表什麼:

雖然縮略詞展開後看似複雜,但其實非常容易掌握。

  • S – 單一職責原則
  • O – 開放封閉原則
  • L – 里氏替換原則
  • I – 介面隔離原則
  • D – 依賴倒置原則

讓我們來單獨看看每個原則,來理解為什麼 S.O.L.I.D 能幫助我們成為更優秀的開發人員。

單一職責原則

(伯樂線上配圖)

S.R.P(簡稱)原則指出:

一個類應該有且只有一個去改變它的理由,這意味著一個類應該只有一項工作。

例如,假設我們有一些shape(形狀),並且我們想求所有shape的面積的和。這很簡單對嗎?

首先,我們建立shape類,讓建構函式設定需要的引數。接下來,我們繼續通過建立AreaCalculator類,然後編寫求取所提供的shape面積之和的邏輯。

使用AreaCalculator類,我們簡單地例項化類,同時傳入一個shape陣列,並在頁面的底部顯示輸出。

輸出方法的問題在於,AreaCalculator處理了輸出資料的邏輯。因此,如果使用者想要以json或其他方式輸出資料該怎麼辦?

所有的邏輯將由AreaCalculator類處理,這是違反單一職責原則(SRP)的;AreaCalculator類應該只對提供的shape進行面積求和,它不應該關心使用者是需要json還是HTML。

因此,為了解決這個問題,你可以建立一個SumCalculatorOutputter類,使用這個來處理你所需要的邏輯,即對所提供的shape進行面積求和後如何顯示。

SumCalculatorOutputter類按如下方式工作:

現在,不管你需要何種邏輯來輸出資料給使用者,皆由SumCalculatorOutputter類處理。

開放封閉原則

物件或實體應該對擴充套件開放,對修改封閉。

這就意味著一個類應該無需修改類本身但卻容易擴充套件。讓我們看看AreaCalculator類,尤其是它的sum方法。

如果我們希望sum方法能夠對更多的shape進行面積求和,我們會新增更多的If / else塊,這違背了開放封閉原則。

能讓這個sum方法做的更好的一種方式是,將計算每個shape面積的邏輯從sum方法中移出,將它附加到shape類上。

對Circle類應該做同樣的事情,area方法應該新增。現在,計算任何所提的shape的面積的和的方法應該和如下簡單:

現在我們可以建立另一個shape類,並在計算和時將其傳遞進來,這不會破壞我們的程式碼。然而,現在另一個問題出現了,我們怎麼知道傳遞到AreaCalculator上的物件確實是一個shape,或者這個shape具有一個叫做area的方法?

對介面程式設計是S.O.L.I.D不可或缺的一部分,一個快速的例子是我們建立一個介面,讓每個shape實現它:

在我們AreaCalculator的求和中,我們可以檢查所提供的shape確實是ShapeInterface的例項,否則我們丟擲一個異常:

里氏替換原則

(伯樂線上配圖)

在物件 x 為型別 T 時 q(x) 成立,那麼當 S 是 T 的子類時,物件 y 為型別 S 時 q(y) 也應成立。(即對父類的呼叫同樣適用於子類)

這一切說明的是,每一個子類或派生類應該可以替換它們基類或父類。

還利用AreaCalculator類,我們有一個VolumeCalculator類,它擴充套件了AreaCalculator類:

In the SumCalculatorOutputter class:

在SumCalculatorOutputter類中:

如果我們試圖這樣來執行一個例子:

程式可以執行,但是當我們在$output2物件呼叫HTML方法,我們得到一個E_NOTICE錯誤,提示陣列到字串的轉換。

為了解決這個問題,不要從VolumeCalculator類的sum方法返回一個陣列,你應該:

求和的結果作為一個浮點數,雙精度或整數。

介面隔離原則

不應強迫客戶端實現一個它用不上的介面,或是說客戶端不應該被迫依賴它們不使用的方法。

仍然以shape為例,我們知道也有立體shape,如果我們也想計算shape的體積,我們可以新增另一個合約到ShapeInterface:

任何我們建立的shape必須實現volume的方法,但是我們知道正方形是平面形狀沒有體積,所以這個介面將迫使正方形類實現一個它沒有使用的方法。

介面隔離原則(ISP)不允許這樣,你可以建立另一個名為SolidShapeInterface的介面,它有一個volume合約,對於立體形狀比如立方體等等,可以實現這個介面:

這是一個更好的方法,但小心一個陷阱,當這些介面做型別提示時,不要使用ShapeInterface或SolidShapeInterface。

你可以建立另一個介面,可以是ManageShapeInterface,平面和立體shape都可用,這樣你可以很容易地看到它有一個管理shape的單一API。例如:

現在AreaCalculator類中,我們可以輕易用calculate替代area呼叫,同時可以檢查一個物件是ManageShapeInterface而不是ShapeInterface的例項。

依賴反轉原則

最後一條,但肯定不是最無足輕重的一條:

實體必須依靠抽象而不是具體實現。它表示高層次的模組不應該依賴於低層次的模組,它們都應該依賴於抽象。

這聽起來可能有點繞,但它很容易理解。這一原則允許解耦,這似乎是用來解釋這一原則最好的例子:

首先MySQLConnection是低層次模組,而PasswordReminder處於高層次,但根據S.O.L.I.D.中D的定義,即依賴抽象而不是具體實現,上面這段程式碼違反這一原則,PasswordReminder類被迫依賴於MySQLConnection類。

以後如果你改變資料庫引擎,你還必須編輯PasswordReminder類,因此違反了開閉原則。

PasswordReminder類不應該關心你的應用程式使用什麼資料庫,為了解決這個問題我們又一次“對介面程式設計”,因為高層次和低層次模組應該依賴於抽象,我們可以建立一個介面:

介面有一個connect方法,MySQLConnection類實現該介面,在PasswordReminder類的建構函式不使用MySQLConnection類,而是使用介面替換,不用管你的應用程式使用的是什麼型別的資料庫,PasswordReminder類可以很容易地連線到資料庫,沒有任何問題,且不違反OCP。

根據上面的程式碼片段,你現在可以看到,高層次和低層次模組依賴於抽象。

結論

老實說,S.O.L.I.D初看起來可能棘手,但只要通過連續使用並遵守其指導方針,它就會變成你和你的程式碼的一部分,可以讓你的程式碼很容易地擴充套件、修改、測試和重構,不出任何問題。

相關文章