子类化——by Chris Eidhof
这篇文章和我通常写的文章有所不同。它不是一个指南更像是一系列想法和模式。我将要讲述的差不多所有模式都是通过犯错误这样艰难的方式找出来的。绝不是说我就是子类化方面的权威,但是,我只是想把我学到的一些事情分享出来。不要把它作为一个权威指南,而是一个例子集合。
当被问及面向对象编程时(OOP
),Alan Kay(创造者)写到,他不是关于类的,而是消息传递^?。 但,仍然有很多人关注于创建类层次。在这篇文章中,我们将看几个有用的案例,但是我们主要关注创建复杂的类层次结构。在我们的经验中,这会让代码更加简单,更容易维护。关于这个话题有很多文章,你可以在Clean Code和Code Complete这样的书中找到,推荐大家阅读这两本书。
什么时候子类化
首先,让我们讨论一下在哪些情况下创建子类是有意义的。如果你正在构建一个有自定义布局的UITableViewCell
,那么你需要创建一个子类。几乎所有的观点都是如此;一旦你开始布局,把它移到子类里就变得有意义了,这样你不仅优雅的整理了你的代码,还让代码在整个项目都可复用。
假设,你的代码是针对不同的平台和版本的,你需要为每个平台和版本编写自定义的部分。这时创建一个OBJDevice
类就有意义了,它可以子类化出OBJIphoneDevice
和OBJIPadDevice
,甚至可以更深层的子类化出OBJIPhone5Device
,它们可以重写特定的方法。例如,你的OBJDevice
类可以包含applyRoundedCornersToView:withRadius:
方法。它有一个默认的实现,但是这个方法可以被特定的子类重写。
另外一种非常有用的子类化情形是在模型对象(model object
)中。绝大多数时候,模型对象从类中继承的实现方法有isEqual:
、hash
、copyWithZone:
和description
。这些方法通过对属性迭代实现一次,很难出错。(如果你想找这样的基类,可以考虑用Mantle,它正事这样做的,而且不仅仅如此。)
什么时候不子类化
我参与过很多的项目,我也见过深层次的子类化。很惭愧我也这样做过。除非层次很浅,否则很快就会达到极限。
幸运的是,如果你发现自己正处于这样深的层次,这有很多可选方案。在下面的小节中,我们会深入分析每一部分。如果你的子类只是共享相同的接口和协议,这是一个很好的选择。如果你知道一个对象需要修改很多地方,你可能需要使用委托动态的改变和配置它。当你想要对已存在的对象扩展一些简单地功能时,类别(category
)可能是一个选择。当你有一系列子类,每一个都重写(override
)同样的方法,你可能会使用配置对象。最后,当你想重用一些功能时,最好组合多个对象而不是扩展他们。
可选方案
方案:协议
通常的,一个使用子类的原因是你想要保证一个对象响应一个确定的消息。设想一下,在一个应用中你有一个可以播放视频的播放器(player
)对象。现在,如果你想增加YouTobe
的支持,你需要同样的接口,但是不同的实现。一种是用子类实现的方式如下:
@interface Player: NSObject
- (void)play;
- (void)pause;
@end
@interface YouTobePlayer: Player
@end
也许,这两个类并没有共享太多代码,仅仅是是相同的接口。在这种情况下,使用协议可能是一个更好的解决方案。使用协议,你可能会写下如下代码:
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@interface Player: NSObjet <VideoPlayer>
@end
@interface YouTobePlayer: NSObject <VideoPlayer>
@end
这种情况下,YouTobePlayer
不需要知道Player
的内部情况。
方案:委托
再一次假设,你有一个像上面例子中那样的Player
类。现在,在一个地方,你想要在paly
方法中执行一个自定义动作。做到它相当的容易:你可以创建一个自定义子类,重写paly
方法,调用[super paly]
,然后执行自定义的功能。这是一种处理方式。另外一种是改变Player
对象并给他一个委托。例如:
@class Player;
@protocol PlayerDelegate
- (void)playerDidStartPlaying:(Player *)player;
@end
@interface Player: NSObject
@property (nonatomic, weak) id<PlayerDelegate> delegate;
- (void)play;
- (void)pause;
@end
现在,在播放器的play
方法中,委托获得了playerDidStartPlaying:
消息。这个类的任何使用者只需实现委托协议而不需要创建子类,并且Player
对象依然保持着通用性。这是一个非常强大的技术,苹果在他们自己的框架中大量的使用了该技术。有时,你想把不同的方法组织到不同的协议中,如UITableView做的那样,它不仅有委托还有数据源。
方案:类别(Category)
有时,你可能想对一个对象进行一点功能扩展。假设,你想向NSArray中扩展一个arrayByRemovingFirstObject
方法。你可以把它放入类别中,而不是创建一个子类。像这样:
@interface NSArray (OBJExtras)
- (void)obj_arrayByRemovingFirstObject;
@end
当使用类别扩展一个不是你自己创建的类时,在方法前加前缀是一个很好的做法。如果不这样做,其他人可能会使用同样的技术实现同样的一个方法。然后,如果行为不匹配,可能会有意想不到的事情发生。
使用类别的危险之一是,你的项目可能最终会使用大量的类别,这样你可能会丢失你的概述(you can lose your overview.)。这种情况下,创建一个自定义类可能更简单。
方案:配置对象
我一直在犯的一个错误(现在可以很快的意识到)是:会创建一个带有很多抽象方法的类,然后很多子类都会重写一个特定的方法。例如,在一个演示应用程序,你可能会一个Theme
类,他有几个属性,如:backgroundColor
和font
,还有一些在幻灯片上放置东西的逻辑。
然后,对于每一个主题,你创建一个Theme
的子类,重写一个方法(如:setup
),并配置属性。直接使用子类没有意义。这种情况下,你可以使用配置对象让你的代码更加简单。你可以把共享逻辑放在Theme
类中(如:幻灯片布局),但是把配置放在一个仅仅有属性的简单对象中。
例如,一个ThemeConfiguration
类有backgroundColor
和font
属性,并且Theme
类在初始化的时候获取一个这个类的实例。
方案:组合
最强大的子类化的替代方案是组合。如果你想重用已存在的代码但是没有共享同样的接口,组合是你可以选择的“武器”。例如,假设你正在设计一个缓存类:
@interface OBJCache: NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCacheValueForKey:(NSString *)key;
@end
达到此目的的一个简单方式是子类化NSDictionary
并且通过调用字典方法实现这两个方法。
@interface OBJCache: NSDictionary
然而,这存在几个缺点。用字典实现的事实应该是一个实现细节。现在,在任何需要一个NSDictionary
类型参数的地方,你都可以提供一个OBJCache
值。如果你想换一种完全不同的东西(例如,你自己的类库),你需要重构很多代码。
一个好的方式是把这个字典存放到一个私有属性中(或者实例变量中),并且仅仅暴露出两个cache
方法。现在,你可以维持灵活性,当你获取更多的见解时可以随意修改实现,并且类的使用者不需要进行重构。
网友评论