4

虽然我在 ObjC 中编码,但这个问题是有意与语言无关的 - 它应该适用于大多数 OO 语言

假设我有一个“Collection”类,我想创建一个继承自“Collection”的“FilteredCollection”。过滤器将在对象创建时设置,从它们开始,该类将表现得像一个“集合”,过滤器应用于其内容。

我以明显的方式和子类 Collection 做事。我覆盖了所有的访问器,并认为我已经完成了一项非常出色的工作——我的 FilteredCollection 看起来应该像一个 Collection 一样,但是其中的对象与我的过滤器相对应的对象被过滤给用户。我想我可以愉快地创建 FilteredCollections 并将它们作为 Collections 在我的程序中传递。

但我来测试 - 哦不 - 它不起作用。深入调试器,我发现这是因为某些方法的 Collection 实现正在调用重写的 FilteredCollection 方法(例如,Collection 在迭代其对象时依赖于“count”方法,但现在它正在获取过滤后的计数,因为我覆盖了 count 方法以提供正确的外部行为)。

这里有什么问题?为什么感觉某些重要原则被违反了,尽管它也感觉像 OO '应该' 以这种方式工作?这个问题的一般解决方案是什么?有吗?

顺便说一句,我知道这个问题的一个很好的“解决方案”是在我将对象放入集合之前过滤对象,而根本不必更改 Collection,但我问的是一个更一般的问题比这 - 这只是一个例子。更普遍的问题是不透明超类中的方法依赖于可能被子类更改的其他方法的行为,以及在您想要子类化对象以更改此类行为的情况下该怎么做。

4

11 回答 11

11

你继承的Collection那个有一定的契约。类的用户(包括类本身,因为它可以调用自己的方法)假设子类遵守契约。如果幸运的话,合同会在其文档中明确无误地指定...

例如,合同可以说:“如果我添加一个元素x,然后迭代集合,我应该x返回”。您的实施似乎FilteredCollection违反了该合同。

这里还有一个问题:Collection应该是一个接口,而不是一个具体的实现。一个实现(例如TreeSet)应该实现那个接口,当然也应该遵守它的契约。

在这种情况下,我认为正确的设计不是继承自Collection,而是FilteredCollection围绕它创建一个“包装器”。可能FilteredCollection不应该实现该Collection接口,因为它不遵守通常的集合契约。

于 2009-09-28T14:12:27.200 回答
4

与其子类化 Collection 来实现 FilteredCollection,不如尝试将 FilteredCollection 实现为一个单独的类,该类实现 iCollection 并委托给现有集合。这类似于四人帮中的装饰器模式。

部分示例:

class FilteredCollection implements ICollection
{
    private ICollection baseCollection;

    public FilteredCollection(ICollection baseCollection)
    {
        this.baseCollection = baseCollection;
    }

    public GetItems()
    {
        return Filter(baseCollection.GetItems());
    }

    private Filter(...)
    {
        //do filter here
    }

}

将 FilteredCollection 实现为 ICollection 的装饰器还有一个额外的好处,即您可以过滤任何实现 ICollection 的东西,而不仅仅是您子类化的一个类。

为了增加好处,您可以使用命令模式在运行时将 Filter() 的特定实现注入到 FilteredCollection 中,从而无需为要应用的每个过滤器编写不同的 FilteredCollection 实现。

于 2009-09-28T14:19:38.767 回答
3

(请注意,虽然我将使用您的示例,但我将尝试专注于这个概念,而不是告诉您您的具体示例有什么问题)。

黑盒继承?

您遇到的是“黑盒继承”的神话。通常实际上不可能将允许继承的实现与使用该继承的实现完全分开。我知道这与经常教授继承的方式背道而驰,但它确实存在。

以您的示例为例,您希望收藏合同的消费者看到一个与他们可以从您的收藏中获得的项目数量相匹配的计数,这是非常合理的。继承基类中的代码访问它的 Count 属性并获得它所期望的也是非常合理的。有些东西必须付出。

谁是负责的人?

答:基类。为了实现基类之上的这两个目标,需要以不同的方式处理事情。为什么这是基类的责任?因为它允许自己继承并允许覆盖成员实现。现在,您可能无法选择某些有助于 OO 设计的语言。然而,这只会使这个问题更难处理,但它仍然需要处理。

在示例中,基集合类应该有自己的内部方法来确定其实际计数,因为知道子类可能会覆盖现有的 Count 实现。它自己对公共和可覆盖的 Count 属性的实现不应该影响基类的内部操作,而只是实现它正在实现的外部契约的一种手段。

当然,这意味着基类的实现并不像我们想要的那样清晰。这就是我所说的黑盒继承是一个神话的意思,只是为了允许继承存在一些实现成本。

底线...

是一个可继承类,需要进行防御性编码,以便它不依赖于可覆盖成员的假定操作。或者,需要在某种形式的文档中非常清楚地明确成员的重写实现所期望的行为(这在定义抽象成员的类中很常见)。

于 2009-09-28T14:52:30.273 回答
2

你的FilteredCollection感觉不对。通常,当您有一个集合并向其中添加一个新元素时,您希望它count增加一,并将新元素添加到容器中。

你的FilteredCollection工作方式不是这样 - 如果你添加一个被过滤的项目,count容器的 可能不会改变。我认为这是您的设计出错的地方。

如果该行为是有意的,那么合约count使其不适合您的成员函数尝试使用它的目的。

于 2009-09-28T14:12:02.803 回答
2

我认为真正的问题是对面向对象语言应该如何工作的误解。我猜你的代码看起来像这样:

Collection myCollection = myFilteredCollection;

并期望调用 Collection 类实现的方法。正确的?

在 C++ 程序中,这可能会起作用,前提是 Collection 上的方法未定义为虚拟方法。然而,这是 C++ 设计目标的产物。

在几乎所有其他面向对象的语言中,所有方法都是动态调度的:它们使用实际对象的类型,而不是变量的类型。

如果这不是您想知道的,那么请阅读Liskov 替换原则,并问自己是否违反了它。很多类层次结构都可以。

于 2009-09-28T14:18:03.493 回答
1

你所描述的是多态性的一个怪癖。由于您可以将子类的实例作为父类的实例来处理,因此您可能不知道幕后的实现类型。

我认为您的解决方案非常简单:

  1. 您说您不修改集合,仅在人们从中获取时对其应用过滤器。因此,您不应覆盖count 方法。所有这些元素都在集合中,因此不会对调用者撒谎。
  2. 您希望基本 .count 方法正常运行,但您仍然需要计数,因此您应该实现一个getFilteredCount返回过滤后元素数量的方法。

子类化是关于“种类”的关系。您正在做的事情并没有超出规范,但也不是最标准的用例。您正在对集合应用过滤器,因此您可以声称“FilteredCollection”是“一种”集合,但实际上您并没有真正修改集合;你只是用一个简化过滤的层来包装它。无论如何,这应该有效。唯一的缺点是你必须记住调用 'getFilteredCount' 而不是 .getCount

于 2009-09-28T14:12:47.063 回答
1

该示例属于“医生,我这样做时很痛”类别。是的,子类可以以各种方式破坏超类。不,没有简单的防水解决方案可以防止这种情况发生。

如果您的语言支持,您可以密封您的超类(制作一切final),但您会失去灵活性。这是一种不好的防御性编程(好的依赖于健壮的代码,坏的依赖于严格的限制)。

你能做的最好的事情就是在人类层面上行动——确保编写子类的人理解超类。辅导/代码审查、良好的文档、单元测试(大致按照这个重要性顺序)可以帮助实现这一目标。当然,防御性地编写基类代码并没有什么坏处。

于 2009-09-28T15:44:56.657 回答
0

当我第一次了解继承的工作原理时,我经常使用它。我有这些大树,一切都以一种或另一种方式连接起来。

多么痛苦。

对于您想要的,您应该引用您的对象,而不是扩展它。

另外,我个人会隐藏任何从我的公共 API(以及通常我的私有 API)传递集合的痕迹。收藏品是不可能安全的。在 WordCount 类或 UsersWithAges 类或 AnimalsAndFootCount 类中包装一个集合(来吧,它是做什么用的?你可以从签名中猜到,对吧?)可以更有意义。

还具有 wordCount.getMostUsedWord()、usersWithAges.getUsersOverEighteen() 和 animalsAndFootCount.getBipeds() 等方法,可将分散在代码中的重复实用程序功能移动到它所属的新业务集合中。

于 2009-12-04T00:45:16.630 回答
0

它的行为方式是这样的,因为这就是面向对象编程的工作方式!

OOP 的重点应该是子类可以重新定义它的一些超类方法,然后在超类级别完成的操作将获得子类的实现。

让我们让你的例子更具体一点。我们创建了一个包含狗、猫、狮子和蛇怪的“收藏动物”。然后我们创建一个过滤掉狮子和蛇怪的FilteredCollection domesticAnimal。所以现在如果我们迭代国内动物,我们希望只看到狗和猫。如果我们要求计算成员的数量,我们会不会期望结果是“2”?如果我们询问对象它有多少成员并且它说“4”,然后当我们要求它列出它们时它只列出了 2,那肯定会是一种奇怪的行为。

使覆盖在超类级别工作是 OOP 的一个重要特性。它允许我们定义一个函数,在您的示例中,将 Collection 对象作为参数并对其进行操作,而无需知道或关心它下面是真正的“纯”集合还是 FilteredCollection。一切都应该以任何方式工作。如果它是一个纯 Collection,它会得到纯 Collection 函数;如果它是 FilteredCollection,它将获得 FilteredCollection 函数。

如果计数还在内部用于其他目的——比如决定新元素应该去哪里,这样你就添加了真正的第五个元素并且它神秘地覆盖了#3——那么你在类的设计中就有问题了。OOP 为您提供了对类如何运作的强大权力,但强大的权力伴随着巨大的责任。:-) 如果一个函数用于两个不同的目的,并且您重写了实现以满足您对目的 #1 的要求,则由您来确保这不会破坏目的 #2。

于 2009-09-29T14:25:32.723 回答
0

我对您的帖子的第一反应是提到覆盖“所有访问者”。这是我经常看到的:扩展一个基类,然后覆盖大多数基类方法。在我看来,这违背了继承的目的。如果您需要覆盖大多数基类函数,那么是时候重新考虑为什么要扩展该类了。如前所述,接口可能是更好的解决方案,因为它松散地耦合了不同的对象。子类应该扩展基类的功能,而不是完全重写它。我不禁想知道您是否正在覆盖基类成员,那么会发生意外行为似乎很合乎逻辑。

于 2009-10-10T21:21:07.480 回答
0

您可能会争辩说,超类没有为子类化而精心设计,至少不是您想要的方式。当超类调用“Count()”或“Next()”或其他什么时,它不必让该调用被覆盖。在 c++ 中,除非它被声明为“虚拟”,否则它不能被覆盖,但这并不适用于所有语言 - 例如,如果我没记错的话,Obj-C 本质上是虚拟的。

更糟糕的是——即使你没有覆盖超类中的方法,这个问题也会发生在你身上——参见Subtyping vs Subclassing。请特别参阅该文章中的OOP 问题参考。

于 2009-09-28T14:21:23.227 回答