Slide 1

Slide 1 text

PHPで はじめるCQRS @dnskimox

Slide 2

Slide 2 text

自己紹介 ✘ 本名:丹賀健一 ✘ 通称:男爵 ✘ dnskimo ✘ dnskimox ✘ ソフトウェアエンジニア ✘ 北海道在住 ✘ 株式会社インフィニットルー プ ✘ ソシャゲバックエンドAPI開 発 ✘ ブラウザゲーム開発

Slide 3

Slide 3 text

今日話すこと ✘ コマンドクエリ分離原則(CQS) ✘ コマンドクエリ責務分離(CQRS) ✘ CQRSをPHPで実践してみた

Slide 4

Slide 4 text

今日話さないこと ✘ イベントソーシングと結果整合性 ✘ 更新用DBと参照用DBの分離と同期 ✘ RDB以外のミドルウェアを使った事例

Slide 5

Slide 5 text

コマンドクエリ分離原則 命令と問い合わせを分離する

Slide 6

Slide 6 text

バーランド・メイヤーのコマンドクエリ分離原則 (CQS) ✘ クラス設計の原則 ✘ あらゆるクラスの特性はコマンド (命令)とクエリ(問い合わせ)に分 けられる ✘ 両者を明確に区別することで、単 純で読みやすいソフトウェアを作り 出し、信頼性、再利用性、拡張性 を飛躍的に向上させることができ る https://en.wikipedia.org/wiki/Bertrand_Meyer

Slide 7

Slide 7 text

コマンドクエリ分離原則(CQS) コマンド ✘ オブジェクトの状態を 書き換える ✘ 返り値をもたない クエリ ✘ オブジェクトに関する 情報を返す ✘ 副作用をもたらしては ならない

Slide 8

Slide 8 text

副作用(side effect)= クエリにおい て、「問い合わせに答える」という本来 の目的に付随する変更

Slide 9

Slide 9 text

副作用(side effect)≒ オブジェクトの 状態の変更や、システムの外界に与 えられる影響

Slide 10

Slide 10 text

getLevel(); // 1 if ($character->canLevelUp()) { echo “This character can level up!”; } $character->getLevel(); // 2

Slide 11

Slide 11 text

質問をすることで回答を変 化させてはならない

Slide 12

Slide 12 text

0); $this->exp += $exp; // 経験値100毎にレベルアップ while ($this->level < floor($this->exp / 100) + 1) { $this->level++; } } }

Slide 13

Slide 13 text

level; } // クエリの例 public function getExpForNextLevel(): int { return 100 - $this->exp % 100; } }

Slide 14

Slide 14 text

getLevel(); $character->gainExp(100); if ($before_level < $character->getLevel()) { printf(“Level up! Next exp is %d”, $character->getExpForNextLevel()); }

Slide 15

Slide 15 text

クエリの性質 ✘ クラスの不変条件を壊す心配がない(信 頼性) ✘ 副作用がないので様々な用途に使える (再利用性) ✘ 状態を変更しないので継承によるバリ エーションを作りやすい(拡張性)

Slide 16

Slide 16 text

副作用をコマンドに局所化し、クエリの世 界を広げることにより、ソフトウェアの信 頼性・再利用性・拡張性を向上させること ができる

Slide 17

Slide 17 text

CQRSとはなにか? アーキテクチャレベルの視点

Slide 18

Slide 18 text

グレッグ・ヤングのコマンドクエリ責務分離 (CQRS) ✘ コマンドクエリ分離原則に基づい たアーキテクチャパターン ✘ システムのユースケースはコマ ンド(更新)とクエリ(参照)に分類 できる ✘ コマンドとクエリにはそれぞれ非 常に異なるニーズがあるので分 離すべき https://www.developerfusion.com/event/153843/special-guest-greg-young-on-tue-aug-13th/

Slide 19

Slide 19 text

WEBアプリケーションにおけるコマンド・クエリ の分類 コマンド ✘ レコードの追加・更新・削 除 ✘ HTTP: PUT/POST/DELETE クエリ ✘ レコードの参照・集計・検 索 ✘ HTTP: GET

