單一職責(zé)原則,開閉原則,依賴倒置原則(面向接口編程),里氏替換原則,接口隔離原則。
面相對(duì)象設(shè)計(jì)的概念大家也都知道,它的設(shè)計(jì)目標(biāo)就是希望軟件系統(tǒng)能做到以下幾點(diǎn):
- 可擴(kuò)展:新特性能夠很容易的添加到現(xiàn)有系統(tǒng)中,不會(huì)影響原本的東西
- 可修改:當(dāng)修改某一部分的代碼時(shí),不會(huì)影響到其它不相關(guān)的部分
- 可替代:將系統(tǒng)中某部分的代碼用其它有相同接口的類替換時(shí),不會(huì)影響到現(xiàn)有系統(tǒng)
這幾個(gè)可以用來(lái)檢測(cè)我們的軟件系統(tǒng)是不是設(shè)計(jì)得合理,而如何設(shè)計(jì)出易于維護(hù)和擴(kuò)展的軟件系統(tǒng)是有設(shè)計(jì)原則可以遵循指導(dǎo)的,Robert C. Martin提出了面相對(duì)象設(shè)計(jì)的五個(gè)基本原則(SOLID):
- S-單一職責(zé)原則
- O-開放關(guān)閉原則
- L-里氏替換原則
- I-接口隔離原則
- D-依賴倒置原則
單一職責(zé)原則:Single Responsibility Principle
一個(gè)類有且僅有一個(gè)職責(zé),只有一個(gè)引起它變化的原因。
簡(jiǎn)單來(lái)說(shuō)一個(gè)類只做好一件事就行,不去管跟自己不相干的,狗拿耗子多管閑事,其核心就是解耦以及高內(nèi)聚。這個(gè)原則看著很簡(jiǎn)單,我們?cè)趯懘a的時(shí)候即便不知道這個(gè)原則也會(huì)往這個(gè)方向靠攏,寫出功能相對(duì)單一的類,不過(guò)這個(gè)原則很容易違背,因?yàn)榭赡苡捎谀撤N原因,原來(lái)功能單一的類需要被細(xì)化成顆粒更小的職責(zé)1跟職責(zé)2,所以在每次迭代過(guò)程中可能需要重新梳理重構(gòu)之前編寫的代碼,將不同的職責(zé)封裝到不同的類或者模塊中。
舉個(gè)栗子:
@interface DataTransfer : NSObject
-(void)upload:(NSData *)data; //上傳數(shù)據(jù)
-(void)download(NSString*)url; //根據(jù)URL下載東西
@end
DataTransfer包含上傳跟下載功能,仔細(xì)考慮可以發(fā)現(xiàn)這相當(dāng)于實(shí)現(xiàn)了兩個(gè)功能,一個(gè)負(fù)責(zé)上傳的相關(guān)邏輯,另一個(gè)負(fù)責(zé)下載的邏輯,而這個(gè)兩個(gè)功能相對(duì)對(duì)立,當(dāng)有一個(gè)功能改變的時(shí)候,比如我們之前是使用AFNetworking,現(xiàn)在想換成其它第三方或者nsurlconnection來(lái)實(shí)現(xiàn)上傳跟下載:
- 上傳方式變更,導(dǎo)致DataTransfer變更
- 下載方式變更,導(dǎo)致 DataTransfer變更
這就違反了單一職責(zé)的原則,所以需要將不同的功能拆解成兩個(gè)不同的類,來(lái)負(fù)責(zé)各自的職責(zé),不過(guò)這個(gè)拆的粒度可能因人而已,有時(shí)候并不需要拆的過(guò)細(xì),不要成了為設(shè)計(jì)而設(shè)計(jì)。

