Search code examples
rubyinheritancesubclassprivatesuperclass

Can I call a subclass' private method from its parent class?


After reading about Ruby's access controls, I understand that a private method may only be called, implicitly, from within a class and within that class' subclasses. I have an example, though, where a class seems to be calling a private method default_chain on its subclasses, and it still works. Check out the following code (adapted from Sandi Metz' Practical Object-Oriented Design in Ruby):

class Bicycle
    attr_reader :chain

    def initialize(args={})
        @chain = args[:chain] || default_chain
    end

    def parts
        {
            chain: chain
        }
    end
end

class RoadBike < Bicycle
    def parts
        super.merge(
            handlebar_tape_color: "red"
        )
    end

    private

    def default_chain
        "21-speed"
    end
end

class MountainBike < Bicycle
    def parts
        super.merge(
            suspension: "Manitou Mezzer Pro"
        )
    end

    private

    def default_chain
        "10-speed"
    end
end


RoadBike.new.parts  # {:chain=>"21-speed", :handlebar_tape_color=>"red"}
MountainBike.new.parts  # {:chain=>"10-speed", :suspension=>"Manitou Mezzer Pro"}

What's going on?


Solution

  • You're getting it wrong - in your example, there is no such a thing as the parent class calling children methods.

    Methods/constants name lookup in Ruby always works "bottom up": first we check if the method is defined in object's class, then in object's class's superclass and so on (this is a huge simplification because Ruby's object model is more complicated, more on this later). So, in your example things happen in roughly the following order:

    1. When you call RoadBike.new runtime checks if there is an initialize methods defined for the class RoadBike. There is no, so we use the implementation defined for its parent class - Bycicle (but the execution context stays the same - it is still RoadBike instance)

    2. When executing Bycicle#initialize runtime encounters another method call - default_chain. At this moment we start method name resolving in the very same manner - starting from the RoadBike context. Does RoadBike have its own implementation of default_chain? Yes, it does, so we simply call it.

    The following baby example makes it crystal clear, hopefully:

    class Parent
      def initialize
        puts "Parent Initializer is called"
        a
        b
      end
    
      def a
        puts "Parent a is called"
      end
    
      def b
        puts "Parent b is called"
      end
    end
    
    class Child < Parent
      def b
        puts "Child b is called"
      end
    end
    
    pry(main)> Child.new
    Parent Initializer is called
    Parent a is called
    Child b is called
    

    In reality the methods/constants resolution machinery is more complicated(includes so-called singleton classes). This is a bigger topic that will not fit nicely in SO answer, so I strongly recommend reading "Metaprogramming Ruby 2" by Paolo Perotta where this model is wery well explained in great details from the very practical point of view.