Value objects express “‘what’ something is rather than ‘who’ or ‘which’ it is.” In other words, values lack identity.
For example, the number 10 or the color red - all instances of 10 are conceptually equal to all other instances of 10, and likewise, red is always red. Two red bikes, however, have distinct identities.
We’ll explore how extracting value objects can simplify the challenge of bringing the real-world to bear as software. Then, we’ll touch on some strategies for integrating value objects with everyone’s favorite ORM, ActiveRecord.
28. VALUE OBJECTS
Immutable
Easier to reason about
Easier to test
Easier to share
Enable easier method and class naming
Make systems more robust
Enrich semantics
34. class Person
# ...
COMMON_NAMES = %w( Ben Eric Jane )
def name_common?
COMMON_NAMES.include?(first_name)
end
def first_name
name.split(" ").first
end
end
40. class Name
def initialize(name)
@name = name
end
def merge_with(other_name, patronymic: true); end
def common?; end
def similiar_to?(other_name); end
def mononym?; end
def ==(other_name); end
protected
attr_reader :name
end
41. class Name
def first_and_middle
# ...
end
def last
# ...
end
def merge_with(other_name)
Name.new [first_and_middle, other_name.last].join(" ")
end
end
58. class Payment
# ...
def charge
# Add some card number validations
PaymentProcessor.charge(card_number)
end
end
59. class CreditCardNumber
def initialize(number)
# ...
end
def issuer
# ...
end
def valid?
# validations
end
end
class Payment
def credit_card_number
CreditCardNumber.new(@card_number)
end
end
64. class CreditCardNumber
def self.coerce(number)
return number if number.is_a?(CreditCardNumber)
new(number.to_s.gsub(/D/, ''))
end
end
class Payment
def credit_card_number
CreditCardNumber.coerce(@card_number)
end
end
79. query = "SELECT * FROM users"
query += " WHERE last_name = 'Williams'"
query = SqlQuery.new("SELECT * FROM users")
query = query.where(last_name: "Williams")
80. THE VIRTUES OF VALUES
Make the implicit explicit
Segregate domain generality from specificity
Segregate imperative code from functional code
Easy and safe to share
Simplify transactions
Few dependencies
Easier to test
83. READING/WRITING
class Payment < ActiveRecord::Base
def credit_card_number
CreditCardNumber.new(super)
end
def credit_card_number=(number)
super CreditCardNumber.coerce(number).to_s
end
end
84. READING/WRITING
class Person < ActiveRecord::Base
def address
Address.new({
street: street,
city: city,
state: state
})
end
def address=(attributes)
Address.coerce(attributes).tap do |address|
self.street = address.street
self.city = address.city
self.state = address.state
end
end
end
85. FORMS
<%= form_for @person do |f| %>
<%= f.fields_for :address, f.object.address do |ff| %>
<%= ff.input :street %>
<%= ff.input :city %>
<%= ff.input :statee %>
<% end %>
<% end %>
{
person: {
address: {
street: "123 Main",
city: "Boulder",
state: "CO"
}
}
}
87. QUERYING
class Payment < ActiveRecord::Base
def self.with_credit_card_number(number)
where(card_number: number)
end
end
number = CreditCardNumber.new("4012888888881881")
Payment.with_credit_card_number(number)
# => TypeError: Cannot visit CreditCardNumber
88. QUERYING
class Payment < ActiveRecord::Base
def self.with_credit_card_number(number)
where(card_number: number.to_s)
end
end
number = CreditCardNumber.new("4012888888881881")
Payment.with_credit_card_number(number)
89. QUERYING
handler = proc do |column, credit_card_number|
column.eq(credit_card_number.to_s)
end
ActiveRecord::PredicateBuilder.register_handler(
CreditCardNumber, handler)
class Payment < ActiveRecord::Base
def self.with_credit_card_number(number)
where(card_number: number)
end
end
number = CreditCardNumber.new("4012888888881881")
Payment.with_credit_card_number(number)