QA@IT

rails で params に対して複雑な処理をするときのベストプラクティスは?

21716 PV

フォームから値を受け取るときは、 attr_accessible や StrongParameter で不要なフィールドを弾くくらいで十分ですが、 API などを設計していると、もっと複雑な処理をしたい場合があります。

たとえば

  • 値を設定する先の model に該当するフィールドがない
  • model に渡すまえに、 params についてバリデーションが必要
  • params が複数の値セットを受け付けることができて、それぞれを加工してから model の attribute に設定しなければならない

といった状況です。

このようなケースでは、params を解析して、 model の attribute に変換する複雑な処理が必要になります。

この処理はリクエストに関することなので controller でやりたいですが、そうすると controller が fat になります。

model に params をそのまま渡してしまうこともできますが、 model はデータの整合性と永続化が responsibility なので、入力を扱うのは SRP に反します。
また、 create と update があったらそれぞれにメソッドを立てる必要があり、コードが複雑化します。

テスタビリティの観点からは、単一の変換ロジックの形で用意できれば、それを unit test の対象にできるのでテストの効率がよくなりそうです。

以上を考慮し、 MODEL_NAME::ParamsConverter のようなクラスを作ってしまおうかと思っているのですが、どうでしょうか。

params から attributes への変換処理はどのように書くと綺麗に管理できるでしょうか。
みなさんの御知恵をお授けください。

回答

既に一段落付いてますが、回答させてください。

リファクタリング:Rubyエディションに書かれている、引数オブジェクトの導入という形を取った事があります。
検索の条件指定のパラメーターに、条件があったり無かったり、初期値を決めたり、単位を調整したりという目的があって、パラメーターの要素が大量になった時等です。

引数オブジェクトのコンストラクタで、nilの場合などの初期値や単位調整を行い、
モデル側にその引数オブジェクトを受けるメソッドを用意してメインの処理はそっちで書きます。

置き場所については、concernとして名前が付けられる関心事に関わるパラメーターなら、concernの名前空間に置きますが、あるモデル専門だとモデルの名前空間の中に置くのが妥当かなと思います。

基本的にeielさんの回答とあんまり変わりませんが、ActiveModelを利用する程じゃなくても、もっとカジュアルにクラス作って良いんじゃないかと思います。
ネーミングについては、本当に加工しか行わないで、パラメーターをまとめるだけなら、深く考えずに、---Parameterというクラス名で良い気がします。
ビューでフォームヘルパーを活用したい時や、細かなバリデーションが必要な時は、eielさんの回答にあるようなActiveModelをincludeしたForm Objectを利用したいですね。

例: こんな感じのシンプルなオブジェクトでパラメーターをラップしてしまう。

class SearchParameter
  attr_reader :area_id, :page, :per_page, :keyword, :latlng, :distance

  # キーワード引数を使っても良いかも
  def initialize(attrs = {})
    attrs.assert_valid_keys(:area_id, :page, :per_page, :keyword, :latlng, :distance)

    @area_id  = attrs[:area_id].presence
    @page     = attrs[:page] ? attrs[:page].to_i : 1
    @per_page = attrs[:per_page] ? attrs[:per_page].to_i : PER_PAGE
    @keyword  = attrs[:keyword].presence
    @latlng   = attrs[:latlng]
    @distance = attrs[:distance] || DEFAULT_DISTANCE
    freeze
  end
end

更に加工が必要な場合は、ここから変換メソッドを生やす形。

編集 履歴 (0)
  • おお、まさにこれですね!
    converter にするとparamsの値セットを受け取ってattributesの値セットを返すという働きになり、要素の変更のコストが高いですが、parameter は欲しいメソッドをどんどん足せるので綺麗に OO な関係を作れそうです。
    大変参考になりました。ありがとうございます。
    -

MODEL_NAME::ParamsConverter のようなクラスを作ってしまおうかと思っているのですが、どうでしょうか。

私もほぼ同様な見解です。

小規模であれば model にそのまま渡してしまうのがてっとり早いと思います。
見通しが悪いようであれば、paramをそのまま受けとる別の model を作成しています。

この model はActiveRecord::Baseは継承せずに作成して、永続化ができるモデルを利用するか、生成するようにしています。

