---
title: Value Object Semantics in Ruby
teaser: What is the correct behavior of `hash`, `==`, `eql?` and `equal?` for value
  objects?
tags: ruby,good code,domain modeling,web
author: Joël Quenneville
published_on: 2022-07-18
---

There are a bunch of tricky edge-cases to look out for when implementing a value
object in Ruby. I often catch these when reviewing pull requests. It's possible
that someone sent a link here from just such a review! Here is a [handy
overview](#semantics) of expected value object semantics in Ruby.

## What is a value object?

[Value objects] are [richer domain objects] that typically replace the use of a
primitive. For example, we might have a `Duration` class that represents an
amount of time instead of representing this value as a raw integer (this is
separate from the core `Time` class that represents a _moment_ in time).

```ruby
class Duration
  def initialize(minutes)
    @minutes = minutes
  end
end
```

[Value objects]: https://thoughtbot.com/upcase/videos/value-objects
[richer domain objects]: http://wiki.c2.com/?ValueObject

## Equality

There are two high-level ["concepts of equality"] in Ruby:

1. Equal by **identity** (objects being compared are the same object in memory)
2. Equal by **value** (objects being compared represent the same value)

By default, primitives and [structs](#structs) compare by value while instances
of user-created classes compare by identity. When we create a value object, we
are trying to _override the default_ to compare by value rather than by
identity.

For example, a duration object representing 5 minutes should be considered equal
to a different instance also representing 5 minutes but doesn't by default
because it is an instance of a user-created class.

```ruby
Duration.new(5) == Duration.new(5)
# => false
```

["concepts of equality"]: https://mauricio.github.io/2011/05/30/ruby-basics-equality-operators-ruby.html

## Semantics

Two value objects that represent the same value should:

1. `==` each other
2. NOT `equal?` each other (`equal?` should compare by _identity_, not by value)
3. `hash` to the same value (so we don't break hash keys)
4. `eql?` each other (Ruby expects objects that have the same `hash` to be
   `eql?` to each other)

```ruby
class Duration
  attr_reader :minutes

  def initialize(minutes)
    @minutes = minutes
  end

  def ==(other)
    minutes == other.minutes
  end
  alias_method :eql?, :==

  def hash
    [self.class, minutes].hash
  end
end
```

## Hashes

`#hash` and `#eql?` are important to allow our value object to be used as a hash
key correctly. This allows us to index into hashes with any object that's equal
to their key (our natural intuition) rather than only being allowed to use the
exact same instance.

```ruby
events_by_duration =
  {
    Duration.new(5)  => [event1, event3],
    Duration.new(10) => [event2]
  }

# This would return nil if hash and eql? where not overriden since
# we are techinically using a different instance than was used
# when defining the hash.
five_min_events = events_by_duration[Duration.new(5)]
```

## Structs

<aside class="info">
As of Ruby 3.2, you probably want to use the immutable <a href="https://docs.ruby-lang.org/en/master/Data.html">Data
class</a> for your value objects. <code>Struct</code> is still available if you
want something mutable.
</aside>

[Ruby structs] implement these semantics by default! They can be an easy way to
cheaply add value objects to your project, although I like to move to a real
object once I start needing custom methods.

```ruby
Duration = Struct.new(:minutes)
Duration.new(5) == Duration.new(5)
# => true
```

[Ruby structs]: https://ruby-doc.org/core-3.1.1/Struct.html

## Bonus

While the equality rules above are the only ones that are strictly required,
below are extra behaviors I commonly add to my value objects.

In general, value objects should also be **immutable**. Any methods that would
modify the object (e.g. addition) should instead return a new instance. Bonus
points for freezing the object in the constructor.

```ruby
class Duration
  def initialize(minutes)
    @minutes = minutes
    freeze
  end

  def +(other)
    Duration.new(self.minutes + other.minutes)
  end
end
```

Additionally, it may be nice to implement `<=>` and include `Comparable`. This
can get your `==` for free. It will also allow you to [use your value in a
range]. Add `succ` if you want ranges of your object to be iterable.

[use your value in a range]: https://thoughtbot.com/blog/custom-ranges-in-ruby

## Testing

Want to try and remember to implement these correctly on all value objects on
your project? This may be an actually good use for an RSpec shared example.
See [this gist] for what that might look like.

```ruby
RSpec.describe Duration do
  it_behaves_like "a value object"
end
```

[this gist]: https://gist.github.com/JoelQ/8d78e8960e3d7cd60402f1f2de648bfa#file-value_object_shared_examples-rb
