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.

Follow to get a free ebook about Rails!

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.

Run - comparable module usage

I believe this example of value object usage is less popular but it will help to understand another useful feature of our objects - comparisons. In the Color class example we were performing a basic comparison but now we will take it on a more advanced level.

Let's build a simple object that will represent a single run:

class Run
  def initialize(distance:, name:)
    @distance = distance
    @name = name
  end

  attr_reader :distance, :name
end 

Our user performed four runs this month:

run_1 = Run.new(distance: 5_100, name: 'morning run')
run_2 = Run.new(distance: 10_000, name: 'training')
run_3 = Run.new(distance: 42_195, name: 'marathon')
run_4 = Run.new(distance: 10_000, name: 'training')

runs = [run_1, run_2, run_3, run_4]

We now faced two challenges:

  • We want to get unique runs which means runs where a user ran the distance only once in the given month
  • We want to sort runs by the distance

We won't try to complete them without modifying our Run class. Instead, we would use Comparable module to make the Run class comparable with other Run classes:

class Run
  include Comparable

  def initialize(distance:, name:)
    @distance = distance
    @name = name
  end

  attr_reader :distance, :name

  def <=>(other)
    @distance <=> other.distance
  end

  def hash
    @distance.hash
  end

  def eql?(other)
    self.class == other.class && self == other
  end
end 

Now we are able to perform the following actions:

runs.uniq.map(&:distance) # => 5_100, 10_000, 42_195
runs.sort.map(&:distance) # => 5_100, 10_000, 10_000, 42_195

