value objects in ruby on rails

What is a Value Object?

As stated by Martin Fowler a value object is:

A small simple object, like money or a date range, whose equality isn’t based on identity.

two points:

  • A value object represents a simple entity whose equality is based on its value,
  • Value objects should be immutable

Why do we need them?

  • the separation of concerns
  • it allows you to combine behaviour with the data and add functionality to data without polluting the model
  • by isolating functionality your code is easier to test
  • removes duplication
  • improves the understanding and organisation of code. Operations on particular data are now gathered in a single place, instead of disperse throughout the code

use cases

  • Arguments together all the time
  • One attribute with behaviour
  • Two inseparable attributes value and unit
  • Class enumerable

Arguments together all the time

The first situation happens when we have two or more arguments that are passed and used together all the time, often known as a “Data Clump” (code smell). A date range is a common example, when start_date and end_date are passed together all the time in our methods. We can create a class DateRange with the attributes start_date and end_date and this class should be responsible for the start_date and end_date columns of a given ActiveRecord object and accommodate all the related behaviour. We could include methods like include_date?(date), include_date_range?(date_range), overlap_date_range?(date_range) and to_s. This class can look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DateRange
attr_reader :start_date, :end_date

def initialize(start_date, end_date)
@start_date, @end_date = start_date, end_date
end

def include_date?(date)
date >= start_date && date <= end_date
end

def include_date_range?(date_range)
start_date <= date_range.start_date && end_date >= date_range.end_date
end

def overlap_date_range?(date_range)
start_date <= date_range.end_date && end_date >= date_range.start_date
end

def to_s
"from #{start_date.strftime('%d-%B-%Y')} to #{end_date.strftime('%d-%B-%Y')}"
end
end

This is just a standard Ruby object that does not inherit from ActiveRecord::Base. This class can be used, for example, with an Event model with the following columns: name, description, address_city, address_state, starts_at, ends_at. The Event model could look something like this:

1
2
3
4
5
6
7
8
9
10
class Event < ActiveRecord::Base
def date_range
DateRange.new(start_date, end_date)
end

def date_range=(date_range)
self.start_date = date_range.start_date
self.end_date = date_range.end_date
end
end

With all this in place, we get the following usage:

1
2
3
4
5
6
7
8
9
> event = Event.create(name: 'Ruby conf', start_date: Date.today, end_date: Date.today + 1.days)
> event.date_range
=> #<DateRange:0x007fd8760c2690 @start_date=Tue, 06 Jun 2017, @end_date=Fri, 16 Jun 2017>
> event.date_range.include_date?(Date.today)
=> true
> event.date_range.include_date_range?(DateRange.new(Date.today, Date.today + 2.days))
=> false
> event.date_range.include_date_range?(DateRange.new(Date.today, Date.today + 1.days))
=> true

As I mentioned previously, one of the advantages of extracting code that usually goes in the model and create value objects is that you can reuse them and here is an example of that since we could use the DateRange object in the other model as well.

One attribute with behaviour

Another situation where a value object can be useful is when you have one simple attribute that needs some associated behaviour and such behaviour does not belong in the model. Imagine that you have a model Room that inherits from ActiveRecord::Base with a degrees attribute and then you add a Temperature class to answer some questions that your system may need related with the temperature value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Temperature
include Comparable
attr_reader :degrees
COLD = 20
HOT = 25

def initialize(degrees)
@degrees = degrees
end

def cold?
self < COLD
end

def hot?
self > HOT
end

def <=>(other)
degrees <=> other.degrees
end

def hash
degrees.hash
end

def to_s
"#{degrees} °C"
end
end

We get the following usage:

1
2
3
4
5
6
7
8
9
10
11
> room_1 = Room.create(degrees: 10)
> room_2 = Room.create(degrees: 20)
> room_3 = Room.create(degrees: 30)
> room_1.temperature.cold?
=> true
> room_1.temperature.hot?
=> false
> [room_1.temperature, Temperature.new(20), room_3.temperature, room_2.temperature].sort
=> [#<Temperature:0x007fe194378840 @degrees=10>, #<Temperature:0x007fe194378818 @degrees=20>, #<Temperature:0x007fe1943787c8 @degrees=20>, #<Temperature:0x007fe1943787f0 @degrees=30>]
> [room_1.temperature, Temperature.new(20), room_3.temperature, room_2.temperature].uniq
=> [#<Temperature:0x007fe194361e88 @degrees=10>, #<Temperature:0x007fe194361e60 @degrees=20>, #<Temperature:0x007fe194361e38 @degrees=30>]

Two inseparable attributes value and unit

A very popular one is the money gem, which helps you deal with money and currency conventions by providing a Money class that encapsulates all information about a certain amount of money such as its currency and value. The gem readme file is very thorough and self-explanatory so if you are interested go ahead and take a look. You can use it in a model class like Product:

1
2
3
4
5
6
7
8
9
10
class Product < ActiveRecord::Base
def cost
Money.new(cents, currency)
end

def cost=(cost)
self.cents = cost.cents
self.currency = cost.currency.to_s
end
end

In this case when asking for or setting the cost of a product, we would use a Money instance.

1
2
3
4
5
6
7
8

> product = Product.create(cost: Money.new(500, "EUR"))
> product.cost
=> #<Money fractional:500 currency:EUR>
> product.cost.cents
=> 500
> product.currency
=> "EUR"

Class enumerable

It is common practice to define a value object in Rails models by creating an array class like this:

1
2
3
4
5
6
7
class Event < ActiveRecord::Base
SIZE = %w(
small
medium
big
)
end

This practice is not good because the array values may be used in the model attributes but they have nothing to do directly with the model domain. Defining the value object like this has a few disadvantages like impossibility to add functionality to the value object without polluting the model and does not allow to reuse the object. So we can create an object to accommodate the data of that array and also add some useful methods if we need them. The value object class for the example above could look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Size
SIZES = %w(small medium big)
attr_reader :size

def initialize(size)
@size = size
end

def self.to_select
SIZES.map{|c| [c.capitalize, c]}
end

def valid?
SIZES.include?(size)
end

def to_s
size.capitalize
end
end

We can get the set of possible sizes and have a method that can be used in select form fields. I think this is useful if you have more than one model that has the attribute size, if your model has a lot of those arrays and you want to slim your model or if you have logic associated with it. Another common example of this type is Color, when we need to have a set of colors that can be used in some persisted models.

References

https://github.com/tcrayford/Values/tree/master