「ドメイン駆動設計 モデリング/実装ガイド」を読んだ

little-hands.booth.pm

読んだきっかけ

自分の書いているコードに対してある程度「これで問題ない」という確信が持てない状態がそれなりにストレスになると気付いたので読んだ。

今の職場に来てから、新機能の内部設計および開発を一任されるなど、以前よりも粒度の大きなタスクを担当することが増え、それに伴ってコードを書く量も増えた*1。 今まであまり本腰を入れて設計を勉強してこなかったこともあり、動きはするしテストもあるが複雑で保守性のよくなさそうなコードができあがりがちだった。 今にして思えば、アプリケーション層にドメインオブジェクトが持つべき知識が記述されドメインモデル貧血症を起こしていたし、アプリケーション層で外部APIにアクセスするクライアントやレスポンスのパースを行うクラスまで実装してたので、アプリケーション層がごちゃごちゃになっていた。 何度かリファクタを行うが、そもそもあるべき状態がわからないので根本的な解決もできなかった。 そうこうしている内に自分の書いたコードが全くもって駄目だという事実を突き付けられた日はどっと疲れが溜まっていることに気付き、本腰を入れて設計と向き合うことにした。

ただ、保守性の高いコードを書くというのはDDDのもたらすものの本質ではなかったのだが...。

なぜDDDか?

今回DDDに着目したのは、現在携わっているプロジェクトが

  • Play FrameworkがDIコンテナを採用しているためDBアクセスだけはリポジトリとして定義されている
  • プロジェクトのディレクトリ構成を見るとルート直下にdomain, infrastructureというディレクトリがある
  • domainの中に(何も表明やメソッドが定義されていないものの)ドメインオブジェクトになるべきデータクラスが定義されている

など、戦術的設計をいくらか実践しようとした痕跡があり、ここから改めて戦術的設計を導入していくことは全くの不可能ではないと感じたのが大きい。 また、アプリケーションが扱っているドメインも十分複雑性があり、リターンを十分得られないということも無いだろうと考えた。

なお、私はドメインエキスパートを巻き込み、チーム全体がユビキタス言語で会話し始めるまでは軽量DDDだと思っているが、残念ながらその状態への道のりは遠いと言わざるを得ず、まずは軽量DDDを目指していくことになる。

ドメイン駆動設計入門」と比較して

DDDに関しては以下の本を過去に読んだことがあった。

www.shoeisha.co.jp

しかし、途中で今説明されている内容がDDDの全体の中でどのような位置づけになる話なのかということが脳内で整理できず、読み進めてもいまいち知識が身についていないように感じ、積読してしまった。 「ドメイン駆動設計 モデリング/実装ガイド」はこの本とは逆にDDDに登場する概念をざっくり見ていく本で、こちらを読んだあと「ドメイン駆動設計入門」に戻ることで内容がより整理された形で頭に入ってきているのを感じている。 加えて、後述する理由で実際のプロジェクトに戦術的設計を適用していきやすくなるため、私としては「ドメイン駆動設計 モデリング/実装ガイド」から読むのを勧める。

感想

ドメインモデルとは何か、DDDはドメインについて詳しい人と話しドメインモデルを改良し続けていく手法であること、そのためにドメイン層は他のレイヤーに依存してはいけないこと、各レイヤーの役割とその中に何が含まれるかがよくわかる本だった。 「ドメイン駆動設計入門」についての部分で書いたように最初に各レイヤーの役割について押さえておくことは、その後DDDの学習を続けていく際に「今どの部分の話をしているのか」を意識して頭の中に配置していけるようになることにつながる。

また、この本を読んでからコーディングしていて気付いたが、各レイヤーの役割を知ることで「何をこのレイヤーに入れ、何を入れないか」という判断基準を自分の中に構築することができるという点も大きい。

たとえば、ScalaRDBとやりとりを行うためのライブラリの1つにScalikeJDBCというものがある。 ScalikeJDBCでは、たとえば以下のようなカラムを持つusersテーブルがあったとき(データ型は省略)

  • id
  • name
  • email
  • created_at

以下のようなクラスやオブジェクトをScala側で定義することでSQLを書くためのDSLがusersテーブルに対して使えるようになる。

import scalikejdbc.SQLSyntaxSupport

case class User(id: Long, name: String, email: String, createdAt: LocalDateTime)

