Domain Modeling Made Functional
https://gyazo.com/42581b1a6e1fc3b5c03aa0c1676862b5
なぜこの本を読んだか
何が書かれている本か
ただ個人的には DDD に限らず、仕様を語る実装 ( Screaming Architecture ) を実装する上でのパターンとしてモデルの状態と状態遷移図に着目するというパターンの1つをこの本では紹介しているような本だなと思う。 もちろんこれだけでドメインを意識したアプリケーションが作れるほど簡単ではないが1つの側面としては新しい観点なのかなと思ったりした。
メモ
DDD の精神
開発者は、コードを書くだけが仕事ではない。
ソフトウェアを通して問題を解くことが仕事であり、コードを書くのは1つの側面であって、良い設計やコミュニケーションは同様に重要である。
ソフトウェア開発をパイプライン処理に見立てたときに garbage in, garbage out というルールが適用される。
つまり品質の悪い入力からは良いコードは絶対に生み出されない。
garbage in をいかに品質をあげていくかということをやっていくのが DDD の精神であると考えている。
DDD は Domain Expert というドメインに最も詳しい人が存在する。
しかし実際に本番にリリースされるアプリケーションはあくまでも開発者が理解したものがリリースされることに注意しないといけない。
例えば、 Domain Experts -> Business Analyst -> Architect -> Development Team という形で伝言ゲームが一方的に起こった場合末端の Development Team が Domain Experts の考えを理解できるわけがない。
関係者全員の精神を揃える Shared Mental Model が重要である。
Domain Event (アプリケーションにとって重要なイベント) を中心にそのイベントの結果どういう影響がシステム全体に発生するのかという全体像を関係者が把握することが重要となる。
Modeling the Domain
Functions
Fucntion は、一般化すると 入力 -> 出力 という形をしたブラックボックスと捉えることができる。
また 入力 と 出力 をさらに一般化してある型がある型に変換されることが Function と捉えることができるだろう。
code: Kotlin
// Int から Int への変換
fun add1(x: Int): Int = x + 1 // Int -> Int
Types
型には2つの種類の見方がある
それが AND Types と OR Types である。 (要するに直積と直和)
code:Kotlin
// 多分ニュアンスはサラダなのでごちゃまぜという感じかな?
class FruitSalad {
val apple: AppleVariety
val banana: BananaVariety
val cherries: CheryyVariety
}
FruitSalad の値は AppleVariety x BananaVariety x CheryyVariety の値の組み合わせによって定まるので AND Types (直積) と呼ぶ
code: F#
// F# だけど Union はやっぱ | で書いた方がわかりやすいので F# の例も載せておく
// 多分ニュアンスは、単一のフルーツでできたおかし (ドライフルーツとか) のイメージなのかな
type FruitSnack =
| Apple of AppleVariety
| Banana of BananaVariety
| Cherries of CheryyVariety
code: Kotlin
// Kotlin で書くならこういう感じ?
sealed class FruitSnack
class AppleVarietyFruitSnack : FruitSnack
class BananaVarietyFruitSnack : FruitSnack
class CheryyVarietyFruitSnack : FruitSnack
いずれにせよ FruitSnack の値は AppleVariety + BananaVariety + CheryyVariety の値の組み合わせ分しかない (要するに直行するので組み合わせとならない) ので OR Types (直和) と呼ぶ
また上記の他に単一 (別に単一ではなくても意味のまとまりがあるならばそれをまとめてもいいと思う) の値を型でラップしたものもある
e.g.
code: Kotlin
data class CustomerId(val id: Int)
Integrity (有効性)
code: Kotlin
data class WidgetCode(val code: String) // starting with "W" then 4 digits
data class UnitQuantity(val quantity: Int) // between 1 and 1000
data class KilogramQuantity(val quantity: Float) // between 0.05 and 100.00
上記のように多くのアプリケーション上で扱う概念が String や Int そのままの制約とあっているわけではない。
それらをコメントに書いてとりあえず他の開発者に伝えるという策も有効だが、そもそもインスタンス化できないようにしてしまうという手もある
code: Kotlin
class UnitQuantity private constructor(
val quantity: Int
) {
companion object {
fun create(qty: Int): UnitQuantity {
require(qty >= 1) { "UnitQuantity can not negative" }
require(qty <= 1000) { "UnitQuantity can not be more than 1000" }
return UnitQuantity(qty)
}
}
}
ちなみに自分はこのくらいならそもそも init ブロックの中に require を書くのがいいのではと思ってしまう
本題
ここまででおおよそテクニック的な話は終わりでここからが本題
状態
例えば、メールアドレスで認証済みかどうかという状態があったとしたときに
code: Kotlin
class CustomerEmail(
val emailAddress: EmailAddress
val isVerified: Boolean
)
上記のようにフラグを持たせるというのは一般的な解決方法の1つではあるが、この場合にはフラグを変え忘れたりしてしまうし例えば加えて認証済みのメールアドレスしかメールを送ってはいけない (その逆もしかり) などの実装がある場合に isVerified でフィルタリングをするのを忘れてしまうなどがあり得る。 (要するに気をつければいいわけだが、気を付けるにもコンパイラなどの恩恵を受けれないので開発者が気を付ける以外に方法がないというのが主張だと思う)
そもそもこういった2状態あるものについては、以下のように OR Types を使ってモデリングしてみようというのが提案
code: F#
// 毎度のことだけど Union は | の方が直感的に読みやすいので F# の例も書いておく
type CustomerEmail =
| Unveriied of EmailAddress
| Verified of EmailAddress
code: Kotlin
sealed class CustomerEmail {
abstract val address: EmailAddress
}
class UnveriiedCustomerEmail(override val address: EmailAddress): CustomerEmail()
class VerifiedCustomerEmail(override val address: EmailAddress): CustomerEmail()
このような実装をすることで、例えば「パスワードの再設定メールは認証済みのメールにしか送信してはいけない」という要求があるときに以下のような制約をかけたりすることができる
code:Kotlin
fun sendPasswordResetEmail(emailAddress: VerifiedCustomerEmail) {
// do something
}
またもう1つの例として「ある連絡先情報は、メールアドレスもしくは住所が必須となる」というビジネス要求を考えてみる
code:F#
// メールアドレスと住所が必須になってしまう
type Contact = {
Name: Name
Email: EmailContactInfo
Address: PostalContactInfo
}
// メールアドレスと住所どちらも設定しないことができてしまう
type Contact = {
Name: Name
Email: EmailContactInfo option
Address: PostalContactInfo option
}
// メールのみ、住所のみ、どちらも設定されているの3パターンを表現するために直和を利用する
type Contact = {
Name: Name
ContactInfo: ContactInfo
}
type ContactInfo =
| EmailOnly of EmailContactInfo
| AddrOnly of PostalContactInfo
| EmailAndAddr of BothContactMethods
type BothContactMethods = {
Email: EmailContactInfo
Address: PostalContactInfo
}
状態遷移 (state machine)
例えば以下のような簡易的なECサイトにおける買い物カゴの状態と代表的な状態遷移関数を書いてみる
code: F#
type Item = ...
type ActiveCartData = { UnpaidItems: Item list }
type PaidCartData = { PaidItems: Item list; Payment: float }
type ShoppingCart =
| EmptyCart // no data
| ActiveCart of ActiveCartData
| PaidCart of PaidCartData
// 買い物カゴに商品を追加する
let addItem cart item =
match cart with
| EmptyCart ->
ActiveCart { UnpaidItems=item } | ActiveCart { UnpaidItems=exsitingItems } ->
ActiveCart { UnpaidItems=item::exsitingItems }
| PaidCart _ ->
cart // ignore
// 決済する
let makePayment cart payment =
match cart with
| EmptyCart ->
cart // ignore
| ActiveCart { UnpaidItems=exsitingItems } ->
PaidCart { PaidItems=exsitingItems; Payment=payment }
| PaidCart
cart // ignore
こうすることで以下のような状態遷移図がコードによって明確に表現できるためコードがビジネス要求を示すようになる
TBD 状態遷移図
参考情報
自分の登壇でもこの話を扱ったりしてた
こちらの動画でも話題になっていて、過程のイメージがしやすかった
https://www.youtube.com/watch?v=lDMR-tqDRVs
感想