iOS架构师之路:慎用继承

  最近在看大神Casa的文章《跳出面向对象思想(一) 继承》,脑洞大开。文章给咱们展现了一个随着产品需求不断变化的例子,该例子中经过继承实现不一样页面的搜索视图和搜索逻辑的代码复用,随着产品需求的演变,最后致使继承的搜索功能层级愈来愈深,相互依赖愈来愈严重,最后致使拔出萝卜带出泥,又随着个性化需求的发展,最后代码变得愈来愈混乱。相信有经验的开发人员都经历过这方面的痛苦。继承对代码复用来讲很是好用,但同时继承复用的思路随着产品经理的需求变化会致使项目紧耦合,牵一发而动全身。继承作面向对象的三大特性之一,固然宅正确的时候使用它能发挥巨大价值,但若是不加思索的使用也会带来代码维护和扩展上的灾难。那咱们应该在什么状况下使用继承,什么状况下又须要避免滥用继承,并可以有其余方案替代继承实现代码复用的目的呢,这是做为架构师须要掌握的内功,最终达到十八般武艺,样样精通,须要时信手拈来的境界。html

 

适用继承的场合缓存

      大神Chris Eidhof的文章《subclassing》提到须要自定义UITableViewCell等View视图的时候咱们可使用继承来建立自定义View,这些代码放入子类更合理,不光代码获得更好的封装,也能获得一个能够在工程中重用的组件。Chris Edihof还提到model能够继承来实现了 isEqual: 、hash 、 copyWithZone: 和 description 等方法的类。架构

Chris Eidhof说的只是继承的几个应用场景,他没有说使用继承的界限。 Casa还提到是否使用继承须要考虑三个点:框架

1. 父类只是给子类提供服务,并不涉及子类的业务逻辑less

2. 层级关系明显,功能划分清晰,父类和子类各作各的。ide

3. 父类的全部变化,都须要在子类中体现,也就是说此时耦合已经成为需求函数

  在我看来一个很重要的原则就是咱们不能脱离cocoa框架开发,因此咱们能够继承cocoa的类,以达到快速开发的目的,可是若是没有特殊缘由咱们写的代码要控制在继承链不增长两层。ui

 

不适合继承的场景this

我比较赞成Casa的观点。当你发现你的继承超过2层的时候,你就要好好考虑是否这个继承的方案了,第三层继承正是滥用的开端。肯定有必要以后,再进行更多层次的继承。Chris Eidhof也有相似的观点:In a lot of projects that I’ve worked on, I’ve seen deep hierarchies of subclasses. I am guilty of doing this as well. Unless the hierarchies are very shallow, you very quickly tend to hit limits. 在我工做的许多项目中看到过一些深度继承的项目。当我也这么干的时候,总会感到内疚。除非继承的层次很是浅,不然你会很快发现它的局限性。atom

 

替代继承解决复用需求的解决方案

      1.协议(protocols)

  我常用继承来使得对象可以响应某个方法,假设一个APP有播放器(player)对象,它拥有播放(play)方法播放视频,若是APP但愿支持YouTube,须要相同几个播放(player)接口,可是方法的实现不一样,经过继承实现的代码以下:

@interface Player : NSObject

- (void)play;

- (void)pause;
 
@end

@interface YouTubePlayer : Player


@end

  这两个类并无太多共用的代码,它们只不过具备相同的接口。若是这样的话,使用协议可能会是更好的方案。能够这样用协议来写你的代码。

@protocol VideoPlayer <NSObject>

- (void)play;
- (void)pause;

@end


@interface Player : NSObject <VideoPlayer>

@end


@interface YouTubePlayer : NSObject <VideoPlayer>

@end

  这样,YouTubePlayer 类就没必要知道 Player 类的内部实现了。

      2.代理(delegation)            

再以上面的例子为例,player对象但愿在播放的时候执行一些自定义的行为,使用继承也能够轻易的实现:建立个player对象的子类,而后重写play方法,再调用[super play],再跟着但愿执行的行为。可是咱们也能够经过的代理的方式更有优雅的实现这个需求:

@class Player;

@protocol PlayerDelegate

- (void)playerDidStartPlaying:(Player *)player;

@end


@interface Player : NSObject

@property (nonatomic,weak) id<PlayerDelegate> delegate;

- (void)play;
- (void)pause;

@end

  如今在player对象的play方法里,咱们能够经过代理属性调用 playerDidStartPlaying:方法,任何使用Player类的对象,能够遵照代理协议,就能够实现自定义的playerDidStartPlaying:方法了,player类依然保持它的通用性和独立性,方便为对外提供服务。代理是很是强大技巧,苹果自己就常用。像 UITextField 这样的类,有时候你还会想把几个不一样的方法分组到几个单独的协议里,好比UITableView —— 它不只有一个代理(delegate),还有一个数据源(dataSource)。

  3.类别(category)

  咱们有时候会给对象添加方法,经过集成的方式固然能够实现,可是不如category的方式来的方便和容易使用,不增长新的类,可复用的价值也更高。 好比咱们须要给NSArray添加一个arrayByRemovingFirstObject方法,经过category的方式咱们就能够这么作:

@interface NSArray (OBJExtras)

- (void)obj_arrayByRemovingFirstObject;

@end

  在用类别扩展一个不是你本身的类的时候,在方法前添加前缀是个比较好的习惯作法。若是不这么作,有可能别人也用类别对此类添加了相同名字的函数。那时候程序的行为可能跟你想要的并不同,未预期的事情可能会发生。

  使用类别还有另一个风险,那就是,到最后你可能会使用一大堆的类别,连你本身都会失去对代码全局的认识。假如那样的话,建立自定义的类可能更简单一些。

  4.组合(composition)

  Casa提到咱们尽量用组合替代继承。组合是最强大的替代组合的选项。若是你想复用已经存在的代码,而且不想共享一样的接口,组合是最佳选择。举个例子,假设你要设计一个缓存类:

@interface OBJCache : NSObject

- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;

@end

  一个简单的作法就经过聚成NSDictionary而且经过调用字典的方法来实现这上面两个缓存方法。

@interface OBJCache : NSDictionary

  可是这样作会带来一些问题。它原本是应该被详细实现的,但只是经过字典来实现。如今,在任何须要一个 NSDictionary 参数的时候,你能够直接提供一个 OBJCache 值。但若是你想把它转为其它彻底不一样的东西(例如你本身的库),你就可能须要重构不少代码了。

  更好的方式就是组合了。建立一个缓存类,并将添加一个字典的私有属性,对外仍是暴露着两个接口,实现的时候就能够经过调用字典属性的方法实现咱们使用字典的方法了,这样作能够灵活改变其涉嫌,而该类的使用者恩不用进行重构。

总结

  代码复用,尽管他们均可以经过继承实现,可是咱们为了在没有耦合需求的时候尽可能不要使用继承,而是根据不一样场景采用不一样复用代码的方式。若是只是共享接口,咱们可使用协议;若是是但愿共用一个方法的部分实现,但但愿根据须要执行不一样的其余行为,咱们可使用代理;若是是添加方法,咱们能够优先使用类别(category);若是是为了使用一个类的不少方法,咱们可使用组合来实现。,若是当初只是出于代码复用的目的而不区分类别和场景,就采用继承是不恰当的。当你发现你的继承超过2层的时候,你就要好好考虑是否这个继承的方案了,第三层继承正是滥用的开端。肯定有必要以后,再进行更多层次的继承。我认同Casa的见解:万不得已不要用继承,优先考虑组合等方式。

 

参考文章:《跳出面向对象思想(一) 继承》、《subclassing》

相关文章
相关标签/搜索