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 かとじゅん
なぜイベント駆動が必要なのか - CQRS/ESで解く複雑系システムの課題 -
j5ik2o
10
3.6k
アクターシステムに頼らずEvent Sourcingする方法について
j5ik2o
7
780
メッセージとイベントを中核に置いたシステム設計の有用性について
j5ik2o
12
3.1k
私のキャリアの旅路: 技術をきっかけに変化を楽しむ
j5ik2o
3
840
いかに開発効率と品質を高めるか: ドメイン駆動設計と組織パターンの視点から考える
j5ik2o
3
2.6k
社内のメンバーに「関数型プログラミングの学習・教育」についていろいろ聞いてみた
j5ik2o
2
1.9k
AWS データベースブログの記事 「Amazon DynamoDBによる CQRSイベントストアの構築」 を勝手に読み解く
j5ik2o
2
2.8k
EIPとAkkaについて
j5ik2o
3
2.6k
モデルを中心にデザイン(設計)すること
j5ik2o
2
2.8k
Other Decks in Programming
See All in Programming
Boost Performance and Developer Productivity with Jakarta EE 11
ivargrimstad
0
250
JavaScriptツール群「UnJS」を5分で一気に駆け巡る!
k1tikurisu
9
1.8k
第3回関東Kaggler会_AtCoderはKaggleの役に立つ
chettub
3
1k
How mixi2 Uses TiDB for SNS Scalability and Performance
kanmo
37
14k
もう僕は OpenAPI を書きたくない
sgash708
5
1.6k
WebDriver BiDiとは何なのか
yotahada3
1
140
社内フレームワークとその依存性解決 / in-house framework and its dependency management
vvakame
1
560
チームリードになって変わったこと
isaka1022
0
200
一休.com のログイン体験を支える技術 〜Web Components x Vue.js 活用事例と最適化について〜
atsumim
0
470
Honoのおもしろいミドルウェアをみてみよう
yusukebe
1
210
ペアーズでの、Langfuseを中心とした評価ドリブンなリリースサイクルのご紹介
fukubaka0825
2
320
データベースのオペレーターであるCloudNativePGがStatefulSetを使わない理由に迫る
nnaka2992
0
150
Featured
See All Featured
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
4
330
Making the Leap to Tech Lead
cromwellryan
133
9.1k
The World Runs on Bad Software
bkeepers
PRO
67
11k
Why Our Code Smells
bkeepers
PRO
336
57k
YesSQL, Process and Tooling at Scale
rocio
172
14k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
160
15k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
46
2.3k
Scaling GitHub
holman
459
140k
For a Future-Friendly Web
brad_frost
176
9.5k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
133
33k
Automating Front-end Workflow
addyosmani
1368
200k
Building an army of robots
kneath
303
45k
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