Home > Articles > Web Development

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

9.11 Using Value Objects

In domain-driven design14 (DDD), there is a distinction between Entity Objects and Value Objects. All model objects that inherit from ActiveRecord::Base could be considered Entity Objects in DDD. An Entity Object cares about identity, since each one is unique. In Active Record, uniqueness is derived from the primary key. Comparing two different Entity Objects for equality should always return false, even if all its attributes (other than the primary key) are equivalent.

Here is an example comparing two Active Record addresses:

>> home = Address.create(city: "Brooklyn", state: "NY")
>> office = Address.create(city: "Brooklyn", state: "NY")
>> home == office
=> false

In this case, you are actually creating two new Address records and persisting them to the database; therefore, they have different primary key values.

Value Objects, on the other hand, only care that all their attributes are equal. When creating Value Objects for use with Active Record, you do not inherit from ActiveRecord::Base but instead simply define a standard Ruby object. This is a form of composition called an aggregate in DDD. The attributes of the Value Object are stored in the database together with the parent object, and the standard Ruby object provides a means to interact with those values in a more object-oriented way.

A simple example is of a Person with a single Address. To model this using composition, first we need a Person model with fields for the Address. Create it with the following migration:

1 class CreatePeople < ActiveRecord::Migration
2   def change
3     create_table :people do |t|
4       t.string :name
5       t.string :address_city
6       t.string :address_state
7     end
8   end
9 end

The Person model looks like this:

 1 class Person < ActiveRecord::Base
 2   def address
 3     @address ||= Address.new(address_city, address_state)
 4   end
 6   def address=(address)
 7     self[:address_city] = address.city
 8     self[:address_state] = address.state
10     @address = address
11   end
12 end

We need a corresponding Address object, which looks like this:

 1 class Address
 2   attr_reader :city, :state
 4   def initialize(city, state)
 5     @city, @state = city, state
 6   end
 8   def ==(other_address)
 9     city == other_address.city && state == other_address.state
10   end
11 end

Note that this is just a standard Ruby object that does not inherit from ActiveRecord::Base. We have defined reader methods for our attributes and are assigning them upon initialization. We also have to define our own == method for use in comparisons. Wrapping this all up, we get the following usage:

>> gary = Person.create(name: "Gary")
>> gary.address_city = "Brooklyn"
>> gary.address_state = "NY"
>> gary.address
=> #<Address:0x007fcbfcce0188 @city="Brooklyn", @state="NY">

Alternately you can instantiate the address directly and assign it using the address accessor:

>> gary.address = Address.new("Brooklyn", "NY")
>> gary.address
=> #<Address:0x007fcbfa3b2e78 @city="Brooklyn", @state="NY">

9.11.1 Immutability

It’s also important to treat value objects as immutable. Don’t allow them to be changed after creation. Instead, create a new object instance with the new value instead. Active Record will not persist value objects that have been changed through means other than the writer method on the parent object. The Money Gem

A common approach to using Value Objects is in conjunction with the money gem.15

 1 class Expense < ActiveRecord::Base
 2   def cost
 3     @cost ||= Money.new(cents || 0, currency || Money.default_currency)
 4   end
 6   def cost=(cost)
 7     self[:cents] = cost.cents
 8     self[:currency] = cost.currency.to_s
10     cost
11   end
12 end

Remember to add a migration with the two columns—the integer cents and the string currency that money needs.

1 class CreateExpenses < ActiveRecord::Migration
2   def change
3     create_table :expenses do |t|
4       t.integer :cents
5       t.string :currency
6     end
7   end
8 end

Now when asking for or setting the cost of an item, we would use a Money instance.

>> expense = Expense.create(cost: Money.new(1000, "USD"))
>> cost = expense.cost
>> cost.cents
=> 1000
>> expense.currency
=> "USD"
  • + Share This
  • 🔖 Save To Your Account