可以公开不可变对象的状态吗



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

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字段,它们甚至不是最终字段。您的示例似乎很好地使用了公共字段,但您可能不想公开另一个不可变对象的所有内部字段。没有包罗万象的规则。

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

这确实让我想起了我最近在罗伯特·C·马丁的《清洁代码》中读到的一些东西。在第六章中,他给出了一个稍微不同的观点。例如,在第95页,他指出

"对象将其数据隐藏在抽象后面,并公开对该数据进行操作的函数。数据结构公开其数据,没有任何有意义的函数。">

在第100页:

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

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

同样,根据我的经验,我通常会使用getter和setter的私有数据的"bean"方法。但话说回来,从来没有人要求我写一本关于如何写更好的代码的书,所以Martin可能有话要说。

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

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

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

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

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

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

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

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

然而,该决策涉及权衡有时,基于运算符的接口确实比基于函数的接口工作得更好,但如果没有运算符重载,该选项就不可用。无论如何,试图强行说服运营商会把你锁定在一些设计决策中,而你可能真的不想被固定下来。Java设计人员认为这种权衡是值得的,他们甚至可能是正确的。但这样的决定并非没有后果,而这种情况正是后果的所在。

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

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

我只知道一个道具的最终属性需要getter。当您希望通过接口访问属性时,就会出现这种情况。

public interface Point {
int getX();
int getY();
}
public class Foo implements Point {...}
public class Foo2 implements Point {...}

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

您开发的类在其当前化身中应该很好。当有人试图改变这个类或从中继承时,这些问题通常会出现

例如,在看到上面的代码后,有人想添加类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。如果要返回对象或集合,请确保返回不可修改的版本。

不变性对于并发编程至关重要。

一个具有从公共构造函数参数加载的公共最终字段的对象有效地将自己描绘成一个简单的数据持有者。虽然这样的数据持有者不是特别"OOP式",但它们对于允许单个字段、变量、参数或返回值封装多个值非常有用。如果类型的目的是作为将几个值粘合在一起的简单方法,那么在没有实际值类型的框架中,这样的数据持有者通常是最好的表示。

考虑一下,如果某个方法Foo想要给调用方一个封装了"X=5,Y=23,Z=57"的Point3d,而它恰好引用了一个Point3d,其中X=5、Y=23和Z=57,您希望发生什么。如果Foo所拥有的东西是一个简单的不可变数据持有者,那么Foo应该简单地给调用者一个对它的引用。然而,如果它可能是其他东西(例如它可能包含X、Y和Z之外的附加信息),那么Foos应该创建一个包含"X=5,Y=23,Z=57"的新的简单数据持有者,并给调用者一一个对其的引用。

Point3d密封并将其内容公开为公共最终字段意味着像Foo这样的方法可能会假设它是一个简单的不可变数据持有者,并可以安全地共享对其实例的引用。如果存在做出这种假设的代码,那么在不破坏此类代码的情况下,将Point3d更改为简单的不可更改数据持有者之外的任何东西可能是困难的或不可能的。另一方面,假设Point3d是一个简单的不可变数据持有者的代码可能比必须处理它是其他东西的可能性的代码更简单、更高效。

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

  • 您需要提取一个接口或超类(例如,您的类表示复数,并且您还希望有一个具有极坐标表示的同级类)
  • 您需要从您的类继承,并且信息变得多余(例如,x可以根据子类的附加数据计算)
  • 您需要测试约束条件(例如,由于某种原因,x必须为非阴性)

还要注意,不能将这种样式用于可变成员(如臭名昭著的java.util.Date)。只有使用getter,您才有机会进行防御复制或更改表示(例如,将Date信息存储为long)

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

这一切都取决于,如果你正在建模一个对象,一个由其行为定义的对象,在这种情况下,永远不要公开内部属性。其他时候,您正在对数据结构进行建模,而java对数据结构没有特殊的构造,那么使用一个类并公开所有属性是很好的,如果您想要immutability final和public,当然也可以。

例如,罗伯特·马丁在伟大的《清洁代码》一书中有一章是关于这一点的,在我看来,这是一本必读的书。

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

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仍然是不可变的吗?:)

相关内容

  • 没有找到相关文章

最新更新