最近遇到了不可变对象的概念,我想知道控制状态访问的最佳实践。即使我的大脑的面向对象部分让我想看到公众成员的恐惧,但我认为没有类似这样的技术问题:
public class Foo {
public final int x;
public final int y;
public Foo( int x, int y) {
this.x = x;
this.y = y;
}
}
我觉得将字段声明为private
并为每个字段提供getter方法会更舒服但是当状态明确只读时,这似乎过于复杂。
提供对不可变对象状态的访问的最佳实践是什么?
答案 0 :(得分:62)
这完全取决于你将如何使用这个对象。公共领域本质上并不是邪恶的,将一切都公之于众是不好的。例如,java.awt.Point类使其x和y字段成为公共字段,它们甚至不是最终字段。您的示例似乎很好地使用了公共字段,但是您可能不希望再公开另一个不可变对象的所有内部字段。没有包罗万象的规则。
答案 1 :(得分:31)
我在过去的想法一样,但通常最终会将变量设为私有并使用getter和setter,以便稍后我仍然可以选择在保持相同界面的同时对实现进行更改。
这确实让我想起了我最近在Robert C. Martin的“清洁代码”中读到的一些内容。在第6章中,他给出了一个略微不同的观点。例如,在第95页上,他说明了
“对象隐藏抽象背后的数据并公开对该数据进行操作的函数。数据结构暴露其数据并且没有有意义的函数。”
在第100页:
豆类的准封装似乎使一些OO纯粹主义者感觉更好,但通常没有其他好处。
基于代码示例,Foo类似乎是一个数据结构。因此,根据我在清洁代码(不仅仅是我给出的两个引号)中的讨论所理解的内容,该类的目的是公开数据,而不是功能,并且让getter和setter可能没有多大帮助。
同样,根据我的经验,我通常会继续使用私有数据的“bean”方法与getter和setter。但话说回来,没有人要我写一本关于如何编写更好的代码的书,所以也许Martin有话要说。
答案 2 :(得分:11)
如果您的对象具有本地足够的用途,以至于您不关心将来打破API更改的问题,则无需在实例变量之上添加getter。但这是一个通用主题,并非特定于不可变对象。
使用getter的优势来自于一个额外的间接层,如果您正在设计一个将被广泛使用的对象,并且其实用性将延伸到不可预见的未来,可能会派上用场。< / p>
答案 3 :(得分:6)
无论不变性如何,您仍然会暴露此类的实现。在某个阶段,您需要更改实现(或者可能产生各种派生,例如使用Point示例,您可能需要使用极坐标的类似Point类),并且您的客户端代码会暴露给此。
上述模式可能很有用,但我通常会将其限制为非常本地化的实例(例如,传递元组的信息 - 我倾向于发现看似无关的信息对象,但是,要么是错误的封装,要么信息 相关,我的元组转换为完全成熟的对象)
答案 4 :(得分:5)
要记住的重要一点是函数调用提供了一个通用接口。任何对象都可以使用函数调用与其他对象进行交互。您所要做的就是定义正确的签名,然后离开。唯一的问题是你必须通过这些函数调用进行交互,这些调用通常效果很好,但在某些情况下可能很笨拙。
直接公开状态变量的主要原因是能够直接在这些字段上使用原始运算符。如果做得好,这可以增强可读性和便利性:例如,添加复数+
,或使用[]
访问密钥集合。如果您使用语法遵循传统约定,那么这样做的好处可能会令人惊讶。
问题是运算符不是通用接口。只有一组非常特定的内置类型可以使用它们,这些只能以语言所需的方式使用,而你无法定义任何新的。因此,一旦使用基元定义了公共接口,就可以锁定自己使用该基元,只使用该基元(以及其他可以轻松转换为它的东西)。要使用其他任何东西,每次与它进行交互时都必须围绕该原语跳舞,并且从干燥的角度杀死你:事情会很快变得非常脆弱。
有些语言使运算符成为通用接口,但Java却没有。这不是对Java的控诉:它的设计者故意不选择包含运算符重载,他们有充分的理由去做所以。即使你正在处理那些似乎与传统操作符合适的对象,让它们以一种实际上有意义的方式工作也会令人惊讶地细致入微,如果你没有完全指出它,那么你将会以后付钱。使基于函数的界面可读和可用通常要比通过该过程容易得多,并且通常比使用运算符的结果更好。
然而 在该决定中涉及权衡。当基于操作员的界面确实比功能更好地工作时是次基于一个,但没有运算符重载,该选项不可用。无论如何都要试着挑战操作员,这会让你陷入一些你可能并不想真正想要的设计决定。 Java设计者认为这种权衡是值得的,他们甚至可能对此有所了解。但是这样的决定并非没有一些影响,而这种情况就是影响力下降的地方。
简而言之,问题不是暴露您的实施,本身。问题是将自己锁定在该实现中。
答案 5 :(得分:4)
实际上,它打破了封装以任何方式暴露对象的任何属性 - 每个属性都是一个实现细节。仅仅因为每个人都这样做并没有使它成为正确。使用访问器和更改器(getter和setter)并没有让它变得更好。相反,CQRS模式应该用于维护封装。
答案 6 :(得分:3)
我知道只有一个道具可以获得最终属性的吸气剂。当您希望通过界面访问属性时就是这种情况。
public interface Point {
int getX();
int getY();
}
public class Foo implements Point {...}
public class Foo2 implements Point {...}
否则公共最终字段就可以了。
答案 7 :(得分: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。如果要返回对象或集合,请确保返回不可修改的版本。
免疫能力对于并发编程至关重要。
答案 8 :(得分:2)
具有从公共构造函数参数加载的公共final字段的对象有效地将自身描述为一个简单的数据持有者。虽然这些数据持有者不是特别“OOP-ish”,但它们对于允许单个字段,变量,参数或返回值来封装多个值是有用的。如果一个类型的目的是作为将几个值粘合在一起的简单方法,那么这样的数据持有者通常是没有实际值类型的框架中的最佳表示。
如果某个方法Foo
想要给调用者一个Point3d
封装“X = 5,Y = 23,Z = 57”,并考虑以下问题,请考虑您希望发生什么?碰巧有一个Point3d
的引用,其中X = 5,Y = 23,Z = 57。如果已知Foo的东西是一个简单的不可变数据持有者,那么Foo应该只是给调用者一个引用它。但是,如果它可能是其他东西(例如它可能包含X,Y和Z 之外的其他信息),那么Foo应该创建一个新的简单数据持有者,其中包含“X = 5,Y = 23 ,Z = 57“并给呼叫者一个参考。
密封Point3d
并将其内容公开为公共最终字段将意味着像Foo这样的方法可能会认为它是一个简单的不可变数据持有者,并且可以安全地共享对它的实例的引用。如果存在进行此类假设的代码,则可能很难或不可能将Point3d
更改为除了简单的不可变数据持有者之外的其他任何内容而不破坏此类代码。另一方面,假设Point3d
是一个简单的不可变数据持有者的代码可以比代码处理其他东西的可能性更简单,更有效。
答案 9 :(得分:2)
你在Scala中看到了很多这种风格,但这些语言之间存在着重要的区别:Scala遵循Uniform Access Principle,但Java没有。这意味着只要您的课程没有改变,您的设计就会很好,但是当您需要调整您的功能时,它可能会以多种方式中断:
x
可以从子类的其他数据中计算出来)x
由于某种原因必须是非负的)另请注意,您不能将此样式用于可变成员(例如臭名昭着的java.util.Date
)。只有使用getter才有机会制作防御性副本或改变表示形式(例如将Date
信息存储为long
)
答案 10 :(得分:1)
我使用了许多与你在问题中提出的结构非常相似的结构,有时可以使用(有时是不可改变的)数据结构比使用类更好地建模。
如果您正在为对象建模,则所有依赖于由其行为定义的对象,在这种情况下永远不会公开内部属性。其他时候你正在建模数据结构,而java没有特殊的数据结构构造,使用类并公开所有属性,以及如果你想要不变性最终和公共偏离过程,这很好。
例如,罗伯特·马丁在伟大的清洁法典书中有一章关于这一点,我认为必须阅读。
答案 11 :(得分:1)
如果唯一用途是在一个有意义的名称下将两个值相互耦合,您甚至可以考虑跳过定义任何构造函数并保持元素可更改:
public class Sculpture {
public int weight = 0;
public int price = 0;
}
这有利于最小化在实例化类时混淆参数顺序的风险。如果需要,可以通过将整个容器置于private
控制下来实现受限制的可变性。
答案 12 :(得分:0)
只想反映reflection:
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
仍然是不可变的吗? :)