68
60

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.

インフラレイヤがドメインレイヤのリポジトリに依存する実装方法

Posted at

ドメイン駆動と言っても、データを永続化している以上、ドメインレイヤがインフラレイヤを活用する必要があり、ドメインがインフラに依存するアーキテクチャはごく自然のようですが、DIP(dependency inversion principle)を実現したアーキテクチャでは、インフラがドメインに依存するようになり、従来のアーキテクチャとは依存関係が逆になります。

  • 伝統的なアーキテクチャ → ドメインレイヤがインフラレイヤに依存
  • DIPを実現したアーキテクチャ → インフラレイヤがドメインレイヤに依存

この逆転のアーキテクチャをどう実装したらいいか疑問に思っている人も多いのではないでしょうか?Amazon.co.jp: 実践ドメイン駆動設計 (Object Oriented Selection): ヴァーン・ヴァーノン, 高木 正弘: 本で紹介されている実装方法をScalaで説明します。

実装のポイント

  • ドメインレイヤとインフラレイヤに2つリポジトリを作る。
  • ドメインレイヤのリポジトリはインターフェイス。
  • インフラレイヤのリポジトリはドメインレイヤのリポジトリを継承&実装。

ドメインレイヤにはリポジトリのインターフェイスを作ります。また、ビジネスロジックがあれば、ドメインレイヤのリポジトリに記述します。

インフラレイヤのリポジトリはドメインレイヤのリポジトリを継承し、エンティティをどのように永続化するか?どのようにストレージから取り出すか?どのようにストレージからエンティティを削除するか?など技術的な課題を解決するために、具体的な実装をコーディングします。例えば、RDBMSを使ったリポジトリなら、テーブル名やカラム名、SELECT文やINSERT文が現れるのは、インフラレイヤのリポジトリです。ドメインレイヤにはこういった情報は一切出ません。

サンプルコード

私は、DDDでテスト自動化サービスのShouldBee - テスト作業を限りなくゼロにを開発しています。テストを行うため、テストに関するドメインモデルを構築しています。その中から、テストスクリプトに関するモデルを抜粋してサンプルコードとしてお示ししたいと思います。

ドメインレイヤのリポジトリやValue Object

shouldbee/testing/domain/model/development/package.scala
package shouldbee.testing.domain.model

import java.util.UUID

import org.joda.time.DateTime
import shouldbee.testing.domain.model.common.TesterId
import shouldbee.testing.domain.model.plan.ProjectId

package object development {
  case class TestScriptId(id: UUID)

  sealed trait Event
  case class TestScriptCreated(testScriptId: TestScriptId, projectId: ProjectId, script: String, comment: String, occurredOn: DateTime, occurredBy: TesterId) extends Event
  case class TestScriptUpdated(testScriptId: TestScriptId, script: String, comment: String, occurredOn: DateTime, occurredBy: TesterId) extends Event
  case class TestScriptRemoved(testScriptId: TestScriptId, comment: String, occurredOn: DateTime, occurredBy: TesterId) extends Event

  // リポジトリのインターフェイス
  trait TestScriptRepository {
    type Events = Seq[Event]
    def nextIdentity: TestScriptId
    def testScriptOfId(id: TestScriptId): Option[(TestScript, Events)]
    def save(id: TestScriptId, events: Events, oldEvents: Events): Unit
  }
}

ドメインレイヤのエンティティ

shouldbee/testing/domain/model/development/TestScript.scala
package shouldbee.testing.domain.model.development

import java.util.UUID

import org.joda.time.DateTime
import shouldbee.testing.domain.model.common.TesterId
import shouldbee.testing.domain.model.plan.ProjectId
import shouldbee.testing.lang.AssertionConcern._

object TestScript {
  def create(testScriptId: TestScriptId, projectId: ProjectId, script: String, comment: String, createdBy: TesterId) = {
    assertArgumentNotEmpty(script, "The script must be provided.")
    assertArgumentNotEmpty(projectId.id.toString, "The project id must be provided.")
    assertArgumentNotEmpty(createdBy.id, "The tester id must be provided.")
    new TestScript & TestScriptCreated(testScriptId, projectId, script, comment, new DateTime, createdBy)
  }
}

