22

面向对象编程的最大优势之一是封装,我们(或者至少我已经)被教导的“真理”之一是成员应该始终保持私有并通过访问器和修改器提供方法,从而确保验证和验证更改的能力。

不过,我很好奇,这在实践中到底有多重要。特别是,如果您有一个更复杂的成员(例如集合),将其公开而不是制作一堆方法来获取集合的键,从集合中添加/删除项目可能非常诱人,等等

你一般遵守规则吗?您的答案是否会根据它是为自己编写的代码还是供他人使用的代码而改变?我是否因为这种混淆而缺少更微妙的原因?

4

22 回答 22

16

作为一个必须维护许多人过去工作多年的代码的人,我很清楚,如果一个成员属性被公开,它最终会被滥用。我什至听到有人不同意访问器和修改器的想法,因为这仍然没有真正达到封装的目的,即“隐藏类的内部工作”。这显然是一个有争议的话题,但我的意见是“将每个成员变量设为私有,主要考虑类必须做什么(方法),而不是如何让人们更改内部变量”。

于 2008-09-19T04:49:57.367 回答
16

这取决于。这是必须务实决定的问题之一。

假设我有一个代表一个点的类。我可以为 X 和 Y 坐标设置 getter 和 setter,或者我可以将它们都公开并允许对数据进行自由读/写访问。在我看来,这没关系,因为该类就像一个美化的结构 - 一个可能附加了一些有用功能的数据集合。

但是,在很多情况下,您不想提供对内部数据的完全访问权限,而是依赖类提供的方法与对象进行交互。一个例子是 HTTP 请求和响应。在这种情况下,允许任何人通过网络发送任何东西是个坏主意——它必须由类方法处理和格式化。在这种情况下,类被设想为一个实际的对象,而不是一个简单的数据存储。

这实际上归结为动词(方法)是否驱动结构或数据是否驱动。

于 2008-09-19T04:52:13.147 回答
8

是的,封装很重要。暴露底层实现(至少)有两件事是错误的:

  1. 混淆了责任。调用者不需要或不想了解底层实现。他们应该只希望班级做好自己的工作。通过暴露底层实现,你的类没有做它的工作。相反,它只是将责任推给调用者。
  2. 将您与底层实现联系起来。一旦你暴露了底层实现,你就会被它束缚住。如果你告诉调用者,例如,下面有一个集合,你不能轻易地将集合交换为一个新的实现。

无论您是直接访问底层实现还是只是复制所有底层方法,这些(和其他)问题都适用。您应该公开必要的实现,仅此而已。保持实现私有化使整个系统更易于维护。

于 2008-09-19T05:10:33.493 回答
3

我更喜欢尽可能长时间地保持成员的私密性,并且只能通过 getter 访问 em,即使在同一个类中也是如此。我也尽量避免将 setter 作为初稿来推广价值风格的对象,只要有可能。经常使用依赖注入,您通常有 setter 但没有 getter,因为客户端应该能够配置对象但(其他)不知道实际配置了什么,因为这是一个实现细节。

问候,奥利

于 2008-09-19T04:55:40.427 回答
2

我倾向于非常严格地遵守规则,即使它只是我自己的代码。出于这个原因,我真的很喜欢 C# 中的属性。它使控制给定的值变得非常容易,但您仍然可以将它们用作变量。或者将集合设为私有并公开,等等。

于 2008-09-19T04:51:26.410 回答
2

基本上,信息隐藏是关于代码清晰度的。它旨在让其他人更容易扩展您的代码,并防止他们在使用您的类的内部数据时意外创建错误。它基于没有人阅读评论的原则,尤其是那些带有说明的评论。

示例:我正在编写更新变量的代码,并且我需要绝对确保 Gui 更改以反映更改,最简单的方法是添加一个访问器方法(也称为“Setter”),它被调用而不是更新数据被更新。

如果我公开了该数据,并且在不通过 Setter 方法的情况下更改了变量(并且这种情况在每个脏话时间都会发生),那么有人将需要花一个小时调试来找出为什么不显示更新。在较小程度上,这同样适用于“获取”数据。我可以在头文件中添加注释,但很可能没有人会阅读它,直到出现非常非常错误的情况。使用 private 强制执行意味着不能犯错误,因为它会显示为易于定位的编译时错误,而不是运行时错误。

