Slide 1

Slide 1 text

ゆるふわ CQRS 入門 吉田あひる

Slide 2

Slide 2 text

自己紹介 名前: 吉田あひる (@strtyuu) 所属: スターフェスティバル株式会社 仕事: KAGUYAというチームで設計したり、コード書いたり PHPer Tea Night というイベントを共同運営しています。 2

Slide 3

Slide 3 text

はじめに 時間の関係上、前提知識の説明をたくさん省いています!(ごめんなさい!) 不明な点があれば是非あとから質問いただけると嬉しいです! 3

Slide 4

Slide 4 text

お品書き 商品申請機能開発中のお悩み紹介 CQRSの紹介 ゆるふわCQRSの紹介 メリット・デメリット 4

Slide 5

Slide 5 text

商品申請の開発例をご紹介 商品申請機能開発中のお悩み紹介 5

Slide 6

Slide 6 text

商品申請とは? ごちクルなどへ商品を展開するために、管理基盤に商品登録をする必要がある レギュレーションをクリアした内容になっていることを保証したいため、登録・ 変更に社内審査を挟みたい 商品申請機能開発中のお悩み紹介 6

Slide 7

Slide 7 text

集約の設計時の悩み 商品申請機能開発中のお悩み紹介 7

Slide 8

Slide 8 text

極力小さい集約にしたい 集約の大きさ = トランザクションの大きさなので、トランザクションを小さくし たい 申請のトランザクションに商品を含めたくない 申請の段階では気にしなくていい属性も商品には含まれる そういった商品側の属性の増減で申請ドメインに影響が出る可能性を減らし たい 商品申請機能開発中のお悩み紹介 8

Slide 9

Slide 9 text

// 商品申請集約 type ProductApplication = { id: string; productId: string; // 申請と紐づく商品 reviewStatus: ReviewStatus; // 審査ステータス content: ApplicationContent; // 申請内容色々 } // 商品集約 type Product = { id: string; productName: string; // ... } 商品申請機能開発中のお悩み紹介 9

Slide 10

Slide 10 text

表示のことを考えると、大きい集約の方が楽... だいたい申請と商品をペアで表示したくなるんだよなぁ 一覧表示のときとかにアプリケーション側でガチャガチャしたくないなぁ // 商品申請集約 type ProductApplication = { id: string; product: Product; // 商品Entity を丸ごと持つ reviewStatus: ReviewStatus; content: ApplicationContent; } 商品申請機能開発中のお悩み紹介 10

Slide 11

Slide 11 text

どうしようかな? トランザクションの大きさを妥協する? 表示のときにアプリケーション側でがんばって複数の集約を組み合わせる? 商品申請機能開発中のお悩み紹介 11

Slide 12

Slide 12 text

解答の1つが CQRS CQRSの紹介 12

Slide 13

Slide 13 text

CQRSとは? Command and Query Responsibility Segregation (コマンドクエリ責務分離) Greg Young氏が提案 書き込み(Command)と表示(Query)で要求が異なる点に着目 書き込みのモデルと表示のモデルをそれぞれ別に構築しよう、というアイデア CQRSの紹介 13

Slide 14

Slide 14 text

要求の違い 書き込み データの整合性を正しく保ちたい 書き込みの競合などが極力起こらないようにしたい 表示 高速にレスポンスしたい データを様々な表現に変換したい CQRSの紹介 14

Slide 15

Slide 15 text

15

Slide 16

Slide 16 text

CQRSのメリット 書き込みと読み込みをそれぞれ最適化された構造にできる Event Sourcingで組めば書き込みもRDBよりも圧倒的にスケール可能に でも大変じゃ...? Event Sourcing 大変じゃない...? RDB一つだけでやりくりしたいんですけど! Kafka とかドキュメントDBとかよくわかんないよ〜! CQRSの紹介 16

Slide 17

Slide 17 text

ゆるく CQRS のエッセンスを取り入れよう ゆるふわCQRSの紹介 17

Slide 18

Slide 18 text

ゆるふわ CQRS アプリケーションモデルにだけエッセンスを適用 書き込みと読み込みでモデルを分ける データソースは RDBMS 1つ Event Sourcingは採用しない! 読み込みモデル -> 書き込みモデルの依存は少しなら許しちゃう ※ Segregation出来てないので、 CQRSと呼んだら怒られるかもしれない ゆるふわCQRSの紹介 18

