Monday, August 06, 2007

Ruby: Lazily Initialized Attributes

Update: This is probably going to be included in Refactoring, Ruby Edition so I took another pass through it following all of your comments. Thanks for the feedback.

Initialize an attribute on access instead of at construction time.

class Employee
def initialize
@emails = []
end
end

==>

class Employee
def emails
@emails ||= []
end
end

Motivation
The motivation for converting attributes to be lazily initialized is for code readability purposes. While the above example is trivial, when the Employee class has multiple attributes that need to be initialized the constructor will need to contain all the initialization logic. Classes that initialize instance variables in the constructor need to worry about both attributes and instance variables.The procedural behavior of initializing each attribute in a constructor is unnecessary and less maintainable than a class that deals exclusivey with attributes. Lazily Initialized Attributes can encapsulate all their initialization logic within the methods themselves.

Mechanics
  • Move the initialization logic to the attribute getter.
  • Test
Example using ||=
The code below is an Employee class with the email attribute initialized in the constructor.

class Employee
attr_reader :emails, :voice_mails

def initialize
@emails = []
@voice_mails = []
end
end

Moving to a Lazily Initialized Attribute generally means moving the initialization logic to the getter method and initializing on the first access.

class Employee
def emails
@emails ||= []
end

def voice_mails
@voice_mails ||= []
end
end

Example using instance_variable_defined?
Using ||= for Lazily Initialized Attributes is a common idiom; however, this idiom falls down when nil or false are valid values for the attribute.

class Employee...
def initialize
@assistant = Employee.find_by_boss_id(self.id)
end

In the above example it's not practical to use an ||= operator for a Lazily Initialized Attribute because the find_by_boss_id might return nil. In the case where nil is returned, each time the assistant attribute is accessed another database trip will occur. A superior solution is to use code similar to the example below that utilizes the instance_variable_defined? method that was introduced in ruby 1.8.6.

class Employee...
def assistant
@assistant = Employee.find_by_boss_id(self.id) unless instance_variable_defined? :@assistant
@assistant
end
Post a Comment