Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
過去の失敗例から再考するモデル駆動設計
Search
かとじゅん
August 31, 2019
Programming
22
10k
過去の失敗例から再考するモデル駆動設計
過去に僕が失敗した代表例から今ならどう設計するか、という観点でお話します。中心になるトピックは以下です
- 軽量DDDの功罪
- ドメインモデル貧血症対策
- 集約の境界定義の善し悪し
かとじゅん
August 31, 2019
Tweet
Share
More Decks by かとじゅん
See All by かとじゅん
アクターシステムに頼らずEvent Sourcingする方法について
j5ik2o
4
210
メッセージとイベントを中核に置いたシステム設計の有用性について
j5ik2o
11
2.9k
私のキャリアの旅路: 技術をきっかけに変化を楽しむ
j5ik2o
3
790
いかに開発効率と品質を高めるか: ドメイン駆動設計と組織パターンの視点から考える
j5ik2o
3
2.4k
社内のメンバーに「関数型プログラミングの学習・教育」についていろいろ聞いてみた
j5ik2o
2
1.8k
AWS データベースブログの記事 「Amazon DynamoDBによる CQRSイベントストアの構築」 を勝手に読み解く
j5ik2o
2
2.7k
EIPとAkkaについて
j5ik2o
3
2.6k
モデルを中心にデザイン(設計)すること
j5ik2o
2
2.7k
ドメインイベントの観点から再考するソフトウェア設計
j5ik2o
17
10k
Other Decks in Programming
See All in Programming
CSC305 Lecture 26
javiergs
PRO
0
140
テストケースの名前はどうつけるべきか?
orgachem
PRO
0
130
Итераторы в Go 1.23: зачем они нужны, как использовать, и насколько они быстрые?
lamodatech
0
700
tidymodelsによるtidyな生存時間解析 / Japan.R2024
dropout009
1
760
競技プログラミングへのお誘い@阪大BOOSTセミナー
kotamanegi
0
360
Fibonacci Function Gallery - Part 1
philipschwarz
PRO
0
210
range over funcの使い道と非同期N+1リゾルバーの夢 / about a range over func
mackee
0
110
命名をリントする
chiroruxx
1
390
コンテナをたくさん詰め込んだシステムとランタイムの変化
makihiro
1
120
Security_for_introducing_eBPF
kentatada
0
110
第5回日本眼科AI学会総会_AIコンテスト_3位解法
neilsaw
0
170
テスト自動化失敗から再挑戦しチームにオーナーシップを委譲した話/STAC2024 macho
ma_cho29
1
1.3k
Featured
See All Featured
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
28
9.1k
4 Signs Your Business is Dying
shpigford
181
21k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
5
440
The Cult of Friendly URLs
andyhume
78
6.1k
Bootstrapping a Software Product
garrettdimon
PRO
305
110k
Measuring & Analyzing Core Web Vitals
bluesmoon
4
170
Faster Mobile Websites
deanohume
305
30k
Designing on Purpose - Digital PM Summit 2013
jponch
116
7k
jQuery: Nuts, Bolts and Bling
dougneiner
61
7.5k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
507
140k
Visualization
eitanlees
146
15k
Making the Leap to Tech Lead
cromwellryan
133
9k
Transcript
過去の失敗例から再考するモデル駆動設計 かとじゅん(@j5ik2o) レガシーをぶっつぶせ。現場でDDD! コラボカンファレンス
© Chatwork あなた誰? • 加藤潤一(@j5ik2o) • テックリード ◦ 大阪本社(福島) 本日も業務扱い
◦ レガシーシステム移行プロジェクトやってます • 2009年11月に初めてEric本を読む。これ本当に技 術書なの?(笑)と思いながら読んでた(エンティティ の章) • 2011年和訳本がでるときに翻訳レビューした。大 したことはしてない
© Chatwork 最近の活動 • チケット料金モデリング ◦ 映画の料金を決定するモデルを実装する ◦ 値オブジェクトの設計に注力する ▪
DBやWebAPIなどの入出力は一旦忘れる ◦ 僕の実装 ◦ 20人ぐらいがコードを書いてくれてた ▪ セプテーニさん、オプトさんでも自主開催してくれた • Qiita:ドメインロジックはドメインオブジェクトに凝集させる ◦ 内部データをそのまま暴露するGetterは貧血症の温床になるので気 を付けろ的な話
© Chatwork アジェンダ • 過去に僕が失敗した代表例から今ならどう設計する か、という観点でお話します。中心になるトピックは 以下です ◦ 軽量DDDの功罪 ◦
ドメインモデル貧血症対策 ◦ 集約の境界定義の善し悪し • ※失敗はうまくいかない方法を見つけただけ、成功へ のプロセス。Fail Fastで早く学ぶことが失敗の本質
© Chatwork これまでに経験した失敗(学び)談(代表例) • CASE1: 軽量DDDで躓いた話 ◦ というか、トランザクションスクリプト亜種 • CASE2:
集約の境界定義を試行錯誤した話
© Chatwork CASE1: 軽量DDDで躓いた話 • おこったこと ◦ DDDを学びはじめた頃、軽量DDDを実践した。開発メンバとだ けユビキタス言語を共有したつもりだった… •
どうなったか ◦ 開発チームの外側では全然異なる言語が使われてしまい、やは り通訳が必要になってしまった ◦ 分析と設計(実装)が単一モデルになる前提ではないので、エン ティティはテーブルのことだと誤解し実装が手続き型に…。ト ランザクションスクリプト亜種になった 従来のフレームワークで経験した設計方法で判断してしまった
© Chatwork FYI: 軽量DDDとは • 戦術的設計のパターンだけを利用 したDDDのこと • ユビキタス言語を反映しないドメ インモデルが作られる可能性
◦ 共通言語基盤に基づかないソ フトウェアを語るときは通訳 が必要になってしまう ◦ ソフトウェアが正しいか言語 によって証明できなくなる エンティティ サービス リポジトリ 戦術的パターン … 戦略的パターン ユビキタス言語 コアドメイン 境界づけられた コンテキスト コンテキストマッ プ こちらだけつまみ食い
© Chatwork 軽量DDD想定が、トランザクションスクリプト亜種に… • この方法が一番簡単 ◦ レイヤ化アーキテクチャのためにパッケージを切る ◦ エンティティとサービスを作る(あれ?) ▪
データはエンティティ(=テーブルモデル) ▪ ロジックはサービス ◦ データベースアクセスにリポジトリを使う • 実は軽量DDDにもなっていない ◦ エンティティはデータクラス、サービスはロジッククラス。 つまりトランザクションスクリプトになっている… ◦ リポジトリはDAOの代わりになっているだけ… 貧血症オブジェクトになっている
© Chatwork ドメインオブジェクトっぽいが振る舞いがない… • カートの合計金額が上限を超えないなら、カートを追加できる • 属性もプリミティブ型を使っている。本当に正しい? case class Cart(
id: Int, userAccountId: Int, limitPrice: Int, createdAt: Instant, updatedAt: Instant ) trait CartRepository { // ... } case class CartItem( id: Int, no: Int, itemId: Int, quantity: Int ) trait CartItemRepository { // ... }
© Chatwork ドメインロジックが手続き型で書かれている… • ドメインロジックが手続き型。これは貧血症オブジェクトでありテーブルモデ ル…。このようなコードをアプリケーションサービスに書かれている… // 合計金額を計算 val totalPrice
= cartItems.foldLeft(0) { case (r, e) => r + resolvePrice(e.itemId) * e.quantity } // 上限金額を超えていないなら require(totalPrice + resolvePrice(4) < cart.limitPrice) // カートにアイテムを追加 val newCartItems = cartItems :+ CartItem(id = 1, no = 4, itemId = 4, quantity = 1) val now = Instant.now val cart = Cart( id = 1, userAccountId = 1, limitPrice = 100, createdAt = now, updatedAt = now ) val cartItems = Seq( CartItem(id = 1, no = 1, itemId = 1, quantity = 1), CartItem(id = 2, no = 2, itemId = 2, quantity = 3), CartItem(id = 3, no = 3, itemId = 3, quantity = 5) )
© Chatwork FYI: ドメインモデル貧血症とは • ドメインモデル貧血症 - Martin Fowler’s Bliki(ja)
• 本物のドメインモデルに見える。 ◦ オブジェクト同士の関連があり、本物のような構造を持つ ◦ ただし、オブジェクトの振る舞いはわずかしかない ◦ ドメインロジックをドメインオブジェクトの中に入れないルールに 従っている。ドメインロジックはサービスオブジェクト群に存在する • データと処理を統合するアプローチとは真逆 ◦ 単なる手続き型設計(トランザクションスクリプト)と同じ • アプリケーションサービスはドメインロジックを配置する場所ではない、 それはドメインオブジェクトの役割。アプリケーションサービスは進行役
© Chatwork ユビキタス言語を反映しないドメインモデル例 • 実装都合の用語法で作られたオブジェクト群。ビジネス側とエン ジニア側で対応付けが難しくなり、通訳が必要となる ◦ 注文ではなくリクエスト。あいまいな用語が通訳コスト ◦ 注文ではなく注文テーブルと注文詳細テーブル。本来一つの
概念が複数のテーブルオブジェクトと対応づいている。利害 関係者と話すときは注意が必要 • ビジネスロジックを持たないドメインオブジェクト ◦ Getterしかないオブジェクト。どこでどんな計算が行われる かがわかにくい。ロジックの重複に気づけない
© Chatwork 放置しないで改善するには
© Chatwork 言語とモデルを作ってソフトウェアに反映する • 利害関係者間で作る・使う ◦ プロジェクトのすべての活動で ◦ 要求分析で使う言語体系を設計 (実装)に反映する
• ユビキタス言語は要求分析から ◦ 要件「…すること」 ◦ 業務フローや利用シーン ◦ ユースケースやイベント • 右図を議論するなかで言語体系を揃 えていく。考えたモデルはコードに も反映する RDRA2.0ハンドブックより
© Chatwork 関心事に対応する値オブジェクトを導入する • 値オブジェクトを導入する ◦ お金は単なる数値ではない。 量と通貨単位で成り立つ ◦ 加算・減算。減算は加算に
よって作られている ◦ 通貨単位が同じでないと計算 できない • 値オブジェクトにロジックを凝集 させる • 分析を設計(実装)に反映する。実装 できなければ分析を見直す。要件 も見直すこともある。いったり来 たり final class Money(amount: BigDecimal, currency: Currency) { def negated: Money = new Money(-amount, currency) def add(other: Money): Money = { require(currency == other.currency) Money.adjustBy(amount + other.amount, currency) } def subst(other: Money): Money = add(other.negated) def breachEncapsulationOfAmount: BigDecimal = amount def breachEncapsulationOfCurrency: Currency = currency } val result = Money.yens(10).add(other)
© Chatwork FYI: Time and Money Code Library • Evansが関わった日時とお金のための値オブジェクト・ライブラリ
◦ Time and Money Code Library ◦ baseunits-scala (許可を得てScala版としてfork) • 以下は営業日を計算して返すカレンダー値オブジェクト val calendar = BusinessCalendar() .addHolidaySpec(DateSpecification.fixed(1, 1, ZoneIds.Default)) // 元旦 . addHolidaySpec( DateSpecification.nthOccuranceOfWeekdayInMonth(1, DayOfWeek.Monday, 2, ZoneIds.Default) ) // 成人の日 .addHolidaySpec(DateSpecification.fixed(2, 11, ZoneIds.Default)) // 建国記念日 // … // それぞれの日が「営業日」にあたるかどうかチェック。 assert(calendar.isBusinessDay(CalendarDate.from(2010, 10, 8, ZoneIds.Default))) // 金曜日 assert(!calendar.isBusinessDay(CalendarDate.from(2010, 10, 11, ZoneIds.Default))) // 月曜日体育の日
© Chatwork 貧血症のCartオブジェクトを解消した例(1/3) • オブジェクトは自己 防衛する。不正な値 は扱わない • コレクション型はそ のまま扱わずにドメ
イン固有型を介して 使う • 値オブジェクトのま ま比較や計算ができ るようにする(ビジ ネスルールが守られ たまた計算できる) final class Cart(id: CartId, userAccountId: UserAccountId, limitPrice: Price, unitPriceResolver: ItemId => UnitPrice, cartItems: CartItems, createAt: Instant, updateAt: Instant) { def totalPrice: Price = cartItems.totalPrice(unitPriceResolver) require(limitPrice > totalPrice, "totalPrice is over than limitPrice") def addCartItems(otherCartItems: CartItems): Cart = new Cart(id, userAccountId, limitPrice, unitPriceResolver, cartItems + otherCartItems, createAt, updateAt) }
© Chatwork 貧血症のCartオブジェクトを解消した例(2/3) final class CartItem(id: CartItemId, no: CartNo, itemId:
ItemId, quantity: Quantity, createAt: Instant, updateAt: Instant) { def price(unitPriceResolver: ItemId => UnitPrice): Price = unitPriceResolver(itemId) * quantity } final class CartItems(values: Seq[CartItem]) { require(values.nonEmpty, ...) def +(other: CartItem): CartItems = new CartItems(values :+ other) def totalPrice(unitPriceResolver: ItemId => UnitPrice): Price = values.foldLeft(Price.zero) { case (totalPrice, cartItem) => totalPrice + cartItem.price(unitPriceResolver) } }
© Chatwork 貧血症のCartオブジェクトを解消した例(3/3) final class UnitPrice(private[carts] val value: Int) {
require(value > 0) def *(quantity: Quantity): Price = new Price(value * quantity.value) } object Price { val zero = Price(0) } final class Price(private[carts] val value: Int) extends Ordered[Price] { require(value > 0) def +(other: Price): Price = new Price(value + other.value) override def compare(that: Price): Int = value compare that.value }
© Chatwork スケーラブルな設計とは • ドメインオブジェクトと言語の体系が一致している • ドメイン上の値はただのプリミティブ型ではなく、値の範 囲などのルールとそれに基づく計算能力を持つようになっ た •
ドメインロジックの最小部品を値オブジェクトにして、組 み合わせてより高度な問題を解決する • 不変である値オブジェクトは、想定外の挙動を起こさない ため、どんなに複雑な条件で組み合わせても、予測可能な 設計を作り出せる
© Chatwork CASE2: 集約の境界定義に試行錯誤した話 • 例えば、Conversation, Participants, Messages。 Conversationの一部なのか外部 なのか
• テーブル脳で考えると、すべて が独立している集約。 • 一方でオブジェクト脳で考える と、Conversationは会話の場を 表すのでParticipants, Messagesを包含した集約 • どちらも試したことがある Conversation Participants Messages Conversation Participants Messages 全てが集約 集約は1つ
© Chatwork 例えば、Conversationのビジネスルール • Conversationは会話を表し、契約プランによって以下の制約がある ◦ 参加者数 ◦ 投稿できるメッセージ数
© Chatwork すべてが集約の場合
© Chatwork すべてが集約の場合 case class Conversation(id: ConversationId, name: ConversationName, planType:
PlanType) class Participants(values: Seq[Participant]) { def add(participants: Participants): Participants = new Participants(values ++ participants.values) def add(participant: Participant): Participants = ??? def count: Int = values.length } class Messages(values: Seq[Message]) { def add(messages: Messages): Messages = ??? def add(message: Message): Messages = ??? def count: Int = ??? } • 最初に試した実装 • テーブル脳ですべてを集約 として分けた例 • ドメインモデルっぽい?ア プリケーションコードを書 いたら問題が明確になった
© Chatwork アプリケーションサービスのコード(悪い例) • ルールと振る舞いが分離。ルールを違反したコードが書ける…。 • Conversationに関する知識が分散している(貧血症) def addParticipantAndMessage(conversation: Conversation,
participants: Participants, messages: Messages, participant: Participant, message: Message): (Participants, Messages) = { require(conversation.planType.participantCount >= participants.count + 1) require(conversation.planType.messageCount >= messages.count + 1) val newParticipants = participants.add(participant) val newMessages = messages.add(message) (newParticipants, newMessages) } アプリケーションサー ビスのコード
© Chatwork 試行錯誤した例… • ルール適用と状態操作を一体化する…。でも何か違和感がある… • ファーストコレクションの操作はConversation経由で実行する • 集約が異なれば結果整合性の境界も異なる。プラン変更後も一次的に不 正な状態が観測できるかもしれない
def addParticipantAndMessage(conversation: Conversation, participants: Participants, messages: Messages, participant: Participant, message: Message): (Participants, Messages) = { val newParticipants = conversation.addParticipant(participants, participant) val newMessages = conversation.addMessage(messages, message) (newParticipants, newMessages) }
© Chatwork 集約が一つの場合
© Chatwork 集約が一つの場合(1/2) • Participants, Messagesは Conversationの内部オ ブジェクト。 • ルール適用と振る舞い
は一つのオブジェクト に統合される • 強い整合性境界にまと めることができる(要件 による) class Conversation(id: ConversationId, name: ConversationName, participants: Participants, messages: Messages, planType: PlanType) { def addParticipant(addition: Participant): Conversation = { require(planType.participantCount > participants.count + 1) newInstance(participants = participants.add(addition)) } def addMessage(addition: Message): Conversation = { require(planType.messageCount > messages.count + 1) newInstance(messages = messages.add(addition)) } }
© Chatwork 集約が一つの場合(2/2) • アプリケーションロジックもかなりシンプルになる • このコードの意味を理解するのに、余計なドキュメントはいらない def addParticipantAndMessage(conversation: Conversation,
participant: Participant, message: Message): Conversation = conversation .addParticipant(participant) .addMessage(message)
© Chatwork 集約の境界定義の善し悪し • 大前提 ◦ ユースケースを実現できること Pros Cons すべてが集
約 • ロックの競合頻度が下がる • 必要に応じてI/Oできる。レ イテンシが有利 • 細かく分けすぎると貧血症に なりやすい • 結果整合性で厳密なビジネ スルールは強制できない 集約がひと つ • ドメイン知識がドメインオブ ジェクトに凝集できる • ロックの競合頻度が上がる • I/Oレイテンシが不利
© Chatwork 集約の境界定義に必要な観点 • ユースケースだけでは境界定義は定まらない • 集約の粒度がレイテンシに影響を与えるのは事実。説明が 難しいモデルは実装も大きくなり、I/Oにもコストも大きく なる傾向にある •
振る舞いにフォーカスし小さい集約を実現するには、クエ リ責務を他に委譲する必要がある(CQRS, EventSourcing) Conversation def removeMessages(messageIds): Conversation private val messageIds: MessageIds 振る舞いを起こすために 必要な状態しか持たない のが理想
© Chatwork FYI: Event Storming + 値オブジェクト・モデリング • 発見の順序は番号の流れ。Event Storming(Desingn
Level)では③ま でだが、そこから先はロジックの主役は値オブジェクトに注目する コマンド イベント ③集約 エンティティ ④値オブジェクト ユースケース コマンド ②コマンド イベント ①イベント 状態遷移 振る舞い 利用 必要に応じてイベントを 使ってリードモデルを構 築する アクター
© Chatwork まとめ: これまでを振り返って思うこと • 設計は仮説・検証・フィードバックの連続 ◦ FYI: PDR(Prep→Do→Review→PDR) をやっていこう。PDCAは生産管理用
• 不確実性コーンを見極めながら、納期だけ ではなく設計もFail Fastしていこう。うま くいかなかったら、それは学びの機会 • 自分一人で解決できなければ、自ら情報を 共有しながら、コミュニティからヒントを 得られるようにしてきたい 不確実性コーン (アジャイルな見積もりと計画作りより )
© Chatwork ご清聴ありがとうございました!
© Chatwork 例えば、こんなマネージャと一緒に働けます! @j5ik2oまでメッセージください!
None