29
.
04
.
2024
14
.
07
.
2022
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Value Object - DDD in Ruby on Rails

Paweł Strzałkowski
Chief Technology Officer

In this episode of "DDD in Ruby on Rails" I will introduce one of the building blocks of tactical Domain-Driven Design - a value object.

"An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT. VALUE OBJECTS are instantiated to represent elements of the design that we care about only for what they are, not who or which they are"

Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software

Value Object

Value Object is an immutable object, used to represent a value. It can be compared with other value objects to check whether the represented values are equal. It has no identity and no observable history.

The most famous value object, used by most of us, is Money. It represents a combination of an amount and a currency. Together, they form a value (ie. $5). Another example is a combination of latitude and longitude, which together have a meaning of geographic coordinates.

Whenever two (or more) primitive attributes gain proper meaning when read together, it is possible that they should be encapsulated within a single value object. Such transformation gives an explicit meaning to primitives scattered around an entity. It sometimes makes sense to elevate a single primitive to a value object. It allows to show its importance and meaning in the overall design.

Let's go through a real-life story to see how a value object can be used in Ruby on Rails.

We’ve got a job to do

We needed to start tracking events (expressed as strings, for example "User created"). For that, I have created a very simple service object. It had only one purpose - use Analytics::Client to track event names passed as strings.


module Analytics
  class TrackEventService
    def initialize(user:, event:)
      # ...
    end

    def call
      return false unless Configuration.enabled?

      Client.track(
        user_id: user.id,
        event: event,
        properties: properties
      )
    end

    # ...
  end
end

# Usage example

Analytics::TrackEventService.new(user: user, event: 'Item added').call

We can make it better

Primitive strings proved to be error prone, hard to track throughout the application and simply unreliable. After a while, the application ended up with events like "Job Created", "Created job" and so on. It has been suggested to implement a common event name handler and pass a noun and a verb separately.


module Analytics
  class TrackEventService
    def initialize(user:, noun:, verb:)
      # ...
    end

    def call
      # ...
    end

    private

    def event
      "#{noun} #{verb}"
    end

    # ...
  end
end

# Usage example

Analytics::TrackEventService.new(user: user, noun: 'Item', verb: 'Added').call

But… There are exceptions

A new requirement has come. We needed to handle exceptions and define events as custom strings like "Important Job Almost Created" yet again. At this point, we wanted to pass either a noun + verb pair or an event name.

Let's do it!


module Analytics
  class TrackEventService
    def initialize(user:, noun: nil, verb: nil, event: nil)
      # ...
    end

    def call
      return false unless Configuration.enabled?

      Client.track(
        user_id: user.id,
        event: event_name,
        properties: properties
      )
    end

    private

    def event_name
      return event if event.present?

      "#{noun} #{verb}"
    end

    # ...
  end
end

# Usage example

Analytics::TrackEventService.new(user: user, noun: 'Item', verb: 'Added').call
Analytics::TrackEventService.new(user: user, event: 'Item added to cart').call

There is an immediate problem with this service. It's hard to test. Mostly because, in this form, it violates the Single Responsibility Principle. Outside of its primary purpose (communication), it is also responsible for handling changing business needs by gluing 3 terms together. This kind of classes tend to grow fast and loose cohesion in the blink of an eye.

Let's use a value object

According to the definition, a value object is immutable and represents a value. In order to use this pattern, I've created an Analytics::Event class. It takes either a name (through its constructor) or a combination of a noun and a verb (through the class method combination_of) and provides a unified reading interface (name reader).


module Analytics
  class Event
    attr_reader :name

    def initialize(name:)
      @name = name.to_s
    end

    def self.combination_of(noun:, verb:)
      new(name: "#{noun} #{verb}")
    end

    def ==(other) # when comparing, we should compare values
      name == other.name 
    end
  end
end

It has replaced the primitive strings in the service implementation:


module Analytics
  class TrackEventService
    def initialize(user:, event:)
      self.user = user
      self.event = event
    end

    def call
      return false unless Configuration.enabled?

      Client.track(
        user_id: user.id,
        event: event.name,
        properties: properties
      )
    end

    # ...
  end
end

It is no longer the job of this service to know how to construct event's name. The responsibility has been moved to the value object and can be easily tested. It's straightforward to use, readable, testable and reusable.


Analytics::TrackEventService.new(
  user: user,
  event: Analytics::Event.combination_of(noun: 'Jon', verb: 'Created')
)

Analytics::TrackEventService.new(
  user: user,
  event: Analytics::Event.new(name: 'Cloned and archived related clients')
)

