TL;DR
- Mix-inは危険な機能だ
- Mix-inは必要なシーンにだけ使おう
- 関数だけを提供するだけのモジュールを作るのが、弊害が少なくシンプル
Mix-in は弊害が多い
Mix-inはRubyの基本的な機能であり、入門書でも序盤で紹介されます。
それゆえか単に処理をクラス間で共有するためだけにMix-inを使おうとしているのをしばしば見かけます。
実際には、Mix-inにはうかつに使うと後々痛い目に会う機能であり、大抵のケースではMix-inを使うのは適切ではありません。
落とし穴1: インスタンス変数やメソッドが上書きされる
includeがやっているのは、「単にMix-inのメソッドをクラスにコピーする」ことだけです1。
なので同じ名前のメソッドをクラス側で定義すると、Mix-inのメソッドを警告なしに上書きしてしまいます。同様にMix-inのインスタンス変数を、クラス側で参照したり変更したりできてしまいます。
module HttpRequestable
def get(uri)
request(:get, uri)
end
private
def request(http_method, uri)
# HTTPリクエストする処理
end
end
class StoreAPI
include HttpRequestable
...
# 商品の入荷をリクエストする機能を追加しよう!
# (実はHttpRequestableのメソッドを上書きしてしまっている)
def request(product_id, count)
end
end
落とし穴2: メソッドの関係が複雑になる
クラスからMix-inのメソッドを呼び出せるだけでなく、Mix-inからクラスのメソッドを呼び出したり、Mix-inから別のMix-inのメソッドを呼び出すことができるため、
クラスのメソッド => Mix-in Aのメソッド => Mix-in Bのメソッド => クラスのメソッド => ...
のような複雑な参照関係が生じてしまうことがあります。
呼び出しが適切に管理されていればTemplate Method パターンと呼べるのですが、
おっ、親クラスの〇〇っていうメソッドを呼べば動かせるぞ。よし!!
と、深く考えずに呼んでしまうことが、稀によくあります。
落とし穴3: メソッドの定義場所が分かりにくい
最近はIDEが良くなったので問題になりにくいのですが、includeするモジュールが増えてくると、メソッドの定義箇所がわかりにくくなりがちです。
RailsのActionMailer::Baseには12個のモジュールがincludeされているのですが、この中からrelative_url_rootの定義箇所をIDEなしで見つけるのは一苦労です。
落とし穴4: 確信が持てない
最大の問題は、上記のような落とし穴を踏んでいないか確信を持てないことです。Rubyは動的言語なので、静的なチェックが難しく、
これは private メソッドだから、変更しても安全だな!!
と思っていたら、実はMix-inで誤って呼び出していないとも限らないのです。
そこで「関数的モジュール」
(以下の「関数的モジュール」は私の造語です。もっと適切な用語があるのかもしれませんが、調べ方が悪いのか見つからなかったので、ここでは「関数的モジュール」と呼びます)
関数的モジュールとは、特異メソッドだけを提供するモジュールのことです。
module HttpRequestable
class << self
def get(uri)
request(:get, uri)
end
private
def request(http_method, uri)
# HTTPリクエストする処理
end
end
end
使うときは、includeせずメソッドを直接呼び出します。
class StoreApi
# メソッド名が被っても大丈夫!!
def request
HttpRequestable.get(uri)
end
end
関数的モジュールでは、Mix-inのような問題は起きません。
- インスタンス変数を使わない(使えない2)
- メソッドがコピーされることが無い
- クラス側のメソッドを呼び出すことがない(できない)
- どのモジュールのメソッドを呼んでいるかは明らか
それでもMix-in を使うケース
そうはいっても、Mix-inを定義するのが便利なケースもあります。
Mix-inを定義する際にはTemplate Method パターンとして正しく定義することを心がけましょう。
具体的には・・・
- Mix-inがクラスのどのメソッドを呼ぶか明らかにする
- クラスからはMix-inのどのメソッドを呼んでよいのか明らかにする
- インスタンス変数を避ける
- privateメソッドを避ける(RubyのprivateはC++やJavaのprivateと異なることに気をつける)