ScalaDay 7

ドメイン駆動設計と関数プログラミングをScalaで

More than 1 year has passed since last update.


はじめに

この記事は Scala Advent Calendar 2016 - Qiita の 7 日目の記事です。

私事で恐縮ですが、今年の個人的なハイライトは、遅まきながらドメイン駆動設計と関数プログラミングを学べたことでして、その中でも、"Functional and Reactive Domain Modeling" (Debasish Ghosh 著) (以下、FRDM本) を読んで、ふたつのコンセプトが自分の中で繋がったことが最も大きなものでした。

Manning | Functional and Reactive Domain Modeling

この記事では、本書の紹介も兼ねて、ドメイン駆動設計がどのように関数プログラミングと結びつくのか、非常にざっくりではありますが、Scala で表現してみたいと思います (ちなみに、私の Scala 歴は3ヶ月ほどですので、サンプルコードに間違いや改善点がありましたら、コメントいただけるとありがたいです)。


環境


  • Scala 2.11.8

  • Scalaz 7.2.7

  • Play 2.5.10


ドメイン駆動設計について

Eric Evans による、2003年に出版された "Domain-Driven Design" (以下、DDD本)という書籍に基づくソフトウェア設計手法です。

詳しい解説は省きますが、主な特徴は、


  • ユビキタス言語を用いて定義されるドメインモデル

  • モデル駆動設計とモデルに一致した実装

  • ドメインを他と分離するためのレイヤー化アーキテクチャ

  • エンティティ、値オブジェクト、サービスといったデザインパターン

といったところです。

用語についてはこちらも参照してください。

ドメイン駆動設計の用語と解説(抜粋版) - Qiita


関数プログラミングについて

私は「関数プログラミング入門」と「Scala関数型デザイン&プログラミング ― Scalazコントリビューターによる関数型徹底ガイド」いう本で学んでいます (と、もちろん、FRDM本も)。

特徴として理解しているのは、


  • 副作用フリーで参照透過な関数

  • 代数的データ型

  • 不変性

あたりです。

DDD本にも、「値オブジェクトは不変である」という記述や、「副作用のない関数」という節があり、キーワードレベルでも共通部分がありますね。


Scala について

関数型とオブジェクト指向のマルチパラダイムな言語で、JVM 上で動作します。

Scala では、var と val キーワードによって、変数の可変/不変を区別します。また、Set や Map といった一部のコレクションクラスには可変なクラスと不変なクラスがありますが、デフォルトでは不変のクラスが使われます。

scala> Set(1,2,3)

res1: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> import scala.collection.mutable
import scala.collection.mutable

scala> mutable.Set(1,2,3)
res2: scala.collection.mutable.Set[Int] = Set(1, 2, 3)

(mutable プレフィックスを指定しないと自動的に immutable パッケージの Set を使っているのが分かります)

package, package object, trait, class, case class, object, case object といったモジュールに関するキーワードも豊富で、Java にもある final の他、sealed という継承を制限する修飾子もあり、Java に比べると覚えることが多い印象です。


FRDM本におけるドメイン駆動設計に沿った実装例

以下、FRDM本に載っているサンプルコード (銀行口座のアプリケーション) から抜粋して (一部改変しています)、ドメイン駆動設計と Scala によるファンクショナルな実装の例を紹介します。


値オブジェクト

プリミティブな (BigDecimalがプリミティブかどうかはさておき、数値型、ということで) データ型をラップした残高クラスの例です。


Balance.scala

package models

case class Balance(amount: BigDecimal = 0)


引数の val/var を省略するとデフォルトで val になるため、外部から Balance.amount を変更することはできず、イミュータブルな値オブジェクトになります。

値オブジェクトに関しては、Scala で自然に実装できます。


エンティティ

インスタンスが識別可能なクラス。入金と出金の責務を持つ口座クラスが例示されています。


Account.scala

package models

import java.util.Date

case class Account(no: String, name: String, dateOfOpening: Date, balance: Balance = Balance()) {

def debit(amount: BigDecimal): Try[Account] = {
if (balance.amount < amount)
Failure(new Exception("Insufficient balance in account"))
else
Success(new Account(no, name, dateOfOpening, Balance(balance.amount - amount)))
}

def credit(amount: BigDecimal): Try[Account] = Success(new Account(no, name, dateOfOpening, Balance(balance.amount + amount)))
}


