Hanamiのサービス(インタラクタ)層について

[公式](https://guides.hanamirb.org/architecture/interactors/)の意訳に適当に加筆(減筆?)/修正したものです。(ちなみに著者の英語の偏差値は30台です。)

# 2019-12-28 追記

以下はHanamiのバージョンが1.1の頃のものです。
[URL](http://hanamirb.org/guides/1.1/architecture/interactors/)も404になってしまいました。
URLだけは最新のものに書き換えました(Thanks @topstone)が内容は古くなっている可能性があります。

---

# インタラクタとは

Hanamiにはコードを整理するためのオプションのツールを提供しています。それはインタラクタといい、サービスやユースケース、オペレーションなどとも呼ばれます。

以下、[事例](https://guides.hanamirb.org/introduction/getting-started/)を通して使い方を見ていきます。

## 新しい要求: メール通知 (A New Feature: Email Notifications)

こんな要求があったとします。

> 管理者は本が追加されたときにメール通知を受け取りたい。

アプリケーションには認証がないため、だれでも本を追加できます。管理者のメールアドレスは環境変数で与えます。

これはインタラクタ、具体的には`Hanami::Interactor`を使ういい例です。

この例は以下のような別の要求の基礎を提供することができます。

- 新しい書籍が投稿される前に管理者の承認を得る
- ユーザーは電子メールアドレスを入力し、特別なリンクを使用して本を編集することができる。

このプラクティスでは基本的な幾つかの実装のためにインタラクタを使うことができます。これは、コードベースの複雑さを管理するために、一度にいくつかのことをしたいときに特に便利です。

インタラクタは些細ではない(non-trivial)ビジネスロジックを分離するために使用されます。これは単一責任の原則に沿っています。

Webアプリケーションでは、インタラクタは一般的にコントローラのアクションから呼び出されます。これにより、関心事を分けることができます。 あなたのビジネスロジックオブジェクト、インタラクタはWebについて全く知らないでしょう。

## コールバック? いやいや、クッサいコールバックなんていりません！ (Callbacks? We Don't Need No Stinkin' Callbacks!)

<details><summary>簡単な実装方法はcallbackを追加することです。つまり、データベースに新しい本が追加されたあとにメールが送信されます。</summary>
<div>[訳註] Railsでいうとこんな感じ?

```rb:book.rb
class book < ApplicationRecord
  after_create :send_mail_to_admin
end
```
</div></details>

このような仕組みをHanamiは提供していません。これは永続コールバック[^consider persistence]が**アンチパターン**だと考えられるからです。永続コールバックは単一責任の原則に反します。この場合、永続性とメール通知(の責任)が不適切に混在しています。

[^consider persistence]: [訳註] 多分、永続(= DBへの保存)に対するコールバックのこと

上記のような実装をした場合、テストやその他の点でこのコールバックをスキップしたくなるでしょう。また、このような実装は同じイベント(今回の場合はDBへの保存)に対する複数のコールバックが特定の順序でトリガーされるため、すぐ混乱をおこします。

代わりに、**暗黙を明示に(explicit over implicit)**しましょう。

インタラクタは特定のユースケースを表すオブジェクトです。

インタラクタは各クラスに単一の責任を持たせます。インタラクタの単一責任は特定の結果を達成するためにオブジェクトとメソッドコールを順番に結合します。[^bow1]

[^bow1]: [訳註] 訳があやしい。　原文->[An Interactor's single responsibility is to combine object and method calls in order to achieve a specific outcome.]

そのために`Hanami::Interactor`モジュールが提供されています。そのため、ピュアRuby(Plain Old Ruby)オブジェクトとして始めて、必要に応じて`Hanami::Interactor`をインクルードすることができます。

## コンセプト (Concept)

インタラクタの背後にあるアイデアは機能の一部を新しいクラスに分離することです。

振舞いを単一のオブジェクトにカプセル化することでテストが簡単になります。これは複雑さを隠して、明示しないよりもコードを理解するのを容易にします。

## 準備 (Preparing)

[bookshelf application](http://hanamirb.org/guides/getting-started)にメール通知の要求を実装していきましょう。

## インタラクタを作ろう (Creating Our Interactor)

インタラクタを作ってみましょう。まず、以下のディレクトリを作ってください。

```shell-session
% mkdir lib/bookshelf/interactors
% mkdir spec/bookshelf/interactors
```

なぜこれらを`lib`に入れるのでしょう?それはwebアプリケーションから切り離されているからです。[^lib]

[^lib]: [訳註] Hanamiではwebに関するコード(Controller, View, Routesなど)を`apps/web`にいれて、webに依存しないビジネスロジックに関するコード(Entity, Repositoryなど)を`lib`に入れます。

新しいスペック`spec/bookshelf/interactors/add_book_spec.rb`を書いて、`AddBook`インタラクタを呼びましょう。

```rb:spec/bookshelf/interactors/add_book_spec.rb
require 'spec_helper'

describe AddBook do
  let(:interactor) { AddBook.new }

  it "succeeds" do
    expect(interactor.call).to be_a_success
  end
end
```

次に`AddBook`クラスを作りましょう。

```rb:lib/bookshelf/interactors/add_book.rb
require 'hanami/interactor'

class AddBook
  include Hanami::Interactor

  def initialize
    # set up the object
  end

  def call(book_attributes)
    # get it done
  end
end
```

このクラスには２つのメソッドしかありません。
- `initialize`: データのセットアップ
- `call`: ユースケースを満たす

テストを実行してみましょう。

```shell-session
% bundle exec rake
```

すべてのテストがパスするでしょう！

## 本の作成 (Creating a Book)

`spec/bookshelf/interactors/add_book_spec.rb`を編集します。

```rb:spec/bookshelf/interactors/add_book_spec.rb
require 'spec_helper'

describe AddBook do
  let(:interactor) { AddBook.new }
  let(:attributes) { Hash[author: "James Baldwin", title: "The Fire Next Time"] }

  describe "good input" do
    let(:result) { interactor.call(attributes) }

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

    it "creates a Book with correct title and author" do
      expect(result.book.title).to eq("The Fire Next Time")
      expect(result.book.author).to eq("James Baldwin")
    end
  end
end
```

次に、インタラクタを実装していきます。

```rb:lib/bookshelf/interactors/add_book.rb
require 'hanami/interactor'

class AddBook
  include Hanami::Interactor

  expose :book

  def initialize
    # set up the object
  end

  def call(book_attributes)
    @book = Book.new(book_attributes)
  end
end
```

ここでは次の２つの重要なことが書かれています。

1. `expose :book`ではメソッドの実行結果として返される`@book`を公開します。
2. `call`メソッドは新しいBookエンティティを`@book`に割り当てて、公開します。

これでテストはパスするでしょう。

## 作成した本の永続化 (Persisting the Book)

さきほど新しい本をビルドしましたが、まだデータベースには存在していません。

永続化のために`BookRepository`が必要です。

```rb:spec/bookshelf/interactors/add_book_spec.rb
require 'spec_helper'

describe AddBook do
  let(:interactor) { AddBook.new }
  let(:attributes) { Hash[author: "James Baldwin", title: "The Fire Next Time"] }

  describe "good input" do
    let(:result) { interactor.call(attributes) }

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

    it "creates a Book with correct title and author" do
      expect(result.book.title).to eq("The Fire Next Time")
      expect(result.book.author).to eq("James Baldwin")
    end

    it "persists the Book" do
      expect(result.book.id).to_not be_nil
    end
  end
end
```

このテストをパスさせるためにインタラクタの`call`メソッドを編集しましょう。

```rb:lib/bookshelf/interactors/add_book.rb
  def call
    @book = BookRepository.new.create(book_attributes)
  end
```

`Book.new`の代わりに`BookRepository`と`create`を使いました。相変わらず`Book`は返却されますが、レコードはデータベースに保存されます。

テストを実行すればパスするでしょう。

## 依存性の注入 (Dependency Injection)

[依存性の注入](https://martinfowler.com/articles/injection.html)を活用してリファクタリングしましょう。

依存性の注入はおすすめですが、必須ではありません。Hanamiのインタラクタ同様にオプションです。

先程のスペックは役割を果たしますが、リポジトリの振る舞いに依存しています(`id`メソッドは永続化に成功して初めて定義されます)。それはリポジトリがどのように動作するかという実装の詳細です。例えば、もし永続化の**前に**UUIDを作成し、`id`カラムに値を入れるのではなく、他の方法で保存が成功したことを表す場合、スペックを変更する必要があります。

スペックとインタラクタをより強固にすることができ、このファイル外の変更で壊れる可能性が低くなります。

インタラクタで依存性の注入を使用するには以下のようにします。

```rb:lib/bookshelf/interactors/add_book.rb
require 'hanami/interactor'

class AddBook
  include Hanami::Interactor

  expose :book

  def initialize(repository: BookRepository.new)
    @repository = repository
  end

  def call(book_attributes)
    @book = @repository.create(book_attributes)
  end
end
```

`@repository`を使う以外は基本的に一緒です。

今のところのスペックは`id`が作成されたことをチェックしてリポジトリの動作をテストしています(`expect(result.book.id).to_not be_nil`)。

これは実装の詳細です。

代わりに、スペックを単に「リポジトリが`create`メッセージを受け取ることを確認する」ように変更することができます。それはリポジトリが保存をちゃんと行ってくれることを信頼することです(そして、保存をちゃんと行うのはリポジトリの責任です)。

スペックから`it "persists the Book"`を取り除きましょう。代わりに`describe "persistence"`を作成します。

```rb:spec/bookshelf/interactors/add_book_spec.rb
require 'spec_helper'

describe AddBook do
  let(:interactor) { AddBook.new }
  let(:attributes) { Hash[author: "James Baldwin", title: "The Fire Next Time"] }

  describe "good input" do
    let(:result) { interactor.call(attributes) }

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

    it "creates a Book with correct title and author" do
      expect(result.book.title).to eq("The Fire Next Time")
      expect(result.book.author).to eq("James Baldwin")
    end
  end

  describe "persistence" do
    let(:repository) { instance_double("BookRepository") }

    it "persists the Book" do
      expect(repository).to receive(:create)
      AddBook.new(repository: repository).call(attributes)
    end
  end
end
```

これで、関心事の仕切り(the boundaries of the concern)に違反しなくなりました。

ここで行ったのはインタラクタのリポジトリの依存性の**注入**です。
<details><summary>ノート: テストしていないコード(non-test code)は何も変更する必要がありません。`repository:`キーワード引数は指定しなかった場合は新しいリポジトリオブジェクトを提供します。</summary>
<div>[訳註] ここまでの説明ではコードが出てきていませんが、リポジトリはコントローラなどから以下のようにして使います。

```rb:apps/web/controllers/book/create.rb
module Web::Controllers::Book
  class craete
    include Web::Action

    def call(params)
      AddBook.new.call(params[:book])
    end
  end
end
```

`AddBook.new`の引数を与えなかった場合、`AddBook`の`@repositoty`は`BookRepository.new`になります。そのため、依存性の注入を行う前と後でこのコードを変更する必要がない、ということをいっています。
</div></details>

## メール通知 (Email notification)

いよいよメール通知を追加しましょう!

ここで別のライブラリを使うこともできますが、`Hanami::Mailer`を使うことにしましょう。[^mail]

```shell-session
% bundle exec hanami generate mailer book_added_notification
      create  lib/bookshelf/mailers/book_added_notification.rb
      create  spec/bookshelf/mailers/book_added_notification_spec.rb
      create  lib/bookshelf/mailers/templates/book_added_notification.txt.erb
      create  lib/bookshelf/mailers/templates/book_added_notification.html.erb
```

[^mail]: [訳注] なぜ`mailers`が`apps`以下ではなく`lib`以下にあるのか疑問に思った人もいるかもしれません。安心してください。[2.0で変更の可能性があります](https://discourse.hanamirb.org/t/hanami-2-0-ideas/306/10)。

[メーラーがどのように動作するか](http://hanamirb.org/guides/mailers/overview)の詳細には立ち入りませんが、`Hanami::Mailer`クラス、関連スペック、２つのテンプレート(プレーンテキストとHTML)があるシンプルな構成です。

テンプレートは空にしておきます。メールは`'Book added!'`という件名の空メールになります。

メーラースペックを編集します。

```rb:spec/bookshelf/mailers/book_added_notification_spec.rb
RSpec.describe Mailers::BookAddedNotification, type: :mailer do
  subject { Mailers::BookAddedNotification }

  before { Hanami::Mailer.deliveries.clear }

  it 'has correct `from` email address' do
    expect(subject.from).to eq("no-reply@example.com")
  end

  it 'has correct `to` email address' do
    expect(subject.to).to eq("admin@example.com")
  end

  it 'has correct `subject`' do
    expect(subject.subject).to eq("Book added!")
  end

  it 'delivers mail' do
    expect {
      subject.deliver
    }.to change { Hanami::Mailer.deliveries.length }.by(1)
  end
end
```

メーラーも編集します。

```rb:lib/bookshelf/mailers/book_added_notification.rb
class Mailers::BookAddedNotification
  include Hanami::Mailer

  from    'no-reply@example.com'
  to      'admin@example.com'
  subject 'Book added!'
end
```

これでテストはパスするでしょう!

しかし、このメーラはどこからも呼ばれてません。`AddBook`インタラクタから呼び出す必要があります。

`AddBook`のスペックをメーラーが呼ばれていることを確認するように編集しましょう。

```rb:spec/bookshelf/interactors/add_book_spec.rb
  ...
  describe "sending email" do
    let(:mailer) { instance_double("Mailers::BookAddedNotification") }

    it "send :deliver to the mailer" do
      expect(mailer).to receive(:deliver)
      AddBook.new(mailer: mailer).call(attributes)
    end
  end
  ...
```

インタラクタに`mailer`キーワード引数を追加して、メーラーを統合しましょう。

```rb:lib/bookshelf/interactors/add_book.rb
require 'hanami/interactor'

class AddBook
  include Hanami::Interactor

  expose :book

  def initialize(repository: BookRepository.new, mailer: Mailers::BookAddedNotification.new)
    @repository = repository
    @mailer = mailer
  end

  def call(title:, author:)
    @book = @repository.create({title: title, author: author})
    @mailer.deliver
  end
end
```

これでインタラクタは本が追加されたときにメールを送信します。

## インタラクタの部品 (Interactor parts)

### インタフェース (Interface)

もうひとつ実装できる(オプションの)メソッドがあります。それは`valid?`というプライベートメソッドです。

デフォルトでは`valid?`は`true`を返します。もし`valid?`を実装して`false`を返すようにすれば`call`は決して実行されません。

<details><summary>これについての[APIドキュメント](http://www.rubydoc.info/gems/hanami-utils/Hanami/Interactor/Interface)を読むことができます。</summary>
<div>[訳註] サンプルを書いてみました。

```rb:lib/bookshelf/interactors/add_book.rb
require 'hanami/interactor'

class AddBook
  class Validation
    include Hanami::Validations

    validations do
      required(:title)  { filled? & str? }
      required(:author) { filled? & str? & size?(2..64) }
    end
  end

  include Hanami::Interactor

  expose :book

  def initialize(repository: BookRepository.new, mailer: Mailers::BookAddedNotification.new)
    @repository = repository
    @mailer = mailer
  end

  def call(title:, author:)
    @book = @repository.create({title: title, author: author})
    @mailer.deliver
  end

private

  def valid?(title:, author:)
    Validation.new(title: title, author: author).validate.success?
  end
end
```
</div></details>

### 結果オブジェクト (Result)

`Hanami::Interactor#call`の戻り値は`Hanami::Interactor::Result`のオブジェクトです。

これは公開するインタンス変数に関わらずアクセサメソッドが定義されています。

また、エラーの追跡機能も持っています。

インタラクタでは、メッセージを伴って`error`を呼び出し、エラーを追加できます。これにより自動的に結果オブジェクトが失敗します。

(`error!`メソッドというのもあります。それは`error`と同じことをし、**かつ**、流れをとめ、インタラクタがより多くのコードを実行するのを止めます。)

`.errors`を呼びですことで、結果オブジェクトのエラーにアクセスできます。

結果オブジェクトについての詳細は[APIドキュメント](http://www.rubydoc.info/gems/hanami-utils/Hanami/Interactor/Result)を参照してください。

---

# 訳者によるまとめ

コールバックはクッサい。