虽然我在ObjC编码,但这个问题是故意与语言无关的 - 它应该适用于大多数OO语言
假设我有一个“Collection”类,我想创建一个继承自“Collection”的“FilteredCollection”。过滤器将在对象创建时设置,从它们开始,该类将表现得像一个“集合”,过滤器应用于其内容。
我以明显的方式和子类Collection做事。我覆盖了所有的访问器,并认为我已经完成了一个非常整洁的工作 - 我的FilteredCollection看起来应该像一个Collection,但是对象是'in'它对应于我的过滤器被过滤掉给用户。我想我可以愉快地创建FilteredCollections并将它们作为集合传递给我的程序。
但我来测试 - 哦不 - 它不起作用。深入研究调试器,我发现这是因为某些方法的Collection实现调用了重写的FilteredCollection方法(例如,在迭代其对象时,Collection依赖于“count”方法,但现在它正在获取过滤后的计数,因为我重写了count方法以给出正确的外部行为。)
这里有什么问题?为什么感觉某些重要原则被侵犯,尽管它也感觉OO'应该'以这种方式工作?这个问题的一般解决方案是什么?有吗?
顺便说一下,我知道这个问题的一个好的“解决方案”就是在我将它们放入集合之前过滤对象,而不是根本不需要更改集合,但是我问的是比这更普遍的问题 - 这只是一个例子。更普遍的问题是不透明超类中的方法依赖于可能由子类更改的其他方法的行为,以及在您希望子类化对象以更改此类行为的情况下该怎么做。
答案 0 :(得分:11)
您继承的Collection
具有特定合同。类的用户(包括类本身,因为它可以调用自己的方法)假定子类服从合同。如果你很幸运,合同在其文件中明确无误地指明......
例如,合同可以说:“如果我添加元素x
,然后迭代集合,我应该x
返回”。您的FilteredCollection
实施似乎违反了该合同。
这里还有另一个问题:Collection
应该是一个接口,而不是一个具体的实现。实现(例如TreeSet
)应该实现该接口,当然也遵守其合同。
在这种情况下,我认为正确的设计不是继承Collection
,而是创建FilteredCollection
作为它的“包装”。可能FilteredCollection
不应该实现Collection
接口,因为它不遵守通常的集合合同。
答案 1 :(得分:4)
尝试将FilteredCollection实现为实现iCollection并委托给现有集合的单独类,而不是将Collection转换为实现FilteredCollection。这类似于Gang of Four的Decorator pattern。
部分示例:
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的任何内容,而不仅仅是您子类化的一个类。
为了增加优点,您可以使用Command pattern在运行时将Filter()的特定实现注入到FilteredCollection中,从而无需为要应用的每个过滤器编写不同的FilteredCollection实现。
答案 2 :(得分:3)
(注意虽然我将使用你的例子,我会尝试专注于这个概念,而不是告诉你你的具体例子有什么问题。)
黑匣子继承?
你遇到的是“黑匣子继承”的神话。实际上,实际上不可能完全分离允许从使用该继承的实现继承的实现。我知道,面对经常传授的遗传方式,这种情况就是如此。
举个例子,您希望收藏合约的消费者看到一个与他们可以从您的收藏中获得的数字项目匹配的Count,这是非常合理的。对于继承的基类中的代码来说,访问其Count属性并获得它所期望的内容也是非常合理的。必须要付出一些代价。
谁负责?
答案:基类。要实现上述目标,基类需要以不同方式处理事物。为什么这是基类的可重用性?因为它允许自己继承并允许覆盖成员实现。现在它可能在一些语言中促进OO设计,你没有选择。然而,这只会使这个问题更难处理,但仍需要处理。
在该示例中,基本集合类应该有自己的内部方法来确定其实际计数,因为知道子类可能会覆盖现有的Count实现。它自己实现的public和overridable Count属性不应该影响基类的内部操作,而只是实现它正在实现的外部合同的手段。
当然,这意味着基类的实现并不像我们希望的那样清晰和干净。这就是我所说的黑盒子继承是一个神话,有一些实现成本只是为了允许继承。
底线......
是一个可继承的类,需要进行防御性编码,以便它不依赖于可覆盖成员的假定操作。或者它需要在某种形式的文档中非常清楚地确定成员的重写实现期望的行为(这在定义抽象成员的类中很常见)。
答案 3 :(得分:2)
您的FilteredCollection
感觉不对。通常,当您拥有一个集合并在其中添加一个新元素时,您希望它的count
增加1,并将新元素添加到容器中。
您的FilteredCollection
无法正常工作 - 如果您添加了已过滤的项目,则容器的count
可能不会更改。我认为这是你的设计出错的地方。
如果有这种行为,那么count
的合同使其不适合您的成员函数尝试使用它。
答案 4 :(得分:2)
我认为真正的问题是对面向对象语言应该如何工作的误解。我猜你有代码看起来像这样:
Collection myCollection = myFilteredCollection;
期望调用Collection类实现的方法。正确的吗?
在C ++程序中,如果Collection上的方法未定义为虚方法,则可能会有效。但是,这是C ++设计目标的工件。
在几乎所有其他面向对象的语言中,所有方法都是动态调度的:它们使用实际对象的类型,而不是变量的类型。
如果那不是您想知道的,那么请阅读Liskov Substitution Principle,并问问自己是否打破了它。很多类层次结构都可以。
答案 5 :(得分:1)
你所描述的是多态性的怪癖。由于您可以将子类的实例作为父类的实例来处理,因此您可能不知道封面下的实现类型。
我认为您的解决方案非常简单:
getFilteredCount
方法,该方法返回过滤后的元素数量。子类化完全是关于'种类'的关系。你所做的不是常规,也不是最标准的用例。您正在对集合应用过滤器,因此您可以声称'FilteredCollection'是一种'类型'集合,但实际上您实际上并未修改集合;你只是用一个简化过滤的层来包装它。无论如何,这应该有效。唯一的缺点是你必须记住调用'getFilteredCount'而不是.getCount
答案 6 :(得分:1)
这个例子属于“医生,当我这样做时会伤害”类别。是的,子类可以以各种方式打破超类。不,没有简单的防水解决方案可以防止这种情况发生。
如果您的语言支持此功能,您可以密封您的超类(使所有内容final
),但您会失去灵活性。这是一种糟糕的防御性编程(好的依赖于健壮的代码,坏的依赖于强大的限制)。
你能做的最好的事情是在人类层面上行动 - 确保编写子类的人理解超类。辅导/代码审查,良好的文档,单元测试(大致按重要性顺序)可以帮助实现这一目标。当然,防御性地对基类进行编码并没有什么坏处。
答案 7 :(得分:0)
你可能会争辩说,超类不适合子类化,至少不是你想要的方式。当超类调用“Count()”或“Next()”或其他什么时,它不必让该调用被覆盖。在c ++中,它不能被覆盖,除非它被声明为“虚拟”,但这并不适用于所有语言 - 例如,如果我没记错的话,Obj-C本身就是虚拟的。
更糟糕的是 - 即使您没有覆盖超类中的方法,也会发生此问题 - 请参阅Subtyping vs Subclassing。请参阅该文章中的OOP problems引用。
答案 8 :(得分:0)
它的行为方式是因为这是面向对象编程应该如何工作的!
OOP的重点应该是子类可以重新定义它的一些超类方法,然后在超类级别完成的操作将获得子类实现。
让我们的例子更具体一点。我们创造了一个“收集动物”,其中包含狗,猫,狮子和蛇怪。然后我们创建一个FilteredCollection domesticAnimal,过滤掉狮子和蛇怪。所以,现在如果我们遍历domesticAnimal,我们希望只看到狗和猫。如果我们要求计算成员数量,我们不期望结果为“2”吗?如果我们询问对象有多少成员并且说“4”,那肯定是好奇的行为,然后当我们要求它列出它们时它只列出了2。
使覆盖在超类级别工作是OOP的一个重要特性。它允许我们定义一个函数,在您的示例中,将Collection对象作为参数并对其进行操作,而不知道或关心它下面是否真的是“纯粹的”Collection或FilteredCollection。一切都应该工作。如果它是纯粹的Collection,它将获得纯粹的Collection函数;如果它是FilteredCollection,它将获得FilteredCollection函数。
如果计数也在内部用于其他目的 - 比如决定新元素应该去哪里,以便你添加真正的第五个元素并且它神秘地覆盖#3 - 那么你在设计中遇到了问题课程。 OOP为你提供了很强大的能力,可以帮助你掌握很多权力。 :-)如果一个函数用于两个不同的目的,并且你覆盖实现以满足你的目的#1的要求,那么你应该确保这不会破坏目的#2。
答案 9 :(得分:0)
我对你的帖子的第一反应是提到压倒“所有访问者”。这是我经常看到的:扩展基类然后覆盖大多数基类方法。在我看来,这违背了继承的目的。如果您需要覆盖大多数基类函数,那么是时候重新考虑扩展类的原因了。如前所述,界面可能是更好的解决方案,因为它松散地耦合不同的对象。子类应该扩展基类的功能,而不是完全重写它。 我不禁想知道你是否覆盖了基类成员,那么出现意外行为似乎是合乎逻辑的。
答案 10 :(得分:0)
当我第一次了解继承是如何工作的时候,我经常使用它。我有这些大树,所有东西都是这样或那样的。
多么痛苦。
对于你想要的东西,你应该引用你的对象,而不是扩展它。
此外,我个人隐藏了从我的公共API(以及一般来说,我的私有API)传递集合的任何痕迹。收藏是不可能安全的。在WordCount类或UsersWithAges类或AnimalsAndFootCount类中包装一个集合(来吧,它用于什么?你可以从签名中猜出,对吗?)可以更有意义。
同样使用wordCount.getMostUsedWord()等方法,usersWithAges.getUsersOverEighteen()和animalsAndFootCount.getBipeds()方法将遍布代码的重复实用功能移动到您所属的新奇业务集合中。