かとじゅんの技術日誌

技術の話をするところ

Getter/Setterを避けて役に立つドメインオブジェクトを作る

Clean Architecture 達人に学ぶソフトウェアの構造と設計を読んでます。モデリングに関しては成分薄めですが、よい本だと思います。はい。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

Clean Architecture 達人に学ぶソフトウェアの構造と設計

本書の大筋から少し逸れるが、「5章 オブジェクト指向プログラミング」の「カプセル化」が面白かったので、これを切り口にモデリングについて考えてみる。

続きを読む

DDDリポジトリを楽に実装するライブラリ

DDDのリポジトリを実装するのがダルいので、ライブラリ化したというか前から書いていたけど、Redis, Memcachedなどの実装も追加したので、簡単に説明を書いてみる。 プロジェクトなどで自前で実装する際も、参照実装として参考になると思います。Scalaの例だけど、他の言語でも参考にはなるんじゃないかと勝手に想像してます。

https://github.com/j5ik2o/scala-ddd-base

デフォルトで対応している永続化技術は、以下。

  • JDBC
    • SkinnyORM
    • Slick3
  • NOSQL
    • Memcached
    • Redis
    • Guava Cache
  • Freeモナド
    • こちらは永続化そのものは行いません。上記どれかの実装に委譲することになります。

何が楽になるかというと、上記向けのリポジトリの実装があるので、Daoだけ用意すればリポジトリが実装できるようになります。

中核になるトレイト

中核となる抽象トレイトはこれ。実装はついてないです。

https://github.com/j5ik2o/scala-ddd-base/tree/master/core/src/main/scala/com/github/j5ik2o/dddbase

IOするのはAggregateです。リポジトリによっては実装するメソッドが異なるのでトレイトは細かく分かれています。Mは型コンストラクタです。

trait AggregateSingleReader[M[_]] extends AggregateIO[M] {
  def resolveById(id: IdType): M[AggregateType]
}

SkinnyORM向け実装トレイト

一例としてSkinnyORM向けに実装を提供するトレイトの説明を簡単にします。Mはここでは、ReaderT[Task, DBSession, A]としています。Taskmonix.eval.Taskです。この型は実装ごとに変わります。たとえば、Redis向けの実装では、ReaderT[Task, RedisConnection, A]になります。

実装を提供するトレイトはAggregateIOBaseFeatureを継承しますが、ほとんどのロジックでSkinnyDaoSupport#Daoを実装したオブジェクトに委譲します。つまり、リポジトリを作る場合は、これらのトレイトをリポジトリのクラスにミックスインして、Daoの実装を提供するだけでよいことになります。

object AggregateIOBaseFeature {
  type RIO[A] = ReaderT[Task, DBSession, A]
}

trait AggregateIOBaseFeature extends AggregateIO[RIO] {
  override type IdType <: AggregateLongId
  type RecordType <: SkinnyDaoSupport#Record
  type DaoType <: SkinnyDaoSupport#Dao[RecordType]

  protected val dao: DaoType
}

trait AggregateSingleReadFeature extends AggregateSingleReader[RIO] with AggregateBaseReadFeature {

  override def resolveById(id: IdType): RIO[AggregateType] =
    for {
      record <- ReaderT[Task, DBSession, RecordType] { implicit dbSession: DBSession =>
        Task {
          dao.findBy(byCondition(id)).getOrElse(throw AggregateNotFoundException(id))
        }
      }
      aggregate <- convertToAggregate(record)
    } yield aggregate

}

実装サンプル

実装する際は、Aggregate*Featureのトレイトをミックスしてください(UserAccountRepositoryは実装を持たない抽象型です)。あとはDaoの実装の提供だけです。以下の例では、UserAccountComponentUserAccountRecord, UserAccountDaoを提供します。

object UserAccountRepository {
  type BySlick[A] = Task[A]
  def bySkinny: UserAccountRepository[BySkinny] = new UserAccountRepositoryBySkinny
}

class UserAccountRepositoryBySkinny
    extends UserAccountRepository[BySkinny]
    with AggregateSingleReadFeature
    with AggregateSingleWriteFeature
    with AggregateMultiReadFeature
    with AggregateMultiWriteFeature
    with AggregateSingleSoftDeleteFeature
    with AggregateMultiSoftDeleteFeature
    with UserAccountComponent {

  override type RecordType = UserAccountRecord
  override type DaoType    = UserAccountDao.type
  override protected val dao: UserAccountDao.type = UserAccountDao

  override protected def convertToAggregate: UserAccountRecord => RIO[UserAccount] = { record =>
    ReaderT { _ => // このメソッド内部でDBを利用したり、非同期タスクを実行する可能性もあるので、RIO形式を取っている
      Task.pure {
        UserAccount(
          id = UserAccountId(record.id),
          status = Status.withName(record.status),
          emailAddress = EmailAddress(record.email),
          password = HashedPassword(record.password),
          firstName = record.firstName,
          lastName = record.lastName,
          createdAt = record.createdAt,
          updatedAt = record.updatedAt
        )
      }
    }
  }

  override protected def convertToRecord: UserAccount => RIO[UserAccountRecord] = { aggregate =>
    ReaderT { _ =>
      Task.pure {
        UserAccountRecord(
          id = aggregate.id.value,
          status = aggregate.status.entryName,
          email = aggregate.emailAddress.value,
          password = aggregate.password.value,
          firstName = aggregate.firstName,
          lastName = aggregate.lastName,
          createdAt = aggregate.createdAt,
          updatedAt = aggregate.updatedAt
        )
      }
    }
  }

}

DaoはSkinnyDaoSupport#Daoを実装する形式になります。これはSkinnyCRUDMapperの派生型です。 僕の場合は、ここで紹介した方法でスキーマから自動生成しています。

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)

    }

  }

}

利用するときは、以下のような感じでリポジトリが返すReaderT[Task, DBSession, UserAccount]#runDBSessionを渡すとTaskが返ってきます。 それをrunAsyncするとFuture[UserAccount]が取得できます。他の実装でもほとんど同じように使えます。ご参考までに。

val resultFuture: Future[UserAccount] = (for {
  _ <- repository.store(userAccount)
  r <- repository.resolveById(userAccount.id)
} yield r).run(AutoSession).runAsync

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が必要になったのかは、別のエントリで書く予定。