电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

解析Java實現設計模式六大原則之里氏替換原則

瀏覽:7日期:2022-08-10 08:30:23
目錄一、什么是里氏替換原則1.1、里氏替換原則定義1.2、里氏替換原則有至少有兩種含義二、使用里氏替換原則的目的三、里氏替換原則與繼承多態之間的關系四、里式替換的規則4.1、子類方法不能違背父類方法對輸入輸出異常的約定4.2、子類方法不能違背父類方法定義的功能4.3、子類必須完全實現父類的抽象方法五、里氏替換原則的作用六、里氏替換原則的實現方法七、案例分析7.1、案例一: 兩數相減7.2、案例二: '幾維鳥不是鳥'八、總結一、什么是里氏替換原則1.1、里氏替換原則定義

里氏替換原則(Liskov Substitution principle)是對子類型的特別定義的. 為什么叫里氏替換原則呢?因為這項原則最早是在1988年,由麻省理工學院的一位姓里的女士(Barbara Liskov)提出來的。

里氏替換原則有兩層定義:

定義1

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。如果S是T的子類,則T的對象可以替換為S的對象,而不會破壞程序。

定義2:

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。所有引用其父類對象方法的地方,都可以透明的替換為其子類對象

這兩種定義方式其實都是一個意思,即:應用程序中任何父類對象出現的地方,我們都可以用其子類的對象來替換,并且可以保證原有程序的邏輯行為和正確性。

1.2、里氏替換原則有至少有兩種含義

1.里氏替換原則是針對繼承而言的,如果繼承是為了實現代碼重用,也就是為了共享方法,那么共享的父類方法就應該保持不變,不能被子類重新定義。子類只能通過新添加方法來擴展功能,父類和子類都可以實例化,而子類繼承的方法和父類是一樣的,父類調用方法的地方,子類也可以調用同一個繼承得來的,邏輯和父類一致的方法,這時用子類對象將父類對象替換掉時,當然邏輯一致,相安無事。

2.如果繼承的目的是為了多態,而多態的前提就是子類覆蓋并重新定義父類的方法,為了符合LSP,我們應該將父類定義為抽象類,并定義抽象方法,讓子類重新定義這些方法,當父類是抽象類時,父類就是不能實例化,所以也不存在可實例化的父類對象在程序里。也就不存在子類替換父類實例(根本不存在父類實例了)時邏輯不一致的可能。

不符合LSP的最常見的情況是,父類和子類都是可實例化的非抽象類,且父類的方法被子類重新定義,這一類的實現繼承會造成父類和子類間的強耦合,也就是實際上并不相關的屬性和方法牽強附會在一起,不利于程序擴展和維護。

二、使用里氏替換原則的目的

采用里氏替換原則就是為了減少繼承帶來的缺點,增強程序的健壯性,版本升級時也可以保持良好的兼容性。即使增加子類,原有的子類也可以繼續運行。

三、里氏替換原則與繼承多態之間的關系

里氏替換原則和繼承多態有關系, 但是他倆并不是一回事. 我們來看看下面的案例

