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.
Read the rest of the article at https://pdabrowski.com/articles/rails-design-patterns-value-object
Top comments (1)
Very nice! I must admit that I’ve hardly ever used this pattern and have only seen it. Rails also supplies Enum which could be useful in the first case
One thing worth mentioning, perhaps, is that a lot of folks may not be familiar with Martin Fowler and his ideas. Would be nice to expand or remove that part depending on how short you’d like to lee this. I think this is valuable without that mention :)