Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPとEventSauceで始めるイベントソーシングアプリケーション

Df4978f14401325586e9e286b140ac4c?s=47 n1215
February 10, 2020

 PHPとEventSauceで始めるイベントソーシングアプリケーション

2019/02/10(月) PHPerKaigi 2020 の発表資料です。
https://phperkaigi.jp/2020/

サンプルコードはこちら:
https://github.com/n1215/eventsauce-example

Df4978f14401325586e9e286b140ac4c?s=128

n1215

February 10, 2020
Tweet

Transcript

  1. for PHPerKaigi 2020 PHPとEventSauceで始める イベントソーシングアプリケーション 2020年2⽉10⽇ (⽉) 株式会社Nextat 中榮健⼆ Nextat

    Inc. 1
  2. ⾃⼰紹介 京都から来ました - 中榮健⼆ (なかえけんじ) - twitter: @n_1215  - 株式会社Nextat

    取締役 - Laravel/Unityでソーシャルゲーム開発メイン → 最近はごった煮 - ここ最近はサーバサイドTypeScriptやインフラのコード Nextat Inc. 2
  3. 発表概要 1. はじめに 2. イベント 3. イベントソーシング 4. CQRSとの関係性 5.

    EventSauce⼊⾨ 6. なぜ今イベントソーシングなのか 7. まとめ Nextat Inc. 3
  4. 1. はじめに Nextat Inc. 4

  5. 会場の皆様に質問 イベントソーシングをご存じの⽅? イベントソーシングを取り⼊れたシステムを実運⽤している⽅? ソシャゲをやったことがある⽅?(サンプルコードの都合) Nextat Inc. 5

  6. 本スライドのサンプルコードについて スライドの都合上で省略多し(あとでGitHubに上げる予定) よくあるソーシャルゲームを想定 ガチャなどからキャラクターを⼊⼿ ユーザがキャラクターを強化や限界突破などで育てて ストーリーを進めてクエストに挑戦、バトルしたりする Lv. 80 ☆☆☆☆ Nextat

    Inc. 6
  7. 2. イベント Nextat Inc. 7

  8. イベントとは 実際に起きた事象を表す ユーザ所持キャラクターが獲得された ユーザ所持キャラクターが強化された ユーザ所持キャラクターが限界突破した Lv. 1 → Lv.3 Nextat

    Inc. 8
  9. イベントとイベントリスナ ◦◦したとき××するという要件を実現するためのパターン ユーザー所持キャラクターが獲得された時、アイコンをユーザに付与 ユーザー所持キャラクターが獲得された時、履歴を残す イベントを起こした処理とイベントに対応する処理(イベントリスナ)を疎 結合に Nextat Inc. 9

  10. イベントなしの実装例 ユーザがキャラクターを獲得する処理 ユーザアイコンサービス、獲得履歴サービスに直接依存 class UserCharacterService { public function add(UserId $userId,

    Character $character): void { // ユーザが所持キャラクターを獲得 $userCharacter = $this->userCharacterFactory->initiate($userId, $character); $this->userCharacterRepository->persist($userCharacter); // ユーザにアイコンを付与 $this->userIconService->addCharacterIcon($userCharacter); // キャラクター獲得履歴を残す $this->userCharacterAcquisitionHistoryService->add($userCharacter); } } Nextat Inc. 10
  11. イベントを利⽤する実装1 イベントの発⽕ // ユーザ所持キャラクター獲得イベント class UserCharacterAcquired implements Event { //

    略 } class UserCharacterService { public function add(UserId $userId, Character $character): void { // ユーザが所持キャラクターを獲得 $userCharacter = $this->userCharacterFactory ->initiate($userId, $character); $this->userCharacterRepository->persist($userCharacter); // イベントを発⽕ $event = new UserCharacterAcquired($userCharacter); $this->eventDispatcher->fire($event); } } Nextat Inc. 11
  12. イベントを利⽤する実装2 イベントリスナ // ユーザにアイコンを付与するイベントリスナ class AddUserCharacterIcon { public function handle(UserCharacterAcquried

    $event): void { $this->userIconService ->addCharacterIcon($event->getUserCharacter()); } } // 履歴を残すイベントリスナ class RecordCharacterAcquisition { public function handle(UserCharacterAcquried $event): void { $this->userCharacterAcquisitionHistoryService ->add($event->getUserCharacter()); } } $eventDispatcher->listen( // 別途、イベントにイベントリスナを関連づける UserCharacterAcquired::class, [AddUserCharacterIcon::class, RecordCharacterAcquisition::class] ); Nextat Inc. 12
  13. イベントを利⽤しない場合と⽐較 ユーザアイコンサービス、獲得履歴サービスに直接依存しない ユーザ所持キャラクター獲得時の関連処理を変更・追加しやすい イベントリスナはキューなどを⽤いて⾮同期で動作させることも可能 履歴のロギングが多少遅れても良い場合、レスポンスは早く返せる 疎結合とシステム全体としての複雑さ(記述量も増加)のトレードオフ アプリケーションの外部拡張のフックポイント CMSやWeb FWなどでも活⽤されている Nextat

    Inc. 13
  14. 3. イベントソーシング Nextat Inc. 14

  15. 昔ながらのステートソーシング user_characters テーブル ユーザ所持キャラクターID キャラクターID(マスタID) レベル 限界突破数 ... ... ...

    ... 1234 1001 50 3 ... ... ... ... 変更があるたびにテーブルのレコードを更新 ステートソーシング:最新の状態をデータストアに保存する⽅式 Nextat Inc. 15
  16. イベントソーシング イベントストア (イベントを記録しておくデータストアのこと) ユーザ所持キャラクターID イベント イベントのデータ 1234 LimitBreak {"before":0, "after":1}

    1234 Enhanced {"experience": 50000} 1234 Acquired {"from": "gacha", "characterId": 1001} イベントソーシング:起こったイベントをデータストアに保存する⽅式 起こったイベントを時系列順に再実⾏(リプレイ)して最新の状態を得る 最新の状態はイベントが積み重なった結果に過ぎない Nextat Inc. 16
  17. ところで アプリケーションで起こった出来事を記録する、といえば? Nextat Inc. 17

  18. 履歴(主にCSやデバッグ⽤) 「いつ何が起こったか後から確認できるようにしておいて!」 ↓ ぼく「わかりました」 history DB user_character_acquisition_logsテーブル user_character_enhancement_logsテーブル user_character_break_limit_logsテーブル Nextat

    Inc. 18
  19. つまり履歴がイベントだったんだよ!!!! な、なんだってー Nextat Inc. 19

  20. 状態と履歴の関係 雑に⾔えば、履歴と状態の主従を逆転させるとほぼイベントソーシング 状態は履歴の積分、履歴は状態の微分のようなもの 全ての履歴を残しておけばそこから再計算して最新の状態を得ることができる ex) クエストへの⽇別挑戦回数をクエストの履歴から算出 イベントソーシングに近しい記録⽅法を採⽤すると嬉しい機能の例 ゲーム内通貨 ⼈気投票 null許容

    or ミュータブルなカラムを減らしてイベントのようになる例 バトルテーブル(終了⽇時にnull許容) → バトル開始+バトル結果テーブル プレゼントテーブル → プレゼント配布 + プレゼント受取テーブル Nextat Inc. 20
  21. ⾝近なイベントソーシング イベントソーシング⾃体は古くからある考え⽅ バージョン管理システム データベース管理システムのトランザクションログによるリカバリ 家計簿の出⾦と⼊⾦の記録 ⇨ 過去の状態や変更を確認、再現できるというメリット Nextat Inc. 21

  22. イベントソーシングのメリットとデメリット メリット 監査⽤に向く。証跡を残せる 状態が変化した理由や経緯がわかる 過去の状態も再現できる イベントのデータは不変。イベント⽤のデータストアが追記専⽤になる デメリット 最新状態を得るためのリプレイのコストがかかるためパフォーマンスや検索に難 データ量の増加とストレージのコスト 開発者のマインドセットの⼤きな変更を要求

    (多くの場合)インフラ構成の変更が必要 Nextat Inc. 22
  23. 4. CQRSとの関係性 Nextat Inc. 23

  24. ご注意 ⽤語の混乱・混同が多いのでじっくり⾏きます Nextat Inc. 24

  25. CQS (コマンドクエリ分離) Command Query Separation すべてのメソッドはアクションを実⾏するコマンドまたは呼び出し側にデータを 返すクエリのいずれかでなくてはならず、双⽅であってはならない。 要するに、質問をしたことで答えを変化させてはならない コマンド(状態を変化させる副作⽤を持つ処理)+ クエリ(データの参照)

    クエリは参照透明であれ メソッドレベルでのコマンドとクエリの純粋性を要求 by Bertrand Meyer プログラミング⾔語Eiffelの開発者 契約による設計(Design by Contract / DbC)の創始者 Nextat Inc. 25
  26. CQRS (コマンドクエリ責務分離) Command Query Responsibility Segregation クエリ側のモデルとコマンド側のオブジェクト(モデル)を分離 リードモデル(クエリモデル) ライトモデル(コマンドモデル) クエリ側の要求が変わっても、コマンド側への影響を最⼩限にした修正が可能

    by Greg Young DDDへのイベントソーシング+CQRSの適⽤。昨今のESブームの⽕付け役 Nextat Inc. 26
  27. CQS と CQRSの違い CQRSはMeyer⽒のコマンドとクエリと同じ定義を使いますが、この2つは純粋 であるべきという視点を貫いています。CQRSでは、CQS と異なり、オブジェク トを2 種類に分けます。(複数の)コマンドを持つオブジェクトと、(複数の)クエリ を持つオブジェクト です。

    CQRS Documents by Greg Young https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf ⽇本語訳付: http://www.minato.tv/cqrs/cqrs_documents_jp.pdf オブジェクトが分けられる → システム・アーキテクチャレベルでの分離も可能 イベントソーシングとの相性◎ Nextat Inc. 27
  28. イベントソーシングとCQRSの併⽤ コマンド側はイベントを扱うが、クエリ側は最新の状態さえあればいい いちいちイベントをフル再⽣して最新の状態を取得すると遅い+検索に弱い イベント群からクエリ側で求められるデータを作る(Projection)のは容易 イベントソーシングによるライトモデル 別⽴てで最新状態の記録を元にするリードモデル クエリ側の要求によるコマンド側への影響を最⼩化 Nextat Inc. 28

  29. イベントソーシング + CQRS コマンド側でイベントを永続化し、クエリ側にメッセージを送って⾮同期で反映 コマンド側とクエリ側が疎結合になり、それぞれの都合で融通がきく Nextat Inc. 29

  30. おまけ. CQRSにまつわるよくある誤解 CQRS導⼊の⼼理的ハードルが⾼い理由? CQRSはイベントソーシングと⼀緒に採⽤しなければならない ステートソーシング + CQRSという選択肢 反例) ランキング集計処理。最新の状態をRDBに保存、Redisにもデータを保 存しランキングの読み取りはRedisを元にする

    反例) ECサイトの複雑な商品検索。最新の状態をRDBに保存、検索は最新の 状態から計算したElasticsearchのデータを元にする CQRSはデータストアを分けなければならない 反例) ライトモデルはRDB、リードモデルもRDBだが別のテーブルやビュー 反例) 同じテーブルを参照するがリードモデルとライトモデルが別のクラス Nextat Inc. 30
  31. ここまでのまとめ イベントソーシングは最新の状態ではなく、イベントによる状態差分を重要視す る考え⽅ 履歴保存が必須とされるような場合には向いている ⾝近にもあるので知らず知らず実践している⽅も多いのでは? パフォーマンス⾯の劣化はCQRSを併⽤してカバーできる CQRSとイベントソーシングは必ずしもセットではない 疎結合性とシステム全体としての複雑性がトレードオフ 最新の状態しか必要のないアプリケーションでは無駄な複雑さを追加する Nextat

    Inc. 31
  32. 5. EventSauce⼊⾨ Nextat Inc. 32

  33. イベントソーシング実装を⽀援するFW/ライブラリ イベントソーシング⾃体はプログラミング⾔語を問わない設計⽅法 PHP⽤のFW/ライブラリ Prooph http://getprooph.org EventSauce https://eventsauce.io Broadway https://github.com/broadway/broadway predaddy

    https://github.com/szjani/predaddy Nextat Inc. 33
  34. EventSauce https://eventsauce.io A pragmatic event sourcing library for PHP with

    a focus on developer experience. by Frank de Jonge. https://frankdejonge.nl/ The Creator of Flysystem (league/flysystem) https://flysystem.thephpleague.com/v1/docs/ Nextat Inc. 34
  35. 特徴(公式サイトより) DXにフォーカス 複雑なビジネス要件への対応に明快さを与えるメッセージ駆動のアプローチ 利⽤者がフルコントロールできるように設計されている 表現⼒豊かなBDD(振る舞い駆動開発)スタイルのテスト コード⽣成による実装スピードアップ Nextat Inc. 35

  36. EventSauceの設計思想 イベントソーシング + DDDのパターンである集約の考え⽅からの影響 Greg Youngの CQRS + ESからの流れ Aggregates

    and Event Sourcing (A+ES) イベントの主体 = 集約(エンティティ) 〇〇(=エンティティ) が XX した ※ cf. 実践ドメイン駆動設計 付録A 通信中⼼(Communication)、メッセージ駆動のインターフェース Nextat Inc. 36
  37. ⽤語補⾜1 AggregateRoot 集約 メインのモデリング対象。エンティティとも。IDで識別される。 モデルの整合性を保ち、不変条件を守り、コマンドによって起こったイベントを 保持する責務 このスライドのサンプルではユーザ所持キャラクターが相当。 Nextat Inc. 37

  38. ⽤語補⾜2 Message メッセージ 通知と永続化のためにEventをラップしたオブジェクト Nextat Inc. 38

  39. ⽤語補⾜3 AggregateRootRepository 集約リポジトリ 集約を取得、永続化する役⽬ + Consumerへのメッセージを通知する役⽬。 実際にはそれぞれの役⽬の⼤半を下記2つのオブジェクトに移譲している MessageRepository: メッセージの取得・永続化 MessageDispatcher:

    メッセージの通知 MessageRepository、MessageDispatcherもインターフェースとデフォ実装あ り Nextat Inc. 39
  40. ⽤語補⾜4 Consumer コンシューマ イベントはメッセージにラップして通知される 通知されたメッセージをハンドルする Nextat Inc. 40

  41. 使ってみました $ composer require eventsauce/eventsauce Nextat Inc. 41

  42. 実装1. AggregateRoot (集約) AggregateRootBehaviorというTraitがインターフェースの実装を補助 集約のID = AggregateRootIdを持つ必要がある /** ユーザ所持キャラクター */

    class UserCharacter implements AggregateRoot { use AggregateRootBehaviour; private UserId $userId; ユーザID private CharacterId $characterId; // キャラクターのマスタID private int $experience = 0; // 獲得経験値累計 // コンストラクタはprivate 。static メソッドが名前付きコンストラクタの代わり public static function initiate(): AggregateRoot { $userCharacterId = UserCharacterId::fromString(Uuid::uuid4()->toString()); return new static($userCharacterId); } } Nextat Inc. 42
  43. 実装2. AggregateRootId (集約ID) fromString()とtoString()を実装していればOK /** ユーザ所持キャラクターID */ class UserCharacterId implements

    AggregateRootId { private string $id; private function __construct(string $id) { $this->id = $id; } public function toString(): string { return $this->id; } public static function fromString(string $id): UserCharacterId { return new static($id); } } Nextat Inc. 43
  44. 実装3. Event (イベント) Serialize,Deserializeのためのメソッドを実装+Getter /** ユーザ所持キャラクター強化イベント */ class UserCharacterEnhanced implements

    SerializablePayload { private UserCharacterId $userCharacterId; private int $experience; // 獲得経験値 // コンストラクタ省略 // Getter 省略 public static function fromPayload(array $payload): SerializablePayload { return new self( UserCharacterId::fromString($payload['userCharacterId']), (int) $payload['experience'], ); } public function toPayload(): array { return [ 'userCharacterId' => $this->userCharacterId->toString(), 'experience' => $this->experience, ]; } } Nextat Inc. 44
  45. 実装4. Command (コマンド) ほぼGetterのみのDTO。単純な処理の場合、イベントと似た感じになる /** ユーザ所持キャラクター獲得コマンド */ class EnhanceUserCharacter { private

    UuidInterface $id; private UserCharacterId $userCharacterId; private int $experience; // コンストラクタ省略 // Getter 省略 public static function new( UserId $userId, CharacterId $characterId ): AcquireUserCharacter { return new self(Uuid::uuid4(), $userId, $characterId); } } Nextat Inc. 45
  46. (おまけ)20秒でわかるイベントとコマンドの違い 神は「光あれ」と⾔われた。すると光があった ーー 創世記 1章1-8節 コマンド(命令形) 光あれ イベント(過去形) 光があった コマンドは条件次第で失敗するかも イベントの代わりにコマンドを記録すると状態を再現できない Nextat

    Inc. 46
  47. 実装5. Aggregateにコマンドの実⾏を記述 集約が受け付けたコマンドを適⽤できる状態かどうかをまず検証 recordThat()でイベントを記録するのがポイント /** ユーザ所持キャラクター */ class UserCharacter implements

    AggregateRoot { public function performEnhance(EnhanceUserCharacter $command): void { // 条件の検証 if ($this->isMaxLevel()) { throw new AlreadyMaxLevelException(' 最⼤レベルです'); } // イベントの記録 $this->recordThat(new UserCharacterEnhanced( $command->getUserCharacterId(), $command->getExperience() )); } } Nextat Inc. 47
  48. 実装6. Aggregateにイベントの適⽤を記述 イベントに応じて集約の状態を変化させる コマンドの実⾏時とイベントのリプレイ時に同じロジックが使われる 例外を投げてはいけない=失敗してはいけない 集約の整合性はコマンド実⾏時に担保されているはず /** ユーザ所持キャラクター */ class

    UserCharacter implements AggregateRoot { private int $experience = 0; // 経験値 // apply{ イベントクラス名}() という命名規約で暗黙的に呼ばれる protected function applyUserCharacterEnhanced( UserCharacterEnhanced $event ): void { $this->experience += $event->getExperience(); } } Nextat Inc. 48
  49. 実装7. Consumer Eventを包んだMessageを受け取るので好きに処理する /** イベントをロギング */ class LogEvents implements Consumer

    { public function handle(Message $message) { $event = $message->event(); Log::info(get_class($event) . ' event handled.', array_merge( [ 'aggregateRootId' => $message->aggregateRootId()->toString(), 'aggregateRootVersion' => $message->aggregateVersion(), ], $event->toPayload(), )); } } Nextat Inc. 49
  50. 実装8. 処理全体の流れ サービス、コマンドハンドラなどに記述してくださいとある ステートソーシングの場合と⾒た⽬はあまり変わらない $repository = new ConstructingAggregateRootRepository( UserCharacter::class, new

    InMemoryMessageRepository(), new SynchronousMessageDispatcher(new LogEvents()) ); // リポジトリからの集約の取得 $userCharacter = $repository->retrieve($newUserCharacterId); $userCharacterId = $userCharacter->aggregateRootId(); // 強化 $command = EnhanceUserCharacter::new($userCharacterId, $exp = 1000); $userCharacter->performEnhance($command); // 永続化 $userCharacterRepository->persist($userCharacter); Nextat Inc. 50
  51. 実装9. テスト PHPUnitを継承してBDDスタイルで書ける仕組みを提供 then()の代わりにexpectToFail()で準正常系のテストも書ける class UserCharacterTest extends AggregateRootTestCase { public

    function test_enhance(): void { $userCharacterId = $this->aggregateRootId(); // ユーザキャラクタを獲得後に // 強化コマンドを実⾏すると強化されたというイベントが発⽣ $this->given( new UserCharacterAcquired( $userCharacterId, UserId::fromString('user1'), CharacterId::fromInt(1) ) ) ->when(EnhanceUserCharacter::new($userCharacterId, 1000)) ->then(new UserCharacterEnhanced($userCharacterId, 1000)); } Nextat Inc. 51
  52. 今回紹介できなかった機能など YAMLからEventとCommandのコードを⾃動⽣成 CQRSのためのProjection、ReadModel スナップショット システムクロックパターンのインターフェース Time / Clock ステートソーシングでも使えるEventDispatcher イベントストアのデータ構造についてのパターン考察

    Nextat Inc. 52
  53. 感想 CQRSまでできればエンティティに読み取りのためのGetterをつけなくて済む コマンド実⾏時の事前条件の検証や不変条件を意識しやすい 重要な機能はインターフェースになっており、置き換えやすい AggregateRoot、Repositoryのデフォ実装がIDEの静的解析に引っかかる PHP7.4の共変反変の改善、8以降のstaticの戻り値型宣⾔があればもっと書 きやすそう EventSauce⾃体の設計・実装はシンプルで綺麗 ドキュメントの概念の説明は丁寧だが、実装詳細の説明が若⼲不親切 ソースを読めば⼤体わかる

    Nextat Inc. 53
  54. 適切な粒度のインターフェースで実装の詳細が隠蔽される → ES + CQRSの⼤掛かりな部分の実践は後に回せる データストアの詳細 スナップショット キューの導⼊ プロジェクションによるリードモデルの構築 → イベントソーシングの根本や個々の概念を段階を踏んで学習し

    やすい Nextat Inc. 54
  55. EventSauceはいいぞ! Nextat Inc. 55

  56. 6. なぜ今イベントソーシングなのか Nextat Inc. 56

  57. マイクロサービス、リアクティブシステムの⽂脈 疎結合なシステム間をつなぐ鍵がイベントやメッセージ ex) SlackとWeb Hook リアクティブなイベント・メッセージ駆動のアーキテクチャ (※ ステートソーシングでもメッセージ駆動の部品にはなれるが) 各社クラウドにもメッセージ駆動アーキテクチャのための部品が充実 イベント駆動型のFaaS

    (AWS Lambda、Cloud Function、Azure Function) メッセージキューとして使えるサービス群(Amazon SQS, Cloud Pubsub、 Azure Service Bus) イベントバス (Amazon EventBridge、Azure EventGrid) Nextat Inc. 57
  58. まとめ 状態ではなく状態の変化した事象に着⽬するのがイベントソーシング 銀の弾丸ではないが、使い⽅を間違えなければ強⼒ EventSauceは段階的にイベントソーシングに⼊⾨できる良質なライブラリ イベントを重視してアプリケーションを設計すると新しい視点が得られるはず Nextat Inc. 58

  59. PR Nextat Inc. 59

  60. PR1: We're hiring! 株式会社 Nextat 受託開発 業務システム、ECサイト、ソシャゲ この度東京オフィスを本格始動 私も4⽉から東京の予定 設計の話に付き合ってくれる⽅⼤歓迎!

    Nextat Inc. 60
  61. PR2: 沖縄でイベントをやります MESHミニハッカソンin沖縄 2020/03/04 (⽔) https://nextat2.connpass.com/event/163745/ Nextat Inc. 61

  62. MESHというIoTブロックデバイスを使ったハッカソン 無線で繋がるボタンやセンサーを起点に⾊々なサービスをつなぐ直感的なプログ ラミングスタイルがウリ https://meshprj.com/jp/ Nextat Inc. 62

  63. PR完 Nextat Inc. 63

  64. ご清聴ありがとうございました Nextat Inc. 64