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

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

全2部作となっていて、
1. [Interactor 概論](https://qiita.com/verdy89/items/00a2992a9a62cacec00e)
2. ActiveInteractor Gem で Interactor を使いやすく（今回）
という構成でお届けしています。
今回は、 activeinteractor の [wiki](https://github.com/aaronmallen/activeinteractor/wiki) をベースに、 activeinteractor 特有の機能について説明します。

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

# ActiveInteractor
## 概要
Interactor Gem を改良した Gem として、今回は [activeinteractor](https://github.com/aaronmallen/activeinteractor)[^1] を紹介します。
基本的な使い方は同じですが、以下の点を中心に書き方に違いが見られます。

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

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

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

```ruby
# interactor gem
class AuthenticateUser
  include Interactor

  def call
    # ...
  end
end

# activeinteractor gem
class AuthenticateUser < ActiveInteractor::Base
  def perform
    # ...
  end
end
```

```ruby
# 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 するかどうか決める条件やコールバックを指定することができます。

```ruby
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
```

```ruby
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 を並行して動作させることができます。

```ruby
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` が自動的に作成されます。

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

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

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

MyInteractor.context_class
#=> MyInteractor::Context
```

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

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

class MyInteractor
  contextualize_with :my_generic_context
end

MyInteractor.context_class
#=> MyGenericContext
```

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

```ruby
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` の後にバリデーションを行う

```ruby
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` でバリデーション後に実行されるコールバックを指定することができます。

```ruby
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'
```
```ruby
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` で指定することができます。

```ruby
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...>
```
```ruby
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
```
```ruby
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` のみ具体例を見てみます。

```ruby
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]
```

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

```ruby
# 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` メソッドを使用します。

```ruby
# 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'>
```