The value object pattern

Ruby on Rails / Refactoring

Similar to null object pattern, Value Object is a Ruby plain object. Such object represents a value but not something unique in your system like a user object. Value Objects always return only values. They are like policy objects but instead of boolean values, they can return also other values, mostly strings. The rule of this pattern is to keep it simple and don’t change attributes value during object life cycle.

Demonstration

To better demonstrate the idea behind this refactoring pattern let’s build example class:

class Report
  def initialize(emails:)
    @emails = emails
  end

  def data
    emails_data = []

    emails.each do |email|
      emails_data << {
        username: email.match(/([^@]*)/).to_s,
        domain: email.split("@").last
      }
    end

    emails_data
  end

  private
  attr_reader :emails
end

We are doing a few things with given email:

  1. We don’t change email value
  2. We return only values
  3. We are operating on a primitive object

Building a value object

If we would build a value object from above logic, our class would look like this:

class Email
  def initialize(email)
    @email = email
  end

  def username
    email.match(/([^@]*)/).to_s
  end

  def domain
    email.split("@").last
  end

  private
  attr_reader :email
end

Refactoring with the value object pattern

We end up with a very simple Ruby object, easy to test and understand. After refactoring Report class final solution is quite clear and simple:

class Report
  def initialize(emails: emails)
    @emails = emails
  end

  def data
    emails_data = []

    emails.each do |email|
      email_obj = Email.new(Email)

      emails_data << {
        username: email_obj.username,
        domain: email_obj.domain
      }
    end

    emails_data
 end

 private
 attr_reader :emails
end

Final refactoring

Since our Report#data method is simpler now and logic is separated it’s still quite long. Let’s create #to_h method in Email report that would return a hash representation of the object. For me it’s very natural and allows us to transform Report#data to one-liner:

class Email
  def initialize(email)
    @email = email
  end

  def username
    email.match(/([^@]*)/).to_s
  end

  def domain
    email.split("@").last
  end

  def to_h
    { username: username, domain: domain }
  end

  private
  attr_reader :email
end
class Report
  def initialize(emails: emails)
    @emails = emails
  end

  def data
    emails.map { |email| Email.new(email).to_h }
  end

  private
  attr_reader :emails
end