【问题标题】:A question about encapsulation and inheritence practices关于封装和继承实践的问题
【发布时间】:2025-11-26 23:55:02
【问题描述】:

我听说有人说拥有protected 成员会破坏封装点并且不是最佳实践,应该设计程序使得派生类不需要访问private 基类成员。


示例情况

现在,想象以下场景,一个简单的 8 位游戏,我们有一堆不同的对象,例如,常规框充当障碍物,尖峰硬币移动平台等。列表可以继续。

它们都具有x 和y 坐标一个指定对象大小的矩形和碰撞框,以及一个纹理。他们还可以共享设置位置渲染加载纹理检查碰撞等功能。 p>

但其中一些还需要修改基本成员,例如盒子可以被推来推去,因此它们可能需要移动功能,一些物体可能会自行移动,或者某些块可能会在游戏中改变纹理。

因此,像 object 这样的基类确实可以派上用场,但这需要大量的 getters - settersprivate 成员才能成为 @ 987654326@ 代替。无论哪种方式,都会损害封装性。


鉴于轶事背景,这将是一个更好的做法:

1. 有一个具有共享函数和成员的公共基类,声明为受保护。能够使用常用函数,将基类的引用传递给只需要访问共享属性的非成员函数。但妥协封装。

2. 为每个类设置一个单独的类,将成员变量声明为私有且不损害封装性。

3。一个我想不到的更好的方法。


我不认为封装是非常重要的,并且可能只是拥有受保护的成员,但我对这个问题的目标是编写一个经过良好实践的标准代码,而不是解决那个特定问题。

提前致谢。

【问题讨论】:

  • “我听说有人说拥有protected 成员有点破坏封装的点,这不是最佳实践......”这是不正确的,此时你应该停止倾听并咨询更权威的来源,如Design Patterns

标签: c++ inheritance encapsulation


【解决方案1】:

首先,我首先要说的是,设计并没有一刀切的答案。不同的问题需要不同的解决方案;但是,随着时间的推移,有些设计模式通常比其他设计模式更易于维护。

确实,很多关于设计的建议会让他们在团队环境中变得更好——但是好的实践对于单独的项目也很有用,这样它就可以更容易理解和改变未来。

有时需要理解你的代码的人会是你,一年后 - 所以请记住这一点?

我听说有人说拥有受保护的成员会破坏封装的点

像任何工具一样,它可能会被滥用;但是protected 访问本身并没有破坏封装。

定义对象封装的是预期的投影 API 表面积。有时,protected 成员在逻辑上是表面区域的一部分——这是完全有效的。

如果误用protected 成员可以让客户端访问可能破坏类的预期不变量的可变成员——这很糟糕。例如,如果您能够派生一个暴露 rectangle 的类,并且能够将宽度/高度设置为负值。基类中的函数(例如 compute_area)可能会突然产生错误的值,并导致级联故障,否则这些故障应该通过更好的封装加以防范。

至于您的示例设计:

基类不一定是坏事,但很容易被过度使用,并可能导致“上帝”类无意中暴露太多功能以共享逻辑。随着时间的推移,这可能会成为维护负担,并且只会造成混乱。

你的例子听起来更适合组合,有一些更小的接口:

  • pointvector 这样的类型将是基本类型,以产生像rectangle 这样的高阶组合。
  • 然后可以将它们组合在一起以创建一个 model,它处理 2D 空间中发生碰撞的一般(逻辑)对象。
  • 可以从外部实用程序类处理交叉/碰撞逻辑
  • 可以从renderable 接口处理渲染,任何需要渲染的类都从该接口扩展而来。
  • 交叉点处理逻辑可以通过intersectable 接口处理,该接口确定对象在交叉点上的行为(这有效地将每个游戏对象抽象为原始行为)

【讨论】:

    【解决方案2】:

    封装不是一个安全的东西,它是一个整洁的东西(因此是可支持性、可读性......)。您必须假设派生类的人基本上是明智的。毕竟,他们要么使用您的基类编写自己的程序(所以谁在乎),或者他们正在与您一起编写程序

    【讨论】:

      【解决方案3】:

      "encapsulation" 在面向对象编程中的主要目的是限制对数据的直接访问以最小化依赖关系,并且在必须存在依赖关系的情况下,用 函数 而不是 数据。

      这与Design by Contract 相关,您允许“公开”访问某些功能,并保留随时以任何理由任意修改其他功能的权利,甚至删除它们,将其表述为“受保护”。

      也就是说,你可以有一个像这样的游戏对象:

      class Enemy {
      public:
        int getHealth() const;
      }
      

      getHealth() 函数返回一个 int 值,表示运行状况。它是如何得出这个值的?呼叫者不需要知道或关心。也许它是您刚刚收到的二进制数据包的字节 9。也许它是来自 JSON 对象的字符串。没关系。

      最重要的是,您可以随意更改getHealth() 在内部的工作方式,而不会破坏任何依赖它的代码。

      但是,如果您要公开一个公开的 int health 属性,就会引发一连串的问题。如果操作不当怎么办?如果它设置为无效值怎么办?您如何捕获对被操纵属性的访问权限?

      当您拥有setHealth(const int health) 时,您可以更轻松地执行以下操作:

      • 将其限制在特定范围内
      • 当事件超出特定范围时触发事件
      • 更新保存的游戏状态
      • 通过网络传输更新
      • 挂钩其他可能需要知道该值何时被操纵的“观察者”

      如果没有封装,这些东西都不容易实现。

      protected 不仅仅是“离开我的草坪”,它是确保您的实现正确按预期使用的重要工具。 p>

      【讨论】: