我们今天来谈谈"继承"

[翻译]本文翻译自objc.io创始人、iOS大神Chris Eidhof的文章, 原文链接可查看Subclassing

——————————————我是分割线——————————————

这篇文章和我平时写的文章有点不同,它不是一份标准指南,它更像一些想法和模式的汇总。以下我将要描述的几乎所有模式的弊端都是等到自己犯了错误才发现的。我绝不认为自己是使用继承的权威,但我确实想分享我学到的一些东西。

当被问及OOP(Object Oriented Programing,面向对象编程)时艾伦凯(发明者)表示,OOP不是关于类而是关于消息传递的技术。尽管如此,很多人还是过分关注创建类的层次结构。在本文中,我们将看看继承的一些有用情景,但我们主要关注复杂继承结构的替代方案。根据我们的经验,这会导致代码更简单,更易于维护。关于这个主题已经有人写了很多东西,你可以在Clean CodeCode Complete这样的书中找到,这两本书也推荐大家阅读。

什么时候用继承

首先,我们来谈谈创建子类的一些有利情景。如果您正在构建自定义布局的UITableViewCell,几乎所有的观点都一样——请创建一个子类。将布局代码移入子类是有意义的,这样您不仅可以很好地聚合代码,而且还可以在项目随处复用这个对象。

假设你的代码是针对多个平台和版本的,你需要以某种方式为每个平台和版本编写一些自定义的细节。此时创建一个OBJDevice类是有意义的,它可以有OBJIPhoneDeviceOBJIPadDevice这样的子类,甚至可以包含像OBJIPhone5Device这样更深层次的子类,这些子类覆盖了特定的方法。例如,你的OBJDevice可能包含applyRoundedCornersToView:withRadius:方法。它虽然被默认实现,但也可以被特定子类覆盖并重写。

另一种有利情况是用在实体类对象上。大多数情况下,我的实体对象会继承于一个基类,这个基类实现了isEqualhashcopyWithZone: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类非常简洁。 这是一项非常强大的技术,苹果在其自己的框架中也大量使用了它,例如UITextFieldNSLayoutManager。 有时候你还想用不同的协议将不同的方法组合在一起,它不仅有一个delegate,而且还有一个dataSource,这时你就可以参照UITableView

使用类拓展代替继承

有时候,你可能想给一个对象拓展一点点额外的功能。假设你想通过添加一个arrayByRemovingFirstObject方法来扩展NSArray。这时您可以将其归入一个类拓展,而不是使用继承。它是这样实现的:

@interface NSArray(OBJExtras)

- (void)obj_arrayByRemovingFirstObject;

@end

使用Categories技术扩展一个不属于你自己的类时,给方法名添加前缀是一个好习惯。你不这样做,有可能有人会不小心实现相同的方法,这时很容易引发意外错误。

使用类拓展的局限之一是,您最终可能会因为大量的类拓展失去你对这个类的全局观。在这种情况下,创建自定义子类可能更合理。

使用配置对象代替继承

我长期犯的错误之一是使用一个抽象类,它有很多子类会重写一个特定方法。例如,在幻灯片类型的App中,您可能会有一个类叫Theme,它拥有一些属性,如backgroundColorfont,以及在幻灯片上用于布局的一些逻辑。

接着我为每个主题创建Theme的子类,并重写一个父类方法(例如setup)来配置属性。这里直接使用父类是没有意义的。在这种情况下,你可以使用配置对象使代码更简单一些。您可以保留Theme类中的共用逻辑(例如幻灯片布局),并将配置逻辑移动到只拥有属性的简单对象中。

例如,创建一个配置类ThemeConfiguration,它拥有backgroundColorandfont等属性,并且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类切换到完全不同的东西(例如你自己的库),你可能需要重构很多代码。

更好的方法是在私有属性(或实例变量)中持有一个字典,并仅公开这两个缓存方法。现在,您可以灵活地改变其实现以满足更多需求,同时这个类的调用者也不需要重构相关代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 5继承 5.1 类、超类和子类 重用部分代码,并保留所有域。“is-a”关系,用extends表示。 已存在的类被...
    我快要上天啦阅读 763评论 1 3
  • 1.面向对象三大特性 -封装性 -继承性 -多态性 2.什么是封装 封装性就是隐藏实现细节,仅对外公开接口。 3....
    梦夜繁星阅读 1,082评论 0 6
  • 一、继承 当两个事物之间存在一定的所属关系,即就像孩子从父母那里得到遗传基因一样,当然,java要遗传的更完美,这...
    玉圣阅读 1,046评论 0 2
  • 春水初生,春林初盛,春风十里,不如你 夏河始溢,夏木始密,夏夜万星,未及卿
    呐阳光_阅读 68评论 0 0
  • 各位亲爱的小伙伴,欢迎你们参加小数点数据分析特训营(第一期) 培训时间:2017-9-16—2017-9-17 (...
    小数点数据咨询阅读 1,752评论 0 4