繼承
面向對象的三大特點:繼承、封裝、多態,其中繼承的最大優點就是代碼復用。但是很多時候繼承如果沒有限制很可能會被濫用,造成代碼結構散亂,分散到各個類中,如果想要做功能遷移,可能會拔出蘿卜帶出泥,高耦合也是繼承無法避免的問題。另外,后期維護困難,如果新人加入項目,那么掌握各個父類中的功能也是一項不小的成本。
什么是接口
接口的概念,不同的語言的實現形式不同。Java中,由于不支持多重繼承,因此提供了一個 Interface
關鍵詞。而在 C++ 中,通常是通過定義抽象基類的方式來實現接口定義的。Object-C 既不支持多重繼承,也沒有使用 Interface
關鍵詞作為接口的實現,而是通過抽象基類和協議(protocol)來共同實現接口的。OC 中的 Protocol 可以理解為接口,面向接口編程即面向協議編程。
我們使用一個小的例子來學習繼承和接口的使用。
眾所周知,人類可以是屬于動物類,狗狗也屬于動物類,動物可以跑動,吃食物等等行為。那么我們使用繼承來定義動物類、狗狗、人。
@interface Animal : NSObject
@property (copy, nonatomic) NSString* name;
-(void)run;
-(void)eat;
@end
@implementation Animal
-(void)run{
NSLog(@"%@ running.", NSStringFromClass(self.class));
}
-(void)eat{
NSLog(@"%@ eating.", NSStringFromClass(self.class));
}
@end
@interface Person : Animal
@end
@interface Dog : Animal
@end
我們定義了一個動物類,并給他跑、吃的行為動作,當人和狗狗都繼承自動物后,人和狗狗都能夠進行跑、吃的行為(繼承提高代碼復用),有時候,子類并沒滿足于父類的基本行為,此時可能需要子類重寫父類的行為(多態):
@implementation Person
-(void)run{
NSLog(@"I'm running and singing.");
}
@end
我們使用面向接口的形式來實現上述例子。首先,我們使用抽象基類和協議(protocol)來將動物的行為進行抽象:
@protocol Action <NSObject>
@property (copy, nonatomic) NSString* name;
@required // 必須實現
-(void)run;
@optional // 可選
-(void)eat;
@end
接著,我們來讓 Person
和 Dog
類遵循該行為接口并各自實現(接口只有抽象聲明,沒有實現部分)。
@interface Person : NSObject <Action>
@end
@implementation Person
@synthesize name;
-(void)run{
NSLog(@"I'm running and singing.");
}
@end
@interface Dog : NSObject <Action>
@end
@implementation Dog
@synthesize name;
-(void)run{
NSLog(@"%@ running.", NSStringFromClass(self.class));
}
-(void)eat{
NSLog(@"%@ eating.", NSStringFromClass(self.class));
}
@end.
我們可以看到,Person
和 Dog
類都沒有繼承之前的 Animal
,這意味著Person
和 Dog
類不再和 Animal
耦合 ,但是他們通過接口協議都具有了跑、吃的行為能力。
繼承的多態和面向接口對比
不同對象以自己不同的方式響應相同的消息的能力叫做多態,就如上述例子中,Person
對 -run
的重寫。
繼承的多態和面向接口對比,我們以一個文件解析類為例,文件解析的過程需要兩個步驟:讀取文件和解析文件。假如實際中可能會有一些格式十分特殊的文件,所用到的文件讀取方式和解析方式不同于常規方式。
@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 SpecialFileParseTool
- (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
相比較于繼承的寫法,面向接口的寫法幾個優勢:
- 父類中將不會再出現空載方法。
- 需要覆蓋的重載的方法,不用出現在父類的聲明中,而是放在接口中去實現。
- 子類如果引入了其他的邏輯,通過協議的控制,引入的邏輯很容易被剝離。
OC 中的委托代理
細心的童鞋應該發現,在上述文件解析工具的例子中,使用面向接口的實現方式特別像委托代理。是的,如果去掉繼承這一層關系,就完全是委托代理的使用方式。
既然說到了委托代理,我們重新來認識它一下。很多 iOS 開發工程師只知道委托代理是用來傳遞信息的,實際上,委托代理就是面向接口編程的一種示例。如果你只把它拿來做傳遞信息來使用,那就太缺乏想象力了。
回到正題,我們來分析一下 OC 中的委托代理。UITableView
是開發過程中最常見的視圖,它的使用就是典型的委托代理,即面向接口編程的示例。
UITableView
中將數據源和視圖相關抽象出來:UITableViewDataSource
和 UITableViewDelegate
,當然 UITableView
還有其他的協議,這里不一一列舉 。這些接口協議是需要我們 iOS 開發工程師去實現的,而蘋果開發工程師已經將UITableView
底層的邏輯實現,他們并不知道列表視圖的樣式,個數等等信息,這些信息需要我們開發工程師來提供。
在使用 UITableView
視圖時,我們通常讓控制器或者視圖容器遵循列表視圖的接口協議 dataSource
和 delegate
,讓控制器或者視圖容器具有實現接口協議中的功能的能力。如此,UITableView
中的 dataSource
和 delegate
便可以獲取到我們自定義視圖的信息,委托代理讓我們能夠自定義視圖(自定義功能)。
一個封裝良好的對象,通常都會使用接口編程的思想。
面向接口編程的優勢
- 降低耦合,易于程序擴展。可以在不破壞上層代碼的情況下修改甚至替換整個底層代碼,反之也一樣;
- 動態修改類的行為。在不同的條件下,由高層模塊決定具體行為(如:UITableView);
- 并行開發。在定義接口情況下,業務開發人員只需關心接口的方法,而不關心具體的實現,而底層開發人員則根據需求完善具體的實現;
- 便于單元測試。面向接口編程可以很好的將業務代碼模塊化,從而針對不同的邏輯進行測試;
- 清晰的業務功能;
- 引入的邏輯很容易被剝離,相比繼承的高耦合性,接口可以清晰的知道哪些是新引入的邏輯,通過它可以快速的將新引入的邏輯刪除。
面向接口編程的應用
- 為類添加新功能聲明
接口協議具有即插即用的能力,可以快速的成為類的組成部分,也可以快速的被移除,如通過 NSCoding
協議,你讓自定義對象具有編碼和解碼的能力。接口協議稱為類的功能代言者,你可以快速的了解到該類的具體功能。
- 委托代理的橋梁
在 OC 的委托代理模式中,接口協議就是兩個對象間的粘合劑,使用方將需求抽象成接口協議,實現方聲明遵循該接口協議并實現必要功能,為使用方提供服務。在接口協議的作用下,我們可以任意替換掉兩邊中的任意一邊而不影響到彼此。
- 限制類型
@interface Person : NSObject <Action>
@property (strong, nonatomic) id <Action> object;
-(void)initWithObject:(id <Action>)object;
@end
<協議>
的寫法實際上也是一種限制類型,一旦出現就意味著你的類必須符合協議內容或者傳遞傳遞的對象符合協議內容。
限制類型并不是指定特定類型,相比于指定特定類型,<協議>
要更靈活,如:
@interface Person : NSObject
@property (strong, nonatomic) Animal* animal;
-(void)initWithObject:(Animal*)animal;
@end
上看的寫法,Person
和 Animal
產生了直接耦合,一旦 Animal
需要替換或者刪除,你需要連同 Person
一起修改。而<協議>
僅僅需要你的對象符合接口條件,而不限制你的對象是何種類型,有著何種的其他額外屬性/功能。關聯到 Person
只需要遵循同樣的接口協議即可。
優秀的三方庫
在上一篇 iOS 依賴注入:Objection 和 Typhoon 中,我們分別介紹了依賴注入的兩個優秀的三方庫,他們都支持面向接口編程的依賴注入。
配置
-(void)configure {
// 通過接口協議,將符合BViewControllerProtocol的類進行綁定
[self bindClass:BViewController.class toProtocol:@protocol(BViewControllerProtocol)];
}
通過注入器獲取符合該協議的實例對象
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
JSObjectionInjector* injector = [JSObjection defaultInjector];
UIViewController <BViewControllerProtocol>* nextCtrl = [injector getObject:@protocol(BViewControllerProtocol)];
nextCtrl.bgColor = UIColor.redColor;
nextCtrl.others = @"something...";
[self.navigationController pushViewController:nextCtrl animated:YES];
}
Objection “隱藏” 掉了 BViewControllerProtocol
接口協議實現方,而你只需要關注接口協議中的信息即可。
配置
@interface MyAssembly : TyphoonAssembly
-(id<Action>)animal;
@end
@implementation MyAssembly
-(id<Action>)animal{
return [TyphoonDefinition withClass:Animal.class];
}
@end
獲取符合接口協議的對象
MyAssembly* assembly = [[MyAssembly new] activated];
Animal* animal = assembly.animal;
和 Objection 一樣,Typhoon 只需要你將實現接口協議的對象類型進行綁定即可。
這兩個三方庫主要的功能還是依賴注入的部分,有興趣的小伙伴可以研究一下。
總結
本文開篇首先提出了繼承的濫用造成的窘境,進而引入面向接口編程的思想。不是說繼承不好,繼承讓代碼的復用性提高,在某些情況下,使用繼承有著相當的優勢。繼承的高耦合高內聚的特點讓它在某些情況下并不是很友好,例如功能遷移。接口協議的出現,一定程度減少了耦合度,讓一些沒有關聯性,卻具有相同能力的對象有了相同的抽象聲明,對象只需要遵循接口協議并實現就能具有一定的功能(比如:飛機和鳥類都具有飛行的能力,但他們可能并不是來自同一父類,我們將飛行能力抽象成協議,讓飛機和鳥類遵循實現,那么飛機和鳥類都獲得飛行的能力)。
在 OC 中有著非常多的面向接口例子,比如經典的委托代理模式,我們通過接口協議讓不同的控制器、視圖容器甚至只是普通類都能為UITableView
提供 cell 樣式,cell 個數等等(不同的接口協議可以有著不同的響應方式,因此接口協議也是一種多態的表現);再比如我們讓自定義的類遵循 NSCopying
和 NSMutableCopying
協議,我們就可以復制自定義的對象,遵循 NSCoding
協議就可以讓自定義對象具有了編碼和解碼的能力,并可以保存到本地。
在合作開發過程中,我們可以利用面向接口編程達到類與類、模塊和模塊的解耦,便于項目的迭代更新。利用好接口協議會有非常多的好處,從此刻開始,讓面向接口編程的思想深入到你的項目中去吧!