Value Objects in Ruby on Rails

The Ruby on Rails framework supports value objects in the form of the composed_of method. You can read more about it at https://apidock.com/rails/ActiveRecord/Aggregations/ClassMethods/composed_of

It allows you to treat a subset of an ActiveRecord entity's attributes as a value object.


# Value Object class definition

class Coordinates
  attr_reader :lat, :lon

  def initialize(lat, lon)
    @lat = lat
    @lon = lon
  end

  def ==(other)
    lat === other.lat && lon == other.lon
  end

    # ... How else can equality be checked?
end


# ActiveRecord class

class Location < ApplicationRecord
  composed_of :coordinates,
              class_name: 'Coordinates',
              mapping: [
                %w[latitude lat],
                %w[longitude lon]
              ]
end


# How to use

> a = Location.new(name: 'Warsaw One', latitude: '52.23', longitude: '21.01')
> b = Location.new(name: 'Warsaw Two', latitude: '52.23', longitude: '21.01')
> c = Location.new(name: 'London', latitude: '51.50', longitude: '-0.13')

> a.coordinates
 => #<Coordinates:0x00007fa846f7c5d8 @lat="21.01", @lon="52.23">

> a.latitude
 => "52.23"

> a.coordinates == b.coordinates
 => true

> a.coordinates == c.coordinates
 => false

Value Object’s equality

There is more than one way to check for equality in Ruby. The above examples work well for a simple == comparison. But what about eql?, which is based on the output of an internal .hashmethod? Be sure to override it to make it dependent only on the object’s value.


# Example from Money gem
# https://github.com/RubyMoney/money/blob/main/lib/money/money.rb

class Money
  # ...

  def hash
    [fractional.hash, currency.hash].hash
  end

  # ...
end

Summary

No matter if you use the composed_of method or if you create your own objects - Value Objects are a great tool to introduce consistency, testability and readability to your code. Use them and make your code better.

If you'd like to know more, I encourage you to read about the concept. As anything within the DDD realm, they are widely used in other programming languages. It makes it easy to find good examples. You may start from a great article by Martin Fowler, linked below.

Resources

Articles in this series

Paweł Strzałkowski
Chief Technology Officer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

JSON:API consumption in Rails

14
.
11
.
2023
Jan Matusz
Ruby on Rails
Backend
Tutorial

Marketing hacks #01: How to Track off-line conversions

14
.
11
.
2023
Marek Łukaszuk
Ruby on Rails
Business
Marketing

Common communication issues in project management

02
.
10
.
2024
Michał Krochecki
Project Management

Selected SXSW lectures takeaways

14
.
11
.
2023
Michał Piórkowski
Conferences
Frontend
Backend
Business

SXSW Summary

14
.
11
.
2023
Michał Piórkowski
Ruby on Rails
Conferences
Frontend
Backend
Business

How to get the most out of SXSW Interactive

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Conferences
Frontend
Backend
Business

Guide to recruitment at Visuality

14
.
11
.
2023
Michał Piórkowski
HR
Visuality

TOP Ruby on Rails Developers

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business

How to conquer Westworld?

14
.
11
.
2023
Maciej Zdunek
Business
Marketing

2018 Rewind by Visuality

02
.
10
.
2024
Michał Krochecki
HR
Visuality

Quality Assurance Testing

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Backend

Why do we like to be together?

02
.
10
.
2024
Michał Krochecki
Visuality
HR

Wallboards - a great value for our teams and clients

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Design
Project Management
Backend

2018 Clutch Global Leader

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business
Marketing

Hot topic: Progressive Web Apps instead of native mobile apps

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Business
Backend
Frontend

Docker hosted on Jelastic

14
.
11
.
2023
Marcin Prokop
Ruby on Rails
Backend
Tutorial

All the pieces matter - Visuality DNA

14
.
11
.
2023
Michał Piórkowski
Visuality
HR

Tech conferences 2018/2019 you definitely should attend

02
.
10
.
2024
Michał Krochecki
Conferences

Visuality Poznań is here!

14
.
11
.
2023
Michał Piórkowski
Visuality
Business
HR

Why we chose Ruby on Rails and React.js for our main technologies? (FAQ).

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Backend
Frontend
Visuality

Branding: How to style your Jira?

14
.
11
.
2023
Lukasz Jackiewicz
Tutorial
Design
Project Management

How to start your UX/UI designer career

14
.
11
.
2023
Bartłomiej Bednarski
Design
Tutorial
HR