強いて言えば「集約どう実装す
るのかな、を考える」な話
kawasima
アーキ部 #13
CartにCartItemを追加する
Add Cart
Add Cart
Add Cart
1
1
1
{
“productId”: “mikan”,
“quantity”: 1
}
API Endpoint
AddCartItemUseCase
{
“userId”: “kawasima”,
“productId”: “mikan”,
“quantity”: 1
}
ユースケースシナリオ
1. ユーザのカートが存在することを確認する。
2. productIdが実在する商品かつ販売中の商品であることを確認する。
2a. 販売中でない場合
2a1. その旨をユーザに通知して終了
3. カートに商品と数量を追加する
3a. 既にカートの中に同一の商品があれば数量を足し合わせる
3a カート内の商品の数量が100を超える場合
その旨をユーザに通知して終了
4. カートの内容を保存する
高々100件くらい全部メモリにロード
しちゃえばいいじゃないか…
じゃあその件数上限が、数十万件に
なったら?
https://github.com/kawasima/revisiting-domain-model/blob/main/src/victim_performance.ts
ドメインを中心に据えて、外界と
のやり取りをエッジに置く設計
カート
カートアイテム
スタジアム
座席
列
番号
座席ランク
商品ID
数量
1 *
ユースケース: あるカートアイテム
の数量を変更する
ユースケース: ある座席のランクを
変更する
制約: 同一ランクの座席数上限はス
タジアム毎に決まりがある
制約: カート全体に入れることがで
きる商品数量は100個まで
1 1..*
※10万席あるスタジアムもある
A B
ドメインモデルのトリレンマ
https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/
純粋性
完全性
性能
完全性を犠牲にドメインレイヤの外
にドメインロジックを実装する
純粋を犠牲にドメインレイヤに外界
とのやり取りのコードをインジェク
トする。
全ての外界とのやり取りを業務ロ
ジックの端に追いやる。
ドメインロジックが全てドメイン層で実装されること
ドメイン層が他のレイヤに
依存しないこと
それぞれのメリット
テスト容易性と移植性が向上する。
完全性
性能
純粋性
高凝集が達成され、コードの変更容易性が向
上する。
速ければ速いほどCVRが上がる。
完全性+性能
カートアイテム追加のた
めに、わざわざ全部の
カートアイテムをロード
する必要はない。
(そもそも性能面を考える
と出来ない)
でも、参照系(カートの中
身表示ページ)では必要…
ReadとWriteを分ける
(遅延ロードを使うという
手もある…が事故多し)
https://github.com/kawasima/revisiting-domain-model/blob/main/src/victim_purity.ts
純粋性+性能
ドメインオブジェクトに、DBア
クセスを含めないようにする。
結果として、今回のユースケー
スでは、ドメインロジックはな
くなり、全てがユースケース層
に染み出す結果に。
https://github.com/kawasima/revisiting-domain-model/blob/main/src/victim_completeness.ts
DDDトリレンマ: それぞれの選択に潜む悪魔
純粋性+完全性
純粋性+性能
完全性+性能
理想論の悪魔
👿 「純粋かつ完全なるドメイン …美しい」
ただし、遅くて使い物にならないけどな
アンチドメインモデル貧血症 の悪魔
👿 「業務ロジックを発見し、濃いドメインモデルができたで〜」
こんな簡単なものに、こんな複雑な仕掛けが必要なの ?
テスタビリティの悪魔
👿 「ドメイン層がピュアでテストしやすぅ〜」
ドメイン層スッカスカでテストが簡単に書けても、ユースケースからテスト通
さんと、ほとんど品質保証の意味をなさない。
どれも追求しすぎると、悪魔に取り憑かれる
銀の弾丸(美しい解決策)はなさそう
テスト容易性と移植性が向上する。
完全性
純粋性
高凝集が達成され、コードの変更容易性が向上する。
とはいえ、性能は保った状態で、「完全性」と「純粋性」のメリット、を
少しずつでも享受できる手立てはないものか…?
所詮、どちらも直接的に顧客に不利益をもたらす品質特性ではないので、Take It Easy
高凝集について
完全性を取りに行って、ドメインモデルが貧血症を起こさないように設計出来た
ら「高凝集」になる?
→ 世の中にあるドメインモデルの例たちは、ちょっと怪しい。
https://scrapbox.io/kawasima/ドメインモデル貧血症
「複雑さ」がポイント
実際に高凝集目指すならば、複雑さを紐解かなければならない
異なる振る舞いをするものは、異なるものとしてみなす
(まぁ、ふつうはそれを型で実装する)
(ドメインサービスの乱用による)ドメイン貧血症
class User {
public exists(user: User): bool {
// 重複チェック
}
}
class UserService {
public exists(user: User): bool {
// 重複チェック
}
}
Userに自分自身の存在確認メソッドを持た
せるのは不自然。
なので、そういう場合にだけドメインサービ
スを作るといいよ。
ドメインサービスの乱用は、ドメインモデル
貧血症を招くのでほどほどに …
『ドメイン駆動設計入門』より
どこに定義するかより、同じでないものを区別することが先決では?
内包されている「複雑さ」(異なる概念)を型で表現するのが重要
重複しているかもし
れないユーザ
重複していないことが
保証されているユーザ
両者は別の概念
(なぜなら定義される
振る舞いが異なる)
// ユーザの重複チェック ; 「重複しているかもしれないユーザ」にしか適用されない
type existsUser = MaybeDuplicatedUser -> UniqueUser
// ユーザの登録; 「重複していないことが保証されているユーザ」にしか適用されない
type registerUser = UniqueUser -> RegisteredUser
Parse don’t validate
Parse don’t validate
https://sporto.github.io/elm-patterns/basic/parse-dont-validate.html
Origin: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
type alias UserInput =
{ name: Maybe String
, age: Maby Int
}
isValidUser : UserInput -> Bool
このようなバリデータだけが用意されていると、UserInput型のデータはバリデートが済
んでも、invalidな値を保持している可能性がある。
実際に、これが元で過去いくつもの脆弱性が発生している。
http://langsec.org/papers/langsec-cwes-secdev2016.pdf
type alias UserInput =
{ name: Maybe String
, age: Maby Int
}
type alias ValidUser =
{ name: String
, age: Int
}
validateUser : UserInput -> Result String ValidUser
バリデート済みであることを型によって保証する。
なので、バリデートはBooleanを返すのではなく、バリデーション済みの型にParseして返す
Type Safety Back and Forth
https://www.parsonsmatt.org/2017/10/11/type_safety_back_and_forth.html
失敗可能性を後ろへ
失敗可能性を前へ (こっちの方がいいよね、という話)
入力を型によって制約保証す
る。
Domain Modeling Made Functional
https://www.slideshare.net/ScottWlaschin/domain-modeling-made-functional-devternity-2022
Domain Expertとの対話を繰り返しながら、違うものを見つけていく
これで先の
AddCartItemUseCase
を実装してみよう
https://github.com/kawasima/revisiting-domain-model/blob/main/src/made_functional.ts
「複雑さ」を型で表現できれば、それ
を紡ぎ合わせるのがビジネスロジック
になる。
まとめ
● トリレンマにしたがい完全+純粋、純粋+性能、完全+性能な手段が考えら
れる。
● 1つのやり方に執着しすぎると、悪魔に取り憑かれるので注意しよう。
● そもそもドメインにまつわる「複雑さ」は明らかにできているのか?

強いて言えば「集約どう実装するのかな、を考える」な話