Interactor のまとめ記事第2弾です。
全2部作となっていて、
- Interactor 概論
- 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 を以下の順番で探索します。
InteractorName::Context
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'>
-
Copyright (c) 2019 Aaron Allen, Released under the MIT license: https://github.com/aaronmallen/activeinteractor/blob/main/LICENSE ↩