この記事は リクルートライフスタイル Advent Calendar 2016 5日目の記事です。
ビューティー開発Tの@tacuma_igeiです。主にRailsを使って、まだここにない出会いを創出していこうと励んでいます。
はじめに
中規模〜大規模のRails開発をする上で、多くの人が直面するであろう「ApplicationController・Helper散らかる問題」に対して、多くの人がやりがちな対処法とその落とし穴について理解を深め、回避策を提示していければと思います。 需要がありそうなので 事例をRailsにしていますが、基本的にRubyの話なので、Sinatra
や Padrino
でも同様の問題が起こりえます。
結論
結論から述べると、
- モジュールに切り出してMix-inする場合は、
module_function
を使って、明示的にモジュール関数を呼び出すようにしましょう - 時間を作って、クラスとモジュールの理解を深めていきましょう
ApplicationController・Helper散らかる問題
(基本的に)RailsではすべてのコントローラはApplicationController
を継承するので、コントローラ共通の処理はそこに書くことが多いです。例えば、
- user-agentの分別
- Cookieの処理
- ログイン周辺の処理
などはよくApplicationController
に書きがちではないでしょうか。
helperにも、View層で使いたい便利処理を書くことが多いですよね。
でも、そうやって無邪気にメソッドを追加していくと、いつのまにかApplicationController, helperが肥大化し、手に負えない状態になってしまいます。今回は、例としてApplicationController
を使って話を進めます。
対処法
Rubyにはモジュールという機能があるので、簡単に複数のメソッドをモジュールに入れて、散らかったメソッドを整理整頓し、見通しのよい状態にリファクタすることができます。例えばこんな感じに。
# リファクタ前
class ApplicationController < ActionController::Base
include CookieHandler
def smart_phone?
...
end
def tablet?
...
end
def app_webview?
...
end
def crawler?
...
end
def has_xxx_cookies?
...
end
def is_logined?
...
end
def generate_cookie
puts 'ex) 汎用的にCookieをセットするための処理が実行される'
end
... # 無限に続くメソッド群
end
↓
# リファクタ後
class ApplicationController < ActionController::Base
include CookieHandler # Cookieに関する処理を読み込む。 generate_cookieメソッドもこちらに入ってる
include LoginHandler # ログイン・ログアウトに関する処理を読み込む
include UserAgent # ユーザーエージェントに関する処理を読み込む
# 本当にここに書く必要があるメソッドが以下に続く
...
end
いい感じにすっきりして、見通しがよくなりました。
安易にモジュール分割すると出てくる問題点
月日は流れ、追加要件が発生したようです。例えば、「ゲストユーザーの場合は特別なCookieをセットしたい」という要件だったとする。無邪気に「モジュール分割してみやすくする」というルールに則り、実装してみます。
class ApplicationController < ActionController::Base
include CookieHandler # Cookieに関する処理を読み込む。generate_cookieメソッドもこちらに入ってる
include LoginHandler # ログイン・ログアウトに関する処理を読み込む
include UserAgent # ユーザーエージェントに関する処理を読み込む
+ include GuestUserCookieHandler # ゲストユーザーに関する処理を読み込む
# 本当にここに書く必要があるメソッドが以下に続く
...
end
module GuestUserCookieHandler
def generate_cookie
puts 'ゲストユーザーの場合のみ何かしらの処理が実行される'
end
end
ここで気をつけたいのが、GuestUserCookieHandler
が実装される以前に、CookieHandler
で#generate_cookie
を定義していることです。
何が問題かというと、この実装のままApplicationController
で #generate_cookie
を使うと、#generate_cookie
がモジュールのロード順によって、上書きされてしまい、GuestUserCookieHandler#generate_cookie
が実行されてしまうことです。本来、CookieHandler#generate_cookie
を使うところで、GuestUserCookieHandler#generate_cookie
が実行されてしまい、(名前は同じだが)全然違った処理をしてしまうのです。大変です。
回避策
良い感じにモジュール分割して名前空間も区切ったはずなのに、どうしてそんなことが起きるんだ!しかもめっちゃ気づきにくいし、追いづらい...!
でも大丈夫です。そんなこともRubyなら簡単に解決できます。
結論でも述べた通り、ここはひとつ、落ち着いて、モジュール関数を使っていきましょう。
ってことで、実装し直してみます。
class ApplicationController < ActionController::Base
include CookieHandler # Cookieに関する処理を読み込む
include LoginHandler # ログイン・ログアウトに関する処理を読み込む
include UserAgent # ユーザーエージェントに関する処理を読み込む
include GuestUserCookieHandler # ゲストユーザーに関する処理を読み込む
# 本来はCookieHandlerの#generate_cookieを使って処理をするメソッド
def hoge
cookies[:hoge] = CookieHandler::generate_cookie
end
# 本来はGuestUserCookieHandlerの#generate_cookieを使って処理をするメソッド
def fuga
cookies[:fuga] = GuestUserCookieHandler::generate_cookie
end
end
module CookieHandler
+ module_function
def generate_cookie
puts '汎用的にCookieをセットするための処理が実行される'
end
end
module GuestUserCookieHandler
+ module_function
def generate_cookie
puts 'ゲストユーザーの場合のみ何かしらの処理が実行される'
end
end
このように、モジュール関数として使ってあげることで
- ApplicationControllerの散らかりは解消
- 明示的にどのモジュールのメソッドを呼び出しているのか、分別することができるようになったので、メソッドが上書きされてしまう問題も解消
されます。
反省
実際に業務でも知らないうちに、この問題にぶち当たってしまっていたようで、わりと長い間、気づかれなかったという事実があり、このような記事を書くに至りました。気付いたきっかけも、プロダクトコードを書いていてぶちあたったわけではなく、個人的にメタプログラミング周辺の勉強をしていた最中だったのです。わりと気づきにくいですし、かつ開発が進めば進むほど影響範囲が広く、修正しづらいところなので、今一度、開発しているプロダクトを振り返ってみると良いかもしれません。
以下、改めて知識を入れ直すためのガイドです。
回避策はわかった。でもなぜそれで回避したことになる?
モジュールの基本的な役割や振る舞いを知ると納得します。ここで書くのもよいのですが、先人が良い感じにわかりやすくまとめてくださっていますので、ぜひ読んでみてみるとよいかもです。
書籍を何度も読み返してみる
自分もEffective RubyやパーフェクトRubyといった、入門の次の本?的な立ち位置にある書籍を手元に置き、一度は読んでいました。ただ、この記事を書くに至るあたり、「読んだだけで満足し、わかってるフリだったんだな」と反省しました。例えば、この記事の話は「やさしいRuby」という入門書でも補える知識なんですよね。プロダクトコードを書きつつ、状況や求められることに応じて書籍を読み直すというのも、本当に大事だなと思いました。