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

Historiaの紹介

Avatar for Masashi Umezawa Masashi Umezawa
May 02, 2025
0

 Historiaの紹介

Historia はPharo Smalltalkで実装されたイベントソーシングのフレームワークです。

オブジェクトの状態変化をイベントとして記録し、任意の時点の状態を再現できる機能を提供します。
ModelSpace と呼ばれる Aggregate によって、関連するドメインオブジェクトのライフサイクルを管理できます。
さらに、複数の Pharo 間で ModelSpace の状態を同期したり、イベントを伝搬させて別の ModelSpace が受け取ったりすることも可能です。

この資料ではシンプルな銀行口座アプリをサンプルとして、Historia主なの機能を紹介していきます。

Avatar for Masashi Umezawa

Masashi Umezawa

May 02, 2025
Tweet

Transcript

  1. Historia とは?
 • Pharo のイベントソーシングのフレームワーク ◦ オブジェクトへの変更がイベントとして記録される ◦ オブジェクトの任意の時点の状態を復元できる (Time-travel)

    ◦ 複数ノード間でオブジェクトの同期ができる ◦ オブジェクトは ModelSpace という Aggregate で管理される ◦ ModelSpace 間を EventBridge でつなぎ疎結合な連携ができる ◦ イベントは Redis Streamで永続化される (高可用、低レイテンシ) • https://github.com/mumez/Historia
  2. Historiaのインストール
 • Metacello でインストール Metacello new baseline: 'Historia'; repository: 'github://mumez/Historia:main/src';

    load. • Redis も入れて立ち上げておく! ◦ https://hub.docker.com/r/redis/redis-stack docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
  3. デモ: イメージ間でのオブジェクトの緩い同期 (1)
 • イメージA "ModelSpaceの作成" modelSpace := HsModelSpace spaceId:

    'sample-space'. "OrderedCollectionのモデルを登録して値を追加" modelSpace putModelOf: HsOrderedCollectionModel id: 'numbers'. vmodel := modelSpace modelAt: 'numbers'. 1 to: 5 do: [ :idx | vmodel add: idx ]. "モデルの保存" vmodel save.
  4. デモ: イメージ間でのオブジェクトの緩い同期 (2)
 • イメージB "同じspace idでModelSpaceを作成" modelSpace2 := HsModelSpace

    spaceId: 'sample-space'. "イベントを再生して最新化" modelSpace2 catchup. "モデルを参照して値を確認" (modelSpace2 modelAt: 'numbers') values. "-> an OrderedCollection(1 2 3 4 5)"
  5. 主要なクラス
 • HsModel ◦ ドメインのモデルを表す • HsEvent (のサブクラス群) ◦ モデルに対する状態変更イベントを表す

    • HsModelSpace ◦ モデルを集約しライフサイクルを管理する • 以下の3つのタイプのクラスを継承して利用する => 銀行口座アプリの例を使って説明 https://gist.github.com/mumez/75f1b3cbd08035db6c5f45af7e1590ba
  6. HsModel << #HtBankAccount slots: { #name . #balance }; package:

    'Historia-Examples-SimpleBank' モデルの定義
 • HsModel を継承する形で定義する ◦ 口座名義: name 残高: balance initialize name := ''. balance := 0. • アクセッサも作っておく • initialize メソッドを定義
  7. イベントの定義
 • モデルへの変更はイベントを通じて実行される ◦ モデルに対して意味のある操作を加えるにはイベントを定義する HsValueChanged << #HtBankAccountBalanceChanged slots: {};

    package: 'Historia-Examples-SimpleBank' • 残高の変更を BankAccountBalanceChanged として定義 ◦ 属性の値の変化なので HsValueChanged を継承する • クラスメソッド typeName も定義する typeName ^ self name HtBankAccountBalanceChanged class (accessing)
  8. モデルスペースの定義
 • モデルを集約して管理するため ModelSpace を定義する HsModelSpace << #HtBankAccountSpace slots: {};

    package: 'Historia-Examples-SimpleBank' • BankAccountSpace の責務 ◦ 複数の銀行口座のライフサイクルを管理する ◦ 銀行口座に対する一連の意味のある操作(アクション)を提供する ▪ 口座開設、預け入れ、引き出し、振り込みなど • ModelSpace の基本機能 ◦ イベントの再生 ◦ スナップショット
  9. ミューテーション(状態変更)の実装 - 1
 • Historia での オブジェクトの状態変更は2段階で行われる ◦ 状態変更をイベントとして記録 ◦

    イベントをモデルに適用 • まずモデル側に mutateBalanceChange: を定義 ◦ balanceの状態変化を HtBankAccountBalanceChanged イベント として記録 mutateBalanceChange: newBalanceChange self mutate: HtBankAccountBalanceChanged using: [ :ev | ev value: newBalanceChange ] HtBankAccount (mutating)
  10. ミューテーション(状態変更)の実装 - 2
 • イベント側に applyTo: を定義 ◦ イベントの値をモデルに適用させる •

    モデル側に applyBalanceChange: を定義 ◦ 値を普通に変える (mutationを使わない!) applyTo: target target applyBalanceChange: self value HtBankAccountBalanceChanged (applying) applyBalanceChange: newBalanceChange balance := balance + newBalanceChange HtBankAccount (applying)
  11. ModelSpace への Model の登録
 • spaceId: を指定して ModelSpace を生成 ◦

    ModelSpace 自体の識別子 ▪ ModelSpace の復元や同期で使われる • putModelOf:id:withArguments: で Model を生成して登録 ◦ 登録自体も HsModelCreated イベントとして記録される "モデルスペースの作成" spaceId := 'bank-account-app-1'. modelSpace := HtBankAccountSpace spaceId: spaceId. "モデルの登録" accId := '00001'. bankAccount1 := modelSpace putModelOf: HtBankAccount id: accId withArguments: {'name'->'John Smith'}.
  12. 登録された Model の参照
 • modelAt: で Model の id を指定する

    ◦ models, modelAt:ifAbsent: など、各種用意されている "idを指定してドメインモデルを取り出す" accId := '00001'. modelSpace modelAt: accId. "-> bankAccount1が返る"
  13. ドメインアクションの提供
 • ModelSpace にドメイン固有の操作用メソッドを持たせる ◦ ドメインを操作するための明確なAPIとして提供する ◦ ModelSpace が管理するドメインのビジネスロジックを持つ •

    HtBankAccountSpace のドメインアクション ◦ 残高の取得 (getBalanceAt:) ◦ 預け入れ (deposit:at:) ◦ 引き出し (withdraw:at:) • 必ず Model の id が引数になっていることが特徴 ◦ Aggregate として、外部から直接 Model を参照しない書き方を推奨
  14. • modelAt: で bankAccount を取得して balance を返す 残高の取得 (getBalanceAt:)
 getBalanceAt:

    accountId | acc | acc := self modelAt: accountId. ^ acc balance HtBankAccountSpace (actions)
  15. • modelAt: で bankAccount を取得 • mutateBalanceChange: でミューテイト • save:

    で 保存 ( イベントの確定 ) 預け入れ (deposit:at:)
 deposit: amount at: accountId | acc | acc := self modelAt: accountId. acc mutateBalanceChange: amount. self save: acc HtBankAccountSpace (actions)
  16. • modelAt: で bankAccount を取得 • mutateBalanceChange: でミューテイト • save:

    で 保存 ( イベントの確定 ) 引き出し (withdraw:at:)
 withdraw: amount at: accountId | acc | acc := self modelAt: accountId. acc mutateBalanceChange: amount negated. self save: acc HtBankAccountSpace (actions)
  17. • HtBankAccountSpace 経由で銀行口座を操作 ◦ 裏ではイベントが蓄積されている ドメインアクションの動作確認
 "口座に預け入れ" modelSpace deposit: 100

    at: accId. "口座から引き出す" modelSpace withdraw: 30 at: accId. "現在の残高を取得する" modelSpace getBalanceAt: accId. "-> 70"
  18. • EventJournalStorage が各種メソッドを提供 保存されたイベントの取得
 "すべてのイベントを取得" modelSpace eventJournalStorage allEvents. "print it"

    "an OrderedCollection([#1744204795457-0: modelCreated targetIds:#('00001')] [#1744204795460-0: HtBankAccountBalanceChanged class targetIds:#('00001')] [#1744204797556-0: HtBankAccountBalanceChanged class targetIds:#('00001')])" "直近から最大5つのイベントバージョンを取得" modelSpace eventVersionsReversedFromLast: 5. "print it" "an OrderedCollection('1744204797556-0' '1744204795460-0' '1744204795457-0')" • よく使うショートカット
  19. • ModelSpace の特定時点の状態を保存したもの スナップショットの保存
 "スナップショット保存" modelSpace saveSnapshot. "もう少し預けてまた保存" modelSpace deposit:

    100 at: accId. modelSpace saveSnapshot. • SnapshotStorage 経由でバージョン取得など可能 "すべてのスナップショットバージョンを取得" modelSpace snapshotStorage listSnapshotVersions. "print it" "an OrderedCollection('1744205230917-0' '1744205252394-0')"
  20. • バージョンを指定して復元が可能 スナップショットからの復元
 "最近から最大2つのスナップショットバージョンを取得" snapVersions := modelSpace snapshotVersionsReversedFromLast: 2. "

    -> an OrderedCollection('1744205252394-0' '1744205230917-0')" "古い方のバージョンを選択" snapshotVersion := snapVersions last. "スナップショットからの復元" modelSpace loadSnapshot: snapshotVersion.
  21. • イベント再生で ModelSpace を復元してみる イベントの再生
 "空のModelSpaceをmodelSpace2として作成" modelSpace2 := HtBankAccountSpace spaceId:

    spaceId. modelSpace2 models isEmpty. "-> true" "最新の状態に追いつく" modelSpace2 catchup. "同期が行われる" modelSpace2 getBalanceAt: accId. "-> 170" • 単純に最初から最新までのイベントを再生するわけではない ◦ 最新 Snapshot からの差分イベントを再生して復元している
  22. • goTo: を使う ◦ 過去の任意の更新時の状態を復元できる ◦ すべてのイベントにはバージョン(タイムスタンプ)が付与されている タイムトラベル
 "イベントバージョンを最新から最大 10個まで取得"

    recentEventVersions := modelSpace2 eventVersionsReversedFromLast: 10. modelSpace2 goTo: recentEventVersions last. "最初のバージョンに移動" modelSpace2 getBalanceAt: accId. "-> 0" modelSpace2 goTo: recentEventVersions first. "最新バージョンに移動" modelSpace2 getBalanceAt: accId. "-> 200" modelSpace2 goTo: recentEventVersions third. "途中のバージョンに移動" modelSpace2 getBalanceAt: accId. "-> 170"
  23. • EventBridge ◦ ModelSpace 間の連携の仕組み ◦ Space id を指定した ModelSpace

    からイベントを受け取れる ▪ イベントに対するコールバックを実装し 別の ModelSpace や外部サービスへメッセージを送れる ◦ 内部では Announcer を使用 ▪ イベント処理の細かいカスタマイズも可能 • ModelSpaceA -> EventBridgeA -> ModelSpaceB -> EventBridgeB … ◦ といった形で非同期に連携する疎結合なアプリを作れる EventBridge による ModelSpace 連携

  24. • 口座間の振り込みを HtBankAccountSpace に追加 ◦ notify:withArguments: で Notification のイベントを送るようした 振り込み

    (transfer:from:to:) の追加
 transfer: amount from: fromAccountId to: toAccountId | fromAcc toAcc | fromAcc := self modelAt: fromAccountId. toAcc := self modelAt: toAccountId. fromAcc mutateBalanceChange: amount negated. toAcc mutateBalanceChange: amount. toAcc notify: #transferredTo withArguments: { (#to -> toAccountId). (#from -> fromAccountId). (#amount -> amount) }. self saveAll: { fromAcc. toAcc } HtBankAccountSpace (actions)
  25. • HsModelEventBridge を継承 EventBridge の実装 - 1
 initialize super initialize.

    self notificationAnnouncedDo: [ :announcement | announcement kind = #transferredTo ifTrue: [ self handleTransferredToEvent: announcement event ]] HtBankAccountMailBridge (initialization) HsModelEventBridge << #HtBankAccountMailBridge slots: {}; package: 'Historia-Examples-SimpleBank' • 初期化時に Notification のイベントハンドラを登録 ◦ notificationAnnouncedDo: を使う
  26. • handleTransferredEvent: のイベントハンドラの実装 EventBridge の実装 - 2
 HtBankAccountMailBridge (event handling)

    handleTransferredToEvent: notificationEvent | accountId mailAddress | accountId := notificationEvent argsAt: 'to' ifAbsent: ['']. mailAddress := self mailAccountSpace "メールドメインModelSpaceへ" getMailAddressAt: accountId. self externalMailService "外部メールサービスへ" sendMailTo: mailAddress content: (self mailTemplateSpace "メールテンプレートSpaceへ" getContentAt: notificationEvent kind values: notificationEvent arguments)
  27. • Mock を返却するアクセッサの実装 EventBridge の実装 - 3
 HtBankAccountMailBridge (accessing) mailAccountSpace

    ^ self HtBankAccountMailBridge (accessing) mailTemplateSpace ^ self HtBankAccountMailBridge (accessing) externalMailService ^ self
  28. • Mock の実装 EventBridge の実装 - 4
 HtBankAccountMailBridge (mocking) getMailAddressAt:

    accountId ^ { ('00001' -> '[email protected]') } asDictionary at: accountId ifAbsent: [ '' ] HtBankAccountMailBridge (mocking) getContentAt: templateKey values: templateValues ^ 'You just received {amount} from {from}' format: templateValues HtBankAccountMailBridge (mocking) sendMailTo: mailAddress content: mailContent Transcript cr; show: ('## Send mail to: {1} content: {2}' format: {mailAddress. mailContent})
  29. • 振り込みを行うとメール送信ログが表示される 振り込みによる連携の確認
 spaceId := 'bank-account-app-1'. bankMailBridge := HtBankAccountMailBridge spaceId:

    spaceId. bankMailBridge catchup. modelSpace putModelOf: HtBankAccount id: '00002' withArguments: {'name'->'Masashi Umezawa'}. modelSpace deposit: 10000000 at: '00002'. modelSpace transfer: 2000 from: '00002' to: '00001' "振り込みの実行" ## Send mail to: [email protected] content: You just received 2000 from 00002 Image B Image B の Transcript Image A
  30. • Notificationに加え任意の状態変更もイベントとして受け取れる ◦ eventAccouncedDo: を使う ミューテーションイベントを受け取る
 "別のEventBridgeを作る" auditorEventBridge := HsModelEventBridge

    spaceId: spaceId. "ミューテーションのイベントハンドラを登録" auditorEventBridge eventAnnouncedDo: [:ann | | event | event := ann event. Transcript cr; show: ('##{1} typeName:{2} arguments:{3}' format: { event targetId. event typeName. event arguments }) ]. auditorEventBridge catchup. "or #startPullingEventsFromLastExecuted"
  31. • ドメインアクションをいろいろ実行してみる イベントハンドラの動作確認
 modelSpace deposit: 30 at: accId. modelSpace withdraw:

    40 at: accId. modelSpace transfer: 5000 from: '00002' to: accId. ##00001 typeName:HtBankAccountBalanceChanged class arguments:a Dictionary('value'->30 ) ##00001 typeName:HtBankAccountBalanceChanged class arguments:a Dictionary('value'->-40 ) ##00002 typeName:HtBankAccountBalanceChanged class arguments:a Dictionary('value'->-5000 ) Image B の Transcript Image A
  32. • Historia でイベントソーシングに基づくアプリを開発できる ◦ 状態変更イベントによるタイムトラベル ◦ 任意の時点でのスナップショット ◦ ModelSpace の自動同期

    ◦ EventBridge による ModelSpace 間の緩やかな連携 ◦ Redis Stream によるイベントの永続化 • フィードバックお待ちしています! まとめ