简述
多态一般都要跟继承结合起来说,其本质是子类通过覆盖或重载,父类的方法,来使得对同一类对象同一方法的调用产生不同的结果。这里需要辨析的地方在:同一类对象指的是继承层级再上一层的对象,更加泛化。
使用多态面临的问题
父类有部分public的方法是不需要,也不允许子类覆重
对于客户程序员来说,他们是有动机去覆重那些不需要覆重的方法的,比如需要在某个方法调用的时候做UserTrack,或者希望在方法调用之前做一些额外的事情,但是又找不到外面应该在哪儿做,于是就索性覆重一个了。
这样做的缺点在于使得一个对象引入了原本不属于它的业务逻辑。如果在引入的这些额外逻辑中又对其他模块产生依赖,那么这个对象在将来的代码复用中就会面临一个艰难的选择:1.是把这些不必要的逻辑删干净然后移过去?2.还是所以把依赖连带着这个对象一起copy过去?前者太累,后者太蠢。
如果是要针对原来的对象进行功能拓展,但拓展的时候发现是需要针对原本不允许覆重的函数进行操作,那么这时候就有理由怀疑父类当初是不是没有设计好了。父类有一些特别的方法是必须要子类去覆重的,在父类的方法其实是个空方法
这非常常见,由于逻辑的主要代码在父类中,若要跑完整个逻辑,则需要调用一些特定的方法来基于不同的子类获得不同的数据,这个特定的方法最终交由子类通过覆重来实现。如果不在父类里面写好这个方法吧,父类中的代码在执行逻辑的时候就调用不到。如果写了吧,一个空函数放在那儿十分难看。
也有的时候客户程序员会不知道在派生之后需要覆重某个方法才能完成完整逻辑,因为空函数在那儿不会导致warning或error,只有在发现程序运行结果不对的时候,才会感觉哪儿有错。如果这时候程序员发现原来是有个方法没覆重,一定会拍桌子骂娘。
总结一下,其实就是代码不好看,以及有可能忘记覆重。父类有一些方法是可选覆重的,一旦覆重,则以子类为准
这是大多数面向对象语言默认的行为。设计可选覆重的动机其中有一个就是可能要做拦截器,在每个父类方法调用时,先调一个willDoSomething(),然后调用完了再调一个didFinishedSomething(),由子类根据具体情况进行覆重。
一般来说这类情况如果正常应用的话,不会有什么问题,就算有问题,也是前面提到的容易使得一个对象引入原本不属于它的业务逻辑。父类有一些方法即便被覆重,父类原方法还是要执行的
这个是经典的坑,尤其是交付给客户程序员的时候是以链接库的模式交付的。父类的方法是放在覆重函数的第一句调用呢还是放在最后一句调用?这是个值得深思的问题。更有甚者索性就直接忘记调用了,各种傻傻分不清楚。
这四种情况在大多数支持多态的语言里面都没有做很好的原生限制,在程序规模逐渐变大的时候,会给维护代码的程序员带来各种各样的坑。
解决方案
面向接口编程(Interface Oriented Programming, IOP)是解决这类问题比较好的一种思路。
举个例子 ~
当一个对象的主要业务功能是搜索,那么它在整个程序里面扮演的角色是搜索者的角色。在基于搜索派生出的业务中,会做一些跟搜索无关的事情,比如搜索后进行人工加权重排列表,搜索前进行关键词分词(假设分词方案根据不同的派生类而不同)。那么这时候如果采用多态的方案,就是由子类覆重父类关于重排列表的方法,覆重分词方法。如果在编写子类的程序员忘记这些必要的覆重或者覆重了不应该覆重的方法,就会进入上面提到的四个困境。所以这时候需要提供一套接口,规范子类去做覆重,从而避免之前提到的四种困境
我们在search.h中先定义一个SearchManager接口,这个接口里面含有原本需要被覆重的方法, 并声明一个对象实现这个接口方法。
import
@protocol SearchManager
- (void) split();
- (void) resort();
@end
@interface Search : NSObject
@property (nonatomic,weak) id< SearchManager > manager;
- (void)search;
@end
在search.m文件中
-
(void)search{
...//self.assistant可以就是self,也可以由初始化时候指定为其他对象,将来进行业务剥离的时候,只要将assistant里面的方法剥离或者讲assistant在初始化时指定为其他对象也好。
if(self. manager && [self. manager respondsToSelector:@selector(split)]){
self.manager.split()
}
...
if(self. manager && [self. manager respondsToSelector:@selector(resort)]){
self.manager. resort()
}
...
继承search ,创建一个subSearch对象,在subSearch.m文件中,指定manager为自己,并实现接口方法
@interface subSearch ()< SearchManager >@end
@implementation testTwo
- (id)init{
if(self = [super init]){
self. manager = self;
}
return self;
}
//由于子类被接口要求必须实现split()和resort()方法,因而规避了前文提到的风险,在剥离业务的时候也能非常方便。
- (void) split(){
NSLog(@"检索");
} - (void) resort(){
NSLog(@"排序");
}
什么时候用多态
由于多态和继承紧密地结合在了一起,我们假设父类是架构师去设计,子类由客户程序员去实现,那么这个问题实际上是这样的两个问题:
- 作为架构师,我何时要为多态提供接入点?
- 作为客户程序员,我何时要去覆重父类方法?
这本质上需要程序员针对对象建立一个角色的概念,越单纯的角色就越容易维护。还有一个就是区分被覆重的方法是否需要被外界调用的问题:
- 如果引入多态之后导致对象角色不够单纯,那就不应当引入多态,如果引入多态之后依旧是单纯角色,那就可以引入多态
- 如果要覆重的方法是角色业务的其中一个组成部分,例如split()和resort(),那么就最好不要用多态的方案,用IOP,因为在外界调用的时候其实并不需要通过多态来满足定制化的需求。
好了,现在我们回到这一节前面提出的两个问题:何时引入接入点和何时采用覆重。针对第一个问题架构师一定要分清楚角色,在保证角色单纯的情况下可以引入多态。另外一点要考虑被覆重的方法是否需要被外界使用,还是只是父类运行时需要子类通过覆重提供中间数据的。如果是只要子类通过覆重提供中间数据的,一律应当采用IOP而不是多态。
总结
多态在面向对象程序中的应用相当广泛,只要有继承的地方,或多或少都会用到多态。然而多态比起继承来,更容易被不明不白地使用,一切看起来都那么顺其自然。在客户程序员这边,一般是只要多态是可行方案的一种,到最后大部分都会采用多态的方案来解决问题。
然而多态正如它名字中所暗示的,它有非常大的潜在可能引入不属于对象初衷的逻辑,巨大的灵活性也导致客户程序员在面对问题的时候不太愿意采用其他相对更优的方案,比如IOP。在决定是否采用多态时,我们要有一个清晰的角色概念,做好角色细分,不要角色混乱。该是拦截器的,就给他制定一个拦截器接口,由另一个对象(逻辑上的另一个对象,当然也可以是自己)去实现接口里的方法集。不要让一个对象在逻辑上既是拦截器又是业务模块。这样才方便未来的维护。另外也要注意被覆重方法的作用,如果只是单纯为了提供父类所需要的中间数据的,一律都用IOP,这是比直接采用多态更优的方案。
IOP能够带来的好处当然不止文中写到的这些,它在其他场合也有非常好的应用,它最主要的好处就在于分离了定义和实现,并且能够带来更高的灵活性,灵活到既可以对语言过高的自由度有一个限制,也可以灵活到允许同一接口的不同实现能够合理地组合。在架构设计方面是个非常重要的思想。