This post may contain outdated information

I initially wrote this post a while ago and some of its content might be outdated.

While writing my previous post, I realized that while using the attributes API allows us to avoid making mistakes when instantiating our value objects, it might not entirely convey that there's a limited number of objects that can be instantiated [1].

To this end, I thought about a way to prevent this: let's pretend that our value objects are constants.


Generating constants

First, let's take our Ship::Category from the previous post:

class Ship::Category
  VALID_CATEGORIES = %w(
    shuttle supply_carrier troop_carrier war_ship
  ).freeze

  def initialize(category)
    raise "invalid category: #{category.inspect}" unless category.in?(VALID_CATEGORIES)

    @raw_category = category
  end

  def to_s
    raw_category
  end

  private

  attr_reader :raw_category
end

Let's start by defining constants for each of our values. Under the initialize method [2], let's create the constants using const_set and a bit of metaprogramming:

class Shift::Category
  # ...

  VALID_CATEGORIES.each do |raw_category|
    const_set(raw_category.upcase, new(raw_category))
  end
end

Calling Ship::Category.constants show us that our constants have correctly been created:

pry(main)> Ship::Category.constants
# => [:WAR_SHIP, :TROOP_CARRIER, :SHUTTLE, :SUPPLY_CARRIER, :VALID_CATEGORIES]

However, inspecting the constant reveals our trickery:

pry(main)> Ship::Category::SHUTTLE
# => #<Ship::Category:0x00007fbddc853350 @raw_category="shuttle">

So, how do we really pretend that our Ship::Category object is truly a constant ? We can do this by overriding the inspect method:

Overriding inspect

As we can see above, by default, inspect returns the class name, a representation of the memory address of the object and a list of instance variables of the object.

In our case, we want inspect to instead display how the object should be accessed. This means making it look like the constants we've created above:

class Shift::Category
  # ...

  def inspect
    "#{self.class}::#{raw_category.upcase}"
  end
end

With this, the value object is now displayed as a constant when inspecting the object or in logs:

# Before we had:
pry(main)> Ship::Category::SHUTTLE
# => #<Ship::Category:0x00007fbddc853350 @raw_category="shuttle">

# Now we have
pry(main)> Ship::Category::SHUTTLE
# => Ship::Category::SHUTTLE

Besides indulging in my whims, there are other interesting reasons to override inspect:

The code examples in this post are also available on GitHub. Thanks to Bachir Çaoui and Stéphanie Chhim for reviewing a draft version of this post.


Footnotes

  1. Of course, we could look at the file defining the constants, but where would be the fun in that? ↩︎

  2. Because our constants are set directly in the class, the new method needs to be already defined, hence defining them under the initialize method. ↩︎