Saturday, October 24, 2009

Recipe 8.1. Managing Instance Data










Recipe 8.1. Managing Instance Data





Problem


You want to associate a variable with an object. You may also want the variable to be readable or writable from outside the object.




Solution


Within the code for the object's class, define a variable and prefix its name with an at sign (
@). When an object runs the code, a variable by that name will be stored within the object.


An
instance of the Frog class defined below might eventually have two instance
variables stored within it, @name and @speaks_english:



class Frog
def initialize(name)
@name = name
end

def speak
# It's a well-known fact that only frogs with long names start out
# speaking English.
@speaks_english ||= @name.size > 6
@speaks_english ? "Hi. I'm #{@name}, the talking frog." : 'Ribbit.'
end
end

Frog.new('Leonard').speak # => "Hi. I'm Leonard, the talking frog."

lucas = Frog.new('Lucas')
lucas.speak # => "Ribbit."



If you want to make an instance variable
readable from outside the object, call the
attr_reader
method on its symbol:



lucas.name
# NoMethodError: undefined method 'name' for #<Frog:0xb7d0327c @speaks_english=true,
@name="Lucas">

class Frog

attr_reader :name
end
lucas.name # => "Lucas"



Similarly, to make an instance variable readable and
writable
from outside the object, call the
attr_accessor
method on its symbol:



lucas.speaks_english = false
# => NoMethodError: undefined method 'speaks_english=' for #<Frog:0xb7d0327c @speaks_
# english=false, @name="Lucas">

class Frog

attr_accessor :speaks_english
end
lucas.speaks_english = true
lucas.speak # => "Hi. I'm Lucas, the talking frog."





Discussion


Some programming languages have complex rules about when one object can directly access to another object's
instance
variables. Ruby has one simple rule: it's never allowed. To get or set the value of an instance variable from outside the object that owns it, you need to call an explicitly defined
getter or setter
method.


Basic getter and
setter methods look like this:



class Frog
def speaks_english
@speaks_english
end

def speaks_english=(value)
@speaks_english = value
end
end



But it's boring and error-prone to write that yourself, so Ruby provides built-in
decorator
methods like
Module#attr_reader
and
Module#attr_accessor
. These methods use metaprogramming to generate custom getter and
setter methods for your class. Calling attr_reader :speaks_english generates the getter method speaks_english and attaches it to your class. Calling attr_accessor :instance_variable generates both the getter method speaks_english and the setter method speaks_english=.


There's also an attr_writer decorator method, which only generates a setter method, but you won't use it very often. It doesn't usually make sense for an instance variable to be writable from the outside, but not readable. You'll probably use it only when you plan to write your own custom getter method instead of generating one.


Another slight difference between Ruby and some other programming languages: in Ruby, instance
variables (just like other variables) don't exist until they're defined. Below, note how the @speaks_english variable isn't defined until the Frog#speak method gets called:



michael = Frog.new("Michael")
# => #<Frog:0xb7cf14c8 @name="Michael">
michael.speak # => "Hi. I'm Michael, the talking frog."
michael
# => #<Frog:0xb7cf14c8 @name="Michael", @speaks_english=true>



It's possible that one Frog object would have the @speaks_english instance variable set while another one would not. If you call a getter method for an instance variable that's not defined, you'll get nil. If this behavior is a problem, write an initialize that initializes all your instance variables.


Given the symbol for an instance variable, you can retrieve the value with
Object#instance_variable_get
, and set it with
Object#instance_variable_set
.


Because this method ignores encapsulation, you should only use it in within the class itself: say, within a call to Module#define_method.


This use of instance_variable_get violates encapsulation, since we're calling it from outside the Frog class:



michael.instance_variable_get("@name") # => "Michael"
michael.instance_variable_set("@name", 'Bob')
michael.name # => "Bob"



This use doesn't violate encapsulation (though there's no real need to call define_method here):



class Frog
define_method(:scientific_name) do
species = 'vulgaris'
species = 'loquacious' if instance_variable_get('@speaks_english')
"Rana #{species}"
end
end
michael.scientific_name # => "Rana loquacious"





See Also


  • Recipe 10.10, "Avoiding Boilerplate Code with Metaprogramming"













No comments:

Post a Comment