根据经验,您唯一希望公开成员变量并省略 Getter 和 Setter 方法的情况是,如果您想明确表示更改它不会产生副作用;特别是如果数据结构很简单,比如一个简单地将两个变量作为对保存的类。

这应该是一种相当罕见的情况,因为通常您会想要副作用,并且如果您创建的数据结构非常简单以至于您不需要(例如配对),那么已经有一个更有效的编写可用在标准库中。

话虽如此,对于大多数一次性无扩展的小程序,比如你在大学里得到的那些,它比任何东西都更“好实践”,因为你会在编写它们的过程中记住它们,然后你我会把它们交出来,再也不要碰代码。此外,如果您编写数据结构是为了了解它们如何存储数据而不是作为发布代码,那么有一个很好的论点是 Getters 和 Setters 无济于事,并且会妨碍学习体验.

只有当您到达工作场所或大型项目时,您的代码很可能会被不同人编写的对象和结构调用,使这些“提醒”变得强大变得至关重要。这是否是一个人的项目令人惊讶地无关紧要,原因很简单,“六周后的你”与同事一样不同。而“六周前的我”往往被证明是懒惰的。

最后一点是,有些人非常热衷于信息隐藏,如果您的数据不必要地公开,他们会感到恼火。最好取笑他们。

于 2008-09-19T07:46:07.927 回答
1

C# 属性“模拟”公共字段。看起来很酷,语法确实加快了创建这些 get/set 方法的速度

于 2008-09-19T04:55:43.263 回答
1

请记住在对象上调用方法的语义。方法调用是一种非常高级的抽象,可以在编译器或运行时系统中以各种不同的方式实现。

如果您正在调用的方法的对象存在于同一进程/内存映射中,则编译器或 VM 可以优化方法以直接访问数据成员。另一方面,如果对象位于分布式系统中的另一个节点上,那么您无法直接访问它的内部数据成员,但您仍然可以调用它的方法,向它发送消息。

通过对接口进行编码,您可以编写不关心目标对象存在于何处、如何调用它的方法或者即使它是用相同语言编写的代码。


In your example of an object that implements all the methods of a collection, then surely that object actually is a collection. so maybe this would be a case where inheritance would be better than encapsulation.

于 2009-02-10T18:38:43.567 回答
0

这一切都是为了控制人们可以用你给他们的东西做什么。你控制得越多,你能做出的假设就越多。

此外,从理论上讲,您可以更改底层实现或其他内容,但因为在大多数情况下它是:

private Foo foo;
public Foo getFoo() {}
public void setFoo(Foo foo) {}

这有点难以证明。

于 2008-09-19T04:47:54.873 回答
0

在实践中,我总是只遵循一条规则,即“不适合所有人”的规则。

封装及其重要性是您项目的产物。什么对象将访问您的界面,他们将如何使用它,如果他们对成员拥有不需要的访问权限,这是否重要?在进行每个项目实施时,您需要问自己这些问题以及类似的问题。

于 2008-09-19T04:48:39.083 回答
0

我的决定基于代码在模块中的深度。如果我正在编写模块内部的代码,并且不与外部世界交互,那么我不会用私有的东西封装东西,因为它会影响我的程序员性能(我可以多快地编写和重写我的代码)。

但是对于作为模块与用户代码接口的对象,我坚持严格的隐私模式。

于 2008-09-19T04:49:15.620 回答
0

当至少满足以下一项时,封装很重要:

  1. 除了您之外的任何人都将使用您的课程(否则他们会破坏您的不变量,因为他们不阅读文档)。
  2. 任何不阅读文档的人都会使用您的课程(否则他们会破坏您仔细记录的不变量)。请注意,此类别包括两年后的您。
  3. 在未来的某个时候,有人将从你的类继承(因为当字段的值发生变化时可能需要采取额外的操作,所以必须有一个 setter)。

如果它只适合我,并且在少数地方使用,并且我不会继承它,并且更改字段不会使类假定的任何不变量无效,那么我只会偶尔公开一个字段。

