active_interaction でアプリケーション固有のビジネスロジックを管理する

# はじめに

active_interaction というコマンドパターンを実装した gem があります。
どういうメリットがあるのか理解を深めるために、README の部分的な翻訳 + 理解しやすいように加筆をしてみます。

> Active Interactionは、アプリケーション固有のビジネスロジックを管理します。
> これはRubyのコマンドパターンを実装したものです。

> Active Interactionは、ビジネス・ロジックを配置する場所を提供します。
> また、入力が期待通りのものであるかどうかを検証することで、より安全なコードを書くことができます。ActiveModel が名詞を扱う場合、Active Interaction は動詞を扱います。

# Rails での使い方

インタラクションは `app/interaction` に入れることをお勧めします。

また、モデルごとにグループ化するのも非常に便利です。
例えば Account モデルに関する処理は `app/interactions/accounts` 配下で探すことができます。

```
- app/
  - controllers/
    - accounts_controller.rb
  - interactions/
    - accounts/
      - create_account.rb
      - destroy_account.rb
      - find_account.rb
      - list_accounts.rb
      - update_account.rb
  - models/
    - account.rb
  - views/
    - account/
      - edit.html.erb
      - index.html.erb
      - new.html.erb
      - show.html.erb
```

この構造を使うためには以下を `application.rb` に追加してください。

```ruby
# config/application.rb
config.autoload_paths += Dir.glob("#{config.root}/app/interactions/*")
```

## Index

```ruby
# GET /accounts
def index
  @accounts = ListAccounts.run!
end
```

```ruby
class ListAccounts < ActiveInteraction::Base
  def execute
    Account.not_deleted.order(last_name: :asc, first_name: :asc)
  end
end
```

`ListAccounts` には何も入力を渡さないので、`.run` の代わりに `.run!` を使うのは理にかなっています。
これが失敗した場合、インタラクションの記述に失敗したことになります。

## Show

このアクションでは、正しいエラーを発生させるためのヘルパーメソッドを定義します。

`.run!` を呼び出した時、エラーが発生すると `ActiveInteraction::InvalidInteractionError` が発生します。つまり、`ActiveRecord::RecordNotFound` ではなくなるので、明示的に `fail ActiveRecord::RecordNotFound`と書く必要があります。


```ruby
# GET /accounts/:id
def show
  @account = find_account!
end

private

def find_account!
  outcome = FindAccount.run(params)

  if outcome.valid?
    outcome.result
  else
    fail ActiveRecord::RecordNotFound, outcome.errors.full_messages.to_sentence
  end
end
```

これはおそらく、あなたが慣れ親しんでいるものとは少し違っているように見えます。
Railsは一般的に `before_action` で `@account` を定義します。

なぜこのようなインタラクションコードがすべて良いのでしょうか？
理由は2つあります。1つは、APIコントローラや `Resque` タスクなど、別の場所で `FindAccount` インタラクションを再利用できるからです。2つ目は、アカウントの検索方法を変更したい場合は、1つの場所 (今回の場合は `FindAccount` クラス) を変更するだけで済むということです。

インタラクションの内部では、`#find_by_id` の代わりに `#find` を使うことができます。そうすれば、コントローラの `#find_account!` ヘルパーメソッドは不要になります。しかし、<b>インタラクションからエラーを発生させないようにしなければなりません。</b>そうすると、<b>結果の妥当性だけでなく例外が発生することにも対処しなければいけないからです。</b> 

```ruby
class FindAccount < ActiveInteraction::Base
  integer :id

  def execute
    account = Account.not_deleted.find_by_id(id)

    if account
      account
    else
      errors.add(:id, 'does not exist')
    end
  end
end
```

実行中にエラーを追加しても全く問題ないことに注意してください。
すべてのエラーが型チェックやバリデーションに起因するものである必要はありません。

## New

New は、これまで見てきたものとは少し異なります。
`.run` や `.run!` を呼び出す代わりに、新しいインタラクションを初期化します。これはインタラクションが `ActiveModels` のように振る舞うためです。

