Value Objects Explained with Ruby

Share this article

val_obj
This article explains the concept of value objects. It first defines and demonstrates various kinds of value objects, then it explains the rule to construct valid ones while covering the consequences of violating the concept. At last, it shows several ways to implement value objects in Ruby. Although the examples are written in Ruby, the concept could be easily applied to other languages as well.

What is a Value Object?

A value object as defined in P of EAA is:
…their notion of equality isn’t based on identity, instead two value objects are equal if all their fields are equal.
That means value objects which have the same internal fields must equal to each other. The value of all fields sufficiently determines the equality of a value object. The simplest examples are the primitive objects – Symbol, String, Integer, TrueClass(true), FalseClass(false), NilClass(nil), Range, Regexp etc. The value of each of these objects determines their equality. For example, whenever 1.0 appears in a program, it should be the equal1 to 1.0
because they have the same value.
var1 = :symbol
var2 = :symbol
var1 == var2  # => true

var1 = 'string'
var2 = 'string'
var1 == var2  # => true

var1 = 1.0
var2 = 1.0
var1 == var2  # => true

var1 = true
var2 = true
var1 == var2  # => true

var1 = nil
var2 = nil
var1 == var2  # => true

var1 = /reg/
var2 = /reg/
var1 == var2  # => true

var1 = 1..2
var2 = 1..2
var1 == var2  # => true

var1 == [1, 2, 3]
var2 == [1, 2, 3]
var1 == var2  # => true

var1 == { key: 'value'}
var2 == { key: 'value'}
var1 == var2  # => true
These are examples of value objects with one field. Value objects can also be composed of multiple fields. For example, the IPAddr class in the standard library has three fields, @addr, @mask_addr and @family. @addr and @mask_addr define the IP address values and @family determines its type as IPv4 or IPv6. IPAddr objects which have the same field values are equal to each other.
require 'ipaddr'

ipaddr1 = IPAddr.new "192.168.2.0/24"
ipaddr2 = IPAddr.new "192.168.2.0/255.255.255.0"

ipaddr1.inspect
# => "#<IPAddr: IPv4:192.168.2.0/255.255.255.0>"

ipaddr2.inspect
#=> "#<IPAddr: IPv4:192.168.2.0/255.255.255.0>"

ipaddr1 == ipaddr2 # => true
Similarly, money, GPS data, tracking data, date range etc. are all proper candidates for value objects. The above examples demonstrate the definition of value objects – objects whose equality is based on their internal fields rather than their identity. To guarantee value objects with the same fields will be equal to each other whenever it appears in a program, there is an implicit rule to follow when constructing values objects.

Rule to Construct Value Objects

The rule to guarantee the equality of value objects across their life cycle is: the attributes of a value object will remain unchanged from instantiation to the last state of its existence. “…this is required for the implicit contract that two value objects created equal, should remain equal.”2 Following this rule, value objects should have an immutable interface. Sometimes, the need to create variants of value objects might break the rule, if they are constructed without careful implementation. Take the following Money class, for example.
class Money
  attr_accessor :currency, :amount

  def initialize(amount, currency)
    @amount = amount
    @currency = currency
  end
end

usd = Money.new(10, 'USD')
# <Money:0x007f987f283b50 @amount=10, @currency="USD">

usd.amount = 20
usd.inspect
# <Money:0x007f987f283b50 @amount=20, @currency="USD">
The usd money value object is changed during its life cycle due to the changes to its @amount field. The public setter method amount= violates the rule because, when it is called, the value shifts from the object’s original one. The correct approach to create variants of value objects is to implement the setter method to initialize a new value object instead of modifying the current one:
class Money
  # remove the public setter interface
  attr_reader :currency, :amount

  def initialize(amount, currency)
    @amount = amount
    @currency = currency
  end

  # a setter method to return a new value object
  def amount=(other_amount)
    Money.new(other_amount, currency)
  end
end

usd = Money.new(10, 'USD')
usd.inspect
# <Money:0x007f9672753ba8 @amount=10, @currency="USD">

other_usd = (usd.amount = 20)
usd.inspect
# <Money:0x007f9672753ba8 @amount=20, @currency="USD">
In this way, once a Money
value object is created, it will remain in its initial state throughout its life cycle. New variants are created as different value objects instead of making changes to the original one.

How to Implement a Value Object in Ruby

In review, to implement a value object following the above definition and rules:
  • Value objects have multiple attributes
  • Attributes should be immutable througout its life cycle
  • Equality is determined by its attributes (and its type)
We’ve already seen the implementation of Money value objects with Ruby normal class syntax. Let’s complete the implementation by adding methods for determining equality.
class Money
  def ==(other_money)
    self.class == other_money.class &&
    amount == other_money.amount &&
    currency == other_money.currency
  end
  alias :eql? :==

def hash
    [@amount, @currency].hash
  end
end

usd = Money.new(10, 'USD')
usd2 = Money.new(10, 'USD')
usd == usd2 # => true
eql? and == are standard Ruby methods to determine equality at the object level. By the definition of value objects, the comparison results of all the fields are tested. It is also required to distinguish Money from other objects which may have the same attributes. For example:
AnotherMoney = Struct.new(:money, :currency)
other_usd = AnotherMoney.new(10, 'USD')
usd == other_usd # => false
That is accomplished by the line self.class == other_money.class. The hash method is the standard Ruby method to generate a hash value for an object. From the Ruby docs, “…this function must have the property that a.eql?(b) implies a.hash == b.hash.” So, the implementation uses all the field values to generate the hash. Besides the normal class syntax, Struct as shown in the last example is a very concise way to build value objects. Here is an example to implement the same Money value objects using Struct:
class Money < Struct.new(:amount, :currency)
  def amount=(other_amount)
    Money.new(other_amount, currency)
  end
