Fragile Base Class是“继承打破封装”的唯一原因吗?

问题描述 投票:5回答:3

正如四人组在“Design Patterns”中所述:“it's often said that'继承打破了封装'”,在“面向对象编程语言中的封装和继承”中解释了Snyder。

但是,每当我读到“inheritance breaks encapsulation”时,这个主张背后的原因要么含糊不清,要么用Fragile Base Class问题的例子来解释。

在阅读论文时,我感觉真正打破封装的唯一继承属性是downcalls,这是open recursiondynamic dispatchthis上)允许的特性,并定义为“当超类方法调用在子类中重写的方法时”,根据Ruby&Leavens的“在不看超类代码的情况下安全地创建正确的子类”。 此外,根据Aldrich在“选择性开放递归:脆弱基类问题的解决方案”中的说法,开放递归显然是导致Fragile Base Class问题的原因。

因此,如果脆弱基类问题是“继承打破封装”的唯一原因,那么更明确地说,下调会破坏封装。由于存在一些解决方案以避免在使用继承时进行下调,因此继承本身并不真正涉及破坏封装。此外,四人帮提出的取消继承的委托模式也可以允许开放递归和下调,因为委托人的上下文(this)被委托使用(这可能导致一种脆弱的委托类问题)。

因此我的问题是: 脆弱的基类问题是否说“继承打破封装”的唯一原因?

oop inheritance encapsulation late-binding dynamic-dispatch
3个回答
4
投票

你的是一个有趣的问题。我倾向于同意你的观点:继承的主要问题是父母与子女之间的耦合。这种关系阻碍了父类在不破坏其子女的情况下进化的能力。

我在解释你是想要问脆弱的基类问题是否是违反“继承中断封装”原则的唯一表现,对吧?

TLDR;

我相信(像你一样)如果我们在使用继承时打破封装,那么肯定的是,这种强耦合问题的表现在父类的脆弱性中很明显,当它改变时会破坏它的子节点。

所以,从这个意义上来说,我认为你可能是正确的。

相反的情况不一定正确,即,具有脆弱的基类并不一定意味着您破坏了继承封装规则。

耦合就是问题

我仔细阅读了你提供的相同参考书目,这段摘录可以为这个问题提供一些启示。

从设计模式:可重用的面向对象软件的元素:

“父类通常至少定义其子类的物理表示的一部分。因为继承将子类暴露给其父实现的细节,所以通常会说“继承打破了封装”[Sny86]。

因此,似乎GoF在这里暗示了基本问题是耦合问题。显然,脆弱的基类是耦合问题的一种表现形式,所以你可能仍会对某些东西产生影响

对象通常具有公共接口,向世界公开它可以做什么的契约。对象的公共接口中的这​​些方法必须满足对象提供的服务的几个前提条件,不变量和后置条件。因此,对象的用户根据该合同与其进行交互。

对象的用户不需要知道合同的实现细节。因此,如果我们违反该合同,该对象的所有用户都会受到影响。

对对象的公共接口的这种依赖是一种耦合形式,当存在耦合时,存在一种易于改变的脆弱形式。

例如,驾驶员不需要知道液压方向盘系统如何在他们的汽车中工作,但他们仍然可以驾驶轿车或SUV,就好像它们是相同的东西,因为他们了解方向盘和合同的抽象管理其公共接口。然而,如果我们改变方向盘,一般来说,就好像是叉车那样工作,那么可能每个人都会撞车(我们打破了方向盘公共接口)。

因此,期望一个类的公共接口非常稳定,并且预计那里的任何更改肯定会破坏其客户端的功能。

当子类不仅仅依赖于对象的公共接口时,继承会破坏封装,当他们需要有关其父类如何实现的额外知识时才能提供功能性子实现(这就是为什么委托在某些情况下是一个很好的选择,因为委托只取决于对象的公共接口)。

状态耦合

这表现在不同的方式,例如,如果您可以直接访问父类中的状态变量,则可以打破封装(在您分享的Snyder文章中也提到过)。

从面向对象编程语言中的封装和继承:

