Tuesday, December 15, 2009

Recipe 8.6. Validating and Modifying Attribute Values










Recipe 8.6. Validating and Modifying Attribute Values









Problem


You want to let outside code set your objects'
instance variables, but you also want to impose some control over the values your variables are set to. You might want a chance to validate new values before accepting them. Or you might want to accept values in a form convenient to the caller, but transform them into a different form for internal storage.




Solution


Define your own setter method for each instance variable you want to control. The setter method for an instance variable quantity would be called quantity=. When a user issues a statement like object.quantity = 10, the method object#quantity= is called with the argument 10.


It's up to the quantity= method to decide whether the instance variable quantity should actually take the value 10. A setter method is free to raise an ArgumentException if it's passed an invalid value. It may also modify the provided value, massaging it into the canonical form used by the class. If it can get an acceptable value, its last act should be to modify the instance variable.


I'll define a class that keeps track of peoples' first and last names. It uses setter
methods to enforce two somewhat parochial rules: everyone must have both a first and a last name, and everyone's first name must begin with a capital letter:



class Name

# Define default getter methods, but not
setter methods.
attr_reader :first, :last

# When someone tries to set a first name, enforce rules about it.
def first=(first)
if first == nil or first.size == 0
raise ArgumentError.new('Everyone must have a first name.')
end
first = first.dup
first[0] = first[0].chr.capitalize
@first = first
end
# When someone tries to set a last name, enforce rules about it.
def last=(last)
if last == nil or last.size == 0
raise ArgumentError.new('Everyone must have a last name.')
end
@last = last
end

def full_name
"#{@first} #{@last}"
end

# Delegate to the setter methods instead of setting the instance
# variables directly.
def initialize(first, last)
self.first = first
self.last = last
end
end



I've written the Name class so that the rules are enforced both in the constructor and after the object has been created:



jacob = Name.new('Jacob', 'Berendes')
jacob.first = 'Mary Sue'
jacob.full_name # => "Mary Sue Berendes"

john = Name.new('john', 'von Neumann')
john.full_name # => "John von Neumann"
john.first = 'john'
john.first # => "John"
john.first = nil
# ArgumentError: Everyone must have a first name.

Name.new('Kero, international football star and performance artist', nil)
# ArgumentError: Everyone must have a last name.





Discussion


Ruby never lets one object
access another object's instance variables. All you can do is call methods. Ruby simulates instance variable access by making it easy to define getter and setter methods whose names are based on the names of instance variables. When you access object.my_var, you're actually calling a method called my_var, which (by default) just happens to return a reference to the instance variable my_var.


Similarly, when you set a new value for object.my_var, you're actually passing that value into a setter method called my_var=. That method might go ahead and stick your new value into the instance variable my_var. It might accept your value, but silently clean it up, convert it to another format, or otherwise modify it. It might be picky and reject your value altogether by raising an ArgumentError.


When you're defining a class, you can have Ruby generate a setter method for one of your instance variables by calling Module#atttr_writer or Module#attr_accessor on the symbol for that variable. This saves you from having to write code, but the default setter method lets anyone set the instance variable to any value at all:



class SimpleContainer
attr_accessor :value
end

c = SimpleContainer.new

c.respond_to? "value=" # => true

c.value = 10; c.value # => 10

c.value = "some random value"; c.value # => "some random value"

c.value = [nil, nil, nil]; c.value # => [nil, nil, nil]



A lot of the time, this kind of informality is just fine. But sometimes you don't trust the data coming in through the setter methods. That's when you can define your own methods to stop bad data before it infects your objects.


Within a class, you have direct access to the instance variables. You can simply assign to an instance variable and the setter method won't be triggered. If you do want to trigger the setter method, you'll have to call it explicitly. Note how, in the Name#initialize method above, I call the first= and last= methods instead of assigning to @first and @last. This makes sure the validation code gets run for the initial values of every Name object. I can't just say first = first, because first is a variable name in that method.




See Also


  • Recipe 8.1, "Managing Instance Data"

  • Recipe 13.14, "
    Validating Data with ActiveRecord"













No comments: