子类导致超类出现意外行为OO设计问题

发布于 2024-08-06 21:54:22 字数 748 浏览 4 评论 0原文

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

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

我以显而易见的方式做事,并将 Collection 子类化。我重写了所有访问器,并认为我已经完成了一项非常巧妙的工作 - 我的 FilteredCollection 看起来应该像集合一样运行,但其中的对象对应于我的过滤器被过滤给用户。我想我可以愉快地创建 FilteredCollections 并将它们作为 Collections 在我的程序中传递。

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

这是怎么回事?尽管事实上 OO“应该”以这种方式工作,但为什么感觉有些重要原则被违反了?这个问题的一般解决方案是什么?有吗?

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

Although I'm coding in ObjC, This question is intentionally language-agnostic - it should apply to most OO languages

Let's say I have an "Collection" class, and I want to create a "FilteredCollection" that inherits from "Collection". Filters will be set up at object-creation time, and from them on, the class will behave like a "Collection" with the filters applied to its contents.

I do things the obvious way and subclass Collection. I override all the accessors, and think I've done a pretty neat job - my FilteredCollection looks like it should behave just like a Collection, but with objects that are 'in' it that correspond to my filters being filtered out to users. I think I can happily create FilteredCollections and pass them around my program as Collections.

But I come to testing and - oh no - it's not working. Delving into the debugger, I find that it's because the Collection implementation of some methods is calling the overridden FilteredCollection methods (say, for example, there's a "count" method that Collection relies upon when iterating its objects, but now it's getting the filtered count, because I overrode the count method to give the correct external behaviour).

What's wrong here? Why does it feel like some important principles are being violated despite the fact that it also feels like OO 'should' work this way? What's a general solution to this issue? Is there one?

I know, by the way, that a good 'solution' to this problem in particular would be to filter the objects before I put them into the collection, and not have to change Collection at all, but I'm asking a more general question than that - this is just an example. The more general issue is methods in an opaque superclass that rely on the behaviour of other methods that could be changed by subclasses, and what to do in the case that you want to subclass an object to change behaviour like this.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

你的心境我的脸 2024-08-13 21:54:22

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

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

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

在这种情况下,我认为正确的设计不是继承 Collection,而是创建 FilteredCollection 作为其周围的“包装器”。也许 FilteredCollection 不应该实现 Collection 接口,因为它不遵守集合的通常约定。

The Collection that you inherit from has a certain contract. Users of the class (and that includes the class itself, because it can call its own methods) assume that subclasses obey the contract. If you're lucky, the contract is specified clearly and unambiguously in its documentation...

For example, the contract could say: "if I add an element x, then iterate over the collection, I should get x back". It seems that your FilteredCollection implementation breaks that contract.

There is another problem here: Collection should be an interface, not a concrete implementation. An implementation (e.g. TreeSet) should implement that interface, and of course also obey its contract.

In this case, I think the correct design would be not to inherit from Collection, but rather create FilteredCollection as a "wrapper" around it. Probably FilteredCollection should not implement the Collection interface, because it does not obey the usual contract for collections.

柠檬色的秋千 2024-08-13 21:54:22

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

部分示例:

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 实现。

Rather than sublcassing Collection to implement FilteredCollection, try implementing FilteredCollection as a separate class that implements iCollection and delegates to an existing collection. This is similar to the Decorator pattern from the Gang of Four.

Partial example:

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
    }

}

Implementing FilteredCollection as a decorator for ICollection has the added benefit that you can filter anything that implements ICollection, not just the one class you subclassed.

For added goodness, you can use the Command pattern to inject a specific implementation of Filter() into the FilteredCollection at runtime, eliminating the need to write a different FilteredCollection implementation for every filter you want to apply.

电影里的梦 2024-08-13 21:54:22

(请注意,虽然我将使用您的示例,但我会尝试集中精力在概念上,而不是告诉您您的具体示例有什么问题)。

黑匣子继承?

您遇到的是“黑匣子继承”的神话。实际上通常不可能将允许继承的实现与使用该继承的实现完全分开。我知道这与通常教授继承的方式背道而驰,但事实就是如此。

拿你的例子来说,你希望收藏合同的消费者看到一个与他们可以从你的收藏中获得的物品数量相匹配的计数是非常合理的。继承基类中的代码访问其 Count 属性并获得其期望的内容也是相当合理的。必须付出一些东西。

谁负责?

答案:基类。为了实现基类之上的两个目标,需要以不同的方式处理事情。为什么这是基类的责任?因为它允许自身继承并允许覆盖成员实现。现在,在某些促进面向对象设计的语言中,您可能没有选择。然而,这只会让这个问题更难处理,但仍然需要处理。

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

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

底线...

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

(Note whilst I'll use your example I'll try to concentrate on the concept rather then tell you what's wrong with your specific example).

Black Box Inheritance?

What you're crashing into is the myth of "Black box inheritance". Its often not actually possible to separate completely implementations that allow inheritance from implementations that use that inheritance. I know this flys in the face of how inheritance is often taught but there it is.

To take your example, its quite reasonable for you to want the consumers of the collection contract to see a Count which matches the number items they can get out of your collection. Its also quite reasonable for code in the inherited base class to access its Count property and get what it expects. Something has to give.

Who is Responsible?

Answer: The base class. To achieve both the goals above the base class needs to handle things differently. Why is this the reponsibility of the base class? Because it allows itself to be inherited from and allowed the member implementation to be overriden. Now it may be in some languages that facilitate an OO design that you aren't given a choice. However that just makes this problem harder to deal with but it still needs be dealt with.

In the example, the base collection class should have its own internal means of determining its actual count in the knowledge that a sub-class may override the existing implementation of Count. Its own implementation of the public and overridable Count property should not impact on the internal operation of the base class but just be a means to acheive the external contract it is implementing.

Of course this means the implementation of the base class isn't as crisp and clean as we would like. That's what I mean by the black box inheritance being a myth, there is some implementation cost just to allow inheritance.

The Bottom Line...

is an inheritable class needs to be coded defensively so that it doesn't rely on assumed operation of overridable members. OR it needs to be very clear in some form of documentation exactly what behaviour is expected from overriden implementations of members (this is common in classes that define abstract members).

や三分注定 2024-08-13 21:54:22

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

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

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

Your FilteredCollection feels wrong. Usually, when you have a collection and you add a new element into it, you expect that it's count increases by one, and the new element is added to the container.

Your FilteredCollection does not work like this - if you add an item that is filtered, the count of the container might not change. I think this is where your design goes wrong.

If that behaviour is intended, then the contract for count makes it unsuitable for the purpose your member functions are trying to use it for.

厌味 2024-08-13 21:54:22

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

Collection myCollection = myFilteredCollection;

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

在 C++ 程序中,如果 Collection 上的方法未定义为虚拟方法,则这可能有效。然而,这是 C++ 设计目标的产物。

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

如果这不是您想知道的,请阅读里氏替换原则,并询问不管你是否打破了它。很多类层次结构都是如此。

I think that the real issue is a misunderstanding of how object-oriented languages are supposed to work. I'm guessing that you have code that looks something like this:

Collection myCollection = myFilteredCollection;

And expect to invoke the methods implemented by the Collection class. Correct?

In a C++ program, this might work, provided that the methods on Collection are not defined as virtual methods. However, this is an artifact of the design goals of C++.

In just about every other object-oriented language, all methods are dispatched dynamically: they use the type of the actual object, not the type of the variable.

If that's not what you're wondering, then read up on the Liskov Substitution Principle, and ask yourself whether you're breaking it. Lots of class hierarchies do.

半葬歌 2024-08-13 21:54:22

您所描述的是多态性的一个怪癖。由于您可以将子类的实例视为父类的实例,因此您可能不知道其背后的实现类型。

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

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

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

What you described is a quirk of polymorphism. Since you can address an instance of a subclass as an instance of the parent class, you may not know what kind of implementation lies underneath the covers.

I think your solution is pretty simple:

  1. You stated that you don't modify the collection, you only apply a filter to it when people fetch from it. Therefore you should not override the count method. All of those elements are in the collection therefore don't lie to the caller.
  2. You want the base .count method to behave normally, but you still want the count so you should implement a getFilteredCount method which returns the amount of elements post filtering.

Subclassing is all about the 'Kind of' relationship. What you're doing is not out of the norm but not the most standard use case either. You're applying a filter to a collection, so you can claim that a 'FilteredCollection' is a 'kind of' collection, but in reality you're not actually modifying the collection; you're just wrapping it with a layer that simplifies filtering. In any case, this should work. The only downside is that you have to remember to call 'getFilteredCount' instead of .getCount

一张白纸 2024-08-13 21:54:22

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

如果您的语言支持的话,您可以密封您的超类(将所有内容都定为最终的),但这样您就会失去灵活性。这是一种糟糕的防御性编程(好的依赖于健壮的代码,坏的依赖于强大的限制)。

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

The example falls into "Doctor, it hurts when I do this" category. Yes, subclasses can break superclasses in various ways. No, there is no simple waterproof solution to prevent that.

You can seal your superclass (make everything final) if your language supports this but then you lose flexibility. This is the bad kind of defensive programming (the good relies on robust code, the bad relies on strong restrictions).

The best you can do is to act at human level - make sure that the human that writes the subclass understands the superclass. Tutoring/code review, good documentation, unit tests (in roughly this order of importance) can help achieve this. And of course it doesn't hurt to code the base class defensively.

冬天旳寂寞 2024-08-13 21:54:22

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

更糟糕的是 - 即使您不重写超类中的方法,这个问题也可能发生在您身上 - 请参阅 子类型与子类化。请特别参阅该文章中的 OOP 问题参考。

You could argue that the superclass is not well-designed for subclassing, at least not in the way you want to. When the superclass calls "Count()" or "Next()" or whatever, it doesn't have to let that call be overridden. In c++, it can't be overridden unless it's declared "virtual", but that doesn't apply in all languages - for example, Obj-C is inherently virtual if I remember correctly.

It's even worse - this problem can happen to you even if you don't override methods in the superclass - see Subtyping vs Subclassing. See in particular the OOP problems reference in that article.

疑心病 2024-08-13 21:54:22

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

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

让我们让你的例子更具体一些。我们创建了一个“动物集合”,其中包含狗、猫、狮子和蛇怪。然后我们创建一个 FilteredCollection DomesticAnimal 来过滤掉狮子和蛇怪。因此,现在如果我们迭代domesticAnimal,我们预计只会看到狗和猫。如果我们要求统计成员的数量,我们会不会期望结果是“2”?如果我们询问该对象有多少个成员,它说“4”,然后当我们要求它列出它们时,它只列出 2,这肯定是一种奇怪的行为。

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

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

It behaves this way because this is how object-oriented programming is supposed to work!

The whole point of OOP is supposed to be that a sub-class can redefine some of its superclasses methods, and then operations done at the superclass level will get the subclass implementation.

Let's make your example a little more concrete. We create a "Collection animal" that contains dog, cat, lion, and basilisk. Then we create a FilteredCollection domesticAnimal that filters out the lion and basilisk. So now if we iterate over domesticAnimal we expect to see only dog and cat. If we ask for a count of the number of members, would we not expect the result to be "2"? It would surely be curious behavior if we asked the object how many members it had and it said "4", and then when we asked it to list them it only listed 2.

Making the overrides work at the superclass level is an important feature of OOP. It allows us to define a function that takes, in your example, a Collection object as a parameter and operates on it, without knowing or caring whether underneath it is really a "pure" Collection or a FilteredCollection. Everything should work either way. If it's a pure Collection it gets the pure Collection functions; if it's a FilteredCollection it gets the FilteredCollection functions.

If the count is also used internally for other purposes -- like deciding where new elements should go, so that you add what is really a fifth element and it mysteriously overwrites #3 -- then you have a problem in the design of the classes. OOP gives you great power over how classes operate, but with great power comes great responsibility. :-) If a function is used for two different purposes, and you override the implementation to satisfy your requirements for purpose #1, it's up to you to make sure that that doesn't break purpose #2.

久隐师 2024-08-13 21:54:22

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

My first reaction to your post was the mention of overriding "all the accessors." This is something I've seen a lot of: extending a base class then overriding most of the base class methods. This defeats the purpose of inheritance in my opinion. If you need to override most base class functions then it's time to reconsider why you're extending the class. As said before, an interface may be a better solution, since it loosely couples disparate objects. The sub-class should EXTEND the functionality of the base class, not completely rewrite it.
I couldn't help but wonder if you are overriding the base class members then it would seem quite logical that unexpected behavior would occur.

笑叹一世浮沉 2024-08-13 21:54:22

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

多么痛苦啊。

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

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

还有像 wordCount.getMostUsedWord()、usersWithAges.getUsersOverEighteen() 和 AnimalAndFootCount.getBipeds() 这样的方法将分散在代码中的重复实用功能移动到它所属的新奇业务集合中。

When I first grok'd how inheritance worked I used it a lot. I had these big trees with everything connected one way or another.

What a pain.

For what you want, you should be referencing your object, not extending it.

Also, I'd personally hide any trace of passing a collection from my public API (and, in general, my private API as well). Collections are impossible to make safe. Wrapping a collection (Come on, what's it used for??? You can guess just from the signature, right?) inside a WordCount class or a UsersWithAges class or a AnimalsAndFootCount class can make a lot more sense.

Also having methods like wordCount.getMostUsedWord(), usersWithAges.getUsersOverEighteen() and animalsAndFootCount.getBipeds() method moves repetitive utility functionality scattered throughout your code into your new-fangled business collection where it belongs.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文