Edited at

続 Rails でユーティリティ処理を書く

More than 1 year has passed since last update.


経緯

自分が投稿した記事の中では以前書いた Rails のユーティリティに関する記事がよくストックされるのですが、2年経った今あらためて見ると「内容によってはもうちょっと適切な方法があるんじゃないのかな」と思い始めて来たので、新たに書き起こしました。

「自分はこう書いている」「それは Rails 的に適切じゃないよ」という点がありましたら、コメントにてツッコミを頂ければ幸いです。


どんなユーティリティを書くのか

ひと口にユーティリティと言っても、その捉え方は経験年数の差や言語やフレームワークによって実際のところまちまちだったりします。前の記事ではそういった事を気にせず「とにかく Rails 全体で使いたいユーティリティをどこにどうやって書くのか」を主題としていましたが、Rails 4 から 5 をメインで使う事が多くなったのと、自分自身の習熟が進んできたこともあるため、改めて考察してみたいと思います。


用途別


ビューでもモデルでもコントローラでも共有したい

この場合、基本的には以前の記事でほぼ変わらないかと思っています。

ただしこれはあくまで「ユーティリティという規模の範囲内で済む(他のインスタンスに影響を及ぼすことなく、関数内だけで処理が完結するような)ロジック」であることが前提になります。

詳しくは最後の「まとめ」に記載致しました。要点だけ確認したい方はそちらを参照下さい。


ドメインロジックを共有したい

いわゆるドメインロジック(もはやユーティリティではないのですが)に相当するものを書きたい場合、基本的にはモデルに書く形になります。

ここ本来であればもうちょっと掘り下げて書きたいのですが、それだけで1つの記事になる勢いなので、省略させて頂きます(いずれ別記事で書きたいと思います)。

ドメインロジックの話でよく挙がる方法としては service クラスを使うというパターンもあります。

Rails4.2 から導入された ActiveJob を検討するのもひとつの方法です。往々にしてドメインロジックはバッチ的なものになりがちで、非同期処理でも問題ない(≒馬鹿正直にタスクが終わるのを待っている必要がない)という場合が多々あったりします。ドメインロジックとまではいかない規模だけど・・・という場合でも、検討する価値はあるかもしれません。

参考: http://ruby-rails.hatenadiary.com/entry/20150304/1425396671


モデル内だけで共有したい

モデル内で使える共通のメソッドを書く際、私が最初に思ったのは「どうしてコントローラには ApplicationController があるのにモデルに ApplicationModel が無いのはなぜ?」という事なのですが、いちおう ApplicationModel を作ってしまう方法もあるにはあります。 こちらの件 Rails5 からは ApplicationRecord という基底クラスがデフォルトで存在するようになりました。

ただしモデル内だけで共有したいメソッドを ApplicationRecord に書いていくと結局 ApplicationRecord がドンドン肥大化することになって見通しが悪くなるため、適宜 ActiveSupport::Concern を使うのが良い方法ではないかと思います。


コントローラ内だけで共有したい

コントローラの場合真っ先に ApplicationController が思い浮かびますが、モデル同様 ApplicationController の肥大化が懸念されるので、モデル同様コントローラでも app/controllers/concerns を使い、見通しを良くすることが重要と思います。

ただ、コントローラ内だけで共有したいメソッドとは言いつつも、実際にはモデルを参照したりする例も少なくないため「コントローラ内だけで使うから」という理由でモデルが絡むメソッドを app/controllers/concerns に 書いていったりするとかえって見通しが悪くなる場合もあります。内容に応じてモデル側に書くといったことを検討した方が良い場合も少なくありません。

その場合は app/models/concerns に書くほか、モデルの階層化などを検討するのも良い方法です。


ビュー内だけで共有したい

これは素直に app/helpers/application_helper.rb を使いましょう。


ビューとコントローラで共有したい

著名な方法としては helper_method が挙げられます。これもコントローラ内の例と同様に app/controllers/concerns を併用していく方が良いでしょう。


モデルとビューで共有したい

私自身が「MVC フレームワークではコントローラを介さずにビューからモデルを参照することは無い」と学んだというのもあるのですが、モデルとビューでメソッドを共有したいと考えた場合、その方法は果たして適切なのかどうかを検討した方が良いと考えます。

MVC フレームワークの解説図によってはビューからモデルを「参照のためだけに」利用しているようなケースがいくつかあるため、個人個人での解釈が異なるような気もしますが、基本的には「モデルとビューで共有する(ビューからモデルを参照したり、モデルからビューに作用するような)」メソッドは無いのではないかと考えています。

ありがちな例として、


app/views/users/index.html.haml

- User.order(:sort_number).each do |user|

p= user.name

上記のようなコードをビューに書いてしまうといったことがありますが、これは以下のように書き換えられます。


app/models/user.rb

class User < ActiveRecord::Base

scope :sorted, -> { order(:sort_number) }
end


app/users_controller.rb

class UsersController < ApplicationController

def index
@users = User.sorted
end
end


app/views/users/index.html.haml

- @users.each do |user|

p= user.name

これらは rails g scaffold コマンドで出力されるコントローラを見ていれば把握できます。

ただし、これは実際のところ


app/views/users/index.html.haml

- User.sorted.each do |user|

p= user.name

とも書けてしまい、また動いてもしまうため「ビューからモデルを参照することもあるじゃん」と言われてしまうきらいもあります。このへん言及しているサイトがあればコメント欄で教えて下さい。


まとめ

最初の「ビューでもモデルでもコントローラでも共有したい」という部分ですが、これは言い方を変えれば「どこで使われるか判らないからどこでも使えるようにしておきたい」という考え方にもなります。

繰り返しになりますが「ユーティリティという規模の範囲内で済む(他のインスタンスに影響を及ぼすことなく、関数内だけで処理が完結するような)ロジック」であることが前提だとすれば、lib/utils などのフォルダを利用することはある程度適切ではないかと考えます。

ただ「使われたくない場所で使われると困る」メソッドについてはきちんと責務を分ける必要があります。その場合はユーティリティの本来の意味でもある公益に従い、ユーティリティとしてではなく、特定のクラスの一連のロジックを内包するメソッドとして、適材適所に書いていくという事が必要になってきます。

そういう意味では service クラスの採用も汎用性が高すぎる故に(特に大規模開発、多人数開発では)破綻したりしがちになるので、事前に作業者間での共有が大事であることは間違いないでしょう。

そのように考えていくと、実際ユーティリティとして書かなくてはいけないメソッドって何かあるんだろうか、という気持ちにもなったりするのですが、上記の考慮を実施してもなお「ユーティリティとして書かなくてはいけない」ものである場合は、その段階で適切な見通しを持ったものとして書かれると思うので、結局きちんと書いていけば「ビューでもモデルでもコントローラでも共有したい」ユーティリティ的なものはかなり少なくなるんじゃないの、と思ったりするに至りました。