是否可以公开Immutable对象的状态?

Sti*_*ube 63 java immutability value-objects

最近遇到了不可变对象的概念,我想知道控制状态访问的最佳实践.即使我的大脑的面向对象部分让我想要在公众成员的视线中畏缩,但我认为没有类似这样的技术问题:

public class Foo {
    public final int x;
    public final int y;

    public Foo( int x, int y) {
        this.x = x;
        this.y = y;
    }
}
Run Code Online (Sandbox Code Playgroud)

我觉得更适合声明字段private并为每个字段提供getter方法,但是当状态明确只读时,这似乎过于复杂.

提供对不可变对象状态的访问的最佳实践是什么?

Kev*_*man 62

这完全取决于你将如何使用该对象.公共领域本质上并不是邪恶的,将一切都公之于众是不好的.例如,java.awt.Point类使其x和y字段成为公共字段,它们甚至不是最终字段.您的示例似乎很好地使用了公共字段,但是您可能不希望再公开另一个不可变对象的所有内部字段.没有包罗万象的规则.

  • @KevinWorkman在大多数情况下,所有开销的痕迹都将通过方法内联来删除. (15认同)
  • @Marko Topolnik:正如凯文所指出的那样,没有一个全能的规则.但我宁愿将接口+工厂视为"默认",而不是公共构造函数的公共类.我偶尔会想"哦,我应该在这里使用一个接口而不是一个具体的类" - 但我*从来没有想过"哦,我应该在这里使用一个具体的类而不是一个接口";-) (7认同)
  • 确实是@MarkoTopolnik,但我也认为这会导致选择除Java以外的语言.:) (7认同)
  • @ Marco13另一方面,我的座右铭是"每行代码必须证明其价值." 它似乎导致完全不同的默认值. (6认同)
  • @Zack:您无法重构使用代码的代码.当您发布代码时(甚至在公司内部),几乎不可能更改接口. (3认同)
  • 我认为可读性是主观的,但它确实有一点帮助,它减少了调用额外方法获得字段值所带来的轻微开销. (2认同)
  • `java.awt.Point`暴露其x/y的事实可能是一个误导性的例子.自Java 1.0以来,`Point`类已经存在,人们无法依赖聪明的方法内联和JIT - 但考虑到它在AWT中的使用,它是一个相当性能关键的类.今天,我无法想象将田野公之于众的真正理由.我宁愿质疑今天API中是否应该有public*classes*,它包含公共字段的问题...... (2认同)

Pau*_*thy 31

我在过去的想法一样,但通常最终将变量设为私有并使用getter和setter,以便稍后我仍然可以选择在保持相同的界面的同时对实现进行更改.

这让我想起了我最近在Robert C. Martin的"清洁代码"中读到的一些内容.在第6章中,他给出了一个略微不同的观点.例如,在第95页他说

"对象隐藏了抽象背后的数据,并公开了对该数据进行操作的函数.数据结构暴露了他们的数据,没有任何有意义的功能."

在第100页:

豆类的准封装似乎使一些OO纯粹主义者感觉更好但通常没有提供其他好处.

根据代码示例,Foo类似乎是一个数据结构.因此,根据我在清洁代码(不仅仅是我给出的两个引号)中的讨论所理解的内容,该类的目的是公开数据,而不是功能,并且让getter和setter可能没有多大帮助.

同样,根据我的经验,我通常会继续使用私有数据的"bean"方法与getter和setter.但话说回来,没有人要我写一本关于如何编写更好的代码的书,所以也许Martin有话要说.

  • getter是一种非常弱的封装形式,你暴露了类的所有属性,并且具有完全相同的类型.如果更糟糕的话,一个二传手.方法(类在OO理论中接收的消息)是命令对象完成某些工作,而不是请求信息.IMMO对吸气剂的使用非常少,而且我从来没有看到过吸气剂. (9认同)
  • @AlfredoCasado最令人惊奇的是,这个明显的事实很难向Java"最佳实践者"解释,这是一个非常普遍的品种.但请注意,通常,*是*getter/setter的用例,而不是纯数据对象.例如,配置对象可能涉及*set property*action背后的一些逻辑. (5认同)
  • Java中缺乏概念.Java类的许多用例与封装和抽象无关.其他语言提供了诸如向量,元组和散列之类的便利功能,并具有简洁的文字语法.它们完全透明并且暴露了所有数据 - 这是一个很好的东西. (4认同)
  • @Davor当然你可以做到这一点,但这不是一个"getter"这个名称以"get"开头的方法,不是同一个东西,可能是一个非常糟糕的方法名称. (2认同)

Mar*_*nik 11

如果您的对象具有本地足够的用途,以至于您不关心将来如何破坏API更改的问题,则无需在实例变量之上添加getter.但这是一个通用主题,不是特定于不可变对象.

使用getter的优势来自于一个额外的间接层,如果您正在设计一个将被广泛使用的对象,并且其实用性将延伸到不可预见的未来,这可能会派上用场.


Bri*_*new 6

无论不变性如何,您仍然会暴露此类的实现.在某个阶段,您需要更改实现(或者可能产生各种派生,例如使用Point示例,您可能需要使用极坐标的类似Point类),并且您的客户端代码会暴露给此.

上面的模式可能很有用,但我通常将它限制为非常本地化的实例(例如,传递信息元组 - 我倾向于发现看似无关的信息的对象,或者是不好的封装,或者信息相关的,我的元组转换成一个完全成熟的对象)


The*_*est 5

要记住的重要一点是函数调用提供了一个通用接口.任何对象都可以使用函数调用与其他对象交互.您所要做的就是定义正确的签名,然后离开.唯一的问题是你必须通过这些函数调用进行交互,这些调用通常很有效,但在某些情况下可能很笨拙.

直接暴露状态变量的主要原因是能够直接在这些字段上使用原始运算符.如果做得好,这可以增强可读性和便利性:例如,添加复数+,或访问键控集合[].如果您使用语法遵循传统约定,那么这样做的好处可能会令人惊讶.

问题在于运营商不是通用接口.只有一组非常特定的内置类型才能使用它们,这些只能以语言所需的方式使用,而且您无法定义任何新的类型.因此,一旦使用基元定义了公共接口,就可以锁定自己使用该基元,只使用该原语(以及其他可以轻松转换为基础的东西).要使用其他任何东西,每次与它进行交互时都必须围绕该原语跳舞,并且从干燥的角度杀死你:事情会很快变得非常脆弱.

有些语言使运算符成为通用接口,但Java则不然.这不是对Java的起诉:它的设计者故意不选择包含运算符重载,他们有充分的理由这样做.即使你正在处理那些似乎与传统操作符合适的对象,让它们以一种实际上有意义的方式工作也会有惊人的细微差别,如果你没有完全指出它,那么你将会以后付钱.使基于函数的界面可读和可用通常要比通过该过程容易得多,并且您通常比使用运算符时获得更好的结果.

还有参与这一决定权衡,但是.时候操作员为基础的界面确实工作比基于函数的一个更好的,但没有操作符重载,该选项只是不可用.无论如何都要试着挑战操作员,这会让你陷入一些你可能并不想真正想要的设计决定.Java设计者认为这种权衡是值得的,他们甚至可能对此有所了解.但是这样的决定并非没有一些影响,而这种情况就是影响力下降的地方.

简而言之,问题不在于暴露您的实现本身.问题是将自己锁定在该实现中.