64

最近遇到了不可变对象的概念,我想知道控制对状态的访问的最佳实践。尽管我大脑中面向对象的部分让我想在看到公众成员时畏缩不前,但我认为这样的事情没有技术问题:

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

    public Foo( int x, int y) {
        this.x = x;
        this.y = y;
    }
}

将字段声明为并为每个字段提供 getter 方法我会感觉更舒服private,但是当状态显式只读时,这似乎过于复杂。

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

4

13 回答 13

62

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

于 2014-02-20T14:33:36.360 回答
31

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

这确实让我想起了我最近在 Robert C. Martin 的“清洁代码”中读到的一些内容。在第 6 章中,他给出了稍微不同的观点。例如,在第 95 页,他指出

“对象将它们的数据隐藏在抽象之后,并公开对这些数据进行操作的函数。数据结构公开它们的数据并且没有有意义的函数。”

在第 100 页:

bean 的准封装似乎让一些 OO 纯粹主义者感觉更好,但通常没有其他好处。

根据代码示例,Foo 类似乎是一种数据结构。因此,根据我从 Clean Code 中的讨论中了解到的内容(不仅仅是我给出的两个引号),该类的目的是公开数据,而不是功能,并且拥有 getter 和 setter 可能没有多大用处。

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

于 2014-02-20T14:56:32.850 回答
11

如果您的对象足够本地使用,您不关心将来破坏 API 更改的问题,则无需在实例变量之上添加 getter。但这是一个普遍的主题,并不特定于不可变的对象。

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

于 2014-02-20T14:34:53.473 回答
6

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

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

于 2014-02-20T14:36:20.883 回答
5

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

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

问题是运算符不是通用接口。只有一组非常特定的内置类型可以使用它们,它们只能以语言期望的方式使用,并且您不能定义任何新类型。因此,一旦您使用原语定义了公共接口,您就将自己锁定在使用该原语,并且只使用该原语(以及其他可以轻松转换为它的东西)。要使用其他任何东西,每次与它交互时,您都必须围绕该原始元素跳舞,从 DRY 的角度来看,这会杀死您:事物很快就会变得非常脆弱。

一些语言使运算符成为一个通用接口,但 Java 没有。这不是对 Java 的控诉:它的设计者故意选择不包括运算符重载,而且他们有充分的理由这样做。即使您使用的对象似乎很符合运算符的传统含义,但让它们以一种真正有意义的方式工作可能会令人惊讶地细微差别,如果您不能完全确定它,您将稍后支付。使基于函数的界面变得可读和可用通常比完成该过程要容易得多,而且您通常会得到比使用运算符更好的结果。

然而,该决定涉及权衡取舍。有时基于运算符接口确实比基于函数的接口工作得更好,但如果没有运算符重载,则该选项不可用。无论如何,试图硬塞操作员会让你陷入一些你可能并不真正希望一成不变的设计决策。Java 设计者认为这种折衷是值得的,他们甚至可能对此是正确的。但是像这样的决定并非没有后果,而这种情况就是后果发生的地方。

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

于 2014-02-20T18:17:43.853 回答
4

实际上,它破坏了以任何方式公开对象的任何属性的封装——每个属性都是一个实现细节。仅仅因为每个人都这样做并不能使它正确。使用访问器和修改器(getter 和 setter)并没有让它变得更好。相反,应该使用 CQRS 模式来维护封装。

于 2014-02-20T21:11:11.703 回答
3

您开发的课程在当前版本中应该没问题。当有人试图改变这个类或从它继承时,这些问题通常会发挥作用。

比如,看到上面的代码,有人想再增加一个类Bar的成员变量实例。

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

    public Foo( int x, int y, Bar z) {
        this.x = x;
        this.y = y;
    }
}

public class Bar {
    public int age; //Oops this is not final, may be a mistake but still
    public Bar(int age) {
        this.age = age;
    }
}

在上面的代码中,Bar 的实例无法更改,但在外部,任何人都可以更新 Bar.age 的值。

