LoginSignup
10
4

More than 3 years have passed since last update.

interactor gem についてまとめてみた (2/2)

Last updated at Posted at 2021-03-12

Interactor のまとめ記事第2弾です。

全2部作となっていて、
1. Interactor 概論
2. ActiveInteractor Gem で Interactor を使いやすく(今回)
という構成でお届けしています。
今回は、 activeinteractor の wiki をベースに、 activeinteractor 特有の機能について説明します。

前回のおさらい

  • ビジネスロジックをカプセル化するためのオブジェクトとして Interactor というものがある
  • それぞれの Interactor は、自身が所持する Context を変更する形で外部に影響を及ぼす
  • フックを使って、 Interactor の実行前後に実施する処理をまとめることができる
  • Interactor を使うことで、動作が明快になったり、コントローラの肥大化を防いだりすることができる
  • Interactor には、普通の Interactor と、それを束ねる Organizer という2種類がある
  • Interactor が行う動作がきちんと1つに制限されていれば、テストを書くのは簡単

ActiveInteractor

概要

Interactor Gem を改良した Gem として、今回は activeinteractor1 を紹介します。
基本的な使い方は同じですが、以下の点を中心に書き方に違いが見られます。

  • Interactor は ActiveInteractor::Base を、 Context は ActiveInteractor::Context::Base を継承する
  • .call ではなく .perform
  • バリデーションが使える
  • コールバックやヘルパーメソッドが便利

Interactor と Organizer

Interactor や Organizer はクラスの継承で表現されます。

# interactor gem
class AuthenticateUser
  include Interactor

  def call
    # ...
  end
end

# activeinteractor gem
class AuthenticateUser < ActiveInteractor::Base
  def perform
    # ...
  end
end
# interactor gem
class PlaceOrder
  include Interactor::Organizer
  organize CreateOrder, ChargeCard, SendThankYou
end

# activeinteractor gem
class PlaceOrder < ActiveInteractor::Organizer::Base
  organize :create_order, :charge_card, :send_thank_you
end

organize メソッドはブロックを引数にとることができ、 Interactor を organize するかどうか決める条件やコールバックを指定することができます。

class PlaceOrder < ActiveInteractor::Organizer::Base
  organize do
    add :create_order, if :user_registered?
    add :charge_card, if: -> { context.order }
    add :send_thank_you, if: -> { context.order }
  end

  private

  def user_registered?
    context.user&.registered?
  end
end
class PlaceOrder < ActiveInteractor::Organizer::Base
  organize do
    add :create_order, before: -> { puts context.order }, after: :print_order_id
    add :charge_card
    add :send_thank_you
  end

  private

  def print_order_id
    puts context.order.id
  end
end

一方の Interactor の Context が他方の Interactor の実行に依存しないとき、Organizer で .perform_in_parallel メソッドを使うことで、 Interactor を並行して動作させることができます。

class CreateNewUser < ActiveInteractor::Base
  def perform
    context.user = User.create(
      first_name: context.first_name,
      last_name: context.last_name
    )
  end
end

class LogNewUserCreation < ActiveInteractor::Base
  def perform
    context.log = Log.create(
      event: 'new user created',
      first_name: context.first_name,
      last_name: context.last_name
    )
  end
end

class CreateUser < ActiveInteractor::Organizer::Base
  perform_in_parallel
  organize :create_new_user, :log_new_user_creation
end

CreateUser.perform(first_name: 'Aaron', last_name: 'Allen')
#=> <#CreateUser::Context first_name='Aaron' last_name='Allen' user=>#<User ...> log=<#Log ...>>

Context

Context と Interactor の紐付け

Context は ActiveInteractor::Context::Base を継承します。それぞれの Interactor は、自身に紐づく Context を以下の順番で探索します。

  1. InteractorName::Context
  2. InteractorNameContext

該当するクラスが存在しない場合は、InteractorName::Context < ActiveInteractor::Context::Base が自動的に作成されます。