object User extends SQLSyntaxSupport[User] {
  override val tableName = "users"
}

ではこのUserクラスとコンパニオンオブジェクトはどのレイヤーに置くべきなのか? 私が現在携わっているプロジェクトではドメイン層にいずれも定義されていたのだが、ドメイン層のコードが特定のライブラリに依存しているのはおかしいので、コンパニオンオブジェクトの方のUserは明らかにドメイン層に置いてはいけないとわかる。 DBとのやりとりに関係するオブジェクトなので恐らくインフラストラクチャ層に置くのが正しいかと思われる。 ではUserクラスについてはドメイン層に置いても問題無いか? 仮にこのUserクラスをドメインオブジェクトとして流用した場合、ドメインオブジェクトがusersテーブルの構造に依存してしまう。 なので、このUserクラスはドメインオブジェクトではなく、あくまでusersテーブルのレコードを表すクラスとしてインフラストラクチャ層に置くのが望ましいのではないか?

このようなことを「ドメイン駆動設計 モデリング/実装ガイド」を読んでから考えられるようになった。 実際にプロジェクトに対して戦術的設計を適用していく際に逐一誰かに聞くというのはあまり現実的ではないので、これは私の中でかなり大きかった。 自分の中にこういった判断基準を構築するには、ボトムアップよりもトップダウンな入門書の方が向いているのではないかと思う。 この点においてはDDD入門書としてよく挙げられる「ドメイン駆動設計入門」よりもこの本は優れている。

内容もスマートにまとまっており数日もあれば読破できるので、総じて、DDD入門の最初の1冊としてよい本ではないかと感じた。

*1:主に使う言語がRuby/RailsからScala/Play Frameworkに変わり、どうしても書かなければいけないものが増えたという事情もある

Play Frameworkのテスト実行中にDBのコネクションが枯渇する

概要

  • Play Frameworkのテストコードでplay.api.Applicationインスタンスを生成したら必ずplay.api.Play.stop()に渡して終了させる必要がある。
  • そうしなかった場合、コネクションプールも破棄されないので、やがてDBのコネクションが枯渇しテストが失敗するようになる。
  • GuiceOneAppPerSuiteなどのtraitを正しく使うか、そうでなければPlay.stop()Applicationインスタンスを渡す必要がある。

背景

私はScala/Play Frameworkを使ったアプリケーションの開発に携わっていますが、そのアプリケーションはこれまでテストがあまり書かれてこなかったため、少しずつ健全な状態に戻していきたいと思いました。最初に手を付けたのが以下の2点でした。

  • GitHubにpushした際、prを出した際にテストが実行されるようCIを整備する
  • 現在失敗するテストを全て修復する

ところが、sbt testを実行すると途中までは正常に実行されるのですが、いつも同じところでDBのコネクションが枯渇し、その後のテストが全て落ちてしまい、テストコードの大半が機能しなくなっていました。 おま環の可能性も考えましたが、試しにCIを構築してテストを回しても結果は同じでした。

Play Frameworkのコネクションプールの動作

Play Frameworkの公式ドキュメントでDBに関するconfigについて確認すると、play.db.prototype.pool, play.db.poolに何も指定していない場合はPlay Framework2.8ではHikariCPがデフォルトで使用されることが分かります。*1

また、HikariCPはコネクションプールが生成された瞬間play.db.hikaricp.maximumPoolSizeで指定した数だけDBとのコネクションを確立します。*2

Applicationのインスタンスが生成されると同時にコネクションプールも生成されるため、Applicationのインスタンスを作りっぱなしで放置するとsbt test実行中コネクションプールが残り続け、途中でDB側のコネクション数の上限に引っかかってしまうので新しいコネクションプールを生成できなくなり、それに伴ってApplicationのインスタンスの生成にも失敗するようになりエラーが発生するようになります。

対策

GuiceOneAppPerSuiteなど用意されたtraitの枠組みに乗っかる

play.api.Play.stop()にApplicationのインスタンスが確実に渡されるようにすれば解消します。

例として、ScalaTest PlusとGuiceを使っている場合は、GuiceOneAppPerSuiteというtraitをmixinするのが手っ取り早いです。 当然テストクラスにGuiceOneAppPerSuiteをmixinするだけで勝手にApplicationをstop()してくれるわけではなく、正しい方法でtraitのインスタンス変数やメソッドをoverrideする必要があります。