Slide 19

Slide 19 text

19

Slide 20

Slide 20 text

書き込みモデルの構成要素 Entity 集約 Repository Value Object Domain Service etc... 今日は割愛 ゆるふわCQRSの紹介 20

Slide 21

Slide 21 text

読み込みモデルの構成要素 ReadModel Presenter QueryService ゆるふわCQRSの紹介 21

Slide 22

Slide 22 text

ReadModel 単なるDTO 値をどのような表示にするかはユースケースによって結構変わるので 値の加工の責務はPresenterに寄せた方が拡張性高いと思う 表示の都合に合わせたデータ構造を取る 個人的にはREST APIのリソースと1:1で対応づけるイメージで作ることが多い 他のReadModelもプロパティとして持っていてもOK ゆるふわCQRSの紹介 22

Slide 23

Slide 23 text

ReadModel type ProductApplication = { id: string; product: Product; // Read Model の商品 reviewStatus: ReviewStatus; content: ApplicationContent; } ゆるふわCQRSの紹介 23

Slide 24

Slide 24 text

Presenter Presentationロジックの責務を担当するやつ 任意の値を受け取り、別の表現に変換する ReadModel -> HalJSON 複数の注文ReadModelを注文日ごとにまとめる etc 純粋関数にする I/Oを発生させない 内部でQueryServiceを呼ばない(引数として渡す形にしよう) 表現の種類ごとに自由に作っていい ゆるふわCQRSの紹介 24

Slide 25

Slide 25 text

Presenter type HalJsonProductApplication = { id: string; // ... _embedded: { product: HalJsonProduct; }; _links: { self: { href: string }; }; } class HalJsonProductApplicationPresenter { make(application: ProductApplication): HalJsonProductApplication { // 変換処理 } } ゆるふわCQRSの紹介 25

Slide 26

Slide 26 text

QueryService データストアからReadModelを取得する君 RDBMS Elasticsearch Web API etc Interface を切ってもいいし切らなくてもいい モックしたいなどの要求があれば Interface にしておいた方が無難 ReadModel と 1:1 で作るイメージ ゆるふわCQRSの紹介 26

Slide 27

Slide 27 text

QueryService interface ProductApplicationQueryService { // 詳細画面などで必要になる、ID が一致するReadModel を引っ張ってくるやつ findOne: (id: string) => Promise; // 一覧画面のページネーションとかで必要になるやつ paginate: (page: number, perPage: number) => Page; // 複雑な検索などもOK relatedApplications: (id: string) => Promise } // インターフェースを切るならデータソースごとに実装を作るイメージ class MySqlProductApplicationQueryService implements ProductApplicationQueryService { // ... } class InMemoryProductApplicationQueryService implements ProductApplicationQueryService { // ... } ゆるふわCQRSの紹介 27

Slide 28

Slide 28 text

Controller などはこんな感じに... class ShowProductApplicationAction { private readonly presenter: HalJsonProductApplicationPresenter; private readonly queryService: ProductApplicationQueryService; async invoke(req: Request): Promise { const application = await this.queryService.findOne(req.query('id')); if (application === null) { return new Response(404); } const jsonString = JSON.stringify(this.presenter.make(application)); return new Response(200, jsonString); } } ゆるふわCQRSの紹介 28

Slide 29

Slide 29 text

メリット アプリケーションレベルのモデルは分離されているため、書き込みと読み込みそ れぞれの要求に対してシンプルな構造をとれる ガチCQRSに比べて実装コスト/インフラコストを抑えられる 集約の設計に悩む時間が減る ゆるふわCQRSの紹介 29

Slide 30

Slide 30 text

デメリット 構成要素が増えるため、それぞれの構成要素がシンプルでも全体の複雑性は上が りがち 最近よく聞く Complex と Complicated の違い CRUD+αくらいで済むならActive Recordとかで普通に作る方が良さそう あくまでなんちゃってのゆるふわなので、CQRS本来の目的をすべて達成できるわ けではない ゆるふわCQRSの紹介 30

Slide 31

Slide 31 text

まとめ Active Recordは避けたい、けど読み込みと書き込みで違う構造を取りたいという 時の選択肢の一つ 一部のモデルにだけ適用するというのも可能 これはガチCQRSもそう デメリットももちろんあるのでご利用は計画的に おわりに 31