【问题标题】:why derive from a concrete class is a poor design为什么从具体类派生是一个糟糕的设计
【发布时间】:2013-05-23 22:29:41
【问题描述】:

我正在阅读关于NonVirtual Interface pattern 的文章:Herb Sutter 正在谈论为什么虚拟函数在大多数情况下必须是私有的,在某些情况下是受保护的并且从不公开。

但在文章的最后他写道:

不要从具体类派生。或者,正如 Scott Meyers 在更有效的 C++ 的第 33 条中所说的那样,[8]“使非叶类抽象化”。 (诚​​然,它可能在实践中发生——当然是由其他人编写的代码,而不是你!——在这种情况下,你可能必须有一个公共的虚拟析构函数来适应已经很糟糕的设计。最好重构不过,如果可以,请修复设计。)

但我不明白为什么这是一个糟糕的设计

【问题讨论】:

  • 这正是我在阅读文章后偶然发现的,谷歌搜索把我带到了这里哈哈

标签: c++ inheritance


【解决方案1】:

您可以购买更有效的 C++ 的副本,或查看您当地的图书馆以获取副本,并阅读第 33 条以获取完整说明。那里给出的解释是它使您的课程容易出现部分分配,也称为切片:

这里有两个问题。首先,在最后一行调用的赋值运算符是Animal 类的,即使涉及的对象是Lizard 类型。因此,只会修改liz1Animal 部分。这是部分任务。赋值后,liz1Animal 成员拥有从liz2 得到的值,但liz1Lizard 成员保持不变。

第二个问题是真正的程序员会这样写代码。通过指针对对象进行赋值并不少见,尤其是对于已经转向 C++ 的有经验的 C 程序员。在这种情况下,我们想让作业以更合理的方式表现。正如Item 32所指出的,我们的类应该容易正确使用,难于错误使用,上面层次结构中的类容易错误使用。

请参阅this question 和其他有关 C++ 中对象切片问题的描述。

第 33 条也这么说,稍后:

用像AbstractAnimal 这样的抽象基类替换像Animal 这样的具体基类所产生的好处远不止使operator= 的行为更容易理解。它还减少了您尝试多态处理数组的机会,其不愉快的后果将在第 3 项中进行检查。然而,该技术的最重要的好处发生在设计级别,因为用抽象基替换具体基类类迫使您明确识别有用抽象的存在。也就是说,它使您可以为有用的概念创建新的抽象类,即使您不知道有用的概念存在这一事实。

【讨论】:

  • 这是本文中唯一的明智答案(至少在撰写本文时)。
  • 嗯,简短的回答是灵活性。但这必须是规则,所以每次我想要多态时,基类都必须是抽象的?
  • 编译器不强制执行该建议。如果您的雇主也没有强制执行,那么我认为这不是“规则”。由您决定是否要以此为生。如果你打算做大量的 C++ 编程,那么听从 Herb Sutter 的建议是一个非常聪明的主意。
  • @elvis.dukaj 这个建议当然是有效的,但我认为这个特别的“规则”(至少是明确表达的)并不那么好。
  • @NikBougalis 说 SuperCar 可以达到 1000KPH,其中 Car(或期望 Car 的客户端代码)内的所有内容都假定它不是一个合理的值 - 它是一辆混凝土汽车,毕竟 - 并相应地使用它。这个限制是隐含的,可能会脱离Car 的实现。如果合同明确、正确、完整,我的回答不适用;但据我所读,这种情况很少见。
【解决方案2】:

一般来说,很难严格维护两个不同的具体对象之间的契约。

当我们处理泛型行为时,继承变得非常简单和健壮。

假设我们要创建一个名为Ferrari 的类和一个名为SuperFerrari 的子类。 合约为:turnTheKey(), goFast(), skid()

乍一看,听起来很相似,没有冲突。让我们继承这两个具体的类。

但是,现在我们要为SuperFerrari 添加一个功能:turnOnBluRayDisc()

