joestelmach.com

Value Object Aggregation in Rails

Aggregation is an important concept that should arguably be applied to any domain of reasonable complexity. While this statement is certainly open for discussion, I feel that it's important to study what aggregation is and discover the benefits of making a distinction between true Entities and things that are just Value Objects. An Entity can be defined most simply as something that needs to maintain it's own state, whereas a Value Object is just a certain value (or collection of values) that has no state. Consider the following portion of a domain model:

example domain model

In the example shown, both distributor and customer have an address. I would consider the address to be a Value Object since it has no state. Please realize that there may be some application domains where an address should be modeled as an entity, I just don't feel that this domain is one of them. Another way to think about this is that the address has no identity outside of the context of either a distributor or a customer. Therefore, we refer to distributor and customer as Root Entities. A value object should not be accessible without first going through a root entity. So, theoretically speaking, you should never be able to write the following code:

address = Address.new
address.save

The problem with the above code is that it refers to the address as it's own entity, which we said is probably not a good idea. We should only allow access to a value object through an owning entity like so:

address = Address.new
customer = Customer.new(:address => address)
customer.save

The difference here is that we are assigning the address to a customer and then saving the customer. So again, the address only makes sense within the context of a customer (or distributor) object. There is another subtle point going on here. Since an address has no state of its own, we should never be allowed to modify the address object - the address should be Immutable. This means that if the customers address changes, we would need to create a new address object and associate it with the customer and re-save the customer. Calling customer.address.zip = 08080 doesn't make sense (from a pedagogical point of view anyways.)

OK, enough theory - lets talk about implementation. Active Record has direct support for aggregation through the use of the composed_of keyword. Lets assume the following migrations are used to create the Customer and Distributor tables:

class CreateCustomers < ActiveRecord::Migration
  def self.up
    create_table :customers do |t|
      t.column :customer_name, :string
      t.column :phone_number, :string
      t.column :email, :string
      t.column :address_1, :string
      t.column :address_2, :string
      t.column :city, :string
      t.column :state, :string, :limit => 2
      t.column :zip, :string, :limit => 10
    end
  end

  def self.down
    drop_table :customers
  end
end

class CreateDistributors < ActiveRecord::Migration
  def self.up
    create_table :distributors do |t|
      t.column :distributor_name, :string
      t.column :distributor_region, :string
      t.column :phone_number, :string
      t.column :email, :string
      t.column :address_1, :string
      t.column :address_2, :string
      t.column :city, :string
      t.column :state, :string, :limit => 2
      t.column :zip, :string, :limit => 10
    end
  end

  def self.down
    drop_table :distributors
  end
end

Now, what we'd like to do is abstract the address columns into their own Address class. This will allow us to say things like customer.address.address_1 after Active Record has retrieved a collection of Customer objects for us. To further clarify, the following code should not run once we're done: distributor.address_1. The real benefit here is not just the syntactic sugar of adding 'address' to the reader method, rather we can now implement certain address related functionality (like calculate_distance,) and have that functionality be shared among multiple root entities of address objects.

First, we need to tell the Distributor and Customer classes that they should be composed of an Address:

class Customer < ActiveRecord::Base
  composed_of :address,
    :class_name => "Address",
    :mapping => [
      [:address_1, :address_1],
      [:address_2, :address_2],
      [:city, :city],
      [:state, :state],
      [:zip, :zip]]
end
class Distributor < ActiveRecord::Base
  composed_of :address,
    :class_name => "Address",
    :mapping => [
      [:address_1, :address_1],
      [:address_2, :address_2],
      [:city, :city],
      [:state, :state],
      [:zip, :zip]]
end

In the above two class definitions, the mapping and class_name attributes are optional. The class_name attribute isn't needed if the class name is the camel-cased version of the composed_of value. Additionally, the mapping attribute is not needed if each value pair contains identical values (as they do in this case.)

Now we can go ahead and create our Address class:

class Address
  attr_reader :address_1, :address_2, :city, :state, :zip
    
  def initialize(address_1, address_2, city, state, zip)
    @address_1 = address_1
    @address_2 = address_2
    @city = city
    @state = state
    @zip = zip
  end
end

Since Address objects will be immutable, we create only attribute reader methods. The attributes can only be set at initialization time. Rails further enforces immutability by freezing the value object once it has been associated with an entity.

So now that I've shown you how to implement such a thing, let's talk about the abundant shortcomings of this.

Duplication:

As you can see, there is a fair amount of duplication going on here. Not only are we re-defining all of the address attributes in each table that will be composed of an address, but we are also duplicating the mapping and class_name values in both the Customer and Distributor classes.

Validation

This is the deal breaker. Value objects in rails are completely dis-associated from ActiveRecord, which means they cannot use the wonderful validation framework provided. To be quite honest, this pisses me off. I believe this will make value objects in rails unusable in all cases where the value objects don't contain reference data. Yes, I'm aware of workarounds for this problem, like tricking the value object into thinking it's being handled by ActiveRecord, but to be honest with you, its not quite worth the hassle. I'd much sooner just make the value object an entity of it's own and lose the theoretical correctness of not maintaining the state of the object. But wait, that would bring up another problem: How do I associate an entity with more than one kind of other entity? The answer lies in one beautiful addition to Rails 1.1: Polymorphic Associations...

Comments
blog comments powered by Disqus