Railsのデザインパターン: Interactorオブジェクト

複数のActiveRecordモデルをまたぐふるまいや、外部APIとのやりとりといったビジネスロジックは、コントローラに書くと肥大化してしまいます。 モデル層の変更の影響を受けやすくなり、ユニットテストが書きづらく、また再利用性がないという問題もあります。

Interactorオブジェクトを導入することでこの問題を解決できます。 ビジネスロジックをカプセル化する責務をもつのがInteractorオブジェクトです。 このInteractorオブジェクトについて以下に示します。

Interactorオブジェクトとは

Interactorオブジェクトとは「デザインパターンのひとつで、ビジネスロジックをカプセル化するためのモデル層に属するクラス群」です。 ひとつのInteractorオブジェクトはひとつの責務をもちます。 「ひとつの責務」とは、たとえば「記事を投稿する」「決済を行う」という、それ以上分割できない責務です。 またInteractorオブジェクトを使うための共通のインタフェースが定義されます。

Interactorオブジェクトと同じ役割として、Serviceオブジェクトがあります。 ただ、Railsの文脈で語られるServiceオブジェクトは定義があいまいで、特定のルールをもちません。 ルールがないとインタフェースが統一されず、また複数の責務をもってしまうといった問題が生じます。

Interactorオブジェクトはビジネスロジックをカプセル化する役割をもち、インタフェースや責務に関するルールがあるため、コードの秩序を保つことができます。

Interactorオブジェクトの必要性

Interactorオブジェクトはひとつのみの責務をもち、インタフェースが共通なので、使い方を予測することができます。 複雑なビジネスロジックをコントローラに書くと肥大化してしまい、またモデルの変更を受け、テストが書きづらく、再利用性もありません。 Interactorオブジェクトはモデルの変更の影響を受けず、テストしやすく、再利用性もあります。 このためビジネスロジックを記述するためのデザインパターンとしてInteractorオブジェクトが有用になります。

Interactorオブジェクトのユースケース

Interactorオブジェクトは、次のようなビジネスロジックを書くときに適用されます。

  • 複数のActiveRecordモデルを操作する処理。ただし各Interactorは単一の責務になるようにする
  • 外部APIとやりとりする処理

InteractorオブジェクトのためのGem

RailsでInteractorオブジェクトを導入するためのGemとして、collectiveidea/interactorがあります。 コード例は次章で示すとして、機能について示します。

Interactor GemはInteractorオブジェクトのインタフェースを提供します。 具体的にはInteractorをincludeしたクラスに#callを定義し、ここにビジネスロジックを書きます。 フックもあるため、処理を実行する前後にログ出力などの処理をはさむこともできます。

また、Interactor Gemの大きな特徴のひとつとしてOrganizerという機能があります。 これは複数のInteractorからなるInteractorを構成するための機能です。 各Interactor間はContextというオブジェクトを共有します。

ロールバックの機能も備えています。 たとえばOrganizer内のひとつのInteractorが外部APIを叩くとき、その後のInteractorが失敗したときのロールバック処理を記述できます。 Organizer全体をトランザクション下で実行することで、データの整合性を保つことができます。

Interactorオブジェクトの例

Interactorオブジェクトの例として、ショッピングカートのビジネスロジックについて見てみます。 ユーザが注文するときに、注文の確定、決済のための外部APIの実行、配送レコードの作成を行います。 この例ではInteractor Gemを使用しています。 また、具体的な処理や本質でないコードは省略します。

動作環境

この記事で示す例は次の各環境で動作を確認しています。

名前バージョン
Ruby2.7.1
Ruby on Rails6.0.3.2

テーブル定義

この記事で示す例のテーブル定義は次のものを想定しています。

TableColumnTypeNot Null
orderidinteger
amountinteger
deliveryidinteger
order_idinteger

コントローラ

