继承
面向对象的三大特点:继承、封装、多态,其中继承的最大优点就是代码复用。但是很多时候继承如果没有限制很可能会被滥用,造成代码结构散乱,分散到各个类中,如果想要做功能迁移,可能会拔出萝卜带出泥,高耦合也是继承无法避免的问题。另外,后期维护困难,如果新人加入项目,那么掌握各个父类中的功能也是一项不小的成本。
什么是接口
接口的概念,不同的语言的实现形式不同。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
协议就可以让自定义对象具有了编码和解码的能力,并可以保存到本地。
在合作开发过程中,我们可以利用面向接口编程达到类与类、模块和模块的解耦,便于项目的迭代更新。利用好接口协议会有非常多的好处,从此刻开始,让面向接口编程的思想深入到你的项目中去吧!
网友评论