public class Cache { public void set(String key, String value) { }}public class Redis extends Cache { @Override public void set(String key, String value) { }}public class Memcache extends Cache { @Override public void set(String key, String value) { }}public class CacheTest { public static void main(String[] args) {// 父類對象都可以接收子類對象Cache cache = new Cache();cache.set('key123', 'key123');cache = new Redis();cache.set('key123', 'key123');cache = new Memcache();cache.set('key123', 'key123'); }}

通過上面的例子, 可以看出Cache是父類, Redis 和 Memcache是子類, 他們繼承自Cache. 這是繼承和多態的思想. 而且這兩個子類目前為止也都符合里氏替換原則.可以替換父類出現的任何位置,并且原來代碼的邏輯行為不變且正確性也沒有被破壞。看最后的CacheTest類, 我們使用父類的cache可以接收任何一種類型的緩存對象, 包括父類和子類.

但如果我們對Redis中的set方法做了長度校驗

public class Redis extends Cache{ @Override public void set(String key, String value) {if (key == null || key.length() < 10 || key.length() > 100) { System.out.println('key的長度不符合要求'); throw new IllegalArgumentException(key的長度不符合要求);} }}public class CacheTest { public static void main(String[] args) {// 父類對象都可以接收子類對象Cache cache = new Cache();cache.set('key123', 'key123');cache = new Redis();cache.set('key123', 'key123'); }}

如上情況, 如果我們使用父類對象時替換成子類對象, 那么就會拋出異常. 程序的邏輯行為就發生了變化,雖然改造之后的代碼仍然可以通過子類來替換父類 ,但是,從設計思路上來講,Redis子類的設計是不符合里氏替換原則的。

繼承和多態是面向對象語言所提供的一種語法,是代碼實現的思路,而里式替換則是一種思想,一種設計原則,是用來指導繼承關系中子類該如何設計的,子類的設計要保證在替換父類的時候,不改變原有程序的邏輯以及不破壞原有程序的正確性。

四、里式替換的規則

里氏替換原則的核心就是“約定”,父類與子類的約定。里氏替換原則要求子類在進行設計的時候要遵守父類的一些行為約定。這里的行為約定包括:函數所要實現的功能,對輸入、輸出、異常的約定,甚至包括注釋中一些特殊說明等。

4.1、子類方法不能違背父類方法對輸入輸出異常的約定

1. 前置條件不能被加強

前置條件即輸入參數是不能被加強的,就像上面Cache的示例,Redis子類對輸入參數Key的要求進行了加強,此時在調用處替換父類對象為子類對象就可能引發異常。

也就是說,子類對輸入的數據的校驗比父類更加嚴格,那子類的設計就違背了里氏替換原則。

2. 后置條件不能被削弱

后置條件即輸出,假設我們的父類方法約定輸出參數要大于0,調用父類方法的程序根據約定對輸出參數進行了大于0的驗證。而子類在實現的時候卻輸出了小于等于0的值。此時子類的涉及就違背了里氏替換原則

3. 不能違背對異常的約定

在父類中,某個函數約定,只會拋出 ArgumentNullException 異常, 那子類的設計實現中只允許拋出 ArgumentNullException 異常,任何其他異常的拋出,都會導致子類違背里氏替換原則。

4.2、子類方法不能違背父類方法定義的功能

public class Product { private BigDecimal amount; private Calendar createTime; public BigDecimal getAmount() {return amount; } public void setAmount(BigDecimal amount) {this.amount = amount; } public Calendar getCreateTime() {return createTime; } public void setCreateTime(Calendar createTime) {this.createTime = createTime; }} public class ProductSort extends Sort<Product> { public void sortByAmount(List<Product> list) {//根據時間進行排序list.sort((h1, h2)->h1.getCreateTime().compareTo(h2.getCreateTime())); }}

父類中提供的 sortByAmount() 排序函數,是按照金額從小到大來進行排序的,而子類重寫這個 sortByAmount() 排序函數之后,卻是是按照創建日期來進行排序的。那子類的設計就違背里氏替換原則。

實際上對于如何驗證子類設計是否符合里氏替換原則其實有一個小技巧,那就是你可以使用父類的單測來運行子類的代碼,如果不可以正常運行,那么你就要考慮一下自己的設計是否合理了!

4.3、子類必須完全實現父類的抽象方法

如果你設計的子類不能完全實現父類的抽象方法那么你的設計就不滿足里氏替換原則。

// 定義抽象類槍public abstract class AbstractGun{ // 射擊 public abstract void shoot();// 殺人 public abstract void kill();}

比如我們定義了一個抽象的槍類,可以射擊和殺人。無論是步槍還是手槍都可以射擊和殺人,我們可以定義子類來繼承父類

// 定義手槍,步槍,機槍public class Handgun extends AbstractGun{ public void shoot(){ // 手槍射擊 }public void kill(){ // 手槍殺人 }}public class Rifle extends AbstractGun{ public void shoot(){ // 步槍射擊 }public void kill(){ // 步槍殺人 }}

但是如果我們在這個繼承體系內加入一個玩具槍,就會有問題了,因為玩具槍只能射擊,不能殺人。但是很多人寫代碼經常會這么寫。

public class ToyGun extends AbstractGun{ public void shoot(){// 玩具槍射擊 }public void kill(){ // 因為玩具槍不能殺人,就返回空,或者直接throw一個異常出去throw new Exception('我是個玩具槍,驚不驚喜,意不意外,刺不刺激?'); }}

這時,我們如果把使用父類對象的地方替換為子類對象,顯然是會有問題的(士兵上戰場結果發現自己拿的是個玩具)。

而這種情況不僅僅不滿足里氏替換原則,也不滿足接口隔離原則,對于這種場景可以通過 ** 接口隔離+委托** 的方式來解決。

五、里氏替換原則的作用

1.里氏替換原則是實現開閉原則的重要方式之一。

2.它克服了繼承中重寫父類造成的可復用性變差的缺點。

3.它是動作正確性的保證。即類的擴展不會給已有的系統引入新的錯誤,降低了代碼出錯的可能性。

4.加強程序的健壯性,同時變更時可以做到非常好的兼容性,提高程序的維護性、可擴展性,降低需求變更時引入的風險。

盡量不要從可實例化的父類中繼承,而是要使用基于抽象類和接口的繼承。

六、里氏替換原則的實現方法

里氏替換原則通俗來講就是:子類可以擴展父類的功能,但不能改變父類原有的功能。也就是說:子類繼承父類時,除添加新的方法完成新增功能外,盡量不要重寫父類的方法。

根據上述理解,對里氏替換原則的定義可以總結如下:

1.子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法

2.子類中可以增加自己特有的方法

3.當子類的方法重載父類的方法時,方法的前置條件(即方法的輸入參數)要比父類的方法更寬松

4.當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的后置條件(即方法的的輸出/返回值)要比父類的方法更嚴格或相等

通過重寫父類的方法來完成新的功能寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的概率會非常大。

如果程序違背了里氏替換原則,則繼承類的對象在基類出現的地方會出現運行錯誤。這時其修正方法是:取消原來的繼承關系,重新設計它們之間的關系。

關于里氏替換原則的例子,最有名的是“正方形不是長方形”。當然,生活中也有很多類似的例子,例如,企鵝、鴕鳥和幾維鳥從生物學的角度來劃分,它們屬于鳥類;但從類的繼承關系來看,由于它們不能繼承“鳥”會飛的功能,所以它們不能定義成“鳥”的子類。同樣,由于“氣球魚”不會游泳,所以不能定義成“魚”的子類;“玩具炮”炸不了敵人,所以不能定義成“炮”的子類等。

七、案例分析7.1、案例一: 兩數相減

當使用繼承時,遵循里氏替換原則。類B繼承類A時,除添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。

繼承包含這樣一層含義:父類中凡是已經實現好的方法(相對于抽象方法而言),實際上是在設定一系列的規范和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。

繼承作為面向對象三大特性之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,并且父類修改后,所有涉及到子類的功能都有可能會產生故障。

class A{public int func1(int a, int b){return a-b;}} public class Client{public static void main(String[] args){A a = new A();System.out.println('100-50='+a.func1(100, 50));System.out.println('100-80='+a.func1(100, 80));}}

運行結果:

100-50=50100-80=20

后來,我們需要增加一個新的功能:完成兩數相加,然后再與100求和,由類B來負責。即類B需要完成兩個功能:

兩數相減。 兩數相加,然后再加100。

由于類A已經實現了第一個功能,所以類B繼承類A后,只需要再完成第二個功能就可以了,代碼如下:

class B extends A{public int func1(int a, int b){return a+b;}public int func2(int a, int b){return func1(a,b)+100;}} public class Client{public static void main(String[] args){B b = new B();System.out.println('100-50='+b.func1(100, 50));System.out.println('100-80='+b.func1(100, 80));System.out.println('100+20+100='+b.func2(100, 20));}}

類B完成后,運行結果:

100-50=150100-80=180100+20+100=220

我們發現原本運行正常的相減功能發生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法,造成所有運行相減功能的代碼全部調用了類B重寫后的方法,造成原本運行正常的功能出現了錯誤。在本例中,引用基類A完成的功能,換成子類B之后,發生了異常。在實際編程中,我們常常會通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的幾率非常大。如果非要重寫父類的方法,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類,原有的繼承關系去掉,采用依賴、聚合,組合等關系代替。

7.2、案例二: '幾維鳥不是鳥'

需求分析: 鳥通常都是會飛的, 比如燕子每小時120千米, 但是新西蘭的幾維鳥由于翅膀退化不會飛. 假如要設計一個實例,計算這兩種鳥飛行 300 千米要花費的時間。顯然,拿燕子來測試這段代碼,結果正確,能計算出所需要的時間;但拿幾維鳥來測試,結果會發生“除零異常”或是“無窮大”,明顯不符合預期,其類圖如圖 1 所示。

解析Java實現設計模式六大原則之里氏替換原則

源碼如下:

/** * 鳥 */public class Bird { // 飛行的速度 private double flySpeed; public void setFlySpeed(double flySpeed) {this.flySpeed = flySpeed; } public double getFlyTime(double distance) {return distance/flySpeed; }}/** * 燕子 */public class Swallow extends Bird{}/** * 幾維鳥 */public class Kiwi extends Bird { @Override public void setFlySpeed(double flySpeed) {flySpeed = 0; }}/** * 測試飛行耗費時間 */public class BirdTest { public static void main(String[] args) {Bird bird1 = new Swallow();Bird bird2 = new Kiwi();bird1.setFlySpeed(120);bird2.setFlySpeed(120);System.out.println('如果飛行300公里:');try { System.out.println('燕子花費' + bird1.getFlyTime(300) + '小時.'); System.out.println('幾維花費' + bird2.getFlyTime(300) + '小時。');} catch (Exception err) { System.out.println('發生錯誤了!');} }}

運行結果:

如果飛行300公里:燕子花費2.5小時.幾維花費Infinity小時。

程序運行錯誤的原因是:幾維鳥類重寫了鳥類的 setSpeed(double speed) 方法,這違背了里氏替換原則。正確的做法是:取消幾維鳥原來的繼承關系,定義鳥和幾維鳥的更一般的父類,如動物類,它們都有奔跑的能力。幾維鳥的飛行速度雖然為 0,但奔跑速度不為 0,可以計算出其奔跑 300 千米所要花費的時間。其類圖如圖 2 所示。

解析Java實現設計模式六大原則之里氏替換原則

源代碼實現如下

/** * 動物 */public class Animal { private double runSpeed; public double getRunTime(double distance) {return distance/runSpeed; } public void setRunSpeed(double runSpeed) {this.runSpeed = runSpeed; }}/** * 鳥 */public class Bird { // 飛行的速度 private double flySpeed; public void setFlySpeed(double flySpeed) {this.flySpeed = flySpeed; } public double getFlyTime(double distance) {return distance/flySpeed; }}/** * 燕子 */public class Swallow extends Bird {}/** * 幾維鳥 */public class Kiwi extends Animal { @Override public void setRunSpeed(double runSpeed) {super.setRunSpeed(runSpeed); }}/** * 測試飛行耗費時間 */public class BirdTest { public static void main(String[] args) {Bird bird1 = new Swallow();Animal bird2 = new Kiwi();bird1.setFlySpeed(120);bird2.setRunSpeed(110);System.out.println('如果飛行300公里:');try { System.out.println('燕子花費' + bird1.getFlyTime(300) + '小時.'); System.out.println('幾維鳥花費' + bird2.getRunTime(300) + '小時。');} catch (Exception err) { System.out.println('發生錯誤了!');} }}

運行結果

如果飛行300公里:燕子花費2.5小時.幾維鳥花費2.727272727272727小時。

八、總結

面向對象的編程思想中提供了繼承和多態是我們可以很好的實現代碼的復用性和可擴展性,但繼承并非沒有缺點,因為繼承的本身就是具有侵入性的,如果使用不當就會大大增加代碼的耦合性,而降低代碼的靈活性,增加我們的維護成本,然而在實際使用過程中卻往往會出現濫用繼承的現象,而里氏替換原則可以很好的幫助我們在繼承關系中進行父子類的設計。

以上就是解析Java實現設計模式六大原則之里氏替換原則的詳細內容,更多關于Java 設計模式 里氏替換原則的資料請關注好吧啦網其它相關文章!

標簽: Java
相關文章:
主站蜘蛛池模板: 南京交通事故律师-专打交通事故的南京律师 | 传递窗_超净|洁净工作台_高效过滤器-传递窗厂家广州梓净公司 | 对辊式破碎机-对辊制砂机-双辊-双齿辊破碎机-巩义市裕顺机械制造有限公司 | PCB设计,PCB抄板,电路板打样,PCBA加工-深圳市宏力捷电子有限公司 | 中控室大屏幕-上海亿基自动化控制系统工程有限公司 | 爱德华真空泵油/罗茨泵维修,爱发科-比其尔产品供应东莞/杭州/上海等全国各地 | 杭州顺源过滤机械有限公司官网-压滤机_板框压滤机_厢式隔膜压滤机厂家 | 岩棉切条机厂家_玻璃棉裁条机_水泥基保温板设备-廊坊鹏恒机械 | 济南宣传册设计-画册设计_济南莫都品牌设计公司 | 电镀整流器_微弧氧化电源_高频电解电源_微弧氧化设备厂家_深圳开瑞节能 | 广州展台特装搭建商|特装展位设计搭建|展会特装搭建|特装展台制作设计|展览特装公司 | 3d可视化建模_三维展示_产品3d互动数字营销_三维动画制作_3D虚拟商城 【商迪3D】三维展示服务商 广东健伦体育发展有限公司-体育工程配套及销售运动器材的体育用品服务商 | 信阳网站建设专家-信阳时代网联-【信阳网站建设百度推广优质服务提供商】信阳网站建设|信阳网络公司|信阳网络营销推广 | 英思科GTD-3000EX(美国英思科气体检测仪MX4MX6)百科-北京嘉华众信科技有限公司 | 高精度电阻回路测试仪-回路直流电阻测试仪-武汉特高压电力科技有限公司 | LCD3D打印机|教育|桌面|光固化|FDM3D打印机|3D打印设备-广州造维科技有限公司 | 深圳VI设计-画册设计-LOGO设计-包装设计-品牌策划公司-[智睿画册设计公司] | 艾乐贝拉细胞研究中心 | 国家组织工程种子细胞库华南分库 | 大型低温冷却液循环泵-低温水槽冷阱「厂家品牌」京华仪器_京华仪器 | 硅胶制品-硅橡胶制品-东莞硅胶制品厂家-广东帝博科技有限公司 | 北京印刷厂_北京印刷_北京印刷公司_北京印刷厂家_北京东爵盛世印刷有限公司 | 低压载波电能表-单相导轨式电能表-华邦电力科技股份有限公司-智能物联网综合管理平台 | 恒温水槽与水浴锅-上海熙浩实业有限公司 | 天津仓库出租网-天津电商仓库-天津云仓一件代发-【博程云仓】 | 超声波清洗机-超声波清洗设备定制生产厂家 - 深圳市冠博科技实业有限公司 | 胶原检测试剂盒,弹性蛋白检测试剂盒,类克ELISA试剂盒,阿达木单抗ELISA试剂盒-北京群晓科苑生物技术有限公司 | 全温度恒温培养摇床-大容量-立式-远红外二氧化碳培养箱|南荣百科 | 除湿机|工业除湿机|抽湿器|大型地下室车间仓库吊顶防爆除湿机|抽湿烘干房|新风除湿机|调温/降温除湿机|恒温恒湿机|加湿机-杭州川田电器有限公司 | 集装箱标准养护室-集装箱移动式养护室-广州璟业试验仪器有限公司 | CPSE安博会| 液压压力机,液压折弯机,液压剪板机,模锻液压机-鲁南新力机床有限公司 | 搬运设备、起重设备、吊装设备—『龙海起重成套设备』 | 选矿设备,选矿生产线,选矿工艺,选矿技术-昆明昆重矿山机械 | 知名电动蝶阀,电动球阀,气动蝶阀,气动球阀生产厂家|价格透明-【固菲阀门官网】 | 同步带轮_同步带_同步轮_iHF合发齿轮厂家-深圳市合发齿轮机械有限公司 | 衡阳耐适防护科技有限公司——威仕盾焊接防护用品官网/焊工手套/焊接防护服/皮革防护手套 | 喷码机,激光喷码打码机,鸡蛋打码机,手持打码机,自动喷码机,一物一码防伪溯源-恒欣瑞达有限公司 | 识禅_对禅的了解,从这里开始| 光照全温振荡器(智能型)-恒隆仪器| 智能汉显全自动量热仪_微机全自动胶质层指数测定仪-鹤壁市科达仪器仪表有限公司 | 工业铝型材生产厂家_铝合金型材配件批发精加工定制厂商 - 上海岐易铝业 |