Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails: Event Storeの新しいAPIを解説する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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アプリを安心してメンテナンスできるようにするための取り組みやソリューションをご覧いただけます。

合わせて以下の記事もどうぞ。

弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。

関連記事

Rails: ElasticSearchとRedis Streamsのストリームを使う(翻訳)

Rails: RedisとRediSearchで時系列データを扱う(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。