PawkyPenguin
PawkyPenguin

Reputation: 250

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?

Upvotes: 1

Views: 5460

Answers (3)

Daniel Garmoshka
Daniel Garmoshka

Reputation: 6352

With experimental patch you can do:

class Zaloop

  attr_accessor var1: :default_value, var2: 2

  def initialize
    self.initialize_default_values
  end

end

puts Zaloop.new.var1 # :default_value
  • Please be aware, patch below - is experimental solution, be careful if you decide to use it on production

Patch for module:

Module.module_eval do

  alias _original_attr_accessor attr_accessor
  def attr_accessor(*args)
    @default_values ||= {}
    attr_names = []
    args.map do |arg|
      if arg.is_a? Hash
        arg.each do |key, value|
          define_default_initializer if @default_values.empty?
          @default_values[key] = value
          attr_names << key
        end
      else
        attr_names << arg
      end
    end
    _original_attr_accessor *attr_names
  end

  def define_default_initializer
    default_values = @default_values
    self.send :define_method, :initialize_default_values do
      default_values.each do |key, value|
        instance_variable_set("@#{key}".to_sym, value)
      end
    end
  end

end

Upvotes: 0

Ryan Flach
Ryan Flach

Reputation: 66

In your final example:

class Sheep < Animal
  attr_accessor :likes

  def likes
    @likes || @likes = []
  end
end

you're essentially using memoization, although your syntax is a little different from the norm, which would look like:

def likes
  @likes ||= []
end

Additionally, because you have likes now as a memoized method, and not an attribute of the instance, you do not need attr_accessor (or attr_reader, etc.).

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

And you're good to go.

Edit: Per your concern of performance:

[1] pry(main)> require 'benchmark'
=> true
[2] pry(main)> @hello = []
=> []
[3] pry(main)> def hello
[3] pry(main)*   @hello
[3] pry(main)* end
=> :hello
[4] pry(main)> def likes
[4] pry(main)*   @likes ||= []
[4] pry(main)* end
=> :likes
[5] pry(main)> puts Benchmark.measure { 1_000_000.times { hello } }
  0.070000   0.000000   0.070000 (  0.071330)
=> nil
[6] pry(main)> puts Benchmark.measure { 1_000_000.times { likes } }
  0.100000   0.000000   0.100000 (  0.097388)
=> nil 

Upvotes: 2

Simple Lime
Simple Lime

Reputation: 11050

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.

Upvotes: 1

Related Questions