$30 off During Our Annual Pro Sale. View Details »

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

n1215
PRO
February 10, 2020

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

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

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

n1215
PRO

February 10, 2020
Tweet

More Decks by n1215

Other Decks in Programming

Transcript

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

    View Slide

  2. ⾃⼰紹介
    京都から来ました
    - 中榮健⼆ (なかえけんじ)
    - twitter: @n_1215 
    - 株式会社Nextat 取締役
    - Laravel/Unityでソーシャルゲーム開発メイン → 最近はごった煮
    - ここ最近はサーバサイドTypeScriptやインフラのコード
    Nextat Inc. 2

    View Slide

  3. 発表概要
    1. はじめに
    2. イベント
    3. イベントソーシング
    4. CQRSとの関係性
    5. EventSauce⼊⾨
    6. なぜ今イベントソーシングなのか
    7. まとめ
    Nextat Inc. 3

    View Slide

  4. 1. はじめに
    Nextat Inc. 4

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  13. イベントを利⽤しない場合と⽐較
    ユーザアイコンサービス、獲得履歴サービスに直接依存しない
    ユーザ所持キャラクター獲得時の関連処理を変更・追加しやすい
    イベントリスナはキューなどを⽤いて⾮同期で動作させることも可能
    履歴のロギングが多少遅れても良い場合、レスポンスは早く返せる
    疎結合とシステム全体としての複雑さ(記述量も増加)のトレードオフ
    アプリケーションの外部拡張のフックポイント
    CMSやWeb FWなどでも活⽤されている
    Nextat Inc. 13

    View Slide

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

    View Slide

  15. 昔ながらのステートソーシング
    user_characters テーブル
    ユーザ所持キャラクターID キャラクターID(マスタID) レベル 限界突破数
    ... ... ... ...
    1234 1001 50 3
    ... ... ... ...
    変更があるたびにテーブルのレコードを更新
    ステートソーシング:最新の状態をデータストアに保存する⽅式
    Nextat Inc. 15

    View Slide

  16. イベントソーシング
    イベントストア (イベントを記録しておくデータストアのこと)
    ユーザ所持キャラクターID イベント イベントのデータ
    1234 LimitBreak {"before":0, "after":1}
    1234 Enhanced {"experience": 50000}
    1234 Acquired {"from": "gacha", "characterId": 1001}
    イベントソーシング:起こったイベントをデータストアに保存する⽅式
    起こったイベントを時系列順に再実⾏(リプレイ)して最新の状態を得る
    最新の状態はイベントが積み重なった結果に過ぎない
    Nextat Inc. 16

    View Slide

  17. ところで
    アプリケーションで起こった出来事を記録する、といえば?
    Nextat Inc. 17

    View Slide

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

    View Slide

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

    View Slide

  20. 状態と履歴の関係
    雑に⾔えば、履歴と状態の主従を逆転させるとほぼイベントソーシング
    状態は履歴の積分、履歴は状態の微分のようなもの
    全ての履歴を残しておけばそこから再計算して最新の状態を得ることができる
    ex) クエストへの⽇別挑戦回数をクエストの履歴から算出
    イベントソーシングに近しい記録⽅法を採⽤すると嬉しい機能の例
    ゲーム内通貨
    ⼈気投票
    null許容 or ミュータブルなカラムを減らしてイベントのようになる例
    バトルテーブル(終了⽇時にnull許容) → バトル開始+バトル結果テーブル
    プレゼントテーブル → プレゼント配布 + プレゼント受取テーブル
    Nextat Inc. 20

    View Slide

  21. ⾝近なイベントソーシング
    イベントソーシング⾃体は古くからある考え⽅
    バージョン管理システム
    データベース管理システムのトランザクションログによるリカバリ
    家計簿の出⾦と⼊⾦の記録
    ⇨ 過去の状態や変更を確認、再現できるというメリット
    Nextat Inc. 21

    View Slide

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

    View Slide

  23. 4. CQRSとの関係性
    Nextat Inc. 23

    View Slide

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

    View Slide

  25. CQS (コマンドクエリ分離)
    Command Query Separation
    すべてのメソッドはアクションを実⾏するコマンドまたは呼び出し側にデータを
    返すクエリのいずれかでなくてはならず、双⽅であってはならない。
    要するに、質問をしたことで答えを変化させてはならない
    コマンド(状態を変化させる副作⽤を持つ処理)+ クエリ(データの参照)
    クエリは参照透明であれ
    メソッドレベルでのコマンドとクエリの純粋性を要求
    by Bertrand Meyer
    プログラミング⾔語Eiffelの開発者
    契約による設計(Design by Contract / DbC)の創始者
    Nextat Inc. 25

    View Slide

  26. CQRS (コマンドクエリ責務分離)
    Command Query Responsibility Segregation
    クエリ側のモデルとコマンド側のオブジェクト(モデル)を分離
    リードモデル(クエリモデル)
    ライトモデル(コマンドモデル)
    クエリ側の要求が変わっても、コマンド側への影響を最⼩限にした修正が可能
    by Greg Young
    DDDへのイベントソーシング+CQRSの適⽤。昨今のESブームの⽕付け役
    Nextat Inc. 26

    View Slide

  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

    View Slide

  28. イベントソーシングとCQRSの併⽤
    コマンド側はイベントを扱うが、クエリ側は最新の状態さえあればいい
    いちいちイベントをフル再⽣して最新の状態を取得すると遅い+検索に弱い
    イベント群からクエリ側で求められるデータを作る(Projection)のは容易
    イベントソーシングによるライトモデル
    別⽴てで最新状態の記録を元にするリードモデル
    クエリ側の要求によるコマンド側への影響を最⼩化
    Nextat Inc. 28

    View Slide

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

    View Slide

  30. おまけ. CQRSにまつわるよくある誤解
    CQRS導⼊の⼼理的ハードルが⾼い理由?
    CQRSはイベントソーシングと⼀緒に採⽤しなければならない
    ステートソーシング + CQRSという選択肢
    反例) ランキング集計処理。最新の状態をRDBに保存、Redisにもデータを保
    存しランキングの読み取りはRedisを元にする
    反例) ECサイトの複雑な商品検索。最新の状態をRDBに保存、検索は最新の
    状態から計算したElasticsearchのデータを元にする
    CQRSはデータストアを分けなければならない
    反例) ライトモデルはRDB、リードモデルもRDBだが別のテーブルやビュー
    反例) 同じテーブルを参照するがリードモデルとライトモデルが別のクラス
    Nextat Inc. 30

    View Slide

  31. ここまでのまとめ
    イベントソーシングは最新の状態ではなく、イベントによる状態差分を重要視す
    る考え⽅
    履歴保存が必須とされるような場合には向いている
    ⾝近にもあるので知らず知らず実践している⽅も多いのでは?
    パフォーマンス⾯の劣化はCQRSを併⽤してカバーできる
    CQRSとイベントソーシングは必ずしもセットではない
    疎結合性とシステム全体としての複雑性がトレードオフ
    最新の状態しか必要のないアプリケーションでは無駄な複雑さを追加する
    Nextat Inc. 31

    View Slide

  32. 5. EventSauce⼊⾨
    Nextat Inc. 32

    View Slide

  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

    View Slide

  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

    View Slide

  35. 特徴(公式サイトより)
    DXにフォーカス
    複雑なビジネス要件への対応に明快さを与えるメッセージ駆動のアプローチ
    利⽤者がフルコントロールできるように設計されている
    表現⼒豊かなBDD(振る舞い駆動開発)スタイルのテスト
    コード⽣成による実装スピードアップ
    Nextat Inc. 35

    View Slide

  36. EventSauceの設計思想
    イベントソーシング + DDDのパターンである集約の考え⽅からの影響
    Greg Youngの CQRS + ESからの流れ
    Aggregates and Event Sourcing (A+ES)
    イベントの主体 = 集約(エンティティ)
    〇〇(=エンティティ) が XX した
    ※ cf. 実践ドメイン駆動設計 付録A
    通信中⼼(Communication)、メッセージ駆動のインターフェース
    Nextat Inc. 36

    View Slide

  37. ⽤語補⾜1 AggregateRoot
    集約
    メインのモデリング対象。エンティティとも。IDで識別される。
    モデルの整合性を保ち、不変条件を守り、コマンドによって起こったイベントを
    保持する責務
    このスライドのサンプルではユーザ所持キャラクターが相当。
    Nextat Inc. 37

    View Slide

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

    View Slide

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

    Nextat Inc. 39

    View Slide

  40. ⽤語補⾜4 Consumer
    コンシューマ
    イベントはメッセージにラップして通知される
    通知されたメッセージをハンドルする
    Nextat Inc. 40

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  46. (おまけ)20秒でわかるイベントとコマンドの違い
    神は「光あれ」と⾔われた。すると光があった
    ーー 創世記 1章1-8節
    コマンド(命令形) 光あれ
    イベント(過去形) 光があった
    コマンドは条件次第で失敗するかも
    イベントの代わりにコマンドを記録すると状態を再現できない
    Nextat Inc. 46

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  52. 今回紹介できなかった機能など
    YAMLからEventとCommandのコードを⾃動⽣成
    CQRSのためのProjection、ReadModel
    スナップショット
    システムクロックパターンのインターフェース Time / Clock
    ステートソーシングでも使えるEventDispatcher
    イベントストアのデータ構造についてのパターン考察
    Nextat Inc. 52

    View Slide

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

    View Slide

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

    View Slide

  55. EventSauceはいいぞ!
    Nextat Inc. 55

    View Slide

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

    View Slide

  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

    View Slide

  58. まとめ
    状態ではなく状態の変化した事象に着⽬するのがイベントソーシング
    銀の弾丸ではないが、使い⽅を間違えなければ強⼒
    EventSauceは段階的にイベントソーシングに⼊⾨できる良質なライブラリ
    イベントを重視してアプリケーションを設計すると新しい視点が得られるはず
    Nextat Inc. 58

    View Slide

  59. PR
    Nextat Inc. 59

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  63. PR完
    Nextat Inc. 63

    View Slide

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

    View Slide