??在我們項(xiàng)目中經(jīng)??吹胶芏噙`反這條原則的代碼,而且違反的比較明顯,許多類都是豐富功能的超級(jí)集合,整個(gè)類變得臃腫難以理解,這時(shí)候就需要我們有意識(shí)地去重構(gòu)了。
開放關(guān)閉原則:Open Closed Principle
開閉原則的定義是說(shuō)一個(gè)軟件實(shí)體如類,模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放,而對(duì)修改關(guān)閉,具體來(lái)說(shuō)就是你應(yīng)該通過(guò)擴(kuò)展來(lái)實(shí)現(xiàn)變化,而不是通過(guò)修改原有的代碼來(lái)實(shí)現(xiàn)變化,該原則是面相對(duì)象設(shè)計(jì)最基本的原則。
之前說(shuō)過(guò)在項(xiàng)目中每當(dāng)需求需改的時(shí)候經(jīng)常需要對(duì)代碼有很大的改動(dòng),很大程度上就是因?yàn)槲覀儗?duì)這個(gè)原則理解的不夠透徹。
??開閉原則的關(guān)鍵在于抽象,我們需要抽象出那些不會(huì)變化或者基本不變的東西,這部分東西相對(duì)穩(wěn)定,這也就是對(duì)修改關(guān)閉的地方(這并不意味著不可以再修改),而對(duì)于那些容易變化的部分我們也對(duì)其封裝,但是這部分是可以動(dòng)態(tài)修改的,這也就是對(duì)擴(kuò)展開發(fā)的地方,比如設(shè)計(jì)模式中的策略模式和模板模式就是在實(shí)現(xiàn)這個(gè)原則(現(xiàn)在應(yīng)該對(duì)模式有更感性的認(rèn)識(shí)了吧~)。
舉個(gè)例子:我們需要保存對(duì)象到數(shù)據(jù)庫(kù)當(dāng)中,其中有個(gè)類似save()的保存方法,這部分應(yīng)該是不變的,接口相對(duì)穩(wěn)定,而具體保存的實(shí)現(xiàn)卻有可能不同,我們現(xiàn)在可能是保存在Sqlite數(shù)據(jù)庫(kù)中,假如以后如果想保存到一個(gè)自己實(shí)現(xiàn)的數(shù)據(jù)庫(kù)中時(shí),我們只需要實(shí)現(xiàn)一個(gè)擁有同樣接口的擴(kuò)展類添加進(jìn)去即可,這就是對(duì)擴(kuò)展開放,不會(huì)對(duì)之前的代碼造成任何影響,就可以實(shí)現(xiàn)保存到新數(shù)據(jù)庫(kù)的功能,保證了系統(tǒng)的穩(wěn)定性。

實(shí)現(xiàn)開閉原則的指導(dǎo)思想就是:
- 抽象出相對(duì)穩(wěn)定的接口,這部分應(yīng)該不改動(dòng)或者很少改動(dòng)
- 封裝變化
不過(guò)在軟件開發(fā)過(guò)程中,要一開始就完全按照開閉原則來(lái)可能比較困難,更多的情況是在不斷的迭代重構(gòu)過(guò)程中去改進(jìn),在可預(yù)見的變化范圍內(nèi)去做設(shè)計(jì)。
里氏替代原則:Liskov Substitution Principle
該原則的定義:所有引用基類的地方必須能透明地使用其子類的對(duì)象。簡(jiǎn)單來(lái)說(shuō),所有使用基類代碼的地方,如果換成子類對(duì)象的時(shí)候還能夠正常運(yùn)行,則滿足這個(gè)原則,否則就是繼承關(guān)系有問(wèn)題,應(yīng)該廢除兩者的繼承關(guān)系,這個(gè)原則可以用來(lái)判斷我們的對(duì)象繼承關(guān)系是否合理。
比如有一個(gè)鯨魚的類,我們讓鯨魚繼承于魚類,然后魚類有個(gè)呼吸的功能:

然后在水里的時(shí)候,魚能夠進(jìn)行呼吸:
if(isInwater){
//在水中了,開始呼吸
fish.breath();
}
當(dāng)我們把鯨魚這個(gè)子對(duì)象替換原來(lái)的基類魚對(duì)象,鯨魚在水里開始呼吸,這時(shí)問(wèn)題就出現(xiàn)了,鯨魚是哺乳動(dòng)物,在水里呼吸是沒法呼吸的,一直在水里就GG思密達(dá)了,所以這違反了該原則,我們就可以判斷鯨魚繼承于魚類不合理,需要去重新設(shè)計(jì)。
??通常在設(shè)計(jì)的時(shí)候,我們都會(huì)優(yōu)先采用組合而不是繼承,因?yàn)槔^承雖然減少了代碼,提高了代碼的重用性,但是父類跟子類會(huì)有很強(qiáng)的耦合性,破壞了封裝。
接口隔離原則:Interface Segregation Principle
該原則的定義:不能強(qiáng)迫用戶去依賴那些他們不使用的接口。
簡(jiǎn)單來(lái)說(shuō)就是客戶端需要什么接口,就提供給它什么樣的接口,其它多余的接口就不要提供,不要讓接口變得臃腫,否則當(dāng)對(duì)象一個(gè)沒有使用的方法被改變了,這個(gè)對(duì)象也將會(huì)受到影響。接口的設(shè)計(jì)應(yīng)該遵循最小接口原則,其實(shí)這也是高內(nèi)聚的一種表現(xiàn),換句話說(shuō),使用多個(gè)功能單一、高內(nèi)聚的接口總比使用一個(gè)龐大的接口要好。
??舉個(gè)簡(jiǎn)單的例子:比如我們有個(gè)自行車接口,這個(gè)接口包含了很多方法,包括GPS定位,以及換擋的方法

然后我們發(fā)現(xiàn)即便普通的自行車也需要實(shí)現(xiàn)GPS定位以及換擋的功能,顯然這違背了接口隔離的原則。遵循接口最小化的原則,我們重新設(shè)計(jì):

這樣一來(lái)每個(gè)接口的功能相對(duì)單一,使用多個(gè)專門的接口比使用一個(gè)總的接口要好,假如我們的山地車沒有沒有GPS定位的功能,我們不去繼承實(shí)現(xiàn)對(duì)應(yīng)的接口即可,在iOS開發(fā)中有很多這樣的例子,比如UITalbleView的代理有兩個(gè)不同的接口,UITableViewDataSource專門負(fù)責(zé)需要顯示的內(nèi)容,UITableViewDelegate專門負(fù)責(zé)一些view的自定義顯示,然后我們會(huì)繼承多個(gè)接口,這就滿足了ISP原則。
@interface ViewController () <UITableViewDataSource,UITableViewDelegate,OtherProtocol>
依賴倒置原則:Dependence Inversion Principle
該原則的定義:高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;抽象不應(yīng)該依賴細(xì)節(jié);細(xì)節(jié)應(yīng)該依賴抽象。
其實(shí)這就是我們經(jīng)常說(shuō)的“針對(duì)接口編程”,這里的接口就是抽象,我們應(yīng)該依賴接口,而不是依賴具體的實(shí)現(xiàn)來(lái)編程。
如你在Sqlite數(shù)據(jù)庫(kù)的基礎(chǔ)上開發(fā)一套新的數(shù)據(jù)庫(kù)系統(tǒng)AWEDatabase,這時(shí)候Sqlite相當(dāng)于底層模塊,而你的AWEDatabase就屬于高層模塊;而從AWEDatabase開發(fā)使用者來(lái)看,他的業(yè)務(wù)層就相當(dāng)于高層模塊,而AWEDatabase就變成底層模塊了,所以模塊的高低應(yīng)該是從開發(fā)者當(dāng)前的角度來(lái)看的,不過(guò)DIP原則從不同角度來(lái)看它都適合且需要被遵守。假如我們高層模塊直接依賴于底層模塊,帶來(lái)的后果是每次底層模塊改動(dòng),高層模塊就會(huì)受到影響,整個(gè)系統(tǒng)就變得不穩(wěn)定,這也違反了開放關(guān)閉原則。
??通常我們會(huì)通過(guò)引入中間層的方式來(lái)解決這個(gè)問(wèn)題,這個(gè)中間層相當(dāng)于一個(gè)抽象接口層,高層模塊和底層模塊都依賴于這個(gè)中間層來(lái)交互,這樣只要中間抽象層保持不變,底層模塊改變不會(huì)影響到高層模塊,這就滿足了開放關(guān)閉原則;而且假如高層模塊跟底層模塊同時(shí)處于開發(fā)階段,這樣有了中間抽象層之后,每個(gè)模塊都可以針對(duì)這個(gè)抽象層的接口同時(shí)開發(fā),高層模塊就不需要等到底層模塊開發(fā)完畢才能繼續(xù)了。
??比如在我們項(xiàng)目中有涉及IM的功能,現(xiàn)在這個(gè)IM模塊采用的是XMPP協(xié)議來(lái)實(shí)現(xiàn),客戶端通過(guò)這個(gè)模塊來(lái)實(shí)現(xiàn)消息的收發(fā),但是假如后面我們想要換成其它協(xié)議,比如MQTT等,針對(duì)接口編程的話就可以讓我們很輕松的實(shí)現(xiàn)模塊替換:

@protocol MessageDelegate <NSObject>
@required
-(void)goOnline;
-(void)sendMessage:(NSString*)content;
@end
//xmpp實(shí)現(xiàn)
@interface XMPPMessageCenter <MessageDelegate>
@end
//MQTT實(shí)現(xiàn)
@interface MQTTMessageCenter <MessageDelegate>
@end
//業(yè)務(wù)層
@interface BussinessLayer
//使用遵循MessageDelegate協(xié)議的對(duì)象,針對(duì)接口編程,以后替換也很方便
@property(nonatomic,strong)id<MessageDelegate> messageCenter;
@end
當(dāng)我們?cè)谶M(jìn)行面向?qū)ο笤O(shè)計(jì)的時(shí)候應(yīng)該充分考慮上面這幾個(gè)原則,一開始可能設(shè)計(jì)并不完美,不過(guò)可以在重構(gòu)的過(guò)程中不斷完善。但其實(shí)很多人都跳過(guò)了設(shè)計(jì)這個(gè)環(huán)節(jié),拿到一個(gè)模塊直接動(dòng)手編寫代碼,更不用說(shuō)去思考設(shè)計(jì)了,項(xiàng)目中也有很多這樣的例子。當(dāng)然對(duì)于簡(jiǎn)單的模塊或許不用什么設(shè)計(jì),不過(guò)假如模塊相對(duì)復(fù)雜的話,能夠在動(dòng)手寫代碼之前好好設(shè)計(jì)思考一下,養(yǎng)成這個(gè)習(xí)慣,肯定會(huì)對(duì)編寫出可讀性、穩(wěn)定性以及可擴(kuò)展性較高的代碼有幫助。