Reputation: 250
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
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
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
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
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