it-swarm.cn

代码气味:继承滥用

在OO)社区中,人们普遍认为“应该偏重于继承而不是继承”。另一方面,继承的确提供了多态性和将所有内容委派给基类的简单明了的方式除非明确重写,否则极其方便和有用。委托通常(尽管并非总是)是冗长而脆弱的。

恕我直言,最肯定的继承滥用迹象是违反 Liskov替代原则 。还有其他迹象表明,即使看起来方便,继承还是“错误的工作工具”?

52
dsimcha

仅仅为了获得功能而继​​承时,您可能正在滥用继承。这导致创建 上帝对象

只要您看到了类之间的真实关系(例如经典示例,如Dog extended Animal),并且没有将方法放在对某些类没有意义的父类上,继承本身就不是问题。子级(例如,在Animal类中添加Bark()方法在扩展Animal的Fish类中没有任何意义)。

如果一个类需要功能,则使用组合(也许将功能提供者注入构造函数中?)。如果一个类需要像其他类一样,则使用继承。

48
Fernando

我要说的是,冒被击落的风险,继承本身就是代码的味道:)

继承的问题在于它可以用于两个正交的目的:

  • 接口(用于多态)
  • 实现(用于代码重用)

首先,只有一种机制才能同时获得两者,这是导致“继承滥用”的原因,因为大多数人都希望继承是关于接口的,但是即使是其他谨慎的程序员也可以将其用于获取默认实现(这样做很容易忽略这个...)

实际上,诸如Haskell或Go之类的现代语言已放弃继承以分离这两个方面。

回到正轨:

因此,违反Liskov原则是最确定的标志,因为这意味着不遵守合同的“接口”部分。

但是,即使尊重接口,您也可能因为“一种”方法被认为有用而使对象继承自“胖”基类。

因此,仅靠Liskov原理是不够的,您需要知道的是多态是否为二手。如果不是这样,那么首先继承就没有多大意义,那是一种滥用。

摘要:

  • 强制Liskov Principle
  • 检查它是否实际使用

解决方法:

明确区分关注点:

  • 仅从接口继承
  • 使用组合来委托实现

意味着每种方法至少需要两次击键,然后突然人们开始考虑将这种“胖”类重用一小部分是否是一个好主意。

39
Matthieu M.

真的是“普遍接受”吗?这个想法我已经听过几次了,但是这并不是一个普遍公认的原则。正如您刚刚指出的那样,继承和多态是OOP的标志。没有它们,您将只能使用愚蠢的语法进行过程编程。

合成是一种以某种方式构建对象的工具。继承是一种用于以不同方式构建对象的工具。他们两个都没有错。您只需要知道它们如何工作以及何时使用每种样式就合适。

14
Mason Wheeler

继承的优势是可重用性。接口,数据,行为,协作。这是将属性传递到新类的有用模式。

使用继承的缺点是可重用性。接口,数据和行为都被封装并整体传输,这使它成为一个全有或全无的提议。随着层次结构的深入,这些问题变得尤为明显,因为抽象中可能有用的属性变得越来越少,并开始使局部属性变得晦涩。

使用组合对象的每个类都将或多或少地重新实现相同的代码以与其进行交互。尽管这种重复需要一定程度地违反DRY,但它为设计人员提供了更大的自由来选择最适合其领域的类的那些属性。当班级变得更加专业时,这尤其有利。

通常,应该首选通过组合来创建类,然后将这些类的大部分属性共享给继承这些类,而将差异保留为组合。

IOW,YAGNI(您不需要继承)。

5
Huperniketes

如果您将A子类化为B类,但没人关心B是否继承自A,那么这表明您可能做错了。

3
tia

“最偏重于继承的构成”是最愚蠢的主张。它们都是OOP的基本要素。这就像在说“在锯子上用锤子砸”。

当然可以滥用继承,可以滥用任何语言功能,可以滥用条件语句。

2
Chuck Stephanski

也许最明显的是,当继承类最终带有一堆在公开接口中从未使用过的方法/属性时。在最坏的情况下,基类具有抽象成员,您会发现抽象成员的空(或无意义)实现只是按原样“填充空白”。

更巧妙地讲,有时您会看到在试图提高代码重用性的地方,将具有相似实现的两件事压缩到一个实现中,尽管这两项没有关联。事实证明,除非很早就发现相似性只是巧合,否则可能会很痛苦,这往往会增加代码的耦合和分离。

更新了几个示例:尽管与编程本身没有直接关系,但我认为这类事情的最佳示例是本地化/国际化。在英语中,有一个单词是“是”。在其他语言中,为了语法上的同意,可以有很多很多。有人会说,啊,我们给一个字符串“是”,对许多语言来说都很好。然后他们意识到字符串“ yes”实际上并不对应于“ yes”,但实际上是“对该问题的肯定答案”的概念-一直为“ yes”的事实是一个巧合,而节省的时间仅是解开产生的混乱完全没有一个“是”。

我在我们的代码库中看到了一个特别讨厌的例子,其中实际上是父->子关系,其中父和子具有相似名称的属性是通过继承建模的。没有人注意到这一点,因为一切似乎都在起作用,然后孩子(实际上是基本的)和父母(子类)的行为需要朝不同的方向发展。幸运的是,这恰好是在最后期限迫在眉睫的时候发生的-在这里尝试调整模型时,复杂性(从逻辑和代码重复方面而言)立刻就消失了。由于代码根本与业务如何思考或谈论这些类所代表的概念无关,因此也很难进行推理/使用。最后,我们不得不硬着头皮做一些重大的重构,如果原始家伙没有决定几个具有相同值的相似发音属性表示相同的概念,则可以完全避免这些重构。

0
FinnNk

我认为诸如继承与组合之类的战斗实际上只是更深层次的核心战斗的各个方面。

这场斗争是:什么将导致最少的代码量,以及未来增强中最大的可扩展性?

这就是为什么这个问题很难回答的原因。在一种语言中,继承可能比在组合中比在其他语言中更快地实现继承,反之亦然。

或者,这可能是您正在解决的代码中的特定问题,而不是长期的增长,更多的是权宜之计。与编程问题一样,这也是一个商业问题。

因此,回到问题上,如果继承在将来的某个时候使您陷入困境,或者它创建的代码不需要存在(类无缘无故地从其他人继承,或者更糟糕的是,基类开始必须对儿童进行大量的条件测试)。

0

在某些情况下,应该优先考虑继承而不是继承,并且区别要比风格问题更为明确。我是从Sutter和Alexandrescu的C++编码标准来解释的,因为我的副本目前在我的书架上。强烈建议您阅读。

首先,不鼓励在不必要时继承,因为继承会在基类和派生类之间建立非常紧密的紧密联系,从而可能导致维护方面的麻烦。继承的语法使阅读变得更困难,这不仅仅是一些人的观点。

就是说,当您希望在当前正在使用基类的上下文中重用derived类本身而不必在该上下文中修改代码时,则鼓励继承。例如,如果您的代码开始看起来像if (type1) type1.foo(); else if (type2) type2.foo();,则可能有一个很好的案例来研究继承。

总之,如果您只想重用基类方法,请使用组合。如果要在当前使用基类的代码中使用派生类,请使用继承。

0
Karl Bielefeldt