Slide 20

Slide 20 text

異なるニーズ:一貫性 コマンド ✘ トランザクション処理 によるアトミックな実 行 ✘ データを適切にロック する必要がある クエリ ✘ データの不整合を起 こす心配がない

Slide 21

Slide 21 text

異なるニーズ:データストレージ コマンド ✘ 第3正規形のテーブ ルを使うことが多い ✘ マスターDBを参照 クエリ ✘ 非正規化したデータ のほうが都合が良い が場合がある ✘ スレーブDBを参照

Slide 22

Slide 22 text

異なるニーズ:スケーラビリティ コマンド ✘ 一般的にリクエストの 割合が低い ✘ スケーラビリティは必 ずしも重要ではない クエリ ✘ 一般的にリクエストの 割合が高い ✘ スケーラビリティが非 常に重要

Slide 23

Slide 23 text

伝統的なMVCアーキテクチャにつ いて考える

Slide 24

Slide 24 text

伝統的なMVCアーキテクチャの例 ✘ 関心事によってコードを複数のモジュールに分ける ✘ ModelはDBアクセスとドメインロジックを担当する(例 :ActiveRecordパターン、DataMapperパターン) ✘ ViewはHTMLやJSON等、レスポンスの表現を定義する ✘ Controllerはリクエストを読み取り、ModelとViewの助けを 借りてレスポンスを作る

Slide 25

Slide 25 text

伝統的なMVCアーキテクチャ

Slide 26

Slide 26 text

何が問題か?

Slide 27

Slide 27 text

コマンドの大まかな流れ

Slide 28

Slide 28 text

クエリの大まかな流れ

Slide 29

Slide 29 text

全く異なるニーズを持つ処理が同じ モジュールで処理されている!

Slide 30

Slide 30 text

コマンドの実装で困ること ✘ Modelクラスに複雑な検索やページネー ション等、参照のためのロジックが入り込 んで肥大化する

Slide 31

Slide 31 text

クエリの実装で困ること ✘ ORMの検索メソッドで最適なSQLを発行す るのは非常に困難 ✘ 非正規形データはModelのオブジェクト構 造と一致しない(インピーダンスミスマッ チ) ✘ N+1クエリ問題への配慮が必要 本来クエリの実装は単純なはずでは??

Slide 32

Slide 32 text

関心を分離せよ OOPの原則

Slide 33

Slide 33 text

コマンドの責務を持つモジュール と、クエリの責務を持つモジュール に分離する

Slide 34

Slide 34 text

CQRS (Command Query Responsibility Segregation = コマンドクエリ責務分離)

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

コマンドの大まかな流れ

Slide 37

Slide 37 text

クエリの大まかな流れ

Slide 38

Slide 38 text

クエリ側はドメインレイヤー を迂回する

Slide 39

Slide 39 text

そもそも何故ドメインレイヤーが必要か?

Slide 40

Slide 40 text

ドメインロジックを書くため

Slide 41

Slide 41 text

ドメインロジックの例 ✘ キャラクターは経験値100毎にレベルが1上がる ✘ キャラクターは装備を10個まで装着できる ✘ キャラクターがモンスターを攻撃した際のダメージの 計算式は以下である モンスターの防御力 - (レベル × 10 + 武器の攻撃力) ✘ キャラクターのIDの表示フォーマットはPXXXXX(0埋め 5桁)である

Slide 42

Slide 42 text

ドメインロジック = ソフトウェアが扱う問題領域に固有のロジック

Slide 43

Slide 43 text

これはコマンドとクエリどちらの話か?

Slide 44

Slide 44 text

ドメインロジックの例 ✘ キャラクターは経験値100毎にレベルが1上がる ✘ キャラクターは装備を10個まで装着できる ✘ キャラクターがモンスターを攻撃した際のダメージの 計算式は以下である モンスターの防御力 - (レベル × 10 + 武器の攻撃力) ✘ キャラクターのIDの表示フォーマットはPXXXXX(0埋め 5桁)である コマンド コマンド コマンド クエリ

