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