VALUES
Ben Eddy
@ClashingPlaids
ROADMAP
Why our brains like object-orientation
Examples of value objects
ActiveRecord tips
RUBY IS AN OBJECT-ORIENTED
PROGRAMMING LANGUAGE
WHAT IS AN OBJECT?
OBJECTS ARE USEFUL
→
class Car
# car stuff
end
class ShippingContainer
# container stuff
end
class Person
# person stuff
end
OBJECTS HAVE ATTRIBUTES
class Person
attr_accessor :age
end
person = Person.new
person.age = 0
while true do
sleep 31536000
person.age += 1
end
person_1 = Person.new
person_1.age = 40
person_2 = Person.new
person_2.age = 40
hostage = Person.new
hostage.age = 40
captor = Person.new
captor.age = 40
OBJECTS HAVE IDENTITY
hostage = Person.new
hostage.age = 40
captor = Person.new
captor.age = 40
hostage == captor
# => false
hostage = Person.new
hostage.age = 40
captor = Person.new
captor.age = 40
hostage.object_id
# => 70122276807860
captor.object_id
# => 70122276712780
IDENTITY IS IMPORTANT
EXCEPT WHEN IT ISN'T
string_1 = "ABC"
string_2 = "ABC"
string_1 = "ABC"
string_2 = "ABC"
string_1.object_id
# => 70122276804530
string_2.object_id
# => 70122276104871
string_1 == string_2
# => true
string_1 = "ABC"
string_2 = "ABC"
string_1.==(string_2)
# => true
IDENTITY VS. VALUE
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
class Person
end
class Person
attr_accessor :name
end
alice = Person.new
alice.name = "Alice Cranston"
class Person
def take_last_name(last_name)
self.name = first_and_middle_names.push(last_name).join(" ")
end
def first_and_middle_names
name.split(" ")[0..-2]
end
end
NAMING IS HARD
Avoid names which are too common
Avoid infamous names
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
class Person
def name_similiar_to?(other_name)
FuzzyMatch.matches?(name, other_name)
end
end
dictators = ["Adolf Hitler", "Genghis Khan", "Josef Stalin"]
person = Person.new
["Eric Jones", "Mingus Kan", "Timion Lowry"].reject do |name|
person.name = name
person.name_common? ||
dictators.any? { |dictator| person.name_similiar_to?(dictator) }
end
class Person
COMMON_NAMES = []
def take_last_name
# ..
end
def name_common?
# ...
end
def name_similiar_to?(other_name)
# ...
end
end
class Name
def initialize(@name)
@name = name
end
def ==(other_name)
name == other_name.name
end
protected
attr_reader :name
end
Name.new("Ben Eddy") == Name.new("Ben Eddy")
# => true
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
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
husband = Person.new
husband.name = Name.new("Melvin Cartwright")
wife = Person.new
wife.name = Name.new("Alice Cranston")
# marriage
wife.name = wife.name.merge_with(husband.name)
VALUES ARE IMMUTABLE
dictators = [
Name.new("Adolf Hitler"),
Name.new("Genghis Khan"),
Name.new("Josef Stalin")
]
[
Name.new("Eric Jones"),
Name.new("Mingus Kan"),
Name.new("Timion Lowry")
].reject do |name|
name.common? || dictators.any?(&name.method(:similar_to?))
end
name.common?
name.similar_to?(other_name)
name.merge_with(other_name)
MAKING THE IMPLICIT EXPLICIT
DRAW ON ESTABLISHED
FORMALISMS
NOUN AN·THRO·PON·Y·MY
SPECIFIC
GENERAL
SPECIFIC
GENERAL
class Payment
attr_reader :card_number
def initialize(card_number)
@card_number = card_number
end
end
class Payment
# ...
def charge
PaymentProcessor.charge(card_number)
end
end
4321 5678 9012 1234
4321 5678 9012 1234
4321 5678 9012 1234
class Payment
# ...
def charge
# Add some card number validations
PaymentProcessor.charge(card_number)
end
end
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
TEST IN ISOLATION
4321 5678 9012 1234
4321-5678-9012-1234
4321567890121234
Be conservative in what you do, be liberal
in what you accept from others.
class Payment
def credit_card_number
CreditCardNumber.new(@card_number.gsub(/D/, ''))
end
end
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
COERCE AT THE BOUNDARY
COERCE AT THE BOUNDARY
Money.coerce(10)
Money.coerce(10.0)
Money.coerce("10")
Money.coerce("$10.00")
Money.coerce(:vegetables)
# => TypeError
payments = [
Payment.new(CreditCardNumber.new("371449635398431")),
Payment.new(CreditCardNumber.new("4012888888881881")),
Payment.new(CreditCardNumber.new("4012888888881881"))
]
payments.group_by(&:credit_card_number)
payments.group_by(&:credit_card_number)
# => {
CreditCardNumber.new("371449635398431") => [payment]
CreditCardNumber.new("4012888888881881") => [payment]
CreditCardNumber.new("4012888888881881") => [payment]
}
class CreditCardNumber
def eql?(other)
number == other.number
end
end
payments.group_by(&:credit_card_number)
# => {
CreditCardNumber.new("371449635398431") => [payment]
CreditCardNumber.new("4012888888881881") => [payment, payment]
}
payments = [
Payment.new(CreditCardNumber.new("371449635398431")),
Payment.new(CreditCardNumber.new("4012888888881881")),
Payment.new(CreditCardNumber.new("4012888888881881"))
]
grouped = payments.group_by(&:credit_card_number)
# => {
CreditCardNumber.new("371449635398431") => [payment]
CreditCardNumber.new("4012888888881881") => [payment, payment]
}
grouped[CreditCardNumber.new("4012888888881881")]
# => nil
class CreditCardNumber
def hash
number.hash
end
end
payments = [
Payment.new(CreditCardNumber.new("371449635398431")),
Payment.new(CreditCardNumber.new("4012888888881881")),
Payment.new(CreditCardNumber.new("4012888888881881"))
]
grouped = payments.group_by(&:credit_card_number)
# => {
CreditCardNumber.new("371449635398431") => [payment]
CreditCardNumber.new("4012888888881881") => [payment, payment]
}
grouped[CreditCardNumber.new("4012888888881881")]
# => [payment, payment]
RECAP
==
eql?
hash
# optionally
<=>
class User
def active?
status == "invited" || status == "email_confirmed"
end
end
class User
def active?
status.active?
end
def status
Status.new(@status)
end
end
dangerous_symptoms = Symptoms.new(
Symptom.new(:coughing),
Symptom.new(:fever)
)
patients.each do |patient|
if patient.symptoms.match?(dangerous_symptoms)
raise "OUTBREAK DETECTED"
end
end
duration_1 = Duration.new(60)
duration_2 = Duration.from_minutes(1)
duration_1 == duration_2
# => true
duration_1.longer_than?(Duration.new(120))
# => false
query = "SELECT * FROM users"
query += " WHERE last_name = 'Williams'"
query = SqlQuery.new("SELECT * FROM users")
query = query.where(last_name: "Williams")
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
ACTIVERECORD AND VALUE
OBJECTS
DATABASES CAN ONLY STORE
PRIMITIVES
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
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
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"
}
}
}
FORMS
<%= simple_form_for @person do |f| %>
<%= f.input :address, as: :address %>
<% end %>
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
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)
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)
GETTING STARTED
Wrap primitives
Use internally
Get help
Values
Good
FURTHER READING
Domain Driven Design - Eric Evans
Are We There Yet? - Rich Hickey
Value of Values - Rich Hickey
Boundaries - Gary Bernhardt

Values