对字段和方法使用私有而不是受保护的原因

Posted

技术标签:

【中文标题】对字段和方法使用私有而不是受保护的原因【英文标题】:Reasons to use private instead of protected for fields and methods 【发布时间】:2011-06-22 05:33:55 【问题描述】:

这是一个相当基本的 OO 问题,但一直困扰着我一段时间。

我倾向于避免对我的字段和方法使用“私有”可见性修饰符,而使用protected

这是因为,一般来说,我认为隐藏基类和子类之间的实现没有任何用处,除非我想为我的类的扩展(即在框架中)设置特定的指导方针。在大多数情况下,我认为试图限制我或其他用户扩展我的课程的方式是没有好处的。

但是,对于大多数人来说,private 修饰符通常是定义非公共字段/方法时的默认选择。

那么,您能列出private 的用例吗?总是使用私人有一个主要原因吗?还是你也认为它被过度使用了?

【问题讨论】:

我同意你的想法。请注意,在 Objective C 中,它们默认受到保护。 有些人建议将“YAGNI”[你不需要它]作为将字段设为私有的理由,但我不同意这一点。 YAGNI 背后的一般原则是,如果一个人需要什么,那么就可以担心它。一个合理的想法,但如果最终需要某些东西的人与任何可以提供它的人没有联系,它就行不通。例如,有一个类可以被任何需要 List<T> 的代码使用,但也包括像 ActOnItem(int index, ref T item) 这样的方法来允许“就地”更新列表项。 如果List<T>的backing store是protected,这样的方法可以相当容易地实现;不幸的是,因为它不是,所以没有办法编写兼容的List<T> 替换,包括这样的方法。 好点。每当您打包某个软件并将其提供给公众时,用户将如何扩展它并不总是很明显 【参考方案1】:

有一些共识是在 OOP 中应该prefer composition over inheritance。这有几个原因(如果您有兴趣,请谷歌),但主要部分是:

继承很少是最好的工具,也不像其他解决方案那样灵活 受保护的成员/字段形成了与您的子类的接口 接口(以及对其未来使用的假设)很难正确处理和正确记录

因此,如果您选择让您的类可继承,您应该有意识地这样做,并考虑到所有的利弊。

因此,最好不要使类可继承,而是通过使用其他方式确保它尽可能灵活(仅此而已)。

这在您的类的使用超出您的控制范围的大型框架中最为明显。对于您自己的小应用程序,您不会注意到这一点太多,但如果您不小心,它(默认继承)迟早会咬到您。

替代品

组合意味着您将通过显式(完全抽象)接口(虚拟或基于模板)公开可定制性。

因此,您将拥有一个带有 Motor 接口对象的 Vehicle 类,而不是具有虚拟 drive() 函数(以及其他所有内容,例如价格整数等)的 Vehicle 基类,并且Motor 接口只公开了 drive() 函数。现在您可以在任何地方添加和重复使用任何类型的电机(或多或少。:)。

【讨论】:

【参考方案2】:

成员是protected还是private有两种情况:

    如果派生类可以从使用成员中受益,则将成员设置为“受保护”将允许它这样做,而将其设置为“私有”将拒绝它的好处。 如果基类的未来版本可以通过使成员不像当前版本中那样行为而受益,则将成员设为“私有”将允许该未来版本改变行为(或完全消除该成员),而使其“受保护”将要求该类的所有未来版本保持相同的行为,从而剥夺了他们可以从更改它中获得的好处。

如果可以想象派生类可能会从能够访问成员中受益的现实场景,并且无法想象基类可能会从改变其行为中受益的场景,那么成员应该是protected [假设,当然,它不应该公开]。如果无法想象派生类会从直接访问成员中获得很多好处的场景,但可以想象未来版本的基类可能会通过更改它而受益的场景,那么它应该是private。这些案例非常清晰明了。

如果没有任何合理的场景可以使基类受益于更改成员,我建议应该倾向于将其设置为protected。有人会说“YAGNI”(你不需要它)原则支持private,但我不同意。如果您期望其他人继承该类,则将成员设为私有不会假定“YAGNI”,而是“HAGNI”(他不会需要它)。除非“你”需要在未来版本的课程中改变项目的行为,否则“你”不需要它是private。相比之下,在许多情况下,您无法预测您所在班级的消费者可能需要什么。这并不意味着一个人应该在不首先尝试确定可以从更改它们中受益的方式的情况下创建成员protected,因为YAGNI 并不真正适用于任何一个决定。 YAGNI 适用于可以在遇到未来需要时处理它的情况,因此现在不需要处理它。决定将某个类的成员提供给其他程序员privateprotected 意味着决定将提供哪种类型的潜在未来需求,并且将难以为其他程序员提供。

