实践中的私有成员和公共成员(封装有多重要?)

22

面向对象编程的最大优点之一是封装,而我们被教导的“真理”之一是成员变量应该始终设为私有,并通过访问器和修改器方法来使用,从而确保能够验证和验证更改。

然而,我很好奇这在实践中是否真的很重要。特别是,如果您有一个更复杂的成员变量(例如集合),那么只公开它而不是创建一堆获取集合键、添加/删除集合项等方法可能会非常诱人。

您通常遵循这个规则吗?您的答案是否取决于代码是为自己编写还是为他人使用?除了混淆之外,是否还存在其他更微妙的原因?


可能是为什么要使用getter和setter?的重复问题。 - nawfal
请注意,即使您的访问器向调用者公开集合,仍然大多数使用访问器的参数仍然有效。如果您担心封装性,请在将其传递给调用者之前或覆盖旧集合值之前克隆集合。 - jpaugh
22个回答

16

作为一个需要维护多年前其他人编写的代码的人来说,很清楚地认识到如果将成员属性声明为public,最终它会被滥用。我甚至听说过有人不同意使用访问器和修改器,因为这仍然不能真正实现封装的目的,即“隐藏类的内部工作方式”。这显然是个有争议的话题,但我的观点是,“将每个成员变量都声明为private,主要考虑类需要做什么(方法),而不是如何让人们更改内部变量。”


16

这要看情况。这是一个必须实事求是决定的问题。

假设我有一个用于表示点的类。我可以为X和Y坐标设置getter和setter,或者我可以将它们都设置为public,并允许自由读写数据。在我看来,这是可以接受的,因为这个类就像一个增强版的结构体——一个数据集合,可能附带一些有用的功能。

然而,在许多情况下,您不希望提供对内部数据的完全访问权限,并依靠类提供的方法与对象进行交互。一个例子是HTTP请求和响应。在这种情况下,允许任何人通过网络发送任何内容是不明智的,必须通过类方法进行处理和格式化。在这种情况下,该类被构思为一个真正的对象,而不是一个简单的数据存储。

这实际上取决于动词(方法)是否驱动结构,还是数据驱动结构。


我只是想说,我觉得你最后提到的一点(方法或数据驱动结构)特别有见地。谢谢! - Asmor
如果您以后想要增强您的类,使其在X或Y更改时执行某些操作,使用getter和setter会更加容易。 - Trent
1
这就是区别所在 - 如果我存储一个点,我不希望它做任何事情,只是存储信息而已。这完全取决于类将用于什么目的。 - Kyle Cronin
1
如果您决定X和Y的最小和最大合法值是多少呢?那么您可能想在X和Y的不存在的getter和setter中执行它。 - MarkJ
@Trent,“如果你以后想要”……在我看来,这是一个危险的说法 =P。现在越来越多的时候,我开始质疑自己写了多少代码去考虑那些从未发生过的事情。(我知道这是一个非常老的问题/评论,但我忍不住!) - Sprague
显示剩余2条评论

8
是的,封装很重要。暴露底层实现会出现(至少)两个问题:
  1. 混淆职责。调用者不应该需要或想要理解底层实现。他们只想让类完成它的工作。通过暴露底层实现,你的类没有完成它的工作。相反,它只是将责任推给了调用者。
  2. 将你绑定到底层实现。一旦您公开底层实现,您就与之绑定。如果您告诉调用者,例如,下面有一个集合,您无法轻松地将集合替换为新的实现。

这些(和其他)问题适用于您是否直接访问底层实现或仅复制所有底层方法。您应该公开必要的实现,而不多余。将实现保持私有使整个系统更易维护。


3

我倾向于尽可能将成员保持私有,并仅通过getter方法访问,即使在同一类中也是如此。我还尝试避免使用setter方法作为第一草稿,尽可能促进值类型对象的使用。在大量使用依赖注入时,您经常会拥有setter方法但没有getter方法,因为客户端应该能够配置对象,但(其他人)不需要知道实际配置情况,因为这是一个实现细节。

敬礼, Ollie


2

我倾向于严格遵循规则,即使只是我的代码。我真的很喜欢C#中的属性,因为这个原因。它使得控制赋值变得非常容易,但你仍然可以像使用变量一样使用它们。或者将set设置为私有,get设置为公有等。


我也喜欢属性。可惜C++没有它们。 - Thomas

