Interactorメモ

Interactorレイヤを追加するGem。interactor 3.1.2、interactor-rails 2.2.1 で確認。

サービスレイヤ(ServiceLayer)パターン(PofEAA)でも書いているが個人的には、このGemは使わずにプレーンなRubyクラスでServiceレイヤを構築すれば良いと考えている。

2021年3月現在、このGemは更新が停滞している。2014年以降、二回しかリリースがなされていないし、その間にRubyファイルが変更されたのは6コミットだけである。v4の開発も2017年3月から放置されている。2019年12月29日以降活動はなく簡単なtypo修正のPRも放置されている。

また、1クラス1アプリケーションロジックになるため、無闇にクラス名が長くなりやすい。関連するアプリケーションロジック(例えばCRUD操作など)を1つのクラスにまとめることができない。

インストール

以下のgemを追加する。

gem 'interactor-rails', '~> 2.0'

interactor-railsはRailsにInteractorのgeneratorを追加するgem。generatorが不要ならgem 'interactor', '~> 3.0'でよい。

Generator

bin/rails g interactor

bin/rails g interactor Interactor名app/interactors/の中にファイルが作成される。

bin/rails g interactor:organizer

bin/rails g interactor:organizer Interactor名 Organize先のInterractorクラス名1 Organize先のInterractorクラス名2 ...app/interactors/の中にorganizeが追加されたファイルが作成される。

organizeについては後述。

Context

Interactorクラスの中ではattr_readercontextが宣言されており、このオブジェクトがInteractorクラスの外と中を繋ぐデータコンテナになる。

外からのデータはInteractorClass.call(key: value, ...)のようにcallメソッドの引数に指定しcontext.keyの形でアクセスする。

中からのデータはcontext.key = valueの形でInteractorクラスの中で設定し、callメソッドの戻り値の形でcontextが外に渡される。

基本的にはデータコンテナだが、いくつか特殊なメソッドが用意されている。下記の他(例えばcontext.errorなど)には決まりはない。自分で好き勝手に値の代入と参照ができる。

context.fail!

Interactorが失敗した状態にする。このメソッドを呼び出さなければ成功状態になる。このメソッドを呼び出した場合Interactor::Failure例外が投げられるが、callメソッドを使ってInteractorを呼び出しているのであれば、Interactorの内側でこの例外は処理される。

また、

context.error = '失敗'
context.fail!

と書く代わりにcontext.fail!(error: '失敗')という書き方ができる。

context.success?context.failure?

Interactorが成功したかどうかを返す。成功していればcontext.success?truecontext.failure?falseを返す。

context.rollback!

成功したInteractorのrollbackメソッドが呼ばれる。rollbackメソッドはcallメソッドのように自分で定義する必要がある。失敗したInteractorの場合はrollbackメソッドは呼ばれない。

_calledcalled!

内部的に使われているメソッド。

Hook

beforeafteraroundフックがある。

around do |interactor|
  # ...
  interactor.call
  # ...
end

Controllerからの呼び出し

CreateUserというInteractorがある場合、Controllerからの呼び出しは単にCreateUser.call(name: params[:user][:name])のようにすれば良い。あるいは、permitを使ってCreateUser.call(params.require(:user).permit(:name))でもよい。

すでに書いている通り、CreateUser.callの戻り値としてContextが返される。この戻り値を利用してControllerでは処理を行う。

class UsersController < ApplicationController
  def create
    result = CreateUser.call(user_params)
    @user = result.user

    if result.success?
      redirect_to @user, notice: "User was successfully created."
      return
    end
    render :new, status: :unprocessable_entity
  end
end

Interactor

基本的なInteractorは以下のような感じで記述する。

class CreateUser
  include Interactor

  def call
    user = User.new(name: context.name)
    context.user = user
    context.fail! unless user.valid?

    user.save!
  rescue ActiveRecord::RecordInvalid
    context.fail!
  end
end

いくつかのInteractorをまとめたい場合、Organizerという機能を使う。

class Campaign
  include Interactor::Organizer

  organize CreateUser # , OtherInteractor, ...
end

organizeに指定したInteractorが順々に実行される。失敗したInteractorが現れた場合はそこで実行は打ち切られる。Organizer自身にもcallメソッドを定義できるが、superを呼ばないといけないので注意。