Slide 1

Slide 1 text

過去の失敗例から再考するモデル駆動設計 かとじゅん(@j5ik2o) レガシーをぶっつぶせ。現場でDDD! コラボカンファレンス

Slide 2

Slide 2 text

© Chatwork あなた誰? ● 加藤潤一(@j5ik2o) ● テックリード ○ 大阪本社(福島) 本日も業務扱い ○ レガシーシステム移行プロジェクトやってます ● 2009年11月に初めてEric本を読む。これ本当に技 術書なの?(笑)と思いながら読んでた(エンティティ の章) ● 2011年和訳本がでるときに翻訳レビューした。大 したことはしてない

Slide 3

Slide 3 text

© Chatwork 最近の活動 ● チケット料金モデリング ○ 映画の料金を決定するモデルを実装する ○ 値オブジェクトの設計に注力する ■ DBやWebAPIなどの入出力は一旦忘れる ○ 僕の実装 ○ 20人ぐらいがコードを書いてくれてた ■ セプテーニさん、オプトさんでも自主開催してくれた ● Qiita:ドメインロジックはドメインオブジェクトに凝集させる ○ 内部データをそのまま暴露するGetterは貧血症の温床になるので気 を付けろ的な話

Slide 4

Slide 4 text

© Chatwork アジェンダ ● 過去に僕が失敗した代表例から今ならどう設計する か、という観点でお話します。中心になるトピックは 以下です ○ 軽量DDDの功罪 ○ ドメインモデル貧血症対策 ○ 集約の境界定義の善し悪し ● ※失敗はうまくいかない方法を見つけただけ、成功へ のプロセス。Fail Fastで早く学ぶことが失敗の本質

Slide 5

Slide 5 text

© Chatwork これまでに経験した失敗(学び)談(代表例) ● CASE1: 軽量DDDで躓いた話 ○ というか、トランザクションスクリプト亜種 ● CASE2: 集約の境界定義を試行錯誤した話

Slide 6

Slide 6 text

© Chatwork CASE1: 軽量DDDで躓いた話 ● おこったこと ○ DDDを学びはじめた頃、軽量DDDを実践した。開発メンバとだ けユビキタス言語を共有したつもりだった… ● どうなったか ○ 開発チームの外側では全然異なる言語が使われてしまい、やは り通訳が必要になってしまった ○ 分析と設計(実装)が単一モデルになる前提ではないので、エン ティティはテーブルのことだと誤解し実装が手続き型に…。ト ランザクションスクリプト亜種になった 従来のフレームワークで経験した設計方法で判断してしまった

Slide 7

Slide 7 text

© Chatwork FYI: 軽量DDDとは ● 戦術的設計のパターンだけを利用 したDDDのこと ● ユビキタス言語を反映しないドメ インモデルが作られる可能性 ○ 共通言語基盤に基づかないソ フトウェアを語るときは通訳 が必要になってしまう ○ ソフトウェアが正しいか言語 によって証明できなくなる エンティティ サービス リポジトリ 戦術的パターン … 戦略的パターン ユビキタス言語 コアドメイン 境界づけられた コンテキスト コンテキストマッ プ こちらだけつまみ食い

Slide 8

Slide 8 text

© Chatwork 軽量DDD想定が、トランザクションスクリプト亜種に… ● この方法が一番簡単 ○ レイヤ化アーキテクチャのためにパッケージを切る ○ エンティティとサービスを作る(あれ?) ■ データはエンティティ(=テーブルモデル) ■ ロジックはサービス ○ データベースアクセスにリポジトリを使う ● 実は軽量DDDにもなっていない ○ エンティティはデータクラス、サービスはロジッククラス。 つまりトランザクションスクリプトになっている… ○ リポジトリはDAOの代わりになっているだけ… 貧血症オブジェクトになっている

Slide 9

Slide 9 text

© 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 { // ... }

Slide 10

Slide 10 text

© 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) )

Slide 11

Slide 11 text

© Chatwork FYI: ドメインモデル貧血症とは ● ドメインモデル貧血症 - Martin Fowler’s Bliki(ja) ● 本物のドメインモデルに見える。 ○ オブジェクト同士の関連があり、本物のような構造を持つ ○ ただし、オブジェクトの振る舞いはわずかしかない ○ ドメインロジックをドメインオブジェクトの中に入れないルールに 従っている。ドメインロジックはサービスオブジェクト群に存在する ● データと処理を統合するアプローチとは真逆 ○ 単なる手続き型設計(トランザクションスクリプト)と同じ ● アプリケーションサービスはドメインロジックを配置する場所ではない、 それはドメインオブジェクトの役割。アプリケーションサービスは進行役

Slide 12

Slide 12 text

© Chatwork ユビキタス言語を反映しないドメインモデル例 ● 実装都合の用語法で作られたオブジェクト群。ビジネス側とエン ジニア側で対応付けが難しくなり、通訳が必要となる ○ 注文ではなくリクエスト。あいまいな用語が通訳コスト ○ 注文ではなく注文テーブルと注文詳細テーブル。本来一つの 概念が複数のテーブルオブジェクトと対応づいている。利害 関係者と話すときは注意が必要 ● ビジネスロジックを持たないドメインオブジェクト ○ Getterしかないオブジェクト。どこでどんな計算が行われる かがわかにくい。ロジックの重複に気づけない

Slide 13

Slide 13 text

© Chatwork 放置しないで改善するには

Slide 14

Slide 14 text

