かとじゅんの技術日誌

技術の話をするところ

sbtでDAOを自動生成する方法

DDDのリポジトリを実装する際、ほとんどのケースでDAOが必要になります。が、ボイラープレートが多く、自動生成したいところです。というわけで作りました。

どうやって自動化するか

septeni-original/sbt-dao-generator

指定されたスキーマのJDBCメタ情報とテンプレートをマージさせて、ソースコードを出力します。その機能を提供するのがsbt-dao-generator1です。つまり、sbtからコマンド一発でこういうことができるようになるわけですが、 DBのインスタンスが立ち上がっていて、スキーマ情報が組み込まれた状態でないと使えません。

chatwork/sbt-wix-embedded-mysql

sbtから組み込みMySQLを起動するプラグインです。MySQL固定です…。

flyway/flyway-sbt

こちらは言わずもがな、有名なsbtプラグイン。組み込みMySQL上にスキーマを自動作成するために使います。

環境構築手順

実際のサンプルコードは、j5ik2o/scala-ddd-baseをみてください。 flywayを扱うプロジェクトflywayとDAOを自動生成するプロジェクトexampleはわけています。

project/plugins.sbt

プラグインを追加しましょう

addSbtPlugin("com.chatwork" % "sbt-wix-embedded-mysql" % "1.0.9")

addSbtPlugin("jp.co.septeni-original" % "sbt-dao-generator" % "1.0.8")

addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "5.0.0")

テンプレートを作りましょう

FTLでDAOのテンプレートを書きます。以下はSkinnyORMのための例です。レコードクラスとDAOクラスです。

  case class ${className}Record(
<#list primaryKeys as primaryKey>
    ${primaryKey.propertyName}: ${primaryKey.propertyTypeName}<#if primaryKey_has_next>,</#if></#list><#if primaryKeys?has_content>,</#if>
<#list columns as column>
    <#if column.columnName == "status">
        <#assign softDelete=true>
    </#if>
    <#if column.nullable>    ${column.propertyName}: Option[${column.propertyTypeName}]<#if column_has_next>,</#if>
    <#else>    ${column.propertyName}: ${column.propertyTypeName}<#if column_has_next>,</#if>
    </#if>
</#list>
  ) extends Record

  object ${className}Dao extends Dao[${className}Record] {

      override def useAutoIncrementPrimaryKey: Boolean = false

      override val tableName: String = "${tableName}"

      override protected def toNamedValues(record: ${className}Record): Seq[(Symbol, Any)] = Seq(
<#list columns as column>       '${column.name} -> record.${column.propertyName}<#if column.name?ends_with("id") || column.name?ends_with("Id")>.value</#if><#if column_has_next>,</#if>
</#list>
    )

      override def defaultAlias: Alias[UserAccountRecord] = createAlias("${className[0]?lower_case}")

      override def extract(rs: WrappedResultSet, s: ResultName[${className}Record]): ${className}Record = autoConstruct(rs, s)


  }

特定のDAOに依存しないので、ほとんどのものに対応できるはず。以下は、Slick用とSkinny用の両方に対応したテンプレート例です。どちらでも好きなORMを使ってください。

https://github.com/j5ik2o/scala-ddd-base/blob/reboot/example/templates/template.ftl

テンプレートの書き方はこちら参照。カラム名をあらかじめプロパティ名としてテンプレートコンテキストに含めているので、簡単に書けるはずです。

build.sbt

  • flywayプロジェクト

https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L114-L136

このプロジェクトではchatwork/sbt-wix-embedded-mysqlとflyway/flyway-sbtを使って自動的にスキーマを作ります。flywayMigrate := (flywayMigrate dependsOn wixMySQLStart).value としているので、sbt flyway/flywayMigrateする前に組み込みMySQLが起動します。

  • exampleプロジェクト

https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L138-L188

JDBCの接続先設定は、flywayプロジェクトと同じ設定を指定してください。