考え方

上記の答えの考え方を書きます。

Rails は フォーム と テーブルが が対応する時に楽ができるフレームワークです。
paramsを考慮してデータの流れを考えると

form -> params -> model -> データベース -> model -> view -> form

となります。

form と データベースがうまく一致しないのはどうしようもないので、どこで違いを吸収するかということになります。
なるべくviewやcontrollerで処理をしたくないので、model で対応させることを考えます。しかし、そうすると model が膨らみすぎるので、formに対応する model を用意することで対処します。

form -> params -> modelA -> modelB -> データベース -> modelB -> modelA -> view -> form

modelA から model B は大きな視点でみれば model なので問題ないですし、modelA は ActiveModel を使って実装していくことで view側 は レール に乗ったままプログラミングすることができますし、永続化を行うBはシンプルなままを維持できます。

API の場合は 端の form がなくなるだけなので同じ考え方ができそうです。
ActiveModelも使う必要はないのではないかと思います。

編集 履歴 (0)

コメントにしようと思ったのですが、ぜんぜん足りないので回答の形でまとめます。

eiel さんの意見はとても参考になりました。
tkawa さんに挙げていただいた実装も良さそうです。

実装方法の面ではたぶん同じことになるのですが、これを model と呼ぶのは抵抗があります。

なぜなら、model は永続化されるかされないかに係らず、エンティティであるべきです。
いわば「一級の市民」としてビジネスロジックの実現にたずさわります。

しかし、いま提案していただいたクラスの責務は与えられた params を attributes (あるいは AR の model)に変換することです。
builder pattern のような性質を持ちます。これはユーティリティであり、通り過ぎる場所にすぎません。

最大の問題はそのようなモデルにはどういった名前を付けるかという点です。
モデルと見て、エンティティのような名前のつけかたをしようと思うと、いい例が思いうかびませんでした。
(たとえば内側の modelB が質問への回答を表現する Answer だったら modelA はどうなるんでしょうか)。

もしかしたら、中間のクラスに与える重みの考え方に齟齬があるのかもしれません。
私は変換することだけを考えていたのですが、ビジネスロジックの中で意味のある単位になる場合は model というのが正解でしょう。
(実際私たちのアプリケーションにも、そのような入力を処理し、複数のクラスの中で働くけれど、永続化されない model があります)

また、ActiveModel にある Validation や I18n の機能を使うことで効率化できれば、名前はきにせず使ったほうがよさそうですね。

mori_dev さんの before_filter として刺せるようなメソッドを定義し、params を受けて @attributes に変換するというやり方は、 params[:id] からオブジェクトにする時などに一般的ですね。
Ruby っぽい方法だと思います。
controller の action からは分離できますし、長くなってくれば別のファイルにメソッドを移動することもできます(「ゴミを引き出しにつっこんでいる」状況にならないよう注意が必要ですが)。
小規模の場合にはコストがすくないのでうまく行きそうです。

ということで、次のような感じでしょうか。

  • 小規模のときは controller の filter。StrongParameter などが使える
  • 変換のみの時は変換クラスを作る。 AM が使えれば使う
  • エンティティにできる場合は AM::Model などで一級の model にする

下の2つは実装としてはよく似たものになりそうです。
前者が変換後の attributes を一括で出力する機能を持ち、後者は他の model とのメッセージのやり取りするくらいの差でしょうか。

みなさん大変参考になりました、ありがとうございます。

編集 履歴 (0)

eielさんに同感です。
バリデーションもこんな感じで組み込むことができますね。

class ModelA
  include ActiveModel::Validations
  validates :email, presence: true
  def initialize(params = {})
    @params = params
  end
  def read_attribute_for_validation(key)
    @params[key]
  end
  def to_modelb_params
    # convert
  end
end

(コメントに書こうと思ったのですが字数オーバー…)

編集 履歴 (0)

僕も model に params をそのまま渡して、期待通り動作されられたものの微妙だったなーと後悔したことがあります。

MODEL_NAME::ParamsConverter のような

質問文を読んで、パラメータチェック、変換用の before_filter/before_action を担当する ActiveSupport::Concern のモジュールをつくる方がよかったかもと思いました。

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。