はじめに
Railsでは app/helpers
配下に自作のヘルパーモジュールを定義できます。
module ProductsHelper
def product_count
Product.count
end
end
(Railsのデフォルトの設定では)上で定義した product_count
メソッドはview(erb)のどこからでも呼び出せます。
<%# どのview(erb)でも product_count を呼び出せる %>
<%= product_count %>
つまり、ヘルパーメソッドは一種のグローバルメソッドのように振る舞います。
もし名前がかぶったら??
では、以下のようにもしうっかり同じメソッド名を定義してしまったらどうなるでしょうか?
module ProductsHelper
def calc_total(product, qty)
product.price * qty
end
end
module OrdersHelper
# ProductsHelperのcalc_totalメソッドと全く同じ名前にしてしまった!
def calc_total(order, tax_rate)
order.items.sum(&:price) * tax_rate
end
end
この場合は、後から読み込まれた方が優先されます。
つまり、以下のコードはProductsHelperのcalc_total
が呼び出されるのか、OrdersHelperのcalc_total
が呼び出されるのか、ハッキリしません。
<%# ProductsHelperかもしれないし、OrdersHelperかもしれない %>
<%= calc_total(product, qty) %>
いずれにしても、どちらか一方のメソッド定義しか有効にならないのであれば、それがバグの原因になるのは火を見るより明らかですね。
<%# どちらか一方のメソッド呼び出しは確実に失敗する!!💥 %>
<%= calc_total(product, qty) %>
<%= calc_total(order, tax_rate) %>
解決策:具体的なメソッド名を付ける
この問題の解決策は、より具体的なメソッド名を付けて名前の重複を回避する、です。
まあ、何のヒネリもない解決策なんですけど・・・😅
module ProductsHelper
def calc_product_total(product, qty)
product.price * qty
end
end
module OrdersHelper
def calc_order_total(order, tax_rate)
order.items.sum(&:price) * tax_rate
end
end
名前が重複しなければ、意図せず違う方のメソッドが呼び出されることはありません。
<%# メソッド名が重複しなければ安心! %>
<%= calc_product_total(product, qty) %>
<%= calc_order_total(order, tax_rate) %>
大事なポイント:汎用的なメソッドか、何かに特化したメソッドかを意識すべし
この問題を避けるために押さえておくべき大事なポイントが2つあります。
1つは冒頭にも書いたように、「Railsのヘルパーメソッドは一種のグローバルメソッドのように振る舞う」ということです。
あなたの定義したヘルパーメソッドは誰でもどこからでも呼び出せてしまうのです(viewの中であれば)。
そしてもう1つは「汎用的なメソッドには抽象的な名前を付け、何かに特化したメソッドには具体的な名前を付ける」ということです。
上で示したメソッド名重複の例は、何かに特化したメソッドなのに、抽象的な名前を付けてしまったのが問題でした。
module ProductsHelper
# このメソッドはproductに対してしか使えない。つまりproductに特化している
# しかしメソッド名は抽象的(calc_totalという名前はproductに特化していない)
def calc_total(product, qty)
product.price * qty
end
end
productにしか使えないメソッドなのであれば、メソッド名でそれを明示すべきです。
そうすればメソッド名重複のリスクを減らすことができます。
module ProductsHelper
# メソッド名にproductを含めることで、抽象的な名前から具体的な名前になった
def calc_product_total(product, qty)
product.price * qty
end
end
反対に、モデルやviewを問わず、誰でもいつでも呼んでよいメソッドなら、抽象的なメソッド名でもOKです。
また、そういうメソッドはApplicationHelperに定義しましょう。
module ApplicationHelper
# 金額表示を整形するメソッド。モデル、viewを問わず、どこからでも呼び出しOK
# 汎用性が高いのでメソッド名も抽象的にする
def format_JPY(amount)
if amount >= 0
"¥#{number_with_delimiter(amount)}"
else
"-¥#{number_with_delimiter(amount.abs)}"
end
end
end
ヘルパーメソッドを定義する際は、一度立ち止まってよく考えよう
業務で開発する大規模なRailsアプリケーションになると、いろんな開発者がいろんなヘルパーメソッドを定義します。
ですので、「意図せずAさんとBさんが同じメソッド名を定義してしまった」というリスクも上がります。
ヘルパーメソッドを定義する際は「私が今、このメソッド名を使ってもあとの人が困らないか?(将来誰かが同じような名前を付ける可能性がないか?)」を自問自答してください。
たとえば、この記事をここまで読んだあなたなら、以下のメソッドを見たときに「えっ、ちょっと怖い😨」って思うはずですよね!?
module ProductsHelper
# そんなメソッド名で大丈夫か?
def status_text(product)
t("model.product.status.#{product.status}")
end
end
もし「この名前は抽象的すぎるから、将来他のメソッドとかぶってしまうかもしれないな」という予感がしたら、より具体的な名前を付けて重複するリスクを下げましょう。
module ProductsHelper
# productに特化したメソッド名にして重複のリスクを下げる
def product_status_text(product)
t("model.product.status.#{product.status}")
end
end
参考:ヘルパーをグローバルメソッドにしない方法もある、が。。
Railsには config.action_controller.include_all_helpers
という設定があります。
このフラグをfalse
に設定すると、controller名に一致するhelperモジュールしかincludeされなくなり、ヘルパーメソッドの名前が衝突するリスクを減らせます。
# controller名に一致するhelperモジュールしか呼び出せないようにする
config.action_controller.include_all_helpers = false
ただし、この設定を入れると、使い慣れたRailsの開発スタイルとかなりかけ離れたものになります。
ヘルパーメソッドを提供するgemの挙動もおかしくなったりして、いろいろとハックしないといけなくなります。
過去に一度false
に設定したことがあるのですが、そのときのRailsアプリ開発はかなりの苦行だったので、個人的にはあまりお勧めしません。
ボーナス!重複したヘルパーメソッドを検出するスクリプト
今まで名前の重複なんて気にしたことがなかった!
もしかしたら、同名メソッドが定義されているかもしれない 😱
・・・と、不安になったそこのあなた!
重複した名前のヘルパーメソッドを検出するスクリプトを作ってみました(with a little help from my ChatGPT)。
以下のスクリプトをターミナルに貼り付ければ、重複したヘルパーメソッドを検出できます(macOS標準のターミナル + zshで検証済み)。
git grep -h '^\s*def ' app/helpers/ \
| sed -E 's/.*def ([a-zA-Z0-9_!?]+).*/\1/' \
| sort | uniq -d \
| while read method; do
echo "\n==== $method ===="
GIT_PAGER=cat git grep -n "def $method\\b" app/helpers/
done
1行バージョンが欲しい方はこちらをどうぞ。
git grep -h '^\s*def ' app/helpers/ | sed -E 's/.*def ([a-zA-Z0-9_!?]+).*/\1/' | sort | uniq -d | while read method; do echo "\n==== $method ===="; GIT_PAGER=cat git grep -n "def $method\\b" app/helpers/; done
もし重複メソッドがあるとこんなふうに出力されます。
==== calc_total ====
app/helpers/products_helper.rb:2: def calc_total(product, qty)
app/helpers/orders_helper.rb:2: def calc_total(order, tax_rate)
何も表示されなければ、重複したメソッドはありません(ほっ)。
まとめ:いいコードを書けば、バグは生まれにくい
というわけでこの記事ではRailsのヘルパーメソッドの名前が重複した場合に起きる問題と、この問題を避けるために意識しておきたい命名アプローチについて説明してみました。
余談ですが、こういった問題が起きるのは動的型付け言語のRubyならでは、という見方もできます。
静的型付けならもっと簡単に問題を検出できるのに、と思わなくもないですが、動的型付け言語には動的型付け言語の良さがあります(それを語り出すと長くなるのでやめておきますが)。
前述したように、「汎用的なメソッドには抽象的な名前を付け、何かに特化したメソッドには具体的な名前を付ける」ということを意識すれば、重複のリスクはかなり減らせるはずです。
そもそもこれって、ヘルパーメソッドに限らず、メソッドや関数を定義する際にはいつでも意識しておきたい考え方ですよね(名前重要、ってやつです)。
つまり、「いいコードを書けば、バグは生まれにくい」というわけです。
型があろうとなかろうと、いい名前を付けるというのはプログラミングの基本です。
なので、型の助けも便利といえば便利なのですが、その前にまず、プログラマとしていいコードを書けるようになりましょう!
あわせて読みたい
「はっ、もしかしてprivateメソッドとして定義したら、少なくともそのメソッドは名前の重複を気にしなくていいのでは・・・!?」と思ったそこのあなた!
残念ながら、ヘルパーメソッドはprivateメソッドにはできないので、重複問題は何も解決しません😝
詳しくは以下の記事をご覧ください。