什麼是SOLID原則(第2部分)

2021-09-13 01:15:52 字數 4471 閱讀 3715

翻譯自:

what』s the deal with the solid principles? (part 2)

在文章的 第1部分,我們主要討論了前兩個 solid 原則,它們分別是單一職責原則和開閉原則。在這一部分,我們將按照首字母縮略詞中的順序來處理接下來的兩個原則。讓我們啟程吧!

在 solid 原則中,最具神秘色彩的就是黎克特制替換原則(liskov substitution principle,簡稱 lsp)了。此原則以 barbara liskov 的名字命名,他在 2023年 首次提出了這一原則。黎克特制替換原則要闡述的內容是:如果物件 a 是物件 b 的子類,或者物件 a 實現了介面 b(本質上講,a 就是 b 的乙個例項),那麼我們應該能夠在不做任何特殊處理的情況下,像使用乙個物件 b 或者 b 的乙個例項那樣使用物件 a。

為了理清思路,讓我們看乙個關於多個自行車的示例。bike基類如下:

class bike 

void steer()

void handbrakefront()

void handbrakeback()

}

山地自行車類mountainbike繼承自基類bike(譯者注:山地車有通過齒輪的機械原理調整檔位的特性):

class mountainbike extends bike 

}

mountainbike類遵循了黎克特制替換原則,因為它能夠被當作乙個bike類的物件使用。如果我們有乙個自行車型別陣列,並用bikemountainbike的例項物件來填充它,那麼我們完全可以正確無誤地呼叫steer()pedal()bike基類的所有方法。所以,我們可以在不經過特殊處理的情況下,把mountainbike型別的元素當作bike型別的元素來使用。

現在想象一下,我們新增了乙個名為classicbike的類,如下所示:

class classicbike extends bike 

}

這個類代表了一種經典自行車,你可以通過向後踩踏板來進行制動。這種自行車沒有手剎。基於此,如果我們有乙個classicbike型別的元素混在了上述的自行車陣列中,我們仍然能夠無誤地呼叫steerpedal方法。但是,當我們嘗試呼叫handbrakefront或者handbrakeback的時候,問題就暴露出來了。取決於具體的實現,呼叫這些方法可能導致系統崩潰或者什麼也不會做。我們可以通過檢查當前元素是否是classicbike的例項來解決這個問題:

foreach(var bike in bikes)  else 

}

如你所見,假如沒有類似上面的型別判斷,我們就不能再把乙個classicbike的例項看作乙個bike例項了。這顯然違背了黎克特制替換原則。有多種方法可以解決這個問題,當我們討論到 solid 中的 i 原則時,就會看到乙個解決之道。遵循黎克特制替換原則的乙個有趣的後果就是你編寫的**將很難不符合開閉原則。

【譯者注】原文中,上圖有個標題「there is no spoon」,這句話是電影《黑客帝國》的一句台詞。
物件導向程式設計存在的乙個問題就是我們常常忘記正在打交道的資料,以及處理這些資料和真實世界裡物件的關係。現實生活中的有些事情無法在**中直接建立模型,因此我們必須牢記:抽象本身並不神奇,底層的資料僅是資料而已(並不是乙個真正的自行車)。

忽視黎克特制替換原則可能會讓你遇到各種麻煩。拿 donald (之前一篇文章 中的乙個開發者) 來說,他寫了乙個string的子類,名叫supersmartstring。這個子類做了所有的事情並覆寫了父類string中的一些方法。他的這種編碼方式顯然違背了黎克特制替換原則。之後,他在他的**中全都使用子類supersmartstring的例項,而且還把這些例項視同string例項。不久,donald 就注意到了一些「奇怪」、「神秘」的 bug 開始四處出現。當這些問題出現時,程式設計師就該開始抱怨之旅了,程式語言、編譯器,編碼平台、作業系統,甚至是市長和上帝都要跟著挨批評了。這些「神奇的」 bug 其實可以通過遵守黎克特制替換原則來避免。就算不是為了**質量,單單是為了程式設計師應該頭腦清晰的職業屬性,這個原則也應該受人尊敬。如果你的工作專案稍有複雜,那麼只需少許的supersmartstringsclassicbike們就能讓你工作不堪忍受。

至此,我們還剩下兩個原則。i 代表的是介面隔離原則(inte***ce segregation principle,簡稱 isp)。這個很容易理解。它說的是我們應該保持介面短小,在實現時選擇實現多個小介面而不是龐大的單個介面。我會再次使用自行車的例子,但是這次我用乙個bike介面而不是bike基類:

inte***ce bike
mountainbike類必須實現這個介面的所有方法:

class mountainbike implements bike 

override void steer()

override void handbrakefront()

override void handbrakeback()

void changegear()

}

目前尚好。對於有問題的帶有腳剎功能的classicbike類,我們可以採用下面這種笨拙的實現:

class classicbike implements bike 

override steer()

override handbrakefront()

override handbrakeback()

void brake()

}

在這個例子中,我們不得不重寫手剎的兩個方法,儘管不需要它們。正如前所述,我們打破了黎克特制替換原則。

比較好的乙個做法就是重構這個介面:

inte***ce bike() 

inte***ce handbrakebike

inte***ce footbrakebike

mountainbike類將實現bikehandbrakebike介面,如下所示:

class mountainbike implements bike, handbrakebike
classicbike將實現bikefootbrakebike,如下所示:

class classicbike implements bike, footbrakebike 

override steer()

override footbrake()

}

介面隔離原則的優勢之一就是我們可以在多個物件上組合匹配介面,這提高了我們**的靈活性和模組化。

我們也可以有乙個multiplegearsbike介面,在它裡面新增changegear()方法。現在,我們就可以構建乙個擁有腳剎和換擋功能的自行車了。

此外,我們的類現在也遵循了黎克特制替換原則,classicbikemountainbike都能夠看作bike而毫無問題了。如前所述,遵循黎克特制替換原則也有助於開閉原則的實現。

練習2部分題解

問題 g 汽水瓶 時間限制 1 sec 記憶體限制 128 mb提交 93 解決 45 201501010119 提交狀態討論版 題目描述 有這樣一道智力題 某商店規定 三個空汽水瓶可以換一瓶汽水。小張手上有十個空汽水瓶,她最多可以換多少瓶汽水喝?答案是5瓶,方法如下 先用9個空瓶子換3瓶汽水,喝掉...

重構練習 大二作業 第2部分

現在開始修改資料結構。當初我在寫 的時候不知道抽了什麼風,過載了一大堆比較函式,還都是友元函式。friend bool operator bigamount ba1,bigamount ba2 friend bool operator long long ba1,bigamount ba2 frie...

精確獲採樣式屬性(第2部分)

繼續上一部分,我們要看一下顏色。火狐好像不管三七二十一都會轉變為rgb格式,不過我們通常比較習慣的是hex格式。這就用到以下兩函式。var rgb2hex function rgb var tohex function x 我們用正規表示式在檢測其是否為rgb格式,是就用rgb2hex來轉換它。但如...