🐫

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 を呼ぶのはもちろん問題ありません。しかし barinclude 先の 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 先のスコープから特定のメソッドの存在を隠すことができます。
以下のように barrefine の中に定義します。

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