```ruby
# GET /accounts/new
def new
  @account = CreateAccount.new
end
```

インタラクションは `ActiveModels` のように動作するので、 `ActiveModel` のバリデーションを使用することができます。ここでは `first_name` と `last_name` が空白になっていないことを確認するためにバリデーションを使用します。

```ruby
class CreateAccount < ActiveInteraction::Base
  string :first_name, :last_name

  validates :first_name, :last_name,
    presence: true

  def to_model
    Account.new
  end

  def execute
    account = Account.new(inputs)

    unless account.save
      errors.merge!(account.errors)
    end

    account
  end
end
```

ここではいくつかの高度な機能を使用しました。`to_model` メソッドは、ビューで使用する正しいフォームを決定するのに役立ちます。詳細はフォームのセクションをチェックしてください。`execute`の中では、エラーをマージします。これはエラーをあるオブジェクトから別のオブジェクトに移動させるのに便利な方法です。詳しくはエラーのセクションを参照してください。

## Create

createアクションはnewアクションと多くの共通点があります。どちらも `CreateAccount` インタラクションを使用します。アカウントの作成に失敗した場合、このアクションは新しいアクションのレンダリングにフォールバックします。

```ruby
# POST /accounts
def create
  outcome = CreateAccount.run(params.fetch(:account, {}))

  if outcome.valid?
    redirect_to(outcome.result)
  else
    @account = outcome
    render(:new)
  end
end
```

`.run` にハッシュを渡さなければならないことに注意してください。 `nil` を渡すとエラーになります。

インタラクションを使用しているので、Strong Parameter は必要ありません。
インタラクションはフィルタで定義されていない入力を無視します。つまり、 `params.require` や `params.permit` は無くても大丈夫です。

## Destroy

`destroy` アクションは、先ほど書いた `#find_account!` ヘルパーメソッドを再利用します。

```ruby
# DELETE /accounts/:id
def destroy
  DestroyAccount.run!(account: find_account!)
  redirect_to(accounts_url)
end
```

この単純な例では、`destroy` インタラクションはあまり何もしていません。これをインタラクションに入れることで何かを得ることは明らかではありません。しかし、将来的に `account.destroy` 以上のことをする必要があるときには、`DestrotAccount` クラスを変更するだけで済むようになるでしょう。

## Edit

`destroy` アクションと同様に、編集は `#find_account!` ヘルパーを使用します。
そして、フォームオブジェクトとして使用するための新しいインタラクションのインスタンスを作成します。

```ruby
# GET /accounts/:id/edit
def edit
  account = find_account!
  @account = UpdateAccount.new(
    account: account,
    first_name: account.first_name,
    last_name: account.last_name)
end
```

アカウントを更新するインタラクションは、他のインタラクションよりも複雑です。更新にはアカウントが必要ですが、その他の入力は任意です。省略された場合、その属性は無視されます。属性が存在する場合は、それらを更新します。

ActiveInteractionは入力に対して述語メソッド(`#first_name?`のような)を生成します。これらのメソッドは入力が `nil` の場合は`false` を返し、そうでない場合は `true` を返します。述語についての詳細は述語のセクションを確認してください。

```ruby
class UpdateAccount < ActiveInteraction::Base
  object :account

  string :first_name, :last_name,
    default: nil

  validates :first_name,
    presence: true,
    if: :first_name?
  validates :last_name,
    presence: true,
    if: :last_name?

  def execute
    account.first_name = first_name if first_name?
    account.last_name = last_name if last_name?

    unless account.save
      errors.merge!(account.errors)
    end

    account
  end
end
```

## Update

もうコツがつかめたでしょう。アカウントを取得するために `#find_account!` を使い、 `UpdateAccount` のための入力を作成します。そしてインタラクションを実行し、更新されたアカウントにリダイレクトするか、編集ページに戻ります。

```ruby
# PUT /accounts/:id
def update
  inputs = { account: find_account! }.reverse_merge(params[:account])
  outcome = UpdateAccount.run(inputs)

  if outcome.valid?
    redirect_to(outcome.result)
  else
    @account = outcome
    render(:edit)
  end
end
```

