讀懂 SOLID 的「里氏替換」原則

這是理解 SOLID原則,關於 里氏替換原則爲何提倡咱們面向抽象層編程而不是具體實現層,以及爲何這樣可使代碼更具維護性和複用性。

什麼是里氏替換原則

Objects should be replaceable with instances of their subtypes without altering the correctness of that program.

某個對象實例的子類實例應當能夠在不影響程序正確性的基礎上替換它們。javascript

這句話的意思是說,當咱們在傳遞一個父抽象的子類型時,你須要保證你不會修改任何關於這個父抽象的行爲和狀態語義。前端

若是你不遵循里氏替換原則,那麼你可能會面臨如下問題:java

  • 類繼承會變得很混亂,所以奇怪的行爲會發生
  • 對於父類的單元測試對於子類是無效的,所以會下降代碼的可測試性和驗證程度

一般打破這條原則的狀況發生在修改父類中在其餘方法中使用的,與當前子類無關聯的內部或者私有變量。這一般算得上是一種對於類自己的一次潛在攻擊,並且這種攻擊多是你在不經意間本身發起的,並且不只在子類中。程序員

反面例子

讓咱們經過一個反面例子來演示這種修改行爲和它所產生的後果。好比,咱們有一個關於Store的抽象類和它的實現類BasicStore,這個類會儲存一些消息在內存中,直到儲存的個數超過每一個上限。客戶端代碼的實現也很簡單明瞭,它指望經過調用retrieveMessages就能夠獲取到全部儲存的消息。typescript

代碼以下:編程

interface Store {
    store(message: string);
    retrieveMessages(): string[];
}

const STORE_LIMIT = 5;

class BasicStore implements Store {
   protected stash: string[] = [];
   protected storeLimit: number = STORE_LIMIT;
  
   store(message: string) {
     if (this.storeLimit === this.stash.length) {
         this.makeMoreRoomForStore();
      }
      this.stash.push(message);
    }
  
    retrieveMessages(): string[] {
      return this.stash;
    }

    makeMoreRoomForStore(): void {
       this.storeLimit += 5;
    }
}

以後經過繼承BasicStore,咱們又建立了一個新的RotatingStore實現類,以下:後端

class RotatingStore extends BasicStore {
    makeMoreRoomForStore() {
        this.stash = this.stash.slice(1);
    }
}

注意RotatingStore中覆蓋父類makeMoreRoomForStore方法的代碼以及它是如何隱蔽地改變了父類BasicStore關於stash的狀態語義的。它不只修改了stash變量,還銷燬了在程序進程中已儲存的消息已爲未來的消息提供額外的空間。前端框架

在使用RotatingStore的過程當中,咱們會遇到一些奇怪的現象,這正式因爲RotatingStore自己產生的,以下:框架

const st: Store = new RotatingStore()

st.store("hello")
st.store("world")
st.store("how")
st.store("are")
st.store("you")
st.store("today")
st.store("sir?")

st.retrieveMessages() // 一些消息丟失了

一些消息會無端消失,當前這個類的表現邏輯與全部消息都可以被取出的基本需求不一致。編程語言

如何實踐里氏替換原則

爲了不這種奇怪現象的發生,里氏替換原則推薦咱們經過在子類中調用父類的公有方法來獲取一些內部狀態變量,而不是直接使用它。這樣咱們就能夠保證父類抽象中正確的狀態語義,從而避免了反作用和非法的狀態轉變。

它也推薦咱們應當儘量的使基本抽象保持簡單和最小化,由於對於子類來講,有助於提供父類的擴展性。若是一個父類是比較複雜的,那麼子類在覆蓋它的時候,在不影響父類狀態語義的狀況下進行擴展絕非易事。

對於內部系統作可行的後置條件檢查也是一個不錯的方式,這種檢查一般會驗證是否子類會攪亂一些關鍵代碼的運行路徑(譯者注:也能夠理解爲狀態語義),可是我自己對這個實踐並無太多的經驗,因此沒法給予具體的例子。

代碼評論也能夠必定程度上給予好的幫助。當你在開發一些你可能無心間作出一些對已有系統的破壞,可是你的同事可能會很容易地發現這些(當局者迷旁觀者清)。軟件設計保持一致性是一件十分重要的事情,所以應當儘早、儘量多地查明那些對對象繼承鏈做出潛在修改的代碼。