end

usd = Money.new(10, 'USD')
usd2 = Money.new(10, 'USD')

usd.hash == usd2.hash # => true
usd == usd2 # => true
It is much more concise than the normal class definition. Attributes are declared through the Struct.new interface. Definition of hash and == methods are omitted because they are inherited from the Struct
class. However, one drawback of using Struct to define value objects is that they are mutable through the default setter methods and they allow default attribute values.
usd = Money.new(10, 'USD')
usd.amount = 20
usd.inspect
# => <struct Money amount=10, currency="USD">

invalid_usd = Money.new(1)
invalid_usd.inspect
# => <struct Money amount=1, currency=nil>
To keep the conciseness of the Struct-way, but fix its problems, we could use the Value gem which is a Struct-like class with all the above problems fixed. Here is an example from the library’s demo page:
Point = Value.new(:x, :y)
Point.new(1)
# => ArgumentError: wrong number of arguments, 1 for 2
# from /Users/tcrayford/Projects/ruby/values/lib/values.rb:7:in `block (2 levels) in new
# from (irb):5:in new
# from (irb):5
# from /usr/local/bin/irb:12:in `<main>

p = Point.new(1, 2)
p.x = 1
# => NoMethodError: undefined method x= for #<Point:0x00000100943788 @x=0, @y=1>
# from (irb):6
# from /usr/local/bin/irb:12:in <main>
Now, working with value objects in Ruby should be easy and fun.

Conclusion

Starting with definition of value objects, this article shows the usage of value objects from primitive object to more complex domain-specific objects. It also dives into the implicit rule for consistent behavior of value objects during its life cycle. At last, we’ve gone through several different ways to implement the concept of values objects using Ruby’s normal class definition and Struct class. Finally, we ended up with a useful Values gem to create value objects with ease and conciseness. Does the explanation of value objects provide you inspiration for writing more elegant code? What’s your opinions towards the usage of value objects?
  1. “Equal” is used in the meaning of equality(== or eql?) instead of identify(equal?) here.
  2. http://en.wikipedia.org/wiki/Value_object

References

Value Object on Wikipedia. Some discussion on value objects at c2.com.

Frequently Asked Questions on Value Objects in Ruby

What are the benefits of using Value Objects in Ruby?

Value Objects in Ruby offer several benefits. Firstly, they help in encapsulating related data into a single cohesive unit, thereby promoting code readability and maintainability. Secondly, they enforce immutability, which means once a Value Object is created, it cannot be changed. This characteristic is particularly useful in multi-threaded environments where mutable objects can lead to inconsistencies. Lastly, Value Objects can be easily tested in isolation, which simplifies the testing process.

How do Value Objects differ from Entities in Ruby?

The primary difference between Value Objects and Entities in Ruby lies in their identity. Entities have a unique identity and even if their attributes change, they are still considered the same. On the other hand, Value Objects are identified by the values they hold. If any attribute of a Value Object changes, it becomes a different object.

Can Value Objects be persisted in a database?

Yes, Value Objects can be persisted in a database. However, since they don’t have a unique identity, they are usually embedded within an Entity. This means that the Value Object’s lifecycle is tied to the Entity it belongs to.

How can I implement Value Objects in Ruby on Rails?

In Ruby on Rails, you can implement Value Objects by creating a class that includes the ActiveModel::Model module. This class should define the attributes of the Value Object and any necessary validations. The Value Object can then be used within an Entity by using the composed_of method in the Entity’s model.

What is the role of the composed_of method in Ruby on Rails?

The composed_of method in Ruby on Rails is used to declare a composition relationship between an Entity and a Value Object. It specifies how the Value Object should be constructed from the Entity’s attributes and how it should be converted back into those attributes.

Can Value Objects have behavior?

Yes, Value Objects can have behavior. In fact, one of the principles of Domain-Driven Design, which promotes the use of Value Objects, is that objects should contain both data and behavior. This means that Value Objects can have methods that perform operations on their attributes.

How does immutability of Value Objects help in Ruby?

The immutability of Value Objects in Ruby helps in maintaining consistency and integrity of data. Since Value Objects cannot be changed once they are created, there is no risk of their state being altered inadvertently. This is particularly useful in multi-threaded environments where mutable objects can lead to inconsistencies.

Can Value Objects be nested?

Yes, Value Objects can be nested. This means that a Value Object can contain other Value Objects. This is useful when you want to model complex data structures.

How do Value Objects improve code readability?

Value Objects improve code readability by encapsulating related data into a single cohesive unit. This means that instead of dealing with individual variables, you can work with objects that represent meaningful concepts in your domain. This makes the code easier to understand and maintain.

Can Value Objects be shared?

Yes, Value Objects can be shared. Since they are immutable, there is no risk of their state being altered. This means that you can safely share a Value Object between multiple entities or use it as a key in a hash.

Steven YangSteven Yang
View Author
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form