経緯
HanamiのControllerから責務を分けるために、ビジネスロジックをInteractorに移していきたい。
同時にパラメータ検証(バリデーション)もInteractor側でやりたいが、Controllerはパラメータ管理が拡張されていて定義書くだけでできるのに対して、Interactorは普通にやるとやぼったい処理を書くことになる。
Hanami::Actionを読み込む方法案も考えたが、expose
などで挙動の違いがあるほか、Interactorでやりたいことに閉じた拡張がしづらそうなので、ひとまず現状やりたいパラメータ検証に特化した汎用化を探ってみる。
InteractorでとりあえずValidationする場合
InteractorとValidationを組み合わせた手法をとると、例えば以下のようなController、Interactorが書ける。
参考: HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか
module Web::Controllers::Items
class Create
include Web::Action
expose :error
expose :item
def call(params)
interactor = ItemInteractor::Create.new(params).call
if interactor.successful?
redirect_to routes.item_path(interactor.item.id)
else
self.status = 422
@error = interactor.error
@item = interactor.item
end
end
end
end
require 'hanami/interactor'
require 'hanami/validations'
module ItemInteractor
class Create
include Hanami::Interactor
class Validation
include Hanami::Validations
validations do
required(:name).filled(:str?, size?: 1..200)
optional(:description) { str? & max_size?(1000) }
end
end
expose :item
def initialize(params)
@params = params
end
def call
@item = ItemRepository.new.create(
name: @params[:name],
description: @params[:description]
)
end
private
def valid?
validation = Validation.new(@params).validate
error(validation.messages) if validation.failure?
validation.success?
end
end
end
これをベースに拡張していけば、Entityごとのロジックは書いていけそうですが、#call以外はDRYじゃなくなりそうなのが気になる。
Interactorの汎用部分をmoduleに切り出してみる
Interactorでバリデーションを使う場合に共通化できそうな部分を切り出すと以下のような感じ。
だいぶすっきり。
Validation.class_eval
のところはもっとスマートなやり方あれば知りたいところ。
require '../../modules/sample/interactor'
module ItemInteractor
class Create
include Sample::Interactor
Validation.class_eval do
validations do
required(:name).filled(:str?, size?: 1..200)
optional(:description) { str? & max_size?(1000) }
end
end
expose :item
def call
@item = ItemRepository.new.create(
name: @params[:name],
description: @params[:description]
)
end
end
end
require 'hanami/interactor'
require 'hanami/validations'
module Sample::Interactor
class Validation
include Hanami::Validations
end
def self.included(klass)
klass.class_eval do
include Hanami::Interactor
def initialize(params)
@params = params
end
end
end
private
def valid?
validation = Validation.new(@params).validate
error(validation.messages) if validation.failure?
validation.success?
end
end
さらにValidationまわりを切り出してみる
Interactorで毎回Validationするとも限らないので、Validationまわりを切り出して着脱可能にしておく。
require '../../modules/sample/interactor'
require '../../modules/sample/action/validatable'
module ItemInteractor
class Create
include Sample::Interactor
include Sample::Action::Validatable
Validation.class_eval do
validations do
required(:name).filled(:str?, size?: 1..200)
optional(:description) { str? & max_size?(1000) }
end
end
expose :item
def call
@item = ItemRepository.new.create(
name: @params[:name],
description: @params[:description]
)
end
end
end
require 'hanami/interactor'
module Sample::Interactor
def self.included(klass)
klass.class_eval do
include Hanami::Interactor
def initialize(params)
@params = params
end
end
end
end
require 'hanami/validations'
module Sample
module Action
module Validatable
class Validation
include Hanami::Validations
end
private
def valid?
validation = Validation.new(@params).validate
error(validation.messages) if validation.failure?
validation.success?
end
end
end
end
I18n対応なども組み込みやすくなりそう。