最後,在單一職責原則中,咱們曾說起,考慮使用組合模式來替換繼承模式

總結

正如你所看到的,在開發軟件時,咱們每每須要額外花一些努力和精力來使它變得更好。將這些原則牢記於心,理解它們所存在的意義以及它們想要解決的問題,這樣會使你的工做變得更加容易、更具條理性,可是同時記住,這並非一件容易的事,相反,你應當在構思軟件時,花至關多的事件思考如何更好地實踐這些原則。

試着讓本身設計的軟件系統具有可適應性,這種適應性能夠抵禦各類不利的變化以及潛在的錯誤,這樣天然而然地可使你少加班和早回家(譯者注:看來加班是每一個程序員都要面臨的問題啊)

譯者注

這是SOLID原則中我所接觸和了解較少的一個原則,但通過仔細思考後,發現其實咱們仍是常常會在實際工做中運用它的。

在許多面向相對的編程語言中,關於對象的繼承機制中,都會提供一些內部變量和狀態的修飾符,好比public(公有)protect(保護)private(私有),關於這些修飾符自己的異同這裏再也不贅述,我想說的是,這些修飾符存在必然有它存在的意義,必定要在實際工做中,使用它們。以前作java後端時,常常在公司的項目的歷史代碼中發現,不多使用protectprivate對類內部的方法和變量作約束,可見當時的編寫者並無對類自己的職能有一個清晰的認識,又或者是隨着時間一步步迭代出來的結果。

那麼問題來了,一些靜態語言有這些修飾符,可是像javascript這種鴨子類型語言怎麼辦呢?其實沒有必要擔憂,最先開始學前端的時候,這個問題我就問過本身無數次,javascript雖然沒有這些修飾符,可是咱們能夠經過別的方式來達到相似的效果,或者使用typescript

除了在編程語言層面,在前端實際工做中,你可能會聽到一個叫做immutable的概念,這個概念我認爲也是里氏替換原則的一直延伸。由於當前的前端框架通常提倡的理念均是f(state) => view,即數據狀態表明視圖,而數據狀態自己因爲javascript動態語言的特性,很容易會在不經意間被修改,一旦存在這種修改,視圖中便會產生一些意想不到的問題,所以immutable函數式的概念纔會在前段時間火起來。

寫在最後

通過這五篇文章,咱們來分別總結一下這五條基本原則以及它們帶來的好處:

  • 單一職責原則:提升代碼實現層的內聚度,下降實現單元彼此之間的耦合度
  • 開閉原則:提升代碼實現層的可擴展性,提升面臨改變的可適應性,下降修改代碼的冗餘度
  • 里氏替換原則:提升代碼抽象層的可維護性,提升實現層代碼與抽象層的一致性
  • 接口隔離原則:提升代碼抽象層的內聚度,下降代碼實現層與抽象層的耦合度,下降代碼實現層的冗餘度
  • 依賴倒置原則:下降代碼實現層由依賴關係產生的耦合度,提升代碼實現層的可測試性

能夠注意到我這裏刻意使用了下降/提升 + 實現層/抽象層 + 特性/程度(耦合度、內聚度、擴展性、冗餘度、可維護性,可測試性)這樣的句式,之因此這麼作是由於在軟件工做中,咱們理想中的軟件應當具有的特色是, 高內聚、低耦合、可擴展、少冗餘、可維護、易於測試,而這五個原則也按正確的方向,將咱們的軟件系統向咱們理想中的標準推動。

爲了便於對比,特別繪製了下面的表格,但願你們從真正意義上作到將這些原則牢記於心,並付諸於行。

原則 耦合度 內聚度 擴展性 冗餘度 維護性 測試性 適應性 一致性
單一職責原則 - + o o + + o o
開閉原則 o o + - + o + o
里氏替換原則 - o o o + o o +
接口隔離原則 - + o - o o + o
依賴倒置原則 - o o - o + + o

Note: +表明增長, -表明下降, o表明持平

關注公衆號 全棧101,只談技術,不談人生

clipboard.png

相關文章
相關標籤/搜索