公开一个不可变对象的状态可以吗?
最近遇到不可变对象的概念,我想知道控制访问状态的最佳实践。 尽pipe我的大脑面向对象的部分让我想要在公众面前畏惧恐惧,但是我没有看到像这样的技术问题:
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方法会感觉更加舒适,但是只有在显式读取状态时,这似乎过于复杂。
提供访问不可变对象状态的最佳实践是什么?
这完全取决于你将如何使用该对象。 公共领域本质上并不是邪恶的,把所有东西都默认为公开是不对的。 例如,java.awt.Point类公开了它的x和y字段,它们甚至不是最终的。 你的例子似乎是公用字段的一个很好的使用,但是你可能不想公开所有的另一个不可变对象的内部字段。 没有全面的规则。
我以前也曾经这么想过,但通常最终会使variables变为private,并使用getter和setter,以便以后我仍然可以在保持相同接口的同时对实现进行更改。
这让我想起了罗伯特·C·马丁(Robert C. Martin)最近在“Clean Code”中读到的东西。 在第六章中,他提出了一个稍微不同的观点。 例如,他在第95页说
“对象将数据隐藏在抽象的背后,暴露出对数据进行操作的function,数据结构暴露了他们的数据,没有任何有意义的function。
和在页100:
豆的准封装似乎使一些面向对象的纯粹主义者感觉更好,但通常没有其他的好处。
基于代码示例,Foo类似乎是一个数据结构。 所以根据我在Clean Code(这不仅仅是我给出的两个引用)中的讨论所理解的内容,这个类的目的是揭示数据而不是function,并且让获取者和引用者可能没有什么好处。
再次,根据我的经验,我通常会走在前面,并使用私有数据的“bean”方法与getter和setter。 但是再一次,没有人要求我写一本关于如何编写更好的代码的书,所以也许马丁有话要说。
如果您的对象具有足够的本地使用性,那么您将不再关心为其打开API更改的问题,因此不需要在实例variables之上添加getter。 但这是一个普遍的主题,不是针对不可变对象的。
使用getter的好处来自一个额外的间接层,如果你正在devise一个将被广泛使用的对象,它的效用将会延伸到不可预见的未来,这可能会派上用场。
不pipe不变性,你仍然暴露这个类的实现 。 在某个阶段,你会想要改变实现(或者可能产生各种派生,例如使用Point实例,你可能需要一个类似的使用极坐标的Point类),并且你的客户代码也暴露在这里。
上面的模式可能是有用的,但我通常会限制它到非常本地化的实例(例如传递信息的元组周围 – 我倾向于发现看似无关信息的对象,或者是封装不好,或者信息是相关的,我的元组转换成一个完整的对象)
需要牢记的是,函数调用提供了一个通用接口。 任何对象都可以使用函数调用与其他对象交互。 你所要做的就是定义正确的签名,然后离开你。 唯一的问题是,你只能通过这些函数调用进行交互,这些调用通常运行良好,但在某些情况下可能很笨重。
直接暴露状态variables的主要原因是能够直接在这些字段上使用原始操作符。 如果做得好,这可以增强可读性和便利性:例如,用+
添加复数,用[]
访问键控集合。 如果您使用的语法遵循传统的惯例,这样做的好处可能会令人惊讶。
问题是运营商不是一个通用的界面。 只有一组非常具体的内置types可以使用它们,这些只能用于语言预期的方式,而不能定义任何新的types。 所以,一旦你使用原语定义了你的公共接口,你已经把自己locking在使用这个原语,并且只有那个原语(和其他可以轻易地转换成它的东西)。 要使用其他任何东西,每次与它交互时,都必须在原始的基础上跳舞,并且从干燥的angular度杀死你:事情会非常迅速地变得非常脆弱。
一些语言使操作符成为通用接口,但Java不能。 这不是对Java的控诉:它的devise者故意不select运算符重载,他们有充足的理由这样做。 即使你在处理那些与操作符的传统意义非常吻合的对象时,使它们以一种真正有意义的方式工作也可能会令人惊讶地变得微妙,如果你没有完全明白它,那么你将会以后付钱。 使一个基于函数的界面比通过这个过程更容易阅读和使用,而且你通常甚至比使用操作符的结果更好。
然而,这个决定涉及到权衡。 有些时候,基于运算符的接口确实比基于函数的接口工作得更好,但是没有运算符重载,那个选项就不可用。 不pipe怎样,试图迫使操作员将会把你locking在一些devise上,你可能并不是真正想要的。 Javadevise者认为这种折衷是值得的,甚至可能是对的。 但是,这样的决定并不是没有影响的,这种情况就是影响力的来源。
总之,这个问题本身并没有暴露你的实现。 问题在于将自己locking在该实现中。
实际上,它打破封装以任何方式暴露对象的任何属性 – 每个属性是一个实现细节。 正因为大家都这样做并不正确。 使用访问器和增变器(getter和setter)不会使它更好。 相反,应使用CQRS模式来维护封装。
我知道只有一个道具有最终属性的获取者。 当你想通过接口访问属性的时候就是这种情况。
public interface Point { int getX(); int getY(); } public class Foo implements Point {...} public class Foo2 implements Point {...}
否则公共决赛场地是好的。
你所开发的这个class级在当前的化身上应该没问题。 当有人试图改变这个类或从中inheritance时,这些问题通常会发挥作用。
例如,看到上面的代码后,有人想到添加Bar类的另一个成员variables实例。
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的值。
最好的做法是把所有的领域都标记为私人领域,并为领域做好准备。 如果您要返回对象或集合,请确保返回不可修改的版本。
并发编程时,免疫能力至关重要。
具有从公共构造函数参数加载的公共final字段的对象将自己简单描述为一个简单的数据持有者。 虽然这样的数据持有者不是特别的“OOP-ish”,但它们对于允许单个字段,variables,参数或返回值封装多个值是有用的。 如果一个types的目的是作为将几个值粘合在一起的简单方法,那么这样的数据持有者通常是没有实际价值types的框架中的最佳表示。
考虑一下如果某个方法Foo
想要给调用者一个封装了“X = 5,Y = 23,Z = 57”的Point3d
,并且碰巧有一个Point3d
的引用, = 5,Y = 23,Z = 57。 如果Foo的东西被认为是一个简单的不可变的数据持有者,那么Foo应该简单地给调用者一个参考。 然而,如果它可能是别的东西(例如它可能包含X,Y和Z以外的附加信息 ),那么Foo应该创build一个包含“X = 5,Y = 23,Z = 57”的新的简单数据存储器,调用者提到了这一点。
让Point3d
被封闭并将其内容公开为最终字段将意味着像Foo这样的方法可能会认为它是一个简单的不可变的数据持有者,并且可以安全地共享对它的实例的引用。 如果存在进行这种假设的代码,那么将Point3d
更改为除了简单的不可变数据持有者之外的其他任何东西都可能是困难的或不可能的,而不会破坏这样的代码。 另一方面,假定Point3d
是一个简单的不可变数据持有者的代码可以比要处理其他事情的可能性的代码更简单和更高效。
您在Scala中看到了这种风格,但是这些语言之间存在着至关重要的区别:Scala遵循统一访问原则 ,但是Java并不遵循统一访问原则 。 这意味着只要你的课程没有改变,你的devise就没有问题,但是当你需要适应你的function时,它可能会以多种方式破坏:
- 你需要提取一个接口或超类(例如,你的类表示复数,你也想有一个极坐标表示的兄弟类)
- 您需要从您的类inheritance,并且信息变得多余(例如,可以从子类的附加数据计算
x
) - 你需要testing约束(例如,由于某种原因,
x
必须是非负的)
另外请注意,你不能使用这种风格的可变成员(如臭名昭着的java.util.Date
)。 只有获得者有机会做出防御性的副本,或者改变performanceforms(例如,将Date
信息存储为long
)
我使用了很多类似于这个问题的构造,有时候有些东西可以用一个(有时是不可改变的)数据结构来比一个类更好地build模。
所有这一切都取决于,如果你正在build模一个对象,它的行为定义了一个对象,在这种情况下,从不暴露内部属性。 其他时候,你正在build模一个数据结构,而且java没有特殊的数据结构构造,可以使用一个类并且公开所有的属性,并且如果你想要不变性,最终的和公开的。
例如,罗伯特·马丁在“清廉法典”(Clean Code)这本伟大的着作中有一章,在我看来,这是必读的。
如果唯一的目的是以一个有意义的名字将两个值相互耦合,那么您甚至可以考虑跳过定义任何构造函数并保持元素可更改:
public class Sculpture { public int weight = 0; public int price = 0; }
这样做的好处是可以最小化在实例化类时混淆参数顺序的风险。 如果需要,限制的可变性可以通过将整个容器置于private
控制之下来实现。
只是想反映一下反思 :
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
仍然是不可改变的? 🙂