于 2008-09-19T04:53:44.500 回答
0

如果可能的话,我的倾向是尽量将所有内容保密。这使对象边界尽可能清晰地定义,并使对象尽可能地解耦。我喜欢这个,因为当我必须重写我第一次(第二次,第五次?)时搞砸的对象时,它会将损坏包含在较少数量的对象中。

如果您将对象耦合得足够紧密,那么将它们组合成一个对象可能会更直接。如果你足够放松耦合约束,你就会回到结构化编程。

如果您发现一堆对象只是访问器函数,您可能应该重新考虑您的对象划分。如果您没有对该数据执行任何操作,它可能属于另一个对象的一部分。

当然,如果您正在编写类似库之类的东西,您希望界面尽可能清晰锐利,以便其他人可以针对它进行编程。

于 2008-09-19T04:55:25.403 回答
0

使工具适合工作......最近我在我当前的代码库中看到了一些这样的代码:

private static class SomeSmallDataStructure {
    public int someField;
    public String someOtherField;
}

然后这个类在内部用于轻松传递多个数据值。这并不总是有意义的,但如果你只有 DATA,没有方法,并且你没有将它暴露给客户,我发现它是一个非常有用的模式。

我最近使用它是一个 JSP 页面,其中显示了一个数据表,在顶部以声明方式定义。所以,最初它是在多个数组中,每个数据字段一个数组......这最终导致代码很难通过定义中不相邻的字段将一起显示......所以我创建了一个简单的像上面这样的类,它将把它放在一起......结果是真正可读的代码,比以前更多。

道德...有时您应该考虑“接受的不良”替代方案,如果它们可以使代码更简单,更易于阅读,只要您仔细考虑并考虑后果...不要盲目地接受您听到的一切。

也就是说......公共获取器和设置器几乎等同于公共领域......至少在本质上(有一点灵活性,但它仍然是适用于您拥有的每个领域的糟糕模式)。

甚至 java 标准库也有一些公共领域的案例。

于 2008-09-19T04:57:49.780 回答
0

当我使对象变得有意义时,它们易于使用维护

例如:Person.Hand.Grab(howquick, howmuch);

诀窍是不要将成员视为简单的值,而是将其视为对象。

于 2008-09-19T05:33:04.533 回答
0

我认为这个问题确实将封装的概念与“信息隐藏”混淆了
(这不是批评,因为它似乎确实符合对“封装”概念的普遍解释)

但是对我来说,“封装”是:

  • 将多个项目重新组合到一个容器中的过程
  • 容器本身重新组合项目

假设您正在设计一个纳税人系统。对于每个纳税人,您可以将孩子的概念封装

  • 代表孩子的孩子名单
  • to 的地图考虑了来自不同父母的孩子
  • 一个对象 Children (not Child),它将提供所需的信息(例如孩子的总数)

这里有三种不同的封装,两种由低级容器(列表或地图)表示,一种由对象表示。

通过做出这些决定,您不会

  • 使该封装公开或受保护或私有:仍然需要选择“信息隐藏”
  • 做一个完整的抽象(你需要提炼对象Child的属性,你可能决定创建一个对象Child,它只保留从纳税人系统的角度来看的相关信息)
    抽象是选择哪些属性的过程的对象与您的系统相关,必须完全忽略。

所以我的观点是:
这个问题的标题可能是:实践中的私人与公共成员(信息隐藏
有多重要?)

不过,只是我的 2 美分。我完全尊重人们可以将封装视为一个包括“信息隐藏”决策的过程。

但是,我总是试图区分“抽象”-“封装”-“信息隐藏或可见性”。

于 2008-09-19T06:25:59.600 回答
0

@VonC

您可能会发现国际标准化组织的“开放分布式处理的参考模型”是一本有趣的读物。它定义:“封装:对象中包含的信息只能通过对象支持的接口上的交互来访问的属性。”

我试图证明信息隐藏是这个定义的关键部分: http ://www.edmundkirwan.com/encap/s2.html

问候,

埃德。

于 2008-10-31T11:05:54.613 回答
0

