摘要:什么是里氏替換原則某個對象實例的子類實例應當可以在不影響程序正確性的基礎上替換它們。除了在編程語言層面,在前端實際工作中,你可能會聽到一個叫作的概念,這個概念我認為也是里氏替換原則的一直延伸。
這是理解SOLID原則,關于里氏替換原則為什么提倡我們面向抽象層編程而不是具體實現層,以及為什么這樣可以使代碼更具維護性和復用性。什么是里氏替換原則
Objects should be replaceable with instances of their subtypes without altering the correctness of that program.某個對象實例的子類實例應當可以在不影響程序正確性的基礎上替換它們。
這句話的意思是說,當我們在傳遞一個父抽象的子類型時,你需要保證你不會修改任何關于這個父抽象的行為和狀態語義。
如果你不遵循里氏替換原則,那么你可能會面臨以下問題:
類繼承會變得很混亂,因此奇怪的行為會發生
對于父類的單元測試對于子類是無效的,因此會降低代碼的可測試性和驗證程度
通常打破這條原則的情況發生在修改父類中在其他方法中使用的,與當前子類無關聯的內部或者私有變量。這通常算得上是一種對于類本身的一次潛在攻擊,而且這種攻擊可能是你在不經意間自己發起的,而且不僅在子類中。
反面例子讓我們通過一個反面例子來演示這種修改行為和它所產生的后果。比如,我們有一個關于Store的抽象類和它的實現類BasicStore,這個類會儲存一些消息在內存中,直到儲存的個數超過每個上限。客戶端代碼的實現也很簡單明了,它期望通過調用retrieveMessages就可以獲取到所有儲存的消息。
代碼如下:
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后端時,經常在公司的項目的歷史代碼中發現,很少使用protect和private對類內部的方法和變量做約束,可見當時的編寫者并沒有對類本身的職能有一個清晰的認識,又或者是隨著時間一步步迭代出來的結果。
那么問題來了,一些靜態語言有這些修飾符,但是像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,只談技術,不談人生
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107361.html
這是理解SOLID原則中,關于依賴倒置原則如何幫助我們編寫低耦合和可測試代碼的第一篇文章。 寫在前頭 當我們在讀書,或者在和一些別的開發者聊天的時候,可能會談及或者聽到術語SOILD。在這些討論中,一些人會提及它的重要性,以及一個理想中的系統,應當包含它所包含的5條原則的特性。 我們在每次的工作中,你可能沒有那么多時間思考關于架構這個比較大的概念,或者在有限的時間內或督促下,你也沒有辦法實踐一些好...
摘要:前言本章我們要講解的是五大原則語言實現的第篇,里氏替換原則。因此,違反了里氏替換原則。與行為有關,而不是繼承到現在,我們討論了和繼承上下文在內的里氏替換原則,指示出的面向對象。 前言 本章我們要講解的是S.O.L.I.D五大原則JavaScript語言實現的第3篇,里氏替換原則LSP(The Liskov Substitution Principle )。英文原文:http://fre...
摘要:設計原則梳理,參考核心技術與最佳實踐敏捷開發原則模式與實踐,文章面向對象設計的五大原則設計模式原則單一職責原則定義特性僅有一個引起類變化的原因一個類只承擔一項職責職責變化的原因避免相同的職責分散到不同的類,功能重復問題一個類承擔的職責過多, PHP設計原則梳理,參考《PHP核心技術與最佳實踐》、《敏捷開發原則、模式與實踐》,文章PHP面向對象設計的五大原則、設計模式原則SOLID 單一...
摘要:在開發設計中有一些常用原則或者潛規則,根據筆者的經驗,這里稍微總結一下最最常用的,以饗讀者。是處理復雜性的一個原則。參考六大設計原則里氏替換原則奧卡姆剃刀如有問題可以通過郵件微信聯系我。 在開發設計中有一些常用原則或者潛規則,根據筆者的經驗,這里稍微總結一下最最常用的,以饗讀者。 DRY 這里的DRY是Do Not Repeat Yourself的縮寫。具體解釋參見 ,嚴謹的定義是 E...
閱讀 1125·2021-11-24 10:21
閱讀 2561·2021-11-19 11:35
閱讀 1662·2019-08-30 15:55
閱讀 1293·2019-08-30 15:54
閱讀 1192·2019-08-30 15:53
閱讀 3498·2019-08-29 17:21
閱讀 3308·2019-08-29 16:12
閱讀 3412·2019-08-29 15:23