有时这两种情况都是合理的,在这种情况下,提供两个类可能会有所帮助——其中一个公开有问题的成员,一个从不公开的类派生(没有标准的惯用语是派生类隐藏从其父级继承的成员,尽管声明具有相同名称但没有可编译功能且标有Obsolete 属性的新成员会产生这种效果)。作为所涉及的权衡示例,请考虑List<T>。如果该类型将后备数组公开为受保护的成员,则可以定义派生类型CompareExchangeableList<T> where T:Class,其中包括一个成员T CompareExchangeItem(index, T T newValue, T oldvalue),该成员将返回Interlocked.CompareExchange(_backingArray[index], newValue, oldValue);任何期望List<T> 的代码都可以使用这种类型,但是知道实例是CompareExchangeableList<T> 的代码可以在其上使用CompareExchangeItem。不幸的是,因为List<T> 没有将支持数组公开给派生类,所以不可能定义一个允许CompareExchange 出现在列表项上的类型,但仍然可以被期望List<T> 的代码使用。

不过,这并不意味着公开支持数组完全没有成本;即使List<T> 的所有现有实现都使用单个后备数组,Microsoft 可能会在将来的版本中实现当列表超出 84K 时使用多个数组,以避免与大对象堆相关的低效率。如果支持数组作为受保护成员公开,则不可能在不破坏任何依赖该成员的代码的情况下实现这样的更改。

实际上,理想的做法可能是通过提供一个受保护的成员来平衡这些利益,给定一个列表项索引,该成员将返回一个包含指定项的数组段。如果只有一个数组,该方法将始终返回对该数组的引用,偏移量为零,起始下标为零,长度等于列表长度。如果List<T> 的未来版本将数组拆分为多个部分,则该方法可以允许派生类有效地访问数组的段,而没有这种访问是不可能的[例如使用Array.Copy] 但List<T> 可以改变它管理其后备存储的方式,而不会破坏正确编写的派生类。如果基实现发生更改,编写不当的派生类可能会损坏,但这是派生类的错,而不是基类。

【讨论】:

