概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Rails Event Store - better APIs coming | Arkency Blog
- 原文公開日: 2018/03/03
- 著者: Robert Pankowecki
- サイト: Arkency Blog
Rails: Event Storeの新しいAPIを解説する(翻訳)
Rails Event Store v0.26に新しい素敵なAPIがあります。変更点の一部をざっと見てみましょう。
永続的なサブスクライバとハンドラー
以前のsubscribe
は、handler
(インスタンス/クラス/procのいずれか)とevent_type
(ハンドラーがサブスクライブされたイベントの種類)という2つの引数を取りました。
class OrderSummaryEmail
def call(event)
order = Order.find(event.data.fetch(:event_id))
OrderMailer.summary(order).deliver_later
end
end
client = RailsEventStore::Client.new
client.subscribe(OrderSummaryEmail.new, [OrderPlaced])
今回の変更で、以下のように書けるようになりました。
client.subscribe(OrderSummaryEmail.new, to: [OrderPlaced])
to:
という名前付き引数のおかげでずっと読みやすくなったと思います。
さらに、Proc
のサブスクライブ方法が大きく改善されています。従来は以下のように書きました。
OrderSummaryEmail = -> (event) {
order = Order.find(event.data.fetch(:event_id))
OrderMailer.summary(order).deliver_later
}
client = RailsEventStore::Client.new
client.subscribe(OrderSummaryEmail, [OrderPlaced])
今後は以下のようにブロックを直接渡せるようになります。
client = RailsEventStore::Client.new
client.subscribe(to: [OrderPlaced]) do |event|
order = Order.find(event.data.fetch(:event_id))
OrderMailer.summary(order).deliver_later
end
一時的なサブスクライバとハンドラー
以前の一時的なサブスクライバに用いられていたAPIはどうも好きになれません。以前は以下のような感じでした。
client = RailsEventStore::Client.new
client.subscribe(OrderSummaryEmail, [OrderPlaced]) do
PlaceOrder.call
end
2つのコードブロックをきれいに渡す方法が定まっておらず、不便でした。サブスクライバ用のコードブロックと、一時的なサブスクライバの中で有効にしておきたいコード片用のコードブロックは今まで次のような感じで書きました。
order_summary_email = -> (event) {
order = Order.find(event.data.fetch(:event_id))
OrderMailer.summary(order).deliver_later
}
client = RailsEventStore::Client.new
client.subscribe(order_summary_email, [OrderPlaced]) do
PlaceOrder.call
end
興味深いことに、ActiveSupport::Notifications
にも同様の制約があります。
subscriber = lambda {|*args| ... }
ActiveSupport::Notifications.subscribed(subscriber, "sql.active_record") do
# ...
end
今後使える新しいAPIでは次のようになりました。
client = RailsEventStore::Client.new
client.within do
PlaceOrder.call
end.subscribe(to: [OrderPlaced]) do
order = Order.find(event.data.fetch(:event_id))
OrderMailer.summary(order).deliver_later
end.call
チェイン可能なAPIはコントローラで使うことも、内部で起きたことをインポートして調べることもできます。
client.within do
PlaceOrder.call
end.subscribe(to: [OrderPlaced]) do |ev|
head :ok
end.subscribe(to: [OrderRejected]) do |ev|
render json: {errors: [...]}
end.call
success = 0
failure = 0
client.within do
ImportCustomer.call
end.subscribe(to: [CustomerImported]) do |_|
success += 1
end.subscribe(to: [CustomerImportFailed]) do |_|
failure += 1
end.call
もちろんサブスクライバは従来どおり最初の引数に渡せます。サブスクライバはブロックでなくても構いません。
client.within do
PlaceOrder.call
end.subscribe(order_summary_email, to: [OrderPlaced]).call
AggregateRoot#on
AggregateRoot
で、(オブジェクトに適用されようとしているイベントに応答する)ハンドラーメソッドを簡単に定義できるようになりました。これにより、デフォルトの規則であるdef apply_order_submitted(event)
などのアンダースコアを含むメソッド名を使わなくとも、on OrderSubmitted do |event|
と書けば済むようになりました。
以下は従来の書き方です(現在も使用可能)。
class Order
include AggregateRoot
class HasBeenAlreadySubmitted < StandardError; end
class HasExpired < StandardError; end
def initialize
@state = :new
end
def submit
raise HasBeenAlreadySubmitted if state == :submitted
raise HasExpired if state == :expired
apply OrderSubmitted.new(data: {delivery_date: Time.now + 24.hours})
end
def expire
apply OrderExpired.new
end
private
attr_reader :state
def apply_order_submitted(event)
@state = :submitted
@delivery_date = event.data.fetch(:delivery_date)
end
def apply_order_expired(_event)
@state = :expired
end
end
以下は新しい書き方です。
class Order
include AggregateRoot
class HasBeenAlreadySubmitted < StandardError; end
class HasExpired < StandardError; end
def initialize
@state = :new
end
def submit
raise HasBeenAlreadySubmitted if state == :submitted
raise HasExpired if state == :expired
apply OrderSubmitted.new(data: {delivery_date: Time.now + 24.hours})
end
def expire
apply OrderExpired.new
end
on OrderSubmitted do |event|
@state = :submitted
@delivery_date = event.data.fetch(:delivery_date)
end
on OrderExpired do |_event|
@state = :expired
end
private
attr_reader :state
end
on OrderSubmitted do |event|
という記法のありがたい点は、コードベースのどこでOrderSubmitted
使われているかをgrepで探しやすいところです。
Rails Event Storeをさらに使いやすく書きやすくするためのアイデアをもっと知りたい方は、必要に応じて以下もご覧ください。
もっと読む
本記事を気に入っていただけましたら、ぜひニュースレターの購読をお願いします。私たちが日頃Railsアプリを安心してメンテナンスできるようにするための取り組みやソリューションをご覧いただけます。
合わせて以下の記事もどうぞ。
- イベントソーシングでCQRSやRead Modelが基本的に必要な理由 - イベントソーシングは一定の効果を得られる素敵な手法ですが、1つ大きな制約があります。現在のステートを簡単に取得する概念を欠いているため、「個数が10より少ないすべての製品のリスト」といったクエリの回答を得るのが容易ではありません。
- アプリケーションサービスの10の疑問に答える - アプリ構築に用いられる「ドメイン駆動設計」というアプローチについて聞いたことがおありでしょうか。このアプローチにある「アプリケーションサービス」という水平層について解説します。
- ActiveRecordのコールバック/セッター/派生データ - コールバックは今でも多くの場面で使われています。ひと味違うコード例でコールバックをさらに詳しく解説します。
弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。
I'm alpha-reading @arkency's "Beyond Service Objects" book and it's great so far. Many real-life examples and simple heuristics to apply.
— Jan Filipowski (@janfilipowski) July 13, 2017