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:
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...