yoskhdia’s diary

DDDとかプログラミングとかアーキテクチャとか雑多に

エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か

このエントリは ドメイン駆動設計 #1 Advent Calendar 2018 の5日目です。
4日目は @s_edward さんの「Microservices と DDD」でした。
6日目は @kawakawa さんの ドメインオブジェクトとユースケースの関係について です。

TL;DR

エンティティの同一性を表現するためにequalsをオーバーライドすべき ではない と考えています。

稀によくあるサンプル

次のような実装を目にすることがあります。以降のコードは Scala 2.12.7 で動作確認しています。

※私が書きやすいのでScalaを用いていますが、他言語においても同様のことは言えるかと思います。

trait Identifier

trait Entity[ID <: Identifier] {
  def identifier: ID

  def canEqual(that: Any): Boolean = this.getClass == that.getClass

  override final def equals(that: Any): Boolean = {
    that match {
      case that: Entity[_] => canEqual(that) && identifier == that.identifier
      case _ => false
    }
  }

  override final def hashCode(): Int = identifier.hashCode()
}

equalsをオーバーライドすることで、エンティティが同じであるかをIdentifierを使って判定できるようになっています。 しかし、この実装は果たして良いのかどうかを考えてみるエントリです。

ポイント

タイトルについて考えるために、大きく2つのポイントがあるかなと思っています。

順に説明します。

DDD本の定義

エンティティの章では次のように述べられています。

多くのオブジェクトは、本質的に、その属性によってではなく、連続性と同一性 (identity) によって定義される。

(中略)

オブジェクトの中には、主要な定義が属性によってなされないものもある。そういうオブジェクトは同一性のつながりを表現するのであり、その同一性は、時間が経っても、異なるかたちで表現されても変わらない。 そういうオブジェクトは属性が異なっていても、他のオブジェクトと一致しなければならないことがある。また、あるオブジェクトは、同じ属性を持っていたとしても、他のオブジェクトと区別しなければならない。 同一性を取り違えるとデータの破損につながりかねない。

主として同一性によって定義されるオブジェクトはエンティティと呼ばれる。

− エリック・エヴァンスのドメイン駆動設計 p.87-89

identityですね。
equalityではないですね。

等価性について

同一性と混同しやすい言葉として等価性*1があります。

  • 同一性は「2つの変数が同じオブジェクトを参照している」こと
  • 等価性は「2つの変数の値が等しい」こと

一般に同一性を満たしていれば、等価性を満たしていることも含意しています。

Scalaでは、等価性は equals or == で判定し、同一性は eq で判定します。 Javaにおいては、等価性は equals で、同一性は == です。

DDDの文脈では、等価性は(エンティティが持つ)値がすべて同じであればtrueとみなし、同一性は識別子が同じであればtrueとみなすものと解釈できそうです。 つまり、エンティティの定義である「同一性によって定義されるオブジェクト」に照らすと equals をオーバーライドすることは適切ではありません。 この点では、本来オーバーライドすべきは eq と言えるかもしれません。

参考:

equalsはプログラマにとって大きな暗黙の前提である

多くのプログラマequals を見れば、まずは上記でいう等価性の判定を期待するのではないでしょうか。 プログラミングによって何かを成す以上、プログラマのコンテキストはドメインモデルを実装するうえでより根源的なものでしょう。 Any (=Object) を継承していると考えれば、(より根源的なプログラマのコンテキストにおいて)リスコフの置換原則に反しているようにも見えます*2

たとえば、エンティティを極力Immutableなオブジェクトとして扱いたい場合、次のようなケースで思わぬ振る舞いに遭遇することがあります。

val foo = FooEntity(...)
// fooが一定の条件を満たすなら、その属性を変更するようなメソッド
val adjustedFoo = BarService.adjust(foo)
// 変更を期待するテストケースで単純に操作されていることを確認したい
assert(foo != adjustedFoo)

もちろん、どの属性が変わるのかまでをテストすべきで、この例はテストとしてはあまり好ましくありません。 しかし、 foo equals adjustedFoo であるのに foo.bar not equals adjustedFoo.bar となることは、この場面では違和感を覚えます。 テストというコンテキストは、ドメインへの関心よりもプログラムへの関心が強く、equalsが(等価性の比較のためのものだという頭であるので)同一性のみで判定を行うことは直感に反していると感じるためです。

たしかに、エンティティの equals は同一性を判定するためのものですよ、と周知徹底すれば良い話なのかもしれません。 ただ、プログラムを書いているのはプログラマである、ということは忘れてはならないように思います。 特に今回の例のように Layer Supertype を使っている場合、実装を追わなければ equals がオーバーライドされていることが分からないことは、余分な意識コストとなってしまうでしょう。

まとめ

定義の意味としても、実装上の暗黙の期待としても同一性と等価性は区別して捉えるべきことを見てきました。

同一性の判定のために equals をオーバーライドすることは避けることを推奨します。 値オブジェクト (ValueObject) は前述の等価性で表すことが自然であるため、equalsをオーバーライドする方が好ましいでしょう*3

では、どうするか

Identifierで同一性を判定するメソッドを用意します。

trait Entity[ID <: Identifier] {
  def identifier: ID

  def sameIdentityAs(that: Entity[ID]): Boolean = {
    this.getClass == that.getClass && this.identifier == that.identifier
  }
}

「等価性の判定は equals を使う」と使い分けることは一般的に思えますし、同一性と等価性のいずれを期待した比較であるのかもコードから意図が明確になります。

*1:同値性とも言います。

*2:Objectにequalsがあるのは誤りだという話もありますが…

*3:Scalaではcase classを使うと楽々ですね。