GuiceOneAppPerSuiteBaseOneAppPerSuiteGuiceFakeApplicationFactoryを合成したtraitになっています。 BaseOneAppPerSuite.run()の処理を確認すると

  1. GuiceFakeApplicationFactory.fakeApplication()が返すインスタンスインスタンス変数appに格納される
  2. appに入ったApplicationのインスタンスPlay.start()に渡される
  3. テストが実行される
  4. appに入ったApplicationのインスタンスPlay.stop()に渡される

という流れになっています。 つまり、GuiceOneAppPerSuiteを正しく機能させるには以下のどちらかが必要になります。

  • fakeApplication()をoverrideしてテストに実際に使いたいApplicationのインスタンスを返すよう実装する
  • appをoverrideしてfakeApplication()から取得したインスタンスを格納するのではなくテストに使うインスタンスを格納するよう実装する

GuiceFakeAppicationFactoryのコメントを読む限りではfakeApplication()をoverrideすることを想定していそうなので、私はそのようにしています。 基本はGuiceOneAppPerSuiteなど用意された仕組みで何とかすると良いでしょう。

テストケースごとにModuleをoverrideしたい場合

テストケースごとにDIで注入するオブジェクトを変更したい場合については、どうやらそのためのtraitは用意されておらず、try-finallyのfinallyブロックでPlay.stop()Applicationインスタンスをを渡してやる他無さそうでした。 しばしば使う処理だったので、私はApplication => Unit型のテストを行う処理を受け取るloan patternのメソッドをテスト関連のUtilityに追加しました。

def withApplication(overrideModules: GuiceableModule*)(block: Application => Unit): Unit = {
  val app = new GuiceApplicationBuilder().overrides(overrideModules: _*).build()

  Play.start(app)
  
  try {
    block(app)
  } finally {
    Play.stop(app)
  }
}

for-comprehensionをうまく使うために必要そうだと思ったこと

新しい仕事に入り、1週間が経過しました。 私としては有給消化中にコップ本を読んでおけば事前準備としてはよくやった方ではないかと思っていたのですが、Play FrameworkやDBとのやりとりに使っているScalikeJDBCなど、新しく知るべきことはたくさんあり、逆にコップ本すら読んでいなかったらどれほどの混乱が待ち受けていたのか、少し怖くなりました。

一方で、for-comprehensionはHaskellのdo notation同様、複数のOption, Either値を扱う際、ネストしたパターンマッチングの平坦化などにおいて強力な武器になるすばらしい構文だと感じています。 しかし、私の職場ではどうもうまく扱えていないケースが多いと感じたので原因を考えたところ、以下のような部分に原因があるのではないかと思いました。

エラーの原因が分かるように型を設計していない

たとえば、Web APIを提供するある外部システムに対して、2回連続でリクエストを送るとします。 2回目のリクエストは、1回目のリクエストが正常に処理された場合のみ行うものとします。

sealed abstract class ApiError
case class ApiError1(reason: String) extends ApiError
case class ApiError2(reason: String) extends ApiError

def requestA(): Either[ApiError, String] = ???
def requestB(): Either[ApiError, String] = ???

for-comprehensionを使わずに書くと大体このようになるでしょう。

requestA() match {
  case Left(ApiError1(reason)) => println(s"requestAが失敗しました ${reason}")
  case Right(resultA) =>
    requestB() match {
      case Left(reason) => println(s"requestBが失敗しました ${reason}")
      case Right(resultB) => {
        println("成功しました")
        println(resultA)
        println(resultB)
      }
  }
}

次に、for-comprehensionを使うとこうなります。

val wholeResult = for {
  resultA <- requestA()
  resultB <- requestB()
} yield (resultA, resultB)

wholeResult match {
  case Left(ApiError1(reason)) => println(s"requestAが失敗しました ${reason}")
  case Left(ApiError2(reason)) => println(s"requestBが失敗しました ${reason}")
  case Right((resultA, resultB)) => {
    println("成功しました")
    println(resultA)
    println(resultB)
  }
}

途中でLeft値が返ってきた場合、for-comprehensionがその時点でLeft値を返し、最後まで成功した時のみRight値が返ってくるので、最後にfor-comprehensionが返した値に対して一度だけパターンマッチングを書いてハンドリングをすればよくなります。 こちらの書き方の方が、リクエストとそれに対するハンドリングが分離していて読みやすいと思います。