class MyInteractor < ActiveInteractor::Base; end
class MyInteractor::Context < ActiveInteractor::Context::Base; end

MyInteractor.context_class
#=> MyInteractor::Context
class MyInteractorContext < ActiveInteractor::Context::Base; end
class MyInteractor < ActiveInteractor::Base; end

MyInteractor.context_class
#=> MyInteractorContext
class MyInteractor < ActiveInteractor::Base; end

MyInteractor.context_class
#=> MyInteractor::Context

.contextualize_with メソッドを使って、紐付ける Context を明示的に指定することもできます。

class MyGenericContext < ActiveInteractor::Context::Base; end

class MyInteractor
  contextualize_with :my_generic_context
end

MyInteractor.context_class
#=> MyGenericContext

明示的にプロパティを指定する

それぞれの Context は、 Interactor が動作した後に自身にどんなプロパティが指定されているべきか明示的に定義することができます。.attributes メソッドを使うことで、 Context にどのようなプロパティが割り当てられているか確認することができます。

class MyInteractorContext < ActiveInteractor::Context::Base
  attributes :first_name, :last_name, :email, :user
end

class MyInteractor < ActiveInteractor::Base; end

result = MyInteractor.perform(
  first_name: 'Aaron',
  last_name: 'Allen',
  email: 'hello@aaronmallen.me',
  occupation: 'Software Dude'
)
#=> <#MyInteractor::Context first_name='Aaron' last_name='Allen' email='hello@aaronmallen.me' occupation='Software Dude'>

result.attributes
#=> { first_name: 'Aaron', last_name: 'Allen', email: 'hello@aaronmallen.me' }

result.occupation
#=> 'Software Dude'

バリデーション

ActiveModel::Validations で定義されているバリデーション用のメソッドは、 Context でも使用することができます。 Interactor 内でも context_ という接頭辞をつけることでバリデーションメソッドを使うことができますが、 Context クラスの内部でバリデーションを完結させることが推奨されます。
バリデーションを行うタイミングは、以下の2通りに指定できます。

  • :calling : #perform の前にバリデーションを行う
  • :called : #perform の後にバリデーションを行う
class MyInteractorContext < ActiveInteractor::Context::Base
  attributes :first_name, :last_name, :email, :user

  # #perform の前でのみバリデーションされる
  validates :first_name, presence: true, on: :calling

  # #perform の前後でバリデーションされる
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }

  # #perform の後でのみバリデーションされる
  validates :user, presence: true, on: :called
  validate :user_is_a_user, on: :called

  private

  def user_is_a_user
    return if user.is_a?(User)

    errors.add(:user, :invalid)
  end
end

class MyInteractor < ActiveInteractor::Base
  def perform
    context.user = User.create_with(
      first_name: context.first_name,
      last_name: context.last_name
    ).find_or_create_by(email: context.email)
  end
end

result = MyInteractor.perform(last_name: 'Allen')
#=> <#MyInteractor::Context last_name='Allen>

result.failure? #=> true
result.valid?   #=> false
result.errors[:first_name] #=> ['can not be blank']

result = MyInterator.perform(first_name: 'Aaron', email: 'hello@aaronmallen.me')
#=> <#MyInteractor::Context first_name='Aaron' email='hello@aaronmallen.me' user=<#User ...>>

result.success?      #=> true
result.valid?        #=> true
result.errors.empty? #=> true

コールバック

バリデーション前後のコールバック

.before_context_validation でバリデーション前に実行されるコールバックを、 .after_context_validation でバリデーション後に実行されるコールバックを指定することができます。

class MyInteractorContext < ActiveInteractor::Context::Base
  attributes :first_name, :last_name, :email
  validates :last_name, presence: true
end

class MyInteractor < ActiveInteractor::Base
  before_context_validation { context.last_name ||= 'Unknown' }
end

result = MyInteractor.perform(first_name: 'Aaron', email: 'hello@aaronmallen.me')
result.valid?
#=> true

