RubyのHanamiでInteractorを使う

シンプルでスケールしやすく、軽量で早い、そしてDDD(Domain Driven Development)思想を取り入れているフレームワークのHanamiでInteractorを使う方法についてのメモです。

■HanamiのGetting Started

HanamiのGetting StartedはInteractorを使うところがなく、Controllerにビジネスロジックを書いていますが、単一責任の考えより、HanamiではService層にあたるInteractorにロジックを切り出して書くことを推奨しています。

Hanami v1.1 Getting Started
※他の方が日本語で説明されているページもありますが本筋と関係ないので省略

■HanamiのInteractor

Getting Startedでは、Interactorの実装まで書かれておらず、個別のページで説明されています。これはまだ日本語版はないようです。内容はGetting Startedの続きのような形で書かれています。

Hanami v1.1 Architecture – Interactors

■Interactorの実装

上記をそのまま実装するだけですが、Controller側の実装は書かれていなかったため(テストコードはありましたが)、以下にController含めた、ポイントになる箇所のソースコードを記載します。

[lib/bookshelf/interactors/add_book.rb]
———————————————————————————-
require ‘hanami/interactor’

class AddBook
  include Hanami::Interactor

  expose :book

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

  def call(title:, author:)
    @book = @repository.create({title: title, author: author})
    @mailer.deliver
  end
end
———————————————————————————-
↑ Dependency Injection(依存性の注入)の考えより、initializeとcallで処理を分けています。これを分けずにcallに全部書くことも可能です。その場合は、initializeにparamsを引数にしてexposeし(@params = params)、callは何も引数を取らずリポジトリもcall内で取得、という書き方になります。

[apps/web/controllers/books/create.rb]
———————————————————————————-
module Web::Controllers::Books
  class Create
    include Web::Action

    expose :book

    params do
      required(:book).schema do
        required(:title).filled(:str?)
        required(:author).filled(:str?)
      end
    end

    def call(params)
      if params.valid?
        @book = AddBook.new.call(params[:book])

        redirect_to “/books”
      else
        self.status = 422
      end
    end
  end
end
———————————————————————————

↑ 念のため補足しますと、HanamiのInteractor説明ページのテストコード(Specのコード)をControllerにほとんど転記しているだけです。

以下引用:Hanami Interactor – Creating Book

Edit 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

■エラーメッセージの例

・ArgumentError: unknown keyword: book
→ newにparamsを入れていませんか?(例)AddBook.new(params).call
newではなくcallに引数を入れます。

・ArgumentError: unknown keyword: title, author
→ newにparams[:book]を入れていませんか?(例)AddBook.new(params[:book]).call
newではなくcallに引数を入れます。

■参考になるサイト

以下はInteractorのcallではなくinitializeにparamsを引数にした書き方になっています。

Rubyist Magazine – HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか
→ Interactorの説明が非常に詳しく、分かりやすく書かれています

The Pragmatic Hanami by kbaba1001
→ 上記と同じ方が書かれていますが、こちらも分かりやすいです

ossboard – lib/interactorossboard – apps/controller
→ Hanamiで作っているossboardというプログラムがGitHubに載っているのでInteractorの実装方法を見ることができます

■その他

Hanami Bookshelf (example application) 
→ ちなみにHanamiのGetting StartedのソースはGitHubにあります

■(オマケ)initializeにparamsを引数にする場合のInteractorの書き方

[lib/bookshelf/interactors/add_book.rb]
———————————————————————————-
require ‘hanami/interactor’

class AddBook
  include Hanami::Interactor

  expose :book_attributes

  def initialize(params)
    @params = params
  end

  def call
    @book_attributes = BookRepository.new.create(@params[:book])
  end
end
———————————————————————————- 

[apps/web/controllers/books/create.rb]
———————————————————————————-
module Web::Controllers::Books
  class Create
    include Web::Action

    expose :book

    params do
      required(:book).schema do
        required(:title).filled(:str?)
        required(:author).filled(:str?)
      end
    end

    def call(params)
      if params.valid?
        #@book = BookRepository.new.create(params[:book]) #コメントアウト
        result = AddBook.new(params).call
        @book = result.book_attributes #.book_attributesを忘れずに

        redirect_to routes.books_path
      else
        self.status = 422
      end
    end
  end
end
———————————————————————————-