36
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DDDにおけるIdentifier/Entity/Repository間の関係をScalaで型付けする

Last updated at Posted at 2015-06-13

2016/06/07 追記
下記の実装には問題点がある(gakuzzzzさんからコメントで指摘いただきました)ので、そちらも合わせてご参照ください。

はじめに

DDDにおいては、ドメインの登場人物を取り扱うための、様々なデザインパターンが登場します。

典型的には、例えばユーザというエンティティを考えると

  • ユーザは一意なIdentifier(ID)を持つ
  • IDをリポジトリに渡すと、リポジトリはDB上のデータからユーザを生成する

リポジトリはエンティティのライフサイクルにおける、永続化部分の隠蔽を担当します。関連する部分の雰囲気だけコードに表すと、こんな感じになるでしょうか。

class User(val id: Long)

class UserRepositry {
    def resolve(id: Long): User = ??? //省略: DBアクセスしてUserを取得するコード
}

問題点

上のようなシンプルな方針には問題点があるので、ある程度以上大きなプログラムだと何らかの改良をしたくなります。

  • idLongになっている。型として一般的すぎるので、何の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メソッドを持ちます。IDEとの間にE <: Entity[ID]の関係があり、かつresolveの戻り値の型がEである点に注意してください。つまり、必ずIDと対応するエンティティが返ってくることが保証できています。

trait Repository[ID <: Identifier[_], E <: Entity[ID]] {
  def resolve(id: ID): E
}

上記のように定義された一般的なIdentifierEntityRepositoryを枠組みとして使うことで、最初に題材にした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実装しました。

実際のところ、じゅんいちかとう氏の実装例の焼き直しになっているような……うん、ほとんど変わらないですね。これ、もしかして結構一般的なパターンなのかなあ……?

36
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?