Slide 45

Slide 45 text

(一般的に)複雑なドメインロジックはコマンド 側に多く存在する

Slide 46

Slide 46 text

(一般的に)クエリ側はドメインレイヤーを実装 するメリットが少ない

Slide 47

Slide 47 text

代わりに何を実装するか?

Slide 48

Slide 48 text

Thin Read Layer(薄い読み取り層)

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

薄い読み取り層とは? ✘ DBに直接依存するレイヤー(必要ならDB ベンダーと結びついても良い) ✘ 小さな規約をベースとしたマッピングユー ティリティ ✘ ORMほどの機能性は要らない

Slide 51

Slide 51 text

CQRSを適用すると何が起きるか?

Slide 52

Slide 52 text

コマンドの実装はこうなる ✘ ドメインレイヤーは更新系の複雑なドメイ ンロジックに集中できる

Slide 53

Slide 53 text

クエリの実装はこうなる ✘ ドメインモデルに捕らわれること無く、欲し い情報を取得するための最適な方法を選 択できる ✘ 副作用のない参照処理を扱うだけのシン プルなコードにできる

Slide 54

Slide 54 text

PHPでCQRSを実践する 要点を踏まえて手軽な方法を考える

Slide 55

Slide 55 text

導入したプロジェクトの概要 ✘ HTMLベースのブラウザゲーム ✘ 構成的には一般的なWEBサイトと同じ ✘ 旧ナンバリングタイトルからの共用レガ シーコードあり

Slide 56

Slide 56 text

どのように実践したか? 1. 伝統的なMVCを変形させる 2. SQLが持つ本来の力を引き出す 3. Value Objectを使ってドメインロジックを共有 する

Slide 57

Slide 57 text

1. 伝統的なMVCを変形させる

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

コマンドの大まかな流れ

Slide 60

Slide 60 text

クエリの大まかな流れ

Slide 61

Slide 61 text

クエリはORM(ActiveRecord)を迂回する

Slide 62

Slide 62 text

ActiveRecordやDataMapperがDBのレコードを オブジェクトにマッピングするのは何のため か?

Slide 63

Slide 63 text

Domain Modelパターンを実装するため

Slide 64

Slide 64 text

Domain Modelパターン ✘ ドメインロジックを記述する設計パターン の一つ ✘ データとドメインロジックの両方を持つオ ブジェクトを設計する

Slide 65

Slide 65 text

Transaction Scriptパターン ✘ ドメインロジックを記述する設計パターン の一つ ✘ ロジックを持たないオブジェクトを使って、 手続き的にドメインロジックを記述する ✘ ドメインモデル貧血症

Slide 66

Slide 66 text

exp += 100; while ($character->level < floor($character>exp / 100) + 1) { $character>level++; } } }

Slide 67

Slide 67 text

データとロジックを一体化する OOPの原則

Slide 68

Slide 68 text

ORMを使ってDomain Modelパターンを実装すること で、OOPの力が最大化される

Slide 69

Slide 69 text

ORMを使うとSQLの力が制限される BUT

Slide 70

Slide 70 text

2. SQLが持つ本来の力を引き出す

Slide 71

Slide 71 text

SQLの特性 ✘ 検索・集計が得意 ✘ 非正規形のデータを簡単に作れる ✘ 集合関数、CASE式等を使って「何が欲し いか?」を宣言的に記述できる ✘ パフォーマンスチューニングしやすい(ORMに 比べれば)

Slide 72

Slide 72 text

SQLは「集合」を扱う上で 強力な言語である

Slide 73

Slide 73 text

SQLの結果を連想配列で扱うのは避けたい とはいえ

Slide 74

Slide 74 text

SQLの結果とPHPオブジェクトの単純なマッパー (Thin Read Layer)を用意する

Slide 75

Slide 75 text

