控制器和交互器错误处理的最佳实践
Best practice of error handling on controller and interactor
# users_show_controller.rb
class Controllers::Users::Show
include Hanami::Action
params do
required(:id).filled(:str?)
end
def call(params)
result = users_show_interactor(id: params[:id])
halt 404 if result.failure?
@user = result.user
end
end
# users_show_interactor.rb
class Users::Show::Interactor
include Hanami::Interactor
expose :user
def call(:id)
@user = UserRepository.find_by(:id)
end
end
我有一个控制器和一个像上面那样的交互器。
我正在考虑在控制器上区分 ClientError 和 ServerError 的更好方法。
我想如果我能像下面这样处理错误就好了。
handle_exeption StandardError => :some_handler
但是,hanami-interactor 包装了自身内部引发的错误,因此,控制器通过 interactor 的结果对象接收错误。
我不认为在控制器上重新引发错误是个好方法。
result = some_interactor.call(params)
raise result.error if result.failure
如何实现这样的错误处理程序?
我知道 if
语句很容易增加,所以这种方式并不聪明。
def call(params)
result = some_interactor.call(params)
handle_error(result.error) if result.faulure?
end
private
def handle_error(error)
return handle_client_error(error) if error.is_a?(ClientError)
return server_error(error) if error.is_a?(ServerError)
end
Hanami 方式是在每个请求处理程序之前验证输入参数。因此,必须始终在操作逻辑之前识别 ClientError。
halt 400 unless params.valid? #halt ClientError
#your code
result = users_show_interactor(id: params[:id])
halt 422 if result.failure? #ServerError
halt 404 unless result.user
@user = result.user
实际上不是花见导向的方式,但请看一下dry-monads with do notation。基本思路是,你可以按照下面的方式编写类似interactor的处理代码
def some_action
value_1 = yield step_1
value_2 = yield step_2(value_1)
return yield(step_3(value_2))
end
def step_1
if condition
Success(some_value)
else
Failure(:some_error_code)
end
end
def step_2
if condition
Success(some_value)
else
Failure(:some_error_code_2)
end
end
然后在控制器中,您可以使用 dry-matcher:
来匹配故障
matcher.(result) do |m|
m.success do |v|
# ok
end
m.failure :some_error_code do |v|
halt 400
end
m.failure :some_error_2 do |v|
halt 422
end
end
匹配器可以在所有控制器的prepend
代码中定义,因此很容易消除代码重复。
我通常会在交互器中引发范围错误,然后控制器只需要挽救交互器引发的错误和return适当的状态响应。
互动者:
module Users
class Delete
include Tnt::Interactor
class UserNotFoundError < ApplicationError; end
def call(report_id)
deleted = UserRepository.new.delete(report_id)
fail_with!(UserNotFoundError) unless deleted
end
end
end
控制器:
module Api::Controllers::Users
class Destroy
include Api::Action
include Api::Halt
params do
required(:id).filled(:str?, :uuid?)
end
def call(params)
halt 422 unless params.valid?
Users::Delete.new.call(params[:id])
rescue Users::Delete::UserNotFoundError => e
halt_with_status_and_error(404, e)
end
end
end
fail_with!
和 halt_with_status_and_error
分别是我的交互器和控制器常用的辅助方法。
# module Api::Halt
def halt_with_status_and_error(status, error = ApplicationError)
halt status, JSON.generate(
errors: [{ key: error.key, message: error.message }],
)
end
# module Tnt::Interactor
def fail_with!(exception)
@__result.fail!
raise exception
end
# users_show_controller.rb
class Controllers::Users::Show
include Hanami::Action
params do
required(:id).filled(:str?)
end
def call(params)
result = users_show_interactor(id: params[:id])
halt 404 if result.failure?
@user = result.user
end
end
# users_show_interactor.rb
class Users::Show::Interactor
include Hanami::Interactor
expose :user
def call(:id)
@user = UserRepository.find_by(:id)
end
end
我有一个控制器和一个像上面那样的交互器。 我正在考虑在控制器上区分 ClientError 和 ServerError 的更好方法。
我想如果我能像下面这样处理错误就好了。
handle_exeption StandardError => :some_handler
但是,hanami-interactor 包装了自身内部引发的错误,因此,控制器通过 interactor 的结果对象接收错误。
我不认为在控制器上重新引发错误是个好方法。
result = some_interactor.call(params)
raise result.error if result.failure
如何实现这样的错误处理程序?
我知道 if
语句很容易增加,所以这种方式并不聪明。
def call(params)
result = some_interactor.call(params)
handle_error(result.error) if result.faulure?
end
private
def handle_error(error)
return handle_client_error(error) if error.is_a?(ClientError)
return server_error(error) if error.is_a?(ServerError)
end
Hanami 方式是在每个请求处理程序之前验证输入参数。因此,必须始终在操作逻辑之前识别 ClientError。
halt 400 unless params.valid? #halt ClientError
#your code
result = users_show_interactor(id: params[:id])
halt 422 if result.failure? #ServerError
halt 404 unless result.user
@user = result.user
实际上不是花见导向的方式,但请看一下dry-monads with do notation。基本思路是,你可以按照下面的方式编写类似interactor的处理代码
def some_action
value_1 = yield step_1
value_2 = yield step_2(value_1)
return yield(step_3(value_2))
end
def step_1
if condition
Success(some_value)
else
Failure(:some_error_code)
end
end
def step_2
if condition
Success(some_value)
else
Failure(:some_error_code_2)
end
end
然后在控制器中,您可以使用 dry-matcher:
来匹配故障matcher.(result) do |m|
m.success do |v|
# ok
end
m.failure :some_error_code do |v|
halt 400
end
m.failure :some_error_2 do |v|
halt 422
end
end
匹配器可以在所有控制器的prepend
代码中定义,因此很容易消除代码重复。
我通常会在交互器中引发范围错误,然后控制器只需要挽救交互器引发的错误和return适当的状态响应。
互动者:
module Users
class Delete
include Tnt::Interactor
class UserNotFoundError < ApplicationError; end
def call(report_id)
deleted = UserRepository.new.delete(report_id)
fail_with!(UserNotFoundError) unless deleted
end
end
end
控制器:
module Api::Controllers::Users
class Destroy
include Api::Action
include Api::Halt
params do
required(:id).filled(:str?, :uuid?)
end
def call(params)
halt 422 unless params.valid?
Users::Delete.new.call(params[:id])
rescue Users::Delete::UserNotFoundError => e
halt_with_status_and_error(404, e)
end
end
end
fail_with!
和 halt_with_status_and_error
分别是我的交互器和控制器常用的辅助方法。
# module Api::Halt
def halt_with_status_and_error(status, error = ApplicationError)
halt status, JSON.generate(
errors: [{ key: error.key, message: error.message }],
)
end
# module Tnt::Interactor
def fail_with!(exception)
@__result.fail!
raise exception
end