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

業務で [interactor gem](https://github.com/collectiveidea/interactor)[^1] を使用する機会があったのですが、日本語の情報がそれほど多くなかったためまとめました。

[^1]: Copyright (c) 2013 Collective Idea, Released under the MIT license: https://github.com/collectiveidea/interactor/blob/master/LICENSE.txt

全2部作となっていて、
1. Interactor 概論（今回）
2. [ActiveInteractor Gem で Interactor を使いやすく](https://qiita.com/verdy89/items/8feacd32d5e08942b9d6)
という構成でお届けします。
今回は、 interactor gem の [README](https://github.com/collectiveidea/interactor-rails) を翻訳したもの +α の内容となっています。

# Interactor とは
Interactor とは、アプリケーションの[ビジネスロジック](https://en.wikipedia.org/wiki/Business_logic)をカプセル化するために使われるオブジェクトです。それぞれの Interactor はアプリケーションが行う1つの動作を表現します。

## [ビジネスロジック](https://en.wikipedia.org/wiki/Business_logic)とは
ビジネスロジックとは、アプリケーションにおいてデータがどのように作られ、保存され、変更されるか（ = ビジネスルール）を記述したソースコードのことです。データベースそのものの管理やUIの表示、システム構成やプログラムの様々な部分の連動などとは区別されます。

## Context
それぞれの Interactor は、自身が動作するために必要な情報を集めたオブジェクトである Context を持っています。Interactor が動作すると、その Interactor が持つ Context が変更されます。

### Context に情報を追加する

```ruby
context.user = user
```

このように記述すると、 Context にユーザーの情報を追加することができます。

### Context を落とす
Interactor の動作に問題が生じた場合、以下のようにして Context を落とすことができます。

```ruby
context.fail!
```

以下のようにしてエラーメッセージを追加することもできます。

```ruby
context.error = "Boom!"
context.fail!

# または
context.fail!(error: "Boom!")
```

Context が落ちているかどうかを確認することもできます。

```ruby
context.failure? # => false
context.fail!
context.failure? # => true

# または
context.success? # => true
context.fail!
context.success? # => false
```

### 落ちた Context を処理する
`context.fail!` が実行されると常に例外タイプ `Interactor::Failure` が返りますが、この例外を認識することは通常ありません。後に述べるように、コントローラから `call` クラスメソッドを用いて Interactor を呼び出し、その後 `context.success?` メソッドを使ってチェックする方法が推奨されます。`call` クラスメソッドは `fail!` が実行されても例外を発生させることはありません。
ただし、 Interactor の単体テストにおいて `call` ではないビジネスロジックメソッドを呼び出している際は、`fail!` メソッドは `Interactor::Failure` を返すことに注意してください。

## フック
### `before` フック
Interactor が実行される前に Context の準備が必要な場合は、 `before` フックを使用することができます。引数にはブロックかシンボルを指定することができます。

```ruby
before do
  context.emails_sent = 0
end

# または
before :zero_emails_sent
def zero_emails_sent
  context.emails_sent = 0
end
```

### `after` フック
Interactor が実行された後の動作を指定することもできます。

```ruby
after do
  context.user.reload
end
```

ただし、 `after` フックは Interactor が正常終了した場合にのみ実行されます。 `fail!` が実行された場合は、 `after` フックは実行されません。

### `around` フック
`before` フックや `after` フックと同様に `around` フックを定義することもできます。 `around` ブロックやこれに使われるメソッドは1つの引数を受け取り、その引数の `call` メソッドを呼び出すことで Interactor を呼び出します。

```ruby
around do |interactor|
  context.start_time = Time.now
  interactor.call
  context.finish_time = Time.now
end

# または
around :time_execution
def time_execution(interactor)
  context.start_time = Time.now
  interactor.call
  context.finish_time = Time.now
end
```

`fail!` メソッドが呼び出された場合、around フックの動作が止まり、 `interactor.call` の後に記述された処理は実行されません。

### フックの実行順
`before` フックは定義された順番に、`after` フックは定義された順番と逆に実行されます。`around` フックは`before`フックや`after`フックの外側で実行されます。
例えば、

```ruby
around do |interactor|
  puts "around before 1"
  interactor.call
  puts "around after 1"
end

around do |interactor|
  puts "around before 2"
  interactor.call
  puts "around after 2"
end

before do
  puts "before 1"
end

before do
  puts "before 2"
end

after do
  puts "after 1"
end

after do
  puts "after 2"
end
```

このように定義された場合、実行結果は以下のようになります。

```
around before 1
around before 2
before 1
before 2
after 2
after 1
around after 2
around after 1
```

### Interactor の Concern
Interactor は以下のようにして、 Concern の中で共通に用いられる複数の before/after フックを定義することができます。

```ruby
module InteractorTimer
  extend ActiveSupport::Concern

  included do
    around do |interactor|
      context.start_time = Time.now
      interactor.call
      context.finish_time = Time.now
    end
  end
end
```

## Interactor の例
ユーザーの認証を行う場合、次のようにして Interactor を用いることができます。

```ruby
class AuthenticateUser
  include Interactor

  def call
    if user = User.authenticate(context.email, context.password)
      context.user = user
      context.token = user.secret_token
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end
```

`Interactor` モジュールを include したクラスを作り、その中で `call` インスタンスメソッドを作成するだけで Interactor を定義することができます。 Interactor は、 `call` メソッドの中で自身の `context` にアクセスすることができます。

# コントローラ内での Interactor
多くの場合、 Interactor はコントローラの中で用いられます。

```ruby
class SessionsController < ApplicationController
  def create
    if user = User.authenticate(session_params[:email], session_params[:password])
      session[:user_token] = user.secret_token
      redirect_to user
    else
      flash.now[:message] = "Please try again."
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end
```

このようなコントローラは、先ほどの `AuthenticateUser` クラスを用いて次のようにリファクタリングされます。

```ruby
class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.call(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end
```

`call` クラスメソッドを用いることで、 Interactor を動作させることができます。 `call` に引数として渡したハッシュは Interactor のインスタンスである `context` に変換され、該当の Interactor 内で定義されたフックと共に実行され、最終的に変更が加えられた Context が返ります。

# Interactor の利点
ユーザーの認証を行う先ほどの例では、コントローラは以下のようになりました。

```ruby
class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.call(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end
```

このような単純なユースケースでは、 Interactor を使った方がコードの分量は多くなります。ではなぜ Interactor を使うのでしょうか？

## 動作が明快になる
Interactor は全ての破壊的変更（ `POST` や `PUT` 、 `DELETE` リクエスト）を伴う動作に対して使います。 Interactor は `app/interactors` ディレクトリに作成されるため、このディレクトリを見れば全ての開発者がアプリケーションの行う動作を理解することができます。

```
▾ app/
  ▸ controllers/
  ▸ helpers/
  ▾ interactors/
      authenticate_user.rb
      cancel_account.rb
      publish_post.rb
      register_user.rb
      remove_post.rb
  ▸ mailers/
  ▸ models/
  ▸ views/
```

TIP: Interactor は実装ではなく、ビジネスロジックに由来した名前にしましょう。アカウントの削除を行う Interactor を作成する際に  `DestroyUser` ではなく `CancelAccount` という名前にすることで、将来的に様々な責任を担うことができるようになります。

## 将来性[^2]
[^2]: ネタバレになりますが、あなたのユースケースはこれほどシンプルにはならないはずです。

今回のユーザー認証の例では、ユーザーを認証するというシンプルなタスクであっても、次のように複数の責任を担うことになるでしょう。

- 久しぶりにログインしたユーザーに対して「おかえりなさい」と言う
- パスワードを更新するように通知する
- 何度もログインが失敗するユーザーを凍結する
- 凍結した旨をメールで通知する

リストが増えていけば行くほど、コントローラの分量も増えていきます。これで長大なコントローラの出来上がりです。
その代わりに Interactor を導入することで、担うべき責任が増えたとしても、コントローラ（とそのテスト）にはほとんど（あるいは全く）変更を加える必要がなくなります。追加される責任に応じて適切に変更すべき Interactor を選ぶことで、追加される動作が変更されることを防ぐことができます。

# Interactor の種類
Interactor ライブラリの中には、普通の Interactor と Organizer という2つの種類の Interactor が定義されています。

## Interactor
普通の Interactor は `Interactor` を include し、 `call` メソッドを定義したクラスのことを指します。

```ruby
class AuthenticateUser
  include Interactor

  def call
    if user = User.authenticate(context.email, context.password)
      context.user = user
      context.token = user.secret_token
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end
```

通常の Interactor はさしずめ建築用のブロックのようなもので、アプリケーションにおいて単一の働きをします。

## Organizer
Organizer は Interactor の変種として重要な意味を持ちます。 Organizer は**他の** Interactor を動作させるためだけに用いられます。

```ruby
class PlaceOrder
  include Interactor::Organizer

  organize CreateOrder, ChargeCard, SendThankYou
end
```

`PlaceOrder` Organizer は、コントローラ内で他の Interactor と同様に用いることができます。

```ruby
class OrdersController < ApplicationController
  def create
    result = PlaceOrder.call(order_params: order_params)

    if result.success?
      redirect_to result.order
    else
      @order = result.order
      render :new
    end
  end

  private

  def order_params
    params.require(:order).permit!
  end
end
```

Organizer は、自身が organize する Interactor をまとめて順序通り実行します。 Organizer の中の Interactor は、動作して Context を変更した状態で次の Interactor に手渡します。

### ロールバック
organize された Interactor のうちどれかの動作が失敗した場合、 Organizer は動作を停止します。 `ChangeCard` Interactor が失敗した場合、 `SendThankYou` は呼び出されません。
加えて、すでに実行された Interactor は自身の行った動作を巻き戻すことができます。 `rollback` メソッドを Interactor に定義し、呼び出すだけで実現できます。

```ruby
class CreateOrder
  include Interactor

  def call
    order = Order.create(order_params)

    if order.persisted?
      context.order = order
    else
      context.fail!
    end
  end

  def rollback
    context.order.destroy
  end
end
```

動作が失敗した Interactor はロールバックされません。全ての Interactor は単一の目的を持っているはずなので、仮にその動作が失敗したとしても、その動作を巻き戻す必要はないと考えられるからです。

# Interactor のテスト
適切に書かれていれば、それぞれの Interactor は1つのことを行うだけなので、 Interactor をテストするのは簡単です。

```ruby
class AuthenticateUser
  include Interactor

  def call
    if user = User.authenticate(context.email, context.password)
      context.user = user
      context.token = user.secret_token
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end
```

テストする必要があるのは、この Interactor が行う単一の動作と、 Context に与える影響のみです。

```ruby
describe AuthenticateUser do
  subject(:context) { AuthenticateUser.call(email: "john@example.com", password: "secret") }

  describe ".call" do
    context "when given valid credentials" do
      let(:user) { double(:user, secret_token: "token") }

      before do
        allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(user)
      end

      it "succeeds" do
        expect(context).to be_a_success
      end

      it "provides the user" do
        expect(context.user).to eq(user)
      end

      it "provides the user's secret token" do
        expect(context.token).to eq("token")
      end
    end

    context "when given invalid credentials" do
      before do
        allow(User).to receive(:authenticate).with("john@example.com", "secret").and_return(nil)
      end

      it "fails" do
        expect(context).to be_a_failure
      end

      it "provides a failure message" do
        expect(context.message).to be_present
      end
    end
  end
end
```

今回は RSpec で記載しましたが、どのテストフレームワークを用いても考え方は同じです。

## 分離
上のテストにおいて、ユーザーをデータベースに作成せずに  `User.authenticate` をスタブ化したのは、 `spec/interactors/authenticate_user_spec.rb` の目的が `AuthenticateUser` Interactor をテストすることだけだったからです。 `User.authenticate` メソッドは `spec/models/user_spec.rb` でテストされます。

モデルに独自のインターフェースを定義するのは賢いやり方です。このようにすることで、 Interactor に属するある責任がどのモデルと関連づけられるのかがわかりやすくなり、コードを記述しやすくなります。 `User.authenticate` メソッドは良い例です。もしこの Interactor が

```ruby
class AuthenticateUser
  include Interactor

  def call
    user = User.where(email: context.email).first

    # ユーザーを認証する処理が直接記述されている
    if user && BCrypt::Password.new(user.password_digest) == context.password
      context.user = user
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end
```

このようになっていた場合、この Interactor をテストするのはとても難しくなります。仮にテストできたとしても、共にモデルの関心事であるORMや暗号化アルゴリズムを変更した瞬間に、ビジネスロジックの関心事であるこの Interactor は壊れてしまいます。
1行1行の目的を明確にしましょう。

## 統合
Interactor のテストを分離することが大事である一方で、統合テストを適切に行うことも重要です。
あるメソッドをスタブ化すると、そのメソッドが壊れたり変更されたり、もしくはそもそも存在しなかったりする場合を隠蔽することができてしまいます。
統合テストを書けば、アプリケーションの個々のパーツが期待通り動くかどうかを確かめることができ、これは Interactor のような新しいレイヤーをプログラムに追加する場合に特に重要です。
TIP: テストカバレッジを見るのであれば、**統合テストを書く前に**100%のカバレッジを目指しましょう。それから統合テストを書けば、あなたは夜も安心して寝ることができます。

## コントローラ
Interactor を用いる利点のひとつは、コントローラやそのテストを完結にすることができることです。 Interactor は単体テストと統合テストでテストされているので（ですよね？）、コントローラのテストからビジネスロジックのテストを省くことができます。

```ruby
class SessionsController < ApplicationController
  def create
    result = AuthenticateUser.call(session_params)

    if result.success?
      session[:user_token] = result.token
      redirect_to result.user
    else
      flash.now[:message] = t(result.message)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end
```
```ruby
describe SessionsController do
  describe "#create" do
    before do
      expect(AuthenticateUser).to receive(:call).once.with(email: "john@doe.com", password: "secret").and_return(context)
    end

    context "when successful" do
      let(:user) { double(:user, id: 1) }
      let(:context) { double(:context, success?: true, user: user, token: "token") }

      it "saves the user's secret token in the session" do
        expect {
          post :create, session: { email: "john@doe.com", password: "secret" }
        }.to change {
          session[:user_token]
        }.from(nil).to("token")
      end

      it "redirects to the homepage" do
        response = post :create, session: { email: "john@doe.com", password: "secret" }

        expect(response).to redirect_to(user_path(user))
      end
    end

    context "when unsuccessful" do
      let(:context) { double(:context, success?: false, message: "message") }

      it "sets a flash message" do
        expect {
          post :create, session: { email: "john@doe.com", password: "secret" }
        }.to change {
          flash[:message]
        }.from(nil).to(I18n.translate("message"))
      end

      it "renders the login form" do
        response = post :create, session: { email: "john@doe.com", password: "secret" }

        expect(response).to render_template(:new)
      end
    end
  end
end
```

全ての魔法は Interactor の中で起きているので、このコントローラのテストは、アプリケーションが運用され続ける限り小さな変更を加え続けなければいけないでしょう。

## Rails
Interactor を Rails で使うときは、 Interactor は `app/interactors` に格納し、次のように動詞となる名前をつけています。

- `AddProductToCart`
- `AuthenticateUser`
- `PlaceOrder`
- `RegisterUser`
- `RemoveProductFromCart`

参照： [interactor-rails](https://github.com/collectiveidea/interactor-rails)