最佳做法是将所有字段标记为私有,并为这些字段设置 getter。如果要返回对象或集合,请确保返回不可修改的版本。

免疫性对于并发编程至关重要。

于 2014-02-26T01:29:00.410 回答
3

我知道只有一个道具拥有最终属性的吸气剂。当您希望通过接口访问属性时就是这种情况。

    public interface Point {
       int getX();
       int getY();
    }

    public class Foo implements Point {...}
    public class Foo2 implements Point {...}

否则,公共最终字段是可以的。

于 2014-02-20T14:43:30.690 回答
2

您在 Scala 中经常看到这种风格,但这些语言之间存在一个至关重要的区别:Scala 遵循统一访问原则,而 Java 没有。这意味着只要你的类没有改变,你的设计就很好,但是当你需要调整你的功能时,它可能会以多种方式中断:

  • 您需要提取一个接口或超类(例如,您的类表示复数,并且您也希望有一个具有极坐标表示的兄弟类)
  • 你需要从你的类继承,信息变得多余(例如x可以从子类的附加数据中计算出来)
  • 您需要测试约束(例如x,由于某种原因必须是非负数)

另请注意,您不能将这种样式用于可变成员(例如臭名昭​​著的java.util.Date)。只有使用吸气剂,您才有机会制作防御性副本或更改表示形式(例如将Date信息存储为long

于 2014-02-21T22:03:06.023 回答
2

An object with public final fields that get loaded from public constructor parameters effectively portrays itself as being a simple data holder. While such data holders aren't particularly "OOP-ish", they are useful for allowing a single field, variable, parameter, or return value to encapsulate multiple values. If the purpose of a type is to serve as a simple means of gluing a few values together, such a data holder is often the best representation in a framework without real value types.

Consider the question of what you would like to have happen if some method Foo wants to give a caller a Point3d which encapsulates "X=5, Y=23, Z=57", and it happens to have a reference to a Point3d where X=5, Y=23, and Z=57. If the thing Foo has is known to be a simple immutable data holder, then Foo should simply give the caller a reference to it. If, however, it might be something else (e.g. it might contain additional information beyond X, Y, and Z), then Foo should create a new simple data holder containing "X=5, Y=23, Z=57" and give the caller a reference to that.

Having Point3d be sealed and expose its contents as public final fields will imply that methods like Foo may assume it's a simple immutable data holder and may safely share references to instances of it. If code exists that make such assumptions, it may be difficult or impossible to change Point3d to be anything other than a simple immutable data holder without breaking such code. On the other hand, code which assumes Point3d is a simple immutable data holder can be much simpler and more efficient than code which has to deal with the possibility of it being something else.

于 2014-02-21T18:30:39.017 回答
1

我使用了很多与您在问题中提出的非常相似的结构,有时有些东西可以用(有时是不可变的)数据结构比用类更好地建模。

一切都取决于,如果你正在建模一个对象,一个由它的行为定义的对象,在这种情况下永远不要暴露内部属性。其他时候,您正在对数据结构进行建模,而 java 没有针对数据结构的特殊构造,可以使用类并公开所有属性,并且如果您希望不可变,则可以使用 final 和 public。

例如,罗伯特·马丁 (robert martin) 在伟大的《清洁代码》一书中有一章关于这一点,我认为这是必读的。

于 2014-02-20T14:38:28.783 回答
1

如果唯一的目的是在一个有意义的名称下将两个值相互耦合,您甚至可以考虑跳过定义任何构造函数并保持元素可变:

public class Sculpture {
    public int weight = 0;
    public int price = 0;
}

这样做的好处是,可以将在实例化类时混淆参数顺序的风险降到最低。如果需要,可以通过private控制整个容器来实现受限的可变性。

于 2014-02-21T08:35:18.680 回答
0

只想反映反射

Foo foo = new Foo(0, 1);  // x=0, y=1
Field fieldX = Foo.class.getField("x");
fieldX.setAccessible(true);
fieldX.set(foo, 5);
System.out.println(foo.x);   // 5!

那么,Foo仍然是不可变的吗?:)

于 2014-02-20T14:45:58.893 回答