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"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net