result.last_name
#=> 'Unknown'
class MyInteractorContext < ActiveInteractor::Context::Base
  attributes :first_name, :last_name, :email
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end

class MyInteractor < ActiveInteractor::Base
  after_context_validation { context.email&.downcase! }
end

result = MyInteractor.perform(first_name: 'Aaron', last_name: 'Allen', email: 'HELLO@AARONMALLEN.ME')
result.valid? #=> true
result.email  #=> 'hello@aaronmallen.me'

#perform 前後のコールバック

#perform 前後に実行されるコールバックは、 .before_perform .after_perform .around_perform で指定することができます。

class MyInteractor < ActiveInteractor::Base
  before_perform :print_start

  def perform
    puts 'Performing'
  end

  private

  def print_start
    puts 'Start'
  end
end

MyInteractor.perform
# Start
# Performing
#=> <#MyInteractor::Context...>
class MyInteractor < ActiveInteractor::Base
  around_perform :track_time

  def perform
    sleep(1)
  end

  private

  def track_time
    context.start_time = Time.now.utc
    yield
    context.end_time = Time.now.utc
  end
end

result = MyInteractor.perform
result.start_time #=> 2019-01-01 00:00:00 UTC
result.end_time #=> 2019-01-01 00:00:01 UTC
class MyInteractor < ActiveInteractor::Base
  after_perform :print_done

  def perform
    puts 'Performing'
  end

  private

  def print_done
    puts 'Done'
  end
end

MyInteractor.perform
# Performing
# Done
#=> <#MyInteractor::Context...>

ロールバック前後のコールバック

同様に、 .before_rollback .after_rollback .around_rollback で指定することができます。具体例は省略します。

Organizer のコールバック

Organizer が organize するそれぞれの Interactor について、それぞれの実行前後に実行するコールバックを .before_each_perform .after_each_perform .around_each_perform で指定することができます。.before_each_perform のみ具体例を見てみます。

class MyInteractor1 < ActiveInteractor::Base
  def perform
    puts 'MyInteractor1'
  end
end

class MyInteractor2 < ActiveInteractor::Base
  def perform
    puts 'MyInteractor2'
  end
end

class MyOrganizer < ActiveInteractor::Organizer::Base
  before_each_perform :print_start

  organized MyInteractor1, MyInteractor2

  private

  def print_start
    puts "Start"
  end
end

MyOrganizer.perform
# Start
# MyInteractor1
# Start
# MyInteractor2
#=> <MyOrganizer::Context...>

Rails で便利なコマンド

rails g

rails g を使ってファイルを作成することができます。

# ActiveInteractor の導入
rails generate active_interactor:install

# 各種ファイルの生成
rails generate interactor NAME [context_attributes] [options]
rails generate interactor:organizer NAME [interactor interactor] [options]
rails generate interactor:context NAME [attribute attribute] [options]

ジェネレーターの設定を以下のように行うことができます。

# config/application.rb
module MyApplication
  class Application < Rails::Application
    config.generators.interactors :active_interactor,
      dir: 'my_directory', # ファイルを生成するディレクトリを指定
      generate_context: false # Context のクラスを自動生成するかどうかを指定
    end
  end
end

ActiveRecord のモデルを Context として使用する

ActiveRecord モデルを Context として使用するには、 モデル内で acts_as_context を使用した後、 Interactor で contextualize_with メソッドを使用します。

# app/models/user
class User < ApplicationRecord
  acts_as_context
end

# app/interactors/create_user
class CreateUser < ApplicationInteractor
  contextualize_with :user

  def perform
    context.email&.downcase!
    context.save
  end
end

CreateUser.perform(email: 'HELLO@AARONMALLEN.ME')
#=> <#User id=1 email='hello@aaronmallen.me'>

  1. Copyright (c) 2019 Aaron Allen, Released under the MIT license: https://github.com/aaronmallen/activeinteractor/blob/main/LICENSE 

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4