このプロジェクトでは、septeni-original/sbt-dao-generatorを使ってJDBCメタ情報とテンプレートをマージして、DAOクラスのソースコードを生成します。 今回は生成物をGitで管理したかったので、以下のようにして通常のソースコードと同じパスに出力していますが、(sourceManaged in Compile).valueを使ってtarget/src_managedに出力することも可能です。

outputDirectoryMapper in generator := {
  case s if s.endsWith("Spec") => (sourceDirectory in Test).value
  case s => new java.io.File((scalaSource in Compile).value, "/com/github/j5ik2o/dddbase/example/dao")
},
outputDirectoryMapper in generator := { className: String => (sourceManaged in Compile).value },

コンパイル時にソースコード生成するには以下のようにしてください。コンパイルと無関係に生成タスクを実行したい場合はsbt generator::generateAllとしてください。

// sourceGenerators in Compile時に出力
sourceGenerators in Compile += (generateAll in generator).value

コンパイルより前に出力したい場合は以下でも動作します。

// コンパイルより前に出力
compile in Compile := ((compile in Compile) dependsOn (generateAll in generator)).value

あとでexampleプロジェクトからflywayプロジェクトに依存することをお忘れ無く

val example = ...
dependsOn(..., flyway)

生成

コンパイル時に自動的に以下が行われます。

  1. 組み込みMySQLの起動
  2. flyway マイグレーション実行
  3. JDBCメタ情報とテンプレートのマージとソースファイル出力
  4. コンパイル

実際に生成されたソースコードはこちら。

https://github.com/j5ik2o/scala-ddd-base/blob/master/example/src/main/scala/com/github/j5ik2o/dddbase/example/dao/UserAccount.scala

package com.github.j5ik2o.dddbase.example.dao

package slick {
  import com.github.j5ik2o.dddbase.slick.SlickDaoSupport

  trait UserAccountComponent extends SlickDaoSupport {

    import profile.api._

    case class UserAccountRecord(
        id: Long,
        status: String,
        email: String,
        password: String,
        firstName: String,
        lastName: String,
        createdAt: java.time.ZonedDateTime,
        updatedAt: Option[java.time.ZonedDateTime]
    ) extends SoftDeletableRecord

    case class UserAccounts(tag: Tag)
        extends TableBase[UserAccountRecord](tag, "user_account")
        with SoftDeletableTableSupport[UserAccountRecord] {
      // def id = column[Long]("id", O.PrimaryKey)
      def status    = column[String]("status")
      def email     = column[String]("email")
      def password  = column[String]("password")
      def firstName = column[String]("first_name")
      def lastName  = column[String]("last_name")
      def createdAt = column[java.time.ZonedDateTime]("created_at")
      def updatedAt = column[Option[java.time.ZonedDateTime]]("updated_at")
      override def * =
        (id, status, email, password, firstName, lastName, createdAt, updatedAt) <> (UserAccountRecord.tupled, UserAccountRecord.unapply)
    }

    object UserAccountDao extends TableQuery(UserAccounts)

  }

}

package skinny {

  import com.github.j5ik2o.dddbase.skinny.SkinnyDaoSupport
  import scalikejdbc._
  import _root_.skinny.orm._

  trait UserAccountComponent extends SkinnyDaoSupport {

    case class UserAccountRecord(
        id: Long,
        status: String,
        email: String,
        password: String,
        firstName: String,
        lastName: String,
        createdAt: java.time.ZonedDateTime,
        updatedAt: Option[java.time.ZonedDateTime]
    ) extends Record

    object UserAccountDao extends Dao[UserAccountRecord] {

      override def useAutoIncrementPrimaryKey: Boolean = false

      override val tableName: String = "user_account"

      override protected def toNamedValues(record: UserAccountRecord): Seq[(Symbol, Any)] = Seq(
        'status     -> record.status,
        'email      -> record.email,
        'password   -> record.password,
        'first_name -> record.firstName,
        'last_name  -> record.lastName,
        'created_at -> record.createdAt,
        'updated_at -> record.updatedAt
      )

      override def defaultAlias: Alias[UserAccountRecord] = createAlias("u")

      override def extract(rs: WrappedResultSet, s: ResultName[UserAccountRecord]): UserAccountRecord =
        autoConstruct(rs, s)

    }

  }

}

