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

翻譯自:What’s the deal with the SOLID principles? (part 2)程序員

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

L

在 SOLID 原則中,最具神祕色彩的就是里氏替換原則(Liskov Substitution Principle,簡稱 LSP)了。此原則以 Barbara Liskov 的名字命名,他在 1987年 首次提出了這一原則。里氏替換原則要闡述的內容是:若是對象 A 是對象 B 的子類,或者對象 A 實現了接口 B(本質上講,A 就是 B 的一個實例),那麼咱們應該可以在不作任何特殊處理的狀況下,像使用一個對象 B 或者 B 的一個實例那樣使用對象 A。數組

爲了理清思路,讓咱們看一個關於多個自行車的示例。Bike 基類以下:編程語言

class Bike {
  void pedal() {
    // pedal code
  }
  
  void steer() {
    // steering code
  }
  void handBrakeFront() {
    // hand braking front code
  }
  void handBrakeBack() {
    // hand braking back code
  }
}
複製代碼

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

class MountainBike extends Bike {
  void changeGear() {
    // change gear code
  }
}
複製代碼

MountainBike 類遵循了里氏替換原則,由於它可以被看成一個 Bike 類的對象使用。若是咱們有一個自行車類型數組,並用 BikeMountainBike 的實例對象來填充它,那麼咱們徹底能夠正確無誤地調用 steer()pedal()Bike 基類的全部方法。因此,咱們能夠在不通過特殊處理的狀況下,把 MountainBike 類型的元素看成 Bike 類型的元素來使用。模塊化

如今想象一下,咱們添加了一個名爲 ClassicBike 的類,以下所示:post

class ClassicBike extends Bike {
  void footBrake() {
    // foot braking code
  }
}
複製代碼

這個類表明了一種經典自行車,你能夠經過向後踩踏板來進行制動。這種自行車沒有手剎。基於此,若是咱們有一個 ClassicBike 類型的元素混在了上述的自行車數組中,咱們仍然可以無誤地調用 steerpedal 方法。可是,當咱們嘗試調用 handBrakeFront 或者 handBrakeBack 的時候,問題就暴露出來了。取決於具體的實現,調用這些方法可能致使系統崩潰或者什麼也不會作。咱們能夠經過檢查當前元素是不是 ClassicBike 的實例來解決這個問題:編碼

foreach(var bike in bikes) {
  bike.pedal()
  bike.steer()
  
  if(bike is ClassicBike) {
    bike.footBrake()
  } else {
    bike.handBrakeFront()
    bike.handBrakeBack()
  }
}
複製代碼

如你所見,假如沒有相似上面的類型判斷,咱們就不能再把一個 ClassicBike 的實例看做一個 Bike 實例了。這顯然違背了里氏替換原則。有多種方法能夠解決這個問題,當咱們討論到 SOLID 中的 I 原則時,就會看到一個解決之道。遵循里氏替換原則的一個有趣的後果就是你編寫的代碼將很難不符合開閉原則。spa

【譯者注】原文中,上圖有個標題「There is no spoon」,這句話是電影《黑客帝國》的一句臺詞。操作系統

面向對象編程存在的一個問題就是咱們經常忘記正在打交道的數據,以及處理這些數據和真實世界裏對象的關係。現實生活中的有些事情沒法在代碼中直接創建模型,所以咱們必須牢記:抽象自己並不神奇,底層的數據僅是數據而已(並非一個真正的自行車)。

忽視里氏替換原則可能會讓你遇到各類麻煩。拿 Donald (以前一篇文章 中的一個開發者) 來講,他寫了一個 String 的子類,名叫 SuperSmartString 。這個子類作了全部的事情並覆寫了父類 String 中的一些方法。他的這種編碼方式顯然違背了里氏替換原則。以後,他在他的代碼中全都使用子類 SuperSmartString 的實例,並且還把這些實例視同 String 實例。不久,Donald 就注意到了一些「奇怪」、「神祕」的 bug 開始四處出現。當這些問題出現時,程序員就該開始抱怨之旅了,編程語言、編譯器,編碼平臺、操做系統,甚至是市長和上帝都要跟着挨批評了。這些「神奇的」 bug 其實能夠經過遵照里氏替換原則來避免。就算不是爲了代碼質量,單單是爲了程序員應該頭腦清晰的職業屬性,這個原則也應該受人尊敬。若是你的工做項目稍有複雜,那麼只需少量的 SuperSmartStringsClassicBike 們就能讓你工做不堪忍受。

【譯者注】關於里氏替換原則的相關內容遠不止上文提到的這些,好比覆寫(override)與重載(overload)的區別、子類須要個性化時該怎麼作等等都須要咱們關注。

I

至此,咱們還剩下兩個原則。I 表明的是接口隔離原則(Interface Segregation Principle,簡稱 ISP)。這個很容易理解。它說的是咱們應該保持接口短小,在實現時選擇實現多個小接口而不是龐大的單個接口。我會再次使用自行車的例子,可是此次我用一個 Bike 接口而不是 Bike 基類:

interface Bike {
  void pedal()
  void steer()
  void handBrakeFront()
  void handBrakeBack()
}
複製代碼

MountainBike 類必須實現這個接口的全部方法:

class MountainBike implements Bike {
  override void pedal() {
    // pedal implementation
  }
  
  override void steer() {
    // steer implementation
  }
  
  override void handBrakeFront() {
    // front hand brake implementation
  }
  
  override void handBrakeBack() {
    // back hand brake implementation
  }
  
  void changeGear() {
    // change gear code
  }
}
複製代碼

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

class ClassicBike implements Bike {
  override pedal() {
    // pedal implementation
  }
  
  override steer() {
    // steer implementation
  }
  
  override handBrakeFront() {
    // no code or throw an exception
  }
  
  override handBrakeBack() {
    // no code or throw an exception
  }
  
  void brake() {
    // foot brake code
  }
}
複製代碼

在這個例子中,咱們不得不重寫手剎的兩個方法,儘管不須要它們。正如前所述,咱們打破了里氏替換原則。

比較好的一個作法就是重構這個接口:

interface Bike() {
  void pedal()
  void steer()
}

interface HandBrakeBike {
  void handBrakeFront()
  void handBrakeBack()
}

interface FootBrakeBike {
  void footBrake()
}
複製代碼

MountainBike 類將實現 BikeHandBrakeBike 接口,以下所示:

class MountainBike implements Bike, HandBrakeBike {
  // same code as before
}
複製代碼

ClassicBike 將實現 BikeFootBrakeBike ,以下所示:

class ClassicBike implements Bike, FootBrakeBike {
  override pedal() {
    // pedal implementation
  }
  override steer() {
    // steer implementation
  }
  
  override footBrake() {
    // code that handles foot braking
  }
}
複製代碼

接口隔離原則的優點之一就是咱們能夠在多個對象上組合匹配接口,這提升了咱們代碼的靈活性和模塊化。

咱們也能夠有一個 MultipleGearsBike 接口,在它裏面添加 changeGear() 方法。如今,咱們就能夠構建一個擁有腳剎和換擋功能的自行車了。

此外,咱們的類如今也遵循了里氏替換原則,ClassicBikeMountainBike 都可以看做 Bike 而毫無問題了。如前所述,遵循里氏替換原則也有助於開閉原則的實現。

……

若是你還沒看過第 1 部分的內容,能夠在 這裏 查看。在 第3部分 咱們將探討最後一個 SOLID 原則。若是你喜歡這篇文章,你能夠在咱們的 官方站點 上找到更多信息。

相關文章
相關標籤/搜索