2
基本上,信息隐藏是关于代码的清晰度。它旨在使其他人更容易扩展您的代码,并防止他们在使用您类的内部数据时意外创建错误。这基于一个原则,即没有人会读注释,特别是带有指令的注释。
例如:我正在编写更新变量的代码,我需要确保Gui(图形用户界面)反映出变化,最简单的方法是添加一个访问器方法(称为“Setter”),而不是直接更新数据。
如果我将该数据公开,并且某些内容更改了变量而没有通过Setter方法进行更改(这经常发生),那么有人将需要花费一个小时进行调试,以找出为什么更新没有显示。同样适用于“获取”数据,少量适用于“获取”数据。我可以将注释放在头文件中,但很可能在出现问题之前没有人会读它。通过private强制执行,这意味着错误无法发生,因为它将显示为易于定位的编译时错误,而不是运行时错误。
根据经验,您唯一想要使成员变量公开并省略Getter和Setter方法的时候是,如果您想要绝对清楚地表明更改将不产生任何副作用;特别是如果数据结构很简单,比如只是简单地将两个变量作为一对保留的类。
这应该是一个相当罕见的情况,因为通常您会想有副作用,如果您正在创建的数据结构如此简单以至于您不需要它(例如配对),则已经在标准库中提供了更高效的写法。
话虽如此,对于大多数仅使用一次且没有扩展的小型程序,例如您在大学获得的程序,在整个编写过程中您都会记住这一点,然后您会提交它们并再也不接触代码。此外,如果您编写数据结构是为了了解它们存储数据的方式,而不是释放代码,则有一个很好的论点认为Getter和Setter将无济于事,并妨碍学习体验。
只有当您进入工作场所或进行大型项目时,您编写的代码将被来自不同人编写的对象和结构调用(可能性很大),才变得重要起来,使这些“提示”变得强有力。是否是一个人的项目是出奇的无关紧要,简单的原因是,“从现在开始的六周内的你”与同事一样不同。而“我六周前”通常会变得懒惰。
最后一点是,有些人非常热衷于信息隐藏,如果您的数据没有必要公开,他们会感到恼怒。最好使他们开心。

1

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


1

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

如果您要调用的对象的方法存在于同一进程/内存映射中,则编译器或虚拟机可能会对其进行优化,直接访问数据成员。另一方面,如果对象存在于分布式系统中的另一个节点上,则无法直接访问其内部数据成员,但仍可以通过发送消息来调用其方法。

通过编写基于接口的代码,您可以编写不关心目标对象存在位置、如何调用其方法甚至是否使用相同语言编写的代码。


在你提到实现集合所有方法的对象的例子中,那么这个对象实际上就是一个集合。因此,也许这种情况下继承比封装更好。

0

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

同时拥有getter和setter等同于该字段是公共的(当getter和setter是微不足道/自动生成时)。有时候,更好的做法可能是将字段声明为public,这样代码会更简单,除非你需要多态性或框架需要get/set方法(而且你无法更改框架)。

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

当我创建应用程序的GUI时,我尝试将GUI的行为保持在一个类(FooModel)中,以便可以轻松进行单元测试,并将GUI的可视化放在另一个类(FooView)中,只能手动测试。视图和模型通过简单的粘合代码连接;当用户更改字段x的值时,视图调用模型上的setX(String),模型可能会引发某些其他部分已更改的事件,视图将使用getter从模型获取更新的值。
在一个项目中,有一个GUI模型,其中有15个getter和setter,其中仅有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();
}

0

这一切都关乎于控制人们使用你所提供的内容的能力。你越是有控制权,就能够做出更多的假设。

此外,理论上你可以改变底层实现或其他什么东西,但由于大部分情况下它是:

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

这有点难以证明。


2
哈哈,我懂你的感受!这就是为什么在C#中引入了Public Foo Foo { get; set; }的概念,这样你甚至不需要创建一个私有成员变量。 - Jon Limjap
一个显而易见的理由是,setFoo() 可以对 foo 进行检查,看它是否符合允许范围,例如。此外(当没有 setFoo 时),你可以有一个类的私有成员 "FooImpl extends Foo",并将 getFoo() 返回为 Foo,从而允许你随意更改实现。 - quant_dev
@quant_dev 可以,但实际上几乎从不这样做。我认为我从未见过在bean中进行验证,它总是在先前的某个时间完成(例如,如果我们谈论web,则是Web框架生命周期的一部分)。领域对象始终更灵活,没有逻辑。 - SCdF
@Jon,Java中真的需要属性。 - Alex Baranosky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接