まとめ

このプロジェクト構成は、仕事でも結構がっつり使っていて気に入っています。スキーマ変更が起こっても、DAOは一瞬で自動生成できるので楽になると思います。興味あれば使ってみてください!


  1. 僕とセプテーニさんとコラボして作りました。

jupyter-scala の セットアップ方法と使い方

セットアップ

$ pyenv install 3.6.5
$ pyenv virtualenv 3.6.5 jupyter-notebook
$ cd jupyter-scala
$ pyenv local jupyter-notebook
$ pip install --upgrade pip
$ pip install jupyter-notebook
$ wget https://raw.githubusercontent.com/alexarchambault/jupyter-scala/master/jupyter-scala
$ sh ./jupyter-scala

wgetしたjupyter-scalaSCALA_VERSIONは以下のように2.11系なので、2.12を使う場合はコメントどおりに修正する。

SCALA_VERSION=2.11.11 # Set to 2.12.2 for Scala 2.12

起動

$ jupyter notebook

デフォルトブラウザ上で、http://localhost:8888/treeが開かれます。

使い方

NewScalaをクリックします。

jn-home.png

Hello Worldをやってみましょう。コードを入力したらShift+Enterで実行することができます。typoしてもセル内のコードを修正して実行し直すことができる

jn-helloworld.png

外部ライブラリへの依存関係を追加する

build.sbtlibraryDependenciesに書く情報を以下の形式にして入力・評価するだけでダウンロードされます。他に依存関係を除外する$execludeもあるようです。1

import $ivy.`org.typelevel::cats-core:1.1.0`

jn-import.png

NOTE

load.resolverに相当するAPIがよくわからなかった。ここにResolverを追加すればよさそうではあるが、まだ試していない。


  1. このイシュー によるとload.ivyclasspath.addなど古いAPIとのこと。

FluxとDDDの統合方法

おはこんばんにちは、かとじゅんです。 久しぶりにブログを書く…。最近、趣味でAngular2やらReactやらやっています。やっとWebpackになれました…。

さて、今回のお題は「FluxとDDDの統合方法」について。Angular2を先に触っていましたが、FluxといえばやはりReactだろうということで途中で浮気してReactで考えています。Angular2でもできるはずですが、今回はReactで統合方法*1について考えてみたいと思います。一つ断っておくと、FluxはDDDと統合することを想定していない設計パターンなんで云々とかはここでは考えていません。それはこのブログ記事を読む読まないに関わらずご自身で判断されてください。ソースコードについては、Githubへのリンクを一番下に書いてあるので興味がある人は参考にしてみてください。

*1:今回はEvent Sourcingではありません。State Sourcingです。機会があれば実装例を作ります

続きを読む

混乱しがちなサービスという概念について

社内でサービスがよくわからないという話になったので、考察を少しまとめておきます。

過去のエントリでも以下のように触れましたが、もう少しかみ砕いてみよう。

サービスという言葉はあいまい まず、簡単に前提の整理から。単に"サービス"って言葉が何を指すのか結構曖昧です。 サービスは簡単にいうと手続きとか振る舞いのことですが、細かくいうと、PofEAAでいうサービスと、DDDいうサービスは、目的が異なります。前者はアプリケーションのためにドメインモデルを再利用可能にするためのものです。後者はドメインの知識を表している振る舞いです。これはのちほど詳しく説明します。 まぁこのあたりは具体例がないと理解しがたいですが、レイヤーの違いによって責務が異なるという感じです。DDDのサービスの章では、サービスには、アプリケーション層、ドメイン層、インフラストラクチャ層と、複数のレイヤーに存在すると言及されています。PofEAAのService Layerは、DDDでいうアプリケーション層のサービス(以下 アプリケーションサービス)に相当すると思います。

ServiceとDCIについて - かとじゅんの技術日誌

サービスは抽象的でわかりにくい。特にDDDのレイヤー化アーキテクチャのレイヤー分割という概念を踏まえないと混乱する原因になりますので、レイヤーの定義から入りましょう。

