[翻译]本文翻译自objc.io创始人、iOS大神Chris Eidhof的文章, 原文链接可查看Subclassing。
——————————————我是分割线——————————————
这篇文章和我平时写的文章有点不同,它不是一份标准指南,它更像一些想法和模式的汇总。以下我将要描述的几乎所有模式的弊端都是等到自己犯了错误才发现的。我绝不认为自己是使用继承的权威,但我确实想分享我学到的一些东西。
当被问及OOP(Object Oriented Programing,面向对象编程)时艾伦凯(发明者)表示,OOP不是关于类而是关于消息传递的技术。尽管如此,很多人还是过分关注创建类的层次结构。在本文中,我们将看看继承的一些有用情景,但我们主要关注复杂继承结构的替代方案。根据我们的经验,这会导致代码更简单,更易于维护。关于这个主题已经有人写了很多东西,你可以在Clean Code和Code Complete这样的书中找到,这两本书也推荐大家阅读。
什么时候用继承
首先,我们来谈谈创建子类的一些有利情景。如果您正在构建自定义布局的UITableViewCell
,几乎所有的观点都一样——请创建一个子类。将布局代码移入子类是有意义的,这样您不仅可以很好地聚合代码,而且还可以在项目随处复用这个对象。
假设你的代码是针对多个平台和版本的,你需要以某种方式为每个平台和版本编写一些自定义的细节。此时创建一个OBJDevice
类是有意义的,它可以有OBJIPhoneDevice
和OBJIPadDevice
这样的子类,甚至可以包含像OBJIPhone5Device
这样更深层次的子类,这些子类覆盖了特定的方法。例如,你的OBJDevice
可能包含applyRoundedCornersToView:withRadius:
方法。它虽然被默认实现,但也可以被特定子类覆盖并重写。
另一种有利情况是用在实体类对象上。大多数情况下,我的实体对象会继承于一个基类,这个基类实现了isEqual
,hash
,copyWithZone:
和description
等方法。这些方法通过迭代属性实现一次,使得代码更不容易出错。 (如果你正在寻找这样的基类,你可以考虑使用Mantle)
何时不要用继承
在我曾工作过的很多项目中,我见过许多深层的子类结构。我自己也犯过这样的错误。除非继承结构很浅,否则你很快看到它的局限性。
幸运的是,如果你发现自己处于这样的深层次继承结构中,那么你有很多其他选择。在下文我们将更详细地介绍每种替代方案。如果你的子类们仅仅共用相同方法名,协议可能是一个很好的选择。如果你知道一个对象需要经常改动,你可能需要使用协议来动态更改和配置它。当你想给现有对象拓展功能时,类拓展可能是个更好的选择。当你有很多子类需要重写相同的父类方法时,你可以使用配置对象。最后,当你想重用某些功能时,最好是组合多个对象而不是扩展它们。
代替方案
使用协议代替继承
通常,当你想确保一个对象会响应某些消息时你会使用继承。假设你有一个可以播放视频的播放器对象。现在你想让它支持YouTube,此时你会用到相同的方法名,但内部实现不同。你可以用继承实现这一需求:
@interface Player:NSObject
- (void)play;
- (void) pause;
@end
@interface YouTubePlayer:Player
- (void)play;
- (void) pause;
@end
这两个类很可能不共用很多代码,但它们有相同的方法名。在这种情况下,使用协议可能是一种更好的解决方案。使用协议,您可以像这样编写代码:
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@interface Player:NSObject <VideoPlayer>
@end
@interface YouTubePlayer:NSObject <VideoPlayer>
@end
这样,YouTubePlayer
不需要知道Player
的内部实现。
使用代理代替继承
再次假设你有一个像上例中的Player
类。现在,你可能要在play
方法中执行自定义操作。一种相对容易的做法是创建一个自定义的子类重写play
方法,先调用[super play]
,然后执行自定义操作。这是一种方法,另一种方法则是给你的Player
对象设置一个代理。例如:
@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
方法中,delegate
可以接收playerDidStartPlaying:
消息。 比起使用继承,现在这个类的任何调用者可以通过代理协议方法实现自定义需求,而且还可以保证Player
类非常简洁。 这是一项非常强大的技术,苹果在其自己的框架中也大量使用了它,例如UITextField
、NSLayoutManager
。 有时候你还想用不同的协议将不同的方法组合在一起,它不仅有一个delegate
,而且还有一个dataSource
,这时你就可以参照UITableView
。
使用类拓展代替继承
有时候,你可能想给一个对象拓展一点点额外的功能。假设你想通过添加一个arrayByRemovingFirstObject
方法来扩展NSArray
。这时您可以将其归入一个类拓展,而不是使用继承。它是这样实现的:
@interface NSArray(OBJExtras)
- (void)obj_arrayByRemovingFirstObject;
@end
使用Categories技术扩展一个不属于你自己的类时,给方法名添加前缀是一个好习惯。你不这样做,有可能有人会不小心实现相同的方法,这时很容易引发意外错误。
使用类拓展的局限之一是,您最终可能会因为大量的类拓展失去你对这个类的全局观。在这种情况下,创建自定义子类可能更合理。
使用配置对象代替继承
我长期犯的错误之一是使用一个抽象类,它有很多子类会重写一个特定方法。例如,在幻灯片类型的App中,您可能会有一个类叫Theme
,它拥有一些属性,如backgroundColor
和font
,以及在幻灯片上用于布局的一些逻辑。
接着我为每个主题创建Theme
的子类,并重写一个父类方法(例如setup
)来配置属性。这里直接使用父类是没有意义的。在这种情况下,你可以使用配置对象使代码更简单一些。您可以保留Theme类中的共用逻辑(例如幻灯片布局),并将配置逻辑移动到只拥有属性的简单对象中。
例如,创建一个配置类ThemeConfiguration
,它拥有backgroundColorand
、font
等属性,并且Theme
类可以在初始化时获取这个类。
使用组合代替继承
继承最强大的替代方法是组合。如果你想复用现有代码,但这些代码的方法名不同,组合将是一项强力手段。例如,假设你正在设计一个缓存类:
@interface OBJCache:NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
一个简单的做法是继承于NSDictionary
并通过调用字典的方法来实现这两种方法:
@interface OBJCache : NSDictionary
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
@implementation OBJCache
- (void)cacheValue:(id)value forKey:(NSString *)key{
[self setValue:value forKey:key];
}
@end
但是这样做会有一些问题。它用字典实现的事实应该是一个实现细节。现在当你在任何地方想传一个NSDictionary
参数时,你都有可能传了OBJCache
值。当你想把OBJCache
类切换到完全不同的东西(例如你自己的库),你可能需要重构很多代码。
更好的方法是在私有属性(或实例变量)中持有一个字典,并仅公开这两个缓存方法。现在,您可以灵活地改变其实现以满足更多需求,同时这个类的调用者也不需要重构相关代码。