前言
在開篇之前思考幾個問題?
1、繼承最大的缺點是什麼?
2、為什麼說耦合也可能是一種需求?
3、有哪些場景不適合使用繼承?
4、繼承本身就具有高耦合性,但卻可以實現代碼復用,有哪些替代方案可以去除高耦合性並實現代碼的復用?
5、iOS 開發中有否有必要同一派生 ViewController?
6、什麼是面向切面編程思想?
7、為什麼Swift著力宣傳面向協議的思想,而OC 中面向協議的思想為什麼不能像Swift那樣得以普及?
8、函數式鏈式編程中如何對外控制函數調用的先後順序?如:Masonry (面向接口解決問題)
在接下來的分析中,這些問題都會一一得到解答,保證乾貨滿滿。筆者原本想著圍繞繼承和面向接口各寫一片文章,但實際繼承和面向接口在某些方面還有很多的關聯性,因此這裡索性合二為一。
一、繼承 (優缺點、使用原則、替代方案)
二、ViewController是否應統一繼承
三、面向接口思想
四、多態和面向接口的選擇
五、面向接口實現順序控制
繼承、封裝、多態是面向對象的三大支柱。關於繼承毫無疑問最大的優點是代碼復用。但是很多時候繼承也可能會被無止境的濫用,造成代碼結構散亂,後期維護困難等,其中有可能帶來最大的問題是高耦合。
1.2 繼承的使用的原則假設你的代碼是針對多平臺多版本的,並且你需要針對每個平臺每個版本寫一些代碼。這時候更合理的做法可能是創建一個 OBJDevice 類,讓一些子類如 OBJIPhoneDevice 和 OBJIPadDevice ,甚至更深層的子類如 OBJIPhone5Device 來繼承,並讓這些子類重寫特定的方法。關於這個場景就非常適合使用繼承,因為總的來說它滿足如下條件:
父類OBJDevice只是給其他派生的子類提供服務,OBJDevice只做自己分內的事情,並不涉及子類的業務邏輯。不同的業務邏輯由不同的子類自己去完成。子類和父類各做自身的事情,互不影響和幹擾。
父類OBJDevice 的變化要在所有子類中得以體現。也就是說父類牽一動發全部子類,可以理解為此時的高耦合是一種需求,而不是一種缺點。
如果滿足上述兩種條件,可以考慮使用繼承。另外,實際開發中如果繼承超過2層的時候,就要慎重這個繼承的方案了,因為這可能是濫用繼承的開始。
針對不適合用繼承來做的事,或不想用繼承來做的,還有如下幾種備選方案可以適合不同的場景,有利於打開你的思路。
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
如果想重用已有的代碼而不想共享同樣的接口,組合便是首選。
假如: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可以作為一個模塊,本地搜索提示和伺服器搜索提示可以作為不同的模塊分別處理。實際使用中可以通過不同的模塊組合,實現不同的功能。
有時可能會想在一個對象的基礎上增加額外的功能形成另外一個對象,繼承是一種很容易想到的方法。還有另外一種比較好的方案是通過類別。為該對象擴展方法,按需調用,比如為NSArray增加一個移除第一個元素的方法:
@interface NSArray (OBJExtras)- (void)removingFirstObject;@end
假設某個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的利刃是橫向切入系統,提取各個模塊可能都要重複操作的部分(如:權限檢查,日誌記錄等等)。
面向切面的思想可以實現系統資源的統一配置,iOS 中的Method Swizzling替換系統方法可達到同樣的效果。這裡筆者更為推薦使用第三方開源庫Aspects去攔截系統方法。
我們可以創建一個叫做ViewControllerConfigure的類,實現如下代碼。
{
[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;
}#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的控制項擴展一個抖動動畫方法,可以按照如下代碼實現:
import UIKit
protocol Shakeable { }
extension Shakeable where Self: UIView {
func shake() {
}
}
如果想實現這個shake動畫,相關控制項只要遵守這個協議就可以了。
class CustomImageView: UIImageView, Shakeable {
}class CustomButton: UIButton, Shakeable {
}
可能有的人就會問了,直接通過 extension實現不就可以了,這種方案是可以的。但是,如果使用extension方式對於 CustomImageView 和 CustomButton,根本看不出來任何抖動的意圖,整個類裡面沒有任何東西能告訴你它需要抖動。相反,通過協議可以很直白的看出抖動的意圖。這僅僅是面向協議的一個小小好處,除此之外在Swift中還有很多巧妙的用法。
import UIKit
extension UIView {
func shake() {
}
}
不同對象以自己的方式響應相同的消息的能力叫做多態。OC中最直接的體現就是父類指針指向子類對象,如:Animal *a = [Dog new];Dog *d = (Dog *)a; [d eat];。
前段時間看了Casa大神的跳出面向對象思想受益不少。所以想把自己所理解的用文字的形式記錄下來。以一個文件解析類為例,文件解析的過程中主要有兩個步驟:讀取文件和解析文件。假如實際中可能會有一些格式十分特殊的文件,所用到的文件讀取方式和解析方式不同於常規方式。通常按照繼承的寫法可能會是下面這樣。
@interface FileParseTool : NSObject
- (void)parse;
- (void)analyze;
@end
@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
按照繼承的寫法,會存在以下問題:
使用面向接口的方式實現代碼如下:
@protocol FileParseProtocol <NSObject>
- (void)readFile;
- (void)analyze;
@end
@interface FileParseTool : NSObject
@property (nonatomic, weak) id<FileParseProtocol> assistant;
- (void)parse;
@end
@implementation FileParseTool
- (void)parse {
[self.assistant readFile];
[self.assistant analyze];
}
@end
@interface SpecialFileParseTool: FileParseTool <FileParseProtocol>
@end
@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空方法掛在那裡。
原本需要覆蓋重載的方法,不放在父類的聲明中,而是放在接口中去實現。基於此,公司內部可以規定:不允許覆蓋重載父類中的方法、子類需要實現接口協議中的方法,可以避免繼承上帶來的困惑。子類中如果引入了父類的外部邏輯,此時通過協議的控制,原本引入了不相關的邏輯也很容易被剝離。
casa提出使用多態面臨的四個問題:
父類有部分public的方法是不需要,也不允許子類覆重。
父類有一些特別的方法是必須要子類去覆重的,在父類的方法其實是個空方法。
父類有一些方法即便被覆重,父類原方法還是要執行的。
父類有一些方法是可選覆重的,一旦覆重,則以子類為準。
接著結合上述第二種方式,說說是如何解決這四個問題的。
關於第一個問題,在利用面向接口的方案中,公司內部可以規定:不允許覆蓋重載父類中的方法、子類需要實現接口協議中的方法。
關於第二個問題,第二個方案中父類FileParseTool的.m文件中不再存在空的analyze方法。
關於第三個問題,顯然能在解答第一個問題中找到答案。
關於第四個問題,可能需要再補充一些代碼來解決這個問題。主要思路是:通過在接口中設置哪些方法是必須要實現,哪些方法是可選實現的來處理對應的問題,由子類根據具體情況進行覆重。代碼如下:
@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
@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
@interface SpecialFileParseTool: FileParseTool<FileParseProtocol,InterceptorProtocol>
@end
@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
在次之前先簡單說下類似Masonry框架的函數式和鏈式編程的實現思路。
假如封裝一個資料庫管理工具類,藉助函數式和鏈式編程思想,外部的調用形式可以是這樣:
NSString *sql = [SQLTool makeSQL:^(SQLTool *tool) {
tool.select(nil).from(@"").where(@"");
}];
代碼的實現可以是這樣:
#import <Foundation/Foundation.h>
@class SQLTool;
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;
+ (NSString *)makeSQL:(void(^)(SQLTool *tool))block;
@end
#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的順序去掉用,而不是毫無順序的胡亂調用,請問這種情況該如何處理?下面會藉助面向協議編程思想給出答案。
關於上面的順序調用的問題,我們可以這樣想:某個類遵從了某個協議,從一定程度上講就等同於這個類就有了協議中聲明的方法可供外界調用。如果反過來,如果沒有遵從協議就無法調用了。ps:此處所說的調用,只是從編譯的角度出發。具體實現請看下面代碼,總的來說沒有太高深的語法相關問題。
#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
#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繼承的問題;第三部分簡單介紹了面向接口的思想,以及和面向對象思想的比較;第四部分涉及多態和面向接口的抉擇問題;第五部分的實現代碼中包含函數式、鏈式以及面向接口的思想,其中重點說明了如何利用面向接口的思想控制函數的執行流程順序問題。
作者:ZhengYaWei
連結:https://www.jianshu.com/p/39e6a8409476
相關推薦: