Ruby on Rails: ビューヘルパーのメソッド名が重複したので、私なりのヘルパー分類方法を考察してみた。

前提

この記事では Ruby on Rails 3.2.x 系について話をする。

はじめに

Ruby on Rails ではコントローラーごとにビューヘルパーが作成される。
例えば、次のようなジェネレーターを実行してみる。

$ bundle exec rails g controller hoge
>      create  app/controllers/hoge_controller.rb
>      invoke  erb
>      create    app/views/hoge
>      invoke  test_unit
>      create    test/functional/hoge_controller_test.rb
>      invoke  helper
>      create    app/helpers/hoge_helper.rb
>      invoke    test_unit
>      create      test/unit/helpers/hoge_helper_test.rb
>      invoke  assets
>      invoke    js
>      create      app/assets/javascripts/hoge.js
>      invoke    scss
>      create      app/assets/stylesheets/hoge.css.scss

Rails では基本的に controller と helper はセットで考えるのが自然な感覚である。
Ruby on Rails 3 アプリケーションプログラミング」には、次のような記述がある。

あとからヘルパーを探す時に困らないよう、まずはアプリケーション共通のヘルパーは application_helper.rb に、コントローラー固有のヘルパーは <コントローラ名>_helper.rb に記述するのが望ましいでしょう。

ヘルパーの数が多くなってきたら、自分で format_helper.rb(FormatHelper モジュール)のようなファイルを別に用意して、ヘルパーの種類ごとに整理するという方法もあります。

ちょっとまった!

controller, helper を対になるものとして考えながらコーディングしていたところ、ある問題にあたってしまった。例えば helpers/hoge_helper.rb と helpers/piyo_helper.rb に、それぞれに同じ名前だが別の挙動をするメソッドを定義し、それを hoge_controller のビューから呼び出すと、hoge_helper.rb のメソッドが呼ばれる。わけではなく!hoge_helper.r、piyo_helper.rb のどちらのメソッドが呼ばれるかはわからない!

各モジュール内に入っているメソッドが同じ名前の場合、個別にコールされるのではなく全て同じものがコールされてしまっている。

上記の記事は Rails 2 系の話だが、私が遭遇した問題と同じだ。

実は、最近の Rails では「全てのヘルパーメソッド(helpers/*_helper.rb)」が自動で読み込まれるようになっている。

そのため、別ファイルのヘルパーであっても、同名のメソッドがあると挙動がおかしくなってしまう。

なぜこのような挙動になってしまったのか?

stackoverflow に「Why are all Rails helpers available to all views, all the time? Is there a way to disable this?」というトピックが上がっていた。

これによると、Rails 3.1.x 以降では次の設定を追加することで、コントローラーに該当するヘルパーしか読み込まないようにすることが出来るらしい。

config.action_controller.include_all_helpers = false

しかし、こちらの設定は最初から会ったわけではなく、3.1 でプルリクエストとして追加されたものらしい。

In older rails versions there was a way to use only helpers from
helper file corresponding to current controller and you could also
include all helpers by saying 'helper :all' in controller. This config
allows to return to older behavior by setting it to false.

要するに、こちらは後方互換性のため用意されたもののようだ。Rails としてはヘルパーの全読み込みが基本動作であるようだが...、フーム、いったいどういう運用をするのが正解なんだろうか?

Ruby on Rails を使った有名なオープンソースプロジェクトといえば、Redmine が思い当たる。Redmine ではどのようにしているのだろうか?Redmine の最新バーション(2.3)では Rails 3.2.13 を使用しており、「config/application.rb」には次の記述がみつかった。

...
    # Do not include all helpers
    config.action_controller.include_all_helpers = false
...

一応、全ての helper ロードは OFF にしているようだ。

helper のディレクトリを見てみると、コントローラーと対になるヘルパーファイルの他に、機能別に分類されたヘルパーファイルも見つけることができる。

これは私の推測だが、純粋に共通処理を application_helper に集約させ、コントローラー別の処理を個別の helper に記述するというやり方は、すぐに appliation_helper が大きくなり過ぎるような気がする。そのため、機能別にヘルパーファイルを分けるというやり方は良いような気もするが、ヘルパーメソッドを探すのが大変になりそうな印象がある。それに各ヘルパーに記述されたメソッド名が被ってわいけないというのも、わかりにくい。

何よりも、この辺りのベストプラクティスというのはハッキリさせておくべきだと思う。Rails を導入するメリットは、異なるプロジェクトであっても、それは一定のルールが守られている(Rails の規則に則っている)という約束ごとができるからだ。プロジェクトによって、ヘルパーがコントローラー別に分類されてたり、全読み込みが ON になってたり、OFF だったりするのはつらい気がする。これはヘルパーだけのはなしではなくて、Rails を使っているのに、Rails らしくない記述になっていて、結局そのプロジェクトのフレームワークルーチンみたいなのを、覚えなきゃいけない。という経験が何度もある。

Rails はそういったプロジェクトごとの独自の規則を「軽減する」という思想なのかもしれないが、正直、うーん。という感じがする。


最終的に、どういう運用をすることにしたのか?

私は、まず「config.action_controller.include_all_helpers = false」は使わないようにした。

上記のトピックを見ても分かるように、Rails2〜3 にかけて、かなり変更内容に変遷がある。今後もこの辺りの仕様が変更される可能性は大いにあると思うので、あまり依存したくないと思う。

ヘルパーの分類としては、コントローラーベースではなく、機能ベースでヘルパーファイルを分類するようにした。

ヘルパーのメソッド名が重複しないように&該当のヘルパーファイルがすぐに分かるよう、メソッドは次の命名規則を持たせることにした。

def h_<ヘルパーファイル名>_<メソッド名>

たとえば、hoge_hoge_helper.rb に piyo_piyo というメソッドを追加したいときは、次のような定義になる。

# hoge_hoge_helper.rb
module HogeHogeHelper
  def h_hogehoge_piyo_piyo
    # something ...
  end
end

ビューで呼び出すときは次のような感じ

<%= h_hogehoge_piyo_piyo() %>

ヘルパーメソッドの頭文字に「h_」をつけることは、ruby らしくない!とお怒りになる方もいらっしゃるかと思うが、この行を見ただけヘルパーメソッドであるというのが明示的にわかるので、私はこうしようと思う。