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

gavinliu6發表於2018-12-22

翻譯自: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。

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

為了理清思路,讓我們看一個關於多個自行車的示例。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 (譯者注:山地車有通過齒輪的機械原理調整檔位的特性):

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

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

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

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 原則時,就會看到一個解決之道。遵循里氏替換原則的一個有趣的後果就是你編寫的程式碼將很難不符合開閉原則。

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

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

物件導向程式設計存在的一個問題就是我們常常忘記正在打交道的資料,以及處理這些資料和真實世界裡物件的關係。現實生活中的有些事情無法在程式碼中直接建立模型,因此我們必須牢記:抽象本身並不神奇,底層的資料僅是資料而已(並不是一個真正的自行車)。

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

忽視里氏替換原則可能會讓你遇到各種麻煩。拿 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
  }
}
複製程式碼

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

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

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

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() 方法。現在,我們就可以構建一個擁有腳剎和換擋功能的自行車了。

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

此外,我們的類現在也遵循了里氏替換原則,ClassicBikeMountainBike 都能夠看作 Bike 而毫無問題了。如前所述,遵循里氏替換原則也有助於開閉原則的實現。

……

如果你還沒看過第 1 部分的內容,可以在 這裡 檢視。在 第3部分 我們將探討最後一個 SOLID 原則。如果你喜歡這篇文章,你可以在我們的 官方站點 上找到更多資訊。

相關文章