最后一句话几乎说明了一切。如果你说的不是私人的,我已经允许你改变这种行为,如果你这样做,我提供的仍然会“工作”。如果那不是真的,字帖被弄脏了。 :( @TonyHopkinson:并没有说派生类可以改变行为,而是说派生类可以利用该行为。请参阅我关于 List<T> 的附录。 我换个角度看。就我而言,组件暴露的界面是 UI,“点击”东西不应该破坏它。我尽量避免暴露 List... @TonyHopkinson:按照我的理解,任何允许生成和使用派生自 T 的类型的代码都将能够生成严重损坏的对象并将它们传递给需要 @ 的代码987654349@,除了禁止对象从 T 派生外,真的没有办法防止这种情况发生。基类没有义务防止派生严重损坏的类,基类也不能做太多事情来防止派生这样的类,即使它想。我对List<T> 示例的观点是限制派生类访问基成员的能力... ...可能使派生类无法执行原本完全合理的事情(例如列表项上的CompareExchange);人们必须决定从施加这种限制中获得的好处是否值得对那些将来自您的班级的人施加的成本。【参考方案3】:

我只是在默认情况下更喜欢私有而不是受保护,因为我遵循隐藏尽可能多的可能性的原则,这就是为什么将可见性设置得尽可能低。

【讨论】:

【参考方案4】:

我到达这里。但是,我认为应该有意识地使用 Protected 成员变量,因为您不仅打算继承,而且因为派生类不应该使用基类上定义的 Property Setters/Getters 有充分的理由。

在 OOP 中,我们“封装”了成员字段,以便我们可以控制如何访问和更改代表的属性。当我们在我们的基础上为成员变量定义一个 getter/setter 时,我们本质上是在说这就是我希望这个变量被引用/使用的方式。

虽然存在设计驱动的异常,可能需要更改基类 getter/setter 方法中创建的行为,但在我看来,这将是在仔细考虑替代方案后做出的决定。

例如,当我发现自己需要直接从派生类而不是通过 getter/setter 访问成员字段时,我开始想也许应该将特定的 Property 定义为抽象的,甚至移至派生类.这取决于层次结构的广泛程度以及任何数量的其他考虑因素。但对我来说,绕过基类上定义的公共属性开始闻起来。

当然,在许多情况下,这“无关紧要”,因为除了访问变量之外,我们没有在 getter/setter 中实现任何东西。但同样,如果是这种情况,派生类可以通过 getter/setter 轻松访问。如果持续使用,这也可以防止以后难以发现的错误。如果基类上成员字段的 getter/setter 的行为以某种方式更改,并且派生类直接引用 Protected 字段,则可能会出现问题。

【讨论】:

我应该补充一点,如果一个人决定遵循我在上面的帖子中定义的策略,最后一个重要的组件应该是:当逐步使用基类上定义的 getter/setter 时,记录这样做的理由!你懂的。记录。我们都在编码时立即执行此操作。正确的?嗯,当然。 . . 换一种说法,一些对类“私有”的字段应该被认为比那个更私有——只能由单个属性的 getter/setter 访问。没有任何方法可以严格地声明性地限制对字段的访问,但是通常不应在派生类中访问这样的字段。【参考方案5】:

你在正确的轨道上。您将某些内容设为私有,因为您的实现依赖于它不会被用户或后代更改。

我默认为私有,然后有意识地决定我是否要公开以及公开多少内部运作,你似乎是在这样的基础上工作,无论如何它都会被公开,所以继续吧。只要我们都记得交叉所有的眼睛并点所有的 T 恤,我们就很好。

另一种看待它的方式是这样。 如果您将其设为私有,则某些人可能无法对您的实现做他们想做的事情。

如果您不将其设为私有,则有人可能会在您的实施中做一些您真的不希望他们做的事情。

【讨论】:

【参考方案6】:

我从 1993 年的 C++ 和 1995 年的 Java 开始就一直在编程 OOP。我一次又一次地看到需要扩充或修改一个类,通常是添加与该类紧密集成的额外功能。这样做的 OOP 方法是将基类子类化并在子类中进行更改。例如,某些其他操作需要最初仅在基类中的其他地方引用的基类字段,或者某些其他活动必须更改该字段的值(或该字段的包含成员之一)。如果该字段在基类中是私有的,则子类无法访问它,也无法扩展功能。如果该字段受到保护,它可以这样做。

子类与类层次结构中其他类的其他类没有的基类具有特殊关系:它们继承基类成员。继承的目的是访问基类成员;私人阻碍继承。基类开发人员应该如何知道没有子类需要访问成员?在某些情况下,这可能很清楚,但私有应该是例外而不是规则。子类化基类的开发人员拥有基类源代码,因此他们的替代方法是直接修改基类(可能只是在子类化之前将私有状态更改为受保护)。这不是干净的好做法,但这就是私人让你做的事情。

【讨论】:

【参考方案7】:

我是 OOP 的初学者,但从 ACM 和 IEEE 的第一篇文章就开始了。据我所知,这种开发方式更多是为了建模。在现实世界中,包括流程和操作在内的事物将具有“私有、受保护和公共”元素。所以要忠实于对象.....

除了建模之外,编程更多的是解决问题。 “私有、受保护和公共”元素的问题仅在涉及制定可靠的解决方案时才是一个问题。作为一个问题解决者,我不会犯错误,因为别人如何使用我的解决方案来解决他们自己的问题。现在请记住,导致 .... 问题的主要原因是为数据检查留出空间(即,在将数据用于对象之前验证数据是否在有效范围和结构中)。

考虑到这一点,如果您的代码解决了它所设计的问题,那么您就完成了工作。如果其他人需要你的解决方案来解决相同或类似的问题 - 那么,你真的需要控制他们如何做吗?我会说,“只有当你从中获得一些好处或者你知道设计中的弱点时,你才需要保护一些东西。”

【讨论】:

以上是关于对字段和方法使用私有而不是受保护的原因的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Java 反射 API 允许我们访问私有和受保护的字段和方法?这不会破坏访问修饰符的目的吗? [复制]

将属性置于受保护/私有的任何性能原因?

合成的实例变量是不是生成为私有而不是受保护?

私有/受保护的方法是不是应该进行单元测试? [关闭]

PHP5 保护变量背后的推理

与java中的私有/受保护方法相关的性能与设计[关闭]