【问题标题】:Inheritance or composition: Rely on "is-a" and "has-a"?继承还是组合:依赖“is-a”和“has-a”?
【发布时间】:2010-10-02 00:14:32
【问题描述】:

当我设计类并且必须在继承和组合之间进行选择时,我通常使用经验法则:如果关系是“is-a” - 使用继承,如果关系是“has-a” - 使用组合.

总是正确的吗?

谢谢。

【问题讨论】:

    标签: c++ inheritance oop


    【解决方案1】:

    不——“是一个”并不总是导致继承。一个被广泛引用的例子是正方形和矩形之间的关系。正方形就是长方形,但是设计从 Rectangle 类继承 Square 类的代码会很糟糕。

    我的建议是使用Liskov Substitution Principle 来增强您的“是/具有”启发式。要检查继承关系是否符合里氏替换原则,请询问基类的客户端是否可以在不知道它正在操作子类的情况下对子类进行操作。当然,必须保留子类的所有属性。

    在正方形/矩形的例子中,我们必须要问一个矩形的客户端是否可以在不知道它是正方形的情况下对正方形进行操作。客户必须知道的只是它是在一个矩形上操作的。下面的函数演示了一个假设设置矩形宽度保持高度不变的客户端。

    void g(Rectangle& r)
    {
        r.SetWidth(5);
        r.SetHeight(4);
        assert(r.GetWidth() * r.GetHeight()) == 20);
    }
    

    这个假设适用于矩形,但不适用于正方形。所以函数不能在正方形上运行,因此继承关系违反了里氏替换原则。

    Other examples

    【讨论】:

    • 好的,它说明了这一点。但是,为什么正方形是可变的?
    • 每个人都听说过正方形和矩形问题,但我从未真正遇到过这样的真实示例(我是一名初级开发人员,所以这并不意味着什么)。你能提供一个吗?
    • @Ed - 示例:TCP 套接字使用文件描述符,因此您可能会说它是文件。但是,大多数文件 API 都假设可以进行随机访问(例如 fseek)。 TCP 套接字也是 IP 套接字,但任何客户端都不应直接访问 IP 层(让 TCP 堆栈处理)。
    • @John - 您的问题突出了这样一个事实,即继承(与 Liskov 替换原则有关)完全与行为有关。如果正方形不可变,那么就没有问题,因为没有客户端可以更改宽度或高度。
    • 这是一个毛皮。正方形在数学上可能是矩形,但如果您将矩形定义为可以独立设置高度和宽度的四边形,则正方形不适合。在这种情况下,矩形实际上通过具有两个不同边长的附加属性来扩展正方形,而不仅仅是一个。
    【解决方案2】:

    是和不是。

    这条线可以模糊。 OO 早期的一些非常糟糕的 OO 编程示例并没有帮助解决这个问题,例如:Manager is an Employee is an Person。

    关于继承,您必须记住的一点是:继承破坏了封装。继承是一个实现细节。关于这个主题有各种各样的文章。

    最简洁的总结方式是:

    喜欢构图。

    这并不意味着使用它来完全排除继承。这只是意味着继承是一个后备位置。

    【讨论】:

    • 嗯,经理是员工,而员工个人。你的意思是什么?
    • @Iraimbilamja:是的,人员经理通常是员工,但这与经理模式完全不同。不幸的是,我看到了一些将这两种含义混为一谈的解释。
    • 这是一个完美的例子,可以解释为什么你应该更喜欢作曲。
    • 经理是员工,但人有(零个或多个)员工角色是更好的抽象。如果您的对象更改类型(类),这是一个警告信号,您做错了什么,如果员工成为经理,就会发生这种情况。您也可以争辩说 Manager 也是 Employee 的属性
    • 请注意,问题最初说“is a”和“has a”都是继承 - 请参阅我的回答 cmets。现在已更正以匹配您假定的版本,但“其他方式”正式不准确。
    【解决方案3】:

    如果关系是“is-a” - 使用继承,如果关系是“has-a” - 使用组合。 总是正确的吗?

    从某种意义上说,是的。但您必须注意不要引入不必要的、人为的“is-a”关系。

    例如,有人可能认为ThickBorderedRectangle 是一个 Rectangle,乍一看似乎很合理,并决定使用继承。但是这种情况更好地描述为 Rectangle has-a Border,它可能是也可能不是 ThickBorder。在这种情况下,他会更喜欢作曲。

    以同样的方式,人们可能认为厚边框是一个特殊的边框并使用继承;但最好说边框 has-a 宽度,因此更喜欢构图。

    在所有这些模棱两可的情况下,我的经验法则是三思而后行,并且正如其他人建议的那样,更喜欢组合而不是继承

    【讨论】:

    • 人们可能还认为 Rectangle 没有任何东西,而是某些 RenderableThing 具有 Shape 或类似的东西
    • 很好的例子!可耻的是,我自己也不会想到它们(除非我深思熟虑)。
    【解决方案4】:

    我认为这并不像“is-a”与“has-a”那么简单——正如 cletus 所说,这条线会变得非常模糊。仅仅作为一个指导原则,两个人并不总是会得出相同的结论。

    正如 cletus 所说,更喜欢组合而不是继承。在我看来,必须有充分的理由从基类派生 - 特别是,基类确实需要设计 用于继承,并且确实需要用于您想要的那种专业化申请。

    除此之外 - 这可能是一个不受欢迎的想法 - 它归结为品味和直觉。随着时间的推移,我认为优秀的开发人员会更好地了解继承何时有效,何时无效,但他们可能会发现很难清楚地表达出来。我知道 觉得很难,正如这个答案所表明的那样。也许我只是将困难投射到其他人身上:)

    【讨论】:

    • 乔恩,你从哪里得到这个“为继承而设计”的口头禅?这是我第三次或第四次听到你这样说,我只是不明白。我是否必须购买并阅读您的书才能得到解释?或者我应该问一个问题让你回答,这样你就可以得到更多的代表? ;-) 我会这样做的
    【解决方案5】:

    继承并不总是意味着存在“is-a”关系,缺少继承并不总是意味着不存在。

    例子:

    • 如果您使用受保护或私有 继承然后你失去外部 可替代性,也就是说,只要 您的层次结构之外的任何人都是 担心 Derived 不是 基地。
    • 如果您覆盖一个基础虚拟成员并更改从引用基础的人观察到的行为,从原始基础实现,那么您实际上破坏了可替代性。即使您有一个公共继承层次结构 Derived is-not-a Base。
    • 您的类有时可以在没有任何继承关系的情况下替换为另一个。例如,如果您要为适当的 const 成员函数使用模板和相同的接口,则椭圆恰好是完美圆形的“Ellipse const&”可以替代“Circle const&”。

    我在这里想说的是,无论您是否使用继承,在两种类型之间维持“is-a”可替代关系都需要大量的思考和工作。

    【讨论】:

      【解决方案6】:

      是。如果关系是“has-a”,则使用组合。 (添加:问题的原始版本对两者都说“使用继承” - 一个简单的错字,但更正将我的答案的第一个单词从“否”更改为“是”。)

      并且默认使用合成;仅在必要时使用继承(但不要犹豫使用它)。

      【讨论】:

        【解决方案7】:

        人们经常说继承是一种“is-a”关系,但这会给您带来麻烦。 C++ 中的继承分为两种思路:代码重用和定义接口。

        第一个说“我的类和其他类一样。我将为它们之间的增量编写代码,并尽可能多地重用其他类的其他实现。”

        第二个依赖于抽象基类,是类承诺实现的方法列表。

        第一个可能非常方便,但如果做得不好也会导致维护问题,因此有些人甚至会说你不应该这样做。大多数人强调第二个,将继承用于其他语言显式调用的接口。

        【讨论】:

          【解决方案8】:

          我认为约翰的想法是正确的。您将班级关系视为创建模型,这是他们在开始教您 OOP 原则时试图让您做的事情。确实,您最好根据代码架构 来了解事物的功能。也就是说,对于其他程序员(或您自己)来说,重用您的代码而不破坏它或不必查看实现以了解它的作用的最佳方式是什么。

          我发现继承经常打破“黑匣子”的理想,所以如果信息隐藏很重要,它可能不是最好的方法。我越来越多地发现继承对于提供通用接口比在其他地方重用组件更好。

          当然,每种情况都是独一无二的,没有正确的方法来做出这个决定,只有经验法则。

          【讨论】:

            【解决方案9】:

            我的经验法则(虽然不是具体的)是,如果我可以为不同的类使用相同的代码,那么我会将这些代码放入父类并使用继承。否则我会使用合成。

            这使我可以编写更少的代码,而这本质上更容易维护。

            【讨论】:

            • 我不明白为什么在代码重用方面继承比组合更好?
            • 我以前做过,但现在我知道这不是一个好主意,尤其是在不支持多重继承的语言中。如果稍后您需要在需要属于另一个层次结构的类中重用该代码怎么办?
            • 这也会导致您在代码库中拥有更多的相互依赖关系。虽然代码更少,但由于无法独立分析任何内容,最终会变得更加复杂。
            猜你喜欢
            • 2011-07-29
            • 2016-03-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-11-02
            • 2010-12-27
            • 1970-01-01
            相关资源
            最近更新 更多