まずInteractorオブジェクトのインタフェースを確認するために、利用側であるコントローラの例を示します。 次のように、Interactorオブジェクト(ここではOrganizer)の#callを呼びます。 ここで渡す引数はcontextとしてInteractorオブジェクト内で共有できます。

class OrdersController < ApplicationController
  def update
    load_order

    context = ConfirmOrderOrganizer.call(order: @order, params: order_params)

    if context.success?
      redirect_to root_path
    else
      flash.now[:alert] = context.message
      render :edit
    end
  end

  private

  def load_order
    @order = current_user.orders.find(params[:id])
  end

  def order_params
    params.require(:order).permit(:amount)
  end
end

Organizer

次にOrganizerです。 Organizerは複数のInteractorを構成するクラスです。 ここでは「注文内容を更新する」「外部APIを叩く」「配送レコードを作成する」という各Interactorの使用を定義しています。

Organizerはフックを使用してInteractor全体に対しトランザクションを張ることができます。 このトランザクションは再利用性があるので、モジュール化してもいいかもしれません。

class ConfirmOrderOrganizer
  include Interactor::Organizer

  organize UpdateOrderInteractor, PayFeeInteractor, CreateDeliveryInteractor

  around do |organizer|
    ActiveRecord::Base.transaction do
      organizer.call
    end
  end
end

Interactor

最後にInteractorオブジェクトです。 次のように、Interactorオブジェクトの実行に失敗したときは、Contextオブジェクトの#fail!にエラー用のオブジェクトを渡します。 これにより、使用側(この例でいうコントローラ)のcontext.success?falseになり、エラー用のオブジェクトをとおしてユーザにエラー内容を表示することができます。

class UpdateOrderInteractor
  include Interactor

  delegate :order, :params, to: :context, private: true

  def call
    return if order.update(params)

    context.fail!(message: '...')
  end
end

決済は外部APIを叩く想定です。 外部APIの実行に対するロールバックはActiveRecord::Baseのトランザクションではできません。 このとき#rollbackを定義すると、後続のInteractorオブジェクトが失敗したときにロールバック処理を実行してくれます。

class PayFeeInteractor
  include Interactor

  def call
    # Pay
  end

  def rollback
    # Cancel
  end
end

配送レコード作成は次のとおりです。 このようにContextオブジェクトをとおしてInteractor間でオブジェクトを共有することができます。

class CreateDeliveryInteractor
  include Interactor

  delegate :order, to: :context, private: true

  def call
    delivery = Delivery.new(order: order)

    if delivery.save
      context.delivery = delivery
    else
      context.fail!(message: '...')
    end
  end
end

以上となります。 もちろんOrganizerはなくてもいいです。 この場合はInteractorオブジェクトを直接実行することになります。

以上のように、Interactorオブジェクトはひとつのクラスにひとつの責務をもち、また共有のインタフェースをもつため、ビジネスロジックをシンプルに書くことができます。

Interactorオブジェクトの設計ルール

以上の内容をもとに、Interactorオブジェクトの設計ルールについてまとめます。 InteractorオブジェクトとOrganizerオブジェクトそれぞれについてまとめます。

Interactorオブジェクト

  • app/interactorsにファイルを配置する
  • 接尾辞にInteractorをつける
  • 単一の責務のみをもつ
  • クラス名から予測可能なビジネスロジックを書く
  • ロールバック可能にする
  • Organizerでの使用を想定しているとしても、単一のInteractorとして使用可能にする(他のInteractorに依存しない)
  • テストを書く

Organizerオブジェクト

  • app/interactors/organizersにファイルを配置する
  • 接尾辞にOrganizerをつける
  • 必要に応じてトランザクションを張る
  • テストを書く

Author
著者Hiroki Zenigami

プロダクト開発者。妻と娘、猫とのんびり暮らしています。


Publishing
現場で使えるRuby on Rails 5

共著で「現場で使えるRuby on Rails 5(マイナビ出版)」を出版しました