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.
All my notes are based on years of hands-on experience at iRonin.IT - a top software development company, where we provide custom software development and IT staff augmentation services for a wide array of technologies.
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 heaven and we can easily get a piece of this haven and put it into our app. Let's see how.
Hands on the 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.
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
andeql?
. Thehash
method is available for every object (https://ruby-doc.org/3.2.2/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 ofRun
class is the hash of the distance value itself. We needed to define theeql?
method to tell Ruby that even if we are dealing with different instances they are equal. Now Ruby considers twoRun
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 thanUser
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 differentobject_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 collectionsUse 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.exclude?(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 testWe 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 wellAt 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 objectsUsing 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 < ApplicationRecord
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.