書籍「実践ドメイン駆動設計 (Object Oriented Selection)」が出版されて、ドメイン駆動設計(DDD)の知名度が上がってきているようです。
そのDDDに関連する分野の1つとして、DSL(ドメイン特化言語)を挙げることができると思います。
デバシッシュ・ゴーシュさん(Debasish Ghosh)は、DSLのエキスパートで、「実践プログラミングDSL」(原題 “DSLs in Action")という本を出されています。この本の第1章のタイトルは、「ドメインの言葉を話す方法を学ぶ」となっています。
デバシッシュさんのブログ記事のうちの1つ、"Functional Patterns in Domain Modeling - The Specification Pattern” に惹かれて、翻訳をしました。翻訳記事の公開について、著者ご本人から快諾頂けたため、以下に掲載させて頂きます。
ドメインモデリングにおける関数型パターン―仕様パターン
原文URL:http://debasishg.blogspot.in/2014/03/functional-patterns-in-domain-modeling.html
2014年3月31日月曜
ドメインをモデリングするときは、ドメインのエンティティと振る舞いをモデル化する。エリック・エヴァンスが書籍「ドメイン駆動設計」で述べたように、焦点を合わせるべきはドメインそれ自体だ。設計、実装したモデルは、ユビキタス言語を語っていなければならない。実装にの都合により、偶有的な複雑性が多々生じても、ドメインの本質を捉えることができるようにするためだ。表現力の豊かなモデルにするには、拡張可能であることも必要である。そして、私たちが拡張性について語るとき、関連する性質として合成性がある。
関数は、オブジェクトよりも自然に合成を行う。そこで、この記事では、ドメイン駆動設計のコアを成すパターンのうちの1つを実装するために、関数型プログラミングのイディオムを用いるー 仕様パターンだ。
仕様パターンで最も多いユースケースは、ドメインのバリデーションを実装することである。エリックのDDD本には、仕様パターンについて次のように書かれている:
仕様
の用途は複数あるが、最も基本的な概念を伝えているのは、どんなオブジェクトでも評価して、定義された基準を満たしているかどうかを調べるという使い方である。
仕様は述語として定義され、それによって、業務ルール同士をブール論理を使って互いに連鎖させて結合できるようになる。このように合成の概念があるので、このパターンについて語るときは合成仕様
(Composite Specification
)として語ることができるだろう。DDDにおける各種の文献では、これをComposite
デザインパターンを使って実装しており、ごく一般的にはクラス階層とコンポジションが用いられていた。この記事では、その代わりに関数合成を使う。
仕様はどこにあるのか?
モデルを設計する際によくある問題として、集約ルートやエンティティのバリデーションを行うコードをどこに置くのか、という話がある。
-
エンティティの中でバリデーションをするか? これはダメだ。エンティティが肥大化してしまう。同じエンティティのコアでも、コンテキストによってバリデーションを変えたいことがある。
-
インターフェイスの一部としてバリデーションをするか? JSONを使って、その外側でエンティティを構築するかもしれない。たしかに、ある種のバリデーションはインターフェイスに属すると言えるし、そこにバリデーションを置くことに違和感は無い。
-
しかし、もっとも興味深いバリデーションは、ドメインレイヤーに属するものだ。業務のバリデーション(あるいは仕様)であり、エリック・エヴァンスが他のオブジェクトの状態に関する制約をのべるものと定義している。
業務ルールとして、エンティティを次の処理へ渡す前にバリデーションをかけなければならない。
単純な例で考えよう。注文
(Order
)エンティティとそのモデルがあるとする。新しい注文
は、処理工程に入る前に、下記のドメインの「仕様」を満たすものとする。
- 妥当(valid)な注文でなければならず、ドメインが必要とする正当な日付、正当な商品品目、といった制約に従わなければならない。
-
正しい権限で承認(approved)されていなければならない。処理工程の次段階に進めるのはその場合だけだ。
-
顧客がブラックリストに載っていないことを保証するため、状態を審査しなければならない。
-
注文可能かどうかを調べるため、商品品目の在庫を確認しなければならない。
個々の手順に分かれていて、注文の処理工程に沿って順次完了していく。そのようにして、注文の前に、注文実行の用意が整っているどうかを検証する。どこかでエラーがあると、注文は処理工程から外れて、そこで処理が終わる。そして、私たちが設計するモデルは、この順序を知っている必要があるし、手順の一部として経るべき制約はすべて課す必要がある。
注文を変化させるのは手順だけである―ここは重要なポイントだ。すべての仕様は、最初の注文
のコピーを入力として受け取り、ドメインルールを検証後、処理工程の次の手順へ進ませて良いかどうか判定する。
実装へ……
私たちがここまでで学んできたことをふまえて、実装に落としこんでみよう。
注文
は、少なくとも今回の操作の流れにおいては、不変のエンティティにできる。
-
すべての仕様は1つの注文を必要とする。これがトリックになっていて、仕様に
注文
インスタンスを順次渡すことで、APIをシンプルに保つことができる。
-
関数型プログラミングの原則により、上記の処理手順を
式
としてどのようにモデル化できるか。結果を最後まで合成可能に保ち、注文完了後の次工程へ渡すにはどうするか(次工程については、いずれまた別の記事で議論しよう)。
-
すべての関数は似たシグネチャを持っているようだ―私たちは関数をたがいに合成する必要がある。
あれこれと説明や理論を持ち出すよりも、まずは基本的なビルディングブロックを使ってドメインエキスパートととりまとめた内容を実装していこう。
type ValidationStatus[S] = \/[String, S]
type ReaderTStatus[A, S] = ReaderT[ValidationStatus, A, S]
object ReaderTStatus extends KleisliInstances with KleisliFunctions {
def apply[A, S](f: A => ValidationStatus[S]): ReaderTStatus[A, S] = kleisli(f)
}
ValidationStatus
は、どの関数からも結果として返す型を定義している。それは状態S
、または何らかの異常を報告するエラー文字列となる。正確にはscalazで実装されているEither
型(右側が正)である。
この実装が優れているのは、すべてのメソッドで注文
パラメータが繰り返されることなく処理手順が呼び出されるからだ。そのようにするイディオムの1つとして、Readerモナドが使われる。そして、私たちはすでにモナド―\/
はモナドだ―を持っている。そこで、処理結果をモナド変換子を使って積み上げていく。ReaderT
がこの作業を受け持つ。結果同士を結びつけてくれるありがたい型としてReaderTStatus
を定義する。
次のステップはReaderTStatus
の実装で、クライスリ
と呼ばれる別の抽象を使う。scalazライブラリを使って、クライスリ
の言葉でReaderT
を実装する。実装の詳細に立ち入る話はしないでおく―もし興味があるのであれば、ユージーンによる優れた論文を参照してほしい。
さて、サンプルの仕様はどのようになるのか?
話に入る前に、基本的な抽象を用意する(わかりやすさのため、ごく単純にしてある)。
// 基底の抽象
sealed trait Item {
def itemCode: String
}
// サンプル実装
case class ItemA(itemCode: String, desc: Option[String],
minPurchaseUnit: Int) extends Item
case class ItemB(itemCode: String, desc: Option[String],
nutritionInfo: String) extends Item
case class LineItem(item: Item, quantity: Int)
case class Customer(custId: String, name: String, category: Int)
// 注文のスケルトン
case class Order(orderNo: String, orderDate: Date, customer: Customer,
lineItems: List[LineItem])
そして、下記が注文
オブジェクトの制約を検査する仕様である。
// 基本的なバリデーション
private def validate = ReaderTStatus[Order, Boolean] {order =>
if (order.lineItems isEmpty) left(s"Validation failed for order $order")
else right(true)
}
これは説明用の例に過ぎないので、ドメインルールが多く含まれているわけではない。
重要なのは、関数を実装するために定義した型をどう使っているかだ。
注文
は関数に対する暗黙の引数ではない―それはカリー化される。関数はReaderTStatus
を返す。ReaderTStatus
自体はモナドである。したがって、別の仕様と併せて処理工程を手順化していくことができる。つまり、決められた順序の要求を、式指向プログラミングの枠組みの中で解決できるのだ。
収集したドメイン知識に基づく仕様はほかにもあった。
private def approve = ReaderTStatus[Order, Boolean] {order =>
right(true)
}
private def checkCustomerStatus(customer: Customer) = ReaderTStatus[Order, Boolean] {order =>
right(true)
}
private def checkInventory = ReaderTStatus[Order, Boolean] {order =>
right(true)
}
これらを互いにつなぐこと
しかし、ドメインが与える操作順序を表すには、これらのピースをどうつなげば良いのだろう。モデルで合成性による利点を享受するにはどうするのか?
これは非常に簡単だ。合成に適した型定義という難しい仕事はすでに終わっている。
以下のisReadyForFulfilment
メソッドでは「合成仕様」を定義していて、for内包表記(for-comprehension)でくるまれた個々の仕様すべてを順番に呼び出している。
def isReadyForFulfilment(order: Order) = {
val s = for {
_ <- validate
_ <- approve
_ <- checkCustomerStatus(order.customer)
c <- checkInventory
} yield c
s(order)
}
このように、モナディックな結合により、順序立てられた一連の処理を、抽象の合成性を保ちつつ実装することができる。次回は、注文の後工程を合成可能にする方法、エンティティの情報を読むだけでなく可変とするやり方についても見ていこう。もちろん、関数型のアプローチで。
翻訳後記
この記事は、初学者の私が、背伸びをして勉強しながら訳したものです。推敲を重ねましたが、誤訳等が含まれている可能性はあります。もし何かありましたら、ご指摘頂けると幸いです。
関連記事
参考