[iOS翻译]OrderedDictionary。子类化一个Cocoa类群

450 阅读5分钟

原文地址:www.cocoawithlove.com/2008/12/ord…

原文作者:www.cocoawithlove.com/

发布时间:2008年12月18日

请注意:这篇文章是Cocoa with Love上旧的 "Objective-C时代 "的一部分。我没有保持这些文章的更新,请警惕破损的代码或可能过时的信息。请阅读 "有爱的Cocoa新时代 "以了解更多。

默认的Cocoa集合类能力很强,但在某些情况下,你可能需要覆盖它们以改变其功能。我将用一个例子类来解释什么时候以及如何做这件事。OrderedDictionary (一个NSMutableDictionary的子类,保持了它的键的顺序)。

最佳实践:不要子类化

这篇文章将讨论子类化 NSMutableDictionary。你应该注意到这是一个特殊情况;大多数时候,当你给一个集合类添加功能时,你不应该覆盖它。

大多数类在设计时都没有考虑到子类化的问题。如果你不是一个类的作者,选择覆盖它就会产生一种风险,即你的添加不会正确地维护对象的隐式或私有值和行为。

由于子类化而增加的风险并不适用于那些打算在使用前被子类化的类。突出的例子包括NSObject、NSWindowController、NSView、NSControl和NSCell。对于通过子类设计的类,任何与子类本身相关的风险都被它们所提供的经过良好测试的基础功能所抵消,除非该类被子类化,否则无法访问。

旁白:无需子类化就能定制行为的设计模式

  1. 将自定义功能放在包含非覆盖对象的父类中,而不是放在非覆盖对象本身。这是 "has-a "设计,而不是 "is-a"。
  2. 使用Decorator对象代替。装饰器是一个围绕非覆盖类的包装器。所有到被包含类的消息都会先经过装饰器,所以装饰器可以控制或补充被包含类的行为。
  3. 使用一个观察者来保持非覆盖对象与依赖对象的同步,这样他们的组合状态就可以实现自定义行为,即使每个对象都是非自定义的。在这种情况下,观察者充当了依赖对象的控制器/管理器。

选择重写一个集合类

Cocoa中的集合类是类群。在某些方面,这意味着它们是要被覆盖的,因为基类在功能上是抽象的,不包含所需的具体功能。

类簇的默认具体功能是由隐藏的子类提供的,由基类透明地返回。因为这是透明的,所以通常不期望你自己编写子类。尽管有这样的期望,接口的设计还是考虑到了子类,所以我们可以很容易地自己实现所需的子类行为,而不是依赖其中一个默认的实现。

构成一个类群的 "具体 "行为的功能,在苹果的文档中被称为类群的 "原始 "方法。对于集合类(如NSMutableDictionary),这些 "原始 "方法提供了集合存储的所有行为。由于我们没有继承这些原始方法的默认行为,这意味着在Cocoa中对一个集合类进行子类化的决定是对存储进行重新实现。

设计一个有序的 NSMutableDictionary 子类

我选择了实现 NSMutableDictionary 的有序版本,命名为 OrderedDictionary。这需要有序地存储字典的键值,但是 NSDictionary 将它的键值存储在一个哈希表中,这个哈希表在设计上是无序的。由于这种无序性是哈希表存储的根本,我们对 NSMutableDictionary 的子类化(以及对存储的重新实现)是合适的。

然而,我将为我的子类重新实现基本的存储,把所有的对象和键存储在一个未修改的 NSMutableDictionary 中。从设计的角度来看,这将使我的子类成为围绕未修改的 NSMutableDictionary 的装饰器,而不是一个纯粹的子类。

当消息从我的子类上的 "原始 "方法传递到包含的 NSMutableDictionary 时,我也将把字典的键存储在一个单独的 NSMutableArray 中,从而允许 OrderedDictionary 除了标准的 NSMutableDictionary 功能外,保持对键的排序。

实现原始方法

Apple 的文档为 NSMutableDictionary 列出了以下的 "原始 "方法。

  • count
  • objectForKey:
  • keyEnumerator
  • setObject:forKey:
  • removeObjectForKey:

实际上,以下方法也是必须的,尽管没有被列出。

  • initWithCapacity:

按照我选择的Decorator模式,几乎所有这些都是通过将消息传递给包含的NSMutableDictionary(名为 "字典")来实现的,键也被添加到NSMutableArray(名为 "数组"),keyEnumerator方法返回 "数组 "的objectEnumerator。

- (id)initWithCapacity:(NSUInteger)capacity
{
    self = [super init];
    if (self != nil)
    {
        dictionary = [[NSMutableDictionary alloc] initWithCapacity:capacity];
        array = [[NSMutableArray alloc] initWithCapacity:capacity];
    }
    return self;
}

- (void)dealloc
{
    [dictionary release];
    [array release];
    [super dealloc];
}

- (void)setObject:(id)anObject forKey:(id)aKey
{
    if (![dictionary objectForKey:aKey])
    {
        [array addObject:aKey];
    }
    [dictionary setObject:anObject forKey:aKey];
}

- (void)removeObjectForKey:(id)aKey
{
    [dictionary removeObjectForKey:aKey];
    [array removeObject:aKey];
}

- (NSUInteger)count
{
    return [dictionary count];
}

- (id)objectForKey:(id)aKey
{
    return [dictionary objectForKey:aKey];
}

- (NSEnumerator *)keyEnumerator
{
    return [array objectEnumerator];
}

你可以在这里下载完整的OrderedDictionary类(2kb)。我在可下载的版本中进一步添加了init、insertObject:forKey:atIndex:、keyAtIndex:、reverseKeyEnumerator和descriptionWithLocale:indent:方法来填充一些非必要的功能。

结论

创建NSMutableDictionary的子类,通过为该类群提供一套干净的 "原始 "方法,变得相对简单。有了这些方法的实现,NSDictionary 和 NSMutableDictionary 的其余丰富功能就可以使用了;不需要进一步的工作。

我关于子类的风险的观点应该在initWithCapacity:的无记录要求下变得明显。文档中没有提到这个方法的必要性,所以我不得不利用调试信息的反馈来重新设计这个类,以达到一个功能性的实现--在这种情况下是微不足道的,但还是很烦人。

在类中加入descriptionWithLocale:indent:方法是另一个类似的教训:这个方法的默认实现假设它可以重新排列从keyEnumerator返回的键而不造成问题。在测试中看到错误的结果后,我被要求替换这个方法,以消除不正确的假设。


www.deepl.com 翻译