module の中で、include 先に教えたくない private メソッドを定義するには?
module の中で、include 先に教えたくない private メソッドを定義するには?
Ruby でモジュールを定義するとき、include 先のクラスには見せたくない private メソッドをどう定義すればいいのか、悩んだことはないでしょうか?
この記事では、そんなときに活用できる Ruby の refinement を使ったテクニックを紹介します。
問題の背景
たとえば、以下のようなモジュール Foo を考えます。
module Foo
def foo
bar
end
private
# @private
def bar
1
end
end
このモジュールをクラスに include して使います。
class Bar
include Foo
end
この状態で Bar.new.foo を呼ぶのはもちろん問題ありません。しかし bar は include 先の Bar クラスからもアクセスできます。
class Bar
include Foo
# bar の存在は隠したいという意図があったとしても、
# 単に private メソッドなのでこのようにすれば呼べます。
def baz
bar
end
end
Bar.new.baz
# => 1
このように、意図せず内部実装(この場合は bar メソッド)に依存されてしまう可能性があります。
実際、単に private メソッドを include したい場合も問題ないです。といいますかそれが本来の意図なのですが、では module の中に閉じた private メソッドは作れないのでしょうか?
どうして隠したいのか?
たとえば、Foo モジュールが gem ライブラリとして提供されており、Bar がユーザアプリケーションのコードだったとします。
ライブラリの作者としては、「bar は内部実装なので触ってほしくない」「将来的に自由に変更・削除したい」などの理由で、外部からの依存を避けたいことがあります。
このような「include はしてほしいが、一部のメソッドは外に見せたくない」という状況では、通常の private だけでは不十分です。
解決策: refinement を使う
こういったケースで、Ruby の refinement を使うことで、include 先のスコープから特定のメソッドの存在を隠すことができます。
以下のように bar を refine の中に定義します。
module Foo
module Private
refine Foo do
private
def bar
1
end
end
end
using Private
def foo
bar
end
end
この形にしておくと、bar メソッドは Foo モジュールの中では使えますが、include したクラスのスコープからは見えなくなります。
class Bar
include Foo
def baz
bar
end
end
Bar.new.baz
# => NameError (undefined local variable or method `bar' for #<Bar:...>)
このように、モジュール内部でだけ使えるメソッドを実現できます。
慎重すぎるぐらいがちょうどいい?
なお、実はまだ、使う側で次のように using を書けば bar を呼べてしまいます。
class Bar
include Foo
using Foo::Private
def baz
bar
end
end
Bar.new.baz
# => 1
これも防ぎたい!という場合は、Foo::Private 自体を private_constant にしておくことで、
あえてハックしようとする人に対しての心理的なハードルを上げることができます。
module Foo
module Private
refine Foo do
private
def bar
1
end
end
end
private_constant :Private
using Private
def foo
bar
end
end
ここまでやるかどうかはケースバイケースですが、
内部実装に触れさせたくない強い理由があるときには検討の価値があります。
更にハードルを上げるには?
private_constant による防御でも満足できないあなたには、そもそも定数を作らないという手段があります。
これにより Foo::Private のような定数名で辿られること自体がなくなります。
module Foo
private_module = Module.new do
refine Foo do
private
def bar
1
end
end
end
using private_module
def foo
bar
end
end
これで ObjectSpace.each_object(Module) のような手段でも特定しづらくなり、
意図しない依存の可能性をさらに減らすことができます。
ただし、読みやすさ・保守性とのトレードオフはあるため、本当に必要な場面かどうかは慎重に見極めましょう。
まとめ
- Ruby の
privateは include 先にも届いてしまうため、完全に隠すことはできません。 - どうしても隠したいメソッドは、refinement を使うことでモジュール内部だけに閉じ込めることができます。
-
private_constantや 匿名 module との組み合わせで、さらに踏み込んだ隠蔽も可能です。 - 使用場面は限定的ですが、「見せたくないけど呼び出したい」ような微妙なユースケースには有効です。
ちょっとした小技ではありますが、モジュール設計の自由度が上がるので、覚えておくと便利かもしれません。
Discussion