Search code examples
rubyinheritanceinstance-variables

Specify default values for instance variables outside constructor


My goal is to initialize an instance variable without making use of the initialize method. I have this code:

class Animal
  attr_reader :age
  def initialize(age)
    @age = age
  end
end

class Sheep < Animal
  attr_accessor :likes
  def initialize(age)
    super
    @likes = []
  end
end

sheep = Sheep.new(5)
sheep.likes << "grass"

The initialize method in this subclass calls super. This doesn't scale very nicely: If I change the signature of the superclass, I have to adjust it in all subclasses as well.

It would be nicer if I can initialize an instance variable like @likes = [] outside of the initialize method within the class scope of Sheep, like many other OO-languages can. However, that would make my variable an instance variable of the class object.

Here's a way I discovered that doesn't override the constructor:

class Sheep < Animal
  attr_accessor :likes
  def likes
    @likes || @likes = []
  end
end

That's much more elegant because readjusting signatures is not necessary, but it's still not perfect: Wouldn't Ruby check for non-nil-ness of likes when I access that instance variable? Is there a way to do this without sacrificing runtime or code elegance?


Solution

  • One thing you can do is call a method from the initialize of Animal, providing a hook for subclasses to add custom functionality:

    class Animal
      attr_reader :age
      def initialize(age)
        @age = age
    
        setup_defaults
      end
    
      private
      def setup_defaults
        # NOOP by default
      end
    end
    
    class Sheep < Animal
      attr_accessor :likes
    
      private
      def setup_defaults
        @likes = []
      end
    end
    

    A second way, that you mention in your post, you can do this is use a custom def likes instead of the attr_reader/attr_accessor:

    def likes
      @likes ||= [] # shorter way of doing what you have
    end
    

    As a third option, if you don't mind using initialize (your primary concern seems to be possibly changing the superclass' signature), since you don't care about any the parameters to initializeSheep is you can overwrite the initialize like:

    class Sheep < Animal
      attr_accessor :likes
      def initialize(*)
        super
        @likes = []
      end
    end
    

    this is the same as doing something like def initialize(*args) except you don't name the variable, and works since super passes in the original arguments by default. Now, if you go back and change animal to have, say, a name argument to its initialize:

    class Animal
      attr_reader :age, :name
      def initialize(name, age)
        @name = name
        @age = age
      end
    end
    

    Sheep still works without any changes.