Value objects
A small simple object, like money or a date range, whose equality isn’t based on identity. Martin Fowler
Symbol
, String
, Integer
and Range
are example of value objects.
Value Objects in Ruby on Rails
Notes to this presentation.
March is not 3. 3 is not March. March is March.
March is a range of days, 1st – 31st.
Examples of value objects:
- Date, Time
- Weight, Height
- Duration
- Temperature
- Address
- Money
A value object has:
- Identity based on state
- a dollar is a dollar
- Nov 5 is Nov 5
- 98.6F is 98.6F
- 10 Downing Street is 10 Downing Street
- Equality based on type and state
- 98.6F != $98.60
They are immutable – or should be treated as such – and typically do not have setters.
Scalar values example
class Patient
attr_accessor :height, :weight
end
patient.height #=> 65
patient.weight #=> 90
What do these values represent? What are their units?
class Height
def initialize(inches)
@inches = inches
end
def inches
@inches
end
def centimetres
inches * 2.54
end
end
class Weight
def initialize(pounds)
@pounds = pounds
end
def pounds
@pounds
end
def kilograms
pounds / 2.2
end
end
class Patient
attr_accessor :height_inches, :weight_pounds
def height
Height.new(@height_inches)
end
def weight
Weight.new(@weight_pounds)
end
end
patient.weight.pounds #=> 100
patient.weight.kilograms #=> 45.4545454545
patient.height.inches #=> 65
patient.height.centimetres #=> 165.1
Equality
a = Weight.new(100)
b = Weight.new(110)
c = Weight.new(100)
a == b #=> false
a == c #=> false
a
and c
are not equal because they are different objects.
class Weight
# ...
def hash
pounds.hash
end
def eql?(other)
self.class == other.class &&
self.pounds == other.pounds
end
alias :== eql?
end
We need to override #hash
when we override #==
.
The weight class now looks like:
attr_reader :pounds
def initialize(pounds)
@pounds = pounds
end
def kilograms
pounds / 2.2
end
def hash
pounds.hash
end
def eql?(other)
self.class == other.class &&
self.pounds == other.pounds
end
alias :== eql?
end
Refactor to a struct
Weight = Struct.new(:pounds) do
def kilograms
pounts/2.2
end
end
a = Weight.new(100)
b = Weight.new(110)
c = Weight.new(100)
a == b #=> false
a == c #=> true
But structs are mutable.
Value object gem
You can use value_object
gem.
class Timespan < ValueObject::Base
has_fields :start_time, :end_time
def duration
Duration.new(end_time - start_time)
end
def contains?(time)
(start_time..end_time).cover?(time)
end
def overlays?(other)
contains?(other.start_time) || contains?(other.end_time)
end
end