ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【Ruby v2.6】クラスメソッド その3(public, protected, private)

前置き

Ruby のクラスメソッドについてまとめます。 その3は、クラスメソッドの public, protected, private について

書き方

クラス定義内で、デフォルトでクラスメソッドを定義すると、public のクラスメソッドとなります。

ただ、インスタンスメソッドと同じように、クラス定義内に privateprotected キーワードの後にクラスメソッドの定義を記述しても、プライベート、パブリックなクラスメソッドにはなりません。

書き方は、2通りあります。

  1. public_class_methodprivate_class_method を使って設定する。(但し、protected_class_method がないので、protected なクラスメソッドはこの方法では作れません。。。)

  2. クラスメソッドは、特異クラス内でのインスタンスメソッドなので、特異クラスをオープンし、そこでprivateprotected キーワードで設定する。

private_class_method を使う場合

private_class_method を使う場合は、先にクラスメソッドを定義した後で、private_class_method :[メソッド名] の形式で記述します。

class A
    # self をつけて定義すると、全て public なクラスメソッドになる。
    def self.public_method
        p "called self.public_method."
    end

    def self.private_method
        p "called self.private_method."
    end

    # private_class_method を使って、プライベート化する。
    private_class_method :private_method

    # 各メソッドを呼び出すためのクラスメソッド
    def self.call_public_method
        A.public_method
    end

    def self.call_private_method
        private_method     # レシーバーはつけてはいけない
    end
end

# クラス A を継承するクラス B
class B < A
    # 継承されたクラス B から、クラス A のメソッドを呼び出してみる。
    def self.call_public_method
        A.public_method
    end

    def self.call_private_method
        A.private_method
    end

    # クラス B のインスタンスから呼び出せるか?
    def call_public_method
        A.public_method
    end

    def call_private_method
        A.private_method
    end
end

動作を確認してみます。

A.methods(false)           # => [:public_method, :call_public_method, :call_private_method]
A.private_methods(false)   # => [:private_method, :inherited, :initialize]

確かに、クラスメソッド private_method がプライベートと認識されています。 続いて、クラスA, クラスB のクラスメソッドから呼び出してみます。

A.call_public_method       # => "cataed self.public_method."
A.call_private_method      # => "called self.private_method."
B.call_public_method       # => "called self.public_method."
B.call_private_method      # => NoMethodError (private method `private_method' called for A:Class)

クラス A からはプライベートメソッドが呼び出せて、クラス B からはクラス A のプライベートメソッドは呼び出せていません。さらに、B のインスタンスから呼び出せるか、を確認します。

obj = B.new
obj.call_public_method     # => "called self.public_method."
obj.call_private_method    # => NoMethodError (private method `private_method' called for A:Class)

クラス B のインスタンスからも、クラス A のプライベートメソッドの呼び出しができなくなっています。

特異クラスを開いて定義する場合

特異クラスをオープンし、特異クラスのインスタンスメソッドとしてクラスメソッドを定義する方法です。この場合は、インスタンスメソッドの定義方法と同じように、 protectedprivate の後にメソッド定義をすることで、protected, private の定義ができます。

class A
    # クラス A の特異クラスをオープンする。
    class << self
        def public_method
            p "called public_method."
        end

        protected       # ここ以降は protected なメソッド

        def protected_method
            p "called protected_method."
        end

        private         # ここ以降は private なメソッド

        def private_method
            p "called private_method"
        end
    end     # 特異クラスはここで終わり

    # 各メソッドを呼び出すためのクラスメソッド
    def self.call_public_method
        A.public_method
    end

    def self.call_protected_method
        A.protected_method
    end

    def self.call_private_method
        private_method     # レシーバーはつけてはいけない
    end

    # クラス A のインスタンスから呼び出せるか?
    def call_public_method
        A.public_method
    end

    def call_protected_method
        A.protected_method
    end
    
    def call_private_method
        A.private_method
    end
end

# クラス A を継承するクラス B
class B < A
    # 継承されたクラス B から、クラス A のメソッドを呼び出してみる。
    def self.call_public_method
        A.public_method
    end

    def self.call_protected_method
        A.protected_method
    end

    def self.call_private_method
        A.private_method
    end

    # クラス B のインスタンスから呼び出せるか?
    def call_public_method
        A.public_method
    end

    def call_protected_method
        A.protected_method
    end
    
    def call_private_method
        A.private_method
    end
end

同じように確認してみます。まず、メソッド一覧を確認します。

A.methods(false)            # => [:call_private_method, :public_method, :protected_method, :call_public_method, :call_protected_method]
A.private_methods(false)    # => [:private_method, :inherited, :initialize]
A.protected_methods(false)  # => [:protected_method]

private_methodprotected_method が、ちゃんと認識されています。 続けて、クラスメソッドからの呼び出しです。

A.call_public_method        # => "called public_method."
A.call_protected_method     # => "called protected_method."
A.call_private_method       # => "called private_method"

B.call_public_method        # => "called public_method."
B.call_protected_method     # => "called protected_method."
B.call_private_method       # => NoMethodError (private method `private_method' called for A:Class)

クラス A を継承しているクラス B からは、protected は呼び出せていますが、private は呼び出せません。 さらに、クラス A と無関係なクラス C から、クラス A のprotected_method が呼び出せるか?

class C
    def self.call_protected_method                                              
        A.protected_method
    end
end

C.call_protected_method      # => NoMethodError (protected method `protected_method' called for A:Class)

たしかに、継承関係にないクラス C からは、protected_method は呼び出せていません。

また、インスタンスメソッドからの呼び出しですが、同じクラスのインスタンスでも、継承されたクラスのインスタンスでも、protected なクラスメソッドは呼び出せません。

obj_A = A.new
obj_A.call_public_method        # => "called public_method."
obj_A.call_protected_method     # => NoMethodError (protected method `protected_method' called for A:Class)
obj_A.call_private_method       # => NoMethodError (private method `private_method' called for A:Class)

obj_B = B.new
obj_B.call_public_method        # => "called public_method."
obj_B.call_protected_method     # => NoMethodError (protected method `protected_method' called for A:Class)
obj_B.call_private_method       # => NoMethodError (private method `private_method' called for A:Class)                                        

ちょっと意外です。直観的には呼び出せそうな感じがしてたんですが。。

どうやって使い分けるか?

外から呼び出す必要のあるものは、デフォルトの public で定義し、クラス内の内部処理でしか使えない、外から呼び出されるとかえって問題になるような場合は private にする、という使い分けだと思います。

protected に関しては、特異クラスで定義することはできますが、使わない方法を考える方が良いような感じです。( protected_class_method で定義できないので、使う想定をあまりされてないんじゃないかなーと。)

その3のまとめ

  • クラスメソッドもインスタンスメソッドと同様に、public, protected, private の3種類の定義ができる。
  • 通常の定義では public となる。
  • private にするには、private_class_method :[クラスメソッド名] とするか、特異メソッド内で定義する。
  • protectedにするには、特異メソッド内で定義する方法しかない。
  • クラスメソッドの protected は、そのクラスか、継承されたクラスの「クラスメソッド」からしか呼び出せない。同じクラスでも、インスタンスメソッドからは呼び出せない。