- 9.1 Scopes
- 9.2 Callbacks
- 9.3 Calculation Methods
- 9.4 Single-Table Inheritance (STI)
- 9.5 Abstract Base Model Classes
- 9.6 Polymorphic has_many Relationships
- 9.7 Enums
- 9.8 Foreign-Key Constraints
- 9.9 Modules for Reusing Common Behavior
- 9.10 Modifying Active Record Classes at Runtime
- 9.11 Using Value Objects
- 9.12 Nonpersisted Models
- 9.13 PostgreSQL Enhancements
- 9.14 Conclusion
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
5
6 def address=(address)
7 self[:address_city] = address.city
8 self[:address_state] = address.state
9
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
3
4 def initialize(city, state)
5 @city, @state = city, state
6 end
7
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.
9.11.1.1 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
5
6 def cost=(cost)
7 self[:cents] = cost.cents
8 self[:currency] = cost.currency.to_s
9
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"