前言
在開(kāi)篇之前思考幾個(gè)問(wèn)題?
1、繼承最大的缺點(diǎn)是什么?
2、為什么說(shuō)耦合也可能是一種需求?
3、有哪些場(chǎng)景不適合使用繼承?
4、繼承本身就具有高耦合性,但卻可以實(shí)現(xiàn)代碼復(fù)用,有哪些替代方案可以去除高耦合性并實(shí)現(xiàn)代碼的復(fù)用?
5、iOS 開(kāi)發(fā)中有否有必要同一派生 ViewController?
6、什么是面向切面編程思想?
7、為什么Swift著力宣傳面向協(xié)議的思想,而OC 中面向協(xié)議的思想為什么不能像Swift那樣得以普及?
8、函數(shù)式鏈?zhǔn)骄幊讨腥绾螌?duì)外控制函數(shù)調(diào)用的先后順序?如:Masonry (面向接口解決問(wèn)題)
在接下來(lái)的分析中,這些問(wèn)題都會(huì)一一得到解答,保證干貨滿滿。筆者原本想著圍繞繼承和面向接口各寫(xiě)一片文章,但實(shí)際繼承和面向接口在某些方面還有很多的關(guān)聯(lián)性,因此這里索性合二為一。
一、繼承 (優(yōu)缺點(diǎn)、使用原則、替代方案)
二、ViewController是否應(yīng)統(tǒng)一繼承
三、面向接口思想
四、多態(tài)和面向接口的選擇
五、面向接口實(shí)現(xiàn)順序控制
一、繼承
1.1 繼承的優(yōu)缺點(diǎn)
繼承、封裝、多態(tài)是面向?qū)ο蟮娜笾еjP(guān)于繼承毫無(wú)疑問(wèn)最大的優(yōu)點(diǎn)是代碼復(fù)用。但是很多時(shí)候繼承也可能會(huì)被無(wú)止境的濫用,造成代碼結(jié)構(gòu)散亂,后期維護(hù)困難等,其中有可能帶來(lái)最大的問(wèn)題是高耦合。
1.2 繼承的使用的原則
假設(shè)你的代碼是針對(duì)多平臺(tái)多版本的,并且你需要針對(duì)每個(gè)平臺(tái)每個(gè)版本寫(xiě)一些代碼。這時(shí)候更合理的做法可能是創(chuàng)建一個(gè) OBJDevice 類,讓一些子類如 OBJIPhoneDevice 和 OBJIPadDevice ,甚至更深層的子類如 OBJIPhone5Device 來(lái)繼承,并讓這些子類重寫(xiě)特定的方法。關(guān)于這個(gè)場(chǎng)景就非常適合使用繼承,因?yàn)榭偟膩?lái)說(shuō)它滿足如下條件:
父類OBJDevice只是給其他派生的子類提供服務(wù),OBJDevice只做自己分內(nèi)的事情,并不涉及子類的業(yè)務(wù)邏輯。不同的業(yè)務(wù)邏輯由不同的子類自己去完成。子類和父類各做自身的事情,互不影響和干擾。
父類OBJDevice 的變化要在所有子類中得以體現(xiàn)。也就是說(shuō)父類牽一動(dòng)發(fā)全部子類,可以理解為此時(shí)的高耦合是一種需求,而不是一種缺點(diǎn)。
如果滿足上述兩種條件,可以考慮使用繼承。另外,實(shí)際開(kāi)發(fā)中如果繼承超過(guò)2層的時(shí)候,就要慎重這個(gè)繼承的方案了,因?yàn)檫@可能是濫用繼承的開(kāi)始。
1.3 替代繼承的方式
針對(duì)不適合用繼承來(lái)做的事,或不想用繼承來(lái)做的,還有如下幾種備選方案可以適合不同的場(chǎng)景,有利于打開(kāi)你的思路。
1.3.1 協(xié)議
假設(shè)原本已經(jīng)開(kāi)發(fā)了一個(gè)繼承NSObject的音頻播放器VoicePlayer,但此時(shí)想支持OGG格式的音頻。而實(shí)際上之前的VoicePlayer和現(xiàn)在想要開(kāi)發(fā)的音頻播放器類,只是對(duì)外提供的API類似,內(nèi)部實(shí)現(xiàn)代碼卻差別很大。這里簡(jiǎn)單說(shuō)明一下OGG格式音頻在游戲開(kāi)發(fā)中用的比較普遍,筆者之用原生開(kāi)發(fā)一款游戲應(yīng)用時(shí),就曾使用過(guò)OGG格式音頻,相比于其他音頻而言,OGG最大的特點(diǎn)是體積更小。一段音頻中,沒(méi)有聲音的那一部分將不暫用任何體積,而類似MP3格式則不同,即使是沒(méi)聲音,依然會(huì)存在體積占用。參照上面關(guān)于繼承的使用原則可知,此時(shí)繼承并不適合這種場(chǎng)景。筆者給出的答案是通過(guò)協(xié)議提供相同的接口,代碼結(jié)構(gòu)如下:
@protocolVoicePlayerProtocol
-?(void)play;
-?(void)pause;
@end
@classNormalVoicePlayer:NSObject
@end
@classOGGVoicePlayer:NSObject
@end
1.3.2 用組合替代繼承
如果想重用已有的代碼而不想共享同樣的接口,組合便是首選。
假如:A界面有個(gè)輸入框,會(huì)根據(jù)服務(wù)器上用戶的輸入歷史來(lái)自動(dòng)補(bǔ)全,叫AutoCompleteTextField。后來(lái)某天來(lái)了個(gè)需求,在另外一個(gè)界面中,也用到這個(gè)輸入框,除了根據(jù)輸入歷史補(bǔ)全,增加一個(gè)自動(dòng)補(bǔ)全郵箱的功能,就是用戶輸入@后,我們自動(dòng)補(bǔ)全一些域名。這個(gè)功能很簡(jiǎn)單,結(jié)構(gòu)如下:
@interfaceAutoCompleteTextField:UITextField
-?(void)autoCompleteWithUserInfo;
@end
@interfaceAutoCompleteMailTextField:AutoCompleteTextField
-?(void)autoCompleteWithMail;
@end
過(guò)兩天,產(chǎn)品經(jīng)理希望有個(gè)本地輸入框能夠根據(jù)本地用戶信息來(lái)補(bǔ)全,而不是根據(jù)服務(wù)器的信息來(lái)自動(dòng)補(bǔ)全,我們可以輕松通過(guò)覆蓋來(lái)實(shí)現(xiàn):
@interfaceAutoCompleteLocalTextField:AutoCompleteTextField- (void) autoCompleteWithUserInfo;@end
app上線一段時(shí)間之后,UED不知哪根筋搭錯(cuò)了,決定要修改搜索框的UI,于是添加個(gè)初始化函數(shù)initWithStyle得以解決。
重點(diǎn)來(lái)了,但是有一天,隔壁項(xiàng)目組的哥們想把我們的本地補(bǔ)全輸入框
AutoCompleteLocalTextField移植到他們的項(xiàng)目中。這個(gè)可就麻煩了,因?yàn)槭褂肁utoCompleteLocalTextField要引入AutoCompleteTextField,而AutoCompleteTextField本身也帶著API相關(guān)的對(duì)象,同時(shí)還有數(shù)據(jù)解析的對(duì)象。 也就是說(shuō),要想給另外一個(gè)TEAM,差不多整個(gè)網(wǎng)絡(luò)層框架都要移植過(guò)去。
上面這個(gè)問(wèn)題總結(jié)來(lái)說(shuō)是兩種類型問(wèn)題:第一種類型問(wèn)題是改了一處,其他都要改,但是勉強(qiáng)還能接受;第二種類型就是代碼服用的時(shí)候,要把所有相關(guān)依賴都拷貝過(guò)去才能解決問(wèn)題;兩種類型的問(wèn)題都說(shuō)明了繼承的高耦合性,牽一而動(dòng)全身的特性。
關(guān)于上述問(wèn)題最佳的解決方案,筆者認(rèn)為是通過(guò)組合的形式,區(qū)分不同的模塊來(lái)處理,輸入框本身的UI可以作為一個(gè)模塊,本地搜索提示和服務(wù)器搜索提示可以作為不同的模塊分別處理。實(shí)際使用中可以通過(guò)不同的模塊組合,實(shí)現(xiàn)不同的功能。
1.3.3 類別
有時(shí)可能會(huì)想在一個(gè)對(duì)象的基礎(chǔ)上增加額外的功能形成另外一個(gè)對(duì)象,繼承是一種很容易想到的方法。還有另外一種比較好的方案是通過(guò)類別。為該對(duì)象擴(kuò)展方法,按需調(diào)用,比如為NSArray增加一個(gè)移除第一個(gè)元素的方法:
@interfaceNSArray?(OBJExtras)-?(void)removingFirstObject;@end
1.3.4 配置對(duì)象
假設(shè)某個(gè)app中有主題切換,其中每種主題都對(duì)應(yīng)backgroundColor?和?font?兩個(gè)屬性。按照繼承的思路我們很有可能會(huì)先寫(xiě)一個(gè)父類,為這個(gè)父類實(shí)現(xiàn)一個(gè)空的setupStyle方法,然后各種不同風(fēng)格的主題分別是一個(gè)子類,重寫(xiě)父類的setupStyle方法。
其實(shí)大可不必這樣做,完全可以創(chuàng)建一個(gè)ThemeConfiguration的類,該類中具有backgroundColor和?fontSize?屬性。可以事先創(chuàng)建幾種主題, Theme 在其初始化函數(shù)中獲取一個(gè)配置類 ThemeConfiguration 的值即可。相比繼承而言,就不用創(chuàng)建那么多文件,以及父類中還要寫(xiě)一個(gè)?setupStyle空方法。
二、ViewController是否應(yīng)統(tǒng)一繼承
2.1 不統(tǒng)一繼承的理由
如果ViewController統(tǒng)一繼承了父類控制器,首先可能會(huì)涉及到上面說(shuō)到的高耦合的一個(gè)項(xiàng)目,缺點(diǎn);除此之外,還會(huì)涉及上手接受成本問(wèn)題,新手接受需要對(duì)父類控制器的使用有一定的了解;另外,如果涉及項(xiàng)目遷移問(wèn)題,在遷移子類控制器的同時(shí)還要將父類控制器也遷移出去。最后一個(gè)理由是,即使不通過(guò)繼承,同樣能達(dá)到對(duì)項(xiàng)目控制器進(jìn)行統(tǒng)一配置。
2.2 面向切面(AOP)思想簡(jiǎn)介
上面也說(shuō)了幾種替代繼承的方法,如果ViewController不通過(guò)繼承的方式實(shí)現(xiàn),那么首選的替代方式是什么?這里我們可以采用面向切面的編程思想和分類結(jié)合的方式替代控制器的繼承。
首先簡(jiǎn)單說(shuō)下面向切面的編程思想(AOP),聽(tīng)起來(lái)很高大上,實(shí)際上很多iOS開(kāi)發(fā)者應(yīng)該都用過(guò),在iOS中最直接的體現(xiàn)就是借助 Method Swizzling 實(shí)現(xiàn)方法替換。一般,主要的功能是日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理等等。主要的意圖是:將日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理等代碼從業(yè)務(wù)邏輯代碼中劃分出來(lái),通過(guò)對(duì)這些行為的分離,我們希望可以將它們獨(dú)立到非指導(dǎo)業(yè)務(wù)邏輯的方法中,進(jìn)而改 變這些行為的時(shí)候不影響業(yè)務(wù)邏輯的代碼。可以通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)在不修改源代碼的情況下給程序動(dòng)態(tài)統(tǒng)一添加功能的一種技術(shù)。假設(shè)把應(yīng)用程序想成一個(gè)立體結(jié)構(gòu)的話,OOP的利刃是縱向切入系統(tǒng),把系統(tǒng)劃分為很多個(gè)模塊(如:用戶模塊,文章模塊等等),而AOP的利刃是橫向切入系統(tǒng),提取各個(gè)模塊可能都要重復(fù)操作的部分(如:權(quán)限檢查,日志記錄等等)。
2.3 方案實(shí)現(xiàn)
面向切面的思想可以實(shí)現(xiàn)系統(tǒng)資源的統(tǒng)一配置,iOS 中的Method Swizzling替換系統(tǒng)方法可達(dá)到同樣的效果。這里筆者更為推薦使用第三方開(kāi)源庫(kù)Aspects去攔截系統(tǒng)方法。
我們可以創(chuàng)建一個(gè)叫做ViewControllerConfigure的類,實(shí)現(xiàn)如下代碼。
//.h文件@interface?ViewControllerConfigure?:?NSObject@end//.m文件#import?"ViewControllerConfigure.h"#import?#import?@implementation?ViewControllerConfigure+?(void)load
{
[superload];
[ViewControllerConfigure?sharedInstance];
}
+?(instancetype)sharedInstance
{staticdispatch_once_tonceToken;staticViewControllerConfigure?*sharedInstance;dispatch_once(&onceToken,?^{
sharedInstance?=?[[ViewControllerConfigure?alloc]?init];
});returnsharedInstance;
}
-?(instancetype)init
{self=?[superinit];if(self)?{/*?在這里做好方法攔截?*/
[UIViewControlleraspect_hookSelector:@selector(loadView)?withOptions:AspectPositionAfter?usingBlock:^(idaspectInfo){
[selfloadView:[aspectInfo?instance]];
}?error:NULL];
[UIViewControlleraspect_hookSelector:@selector(viewWillAppear:)?withOptions:AspectPositionAfter?usingBlock:^(id?aspectInfo,BOOLanimated){
[selfviewWillAppear:animated?viewController:[aspectInfo?instance]];
}?error:NULL];
}returnself;
}/*
下面的這些方法中就可以做到自動(dòng)攔截了。
所以在你原來(lái)的架構(gòu)中,大部分封裝UIViewController的基類或者其他的什么基類,都可以使用這種方法讓這些基類消失。
*/
#pragma?mark?-?fake?methods-?(void)loadView:(UIViewController?*)viewController
{NSLog(@"?loadView");
}
-?(void)viewWillAppear:(BOOL)animated?viewController:(UIViewController*)viewController
{/*?你可以使用這個(gè)方法進(jìn)行打日志,初始化基礎(chǔ)業(yè)務(wù)相關(guān)的內(nèi)容?*/
NSLog(@"viewWillAppear");
}@end
關(guān)于上面的代碼主要說(shuō)三點(diǎn):
1、借助 load 方法,實(shí)現(xiàn)代碼無(wú)任何入性型。
當(dāng)類被引用進(jìn)項(xiàng)目的時(shí)候就會(huì)執(zhí)行
load函數(shù)(在main函數(shù)開(kāi)始執(zhí)行之前),與這個(gè)類是否被用到無(wú)關(guān),每個(gè)類的load函數(shù)只會(huì)自動(dòng)調(diào)用一次。除了這個(gè)案列,在實(shí)際開(kāi)發(fā)中筆者曾這么用過(guò)load方法,將app啟動(dòng)后的廣告邏輯相關(guān)代碼全部放到一個(gè)類中的load方法,實(shí)現(xiàn)廣告模塊對(duì)項(xiàng)目的無(wú)入侵性。initialize在類或者其子類的第一個(gè)方法被調(diào)用前調(diào)用。即使類文件被引用進(jìn)項(xiàng)目,但是沒(méi)有使用,initialize不會(huì)被調(diào)用。由于是系統(tǒng)自動(dòng)調(diào)用,也不需要再調(diào)用 [super initialize] ,否則父類的initialize會(huì)被多次執(zhí)行。
2、不單單可以替換
loadView和viewWillAppear方法,還可以替換控制器其他生命周期相關(guān)方法,在這些方法中實(shí)現(xiàn)對(duì)控制器的統(tǒng)一配置。如view背景顏色、統(tǒng)計(jì)事件等。
3、控制器中避免不了還會(huì)拓展一些方法,如無(wú)網(wǎng)絡(luò)數(shù)據(jù)提示圖相關(guān)方法,此時(shí)可以借助
Category實(shí)現(xiàn),在無(wú)法避免使用屬性的情況下,可以借助運(yùn)行時(shí)添加屬性。
關(guān)于控制器的集成問(wèn)題就先說(shuō)到這,接下來(lái)看看面向接口的思想。
三、面向接口思想
對(duì)于接口這一概念的支持,不同語(yǔ)言的實(shí)現(xiàn)形式不同。Java中,由于不支持多重繼承,因此提供了一個(gè)Interface關(guān)鍵詞。而在C++中,通常是通過(guò)定義抽象基類的方式來(lái)實(shí)現(xiàn)接口定義的。Objective-C既不支持多重繼承,也沒(méi)有使用Interface關(guān)鍵詞作為接口的實(shí)現(xiàn)(Interface作為類的聲明來(lái)使用),而是通過(guò)抽象基類和協(xié)議(protocol)來(lái)共同實(shí)現(xiàn)接口的。OC中接口可以理解為Protocol,面向接口編程可以理解為面向協(xié)議編程。先看如下兩端代碼:
ASIHTTPRequest?*request?=?[ASIHTTPRequest?requestWithURL:url];
[request?setDidFinishSelector:@selector(requestDone:)];
[request?setDidFailSelector:@selector(requestWrong:)];
[request?startAsynchronous];
AFHTTPRequestOperationManager?*manager?=?[AFHTTPRequestOperationManager?manager];
[manager?GET:@"www.olinone.com"parameters:nilsuccess:^(AFHTTPRequestOperation?*operation,idresponseObject)?{
}?failure:^(AFHTTPRequestOperation?*operation,NSError*error)?{
}];
觀察上述兩段代碼,是否發(fā)現(xiàn)第二段網(wǎng)絡(luò)請(qǐng)求代碼相比第一段更容易使用。因?yàn)榈诙未a只需初始化對(duì)象,然后調(diào)用方法傳參即可,而第一段代碼要先初始化,然后設(shè)置一堆屬性,最終才能發(fā)起網(wǎng)絡(luò)請(qǐng)求。如果讓一個(gè)新手上手,毫無(wú)疑問(wèn)更喜歡采用第二種方式調(diào)用方法,因?yàn)闊o(wú)需對(duì)AFN掌握太多,僅記住這一個(gè)方法便可發(fā)起網(wǎng)絡(luò)請(qǐng)求,而反觀 ASI 要先了解并設(shè)置各種屬性參數(shù),最終才能發(fā)起網(wǎng)絡(luò)請(qǐng)求。上面的兩端代碼并不是為了說(shuō)明ASI和AFN熟好熟劣,只是想借此引出面向接口的思想。
所以,通過(guò)接口的定義,調(diào)用者可以忽略對(duì)象的屬性,聚焦于其提供的接口和功能上。程序猿在首次接觸陌生的某個(gè)對(duì)象時(shí),接口往往比屬性更加直觀明了,抽象接口往往比定義屬性更能描述想做的事情。
相比于OC,Swift 可以做到協(xié)議方法的具體實(shí)現(xiàn),而 OC 則不行。面向?qū)ο缶幊毯兔嫦騾f(xié)議編程最明顯的區(qū)別在于程序設(shè)計(jì)過(guò)程中對(duì)數(shù)據(jù)類型的抽取(抽象)上,面向?qū)ο缶幊淌褂妙惡屠^承的手段,數(shù)據(jù)類型是引用類型;而面向協(xié)議編程使用的是遵守協(xié)議的手段,數(shù)據(jù)類型是值類型(Swift中的結(jié)構(gòu)體或枚舉)。看一個(gè)簡(jiǎn)單的swift版面向協(xié)議范例,加入想為若干個(gè)繼承自UIView的控件擴(kuò)展一個(gè)抖動(dòng)動(dòng)畫(huà)方法,可以按照如下代碼實(shí)現(xiàn):
//??Shakeable.swift
importUIKit
protocol?Shakeable?{?}
extension?Shakeable?where?Self:?UIView?{
funcshake(){
//?implementation?code
}
}
如果想實(shí)現(xiàn)這個(gè)shake動(dòng)畫(huà),相關(guān)控件只要遵守這個(gè)協(xié)議就可以了。
classCustomImageView:UIImageView,Shakeable?{
}classCustomButton:UIButton,Shakeable?{
}
可能有的人就會(huì)問(wèn)了,直接通過(guò)?extension實(shí)現(xiàn)不就可以了,這種方案是可以的。但是,如果使用extension方式對(duì)于?CustomImageView?和?CustomButton,根本看不出來(lái)任何抖動(dòng)的意圖,整個(gè)類里面沒(méi)有任何東西能告訴你它需要抖動(dòng)。相反,通過(guò)協(xié)議可以很直白的看出抖動(dòng)的意圖。這僅僅是面向協(xié)議的一個(gè)小小好處,除此之外在Swift中還有很多巧妙的用法。
importUIKit
extension?UIView?{
funcshake(){
}
}
四、多態(tài)和面向接口的選擇
4.1 多態(tài)
不同對(duì)象以自己的方式響應(yīng)相同的消息的能力叫做多態(tài)。OC中最直接的體現(xiàn)就是父類指針指向子類對(duì)象,如:Animal *a = [Dog new];Dog *d = (Dog *)a; [d eat];。
4.2 多態(tài)和面向接口的對(duì)比
前段時(shí)間看了Casa大神的跳出面向?qū)ο笏枷?/b>受益不少。所以想把自己所理解的用文字的形式記錄下來(lái)。以一個(gè)文件解析類為例,文件解析的過(guò)程中主要有兩個(gè)步驟:讀取文件和解析文件。假如實(shí)際中可能會(huì)有一些格式十分特殊的文件,所用到的文件讀取方式和解析方式不同于常規(guī)方式。通常按照繼承的寫(xiě)法可能會(huì)是下面這樣。
//.h文件
@interfaceFileParseTool:NSObject
-?(void)parse;
-?(void)analyze;
@end
//.m文件
@implementationFileParseTool
-?(void)parse?{
[selfreadFile];
[selfanalyze];
}
-?(void)readFile?{
//實(shí)現(xiàn)代碼
....
}
-?(void)analyze?{
//子類要重寫(xiě)該方法
}
@end
如果想實(shí)現(xiàn)對(duì)特殊格式文件的解析,此時(shí)可能會(huì)重寫(xiě)父類的analyze方法。
@interfaceSpecialFileParseTool:FileParseTool
@end
@implementationSpecialFileParseToll
-?(void)analyze?{
NSLog(@"%@:%s",NSStringFromClass([selfclass]),?__FUNCTION__);
}
@end
按照繼承的寫(xiě)法,會(huì)存在以下問(wèn)題:
父類中的analyze會(huì)有空方法掛在那里,對(duì)于父類而言沒(méi)有任何實(shí)際意義。
如果架構(gòu)工程師寫(xiě)父類,業(yè)務(wù)工程師實(shí)現(xiàn)子類。那么業(yè)務(wù)工程師很可能不清楚:哪些方法需要被覆蓋重載,哪些不需要。如果子類沒(méi)有覆重方法,而父類提供的只是空方法,就很容易出問(wèn)題。如果子類在覆重的時(shí)候引入了其他不相關(guān)邏輯,那么子類對(duì)象就顯得不夠單純,角色復(fù)雜了。
使用面向接口的方式實(shí)現(xiàn)代碼如下:
//父類.h文件
@protocolFileParseProtocol
-?(void)readFile;
-?(void)analyze;
@end
@interfaceFileParseTool:NSObject
@property(nonatomic,weak)id?assistant;
-?(void)parse;
@end
//??FileParseToolt.m
@implementationFileParseTool
-?(void)parse?{
[self.assistant?readFile];
[self.assistant?analyze];
}
@end
//??SpecialFileParseTool.h
@interfaceSpecialFileParseTool:FileParseTool
@end
//SpecialFileParseTool.m
@implementationSpecialFileParseTool
-?(instancetype)init?{
self=?[superinit];
if(self)?{
self.assistant?=self;
}
returnself;
}
-?(void)analyze?{
NSLog(@"analyze?special??file");
}
-?(void)readFile?{
NSLog(@"read?special?file");
}
@end
相比較于繼承的寫(xiě)法,面向接口的寫(xiě)法恰好能彌補(bǔ)上述三個(gè)缺陷:
父類中將不會(huì)再用analyze空方法掛在那里。
原本需要覆蓋重載的方法,不放在父類的聲明中,而是放在接口中去實(shí)現(xiàn)。基于此,公司內(nèi)部可以規(guī)定:不允許覆蓋重載父類中的方法、子類需要實(shí)現(xiàn)接口協(xié)議中的方法,可以避免繼承上帶來(lái)的困惑。子類中如果引入了父類的外部邏輯,此時(shí)通過(guò)協(xié)議的控制,原本引入了不相關(guān)的邏輯也很容易被剝離。
4.3 面向接口如何解決case大神的四個(gè)問(wèn)題
casa提出使用多態(tài)面臨的四個(gè)問(wèn)題:
父類有部分public的方法是不需要,也不允許子類覆重。
父類有一些特別的方法是必須要子類去覆重的,在父類的方法其實(shí)是個(gè)空方法。
父類有一些方法即便被覆重,父類原方法還是要執(zhí)行的。
父類有一些方法是可選覆重的,一旦覆重,則以子類為準(zhǔn)。
接著結(jié)合上述第二種方式,說(shuō)說(shuō)是如何解決這四個(gè)問(wèn)題的。
關(guān)于第一個(gè)問(wèn)題,在利用面向接口的方案中,公司內(nèi)部可以規(guī)定:不允許覆蓋重載父類中的方法、子類需要實(shí)現(xiàn)接口協(xié)議中的方法。
關(guān)于第二個(gè)問(wèn)題,第二個(gè)方案中父類FileParseTool的.m文件中不再存在空的analyze方法。
關(guān)于第三個(gè)問(wèn)題,顯然能在解答第一個(gè)問(wèn)題中找到答案。
關(guān)于第四個(gè)問(wèn)題,可能需要再補(bǔ)充一些代碼來(lái)解決這個(gè)問(wèn)題。主要思路是:通過(guò)在接口中設(shè)置哪些方法是必須要實(shí)現(xiàn),哪些方法是可選實(shí)現(xiàn)的來(lái)處理對(duì)應(yīng)的問(wèn)題,由子類根據(jù)具體情況進(jìn)行覆重。代碼如下:
//父類.h文件
//流程管理相關(guān)接口,該協(xié)議可以定義子類必須實(shí)現(xiàn)的方法
@protocolFileParseProtocol
-?(void)readFile;
-?(void)analyze;
@end
//攔截相關(guān)接口,該協(xié)議可以定義可選的方法,子類可以根據(jù)實(shí)現(xiàn)情況選擇是否重載父類方法
@protocolInterceptorProtocol
-?(void)willBeginAnalyze;
-?(void)didFinishAnalyze;
@end
@interfaceFileParseTool:NSObject
@property(nonatomic,weak)id?assistant;
@property(nonatomic,weak)id?interceptor;
-?(void)parse;
@end
//??FileParseToolt.m
@implementationFileParseTool
-?(void)parse?{
[self.assistant?readFile];
if([self.interceptor?respondsToSelector:@selector(willBeginAnalyze)])?{
[self.interceptor?willBeginAnalyze];
}
[self.assistant?analyze];
if([self.interceptor?respondsToSelector:@selector(didFinishAnalyze)])?{
[self.interceptor?didFinishAnalyze];
}
}
@end
//??SpecialFileParseTool.h
@interfaceSpecialFileParseTool:FileParseTool
@end
//SpecialFileParseTool.m
@implementationSpecialFileParseTool
-?(instancetype)init?{
self=?[superinit];
if(self)?{
self.assistant?=self;
self.interceptor?=self;
}
returnself;
}
-?(void)analyze?{
NSLog(@"analyze?special??file");
}
-?(void)readFile?{
NSLog(@"read?special?file");
}
@end
4.4 何時(shí)使用多態(tài)
1、如果在子類中可能被外界使用到,則應(yīng)該采用多態(tài)的形式,對(duì)外提供接口;如果只是子類私有要更改的方法,則應(yīng)該采用IOP更為合理。
2、如果引入多態(tài)之后導(dǎo)致對(duì)象角色不夠單純,那就不應(yīng)當(dāng)引入多態(tài),如果引入多態(tài)之后依舊是單純角色,那就可以引入多態(tài)。
五、面向接口實(shí)現(xiàn)順序控制
5.1 函數(shù)式和鏈?zhǔn)骄幊趟枷?/b>
在次之前先簡(jiǎn)單說(shuō)下類似Masonry框架的函數(shù)式和鏈?zhǔn)骄幊痰膶?shí)現(xiàn)思路。
鏈?zhǔn)骄幊蹋褐恍枥斡浄椒ㄕ{(diào)用完成后返回對(duì)象本身即可,返回的對(duì)象本身可以繼續(xù)調(diào)用之后的其它方法,因此可以形成鏈條,無(wú)止境的調(diào)用后續(xù)方法。
函數(shù)式編程:OC中主要借助block實(shí)現(xiàn),通過(guò)聲明一個(gè)block,類似于定義了一個(gè)“函數(shù)”,再將這個(gè)“函數(shù)”傳遞給調(diào)用的方法,以此來(lái)實(shí)現(xiàn)對(duì)調(diào)用該方法時(shí)中間過(guò)程或者對(duì)結(jié)果處理的“自定義”,內(nèi)部的其他環(huán)節(jié)完全不需要暴露給調(diào)用者。實(shí)際上,調(diào)用者也根本不需要知道。
5.2 函數(shù)式和鏈?zhǔn)綄?shí)現(xiàn)
假如封裝一個(gè)數(shù)據(jù)庫(kù)管理工具類,借助函數(shù)式和鏈?zhǔn)骄幊趟枷耄獠康恼{(diào)用形式可以是這樣:
NSString?*sql?=?[SQLTool?makeSQL:^(SQLTool?*tool)?{
tool.select(nil).from(@"").where(@"");
}];
代碼的實(shí)現(xiàn)可以是這樣:
//.h文件
#import
@classSQLTool;
//定義select的block
typedefSQLTool?*(^Select)(NSArray?*columns);
typedefSQLTool?*(^From)?(NSString*tableName);
typedefSQLTool?*(^Where)(NSString*conditionStr);
@interfaceSQLTool:NSObject
@property(nonatomic,strong,readonly)?Select?select;
@property(nonatomic,strong,readonly)?From?from;
@property(nonatomic,strong,readonly)?Where?where;
//添加這個(gè)方法,參數(shù)是一個(gè)block,傳遞一個(gè)SQLTool的實(shí)例
+?(NSString*)makeSQL:(void(^)(SQLTool?*tool))block;
@end
//.m文件
#import"SQLTool.h"
@interfaceSQLTool()
@property(nonatomic,strong)NSString*sql;
@end
@implementationSQLTool
+?(NSString*)makeSQL:(void(^)(SQLTool?*tool))block?{
if(block)?{
SQLTool?*tool?=?[[SQLTool?alloc]?init];
block(tool);
returntool.sql;
}
returnnil;
}
-?(Select)select?{
return^(NSArray?*columns)?{
self.sql?=@"select?篩選的結(jié)果";
//這里將自己返回出去
returnself;
};
}
-?(From)from{
return^(NSString*tableName)?{
self.sql?=@"from?篩選的結(jié)果";
returnself;
};
}
-?(Where)where{
return^(NSString*conditionStr){
self.sql?=@"where?篩選的結(jié)果";
returnself;
};
}
@end
雖然實(shí)現(xiàn)了函數(shù)式和鏈?zhǔn)骄幊趟枷耄侨绻胱屚饨缯{(diào)用者嚴(yán)格按照select、from、where的順序去掉用,而不是毫無(wú)順序的胡亂調(diào)用,請(qǐng)問(wèn)這種情況該如何處理?下面會(huì)借助面向協(xié)議編程思想給出答案。
5.3 實(shí)現(xiàn)順序控制
關(guān)于上面的順序調(diào)用的問(wèn)題,我們可以這樣想:某個(gè)類遵從了某個(gè)協(xié)議,從一定程度上講就等同于這個(gè)類就有了協(xié)議中聲明的方法可供外界調(diào)用。如果反過(guò)來(lái),如果沒(méi)有遵從協(xié)議就無(wú)法調(diào)用了。ps:此處所說(shuō)的調(diào)用,只是從編譯的角度出發(fā)。具體實(shí)現(xiàn)請(qǐng)看下面代碼,總的來(lái)說(shuō)沒(méi)有太高深的語(yǔ)法相關(guān)問(wèn)題。
//.h文件
#import
@classSQLToolTwo;
@protocolISelectable;//1、
@protocolIFromable;//2、
@protocolIWhereable;//3、
typedefSQLToolTwo*(^SelectTwo)(NSArray?*columns);
typedefSQLToolTwo?*(^FromTwo)(NSString*tableName);
typedefSQLToolTwo?*(^WhereTwo)?(NSString*conditionStr);
@protocolISelectable
@property(nonatomic,copy,readonly)?SelectTwo?selectTwo;
@end
@protocolIFromable
@property(nonatomic,copy,readonly)?FromTwo?fromTwo;
@end
@protocolIWhereable
@property(nonatomic,copy,readonly)?WhereTwo?whereTwo;
@end
@interfaceSQLToolTwo:NSObject
+?(NSString*)makeSQL:(void(^)(SQLToolTwo?*tool))block;
@end
//.m文件
#import"SQLToolTwo.h"
@interfaceSQLToolTwo()
@property(nonatomic,strong)NSString*sql;
@end
@implementationSQLToolTwo
+?(NSString*)makeSQL:(void(^)(SQLToolTwo?*tool))block?{
if(block)?{
SQLToolTwo*tool?=?[[SQLToolTwo?alloc]?init];
block(tool);
returntool.sql;
}
returnnil;
}
-?(SelectTwo)selectTwo?{
return^(NSArray?*columns)?{
self.sql?=@"select?篩選的結(jié)果";
returnself;
};
}
-?(FromTwo)fromTwo{
return^(NSString*tableName)?{
self.sql?=@"from?篩選的結(jié)果";
returnself;
};
}
-?(WhereTwo)whereTwo{
return^(NSString*conditionStr){
self.sql?=@"where?篩選的結(jié)果";
returnself;
};
}
@end
按照上述實(shí)現(xiàn)代碼,你將只能嚴(yán)格按照selectTwo、fromTwo、whereTwo的順序執(zhí)行代碼。這是因?yàn)槊勒{(diào)用一次相關(guān)的block,返回的SQLToolTwo實(shí)例對(duì)象遵守不同的協(xié)議。
NSString*sql2?=?[SQLToolTwo?makeSQL:^(SQLToolTwo?*tool)?{
tool.selectTwo(nil).fromTwo(@"").whereTwo(@"");
}];
六、總結(jié)
文章的第一部分首先說(shuō)了繼承的代碼復(fù)用性和高耦合性,然后總結(jié)了繼承應(yīng)當(dāng)在何時(shí)使用,最后有說(shuō)了四種替代繼承的方案(協(xié)議、組合、類別、配置對(duì)象);第二部分利用面向切面的思想,解決了iOS開(kāi)發(fā)中關(guān)于ViewController繼承的問(wèn)題;第三部分簡(jiǎn)單介紹了面向接口的思想,以及和面向?qū)ο笏枷氲谋容^;第四部分涉及多態(tài)和面向接口的抉擇問(wèn)題;第五部分的實(shí)現(xiàn)代碼中包含函數(shù)式、鏈?zhǔn)揭约懊嫦蚪涌诘乃枷耄渲兄攸c(diǎn)說(shuō)明了如何利用面向接口的思想控制函數(shù)的執(zhí)行流程順序問(wèn)題。
作者:ZhengYaWei
鏈接:http://www.lxweimin.com/p/39e6a8409476
相關(guān)推薦: