読者です 読者をやめる 読者になる 読者になる

クラスメソッドのprivate化

Ruby

クラスメソッドの定義

Rubyでクラスメソッド(=クラスオブジェクトの特異メソッド)を定義するには、いくつかの方法があります。
たとえば、Hoge クラスオブジェクトに hoge() メソッドを定義する場合、
1. クラス定義内でクラスオブジェクトに特異メソッドを定義

class Hoge
  def self.hoge()
  end
end

2. クラス定義内で特異クラスに対してメソッドを定義

class Hoge
  class << self
    def hoge()
    end
  end
end

3. クラス定義外でクラスオブジェクトに特異メソッドを定義

class Hoge
end
def Hoge.hoge()
end

4. クラス定義外で特異クラスに対してメソッドを定義

class Hoge
end
class << Hoge
  def hoge()
  end
end

いずれも外部から Hoge.hoge() としてメソッドを呼ぶことができますが、個人的には 1 の方法が好みです。

2 は def の行だけ見たときにクラスメソッドなのかインスタンスメソッドなのかわからないし、インデントが1段階深くなってしまいます。

3 だと class Hoge 内で定義した定数が定数名だけで参照できません。

class Hoge
  Fuga = 123
end
def Hoge.hoge()
  Hoge::Fuga  #=> 123
  Fuga        # uninitialized constant Fuga (NameError)
end

定数の名前解決はクラスやメソッド階層ではなく、定数を参照した箇所からみたスコープに依存するためのようです。詳しくはしりません。

4 はさらにうっかり次のように書くと同じ名前で別の定数が定義できたりします。

class Hoge
  Fuga = 123
end
class << Hoge
  Fuga = 456
  def hoge()
    Fuga
  end
end

「class << Hoge」内のコンテキストは Hoge ではなくて、Hoge の特異クラスだからみたいです。詳しくはしりません。

クラスメソッドの private 化

クラスメソッドもインスタンスメソッドと同様に private 化することができます。

class Hoge
  def self.hoge()
  end
  def self.fuga()
  end
  private_class_method :fuga
end
Hoge.hoge   # OK
Hoge.fuga   # private method `fuga' called for Hoge:Class (NoMethodError)

インスタンスメソッドの private とは異なり、引数なしで指定してもそれ以降が private になるわけではありません。

class Hoge
  def self.hoge()
  end
  private_class_method   # 単に無視される
  def self.fuga()
  end
end
Hoge.hoge   # OK
Hoge.fuga   # OK

次のように特異クラスを使用すれば可能です。

class Hoge
  class << self
    def hoge()
    end
    private
    def fuga()
    end
  end
end
Hoge.hoge   # OK
Hoge.fuga   # private method `fuga' called for Hoge:Class (NoMethodError)

ですが、上述の理由により特異クラスでクラスメソッドの定義はあまりしたくありません。

ということで、引数なしで private_class_method が指定された時に、それ以降で定義されたクラスメソッドが private になるような仕組みを考えてみました。

module PrivateClassMethod
  def private_class_method(*args)
    # 引数が指定されない時は、
    if args.empty?
      # 特異メソッドが追加された時に、
      def self.singleton_method_added(name)
        # 特異メソッドを private に指定
        private_class_method name
      end
      # クラスが継承された場合、
      def self.inherited(subclass)
        # 特異メソッドを private にする機能を無効化
        subclass.class_eval do
          def self.singleton_method_added(name)
          end
        end
      end
    else
      # 引数が空でない時は元の動き
      super
    end
  end
end

class Hoge
  # PrivateClassMethod の機能をクラスに取り込む
  extend PrivateClassMethod

  def self.hoge()
  end
  private_class_method
  def self.fuga()
  end
end

Hoge.hoge   # OK
Hoge.fuga   # private method `fuga' called for Hoge:Class (NoMethodError)

Ruby 本来の動きとは異なりますが、個人的には便利だと思います。