Using ActiveRecord Composed Of

1:02 PM EDT Saturday, April 19 2008

ActiveRecord has a feature called composed_of that allows you to take multiple fields of a model and treat them as one object. Let's say you have a requirement for your application to track the phone number of each user. Also, you need to be able to query for users by parts of the phone number like area code, prefix, line number and extenstion. So the phone number (212) 123-4567 x55 needs to be stored in separate fields as :area_code => 212, :prefix => 123, :line_number => 4567 and :extenstion => 55. So first we create a rails app with a user record with those fields:

$ rails myapp
$ cd myapp
$ script/generate model user name:string \
  phone_number_area_code:integer \
  phone_number_prefix:integer \
  phone_number_line_number:integer \
  phone_number_extension:integer
$ rake db:migrate

What we would like to do is treat all of those phone_number_* columns as one object. So first let's create a base object that has some of the plumbing functionality. Put this in app/models/value_object.rb:

class ValueObject

  class << self
    def has_fields(*args)
      (class << self; self; end).send(:define_method, :fields) do
        args.map(&:to_s)
      end
      fields.each{|f| attr_reader f}
    end
    def mapping
      fields.map{|e| ["#{name.underscore}_#{e}", e]}      
    end
    def mapper
      lambda do |params|
        PhoneNumber.new *PhoneNumber.fields.map{|e| params[e].to_i}
      end
    end
  end

  def initialize(*args)
    args.each_with_index do |a, i|
      instance_variable_set "@#{self.class.fields[i]}", args[i]
    end
  end

  def to_s
    "(#{area_code}) #{prefix}-#{line_number} x#{extension}"
  end

  def ==(o)
    o && self.class.fields.all?{|f| self.send(f) == o.send(f)}
  end

end

And here's our subclass of that specific for the phone number. Add the PhoneNumber class at app/models/phone_number.rb:

class PhoneNumber < ValueObject

  has_fields :area_code, :prefix, :line_number, :extension

  def to_s
    "(#{area_code}) #{prefix}-#{line_number} x#{extension}"
  end

end

Rather than explain what this does, I'll just give you the specs:

require File.dirname(__FILE__) + '/../spec_helper'

describe PhoneNumber do

  before(:each) do
    @phone_number = PhoneNumber.new(212, 123, 4567, 55)
  end  

  it "should have an area_code getter" do
    @phone_number.area_code.should == 212
  end

  it "should have an area_code getter" do
    @phone_number.prefix.should == 123
  end

  it "should have an area_code getter" do
    @phone_number.line_number.should == 4567
  end

  it "should have an area_code getter" do
    @phone_number.extension.should == 55
  end

  describe "#==" do
    it "should be equal if both number match" do
      @phone_number.==(PhoneNumber.new(212, 123, 4567, 55)).should be_true
    end
    it "should not be equal if both numbers do not match" do
      @phone_number.==(PhoneNumber.new(212, 123, 4567, 54)).should be_false
    end
  end

  describe "#to_s" do
    it "should format the phone number as (212) 123-4567 x55" do
      @phone_number.to_s.should == "(212) 123-4567 x55"
    end
  end

end

After you've had a chance to read through that, it should be pretty clear what the PhoneNumber class does. Next we modify the User model to use this class:

class User < ActiveRecord::Base
  composed_of :phone_number, :mapping => PhoneNumber.mapping, &PhoneNumber.mapper
end

The ValueObject base class provides us with the methods mapping and mapper, which are needed to get our User model's phone_number method to do what we want. Here's what we want it to do:

it "should be able to set phone number from hash" do
  @user.phone_number = {"area_code" => "212", "prefix" => "123", 
    "line_number" => "4567", "extension" => "55"}
  @user.phone_number.should == PhoneNumber.new(212, 123, 4567, 55)
end

it "should be able to find a user by area code" do
  @user.phone_number = PhoneNumber.new(212, 123, 4567, 55)
  @user.save
  @user.should == User.find_by_phone_number_area_code(212)
end

Now we can treat our phone number as a single object, but collect and store the values as separate fields.

Posted in  | Tags Rails, ComposedOf, Ruby, ValueObject | 0 Comments