# 突っ込んだ使い方

## Callbacks

ActiveModel はコールバックを定義するための強力なフレームワークを提供しています。Active Interaction はそのフレームワークにフックしてインタラクションのライフサイクルの様々な部分にフックすることができます。

```ruby
class Increment < ActiveInteraction::Base
  set_callback :type_check, :before, -> { puts 'before type check' }

  integer :x

  set_callback :validate, :after, -> { puts 'after validate' }

  validates :x,
    numericality: { greater_than_or_equal_to: 0 }

  set_callback :execute, :around, lambda { |_interaction, block|
    puts '>>>'
    block.call
    puts '<<<'
  }

  def execute
    puts 'executing'
    x + 1
  end
end

Increment.run!(x: 1)
# before type check
# after validate
# >>>
# executing
# <<<
# => 2
```

利用可能なコールバックは、順に `type_check` 、 `validate` 、 `execute` です。
これらのコールバックには `before`、`after`、`around` のいずれかを設定することができます。

## Composition

他のインタラクションの中から `#compose` を使ってインタラクションを実行することができます。
インタラクションが成功した場合は、結果を返します（`.run` で呼び出した場合と同じように）。
何か問題が発生した場合、実行はすぐに停止し、エラーは呼び出し元に移動します。

```ruby
class Add < ActiveInteraction::Base
  integer :x, :y

  def execute
    x + y
  end
end

class AddThree < ActiveInteraction::Base
  integer :x

  def execute
    compose(Add, x: x, y: 3)
  end
end

AddThree.run!(x: 5)
# => 8
```

他のインタラクションからフィルタを導入するには、`.import_filters` を使用します。入力と組み合わせれば、他のインタラクションに委任するのも簡単です。

```ruby
class AddAndDouble < ActiveInteraction::Base
  import_filters Add

  def execute
    compose(Add, inputs) * 2
  end
end
```

コンポジションされたインタラクションのエラーには、いくつかの厄介なケースがあることに注意してください。
それらについての詳細は、エラーのセクションを参照してください。

# トランザクションを貼りたい場合は？ (番外編)

元々はトランザクションをサポートしていたが削除したとのこと。

[Remove transaction support?](https://github.com/AaronLasseigne/active_interaction/issues/205)

> 私たちが元々これらを追加したのは、インタラクションが複雑なビジネスロジックを管理するために使われると考えたからです。実際には、必ずしもそうとは限りません。 ActiveRecord トランザクションの薄いラッパーを提供するより、何も提供しない方が良いかもしれません。

```ruby
# As it is currently:
class AutomaticTransactionInteraction < ActiveInteraction::Base
  def execute
    # ...
  end
end

# As it might be:
class ManualTransactionInteraction < ActiveInteraction::Base
  def execute
    ActiveRecord::Base.transaction do
      # ...
    end
  end
end
```

トランザクションを貼りたい場合は、後者の方で書けばよくない？という話。
具体例としては、以下のようになるだろうか。

```ruby
def execute
  ActiveRecord::Base.transaction do
    compose(A)
    compose(B)
    raise ActiveRecord::Rollback if invalid?
  end
end
```


# ある程度使ってみて

この記事を読んでメリットを感じた方は、ぜひ active_interaction を使ってみてください。

しかし、gem を使うとどうしても暗黙的なコードを書く必要がでてきます。この記事を読んでメリットを感じなかった場合や、チームで以下のようなルールを運用をできる場合は、普通に Plain Ruby で Service 層を作るのが良いかと思います。

```
・ クラス名には動詞と目的語と 「Service」 を付ける
・ 1つのサービスに public なメソッドは、原則1つにする
・ 切り分けたメソッドは全て private なメソッドとして実装する
```

Service の作り方がルールとして徹底されている現場で長く働いていると、gem を入れるまでもないと感じています。 

# 外部リンク

- [active_interaction](https://github.com/AaronLasseigne/active_interaction)
- [Railsにおけるサービスクラスのオリジナルルール](https://qiita.com/chrischris0801/items/58a12d17a440b842db02)