続きを読む

CQRS+Event Sourcingを学ぶための教材

超久しぶりのブログ…。 Octopressに疲れたのではてなブログに戻ってきました(Octopressの過去の記事ははてなブログにインポート済です)。ついでプロに移行。

さて、海外のDDDコミュニティではCQRS+Event Sourcing(以下, ES)が人気なのですが、ようやく日本でも話題になることが多くなったので今回は教材となりそうな書籍を簡単に紹介したいと思います。

DDD といえば まず エリック・エヴァンスのドメイン駆動設計 (以下 DDD本) を読むべきですが、CQRSについては記載がないので 実践ドメイン駆動設計 を読みましょう。

実践ドメイン駆動設計

実践ドメイン駆動設計

さらにDDD本には ES の基礎となる ドメインイベント の解説が含まれていません。そのドメインイベントの概要を掴みたければ、実践ドメイン駆動設計に記載があります。

より実装イメージを掴みたいという人には、以下の本がお勧めです。ただし、コードはC#ですが、より実装視点での知見が得られる本です。

.NETのエンタープライズアプリケーションアーキテクチャ 第2版 (マイクロソフト公式解説書)

.NETのエンタープライズアプリケーションアーキテクチャ 第2版 (マイクロソフト公式解説書)

非同期でノンブロッキングなアーキテクチャでC10K問題を解決するには、Scalaでの実装手段はいろいろあると思いますが、Akkaはその一つです。また、AkkaはDDD+CQRS+ESへの考慮も為されいるツールキットです。ということで、AkkaでDDDをやる場合、参考になるのは以下の書籍らしいです。実践ドメイン駆動設計 の著者(Vaughn Vernon氏)が書いた書籍で、設計上の概念のみならずAkkaでの実装例なども紹介されています。

Reactive Messaging Patterns with the Actor Model: Applications and Integration in Scala and Akka

Reactive Messaging Patterns with the Actor Model: Applications and Integration in Scala and Akka

さらに読書会もあるので、興味がある方は参加してみてはどうでしょうか?(僕も参加する予定)

ddd-cqrs-es.connpass.com

あと、gihub上の参考になるコード例としては以下をあげておきます。ddd-leaven-akka-v2は、akka-dddをベースしたサンプルです。コード量は結構あるので読むには気合いがいると思いますが…。先に上の書籍で概念を押さえてからの方が無難です。

GitHub - pawelkaczor/ddd-leaven-akka-v2: Next generation of ddd-leaven-akka

GitHub - pawelkaczor/akka-ddd: Akka/EventStore DDD framework

関連するブログ記事はこちらです。

Reactive DDD with Akka | Write less, do more!

Reactive DDD with Akka - lesson 2 | Write less, do more!

Reactive DDD with Akka - lesson 3 (Projections) | Write less, do more!

DDD+CQRS+ESとは直接的に関係ないのですが、Reactive Messaging Patterns with the Actor Model: Applications and Integration in Scala and Akka でよく引用されている書籍です。Akkaで提供されている機能のほとんどは、EIPを読むと理解できると思います。

Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions (Addison-Wesley Signature Series (Fowler))

Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions (Addison-Wesley Signature Series (Fowler))

Java EE読書会のドキュメントも併読すると参考になるかもしれません。 EIP - Java EE勉強会

なぜCQRSやESが必要になったのかは、別のエントリで書く予定。

DDDのリポジトリのインターフェイスをどのように設計すべきか

scala-dddbase

scala-dddbaseではどのようなインターフェイスとしているか?

以下のようになっています。

戻り値はモナドにラップして返すことを想定しているので、高階型としてM型を指定できるようにしました。一般的にMには、同期型リポジトリとしてはTry型、非同期型リポジトリとしてはFuture型を指定します。

副作用がない読み込み系メソッドはM[E]もしくはM[Seq[E]]で返します。一方、副作用がある書き込み系メソッドはM[Result],M[Results]を返します(Result, Resultsはリポジトリの新しい状態とエンティティを含む型としています。オンメモリなリポジトリの場合は新しいインスタンスを返す不変型リポジトリを実装し、DBなどのストレージに対応づく場合は可変リポジトリとしてthisを返すにようします)

