設(shè)計模式原則之里氏替換原則

定義

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

所有引用基類的地方必須透明的使用其子類的對象。

定義明確的說,只要父類能出現(xiàn)的地方子類也可以出現(xiàn),而且替換為子類不會產(chǎn)生任何錯誤或異常,但是反過來就不行,有子類出現(xiàn)的地方,父類未必就能適應(yīng)。


繼承

優(yōu)點

代碼共享,減少創(chuàng)建類的工作量,每個子類都擁有父類的方法和屬性

提高代碼的重用性

子類可以形似父類,但是又異于父類。

提高代碼的可擴(kuò)展性,實現(xiàn)父類的方法就可以了。許多開源框架的擴(kuò)展接口都是通過繼承父類來完成。

提高產(chǎn)品或項目的開放性

缺點

繼承是侵入性的,只要繼承,就必須擁有父類的所有方法和屬性

降低了代碼的靈活性,子類必須擁有父類的屬性和方法,讓子類有了一些約束

增加了耦合性,當(dāng)父類的常量,變量和方法被修改了,需要考慮子類的修改,這種修改可能帶來非常糟糕的結(jié)果,要重構(gòu)大量的代碼。

從整體來看,利大于弊。怎么才能讓利的因素發(fā)揮最大的作用,同時減少弊的影響?解決方案是引入里氏替換原則。


里氏替換原則的規(guī)范

1.子類必須完全實現(xiàn)父類的方法

我們在做系統(tǒng)設(shè)計時,經(jīng)常會定義一個接口或抽象類,然后編碼實現(xiàn),調(diào)用類則直接傳入接口或抽象類,其實這已經(jīng)使用了里氏替換原則。

注意:?

如果子類不能完整地實現(xiàn)父類的方法,或者父類的一些方法在子類中已經(jīng)發(fā)生畸變,則建議斷開繼承關(guān)系,采用依賴,聚集,組合等關(guān)系代替繼承。

2.子類可以有自己的個性

子類當(dāng)然可以有自己的行為和外觀,也就是方法和屬性。但是里氏替換原則可以正著用,但是不能反著用。在子類出現(xiàn)的地方,父類未必就可以勝任。

下面的兩條不適合ios,ios 沒辦法區(qū)分參數(shù)類型是放大還是縮小,不檢測參數(shù)類型。

3.覆蓋或?qū)崿F(xiàn)父類的方法時輸入?yún)?shù)可以被放大

4.覆蓋或?qū)崿F(xiàn)父類的方法時輸出結(jié)果可以被縮小


場景模擬

我們以士兵射擊為例。士兵設(shè)計可以使用很多槍步槍,手槍等等


士兵射擊UML圖

士兵設(shè)計UML圖

簡單代碼

@protocol Gun<NSObject>

-(void)shoot;

@end

#import "Gun.h"

@interface Solider :

?NSObject-(void)setGun:(id<Gun>) gun;

-(void)killEnemy;

@end

#import "Solider.h"

@interface Solider()

@property (nonatomic,strong) idsoliderGun;

@end

@implementation Solider

-(void)setGun:(id<Gun>)gun{

? ? self.soliderGun = gun;

}

-(void)killEnemy{

? ? [self.soliderGun shoot];

}

@end

#import "Gun.h"

@interface HandGun : NSObject<Gun>

@end

#import "HandGun.h"

@implementation HandGun

-(void)shoot{

? ? NSLog(@"手槍射擊");

}

@end

#import "Gun.h"

@interface Rifle : NSObject<Gun>

@end

#import "Rifle.h"

@implementation Rifle

-(void)shoot{

? ? NSLog(@"步槍射擊");

}

@end

#import "Gun.h"

@interface MachineGun : NSObject<Gun>

@end

#import "MachineGun.h"

@implementation MachineGun

-(void)shoot{

? ? NSLog(@"機(jī)關(guān)槍射擊");

}

@end

測試代碼

Solider * solider=[[Solider alloc]init];

id<Gun> gun=[HandGun new];

? [solider setGun:gun];

[solider killEnemy];

gun=[Rifle new];

[solider setGun:gun];

[solider killEnemy];

gun=[MachineGun new];

[solider setGun:gun];

[solider killEnemy];

結(jié)果

2018-04-03 15:50:37.308121+0800 設(shè)計模式原則[54809:5684654] 手槍射擊

2018-04-03 15:50:37.308331+0800 設(shè)計模式原則[54809:5684654] 步槍射擊

2018-04-03 15:50:37.308615+0800 設(shè)計模式原則[54809:5684654] 機(jī)關(guān)槍射擊

上面這個實現(xiàn)就是也就是開閉原則。


場景模擬變更

我們用步槍射擊的時候,其實步槍有很多種,有AK,AUG狙擊槍。但是當(dāng)我們用狙擊步槍的時候我們需要打開瞄準(zhǔn)鏡才能射擊。


場景變更UML 圖

士兵設(shè)計場景擴(kuò)展UML圖

場景變更代碼

#import "Rifle.h"

@interface AUG : Rifle

-(void)zoomOut;

@end

#import "AUG.h"

@implementation AUG

-(void)zoomOut{

? ? NSLog(@"打開放大鏡");

}

-(void)shoot{

? ? NSLog(@"使用狙擊槍射擊");

}

@end

solider 增加

-(void)killEnemy:(AUG*)aug;

-(void)killEnemy:(AUG*)aug{

? ? [aug zoomOut];

? ? [aug shoot];

}

測試代碼

AUG * aug = [AUG new];

? ? [solider killEnemy:aug];

結(jié)果

2018-04-03 16:13:57.310796+0800 設(shè)計模式原則[60744:5711673] 打開放大鏡

2018-04-03 16:13:57.311321+0800 設(shè)計模式原則[60744:5711673] 使用狙擊槍射擊

當(dāng)我們給-(void)killEnemy:(AUG*)aug函數(shù)傳入一個?MachineGun 對象的時候回發(fā)生崩潰

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MachineGun zoomOut]: unrecognized selector sent to instance 0x60000000a6a0'

從這里可以看出里氏替換原則的缺點了。不可以向下兼容

這證明了里氏替換原則可以正著用,但是不能反著用。在子類出現(xiàn)的地方,父類未必就可以勝任。


里氏替換原則經(jīng)驗

在項目中,采用里氏替換原則時,盡量避免子類的“個性”,一旦子類有了“個性”,這個子類和父類之間的關(guān)系就難調(diào)和,把子類當(dāng)做父類使用,子類的“個性”被抺殺了,把子類單獨作為一個業(yè)務(wù)來使用,則會讓代碼間的耦合關(guān)系變得撲朔迷離–缺乏類替換的標(biāo)準(zhǔn)。(別人的經(jīng)驗)

參考博客

六大設(shè)計原則之里氏替換原則

源代碼地址

下一篇博客

設(shè)計模式原則之依賴倒轉(zhuǎn)原則

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容