例では口座番号は String 型でしたが、AccountNumber といった型をつくってもいいかもしれません。

2016-12-07 11:00 追記

また、Account を new している箇所は、以下のようにコピーして返すこともできます ( @FScoward さんからアドバイスいただきました。また本書にも copy を使ったコード例も載っております)。

Failure(this.copy(balance = Balance(balance.amount - amount)))

Success(this.copy(balance = Balance(balance.amount + amount)))

そもそも Account は case class なので new する必要ないというのと、コードの重複を避けるというためにも、より実用的な copy を使う方がいいですね (例ではオブジェクトの不変性を分かりやすくするために new のバージョンをそのまま掲載しておきます)。

追記ここまで

オブジェクト指向プログラミングでは、口座に対する入金処理や出金処理の場合、口座オブジェクトの残高プロパティを変更するのが一般的かと思いますが、関数プログラミングでは、上の例でも分かるように、debit メソッドにしても credit メソッドにしても、新しく Account オブジェクトを生成しています。また、Try, Success, Failure というモナディックな型を使ってエラーハンドリングしているところは Scala の特徴です。

呼び出し側はこんなかんじになります (Play のコントローラーから呼び出してみました)。


AccountController.scala

  def index = Action { implicit request =>

def today = Calendar.getInstance().getTime

val account = new Account("a1", "John", today)
val operatedAccount: Try[Account] = for {
creditedAccount <- account.credit(100)
debitedAccount <- creditedAccount.debit(20)
} yield debitedAccount

operatedAccount match {
case Success(a) => Ok(views.html.index.render(account, a))
case Failure(e) => BadRequest(e.toString)
}
}


オブジェクト指向に慣れていると、処理のたびに複製される Account のインスタンスが気になってしまうかもしれません。

実際には、一度に入金して出金する、という処理にはならないでしょうから、Account オブジェクトはもう少し少なくなるでしょうが、それでも、毎度毎度インスタンス化されるのはどうなんでしょう。

この点に関しては、私の見た限り、本書では言及されていませんでした。そういうもんだと割り切るべきでしょうか。

ちなみに、DDD本には、


オブジェクトを変更する代わりに、処理の結果を表す新しい値オブジェクトが生成されて戻される。(略) 値オブジェクトはライフサイクルが慎重に規定されるエンティティとは異なるのだ。


という記述があり、これまで見てきたように、メソッド呼び出しの度に Account エンティティが新たに生成されて戻される、というのは、Eric Evans の言う「ライフサイクルが慎重に規定される」という点において、DDD本におけるエンティティの定義とは異なっているように思えます。


サービス

DDD本では、サービスの説明として、


サービスとは、モデルにおいて独立したインタフェースとして提供される操作で、エンティティと値オブジェクトのようには状態をカプセル化しない。


とあります。つまり、サービスは、状態を持たない操作の集合であると言えると思うので、これも Scala でファンクショナルに実装できそうです。


AccountService.scala

package services

import scala.util.Try
import models.Account

sealed trait AccountService {
def transfer(from: Account, to: Account, amount: BigDecimal): Try[(Account, Account)] = for {
debited <- from.debit(amount)
credited <- to.credit(amount)
} yield (debited, credited)
}

object AccountService extends AccountService


transfer メソッドを呼び出してみます。


AccountController.scala

  def index = Action { implicit request =>

def today = Calendar.getInstance().getTime

val from = new Account("a1", "John", today, Balance(100))
val to = new Account("a2", "Jane", today, Balance(100))
val transferredAccounts = AccountService.transfer(from, to, 30)

transferredAccounts match {
case Success(t) => Ok(views.html.index.render(t))
case Failure(e) => BadRequest(e.toString)
}
}


transferredAccounts は、Success であれば (from, to) のタプルです。


状態を伴う計算について

モナドに関しては、分からないこともまだ多いんですが、関数プログラミングと題しておきながら触れないわけにはいかないと思いましたので、取り上げてみます。

FRDM本には、Scalaz の State を使って、複数の口座に対して、複数の入出金を適用していく関数が載っています。

先に呼び出し側のコードを示します (本では、バッチで実行される処理を想定しているようですが、すぐに結果を見たかったので Play のコントローラーで実行してみました)。