しかし、たとえばrequestA()requestB()がOption型で失敗を表していたとしたらどうでしょう。

def requestA(): Option[String] = ???
def requestB(): Option[String] = ???

val wholeResult = for {
  resultA <- requestA()
  resultB <- requestB()
} yield (resultA, resultB)

wholeResult match {
  case None => // このパターンにマッチした時requestAが失敗したのかrequestBが失敗したのかわからない!
  case Some((resultA, resultB)) => {
    println("成功しました")
    println(resultA)
    println(resultB)
  }
}

このように、for-comprehensionが返した値に対してハンドリングすればいいというわけには行かなくなってしまいます。 ではどうするのかというと、最初にやったように、requestA()requestB()が返した値を逐一パターンマッチングし、Noneが返ってきたタイミングによってハンドリングの内容を決定するというやり方になってしまいます。 最終的に得られる値ではなく、Noneが返ってくるタイミングにハンドリングが委ねられてしまうんですね。

requestA() match {
  case None => println(s"requestAが失敗しました")
  case Some(resultA) =>
    requestB() match {
      case None => println(s"requestBが失敗しました")
      case Some(resultB) => {
        println("成功しました")
        println(resultA)
        println(resultB)
      }
  }
}

さすがにAPIの成否を、エラーメッセージを返せないOption型で表すことはあまり無いと思いますが、私が見た中にこのようにエラーを表現しているケースがありました。

case class ApiError(errorNo: Int, message: String)

この型ですと、2つ以上のエラー原因に対して同じエラーNoが割り当てられていたり、もっとよくないケースとしてはAPIが返してくるエラーNoが仕様書に残されていなかったりすると、結局はLeft値が返ってきたタイミングでエラーハンドリングするしか無くなってしまいます。

結局、私は以下のようなApiError値をラップする型を作り、ApiErrorを返してくる関連するメソッドの処理でこれら型の値を返すようにすることで解決しました。

case class ApiError(errorNo: Int, message: String)

sealed abstract class RequestError
case class RequestAFailed(error: ApiError) extends RequestError
case class RequestBFailed(error: ApiError) extends RequestError

def requestA(): Either[RequestError, String] = ???
def requestB(): Either[RequestError, String] = ???

高階型に包まれていない値をメソッドの引数にしていない

mapfmapの型を見ていると、OptionEitherなどの高階型に包まれていない値を引数としてとる関数を書いていった方が、for-comprehensionをうまく使えるのではないかと思いました。

for-comprehensionはmapflatMap, withFilterの構文糖ですが、それぞれをC[A]型の値のメソッドとして引数と返り値の型を見てみると、

  • map(A => B) => C[B]
  • flatMap(A => C[B]) => C[B]
  • withFilter(A => Boolean) => C[A]

となります。 引数に渡している関数を見ると、どれもC[A]型ではなくA型の値を引数に取ることがわかります。 なので、高階型に包まれていないA型の値を引数にするようなメソッドを積極的に定義することで、for-comprehensionも使いやすくなってくるのではないかと感じました。

他にも原因があると思いますが、しばらくこの2つを意識するとどう変わるか様子を見てみようと思います。

ドワンゴ社のScala研修テキストを読む前に知っておきたかった高階関数への関数オブジェクトの渡し方のパターン

Scalaでは関数リテラルを様々な形で書けますが、入門向けの資料であるドワンゴ社のScala研修テキストでは具体的にどのような書き方ができるのかは解説されていません。 にも関わらずそれぞれの書き方が説明なしに登場するので「高階関数に渡す関数の書き方はどれくらいあるんだろう?」ともやもやしたまま進まざるを得ないのが玉にきずでした。 しかし、コップ本を読んでいてもやもやも解消されました。

最も基本的な関数リテラルの書き方

JavaScriptの匿名関数と同じようなフォーマットで書くことができます。

scala> List(1, 2, 3, 4, 5).filter((x) => x % 2 == 0)
res0: List[Int] = List(2, 4)

関数の引数が1つである場合は引数を囲む括弧を省略できるのも同様です。

scala> List(1, 2, 3, 4, 5).filter(x => x % 2 == 0)
res1: List[Int] = List(2, 4)

