2016/06/07 追記
下記の実装には問題点がある(gakuzzzzさんからコメントで指摘いただきました)ので、そちらも合わせてご参照ください。
はじめに
DDDにおいては、ドメインの登場人物を取り扱うための、様々なデザインパターンが登場します。
典型的には、例えばユーザというエンティティを考えると
- ユーザは一意なIdentifier(ID)を持つ
- IDをリポジトリに渡すと、リポジトリはDB上のデータからユーザを生成する
リポジトリはエンティティのライフサイクルにおける、永続化部分の隠蔽を担当します。関連する部分の雰囲気だけコードに表すと、こんな感じになるでしょうか。
class User(val id: Long)
class UserRepositry {
def resolve(id: Long): User = ??? //省略: DBアクセスしてUserを取得するコード
}
問題点
上のようなシンプルな方針には問題点があるので、ある程度以上大きなプログラムだと何らかの改良をしたくなります。
-
id
がLong
になっている。型として一般的すぎるので、何のIDなのかという情報が欠けてしまっている - そのため、値の混同を起こしやすい。現実的なプログラムには、
User
に振られたID以外にも、様々な種類のIDがあるはずだが、これらが全てLong
になってしまう
うーん困った。そこで、IDにちゃんと専用の型をつけることで、エンティティ・リポジトリを含む枠組みを整理できないか考えてみました。
解決策?
Scalaの場合は、ID、エンティティ、リポジトリそれぞれにベースになるトレイトを作り、これらの間の関係性を型的に保証するというテクニックが使えそうです。
まずはIDを表すトレイトから。「同値比較ができればいいが、具体的な型(Long
かどうか等)は決めない」という方針にするため、型パラメータA
を導入しています。
trait Identifier[A] {
def value: A
override def equals(obj: Any): Boolean = obj match {
case other: Identifier[_] => this.value == other.value
case _ => false
}
override def hashCode: Int = value.hashCode
}
次にエンティティ。必ずIDを持つがそれがどの型かはまだ決まっていないので、ここも型パラメータになります。本当はもっと色々なメソッドが付いていてもおかしくないですが、ここではIdentifier
を利用して比較ができるという事だけを実装しました。
trait Entity[ID <: Identifier[_]] {
val id: ID
override def equals(obj: Any): Boolean = obj match {
case other: Entity[_] => this.id == other.id
case _ => false
}
override def hashCode: Int = id.hashCode
}
最後にリポジトリです。ここで定義する抽象的なリポジトリは、あるIdentifier
から対応するEntity
を探すためのresolve
メソッドを持ちます。ID
とE
との間にE <: Entity[ID]
の関係があり、かつresolve
の戻り値の型がE
である点に注意してください。つまり、必ずIDと対応するエンティティが返ってくることが保証できています。
trait Repository[ID <: Identifier[_], E <: Entity[ID]] {
def resolve(id: ID): E
}
上記のように定義された一般的なIdentifier
、Entity
、Repository
を枠組みとして使うことで、最初に題材にしたUser
周りについては以下のように書くことができます。
class UserId(val value: Long) extends Identifier[Long]
class User(val id: UserId) extends Entity[UserId]
class UserRepository extends Repository[UserId, User] {
def resolve(id: UserId): User = ??? //省略
}
まとめ
リポジトリパターンに関連する部分のエンティティ・IDについて、IDも含めて専用の型を付けるという方針で、Scala実装しました。
実際のところ、じゅんいちかとう氏の実装例の焼き直しになっているような……うん、ほとんど変わらないですね。これ、もしかして結構一般的なパターンなのかなあ……?