Rubyの返り値って
rubyは最後に評価された値を返り値にしていますが、静的型付け言語ではほとんどの場合返り値を指定してメソッドを作ります。
public int sum(int a, int b){ return a + b }
public fun sum(a: Int, b: Int): Int = a + b
rubyの場合わざわざ返り値の型を宣言しなくていいので、そこを意識するとRailsでいうところの
ControllerからViewまでのエラー表現がシンプルになりそうということのメモです.
def sum(a, b)
...
end
RailsにおけるControllerの処理系のパターン
更新系の処理に絞っています。
また、@model
が保存できるかという条件分岐を分けるのがsave
というメソッドが true or false
になるという以下の処理系がScaffoldでも採用されています。
- 綺麗に済む一般的なcrudのパターン
def create
@model = Model.new(create_params)
if @model.save
redirect_to ...........
else
render :new
end
end
### View
- @model.errors.full_messages.each do |msg|
%p.error= msg
なんらか複雑な、トランザクションを含んだ処理型では例外を発生させて、発生しうる例外を
捕捉するようにしています。
- 例外を発生させて捕捉するパターン
def create
@model = Model.new(create_params)
Model.transaction do
@model.save!
@model.store_image!
redirect_to ...........
end
rescue 例外A、B、C、D
render :new
end
### View
- @model.errors.full_messages.each do |msg|
%p= msg
エラーメッセージなのですが、問題があった操作に対しては@model.errors
に含まれるエラーメッセージを表示するというのが一般的でしょう。
ただ、捕捉すべき例外は削除されたり、増えたりする可能性があります。
最近ではこのあたりです 。https://qiita.com/ryohashimoto/items/a8ba7bb8d6f340894188#activerecordnotnullviolation
必要が無いなら分岐を作る場合はなるべくtrue or falseで分岐を作ってあげるのが良さそうです。
Viewには@modelのerrorsを渡せば正規化されたエラーの原因が
分かるのですが、store_image!
で発生し得る例外を幾つか考えてみなければいけません。
なので、@modelのerrorsに加えて、想定し得ない例外をユーザーに提示する場合このようになります。
def create
@model = Model.new(create_params)
Model.transaction do
@model.save!
@model.store_image!
redirect_to ...........
end
rescue 例外A、B、C、D => e
flash[:alart].now = e.message
render :new
end
### View
- @model.errors.full_messages.each do |msg|
%p= msg
= flash[:alert]
途端に怪しくなりました。
flashと@modelのエラーのそれぞれをviewでユーザーに見せるような形になっています。
加えて、素の例外のメッセージは大方英語で、ユーザーが理解できないような形になります。(例えなので普通そんなメッセージはユーザーに見せませんが、、)
単純に全てをrescueするのは把握できない理由も含めて失敗したと捉えるもので、
例外の原因を探るために一手間入れないと行けなくなります。
例えばユーザー向けのメッセージは単一にして、開発者には例外通知のメールを送信するなどです。
rescue => e
ExceptionNotifier.notify_exception(e)
flash[:alart].now = "何らかのエラーが発生しました"
render :new
ただ、こういったことを続けていても、場当たり的にこのような処理を色々な箇所に追加することになってしまうので
基本的には捕捉できる例外は捕捉して、捕捉し得ない例外にだけ自動で例外通知を送るように
するのが自明だと考えています。
つまり捕捉できる例外を開発者が調べて都度追加する必要があるのではないかということを考えると、
なるべくControllerの分岐処理はtrue or false
で判断できる形がいいということでした。
それでも例外処理が必要になる場合
ひとつのアクションで複数の事をしようとした場合に例外が発生しうるケースはほとんどのシステムで
そのようなケースはあります。
例外のメッセージが@modelのエラーに含まれる形になっていないため、
Controllerから具体的な処理を別のクラスにしてエラーをまとめたいと思いました。
Controllerに@errorsをupdaterの返り値で渡して true or falseの形にまとめるようにしました。
作成したPOROからは、どのようなケースでも最後に@errorsを返すようにしています。
エラーがなければ、Viewに表示されるエラーメッセージは無く、
逆は先述の@modelのerrorsに加えて、想定し得ない例外をユーザーに提示する
形です。
結果、Controllerの@errorがupdaterに収められているかで条件分岐をif式で行う事ができ、
Viewに表示するエラーも@errorsにまとめることができました。
# Controller
def create
@model = Model.new(create_params)
updater = ModelUpdater.new(@model)
if (@errors = updater.run).present?
redirect_to ...........
else
render :new
end
end
# PORO
class ModelUpdater
def initialize(model)
@model = model
@errors = []
end
def run
Teacher.transaction do
@model.save!
@model.store_image!
end
@errors
rescue 例外A、B、C、D => e
@errors.push e.message
@model.errors.full_messages | @errors # @errorsの配列を返す
end
end
# Viewのエラー表示
- @errors.each do |err|
%p err
ただ、このケースだと@errorsなるインスタンス変数を余計にControllerに持たせることになるため
テストを少し多めに書かなければなりそうです。
シンプルに以下のような形でエラーを表出させることができないかを考えて見たのですが、
- @model.errors.full_messages.each do |msg|
%p= msg
例外のメッセージ含めそのままerrors.add(:base, :some_error)等としてしまいActiveModel|Recordのエラーとして
しまうのが個人的には良さそうな方法でした。
# PORO
class ModelUpdater
def initialize(model)
@model = model
end
def run
Teacher.transaction do
@model.save!
@model.store_image!
end
true #処理を駆け抜けたらtrueを返す
rescue 例外A、B、C、D => e
@model.errors.add(:base, e.class.to_s.underscore)
false # 失敗したらfalseを返す
end
end
# Locale
ja:
activerecord:
errors:
models:
model:
attributes:
base:
some_error_a: "some_errorA occurred"
some_error_b: "some_errorB occurred"
some_error_c: "some_errorC occurred"
some_error_d: "some_errorD occurred"
# View
- @model.errors.full_messages.each do |msg|
%p= msg
これによりControllerは以下のようにtrue or falseによる分岐処理に専念させることができ、
Viewにおけるエラー表示も正規化することができました。
# Controller
def create
@model = Model.new(create_params)
updater = ModelUpdater.new(@model)
if updater.run
redirect_to ...........
else
render :new
end
end
まとめ
使う側、使われる側における条件分岐をtrue or false
に仕向けること、
使う側が気にするべき状態を使われる側から減らしてやることで処理をシンプルにできました。
言いたいこと自体は、使われる側が使う側を楽にハンドリングさせるために返り値を正規化してあげれば楽になれる箇所があるということだったのですが、エラーメッセージのハンドリングに終始してしまいました。。
スコープが多岐に渡る変数が氾濫していると、どこで何のために使われるものなのかが
見づらくなりますし、他のアクションではまた別の実装方法で@errors
をもたせたりなど、
基本的な事なのですが関連するプロジェクトで見受けられたのでまとめました。
最終的な例では主体となるActiveRecord|ActiveModelインスタンス(@model
)を継承したクラスインスタンスを想定してerrors.add
としていますが、
これがない場合以下のようなカスタムエラーオブジェクトを返却してもいいかもしれません。
エラーメッセージの表現に関してActiveRecord|ActiveModelの恩恵に授かるのが今のところ楽な感触でした。
class SomeErrorModel
include ActiveModel::Model
delegate :full_messages, to: :errors
ERRORS = [:some_error_a, :some_error_b, :some_error_c, :some_error_d].freeze
def add(code)
raise ArgumentError unless code.in?(ERRORS)
errors.add(:base, code)
end
end
ピュアなRubyなら以下のような形で、色々な箇所から共用できるようなエラーを表現できるインスタンスをクラス化しておけば汎用的に使えそうです(apiとかバッチ処理とか)
class SomeErrorModel
attr_reader :errors
ERRORS = {
ok: {code: :E_A, msg: "error a occured"},
invalid: {code: :E_B, msg: "error b occured"},
taken: {code: :E_C, msg: "error c occured"}
}.freeze
def add(code)
raise ArgumentError unless code.in?(ERRORS.keys)
erros << ERRORS[code]
end
def full_messages
erros.map{|h| h[:msg]}.uniq
end
end