毎日、ドメイン駆動設計というか、設計の話が投稿されると、楽しくなりますね。
さて、今日の話題は、以下です!
エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か
”稀によくあるサンプル”。多分これ僕が書いた事例ですね。ということで、なぜそうしたか、理由など書いておきたいと思います。
なぜこうしたか
equals
の責務は以下のとおりで、オブジェクトが等しいかどうかを示すものです。エンティティの等価判定の基準に、識別子以外の属性は含めていません。 これにはトレードオフがあります。
https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Object.html Object#equalsの責務は、「このオブジェクトと他のオブジェクトが等しいかどうかを示します。」
以下のように実装したと場合に、(ID以外の)属性が変わったエンティティを特定することが可能です。
scala> case class EmployeeId(value: Int) extends Identifier defined class EmployeeId scala> case class Employee(identifier: EmployeeId, name: String) extends Entity[EmployeeId] defined class Employee scala> val list = Seq(Employee(EmployeeId(1), "yamada taro"), Employee(EmployeeId(2), "yamada hanako")) list: Seq[Employee] = List(Employee(EmployeeId(1),yamada taro), Employee(EmployeeId(2),yamada hanako)) // "yamada hanako"が結婚して名前が変わるイベントが発生、リストの内容も合わせて変更される scala> list.contains(Employee(EmployeeId(2), "yamada hanako")) res0: Boolean = true
Entity#sameIdentityAs
の場合は、 コレクションのメソッドに組み込むことはできないのでそれなりに実装を工夫する必要がありますね。
論理的等価性検査としてのequals
上記のメリットはよいとして、一体equals
メソッドとして何を求めるているのか。DDDの観点があろうがなかろうか、equals
の契約を逸脱する設計をしないようにすべきと考えます。久しぶりに、Effective Javaを開いてみると、equalsは「論理的等価性」検査の責務を持つというような記述はあるものの、「論理的等価性」が具体的に何かは明記されていませんでした。
ちなみにjava.lang.Object
のデフォルト実装は以下となっています。多くの場合はサブクラスで固有のequalsメソッドとしてオーバーライドされますが、されない場合もあります。java.util.Random
は、同じ乱数列を生成するかで等価判定できたはずですが、クライアントがそれを求めてなかったので、デフォルト実装のままとなっているようです。
public boolean equals(Object obj) { return (this == obj); }
契約プログラミングの観点から
契約プログラミングの観点では、java.lan.Object#equals
のJavadocに書かれていること以上のことは求めてはいけません。つまり、一般契約に従っている以上は「オブジェクトが等しいかどうか」の仕様を満たしているはずです。
DDDの文脈では、等価性は(エンティティが持つ)値がすべて同じであればtrueとみなし、同一性は識別子が同じであればtrueとみなすものと解釈できそうです。つまり、エンティティの定義である「同一性によって定義されるオブジェクト」に照らすと equals をオーバーライドすることは適切ではありません。 この点では、本来オーバーライドすべきは eq と言えるかもしれません。
このアイデアは有益ですしこの設計を取ってもよいと思っていますが、java.lan.Object#equals
の契約に照らして考えるに「適切ではありません」とまでは言い切れないと考えています。
結局どうすべきか
まぁ身の蓋もないですが、こうすべきだと思っています。(ここでオーバーライドするという意味は、IDのみの等価判断する実装としてオーバーライドするかしないかという意味です)
まぁ、オーバーライドするかしないかは、トレードオフがあるので、プロジェクトやチームで判断してくださいというスタンスかな、僕は。
— かとじゅん (@j5ik2o) 2018年12月5日
また、equals
やhashCode
のようにもともと抽象度の高いインターフェイスは意図がわかりにくいことがあります。対策としては、ドキュメンテーションコメントに自然言語できちんと設計の意図を記述するべきで、利用者も求められる契約が何かを理解すべきだと思っています。(自壊の念を込めて)
実装を追わなければ equals がオーバーライドされていることが分からないことは、余分な意識コストとなってしまうでしょう。
DbCの観点でいえば、equals, hashCodeはDDDでいう意図の明白なインターフェイスではありませんね。コードだけでは設計の意図がわからない場合は、javadocなりscaladocに自然言語で仕様を記述すべきですね。
— かとじゅん (@j5ik2o) 2018年12月5日