Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

FormObjectにおける`#to_model`について

こんにちはgengenです。
今回はRailsにおけるデザインパターンであるFormObjectについての記事になります。

FormObjectは、バリデーションや特定のユースケース向けの処理をモデルから分離する手法です。
ネット上のFormObjectについての記事ではFormObjectクラスに、操作対象になるActiveRecordのインスタンスを返すように#to_modelを実装する例をよく見ます。
このやり方は結構ピーキーで拡張性とかを考えると避けた方が無難ではないか?ということについて書いていきたいと思います。

なぜ#to_model実装するのか

FormObjectは色々な実現方法があると思いますが、Rails: Form Objectと#to_modelを使ってバリデーションをモデルから分離する(翻訳)のようにActiveModel::ModelをincludeしたFormObjectクラスに#to_modelをオーバーライドする場合について考えます。

このような形で#to_modelを実装すると、送信先を類推するのに使われる.model_nameや、labelのやバリデーションエラーメッセージの出力に使われる.human_attribute_nameがActiveRecordのクラスに対して呼び出されるようになり、特に設定したりしなくてもscaffoldしたcontrollerやviewはほぼそのまま使うことができます。

何が問題なのか

単にバリデーションを分離するだけであればこのようにしても困ることはないと思います。
しかし、

  • ActiveRecordとは属性名を変えたい
  • 一つの属性を二つに分けて入力させたい
  • ActiveRecordと無関係の入力を処理したい
  • 他のActiveRecordモデルを同じフォームで処理したい

上記のようにActiveRecordの属性とフォームにズレが出てきた場合、そのままでの対応が難しくなります。
大元のActiveRecordのtranslation設定などをいじって無理やり対応できなくもないですが、
分離したかったはずなのに、ActiveRecordの実装や設定が特定のユースケースに引っ張られるのは本末転倒ではないでしょうか。

https://apidock.com/rails/ActiveModel/Conversion/to_model

If your model does not act like an Active Model object, then you should define :to_model yourself returning a proxy object that wraps your object with Active Model compliant methods.
apidock.comより

ドキュメントによるとActiveModelではないオブジェクトをActiveModelとして扱いたい時は、ActiveModel互換のプロキシオブジェクトを#to_modelに挿し込もうということなので、今回の扱いは正攻法ではないと思います。

どうしたらいいのか

困ってから#to_modelを剥がすとなると影響範囲が大きくなりますし、初めからそんなことせず愚直に送信先指定したり属性名のtranslationを設定したりした方がいいのではないでしょうか。
そもそも送信先については、
ActiveRecordからユースケース固有のバリデーションを分離する必要がある≒ActiveRecordがRESTfulルーティングと一対一で対応するような単純な設計ではない
という時点でパスヘルパーで書いた方がわかりやすくて親切だと個人的には思います。

終わりに

Rails wayからはみ出す時はきっちりはみ出した方が予後が良いんじゃないかな〜という気がしました。
ここまで読んでいただき、ありがとうございました。

関連記事

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。