摘要:在我們做系統設計時,經常會設計接口或抽象類,然后由子類來實現抽象方法,這里使用的其實就是里氏替換原則。
1.開閉原則(Open Close Principle/OCP)
定義:一個類、模塊和函數應該對擴展開放,對修改關閉。
開放-封閉原則的意思就是說,你設計的時候,時刻要考慮,盡量讓這個類是足夠好,寫好了就不要去修改了,如果新需求來,我們增加一些類就完事了,原來的代碼能不動則不動。這個原則有兩個特性,一個是說“對于擴展是開放的”,另一個是說“對于更改是封閉的”。面對需求,對程序的改動是通過增加新代碼進行的,而不是更改現有的代碼。這就是“開放-封閉原則”的精神所在
舉例說明什么是開閉原則,以書店銷售書籍為例,其類圖如下:
項目上線,書籍正常銷售,但是我們經常因為各種原因,要打折來銷售書籍,這是一個變化,我們要如何應對這樣一個需求變化呢?
我們有下面三種方法可以解決此問題:
修改接口
在IBook接口中,增加一個方法getOffPrice(),專門用于進行打折處理,所有的實現類實現此方法。但是這樣的一個修改方式,實現類NovelBook要修改,同時IBook接口應該是穩定且可靠,不應該經常發生改變,否則接口作為契約的作用就失去了。因此,此方案否定。
修改實現類
修改NovelBook類的方法,直接在getPrice()方法中實現打折處理。此方法是有問題的,例如我們如果getPrice()方法中只需要讀取書籍的打折前的價格呢?這不是有問題嗎?當然我們也可以再增加getOffPrice()方法,這也是可以實現其需求,但是這就有二個讀取價格的方法,因此,該方案也不是一個最優方案。
通過擴展實現變化
我們可以增加一個子類OffNovelBook,覆寫getPrice方法。此方法修改少,對現有的代碼沒有影響,風險少,是個好辦法(如下圖)。
為什么使用(好處)
可復用性好。
我們可以在軟件完成以后,仍然可以對軟件進行擴展,加入新的功能,非常靈活。因此,這個軟件系統就可以通過不斷地增加新的組件,來滿足不斷變化的需求。如:只變化了一個邏輯,而不涉及其他模塊,比如一個算法是abc,現在需要修改為a+b+c,可以直接通過修改原有類中的方法的方式來完成,前提條件是所有依賴或關聯類都按照相同的邏輯處理。
可維護性好。
由于對于已有的軟件系統的組件,特別是它的抽象底層不去修改,因此,我們不用擔心軟件系統中原有組件的穩定性,這就使變化中的軟件系統有一定的穩定性和延續性。如:一人模塊變化,會對其它的模塊產生影響,特別是一個低層次的模塊變化必然引起高層模塊的變化,因此在通過擴展完成變化。
如何實現
實現開閉原則的關鍵就在于“抽象”。把系統/軟件的所有可能的行為抽象成一個抽象底層,這個抽象底層規定出所有的具體實現必須提供的方法的特征。作為系統設計的抽象層,要預見所有可能的擴展,從而使得在任何擴展情況下,系統的抽象底層不需修改;同時,由于可以從抽象底層導出一個或多個新的具體實現,可以改變系統的行為,因此系統設計對擴展是開放的。抽象是對一組事物的通用描述,沒有具體的實現,也就表示它可以有非常多的可能性,可以跟隨需求的變化而變化。因此,通過接口或抽象類可以約束一組可能變化的行為,并且能夠實現對擴展開放,其包含三層含義:
通過接口或抽象類約束擴散,對擴展進行邊界限定,不允許出現在接口或抽象類中不存在的public方法。
參數類型,引用對象盡量使用接口或抽象類,而不是實現類,這主要是實現里氏替換原則的一個要求。
抽象層盡量保持穩定,一旦確定就不要修改。
里氏替換原則(LSP)、依賴倒轉原則(DIP)、接口隔離原則(ISP)以及抽象類(Abstract Class)、接口(Interface)等等,都可以看作是開閉原則的實現方法。
定義:所有引用基類(父類)的地方必須能透明地使用其子類的對象。通俗講:子類可以擴展父類的功能,但不能改變父類原有的功能。
里氏代換原則意思說,在軟件中將一個基類對象(父類)替換成它的子類對象,程序將不會產生任何錯誤和異常,反過來則不成立,如果一個軟件實體使用的是一個子類對象的話,那么它不一定能夠使用基類對象。里氏代換原則是實現開閉原則的重要方式之一,由于使用基類對象的地方都可以使用子類對象,因此在程序中盡量使用基類類型來對對象進行定義,而在程序運行時再確定其子類類型,用子類對象來替換父類對象。
例如:我喜歡動物,那我一定喜歡狗,因為狗是動物的子類;但是我喜歡狗,不能據此斷定我喜歡動物,因為我并不喜歡老鼠,雖然它也是動物。
為什么使用(好處)
里氏代換原則是實現開閉原則的重要方式之一,優點同開閉原則一樣。
缺點
增加了對象之間的耦合性。因此在系統設計時,遵循里氏替換原則,盡量避免子類重寫父類的方法,可以有效降低代碼出錯的可能性。
實現原則
子類可以實現父類的抽象方法,但是不能覆蓋/重寫父類的非抽象方法。
子類中可以增加自己特有的方法。
當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
舉例逐個講解
子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
在我們做系統設計時,經常會設計接口或抽象類,然后由子類來實現抽象方法,這里使用的其實就是里氏替換原則。子類可以實現父類的抽象方法很好理解,事實上,子類也必須完全實現父類的抽象方法,哪怕寫一個空方法,否則會編譯報錯。里氏替換原則的關鍵點在于不能覆蓋父類的非抽象方法。父類中凡是已經實現好的方法,實際上是在設定一系列的規范和契約,雖然它不強制要求所有的子類必須遵從這些規范,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。如:類C1繼承類C時,可以添加新方法完成新增功能,盡量不要重寫父類C的方法。否則可能帶來難以預料的風險:
public class C { public int func(int a, int b){ return a+b; } } public class C1 extends C{ @Override public int func(int a, int b) { return a-b; } } public class Client{ public static void main(String[] args) { C c = new C1(); System.out.println("2+1=" + c.func(2, 1)); } }
運行結果:2+1=1
上面的運行結果明顯是錯誤的。類C1繼承C,后來需要增加新功能,類C1并沒有新寫一個方法,而是直接重寫了父類C的func方法,違背里氏替換原則,引用父類的地方并不能透明的使用子類的對象,導致運行結果出錯。
子類中可以增加自己特有的方法
在繼承父類屬性和方法的同時,每個子類也都可以有自己的個性,在父類的基礎上擴展自己的功能。前面其實已經提到,當功能擴展時,子類盡量不要重寫父類的方法,而是另寫一個方法,所以對上面的代碼加以更改,使其符合里氏替換原則,代碼如下:
public class C { public int func(int a, int b){ return a+b; } } public class C1 extends C{ public int func2(int a, int b) { return a-b; } } public class Client{ public static void main(String[] args) { C1 c = new C1(); System.out.println("2-1=" + c.func2(2, 1)); } }
運行結果:2-1=1
當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參/入參)要比父類方法的輸入參數更寬松
代碼示例
import java.util.HashMap; public class Father { public void func(HashMap m){ System.out.println("執行父類..."); } } import java.util.Map; public class Son extends Father{ public void func(Map m){//方法的形參比父類的更寬松 System.out.println("執行子類..."); } } import java.util.HashMap; public class Client{ public static void main(String[] args) { Father f = new Son();//引用基類的地方能透明地使用其子類的對象。 HashMap h = new HashMap(); f.func(h); } }
運行結果:執行父類...
注意Son類的func方法前面是不能加@Override注解的,因為否則會編譯提示報錯,因為這并不是重寫(Override),而是重載(Overload),因為方法的輸入參數不同。
當子類的方法實現父類的抽象方法時,方法的后置條件(即方法的返回值)要比父類更嚴格。
代碼示例:
import java.util.Map; public abstract class Father { public abstract Map func(); } import java.util.HashMap; public class Son extends Father{ @Override public HashMap func(){//方法的返回值比父類的更嚴格 HashMap h = new HashMap(); h.put("h", "執行子類..."); return h; } } public class Client{ public static void main(String[] args) { Father f = new Son();//引用基類的地方能透明地使用其子類的對象。 System.out.println(f.func()); } }
執行結果:{h=執行子類...}
持續更新中。。。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76420.html
摘要:單一職責原則開閉原則里氏替換原則依賴倒置原則接口隔離原則迪米特法則組合聚合復用原則單一職責原則高內聚低耦合定義不要存在多于一個導致類變更的原因。建議接口一定要做到單一職責,類的設計盡量做到只有一個原因引起變化。使用繼承時遵循里氏替換原則。 單一職責原則 開閉原則 里氏替換原則 依賴倒置原則 接口隔離原則 迪米特法則 組合/聚合復用原則 單一職責原則(Single Responsi...
摘要:引申意義子類可以擴展父類的功能,但不能改變父類原有的功能。含義當子類的方法實現父類的方法時重寫重載或實現抽象方法,方法的后置條件即方法的輸出返回值要比父類更嚴格或相等。優點約束繼承泛濫,開閉原則的一種體現。降低需求變更時引入的風險。 0x01.開閉原則 定義:一個軟件實體如類,模塊和函數應該對擴展開放,對修改關閉 要點: 當變更發生時,不要直接修改類,而是通過繼承擴展的方式完成變...
摘要:我們今天也來做一個萬能遙控器設計模式適配器模式將一個類的接口轉換成客戶希望的另外一個接口。今天要介紹的仍然是創建型設計模式的一種建造者模式。設計模式的理論知識固然重要,但 計算機程序的思維邏輯 (54) - 剖析 Collections - 設計模式 上節我們提到,類 Collections 中大概有兩類功能,第一類是對容器接口對象進行操作,第二類是返回一個容器接口對象,上節我們介紹了...
摘要:來來來,花分鐘看看的七大新特性,還有代碼樣例。本地是指方法內的變量聲明。從開始,這個正式進入標準庫包。同步請求會阻止當前線程。可喜的是,如果嘗試改變不可變集合,會通過發出警告是在中引入的,增加了三個新方法。 現在Java有多元化的發展趨勢,既有JS又有C++還有C#的影子,不學習那是不行滴。來來來,花5分鐘看看Java9-Java11的七大新特性,還有代碼樣例。Java11 發布了,然...
閱讀 3247·2021-09-22 15:58
閱讀 1717·2019-08-30 14:17
閱讀 1716·2019-08-28 18:05
閱讀 1505·2019-08-26 13:33
閱讀 683·2019-08-26 12:20
閱讀 606·2019-08-26 12:18
閱讀 3192·2019-08-26 11:59
閱讀 1401·2019-08-26 10:36