繼承和面向接口(iOS架構(gòu)思想篇) (轉(zhuǎn) ZhengYaWei Cocoa開(kāi)發(fā)者社區(qū))

前言

在開(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)推薦:

iOS關(guān)聯(lián)對(duì)象技術(shù)原理

24種設(shè)計(jì)模式及案例

iOS項(xiàng)目組件化搭建

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,478評(píng)論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,121評(píng)論 1 32
  • 前言 在開(kāi)篇之前思考幾個(gè)問(wèn)題? 1、繼承最大的缺點(diǎn)是什么? 2、為什么說(shuō)耦合也可能是一種需求? 3、有哪些場(chǎng)景不適...
    ZhengYaWei閱讀 11,032評(píng)論 13 169
  • 雨,淅淅瀝瀝的下著,車輪碾過(guò)水洼,濺起一陣陣水花。冷冽的風(fēng)呼嘯著,夾著冰冷的雨,似刀子割著我的臉。我不禁抖了抖身子...
    百合小香閱讀 211評(píng)論 0 2
  • 荷蘭之家:一家擁有11年歷史的荷蘭本土老店,是一家值得推薦的海淘網(wǎng)站 熱! 好熱! 2月份的尾巴,廣州的最高氣溫已...
    dianxinka閱讀 265評(píng)論 0 0