5

正如四人组在“设计模式”中所说:人们常说‘继承打破封装’,在“面向对象编程语言中的封装和继承”中解释 Snyder。

然而,每次我读到“继承破坏封装”时,这种说法背后的原因要么被模糊地解释,要么以脆弱基类问题的例子来解释。

在阅读论文时,我感觉唯一真正打破封装的继承属性是下调用,这是开放递归动态分派)允许的功能,并this定义为“当超类方法调用在子类中被覆盖的方法时”,根据 Ruby & Leavens 在“安全地创建正确的子类而不查看超类代码”中的说法。此外,根据 Aldrich 在“Selective Open Recursion: A Solution to the Fragile Base Class Problem”中的说法
,开放递归显然是导致脆弱基类问题的原因。

因此,如果 Fragile Base Class 问题是“继承破坏封装”的唯一原因,那么说向下调用破坏封装会更清楚。由于存在一些解决方案来避免向下调用,同时仍然使用继承,因此继承本身并没有真正参与破坏封装。此外,四人组提出的摆脱继承的委托模式也可以允许开放递归和向下调用,因为委托人的上下文 ( this) 被委托人使用(这可能导致一种脆弱的委托类问题)。

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

4

3 回答 3

4

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

我的解释是你的意思是问脆弱的基类问题是否是违反“继承破坏封装”原则的唯一表现,对吗?

TLDR;

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

所以,在这个意义上或解释上,我认为你可能是对的。

相反的不一定正确,即,拥有脆弱的基类并不一定意味着您违反了继承-封装规则。

耦合是问题

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

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

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

因此,GoF 似乎暗示了根本问题是耦合问题。显然,脆弱的基类是耦合问题的表现,所以你可能仍然在做一些事情

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

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

这种对对象公共接口的依赖是一种耦合形式,当存在耦合时,就有一种脆弱性需要改变。

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

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

当子类不能仅仅依赖于对象的公共接口时,继承会破坏封装,当它们需要额外了解其父类的实现方式以提供功能性子实现时(这就是为什么委托在某些情况下是一个很好的选择,因为委托只依赖于对象的公共接口)。

状态耦合

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

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

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

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

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

受保护的接口耦合

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

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

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

在这一点上,我们遇到了一种强耦合形式,它可能会阻止父类在未来发展,因为它可能会在其子类中引起潜在的问题。

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

所以,我的解释是继承引入的耦合导致封装的破坏,进而导致你描述的脆弱的基类问题。

在某种程度上,您的问题似乎暗示了一个因果链,您似乎认为脆弱的基类问题是破坏继承的原因,但在我的情况下,我相信它是相反的:父子之间的耦合破坏了封装和这个高耦合程度在设计中表现为一个脆弱的基类问题。

没有封装破损的脆性

话虽如此,我们现在有一个问题,我们能否在不破坏封装的情况下拥有一个脆弱的基类?

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

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

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

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

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

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

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

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

于 2018-07-04T17:28:00.220 回答
0

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

开放递归只是面向对象设计的一个特定技术问题。“继承打破封装”这句话只是一个更一般的断言。

此外,一个完整的封装不必总是被填充,这取决于您的需求和上下文。

于 2018-07-05T00:52:11.507 回答
0

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

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

封装目前不起作用。如果我创建了一个类,一切都被很好地封装了,你在我不知情的情况下创建了一个子类,而我在不知情的情况下对我的类进行了更改,从而破坏了你的类。这可能会发生。

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

于 2018-06-23T15:24:53.397 回答