case class TestScript(testScriptId: TestScriptId, projectId: ProjectId, script: String, removed: Boolean) {
  def this() = this(TestScriptId(UUID.randomUUID), ProjectId(UUID.randomUUID), "", false)

  def update(script: String, comment: String, updatedBy: TesterId) = {
    assertStateFalse(removed, "The script is already removed.")
    assertArgumentNotEmpty(updatedBy.id, "The tester id must be provided.")
    this & TestScriptUpdated(testScriptId, script, comment, new DateTime, updatedBy)
  }

  def remove(comment: String, removedBy: TesterId) = {
    assertStateFalse(removed, "The script is already removed.")
    assertArgumentNotEmpty(removedBy.id, "The tester id must be provided.")
    this & TestScriptRemoved(testScriptId, comment, new DateTime, removedBy)
  }

  private def &[A](e: A): (TestScript, A) = (this + e, e)

  private[shouldbee] def +[A](event: A) = event match {
    case e: TestScriptCreated => copy(testScriptId = e.testScriptId, projectId = e.projectId, script = e.script)
    case e: TestScriptUpdated => copy(script = e.script)
    case e: TestScriptRemoved => copy(removed = true)
  }
}

インフラレイヤのリポジトリ

パッケージ名がport.adapterになっていますが、これはヘキサゴナルアーキテクチャに由来する命名です。ざっくり言うとinfrastractureと同じ意味です。

ShouldBeeではイベントソーシングという方法で、Entityを永続化していますが、重要なのはそこではなく、EventStoreTestScriptRepositoryがドメインレイヤのTestScriptRepositoryを継承しているところです。

shouldbee/testing/port/adapter/persistence/repository/EventStoreTestScriptRepository.scala
package shouldbee.testing.port.adapter.persistence.repository

import shouldbee.testing.domain.model.development.{ TestScript, TestScriptId, TestScriptRepository }
import shouldbee.testing.domain.model.development.Event
import shouldbee.testing.port.adapter.persistence.event.sourcing.{ AppendMessages, EventDispatcherProvider, EventStoreProvider }
import shouldbee.testing.port.adapter.persistence.serialization.DevelopmentJsonSerialization

// MySQLなら MySQLTestScriptRepository という名前にすることになるでしょう。
object EventStoreTestScriptRepository extends TestScriptRepository with EventStoreProvider with EventDispatcherProvider {
  override def nextIdentity: TestScriptId = TestScriptId(java.util.UUID.randomUUID)

  // 具体的な保存のしかた。ShouldBeeではイベントソーシングという方法でEntityを永続化しています。
  // RDBMSならここでINSERT文やUPDATE文を実行する処理が書かれます。
  override def save(id: TestScriptId, events: Events, oldEvents: Events): Unit = {
    val messages = new AppendMessages[Event](oldEvents.length, "application/json", DevelopmentJsonSerialization.serialize)
    events.foreach(messages.add)
    eventStore.batchAppend(id.id.toString, messages.toSeq)
    eventDispatcher.update
  }


  // 具体的な再構築のしかた。ShouldBeeではイベントソーシングという方法でEntityを永続化しているので、
  // イベントを適用して、Entityを再構築していますが、ステートソーシングのRDBMSを使う場合は、
  // ここにSELECT文を実行する処理が書かれます。
  override def testScriptOfId(id: TestScriptId): Option[(TestScript, Events)] = {
    val optionEvents = eventStore.stream(id.id.toString).map { rawEvent =>
      DevelopmentJsonSerialization.deserialize(rawEvent.eventType, rawEvent.body)
    }
    val events = optionEvents.collect { case Some(event) => event }
    events.size match {
      case 0 => None
      case _ => Some((events.foldLeft(new TestScript)(_ + _), events))
    }
  }
}

なぜインフラがドメインに依存したほうがいいか?

Entityをどういう方法で永続化するかを後で決められるのが大きいです。ひと通りドメインモデルを構築してから、MySQLで保存しようか、NoSQLでやろうか、それともイベントストアにしようか、といった選択が後でできます。ドメインにとって最適なインフラ技術を選ぶことができます。

質問などありましたら、コメントくださいm(_ _)m

68
60
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
68
60

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?