かとじゅんの技術日誌

技術の話をするところ

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

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

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

サービスという言葉はあいまい まず、簡単に前提の整理から。単に"サービス"って言葉が何を指すのか結構曖昧です。 サービスは簡単にいうと手続きとか振る舞いのことですが、細かくいうと、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)"という手法を利用しますが、メンタルモデルを実装に反映するためにあります。

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

続きを読む

ScalaでのDCIの実装を考える

みなさん、こんばんわ。

会社のアドベントカレンダーで、Scalaコードでわかった気になるDDDというブログを書いたのですが、最近、老害を防ぐためにDCIについても勉強中です。

DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien

とりあえず、これを読めということらしいですが、今ひとつ理解できなかったので、

Lean Architecture: for Agile Software Development

を買って読んでます(巻末にScalaのコード例もあってなかなかよさげです)。

この本ではtraitのmix-in方式を紹介しているのですが、この方法はイマイチだと思っているので、別の方法を考えてみたのでさくっと紹介します。

続きを読む

ドメインモデルの関連を表現するには

(Scala前提の記事なので注意してください)

たとえばこんなモデルがあって、相互に依存しているケースを考えよう。

注意:説明を簡単にするために、varを利用しています。

従業員

class Employee(
  val id: Long,
  val name: String,
  var department: Option[Department] = None
)

部署

class department(
  val id: Long,
  val name: String,
  var employees: Seq[Employee] = Seq.empty
)

利用例

val employee = new Employee(1, "KATO")
val department = new Department(1, "Dev")

employee.department = Some(Department) // (1)
department.employees = Department.employees + employee // (2)

加藤という従業員が開発部に所属する状態を表しています。 これの何が問題かってわかりますか?

続きを読む

シナリオ -> モデル -> コード ->

昨日もDDDの話題を少ししたので、シナリオ→モデル→コードのサイクルについて身近な例を踏まえてネタを提供できないかと思った。何でもいいんだけど、鍼とか整体とかマッサージとか一度は行った経験あると思うので、そのドメインで考えてみるか。

続きを読む