プレースホルダー構文

次に、それぞれのパラメータが関数リテラル内で1度しか使われない場合はプレースホルダー構文と呼ばれるものが使えます。 たとえば(x => x % 2 == 0)の中で引数xは1度しか使われていないので

scala> List(1, 2, 3, 4, 5).filter(_ % 2 == 0)
res2: List[Int] = List(2, 4)

と書けます。 プレースホルダーである_に引数の値が入ってくるイメージです。

プレースホルダー構文で2つ以上_を使った場合、2つ目、3つ目...の引数がそこに入ります。 たとえば、畳み込み演算を行うメソッドの1つであるreduceLeftに渡す関数をプレースホルダー構文を使わず書くとこうなります。

scala> List(1, 2, 3, 4, 5).reduceLeft((accumulator, x) => accumulator + x)
res4: Int = 15

プレースホルダー構文を使うとこうです。

scala> List(1, 2, 3, 4, 5).reduceLeft(_ + _)
res3: Int = 15

1つ目の_accumulator, 2つ目の_xの代わりになっていますね。 このように、プレースホルダー構文で_を複数回使うと、それらは別の引数として扱われます。 なので、同じ引数を2回参照することはできません。

部分適用

Scalaなどの関数型言語では、必要な引数を渡して関数を呼び出すことを、「引数に関数を適用する」と言うことがよくあります。 また、そのような関数では関数が必要とする引数のうち一部、あるいは全部を渡さずに関数を呼び出し、残りの引数がすべて渡された時点で関数の適用を行うことができます。 関数に引数のうちの一部を渡して「残りの引数を受け取って結果を返す関数」を得るとも考えられますね。 これを関数の部分適用と呼びます。

実際に関数に引数のうち一部を渡す際は、渡したくない引数を_にして渡します。

scala> def plus(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> val f = sum(1, _, 3)
f: Int => Int = $$Lambda$974/0x00000008006df040@722d3ddb

scala> f(2)
res0: Int = 6

一方で、関数の引数のうち全てを渡さない場合は、以下のように関数の後ろにアンダースコアを置きます。

scala> val f = sum _
f: (Int, Int, Int) => Int = $$Lambda$982/0x00000008006e3840@6971c6b7

scala> f(1, 2, 3)
res0: Int = 6

そして、このように引数を全て渡さない部分適用を行う場合、コードのその位置が関数呼び出しを必要とする位置であれば、_を省略して書くこともできます。

scala> List(1, 2, 3, 4, 5).foreach(println)
1
2
3
4
5

Scala研修テキストをやっていた段階では関数を直接引数に渡しているのかと思っていましたが、実態は部分適用を使って関数オブジェクトを得るという段階を挟んでいたんですね。

ちなみに、関数が直ちに呼び出される以外の場所で関数の引数を全て渡さず部分適用する場合に_をつける必要があるのは、本当は関数を呼び出したいのに誤って引数を渡さなかった場合にコンパイルエラーによって気付くことができるようにしたい、という意図のようです。

引数を{}で囲って渡す

関数オブジェクトを得るのとはまた違った話ですが、関連する内容として書いておきます。 Scalaでは引数を1個だけ渡すメソッドに対し引数を渡す時、()ではなく{}で囲んで渡すことができます。

scala> println("Hello, World!")
Hello, World!

scala> println { "Hello, World!" }
Hello, World!

2つ以上の引数を渡す場合はできません。

scala> def sum(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> sum { 1, 2, 3 }
              ^
       error: ';' expected but ',' found.

この構文を使うと、高階関数を受け取って新しい制御構造を作った際に少し組み込みの制御構造にコードの見た目を似せることができます。 細かい点ですがよく考えられているなと感じました。

object Example extends App {
  def notifyStartAndEnd(process: () => Unit): Unit = {
    println("開始します")
    process()
    println("終了しました")
  }

  // これでも問題無いが...
  notifyStartAndEnd(() => {
    print("名前を入力してください:")
    val name = scala.io.StdIn.readLine()
    println(s"こんにちは、$name!")
  })

  // より制御構造に近い見た目になる
  notifyStartAndEnd { () =>
    print("名前を入力してください:")
    val name = scala.io.StdIn.readLine()
    println(s"こんにちは、$name!")
  }
}