擁有一把錘子未必能成為建筑師
最近在項目開發過程中碰到了一些問題,發現在每波迭代開發過程中,經常需要去修改之前的代碼,雖然出現這樣的情形很正常,新的需求必然會帶來新的功能新的設計,導致之前的代碼受到影響。記得看過一個笑話:
“殺一個程序員不需要用槍,改三次需求就可以了”
其實需求設計是一個方面,另外我們作為設計開發人員有時候也需要去反省,反省一下代碼的設計是否合理,為什么新功能的在原有代碼上擴展會那么難,為什么我們的代碼這么不穩定,牽一發而動全身?
??我覺得能成為一名程序員,至少不會是一個笨的人,要完成一個功能,總能想辦法實現(不然早被開除啦~),但實現的方法思路卻有好有壞,不過我認為思路可以被引導,軟件開發不是才剛開始,它已經存在一段時間,我們可以吸收前人的一些經驗教訓來提高自己,比如GOF的《設計模式:可復用面向對象軟件的基礎》,幫我們總結了很多問題的解決思路。這段時間也花了點時間學習面向對象設計的一些思想,也談談自己的一些理解。
??提到設計模式,我想很多人都看過這塊的一些書籍,不過不知道會不會有跟我一樣的困惑:看的時候都理解,但是實際開發的時候卻無法融入,后面慢慢就忘記了。尷尬,可能我們只是看到了某個模式的表面,而隱藏在模式后面的一些“真理”卻沒有去挖掘,這個模式是要解決什么問題?其實模式設計的背后都是為了遵循某種設計原則。
“比設計模式更重要的是設計原則”
面相對象設計的概念大家也都知道,它的設計目標就是希望軟件系統能做到以下幾點:
- 可擴展:新特性能夠很容易的添加到現有系統中,不會影響原本的東西
- 可修改:當修改某一部分的代碼時,不會影響到其它不相關的部分
- 可替代:將系統中某部分的代碼用其它有相同接口的類替換時,不會影響到現有系統
這幾個可以用來檢測我們的軟件系統是不是設計得合理,而如何設計出易于維護和擴展的軟件系統是有設計原則可以遵循指導的,Robert C. Martin提出了面相對象設計的五個基本原則(SOLID):
- S-單一職責原則
- O-開放關閉原則
- L-里氏替換原則
- I-接口隔離原則
- D-依賴倒置原則
我們在進行面相對象設計的時候應該牢記這幾個原則,這能讓你成為更優秀的設計開發人員---至少你的代碼不會那么爛,下面來簡單了解一下這幾個原則。
單一職責原則:Single Responsibility Principle
一個類有且僅有一個職責,只有一個引起它變化的原因。
簡單來說一個類只做好一件事就行,不去管跟自己不相干的,狗拿耗子多管閑事,其核心就是解耦以及高內聚。這個原則看著很簡單,我們在寫代碼的時候即便不知道這個原則也會往這個方向靠攏,寫出功能相對單一的類,不過這個原則很容易違背,因為可能由于某種原因,原來功能單一的類需要被細化成顆粒更小的職責1跟職責2,所以在每次迭代過程中可能需要重新梳理重構之前編寫的代碼,將不同的職責封裝到不同的類或者模塊中。
舉個栗子:
@interface DataTransfer : NSObject
-(void)upload:(NSData *)data; //上傳數據
-(void)download(NSString*)url; //根據URL下載東西
@end
DataTransfer包含上傳跟下載功能,仔細考慮可以發現這相當于實現了兩個功能,一個負責上傳的相關邏輯,另一個負責下載的邏輯,而這個兩個功能相對對立,當有一個功能改變的時候,比如我們之前是使用AFNetworking,現在想換成其它第三方或者nsurlconnection來實現上傳跟下載:
- 上傳方式變更,導致DataTransfer變更
- 下載方式變更,導致 DataTransfer變更
這就違反了單一職責的原則,所以需要將不同的功能拆解成兩個不同的類,來負責各自的職責,不過這個拆的粒度可能因人而已,有時候并不需要拆的過細,不要成了為設計而設計。
??在我們項目中經??吹胶芏噙`反這條原則的代碼,而且違反的比較明顯,許多類都是豐富功能的超級集合,整個類變得臃腫難以理解,這時候就需要我們有意識地去重構了。
開放關閉原則:Open Closed Principle
開閉原則的定義是說一個軟件實體如類,模塊和函數應該對擴展開放,而對修改關閉,具體來說就是你應該通過擴展來實現變化,而不是通過修改原有的代碼來實現變化,該原則是面相對象設計最基本的原則。
??之前說過在項目中每當需求需改的時候經常需要對代碼有很大的改動,很大程度上就是因為我們對這個原則理解的不夠透徹。
??開閉原則的關鍵在于抽象,我們需要抽象出那些不會變化或者基本不變的東西,這部分東西相對穩定,這也就是對修改關閉的地方(這并不意味著不可以再修改),而對于那些容易變化的部分我們也對其封裝,但是這部分是可以動態修改的,這也就是對擴展開發的地方,比如設計模式中的策略模式和模板模式就是在實現這個原則(現在應該對模式有更感性的認識了吧~)。
舉個例子:我們需要保存對象到數據庫當中,其中有個類似save()的保存方法,這部分應該是不變的,接口相對穩定,而具體保存的實現卻有可能不同,我們現在可能是保存在Sqlite數據庫中,假如以后如果想保存到一個自己實現的數據庫中時,我們只需要實現一個擁有同樣接口的擴展類添加進去即可,這就是對擴展開放,不會對之前的代碼造成任何影響,就可以實現保存到新數據庫的功能,保證了系統的穩定性。
實現開閉原則的指導思想就是:
- 抽象出相對穩定的接口,這部分應該不改動或者很少改動
- 封裝變化
不過在軟件開發過程中,要一開始就完全按照開閉原則來可能比較困難,更多的情況是在不斷的迭代重構過程中去改進,在可預見的變化范圍內去做設計。
里氏替代原則:Liskov Substitution Principle
該原則的定義:所有引用基類的地方必須能透明地使用其子類的對象。簡單來說,所有使用基類代碼的地方,如果換成子類對象的時候還能夠正常運行,則滿足這個原則,否則就是繼承關系有問題,應該廢除兩者的繼承關系,這個原則可以用來判斷我們的對象繼承關系是否合理。
比如有一個鯨魚的類,我們讓鯨魚繼承于魚類,然后魚類有個呼吸的功能:
然后在水里的時候,魚能夠進行呼吸:
if(isInwater){
//在水中了,開始呼吸
fish.breath();
}
當我們把鯨魚這個子對象替換原來的基類魚對象,鯨魚在水里開始呼吸,這時問題就出現了,鯨魚是哺乳動物,在水里呼吸是沒法呼吸的,一直在水里就GG思密達了,所以這違反了該原則,我們就可以判斷鯨魚繼承于魚類不合理,需要去重新設計。
??通常在設計的時候,我們都會優先采用組合而不是繼承,因為繼承雖然減少了代碼,提高了代碼的重用性,但是父類跟子類會有很強的耦合性,破壞了封裝。
接口隔離原則:Interface Segregation Principle
該原則的定義:不能強迫用戶去依賴那些他們不使用的接口。簡單來說就是客戶端需要什么接口,就提供給它什么樣的接口,其它多余的接口就不要提供,不要讓接口變得臃腫,否則當對象一個沒有使用的方法被改變了,這個對象也將會受到影響。接口的設計應該遵循最小接口原則,其實這也是高內聚的一種表現,換句話說,使用多個功能單一、高內聚的接口總比使用一個龐大的接口要好。
??舉個簡單的例子:比如我們有個自行車接口,這個接口包含了很多方法,包括GPS定位,以及換擋的方法
?然后我們發現即便普通的自行車也需要實現GPS定位以及換擋的功能,顯然這違背了接口隔離的原則。遵循接口最小化的原則,我們重新設計:
??這樣一來每個接口的功能相對單一,使用多個專門的接口比使用一個總的接口要好,假如我們的山地車沒有沒有GPS定位的功能,我們不去繼承實現對應的接口即可,在iOS開發中有很多這樣的例子,比如UITalbleView的代理有兩個不同的接口,UITableViewDataSource專門負責需要顯示的內容,UITableViewDelegate專門負責一些view的自定義顯示,然后我們會繼承多個接口,這就滿足了ISP原則。
@interface ViewController () <UITableViewDataSource,UITableViewDelegate,OtherProtocol>
依賴倒置原則:Dependence Inversion Principle
該原則的定義:高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。其實這就是我們經常說的“針對接口編程”,這里的接口就是抽象,我們應該依賴接口,而不是依賴具體的實現來編程。
??如你在Sqlite數據庫的基礎上開發一套新的數據庫系統AWEDatabase,這時候Sqlite相當于底層模塊,而你的AWEDatabase就屬于高層模塊;而從AWEDatabase開發使用者來看,他的業務層就相當于高層模塊,而AWEDatabase就變成底層模塊了,所以模塊的高低應該是從開發者當前的角度來看的,不過DIP原則從不同角度來看它都適合且需要被遵守。假如我們高層模塊直接依賴于底層模塊,帶來的后果是每次底層模塊改動,高層模塊就會受到影響,整個系統就變得不穩定,這也違反了開放關閉原則。
??通常我們會通過引入中間層的方式來解決這個問題,這個中間層相當于一個抽象接口層,高層模塊和底層模塊都依賴于這個中間層來交互,這樣只要中間抽象層保持不變,底層模塊改變不會影響到高層模塊,這就滿足了開放關閉原則;而且假如高層模塊跟底層模塊同時處于開發階段,這樣有了中間抽象層之后,每個模塊都可以針對這個抽象層的接口同時開發,高層模塊就不需要等到底層模塊開發完畢才能繼續了。
??比如在我們項目中有涉及IM的功能,現在這個IM模塊采用的是XMPP協議來實現,客戶端通過這個模塊來實現消息的收發,但是假如后面我們想要換成其它協議,比如MQTT等,針對接口編程的話就可以讓我們很輕松的實現模塊替換:
@protocol MessageDelegate <NSObject>
@required
-(void)goOnline;
-(void)sendMessage:(NSString*)content;
@end
//xmpp實現
@interface XMPPMessageCenter <MessageDelegate>
@end
//MQTT實現
@interface MQTTMessageCenter <MessageDelegate>
@end
//業務層
@interface BussinessLayer
//使用遵循MessageDelegate協議的對象,針對接口編程,以后替換也很方便
@property(nonatomic,strong)id<MessageDelegate> messageCenter;
@end
當我們在進行面向對象設計的時候應該充分考慮上面這幾個原則,一開始可能設計并不完美,不過可以在重構的過程中不斷完善。但其實很多人都跳過了設計這個環節,拿到一個模塊直接動手編寫代碼,更不用說去思考設計了,項目中也有很多這樣的例子。當然對于簡單的模塊或許不用什么設計,不過假如模塊相對復雜的話,能夠在動手寫代碼之前好好設計思考一下,養成這個習慣,肯定會對編寫出可讀性、穩定性以及可擴展性較高的代碼有幫助。
最關鍵的軟件開發工具是受過良好設計原則訓練的思維。