リポジトリはあらゆる永続化技術から中立でなければならないが、現実問題としてDBMS実装の場合はコネクションやトランザクション管理のためのオブジェクトを渡す必要があるため、抽象的なデータ型としてCtx型を渡せるようにしています。利用する際は内部でパターンマッチをして必要な情報を取り出すなどをやっています。この辺はもっといい方法(多分Readerモナドでいける気がする)がありそうだが妥協しています。

EntityReader

trait EntityReader[ID <: Identifier[_], E <: Entity[ID], M[+  _]] extends EntityIO[M] {
  
  /**
   * 識別子からエンティティを解決する。
   * 
   * @param identifier 識別子
   * @return 正常: エンティティ
   *         例外: EntityNotFoundExceptionは、エンティティが存在しない場合。
   */
  def resolveBy(identifier: ID)(implicit ctx: Ctx): M[E]

  /**
   * 複数の識別子から複数のエンティティを解決する。
   * 
   * @param identifier 識別子
   * @return 正常: エンティティの集合
   *         例外: RepositoryExceptionは、リポジトリにアクセスできなかった場合。
   */
  def resolveByMulti(identifiers: ID*)(implicit ctx: Ctx): M[Seq[E]]

}

EntityWriter

trait EntityWriter[ID <: Identifier[_], E <: Entity[ID], M[+ _]] extends EntityIO[M] {

  type This <: EntityWriter[ID, E, M]
  type Result <: ResultWithEntity[This, ID, E, M]
  type Results <: ResultWithEntities[This, ID, E, M]

  /**
   * エンティティを保存する。
   *
   * @param entity 保存する対象のエンティティ
   * @return 正常: リポジトリインスタンスと保存されたエンティティ
   *         例外: RepositoryExceptionは、リポジトリにアクセスできなかった場合。
   */
  def store(entity: E)(implicit ctx: Ctx): M[Result]

  /**
   * 複数のエンティティを保存する。
   *
   * @param entity 保存する対象のエンティティ
   * @return 正常: リポジトリインスタンスと保存されたエンティティ
   *         例外: RepositoryExceptionは、リポジトリにアクセスできなかった場合。
   */
  def storeMulti(entities: E*)(implicit ctx: Ctx): M[Results]

}

EntityIO

/**
 * エンティティをIOするためのトレイト。
 */
trait EntityIO[M[+ _]] {

  type Ctx = EntityIOContext[M]

}

ResultWithEntity

/**
 * [[org.sisioh.dddbase.core.lifecycle.EntityWriter]]の新しい状態とエンティティを保持する値オブジェクト。
 *
 * @tparam EW [[org.sisioh.dddbase.core.lifecycle.EntityWriter]]の型
 * @tparam ID エンティティの識別子の型
 * @tparam E エンティティの型
 * @tparam M モナドの型
 */
trait ResultWithEntity[+EW <: EntityWriter[ID, E, M], ID <: Identifier[_], E <: Entity[ID], M[+A]] {

  /**
   * 結果
   */
  val result: EW

  /**
   * エンティティ
   */
  val entity: E

}

ResultWithEntities

/**
 * [[org.sisioh.dddbase.core.lifecycle.EntityWriter]]の新しい状態と複数のエンティティを保持する値オブジェクト。
 *
 * @tparam EW [[org.sisioh.dddbase.core.lifecycle.EntityWriter]]の型
 * @tparam ID エンティティの識別子の型
 * @tparam E エンティティの型
 * @tparam M モナドの型
 */
trait ResultWithEntities[+EW <: EntityWriter[ID, E, M], ID <: Identifier[_], E <: Entity[ID], M[+A]] {

  /**
   * 結果
   */
  val result: EW

  /**
   * エンティティ
   */
  val entities: Seq[E]

}

考えられるI/Oインターフェイス

上記の設計に至るまでに考えたインターフェイスについてまとめます。

読み込み系のメソッド

戻り値として値だけ返す方法