AccountController.scala

  def index = Action { implicit request =>

val balances: BS = Map(
"a1" -> Balance(),
"a2" -> Balance(),
"a3" -> Balance(),
"a4" -> Balance(),
"a5" -> Balance()
)

val transactions: List[Transaction] = List(
Transaction("a1", BigDecimal(100)),
Transaction("a2", BigDecimal(100)),
Transaction("a1", BigDecimal(-500)),
Transaction("a3", BigDecimal(100)),
Transaction("a2", BigDecimal(200))
)

val updatedBalances = TransactionService.updateBalances(transactions) run balances

Ok(views.html.index.render(updatedBalances._1))
}


a1 から a5 までの口座に対して、その日に実行されたトランザクションによる入出金を適用して残高を更新する処理です。

後述しますが、TransactionService.updateBalances(transactions) run balances の戻り値は (BS, Unit) のタプルなので、テンプレートに渡すときに ._1 で取り出しています。

2016-12-08 9:30 追記


TransactionService.updateBalances(transactions) run balances の戻り値は (BS, Unit)


@kazzna さんから指摘をいただきました。State の結果を捨てるのであれば、run の代わりに exec を使うと、BS をそのまま取り出せます (戻り値は BS になる) ので、今回の例では exec の方がよさそうですね。ただ、FRDM本では run を使っていたのでそのまま載せておきます。

追記ここまで

サービスのコードはこちらです。


TransactionService.scala

package services

import scalaz._
import scalaz.State._
import models.{Balance, Monoid}

object TransactionService {

type AccountNo = String
type BS = Map[AccountNo, Balance]

case class Transaction(accountNo: AccountNo, amount: BigDecimal)

def updateBalances(transactions: List[Transaction]) = modify { (balances: BS) =>
transactions.foldLeft(balances) { (acc, tran) =>
implicitly[Monoid[BS]].op(acc, Map(tran.accountNo -> Balance(tran.amount)))
}
}
}


Monoid のコードは以下を参照してください。

frdomain/Monoid.scala at master · debasishg/frdomain · GitHub

パッと見、何をやっているのかよく分かりませんが(笑)、Transaction のリストを畳み込みながら、Balance を更新していっているのは何となく分かります。

modify メソッドは以下で定義されているように、State モナドが内包している型 (上記例では BS) に対して、関数を適用するだけの関数です。

scalaz/State.scala at series/7.2.x · scalaz/scalaz · GitHub

このようなやり方の何が嬉しいのか、という点について、FRDM本では以下のように述べています。


State helps you carry your model state, so that you don’t have to do it in your application code. It’s all about delegating the plumbing logic to the monad itself. And it does this in a completely generic manner so that you can reuse the State monad and its combinators for managing any kind of state.


(意訳)

「State を使うと、モデルの状態を管理しやすくなり、状態を更新するロジックをモナドに閉じ込めておくことができます。State モナドとコンビネータはどんな状態でも管理できるので、再利用できます。」

たしかに、上記の updateBalances には、状態を変更するロジックはありません。

エンティティのところでも書きましたが、関数プログラミングでは、状態を保持する代わりに、関数を適用した別のインスタンスをつくることになりますが、それは無用のものであり、不具合の温床ともなりうるので、それを排除した書き方ができるのなら、積極的に使っていきたいです。


まとめ

まだ少し曖昧な部分もありますが、値オブジェクト、エンティティ、サービスの実装例を見てきて、ドメイン駆動設計のモデルを関数型のパラダイムで表現することは、とても自然に感じられました。

DDD本にも、


モデル駆動設計がオブジェクト指向である必要はないが、(中略) モデルの構成要素が表現豊かに実装されることにかかっているのは確かだ。


とありますが、Scala および関数プログラミングの豊かな表現力を目の当たりにして、ドメイン駆動設計と関数プログラミングの融合について、もっと掘り下げてみたいと思いました。


参考

Scalaコードでわかった気になるDDD | GREE Engineers' Blog

DDDに役立つScalaの関数型プログラミング的機能 - Qiita


読書会やってます

今回紹介した "Functional and Reactive Domain Modeling" (Debasish Ghosh 著) の読書会に参加しています。興味のある方はぜひご一緒しましょう!

Functional and Reactive Domain Modeling 読書会 - connpass