Last Updated: March 27, 2023
·
10.83K
· pdabrowski

Value objects - a complete guide to Ruby code that is testable, readable and simple

Introduction

Regardless of the type of architecture do you like the most in Rails, you will find value objects design pattern useful and, which is just as important, easy to maintain, implement and test. The pattern itself doesn't introduce any unneeded level of abstraction and aims to make your code more isolated, easier to understand and less complicated.

A quick look at the pattern's name

Let's just quickly analyze its name before we move on:

  • value - in your application there are many classes, some of them are complex which means that they have a lot of lines of code or performs many actions and some of them are simple which means the opposite. This design pattern focuses on providing values that's why it is so simple - it don't care about connecting to the database or external APIs.
  • object - you know what is an object in objected programming and similarly, value object is an object that provides some attributes and accepts some initialization params.

The definition

There is no need to reinvent the wheel so I will use the definition created by Martin Fowler:

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

Won't it be beautiful if a part of our app would be composed of small and simple objects? Sounds like a heaven and we can easily get a piece of this haven and put it into our app. Let's see how.

Hands on keyboard

I would like to discuss the advantages of using value objects and rules for writing good implementation but before I will do it, let's take a quick look at some examples of value objects to give you a better understanding of the whole concept.

Colors - equality comparsion example

If you are using colors inside your app, you would probably end up with the following representation of a color:

class Color
  CSS_REPRESENTATION = {
    'black' => '#000000',
    'white' => '#ffffff'
  }.freeze

  def initialize(name)
    @name = name
  end

  def css_code
    CSS_REPRESENTATION[@name]
  end

  attr_reader :name
end

The implementation is self-explainable so we won't focus on going through the lines. Now consider the following case: two users picked up the same color and you want to compare the colors and when they are matching, perform some action:

user_a_color = Color.new('black')
user_b_color = Color.new('black')

if user_a_color == user_b_color
  # perform some action
end

With the current implementation, the action would never be performed because now objects are compared using their identity and its different for every new object:

user_a_color.object_id # => 70324226484560
user_b_color.object_id # => 70324226449560

Remember Martin's Fowler words? A value object is compared not by the identity but with its attributes. Taking this into account we can say that our Color class is not a true value object. Let's change that:

class Color
  CSS_REPRESENTATION = {
    'black' => '#000000',
    'white' => '#ffffff'
  }.freeze

  def initialize(name)
    @name = name
  end

  def css_code
    CSS_REPRESENTATION[@name]
  end

  def ==(other)
    name == other.name
  end

  attr_reader :name
end

Now the compare action makes sense as we compare not object ids but color names so the same color names will be always equal:

Color.new('black') == Color.new('black') # => true
Color.new('black') == Color.new('white') # => false

With the above example we have just learned about the first fundamental of value object - its equality is not based on identity.

Price - duck typing example

Another very common but yet meaningful example of a value object is a price object. Let's assume that you have a shop application and separated object for a price:

class Price
  def initialize(value:, currency:)
    @value = value
    @currency = currency
  end

  attr_reader :value, :currency
end

and you want to display the price to the end user:

juice_price = Price.new(value: 2, currency: 'USD')
puts "Price of juice is: #{juice_price.value} #{juice_price.currency}"

the goal is achieved but it doesn't look good. Another feature often seen in value object is duck typing and this example is a perfect case where we can take advantage of it. In simple words duck typing means that the object behaves like a different object if it implements a given method - in the above example, our price object should behave like a string:

class Price
  def initialize(value:, currency:)
    @value = value
    @currency = currency
  end

  def to_s
    "#{value} #{currency}"
  end

  attr_reader :value, :currency
end

now we can update our snippet:

juice_price = Price.new(value: 2, currency: 'USD')
puts "Price of juice is: #{juice_price}"

We have just discovered another fundamental of value objects and as you see we still only return values and we keep the object very simple and testable.

Continue reading at https://pdabrowski.com/articles/rails-design-patterns-value-object