エンティティが存在する場合は戻り値で返し、存在しないという状況と、その他の例外状況を、実行時例外で返すことになります。

trait Repository[ID, E] {
  def resolveBy(id: ID)(implicit ctx: Ctx): E
}

val response = try {
  val entity = repository.resolveBy(id)
  Ok(entity.toJson)
} catch {
  case EntityNotFoundException(id) => BadRequest(id)
  case ex: RepositoryException => InternalServerError(ex)
}

Option型でラップする方法

エンティティが存在する場合はSomeでラップして返し、存在しない場合はNoneが返されます。それ以外の例外状況は実行時例外になります。

trait Repository[ID, E] {
  def resolveBy(id: ID)(implicit ctx: Ctx): Option[E]
}

val response = try {
  repository.resolveBy(id).fold(NotFound(id)){ entity =>
    Ok(entity.toJson)
  }
} catch {
  case ex: Exception => InternalServerError(ex)
}

Either型でラップする方法

エンティティが存在する場合はエンティティをRightでラップして返し、存在しない場合はEntityNotFoundErrorで返し、それ以外の例外状況は対応するErrorで返す。

sealed trait Error
case class EntityNotFoundError(id: Identifier) extends Error
case class RepositoryError(id: Identifier, throwable: Throwable) extends Error

trait Repository[ID, E] {
  def resoleBy(id: ID)(implicit ctx: Ctx): Either[Error, E]
}

val response = repository.resolveBy(id).fold({
  case EntityNotFoundError(id) => NotFound(id)
  case RepositoryError(id, thr) => InternalServerError(id)
}, {
  entity =>
    Ok(entity.toJson)
})

Either[Error, Option[E]]版の場合は、エンティティが存在しない場合はRigiht(None)として返す。

trait Repository[ID, E] {
  def resoleBy(id: ID)(implicit ctx: Ctx): Either[Error, Option[E]]
}

val response = repository.resolveBy(id).fold({
  case RepositoryError(id, thr) => InternalServerError(id)
}, {
  entityOpt =>
    entityOpt.fold(NotFound(id)){ entity =>
      Ok(entity.toJson)
    }
})

Try型でラップする方法

Either型とほぼ変わりませんが、例外状況としてFailureにはどんなExceptionも格納できます。

trait Repository[ID, E] {
  def resolveBy(id: ID)(implicit ctx: Ctx): Try[E]
}

val response = repository.resolveBy(id).map{ entity =>
  Ok(entity.toJson)
}.recover{
  case EntityNotFoundException(id) => NotFound(id)
  case ex: RepositoryException => InternalServerError
}.getOrElse(InternalServerError)

エンティティが存在しない状況をNoneで表す場合は以下。

trait Repository[ID, E] {
  def resolveBy(id: ID)(implicit ctx: Ctx): Try[Option[E]]
}