QueryModel(クエリのためのModel) ✘ PDOStatementをオブジェクトにマッピングす るだけ ✘ 定義されていないプロパティへのアクセス は禁止(静的解析可能) ✘ プロパティの書き換えは出来ない(イミュー タブル)

Slide 76

Slide 76 text

['type' => 'type'], 'name' => ['type' => 'string'], 'friend_count' => ['type' => 'int'], ]; }

Slide 77

Slide 77 text

select('character_tbl.character_id', 'name', 'COUNT(friend_id) AS friend_count') ->leftJoin('friend_tbl', 'USING', 'character_id') ->where('character_tbl.character_id', $character_id) ->group_by('character_id') ->get_one(); return self::inflate($result); } }

Slide 78

Slide 78 text

3. Value Objectを使ってドメインロ ジックを共有する

Slide 79

Slide 79 text

No content

Slide 80

Slide 80 text

Value Object(値オブジェクト) ✘ 開発者が定義したある種の値を表すオブ ジェクト ✘ プリミティブなデータ型を独自に作るような もの ✘ 全てのメソッドがクエリである(イミュータブ ル) ✘ 参照系のドメインロジックを持たせることが 可能

Slide 81

Slide 81 text

Value Objectの例 ✘ Money(通貨、金額) ✘ MyDate(年、月、日) ✘ Address(都道府県、市町村、番地...) ✘ Status(HP、MP、ATK、DEF、MATK...) ✘ CharacterId(数値)

Slide 82

Slide 82 text

ドメインロジックの例 ✘ キャラクターは経験値100毎にレベルが1上がる ✘ キャラクターは装備を10個まで装着できる ✘ キャラクターがモンスターを攻撃した際のダメージの 計算式は以下である モンスターの防御力 - (レベル × 10 + 武器の攻撃力) ✘ キャラクターのIDの表示フォーマットはPXXXXX(0埋め 5桁)である

Slide 83

Slide 83 text

character_id = $character_id; } public function formatted(): string { return sprintf("P%05d", $this->character_id); } }

Slide 84

Slide 84 text

character_id); } }

Slide 85

Slide 85 text

character_id); } }

Slide 86

Slide 86 text

getId()->formatted(); // F00123 $character_summary = CharacterSummary::findById(123); $character_summary->getId()->formatted(); // F00123

Slide 87

Slide 87 text

導入から半年後のチームの声 +自分の感想

Slide 88

Slide 88 text

パフォーマンスチューニングしやすい ✘ クエリはSQLの改善に集中できる ✘ 必要ならキャッシュを挟むのも簡単

Slide 89

Slide 89 text

バグの原因を調査しやすい ✘ バグ発生時のURLを見ただけで調査対象 のコードベースが半減する ✘ 処理の流れが決まっているので、他人が 書いたコードも追いやすい

Slide 90

Slide 90 text

従来通りの書き方で済む場所も結構ある ✘ ORMからfindしたModel+関連Modelを表示 するだけで良いページ ✘ 編集系のページのデフォルト入力値は正 規形のデータが求められる

Slide 91

Slide 91 text

生SQLを書くのは楽しい ✘ 集合関数やCASE式を自由に使える ✘ 複雑なSQLをORMにどう発行させるかを考 えなくて良い

Slide 92

Slide 92 text

まとめ PHPでCQRSのはじめかた

Slide 93

Slide 93 text

PHPでCQRSのはじめかた ✘ コマンドクエリ分離原則を意識してクラス を設計する ✘ アーキテクチャレベルで更新と参照の責 務を分離する ✘ ドメインロジックを持つValueObjectを見つ ける ✘ SQL本来の力を活用する

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

参考資料 ✘ Greg Young流CQRSの和訳版 ✘ Greg Young流CQRS - Mark Nijhof ✘ 副作用を最小限に抑えるために必要なこと ✘ オブジェクト指向入門 第2版 方法論・実践 ✘ Patterns of Enterprise Application Architecture ✘ 達人に学ぶSQL徹底指南書 ✘ Domain Model- P of EAA Catalog ✘ Transaction Script - P of EAA Catalog ✘ ValueObject - Martin Fowler