How this was possible? Let's explain the uniq usage first and then sort :

  • uniq - Ruby to return unique values has to know what is the criteria for the given object to be unique. To help Ruby we implemented two methods: hash and eql? . The hash method is available for every object (https://ruby-doc.org/core-2.4.1/Object.html#method-i-hash) but since we are not dealing with simple objects but our custom ones, we had to overwrite the method and tell ruby that hash for the instance of Run class is the hash of the distance value itself. We needed to define the eql? method to tell Ruby that even if we are dealing with different instances they are equal. Now Ruby considers two Run instances with the same distance as the same and can find duplicates to produce a unique set.
  • sort - this implementation is quite simple. We can't sort things if we don't know what is the criteria. If you want to sort fruits you have to know if the criterion is weight or price. In our case criteria is the Run distance.

Now you should understand why and how we can compare value objects and why this feature is so useful when considering implementation of this design pattern.

Person - immutability and verifications

The last example is again a common one but this time I will try to introduce two foundations of value objects considering one case. Let's assume that you have a Person object:

class Person
  def initialize(name:, age:)
    @name = name
    @age = age
  end

  attr_reader :name, :age
end

and you would like to check if given person adult:

john = Person.new(name: 'John', age: '17')
john.age >= 18

# people

people = [john, martha, tim]

martha = Person.new(name: 'Martha', age: 20)
tim = Person.new(name: 'Tim', age: 19)
adults = people.select { |person| person.age >= 18 }

it works but it's far from perfect. When I mentioned verifications in the header, I meant methods with ? that are returning boolean values which are usually used for verifying things like this:

class Person
  def initialize(name:, age:)
    @name = name
    @age = age
  end

  def adult?
    @age >= 18
  end

  attr_reader :name, :age
end

and now our previous operations are looking way better:

john = Person.new(name: 'John', age: 17)
john.adult?

# people

people = [john, martha, tim]

martha = Person.new(name: 'Martha', age: 20)
tim = Person.new(name: 'Tim', age: 19)
adults = people.select(&:adult?)

When it comes to immutability, to have a pure value object we shouldn't be able to modify attributes of our object. When John would have his birthday and become an adult then instead of doing this:

john = Person.new(name: 'John', age: 17)
john.adult? # => false
john.age = john.age + 1 # => undefined method `age='

do this:

john = Person.new(name: 'John', age: 17)
john.adult? # => false

older_john = Person.new(name: john.name, age: john.age + 1)
older_john.adult? # => true

Rules for writing good value objects

In the previous paragraph, we discussed four examples of values objects which showed us the fundamentals of this design pattern. To quickly summarize the fundamentals of good value objects we will go through again what we already discussed.

Rules for writing good value objects are the following:

  • Use meaningful name - this one is obvious and applies to any other class or any other design pattern but in case of value objects bad naming can destroy the implementation and makes it harder to extend. Person class indicates a real person more than User name which is better for an entry in the system. Just like the real world, you may call somebody a programmer but if you would use a more specific name like a backend or frontend developer, it becomes obvious what attributes his object representation may provide.
  • Use proper equality comparison - don't compare by identity but using attributes of a given object. Two instances of Fruit object have different object_id but they should be considered as equal if both instances have the same name, for example, Banana
  • Use comparable module - if you plan to perform operations like sorting or making a unique list out of your objects, use comparable module along with the other methods that will extend your objects and make them behave like a collections
  • Use duck typing - take care of your objects' conversion to make the code more readable and testable
  • Make your objects immutable - once you pass values to your object, don't change them. If you need them changed, create another instance. The idea of value objects is to provide value not maintain any states or change the data.

Advantages of using value objects

We know how to write value objects and when to use them but it might not be obvious yet why using this design pattern is so beneficial for our codebase. Let's check the most important advantages:

  • The attributes of our object won't be accidentally changed so our code won't be exposed to bugs which source of is hard to find
  • We stick to the business domain naming because when creating new classes we are forced to rethink our approach to class designing
  • We can easily extend our objects by adding new methods
  • Value objects are pure Ruby objects which means that it's easy to test them and there are no fancy magical code that is hard to understand and debug

In my opinion, the above arguments are the most important ones. We have the theoretical part behind us so we can focus on some refactoring examples, Ruby gems that help us to implement value object pattern and Struct which is often considered as a built-in alternative for our pattern. Sounds interesting!

Refactoring

Real-world situations where given design pattern can be used to make the code more readable, useful, and testable are the best proof of usability of the given pattern. Below I collected some code that is far from being perfect and is a good candidate for refactoring. I will use value objects to make the code more readable and testable.

Parsing CSV and sending summary e-mail

Let's assume that we have an app where account administrator can upload CSV file with the list of users and then we want to send invitations but only for those who have the e-mail in the domain that is not blacklisted and notify administration of the users who don't have a valid e-mail. Before refactoring, our code might look like the following version:

def invite_users(file_name)
    blacklisted_email_domains = ['some.com', 'other.com']
    invalid_users = []

    CSV.foreach(file_name) do |row|
      email = row[0]
      email_domain = email.split('@').last

      if blacklisted_email_domains.exclude?(email)
        InvitationMailer.send_invitation(email)
      else
        invalid_users << email
      end
    end

    SystemMailer.notify_about_invalid_users(invalid_users)
end

let's clean up our code with introducing the Email value object:

class Email
  BLACKLISTED_DOMAINS = ['some.com', 'other.com'].freeze

  def initialize(address)
    @address = address
  end

  attr_reader :address

  def valid_domain?
    BLACKLISTED_DOMAINS.exclide?(domain)
  end

  private

  def domain
    @address.split('@').last
  end
end

and apply changes to our method:

def invite_users(file_name)
    invalid_users = []

    CSV.foreach(file_name) do |row|
    email = Email.new(row[0])

      if email.valid_domain?
        InvitationMailer.send_invitation(email.address)
      else
        invalid_users << email.address
      end
    end

    SystemMailer.notify_about_invalid_users(invalid_users)
end

looks better but there is still an area for improvements. We isolated e-mail parsing logic in the Email class but how about isolating report parsing logic in a Report class that will represent the CSV file uploaded by the administrator? Let's give it a try:

class Report
  def initialize(file_name)
    @file_name = file_name
  end

  def emails
    @emails ||= CSV.foreach(file_name).map { |row| Email.new(row[0]) }
  end
end

class Email
  BLACKLISTED_DOMAINS = ['some.com', 'other.com'].freeze

  def initialize(address)
    @address = address
  end

  attr_reader :address

  def valid_domain?
    BLACKLISTED_DOMAINS.exclude?(domain)
  end

  def invalid_domain?
    !valid_domain?
  end

  private

  def domain
    @address.split('@').last
  end
end

as you can notice, we added the Report class and invalid_domain? method to the Email class added previously - it would help us to quickly filter emails with invalid domains. Let's see it in action:

def invite_users(file_name)
  report = Report.new(file_name)

  report.emails.select(&:valid_domain?).each |email|
    InvitationMailer.send_invitation(email.address)
  end

  invalid_users = report.emails.select(&:invalid_domain?).map(&:address)
  SystemMailer.notify_about_invalid_users(invalid_users)
end

With the refactored version the invite_users know nothing about parsing the report or deciding which email is invalid - it just does its job, sends emails. However, the method still does a little bit more than the name indicates. Let's quickly refactor it into service:

class ReportService
  def initialize(file_name)
    @file_name = file_name
  end

  def invite_users
    valid_users_emails.each |email|
      InvitationMailer.send_invitation(email.address)
    end
  end

  def notify_about_invalid_users
    SystemMailer.notify_about_invalid_users(invalid_users_emails)
  end

  private

  def report
    @report ||= Report.new(@file_name)
  end

  def valid_users_emails
        report.emails.select(&:valid_domain?)
  end

  def invalid_users_emails
    report.emails.select(&:invalid_domain?).map(&:address)
  end
end

# Usage

service = ReportService.new('some_file_name')
service.invite_users
service.notify_about_invalid_users

I believe the refactoring is done now. Let's quickly summarize the things we did to refactor the original method:

  • We extracted logic responsible for parsing e-mail, checking if an e-mail domain is invalid into the Email value object. Now everything related to the email domain is localized in one place and the code is super easy to test
  • We extracted logic responsible for getting e-mails from the report to the Report value object. In the future, we can easily update it to parse different types of files and the rest of logic won't be aware of this change but still cooperate well
  • At the end, we created ReportService which is a simple service class responsible for sending e-mails for valid and invalid users

Integration with ActiveRecord

Since it's obvious that in a Rails application we won't be dealing with only pure Ruby classes, it's good to know how to make value object pattern to work with the application models. There are two typical approaches:

  • Using composed_of which is a built-in method in Rails for manipulating value objects
  • Using simple methods that return a new instance of a value object populated with the attributes from the model.

I will start with some basic model class along with the code that is a good candidate for refactoring with value object design pattern. Let's assume that we have a User model with the fields name and birth_date that consists of the following logic:

class User < ActiveRecord::Base
  def first_name
    name.split(" ").first
  end

  def last_name
    name.split(" ").last
  end

  def age
    Date.current.year - birth_date.year
  end
end

Since a user is a person representation in the database, we will create the Person value object that will hold the logic responsible for displaying details of the given person:

class Person
  def initialize(name:, birth_date:)
    @name = name
    @birth_date = birth_date
  end

  def first_name
        @name.split(" ").first
  end

  def last_name
        @name.split(" ").last
  end

  def age
        Date.current.year - @birth_date.year
  end
end

now the first option is to simply add User#person method and initialize the value object with record attributes:

class User < ActiveRecord::Base
  def person
    Person.new(name: name, birth_date: birth_date)
  end
end

user = User.find(1)
user.person.first_name # => "John"
user.person.age # => 25

we can also transform this version into version with composed_of method:

class User < ActiveRecord::Base
  composed_of :person,
              mapping: %w(name birth_date),
              constructor: Proc.new { |name, birth_date| Person.new(name: name, birth_date: birth_date) }
end

if you would use standard arguments in the Person value object there would be no need to pass constructor option as composed_of would automatically pass all arguments, otherwise with keyword arguments you have to use the constructor option.

The composed_of is way more flexible but the topic is beyond the scope of the article so I would not describe it more.

Struct as an alternative

The struct is a built-in class that provides some functionalities that you might find useful when creating and manipulating value objects. In previous paragraphs, we created a simple Person class:

class Person
  def initialize(name:, age:)
    @name = name
    @age = age
  end

  def adult?
    @age >= 18
  end

  attr_reader :name, :age
end

with the Struct we can implement it in the following way:

Person = Struct.new(:name, :age) do
  def adult?
    age >= 18
  end
end

person = Person.new("John", 17)
person.adult? # => false
person.name # => 'John'
person.age # => 17

another_person = Person.new("John", 17)

person == another_person # => true

if you are using Ruby higher than 2.5 you can use keyword_init option to be able to use keyword arguments along with the Struct:

Person = Struct.new(:name, :age, keyword_init: true) do
  def adult?
    age >= 18
  end
end

person = Person.new(name: 'John', age: 17)

However, while Struct allows you to write some things quicker its attributes are mutable so it breaks one of the most important concepts of value object design pattern. If you don't like Struct but still want to remove some boilerplate from the code, you should familiarize yourself with the Ruby Gems presented below which are another alternative for creating value objects.

Ruby gems as an alternative

As I mentioned before, there are some Ruby gems available that make the value objects creation faster and allow for more flexibility. I collected the most useful and popular of them.

Virtus

Source: https://github.com/solnic/virtus

Example:

class Person
  include Virtus.value_object

  values do
    attribute :name, String
    attribute :age, Integer
  end

  def adult?
    age >= 18
  end
end

person = Person.new(name: 'John', age: 17)
another_person = Person.new(name: 'John', age: 17)
person == another_person # => true

Anima

Source: https://github.com/mbj/anima

Example:

class Person
  include Anima.new(:name, :age)

  def adult?
    age >= 18
  end
end

person = Person.new(name: 'John', age: 17)
another_person = Person.new(name: 'John', age: 17)
person == another_person # => true

Value semantics

Source: https://github.com/tomdalling/value_semantics

Example:

class Person
  include ValueSemantics.for_attributes {
    name
    age
  }

  def adult?
    age >= 18
  end
end

person = Person.new(name: 'John', age: 17)
another_person = Person.new(name: 'John', age: 17)
person == another_person # => true

Values

Source: https://github.com/tcrayford/Values

Example:

class Person < Value.new(:name, :age)
  def adult?
    age >= 18
  end
end

person = Person.with(name: 'John', age: 17)
another_person = Person.with(name: 'John', age: 17)
person == another_person # => true

As you can see, there are many gems out there that make the value objects creation easier and more flexible. Some of them allow setting the default values assignment and others not but the most important feature is that they all allow for correct comparison and are immutable so they perfectly fit into the value object design pattern ideology.

Summary

You should now have a high-level overview of the value object design pattern and know what are the rules for building a class that will perfectly fit into this design pattern definition.

There is much more to write about composed_of function provided by Rails or alternative gems such as Virtus but those topics are beyond the scope of the value object so feel free to explore them by ourselves.