我发现很多 getter 和 setter 是一种代码味道,表明程序的结构设计得不好。您应该查看使用这些 getter 和 setter 的代码,并寻找真正应该成为该类一部分的功能。在大多数情况下,类的字段应该是私有的实现细节,并且只有该类的方法可以操作它们。

同时拥有 getter 和 setter 等于公开的字段(当 getter 和 setter 很简单/自动生成时)。有时将字段声明为 public 可能会更好,这样代码会更简单,除非您需要多态性或框架需要 get/set 方法(并且您不能更改框架)。

但在某些情况下,拥有 getter 和 setter 是一种很好的模式。一个例子:

当我创建应用程序的 GUI 时,我尝试将 GUI 的行为保留在一个类 (FooModel) 中,以便可以轻松地对其进行单元测试,并在另一个可以测试的类 (FooView) 中显示 GUI只能手动。视图和模型通过简单的胶水代码连接起来;当用户更改 field 的值时x,视图会调用setX(String)模型,这反过来可能会引发模型的其他部分已更改的事件,并且视图将使用 getter 从模型中获取更新的值。

在一个项目中,有一个具有 15 个 getter 和 setter 的 GUI 模型,其中只有 3 个 get 方法是微不足道的(这样 IDE 可以生成它们)。所有其他都包含一些功能或非平凡的表达式,例如:

public boolean isEmployeeStatusEnabled() {
    return pinCodeValidation.equals(PinCodeValidation.VALID);
}

public EmployeeStatus getEmployeeStatus() {
    Employee employee;
    if (isEmployeeStatusEnabled()
            && (employee = getSelectedEmployee()) != null) {
        return employee.getStatus();
    }
    return null;
}

public void setEmployeeStatus(EmployeeStatus status) {
    getSelectedEmployee().changeStatusTo(status, getPinCode());
    fireComponentStateChanged();
}
于 2009-02-09T23:44:03.847 回答
-1

当然,无论您编写内部代码还是供其他人(甚至您自己,但作为一个包含单元)使用的代码都会有所不同。任何要在外部使用的代码都应该有一个明确定义/记录在案的接口,您'会希望尽可能少地改变。

对于内部代码,根据难度,你可能会发现现在用简单的方法做事情的工作量更少,以后会付出一点点代价。当然,墨菲定律将确保短期收益将被多次抹去,因为您必须在以后需要更改您未能封装的类的内部结构时进行广泛的更改。

于 2008-09-19T04:50:40.327 回答
-1

特别是对于您使用将返回的集合的示例,这种集合的实现似乎可能会发生变化(与更简单的成员变量不同),从而使封装的效用更高。

话虽如此,我有点喜欢 Python 的处理方式。默认情况下,成员变量是公共的。如果您想隐藏它们或添加验证,则提供了一些技术,但这些被认为是特殊情况。

于 2008-09-19T04:56:38.753 回答
-1

我几乎一直都遵守这方面的规则。对我来说有四种情况 - 基本上,规则本身和几个例外(都受 Java 影响):

  1. 可由当前类之外的任何东西使用,通过 getter/setter 访问
  2. 内部到类的用法通常以“this”开头,以明确它不是方法参数
  3. 意味着要保持极小的东西,例如运输对象-基本上是属性的直接拍摄;所有公众
  4. 需要是非私有的才能进行某种扩展
于 2008-09-19T05:06:50.040 回答
-1

这里有一个实际问题,大多数现有答案都没有解决。封装和向外部代码公开干净、安全的接口总是很棒,但是当您编写的代码打算由空间和/或时间上很大的“用户”群使用时,它就更重要了。我的意思是,如果你计划让某人(甚至是你)在未来很好地维护代码,或者如果你正在编写一个模块来与来自多个其他开发人员的代码交互,那么你需要考虑更多比您编写一次性或完全由您编写的代码时要小心。

老实说,我知道这是多么糟糕的软件工程实践,但我通常会首先公开所有内容,这会使事情的记忆和输入速度稍微快一些,然后在有意义的时候添加封装。如今,大多数流行 IDE 中的重构工具使您使用的方法(添加封装与取消封装)的相关性大大降低。

于 2008-09-19T05:32:42.787 回答