シンプルでスケールしやすく、軽量で早い、そしてDDD(Domain Driven Development)思想を取り入れているフレームワークのHanamiでInteractorを使う方法についてのメモです。
Agenda
■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
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
→ 上記と同じ方が書かれていますが、こちらも分かりやすいです
・GitHub 256hax/tanebox-on-Hanami
Interactor
→ 自分が作ったプログラムもInteractorを使っていますので、GitHubのURLを載せておきます
・GitHub ossboard - lib/interactor、ossboard - 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