如何保证 Entity 的状态

DDD 的经典 Cargo 1.1 示例中,domain.experiment 包下的 ValueObject 接口定义了一个 copy() 方法。我认为并无用处。有的资料说防止外部修改。这个在 Java 下说不通。Java 没有指针的概念,所以不会有 C/C++ 下的指向指针的指针。也就是说,把 ValueObject 的引用暴露出去,客户代码无法修改这个引用指向的地址。又因为 ValueObject 是不变体,所以客户代码也无法修改引用指向的对象的值。所以暴露 ValueObject 的引用是安全的。

有的人会说 ValueObject 可能不一定是不变体,比如 Cargo 示例中的 Leg,它就包含对一个 Entity 的引用。我认为这个有改进的余地。如果 ValueObject 要包含一个 Entity,它应该包含这个 Entity 的拷贝。不然,ValueObject 从语义上说就不叫“值对象”了,它变得复杂了。而状态可变对象的滥用是万恶之源。

现在假设我们要对 Leg 中引用的 Entity 做一个拷贝,我们需要建一个 EntityDTO 类,这个类的每个域都跟 Entity 一样。然后我们逐个域地进行复制。如果这个 Entity 还包括其它 Entity,那么这个过程就递归下去。你能想象这有多么可怕吗?如果我们在 Entity 的接口中加入一个契约,让它支持拷贝。这样我们就可以避免这样递归拷贝情况的出现。不仅如此,因为每个 Entity 都对自己的拷贝过程负责,所以如果未来有改动导致拷贝方法也需要修改,这么这个修改只会局限在应该对此负责的 Entity 上,而不会散布到所有使用拷贝的地方。

我们有时候需要对 Entity 的 ID 以外的字段做拷贝。就好像两个人,有不同的 ID。我想拷贝那个特别优秀人物的所有特征,只不过,我还想做我自己。这就要求应该提供两个做 Entity 拷贝的接口:
1. copy(Identity id) // Identity 是该 Entity 所对应的 ID 类型
2. snapshot() // 相当于 copy(this.identity())

这样会带来一个问题:Entity 的实现变复杂了。粗看是这样,但我们要问一个问题:这个复杂性是谁带来的,谁应该对此负责?在看了 Clojure 的作者 Rich Hickey 在 [url=http://www.infoq.com/presentations/hickey-clojure]InfoQ 上的演讲[/url]后我发现,这是由于以前 Entity 没有对时间作管理(Time Management)。我们不能要求正在赛跑的运动员说,你们都定格一下,让我把这个瞬间留住。这种要求做快照的需求有时是非常正常,而且必须得到满足的。比如电子商务平台的交易快照。交易在变过状态后必须留有快照以便日后出现纠纷时有证可查。

也许你会说,不是所有 Entity 都是交易记录这样敏感的。没错,我们再看一个例子。我前段时间写过一个 Hosts 绑定工具,有一个 Entity:Hosts。它是 hosts 文件的逻辑内容的抽象。还有一个 aggregate 的内部 Entity:HostLine。它是对每一行的抽象(当然你可以说它可以做成 ValueObject,但我只是拿它来举例说明有多个 Entity 的 aggregate 所具有的问题)。Hosts#addHostsAsGroup(Hosts hosts) 这个方法会把参数 hosts 的内容跟自身这个实例的内容做合并。所以你很自然地会认为,hosts 自己跟自己做合并应该会产生出比原来的内容大一倍的 hosts。这正是我的测试用例的代码所做的。结果大出我的意料。在方法的开头,我校验了参数 hosts 的所有行状态。而在进行合并的过程当中这个校验/断言被打破了!一开始我以为是 JVM 的 bug,因为程序根本不是多线程的。后来才意识到是因为在合并的过程当中,行因为绑定内容重复而被注释掉,这样状态一变,后续的代码才会出现问题。在发现了 Entity#snapshot 的价值之后我重写了程序,这次在方法的开头我给参数做一个防御性拷贝,这回问题解决了。

也许你还会说这个例子还是具有特殊性。那我要说,你不知道什么时候这样需要会出现,可能因为技术上的问题(像上面的 hosts 的例子),可能因为业务需求的变化。我们需要对[b]时间[/b]做出管理!你真正实现的时候会发现这个过程并不会带来太多负担,只是相当于写一个 ValueObject 的 equals(Object) 方法的工作量。而且 Entity 接口的 sameIdentityAs(T), equals(Object), hashCode() 方法都可以通过抽象基类来做。我最后会给出代码。

所以我得出一个结论:这个复杂性是可变体固有的,而不是因为业务的临时需求硬加的。你可以说这加重了我们的负担(实际上只有一点),但请不要说解决这个问题不是我们的责任,我就当没看见,到时候再说。


/**
* An entity, as explained in the DDD book.
*
* @param <T> the target type which should implements this {@code Entity} interface
* @param <ID> the type of the identity field
*
* @author Eric Evans
* @author ydong
*/
public interface Entity<T extends Entity<T, ID>, ID> {

/**
* Returns the identity of this entity.
*/
ID identity();

/**
* Entities compare by identity, not by attributes.
*
* @param other The other entity.
* @return true if the identities are the same, regardles of other attributes.
*/
boolean sameIdentityAs(@Nullable T other);

/**
* Compares the identity equality between the given object and this instance.
* Two entities are equal if their identities are equal.
*
* @param obj the object whose identity's goinng to be compared
* @return true if they are equal, false otherwise
*/
@Override
boolean equals(@Nullable Object obj);

/**
* Returns the hash code of the identity.
*/
@Override
int hashCode();

/**
* A type-safe replacement for {@link Object#clone() clone()}. The copy
* returned by this function uses the given argument as its identity.
* Whether the returned copy is readonly or changable solely depends on
* implementations.
*
* @param identity which is used by the copy returned as identity
* @return a copy with the given argument as identity
*/
T copyAs(ID identity);

/**
* Same as {@link #copyAs(java.lang.Object) copyAs(Object)} except that
* the identity of the returned copy is the same with this instance.
*/
T snapshot();

}



/**
* A support class helps implementing {@link Entity} easier.
*
* @param <T> the target type which should implements this {@code Entity} interface
* @param <ID> the type of the identity field\
*
* @author ydong
*/
public abstract class EntitySupport<T extends Entity<T, ID>, ID>
implements Entity<T, ID> {

/**
* Returns the class object of the entity interface/class.
*/
protected abstract Class<T> entityInterface();

public boolean sameIdentityAs(@Nullable T other) {
return other != null && identity().equals(other.identity());
}

@Override
@SuppressWarnings("unchecked")
public boolean equals(@Nullable Object obj) {
return obj != null && entityInterface().isAssignableFrom(obj.getClass())
&& sameIdentityAs((T) obj);
}

@Override
public int hashCode() {
return identity().hashCode();
}

/**
* Returns the text representation of the identity field. More formally,
* it's returned like this: {@code return identity().toString();}.
*/
@Override
public String toString() {
return identity().toString();
}

public T snapshot() {
return copyAs(identity());
}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值