2

我标记了一个不可变数据模型类,final以确保更改其值的唯一方法是创建一个新实例。(不幸的是,这些字段不能是最终的,因为它们需要由 Hibernate 填充。)

这工作得很好,直到我想检查另一个类在使用无效的模型实例调用时抛出正确的异常。模型的构造函数验证参数,因此必须使用反射来设置字段。这是非常笨拙的,因为模型有很多字段并且字段名称必须是硬编码的。

由于它是最终模型,我也无法模拟该模型。(是否应该使用接口来启用模拟同时保持类不可变是否也值得商榷。通过使用接口,没有办法以编程方式强制方法必须在实例的整个生命周期中返回相同的值。)

在这种情况下,人们通常会怎么做?有什么标准的方法吗?

4

2 回答 2

3

一般来说,您不应该模拟数据对象。数据对象应该没有逻辑,也没有外部依赖,所以模拟对象并没有多大用处。相反,可以很容易地创建假实例,您可以根据需要在方法中填充这些实例。

此外,您可能希望避免将 Hibernate 持久对象视为不可变的其他一些原因:

  • Hibernate 提供的对象本质上不是线程安全的,因此失去了不可变值对象通常提供的线程安全优势。
  • 您可能会发现您的对象实际上是代理,可能会削弱final语义。
  • Hibernate 控制的对象的操作完全不同,无论它们的会话是否仍然打开(附加与分离),这使得它们对于不可变对象来说是一个非常糟糕的选择。如果您的不可变对象依赖于会话生命周期,那么它并不是真正不可变的。
  • 听起来有些对象可能在应用程序层有效或无效,超出了数据库层验证。这使得封装您的验证问题变得更加困难。
  • 您需要有一个公共的无参数构造函数,这与不可变值对象的典型实例控制类型是对立的。
  • 因为对象本质上是可变的,所以覆盖 equals 和 hashCode更加复杂。

我的建议?如果您需要比 Hibernate DAO 提供的更多的不变性和数据验证保证,那么创建一个具有最终字段(或私有构造函数和静态工厂方法)的真正的最终不可变类,然后创建一个构造函数(或静态工厂方法)从您的 Hibernate DAO 复制值。

如果您决定使用此选项,您将面临两个大致并行更改的数据对象的开销,但您还可以获得分离关注点的好处(以防 Hibernate 对象应该发散)和真正不可变的易用性, equals-and-hashcode-overriding、session-agnostic、保证有效的对象,您可以轻松地为测试创建。

于 2012-11-05T02:30:55.197 回答
0

为清楚起见,将类设为 final 可以防止它被子类化。这在类不需要进一步细化的情况下很好。

将类级别变量标记为 final 意味着它只会被分配一次。对于像 String 这样的原语和不可变对象,这具有使变量不可变(默认情况下)的副作用。

但是,对于像 Date 这样的可变对象,您的变量将始终引用同一个实例,但其他有权访问该实例的人仍然能够更改它的状态。例如,如果你有一个方法

public Date getCreatedDate(){
     return this.created; // class variable declared as private final Date created...;
}

然后任何调用者都可以访问创建的实例并更改它的状态。您最好只返回真正不可变的值,或者返回一个克隆。

public Date getCreatedDate(){
     return this.created.clone();
}

编辑

“我将不可变数据模型类标记为 final,以确保更改其值的唯一方法是创建一个新实例”

据我了解,您的问题是 A 类依赖于 B 类。您希望测试 A 类并且您无法模拟 B 类,因为您已将其标记为最终。您将 Class B 标记为 final 以使其不可变(防止其内部状态被更改)。这是不正确的,因为将类标记为 final 会阻止它被子类化。它与更改实例内部状态的能力无关。

您对 final 的使用没有达到预期的效果。将字段标记为 final 不是一种选择,并且由于上述原因不会使类不可变。保护数据的唯一方法是防止数据的客户端访问构成其内部状态的对象。

假设您不是唯一的开发人员,您需要保护数据的用户免受意外更新。确保从 getter 返回克隆是一种方法。让团队成员子类和更改数据只是糟糕的编程,不是无意的,并且可以通过策略和代码审查来管理。

如果您希望保护您的代码免受未知开发人员的外部干扰(例如编写使用相同命名空间来注入其代码的代码),则可以使用其他方法,例如包密封。

于 2012-11-05T02:34:16.060 回答