val response = repository.resolveBy(id).map{ entityOpt =>
  entityOpt.fold(NotFound(id)(_.toJson)
}.recover{
  case ex: RepositoryException => InternalServerError
}.getOrElse(InternalServerError)

Future型でラップする方法

同期か非同期かという違いですが、Try型のインターフェイエスとほとんど変わりません。

trait Repository[ID, E] {
  def resolveBy(id: ID)(implicit ctx: Ctx): Future[E]
}

val response = repository.resolveBy(id).map{ entity =>
  Ok(entity.toJson)
}.recover{
  case EntityNotFoundException(id) => NotFound(id)
  case ex: RepositoryException => InternalServerError
}.getOrElse(InternalServerError)
trait Repository[ID, E, M[+_]] {
  def resolveBy(id: ID)(implicit ctx: Ctx): Future[Option[E]]
}

val response = repository.resolveBy(id).map{ entityOpt =>
  entityOpt.fold(NotFound(id)(_.toJson)
}.recover{
  case ex: RepositoryException => InternalServerError
}.getOrElse(InternalServerError)

書き込み系メソッド

戻り値をUnitする方法

エンティティを保存できなかった場合は例外がスローされます。引数に渡したエンティティと書き込んだ結果のエンティティが異なる場合はresolveByで再度取得する必要があるかもしれません。副作用を起こすメソッドであり、テスタビリティが下がるのが難点。

trait Repository[ID, E] {
  def store(entity: E)(implicit ctx: Ctx): Unit
}

val response = try {
  repository.store(entity)
  Ok(repository.resolveBy(entity.id))
} catch {
  case ex: RepositoryException => InternalServerError
}

保存されたエンティティを返す方法

値を返すが以前として、副作用を起こすメソッドであり、テスタビリティが下がる。

trait Repository[ID, E] {
  def store(entity: E)(implicit ctx: Ctx): E
}

val response = try {
  Ok(repository.store(entity).toJson)
} catch {
  case ex: RepositoryException => InternalServerError
}

新しいリポジトリインスタンスとエンティティを返す方法

Stateモナドの応用例として、新しいリポジトリインスタンスとエンティティを返すことによって、純粋関数化しテスタビリティを向上させる方法。これによって不変リポジトリが実装可能になる(DBMSなど不変との相性が悪い実装の場合は、可変リポジトリとして、(this, storedEntity) としてUnitを返す変わりにthisを返すとよい)。

trait Repository[ID, E] {
  def store(entity: E)(implicit ctx: Ctx): (Repository[ID, E], E)
}

val response = try {
  val (newRepo, storedEntity) = repository.store(entity))
  // 必要に応じて newRepository.store, resolveBy など
  Ok(storedEntity.toJson)
} catch {
  case ex: RepositoryException => InternalServerError
}

Etiher型にラップする方法

trait Repository[ID, E] {
  def store(entity: E)(implicit ctx: Ctx): Either[Error, (Repository[ID, E], E)]
}

repository.store(entity).fold({
  case ex: RepositoryException => InternalServerError
},{ (newRepo, storedEntity) =>
  // 必要に応じて newRepository.store, resolveBy など
  Ok(storedEntity.toJson)
})

Try型にラップする方法

trait Repository[ID, E] {
  def store(entity: E)(implicit ctx: Ctx): Try[(Repository[ID, E], E)]
}

repository.store(entity).map{ (newRepo, storedEntity) =>
  // 必要に応じて newRepository.store, resolveBy など
  Ok(storedEntity.toJson)
}.recovery{
  case ex: RepositoryException => InternalServerError
}

Future型でラップする方法

trait Repository[ID, E] {
  def store(entity: E)(implicit ctx: Ctx): Future[(Repository[ID, E], E)]
}

repository.store(entity).map{ (newRepo, storedEntity) =>
  // 必要に応じて newRepository.store, resolveBy など
  Ok(storedEntity.toJson)
}.recovery{
  case ex: RepositoryException => InternalServerError
}

などなど、挙げればきりがないのですが…。

Scalaz

Scalazでは以下のようになりますかね。

trait Repository[ID, E] {
  def store(entity: E)(implicit ctx: Ctx): \/[Error, State[Repository[ID, E], E]]
}

そしてFreeモナド

あとは、Freeモナドを使う方法がありそうですね。こっちのがモナドが階層化しないようにできるのでよいかもしれない。当然、インタプリタを書く手間はありますが。

https://gist.github.com/xuwei-k/469a2213c7773274272f

あ、Operationalモナドの方がよいかな?

まとめ

とはいえ、現状 以下を採用しています。

trait Repository[ID, E] {
  def resolveBy(id: ID)(implicit ctx: Ctx): Try[E]
  def resolveBy(id: ID)(implicit ctx: Ctx): Future[E]
  def store(entity: E)(implicit ctx: Ctx): Try[(Repository[ID, E] , E)]
  def store(entity: E)(implicit ctx: Ctx): Future[(Repository[ID, E] , E)]
}

val response = (for {
  (newUserRepo, storedUser) <- userRepository.store(user)
  (newGroupRepo, storedGroup) <- groupRepository.store(group)
} yield {
  Ok(toJson(storedUser, storedGroup))
}).recover{
  case ex: RepositoryException => InternalServerError
}

やはりモナドが入れ子構造になるとfor式やmapなどが書きにくくなるので、できるだけ階層化させないというのと、Try/Futureはシンタックスが似ているので、同期や非同期のコードの切り替えがしやすいからです。まぁ、例外のハンドリングだけ考えるとEitherでもよいかも。

def resolveBy(id: ID)(implicit ctx: Ctx): Try[E] = Try {
  sql"select * from users where id = {id}"
    .bindByName('id -> id)
    .map(convertToEntity)
    .single
    .apply()
    .getOrElse(throw new EntityNotFoundException(id))
}

def resolveBy(id: ID)(implicit ctx: Ctx): Future[E] = {
  val executor = getExecutor(ctx)
  future {
    internalRepository.resolveBy(id).get
  }
}

ServiceとDCIについて

面白そうなネタがあったので、自分なりの考えをまとめてみる。

Ruby/Rails 用 DI コンテナ Dee をつくった、あるいは Ruby のカルチャーについて

この記事はRuby用のDIコンテナの話題なんですが、DCIについても言及されているようです。比較軸はDIそのものというより、サービスとDCIだと思うので、それについてダラダラといくつか考えをまとめてみます。多分返事になるようでならないかも。それと宗教上の都合でDDDの視点から書きます...。

サービスという言葉はあいまい

まず、簡単に前提の整理から。単に"サービス"って言葉が何を指すのか結構曖昧です。
サービスは簡単にいうと手続きとか振る舞いのことですが、細かくいうと、PofEAAでいうサービスと、DDDいうサービスは、目的が異なります。前者はアプリケーションのためにドメインモデルを再利用可能にするためのものです。後者はドメインの知識を表している振る舞いです。これはのちほど詳しく説明します。
まぁこのあたりは具体例がないと理解しがたいですが、レイヤーの違いによって責務が異なるという感じです。DDDのサービスの章では、サービスには、アプリケーション層、ドメイン層、インフラストラクチャ層と、複数のレイヤーに存在すると言及されています。PofEAAのService Layerは、DDDでいうアプリケーション層のサービス(以下 アプリケーションサービス)に相当すると思います。

あわせて読んで欲しいのはこのあたり。

ここではDDDでいうところの、ドメイン層のサービス(以下 ドメインサービス)に焦点を絞って考えてみます。

あと、ここでいうモデルというのはドメインモデルとします。DCIのDataもドメインモデルの前提。

オブジェクトはメンタルモデルを写し取るもの

いきなり話は変わってしまいますが、オブジェクト指向の成り立ちの話を少し

DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien - Digital Romanticism から印象的な下りを紹介。

事実、オブジェクト指向プログラミングにおける先駆者たちの目的は、エンドユーザのメンタルモデルをコードにおいてとらえることだった。

そもそも、オブジェクト指向は、人間のメンタルモデルをシミュレーションするための問題解決手法として登場したので、これは素直に理解できます。源流をたどればAlan KayのDynabook構想とか出てきますね。この記事を読んでもらうとわかりますが、DCIもメンタルモデルに近づくための手法の一つです。

一方、DDDですが、Kent BeckはDDDに対する謝辞でこんなことを綴っています。

「ソフトウェアの設計を、今取り組んでいる問題ドメインのメンタルモデルに適合させるにはどうすればよいか、ということについて、Eric Evansは素晴しい本を警いた。」

あと、DHHは、おすすめ書籍のリストの中でこのように述べています。

Evans’ book, Domain-Driven Design, is great. It offers a mental framework for thinking deeper about the abstraction of object oriented programming.

DDDでも、"ユビキタス言語(DDD P24)"や"モデル駆動設計(DDD P45)"という手法を利用しますが、メンタルモデルを実装に反映するためにあります。

どちらも、オブジェクトはメンタルモデルを写し取るものだというスタンスは一致しているといえます。メンタルモデルを反映するのはドメインモデルなので、やはりメンタルモデルの主戦場はドメイン層だと思います。アプリケーションサービスじゃなくて、ドメインサービスの話を重視するのはこのためです。

続きを読む