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