© Chatwork 言語とモデルを作ってソフトウェアに反映する ● 利害関係者間で作る・使う ○ プロジェクトのすべての活動で ○ 要求分析で使う言語体系を設計 (実装)に反映する ● ユビキタス言語は要求分析から ○ 要件「…すること」 ○ 業務フローや利用シーン ○ ユースケースやイベント ● 右図を議論するなかで言語体系を揃 えていく。考えたモデルはコードに も反映する RDRA2.0ハンドブックより

Slide 15

Slide 15 text

© 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)

Slide 16

Slide 16 text

© 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))) // 月曜日体育の日

Slide 17

Slide 17 text

© 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) }

Slide 18

Slide 18 text

© 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) } }

Slide 19

Slide 19 text

© 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 }

Slide 20

Slide 20 text

© Chatwork スケーラブルな設計とは ● ドメインオブジェクトと言語の体系が一致している ● ドメイン上の値はただのプリミティブ型ではなく、値の範 囲などのルールとそれに基づく計算能力を持つようになっ た ● ドメインロジックの最小部品を値オブジェクトにして、組 み合わせてより高度な問題を解決する ● 不変である値オブジェクトは、想定外の挙動を起こさない ため、どんなに複雑な条件で組み合わせても、予測可能な 設計を作り出せる

Slide 21

Slide 21 text

© Chatwork CASE2: 集約の境界定義に試行錯誤した話 ● 例えば、Conversation, Participants, Messages。 Conversationの一部なのか外部 なのか ● テーブル脳で考えると、すべて が独立している集約。 ● 一方でオブジェクト脳で考える と、Conversationは会話の場を 表すのでParticipants, Messagesを包含した集約 ● どちらも試したことがある Conversation Participants Messages Conversation Participants Messages 全てが集約 集約は1つ

Slide 22

Slide 22 text

© Chatwork 例えば、Conversationのビジネスルール ● Conversationは会話を表し、契約プランによって以下の制約がある ○ 参加者数 ○ 投稿できるメッセージ数

Slide 23

Slide 23 text

© Chatwork すべてが集約の場合

Slide 24

Slide 24 text

© 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 = ??? } ● 最初に試した実装 ● テーブル脳ですべてを集約 として分けた例 ● ドメインモデルっぽい?ア プリケーションコードを書 いたら問題が明確になった

Slide 25

Slide 25 text

© 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) } アプリケーションサー ビスのコード

Slide 26

Slide 26 text

© 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) }

Slide 27

Slide 27 text

© Chatwork 集約が一つの場合

Slide 28

Slide 28 text

© 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)) } }

Slide 29

Slide 29 text

© Chatwork 集約が一つの場合(2/2) ● アプリケーションロジックもかなりシンプルになる ● このコードの意味を理解するのに、余計なドキュメントはいらない def addParticipantAndMessage(conversation: Conversation, participant: Participant, message: Message): Conversation = conversation .addParticipant(participant) .addMessage(message)

Slide 30

Slide 30 text

© Chatwork 集約の境界定義の善し悪し ● 大前提 ○ ユースケースを実現できること Pros Cons すべてが集 約 ● ロックの競合頻度が下がる ● 必要に応じてI/Oできる。レ イテンシが有利 ● 細かく分けすぎると貧血症に なりやすい ● 結果整合性で厳密なビジネ スルールは強制できない 集約がひと つ ● ドメイン知識がドメインオブ ジェクトに凝集できる ● ロックの競合頻度が上がる ● I/Oレイテンシが不利

Slide 31

Slide 31 text

© Chatwork 集約の境界定義に必要な観点 ● ユースケースだけでは境界定義は定まらない ● 集約の粒度がレイテンシに影響を与えるのは事実。説明が 難しいモデルは実装も大きくなり、I/Oにもコストも大きく なる傾向にある ● 振る舞いにフォーカスし小さい集約を実現するには、クエ リ責務を他に委譲する必要がある(CQRS, EventSourcing) Conversation def removeMessages(messageIds): Conversation private val messageIds: MessageIds 振る舞いを起こすために 必要な状態しか持たない のが理想

Slide 32

Slide 32 text

© Chatwork FYI: Event Storming + 値オブジェクト・モデリング ● 発見の順序は番号の流れ。Event Storming(Desingn Level)では③ま でだが、そこから先はロジックの主役は値オブジェクトに注目する コマンド イベント ③集約 エンティティ ④値オブジェクト ユースケース コマンド ②コマンド イベント ①イベント 状態遷移 振る舞い 利用 必要に応じてイベントを 使ってリードモデルを構 築する アクター

Slide 33

Slide 33 text

© Chatwork まとめ: これまでを振り返って思うこと ● 設計は仮説・検証・フィードバックの連続 ○ FYI: PDR(Prep→Do→Review→PDR) をやっていこう。PDCAは生産管理用 ● 不確実性コーンを見極めながら、納期だけ ではなく設計もFail Fastしていこう。うま くいかなかったら、それは学びの機会 ● 自分一人で解決できなければ、自ら情報を 共有しながら、コミュニティからヒントを 得られるようにしてきたい 不確実性コーン (アジャイルな見積もりと計画作りより )

Slide 34

Slide 34 text

© Chatwork ご清聴ありがとうございました!

Slide 35

Slide 35 text

© Chatwork 例えば、こんなマネージャと一緒に働けます! @j5ik2oまでメッセージください!

Slide 36

Slide 36 text

No content