问题:继承需要组件之间的 IS-A 关系。有了这个新功能,SuperFerrari 不再是一个简单的Ferrari,有了自己的行为;它现在添加行为。 这将导致一些丑陋的代码,需要一些强制转换,以便在最初处理 Ferrari 引用时选择新功能(多态性)。

有了两个类共有的抽象/接口类,这个问题就会消失,因为 FerrariSuperFerrari 将是两个叶子,而且现在 FerrariSuperFerrari 不应该被视为更符合逻辑类似(不再是 IS-A 关系)。

简而言之,从具体类派生在许多情况下会导致代码质量差/丑陋、不够灵活、难以阅读和维护。

创建由抽象类/接口驱动的层次结构允许具体的子类单独发展而不会出现任何问题,尤其是当您实现一些专用于特定数量的具体类的子层次结构而不会使其他叶子枯燥,并且仍然受益于多态性时。

【讨论】:

  • 我真的不关注... SuperFerrari 完全是 Ferrari - 它只是有一个花哨的蓝光播放器。那么为什么它不能从Ferrari 派生,为什么有某种FerrariCar 基类FerrariSuperFerrari 都派生自更好?它能给我们带来什么好处(如果有的话)?
  • @Nik Bougalis 当有一个包含一些法拉利的数据结构时,人们会迭代,检查有效类型,如果是SuperFerrari,则转动蓝光光盘。 IMO,这降低了多态性的优雅。更好的是,让我们反转样本。添加到Ferrari 的每个新功能都将成为SuperFerrari 的一部分,即使它根本没有任何意义。没有办法单独发展新功能。两个具体类不能属于同一个分支。
  • 我不完全同意。基类是基类,因为它只实现公共接口。派生类必须实现新功能。
  • @Mik378 一个通用的基类不会做任何事情来解决您描述的“迭代列表并在那些拥有它的模型中打开蓝光光盘”问题,除非它实现了一个功能,即默认情况下什么都不做。如果每个Ferrari 功能都在SuperFerrari 中没有意义,那么是的,您需要一个基类。但如果真的发生了怎么办?为什么在Ferrari 可以做到的情况下强制需要一个基类。不,对不起。我只是不相信你的解释。
  • 不过,我不买它。除非 SuperFerrari 不是总是 Ferrari,否则两者的新抽象基础只是多余的。
【解决方案3】:

从具体类型继承的一个问题是它会产生一些歧义,即指定特定类型的代码是否真的想要特定具体类型的对象,或者想要行为方式与具体类型的行为。这种区别在 C++ 中至关重要,因为在许多情况下,对某种类型的对象正确工作的操作在派生类型的对象上会严重失败。在 Java 和 .NET 中,可以使用特定类型的对象但不能使用派生类型的对象的情况要少得多。因此,从具体类型继承几乎没有问题。然而,即使在那里,拥有从抽象类继承的密封具体类,并在除构造函数调用(必须使用具体类型)之外的任何地方使用抽象类类型,将更容易在不破坏代码的情况下更改类层次结构的部分。

【讨论】:

    【解决方案4】:

    我认为这是因为具体类具有具体行为。从它派生时,您承诺保留相同的“契约”,但实际契约是由基类的具体实现定义的,实际上会有许多微妙之处,您可能会在不知不觉中打破。

    免责声明:我不是经验丰富的开发人员;这只是一个建议。

    【讨论】:

    • 在其他一些语言中,类通常被设计为具体的以特定方式子类化,并带有描述这些方式的文档。 (iOS Objective-C SDK 中的UIView 就是一个很好的例子。)C++ 的某些特定属性使这种做法很危险。
    【解决方案5】:

    当您从具体的基类派生时,您继承了功能,如果您没有源代码,这些功能可能不会显示给您。当您对接口(或抽象类)进行编程时,您不会继承功能,这意味着它是一种更好的方式来执行诸如公开 API 之类的事情。话虽如此,我认为很多时候从具体类继承是可以接受的

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-29
      • 1970-01-01
      • 1970-01-01
      • 2021-04-28
      • 1970-01-01
      相关资源
      最近更新 更多