“为了保留封装的全部好处,类定义的外部接口不应包含实例变量”

在具有可访问性修饰符(例如,Java或C#)的静态语言中,如果我们从父类公开非私有字段,则可能会出现此类违规。在动态语言中,如果没有可访问性修饰符,当子类实现者访问仅供私有父类使用的字段时(例如,Python中的_field),就会出现这种违规。

你会认为这个字段访问问题是脆弱基类问题的一部分吗?在我看来,这种形式的继承耦合并不一定包含在您所共享的着名文章的脆弱基类问题的概念中。

受保护的接口耦合

这表明的另一种方式是,现在父类可能需要公开除公共接口之外的新接口,但仅用于继承目的:受保护的接口。因此,它可能需要公开一组“受保护”或“特权”方法,这些方法可以访问通常不会在为常规对象用户提供的公共接口中公开的其他细节。

这显然是必要的,因为子类需要这些额外的细节才能提供具有一些扩展功能或改变行为的父类的合理实现。

现在,父类还需要确保这个受保护的接口非常稳定,因为任何更改都会破坏继承类,因为它的公共接口中的更改会破坏常规类用户。

在这一点上,我们会产生一种强烈的耦合形式,这可能会阻止父类在将来因其子女可能引起的潜在问题而发展。

现在,注意在设计时表现出封装的耦合和破坏,因此,这里也引入了基类的脆弱性,即使这种耦合问题从未在代码中出现,因为我们从不在父类中引起变化。

因此,我的解释是,继承引入的耦合导致封装的破坏,这反过来导致您描述的脆弱的基类问题。

在某种程度上,你的问题似乎暗示了一个因果关系,你似乎建议脆弱的基类问题是什么打破了继承,但在我的情况下,我认为它是另一种方式:父和子之间的耦合打破封装和这个高耦合程度在设计中体现为脆弱的基类问题。

没有封装破坏的脆弱性

话虽如此,我们现在有了一个问题,我们是否可以拥有一个脆弱的基类而不破坏封装?

我相信我们做到了。子类可能完全依赖于父类的公共接口。例如,子类可能提供了一个非继承的全新方法,它不是父公共接口或受保护接口的一部分。

然后有一天,我们在父类中添加一个新方法,该方法与我们很久以前在子类中添加的签名完全相同,但考虑到完全不同的意图。

现在我们已经破坏了一些东西,因为这个对象的用户希望新方法的行为与父类接口中所述的一样,而不是在子类中实现。

这个bug可能很难捕捉到。在某些语言中,这可能会导致子类失败;在其他情况下,可以假设孩子被覆盖的版本是使用权。

此问题的一个变体是,如果父类中定义的新方法具有与子类中定义的完全相同的方法具有不同的可访问性修饰符,作为两个类的独立进化的一部分。

无论如何,这些差异并不一定会破坏封装,但由于继承引入的耦合,它们确实使父/子关系变得脆弱,对吧?

换句话说,尽管在这种情况下父类和子类之间的封装正常,但父类是脆弱的。

使用继承打破封装确实会导致脆弱的基类,但是脆弱的基类很多并不一定意味着继承关系中的封装问题,据我所知。


0
投票

您只需要小心何时以及如何使用继承。

在许多语言中,默认情况下,您可以从基类继承(根据语言规则),除非基类的开发人员采取措施来阻止它。但仅仅因为你可以继承,并不意味着基类被设计为在子类化时正常工作。

封装现在不起作用。如果我创建一个类,一切都很好地封装,你在我不知情的情况下创建一个子类,而我,我没有意识到它,对我的类进行了更改,打破了你的。那可能发生。

所以你需要一个设计为子类的基类。


0
投票

不,这不是它说的唯一原因。据说,因为在做一个不明智的继承使用时,你最终可能会有一个超过必要的类耦合(基础和派生):InheritanceBreaksEncapsulation

开放递归只是OO设计的一个特定技术问题。句子“继承打破封装”只是一个更普遍的断言。

此外,完整的封装不必总是满满的,这取决于您的需求和背景。

© www.soinside.com 2019 - 2024. All rights reserved.