LoginSignup
48
34

More than 5 years have passed since last update.

Railsを事例にして学ぶ、むやみにモジュール分割しているとハマる落とし穴とその回避策

Last updated at Posted at 2016-12-04

この記事は リクルートライフスタイル Advent Calendar 2016 5日目の記事です。
ビューティー開発Tの@tacuma_igeiです。主にRailsを使って、まだここにない出会いを創出していこうと励んでいます。

はじめに

中規模〜大規模のRails開発をする上で、多くの人が直面するであろう「ApplicationController・Helper散らかる問題」に対して、多くの人がやりがちな対処法とその落とし穴について理解を深め、回避策を提示していければと思います。 需要がありそうなので 事例をRailsにしていますが、基本的にRubyの話なので、SinatraPadrinoでも同様の問題が起こりえます。

結論

結論から述べると、

  • モジュールに切り出してMix-inする場合は、module_functionを使って、明示的にモジュール関数を呼び出すようにしましょう
  • 時間を作って、クラスとモジュールの理解を深めていきましょう

ApplicationController・Helper散らかる問題

(基本的に)RailsではすべてのコントローラはApplicationControllerを継承するので、コントローラ共通の処理はそこに書くことが多いです。例えば、

  • user-agentの分別
  • Cookieの処理
  • ログイン周辺の処理

などはよくApplicationControllerに書きがちではないでしょうか。
helperにも、View層で使いたい便利処理を書くことが多いですよね。
でも、そうやって無邪気にメソッドを追加していくと、いつのまにかApplicationController, helperが肥大化し、手に負えない状態になってしまいます。今回は、例としてApplicationControllerを使って話を進めます。

対処法

Rubyにはモジュールという機能があるので、簡単に複数のメソッドをモジュールに入れて、散らかったメソッドを整理整頓し、見通しのよい状態にリファクタすることができます。例えばこんな感じに。

app/controllers/application_controller.rb
# リファクタ前
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

app/controllers/application_controller.rb
# リファクタ後
class ApplicationController < ActionController::Base
 include CookieHandler      # Cookieに関する処理を読み込む。 generate_cookieメソッドもこちらに入ってる
 include LoginHandler       # ログイン・ログアウトに関する処理を読み込む
 include UserAgent          # ユーザーエージェントに関する処理を読み込む

 # 本当にここに書く必要があるメソッドが以下に続く
 ...
end

いい感じにすっきりして、見通しがよくなりました。

安易にモジュール分割すると出てくる問題点

月日は流れ、追加要件が発生したようです。例えば、「ゲストユーザーの場合は特別なCookieをセットしたい」という要件だったとする。無邪気に「モジュール分割してみやすくする」というルールに則り、実装してみます。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
   include CookieHandler                   # Cookieに関する処理を読み込む。generate_cookieメソッドもこちらに入ってる
   include LoginHandler                    # ログイン・ログアウトに関する処理を読み込む
   include UserAgent                       # ユーザーエージェントに関する処理を読み込む
 + include GuestUserCookieHandler          # ゲストユーザーに関する処理を読み込む

 # 本当にここに書く必要があるメソッドが以下に続く
 ...
end
app/controllers/concerns/guest_user_cookie_handler.rb
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なら簡単に解決できます。
結論でも述べた通り、ここはひとつ、落ち着いて、モジュール関数を使っていきましょう。
ってことで、実装し直してみます。 

app/controllers/application_controller.rb
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
app/controllers/concerns/cookie_handler.rb
module CookieHandler

  + module_function

  def generate_cookie
    puts '汎用的にCookieをセットするための処理が実行される'
  end
end
app/controllers/concerns/guest_user_cookie_handler.rb
module GuestUserCookieHandler

  + module_function

  def generate_cookie
    puts 'ゲストユーザーの場合のみ何かしらの処理が実行される'
  end
end

このように、モジュール関数として使ってあげることで

  • ApplicationControllerの散らかりは解消
  • 明示的にどのモジュールのメソッドを呼び出しているのか、分別することができるようになったので、メソッドが上書きされてしまう問題も解消

されます。

反省

実際に業務でも知らないうちに、この問題にぶち当たってしまっていたようで、わりと長い間、気づかれなかったという事実があり、このような記事を書くに至りました。気付いたきっかけも、プロダクトコードを書いていてぶちあたったわけではなく、個人的にメタプログラミング周辺の勉強をしていた最中だったのです。わりと気づきにくいですし、かつ開発が進めば進むほど影響範囲が広く、修正しづらいところなので、今一度、開発しているプロダクトを振り返ってみると良いかもしれません。

以下、改めて知識を入れ直すためのガイドです。

回避策はわかった。でもなぜそれで回避したことになる?

モジュールの基本的な役割や振る舞いを知ると納得します。ここで書くのもよいのですが、先人が良い感じにわかりやすくまとめてくださっていますので、ぜひ読んでみてみるとよいかもです。

書籍を何度も読み返してみる

自分もEffective RubyやパーフェクトRubyといった、入門の次の本?的な立ち位置にある書籍を手元に置き、一度は読んでいました。ただ、この記事を書くに至るあたり、「読んだだけで満足し、わかってるフリだったんだな」と反省しました。例えば、この記事の話は「やさしいRuby」という入門書でも補える知識なんですよね。プロダクトコードを書きつつ、状況や求められることに応じて書籍を読み直すというのも、本当に大事だなと思いました。

48
34
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
34