前言
在開篇之前思考幾個問題?
- 1、繼承最大的缺點是什么?
- 2、為什么說耦合也可能是一種需求?
- 3、有哪些場景不適合使用繼承?
- 4、繼承本身就具有高耦合性,但卻可以實現代碼復用,有哪些替代方案可以去除高耦合性并實現代碼的復用?
- 5、iOS 開發中有否有必要同一派生 ViewController?
- 6、什么是面向切面編程思想?
- 7、為什么Swift著力宣傳面向協議的思想,而OC 中面向協議的思想為什么不能像Swift那樣得以普及?
- 8、函數式鏈式編程中如何對外控制函數調用的先后順序?如:Masonry (面向接口解決問題)
在接下來的分析中,這些問題都會一一得到解答,保證干貨滿滿。筆者原本想著圍繞繼承和面向接口各寫一片文章,但實際繼承和面向接口在某些方面還有很多的關聯性,因此這里索性合二為一。
一、繼承 (優缺點、使用原則、替代方案)
二、ViewController是否應統一繼承
三、面向接口思想
四、多態和面向接口的選擇
五、面向接口實現順序控制
一、繼承
1.1 繼承的優缺點
繼承、封裝、多態是面向對象的三大支柱。關于繼承毫無疑問最大的優點是代碼復用。但是很多時候繼承也可能會被無止境的濫用,造成代碼結構散亂,后期維護困難等,其中有可能帶來最大的問題是高耦合
。
1.2 繼承的使用的原則
假設你的代碼是針對多平臺多版本的,并且你需要針對每個平臺每個版本寫一些代碼。這時候更合理的做法可能是創建一個 OBJDevice 類,讓一些子類如 OBJIPhoneDevice 和 OBJIPadDevice ,甚至更深層的子類如 OBJIPhone5Device 來繼承,并讓這些子類重寫特定的方法。關于這個場景就非常適合使用繼承,因為總的來說它滿足如下條件:
- 父類OBJDevice只是給其他派生的子類提供服務,OBJDevice只做自己分內的事情,并不涉及子類的業務邏輯。不同的業務邏輯由不同的子類自己去完成。子類和父類各做自身的事情,互不影響和干擾。
- 父類OBJDevice 的變化要在所有子類中得以體現。也就是說父類牽一動發全部子類,可以理解為此時的高耦合是一種需求,而不是一種缺點。
如果滿足上述兩種條件,可以考慮使用繼承。另外,實際開發中如果繼承超過2層的時候,就要慎重這個繼承的方案了,因為這可能是濫用繼承的開始。
1.3 替代繼承的方式
針對不適合用繼承來做的事,或不想用繼承來做的,還有如下幾種備選方案可以適合不同的場景,有利于打開你的思路。
1.3.1 協議
假設原本已經開發了一個繼承 NSObject 的音頻播放器 VoicePlayer ,但此時想支持 OGG 格式的音頻。而實際上之前的 VoicePlayer 和現在想要開發的音頻播放器類,只是對外提供的API類似,內部實現代碼卻差別很大。這里簡單說明一下 OGG 格式音頻在游戲開發中用的比較普遍,筆者之用原生開發一款游戲應用時,就曾使用過OGG格式音頻,相比于其他音頻而言,OGG 最大的特點是體積更小。一段音頻中,沒有聲音的那一部分將不暫用任何體積,而類似 MP3 格式則不同,即使是沒聲音,依然會存在體積占用。參照上面關于繼承的使用原則可知,此時繼承并不適合這種場景。筆者給出的答案是通過協議提供相同的接口,代碼結構如下:
@protocol VoicePlayerProtocol <NSObject>
- (void)play;
- (void)pause;
@end
@class NormalVoicePlayer : NSObject <VideoPlayerProtocol>
@end
@class OGGVoicePlayer : NSObject <VideoPlayerProtocol>
@end
1.3.2 用組合替代繼承
如果想重用已有的代碼而不想共享同樣的接口,組合便是首選。
假如:A界面有個輸入框,會根據服務器上用戶的輸入歷史來自動補全,叫AutoCompleteTextField
。后來某天來了個需求,在另外一個界面中,也用到這個輸入框,除了根據輸入歷史補全,增加一個自動補全郵箱的功能,就是用戶輸入@后,我們自動補全一些域名。這個功能很簡單,結構如下:
@interface AutoCompleteTextField : UITextField
- (void)autoCompleteWithUserInfo;
@end
@interface AutoCompleteMailTextField : AutoCompleteTextField
- (void)autoCompleteWithMail;
@end
過兩天,產品經理希望有個本地輸入框能夠根據本地用戶信息來補全,而不是根據服務器的信息來自動補全,我們可以輕松通過覆蓋來實現:
@interface AutoCompleteLocalTextField : AutoCompleteTextField
- (void) autoCompleteWithUserInfo;
@end
app上線一段時間之后,UED不知哪根筋搭錯了,決定要修改搜索框的UI,于是添加個初始化函數initWithStyle
得以解決。
重點來了,但是有一天,隔壁項目組的哥們想把我們的本地補全輸入框AutoCompleteLocalTextField
移植到他們的項目中。這個可就麻煩了,因為使用AutoCompleteLocalTextField
要引入AutoCompleteTextField
,而AutoCompleteTextField
本身也帶著API相關的對象,同時還有數據解析的對象。 也就是說,要想給另外一個TEAM,差不多整個網絡層框架都要移植過去。
上面這個問題總結來說是兩種類型問題:第一種類型問題是改了一處,其他都要改,但是勉強還能接受;第二種類型就是代碼服用的時候,要把所有相關依賴都拷貝過去才能解決問題;兩種類型的問題都說明了繼承的高耦合性,牽一而動全身的特性。
關于上述問題最佳的解決方案,筆者認為是通過組合的形式,區分不同的模塊來處理,輸入框本身的UI可以作為一個模塊,本地搜索提示和服務器搜索提示可以作為不同的模塊分別處理。實際使用中可以通過不同的模塊組合,實現不同的功能。
1.3.3 類別
有時可能會想在一個對象的基礎上增加額外的功能形成另外一個對象,繼承是一種很容易想到的方法。還有另外一種比較好的方案是通過類別。為該對象擴展方法,按需調用,比如為NSArray增加一個移除第一個元素的方法:
@interface NSArray (OBJExtras)
- (void)removingFirstObject;
@end
類似無網絡或數據的提示視圖,也可以借助Category實現,在無法避免使用屬性的情況下,可以借助運行時添加屬性,可以完全同控制器解耦。再比如幀率檢測控件的無入侵實現,在分類的 + load
方法中監聽應用的啟動,當應用啟動的時候將控件添加到 UIWindow 上。 另外,無入侵廣告圖同樣按照幀率檢測的思路實現。
1.3.4 配置對象
假設某個app中有主題切換,其中每種主題都對應backgroundColor
和 font
兩個屬性。按照繼承的思路我們很有可能會先寫一個父類,為這個父類實現一個空的setupStyle方法,然后各種不同風格的主題分別是一個子類,重寫父類的setupStyle
方法。
其實大可不必這樣做,完全可以創建一個ThemeConfiguration
的類,該類中具有 backgroundColor
和 fontSize
屬性。可以事先創建幾種主題, Theme 在其初始化函數中獲取一個配置類 ThemeConfiguration 的值即可。相比繼承而言,就不用創建那么多文件,以及父類中還要寫一個 setupStyle
空方法。
二、ViewController是否應統一繼承
2.1 不統一繼承的理由
如果ViewController統一繼承了父類控制器,首先可能會涉及到上面說到的高耦合的一個項目,缺點;除此之外,還會涉及上手接受成本問題,新手接受需要對父類控制器的使用有一定的了解;另外,如果涉及項目遷移問題,在遷移子類控制器的同時還要將父類控制器也遷移出去。最后一個理由是,即使不通過繼承,同樣能達到對項目控制器進行統一配置。
2.2 面向切面(AOP)思想簡介
上面也說了幾種替代繼承的方法,如果ViewController不通過繼承的方式實現,那么首選的替代方式是什么?這里我們可以采用面向切面的編程思想和分類結合的方式替代控制器的繼承。
首先簡單說下面向切面的編程思想(AOP),聽起來很高大上,實際上很多iOS開發者應該都用過,在iOS中最直接的體現就是借助 Method Swizzling 實現方法替換。一般,主要的功能是日志記錄,性能統計,安全控制,事務處理,異常處理等等。主要的意圖是:將日志記錄,性能統計,安全控制,事務處理,異常處理等代碼從業務邏輯代碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改 變這些行為的時候不影響業務邏輯的代碼。可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種技術。假設把應用程序想成一個立體結構的話,OOP的利刃是縱向切入系統,把系統劃分為很多個模塊(如:用戶模塊,文章模塊等等),而AOP的利刃是橫向切入系統,提取各個模塊可能都要重復操作的部分(如:權限檢查,日志記錄等等)。
2.3 方案實現
面向切面的思想可以實現系統資源的統一配置,iOS 中的Method Swizzling
替換系統方法可達到同樣的效果。這里筆者更為推薦使用第三方開源庫Aspects去攔截系統方法。
我們可以創建一個叫做ViewControllerConfigure的類,實現如下代碼。
//.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;
}
/*
下面的這些方法中就可以做到自動攔截了。
所以在你原來的架構中,大部分封裝UIViewController的基類或者其他的什么基類,都可以使用這種方法讓這些基類消失。
*/
#pragma mark - fake methods
- (void)loadView:(UIViewController *)viewController
{
NSLog(@" loadView");
}
- (void)viewWillAppear:(BOOL)animated viewController:(UIViewController *)viewController
{
/* 你可以使用這個方法進行打日志,初始化基礎業務相關的內容 */
NSLog(@"viewWillAppear");
}
@end
關于上面的代碼主要說三點:
1、借助 load 方法,實現代碼無任何入性型。
當類被引用進項目的時候就會執行load
函數(在main函數開始執行之前),與這個類是否被用到無關,每個類的load
函數只會自動調用一次。除了這個案列,在實際開發中筆者曾這么用過load方法,將app啟動后的廣告邏輯相關代碼全部放到一個類中的load方法,實現廣告模塊對項目的無入侵性。initialize
在類或者其子類的第一個方法被調用前調用。即使類文件被引用進項目,但是沒有使用,initialize不會被調用。由于是系統自動調用,也不需要再調用 [super initialize] ,否則父類的initialize會被多次執行。
2、不單單可以替換loadView
和viewWillAppear
方法,還可以替換控制器其他生命周期相關方法,在這些方法中實現對控制器的統一配置。如view背景顏色、統計事件等。
3、控制器中避免不了還會拓展一些方法,如無網絡數據提示圖相關方法,此時可以借助Category
實現,在無法避免使用屬性的情況下,可以借助運行時添加屬性。
關于控制器的集成問題就先說到這,接下來看看面向接口的思想。
三、面向接口思想
對于接口這一概念的支持,不同語言的實現形式不同。Java中,由于不支持多重繼承,因此提供了一個Interface關鍵詞。而在C++中,通常是通過定義抽象基類的方式來實現接口定義的。Objective-C既不支持多重繼承,也沒有使用Interface關鍵詞作為接口的實現(Interface作為類的聲明來使用),而是通過抽象基類和協議(protocol)來共同實現接口的。OC中接口可以理解為Protocol,面向接口編程可以理解為面向協議編程。先看如下兩端代碼:
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) {
}];
觀察上述兩段代碼,是否發現第二段網絡請求代碼相比第一段更容易使用。因為第二段代碼只需初始化對象,然后調用方法傳參即可,而第一段代碼要先初始化,然后設置一堆屬性,最終才能發起網絡請求。如果讓一個新手上手,毫無疑問更喜歡采用第二種方式調用方法,因為無需對AFN掌握太多,僅記住這一個方法便可發起網絡請求,而反觀 ASI 要先了解并設置各種屬性參數,最終才能發起網絡請求。上面的兩端代碼并不是為了說明ASI和AFN熟好熟劣,只是想借此引出面向接口的思想。
所以,通過接口的定義,調用者可以忽略對象的屬性,聚焦于其提供的接口和功能上。程序猿在首次接觸陌生的某個對象時,接口往往比屬性更加直觀明了,抽象接口往往比定義屬性更能描述想做的事情。
相比于OC,Swift 可以做到協議方法的具體實現,而 OC 則不行。面向對象編程和面向協議編程最明顯的區別在于程序設計過程中對數據類型的抽取(抽象)上,面向對象編程使用類和繼承的手段,數據類型是引用類型;而面向協議編程使用的是遵守協議的手段,數據類型是值類型(Swift中的結構體或枚舉)。看一個簡單的swift版面向協議范例,加入想為若干個繼承自UIView的控件擴展一個抖動動畫方法,可以按照如下代碼實現:
// Shakeable.swift
import UIKit
protocol Shakeable { }
extension Shakeable where Self: UIView {
func shake() {
// implementation code
}
}
如果想實現這個shake動畫,相關控件只要遵守這個協議就可以了。
class CustomImageView: UIImageView, Shakeable {
}
class CustomButton: UIButton, Shakeable {
}
可能有的人就會問了,直接通過 extension
實現不就可以了,這種方案是可以的。但是,如果使用extension
方式對于 CustomImageView
和 CustomButton
,根本看不出來任何抖動的意圖,整個類里面沒有任何東西能告訴你它需要抖動。相反,通過協議可以很直白的看出抖動的意圖。這僅僅是面向協議的一個小小好處,除此之外在Swift中還有很多巧妙的用法。
import UIKit
extension UIView {
func shake() {
}
}
四、多態和面向接口的選擇
4.1 多態
不同對象以自己的方式響應相同的消息的能力叫做多態。OC中最直接的體現就是父類指針指向子類對象,如:Animal *a = [Dog new];Dog *d = (Dog *)a; [d eat];
。
4.2 多態和面向接口的對比
前段時間看了Casa大神的跳出面向對象思想受益不少。所以想把自己所理解的用文字的形式記錄下來。以一個文件解析類為例,文件解析的過程中主要有兩個步驟:讀取文件和解析文件。假如實際中可能會有一些格式十分特殊的文件,所用到的文件讀取方式和解析方式不同于常規方式。通常按照繼承的寫法可能會是下面這樣。
//.h文件
@interface FileParseTool : NSObject
- (void)parse;
- (void)analyze;
@end
//.m文件
@implementation FileParseTool
- (void)parse {
[self readFile];
[self analyze];
}
- (void)readFile {
//實現代碼
....
}
- (void)analyze {
//子類要重寫該方法
}
@end
如果想實現對特殊格式文件的解析,此時可能會重寫父類的analyze
方法。
@interface SpecialFileParseTool: FileParseTool
@end
@implementation SpecialFileParseToll
- (void)analyze {
NSLog(@"%@:%s", NSStringFromClass([self class]), __FUNCTION__);
}
@end
按照繼承的寫法,會存在以下問題:
- 父類中的
analyze
會有空方法掛在那里,對于父類而言沒有任何實際意義。 - 如果架構工程師寫父類,業務工程師實現子類。那么業務工程師很可能不清楚:哪些方法需要被覆蓋重載,哪些不需要。如果子類沒有覆重方法,而父類提供的只是空方法,就很容易出問題。如果子類在覆重的時候引入了其他不相關邏輯,那么子類對象就顯得不夠單純,角色復雜了。
使用面向接口的方式實現代碼如下:
//父類.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
相比較于繼承的寫法,面向接口的寫法恰好能彌補上述三個缺陷:
- 父類中將不會再用
analyze
空方法掛在那里。 - 原本需要覆蓋重載的方法,不放在父類的聲明中,而是放在接口中去實現。基于此,公司內部可以規定:
不允許覆蓋重載父類中的方法
、子類需要實現接口協議中的方法
,可以避免繼承上帶來的困惑。子類中如果引入了父類的外部邏輯,此時通過協議的控制,原本引入了不相關的邏輯也很容易被剝離。
4.3 面向接口如何解決case大神的四個問題
casa提出使用多態面臨的四個問題:
- 父類有部分public的方法是不需要,也不允許子類覆重。
- 父類有一些特別的方法是必須要子類去覆重的,在父類的方法其實是個空方法。
- 父類有一些方法即便被覆重,父類原方法還是要執行的。
- 父類有一些方法是可選覆重的,一旦覆重,則以子類為準。
接著結合上述第二種方式,說說是如何解決這四個問題的。
關于第一個問題,在利用面向接口的方案中,公司內部可以規定:不允許覆蓋重載父類中的方法
、子類需要實現接口協議中的方法
。
關于第二個問題,第二個方案中父類FileParseTool
的.m
文件中不再存在空的analyze
方法。
關于第三個問題,顯然能在解答第一個問題中找到答案。
關于第四個問題,可能需要再補充一些代碼來解決這個問題。主要思路是:通過在接口中設置哪些方法是必須要實現,哪些方法是可選實現的來處理對應的問題,由子類根據具體情況進行覆重。代碼如下:
//父類.h文件
//流程管理相關接口,該協議可以定義子類必須實現的方法
@protocol FileParseProtocol <NSObject>
- (void)readFile;
- (void)analyze;
@end
//攔截相關接口,該協議可以定義可選的方法,子類可以根據實現情況選擇是否重載父類方法
@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 何時使用多態
- 1、如果在子類中可能被外界使用到,則應該采用多態的形式,對外提供接口;如果只是子類私有要更改的方法,則應該采用IOP更為合理。
- 2、如果引入多態之后導致對象角色不夠單純,那就不應當引入多態,如果引入多態之后依舊是單純角色,那就可以引入多態。
五、面向接口實現順序控制
5.1 函數式和鏈式編程思想
在次之前先簡單說下類似Masonry
框架的函數式和鏈式編程的實現思路。
- 鏈式編程:只需牢記方法調用完成后返回對象本身即可,返回的對象本身可以繼續調用之后的其它方法,因此可以形成鏈條,無止境的調用后續方法。
- 函數式編程:OC中主要借助block實現,通過聲明一個block,類似于定義了一個“函數”,再將這個“函數”傳遞給調用的方法,以此來實現對調用該方法時中間過程或者對結果處理的“自定義”,內部的其他環節完全不需要暴露給調用者。實際上,調用者也根本不需要知道。
5.2 函數式和鏈式實現
假如封裝一個數據庫管理工具類,借助函數式
和鏈式
編程思想,外部的調用形式可以是這樣:
NSString *sql = [SQLTool makeSQL:^(SQLTool *tool) {
tool.select(nil).from(@"").where(@"");
}];
代碼的實現可以是這樣:
//.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;
//添加這個方法,參數是一個block,傳遞一個SQLTool的實例
+ (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 篩選的結果";
//這里將自己返回出去
return self;
};
}
- (From)from{
return ^(NSString *tableName) {
self.sql = @"from 篩選的結果";
return self;
};
}
- (Where)where{
return ^(NSString *conditionStr){
self.sql = @"where 篩選的結果";
return self;
};
}
@end
雖然實現了函數式和鏈式編程思想,但是如果想讓外界調用者嚴格按照select、from、where的順序去掉用,而不是毫無順序的胡亂調用,請問這種情況該如何處理?下面會借助面向協議編程思想給出答案。
5.3 實現順序控制
關于上面的順序調用的問題,我們可以這樣想:某個類遵從了某個協議,從一定程度上講就等同于這個類就有了協議中聲明的方法可供外界調用,核心是將屬性和方法寫在協議中,遵守了該協議的對象就能直接使用相關屬性或方法。如果反過來,如果沒有遵從協議就無法調用了。ps:此處所說的調用,只是從編譯的角度出發
。具體實現請看下面代碼,總的來說沒有太高深的語法相關問題。
//.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 篩選的結果";
return self;
};
}
- (FromTwo)fromTwo{
return ^(NSString *tableName) {
self.sql = @"from 篩選的結果";
return self;
};
}
- (WhereTwo)whereTwo{
return ^(NSString *conditionStr){
self.sql = @"where 篩選的結果";
return self;
};
}
@end
按照上述實現代碼,你將只能嚴格按照selectTwo、fromTwo、whereTwo的順序執行代碼。這是因為美調用一次相關的block,返回的SQLToolTwo實例對象遵守不同的協議。
NSString *sql2 = [SQLToolTwo makeSQL:^(SQLToolTwo<ISelectable> *tool) {
tool.selectTwo(nil).fromTwo(@"").whereTwo(@"");
}];
六、總結
文章的第一部分首先說了繼承的代碼復用性和高耦合性,然后總結了繼承應當在何時使用,最后有說了四種替代繼承的方案(協議、組合、類別、配置對象);第二部分利用面向切面的思想,解決了iOS開發中關于ViewController繼承的問題;第三部分簡單介紹了面向接口的思想,以及和面向對象思想的比較;第四部分涉及多態和面向接口的抉擇問題;第五部分的實現代碼中包含函數式、鏈式以及面向接口的思想,其中重點說明了如何利用面向接口的思想控制函數的執行流程順序問題。