Decorators, presenters, and value objects in Ruby: my thoughts

| ruby patterns

I had an interesting conversation with a friend about decorators, presenters, and value objects in Ruby. Here are some of my thoughts.

What brought this on?

Our discussion started with my friend’s statement of intent: “I want to have a value object that quacks like a string, but can format itself in a specific way”. His initial thoughts led him to thinking about subclassing String. That’s not a great idea in and of itself (tl;dr: MRI has a lot of assumptions about handling “primitive” types). But I argued what he was thinking of wasn’t a value object, or at least not just a value object.

Formatting is presentation (I think)

Here’s a hot take: formatting a string means presenting it.

Consider this: when retrieving a specific format of date (e.g. with Date#strftime), it doesn’t actually matter what the underlying object is - or rather, how it is represented. Ruby’s Date is just a bunch of integers under the hood, after all. You could totally do this:

class MyDate
  attr_reader :year, :month, :day
  def initialize(year:, month:, day:)
    @year = year
    @month = month
    @day = day
  end
end

def format_my_date(input)
  "#{input.year}-#{input.month}-#{input.day}"
end

I mean - please don’t do this, but you could. And the output would be indistinguishable from this method’s, which accepts a Date:

def format_date(input)
  input.strftime('%Y-%-m-%-d')
end

Similarly, my friend’s object doesn’t have to be a String, internally. He could store it as an Array of ordinals for the appropriate characters. (Again, doesn’t mean that he should - looking at you, buddy! - but he could.)

Edge case: formatting as part of equality

As we all know, because we all read Martin Fowler’s Catalog of Enterprise Application Architecture1, a value object’s equality is not based on identity. Instead, one could argue that being able to check equality in fancy ways is one of the “always good” reasons to have a value object.

So let’s imagine a simple value object where equality is based on formatting.

class SomeValue
  def initialize(value)
    @value = value
  end
  
  def value
    @value.trim
  end
  
  def ==(other)
    value == other.value
  end
end

SomeValue.new(' asdf ') == SomeValue.new('asdf') #=> true

Trimming a string is formatting. True story.

So I suppose there exists a reason to have a value object which does formatting, but it comes with two pretty large caveats:

  • the intended equality comparison for the object has to require formatting,
  • and the formatting function has to be defined such that at least one case exists where $$x \neq y \Leftrightarrow f(x) = f(y)$$.

Decorators expand an object’s contract

While quacking like the underlying object. That is, they expose all of the underlying object’s contract, and then some. While I appreciate there may be reasons to deviate from the underlying object’s contract more than amending it, I haven’t yet seen a real-world use case where that would be compelling.

To fulfill this description, I like using SimpleDelegator. The example given by RubyDocs illustrates the point brilliantly, so I’ll quote it verbatim:

class User
  def born_on
    Date.new(1989, 9, 10)
  end
end

require 'delegate'

class UserDecorator < SimpleDelegator
  def birth_year
    born_on.year
  end
end

decorated_user = UserDecorator.new(User.new)
decorated_user.birth_year  #=> 1989

This is exactly what I mean: we could still call decorated_user.born_on and it would behave as expected. In addition to that, we get the utility method birth_year which expands on the User contract.

It could be argued that my friend’s intent could be achieved with SimpleDelegator:

require 'delegate'

class FancyString < SimpleDelegator
  def fancily_formatted
    trim
  end
end

decorated_string = FancyString.new(' asdf ')
decorated_string.fancily_formatted #=> 'asdf'

But, since I’ve already argued that formatting is presentation…

Presenters translate between contracts

Presenters are commonly thought of in terms of the MVC architecture (though arguably they are dangerously close to MVVM). In that context they take care of presenting a collection of one or more models to the view. It’s a useful pattern which prevents too much logic leaking into the view itself.

However, I think that the presenter pattern is useful anywhere we need to translate between differing contracts. The presenter then becomes the single bit of coupling: if either side of the presenter needs to change, parts of the presenter need to change - but crucially, the other side of the presenter doesn’t need to change.

Presenters shield areas of logic from changes

Consider the following contrived example:

class ValueProducer
  def produce
    '10'
  end
end

class ValueConsumer
  def consume(value)
    value.tr('1', '2')
  end
end

class ValuePresenter
  def initialize(value)
    @value = value
  end
  
  def value
    @value.produce
  end
end

presenter = ValuePresenter.new(ValueProducer.new)
ValueConsumer.new.consume(presenter.value) #=> '20'

ValueProducer produces a string at the moment, and ValueConsumer expects a string. All is well.

Imagine that we then need to implement a change in ValueProducer:

class ValueProducer
  def produce
    100
  end
end

It now returns a Number from produce. If we didn’t introduce a presenter, we would have to track down callsites of ValueProducer and modify each one. Instead, we can simply adjust ValuePresenter:

class ValuePresenter
  # ...

  def value
    @value.produce.to_s
  end
end

In a real-world scenario, we would probably only have to adjust the specs for ValuePresenter and ValueProducer. ValueConsumer is none the wiser that anything changed.

Presenters shouldn’t use delegation

In the example above, it may have been tempting to initially delegate produce to @value, or generally express the presenter with a SimpleDelegator. I believe this is incorrect. It doesn’t allow us to cleanly decouple the two areas of logic from one another - instead, we’d be “coupling with extra steps”. In case the ValueProducer contract changes, as shown, we get no benefits of presentation, and all the headache.

Worse still, the class of bugs this generates may be hard to track. It’d be pretty apparent what happened if ValueProducer’s contract changed drastically - e.g. produce changing arity from zero to a different value.
Slighter changes, such as returned type, may be harder to track. This would be a non-issue in a statically-typed language, but Ruby isn’t one.2

This isn’t hard and fast

These thoughts are based on what has worked for me over ten years of fun in Ruby. This isn’t gospel; it may well not even be correct. However, I feel like these rules of thumb may be useful to others, so I’m sharing. Feel free to discuss in the comments, remembering to come in good faith and kind spirits.


Hero image by Huy Nguyễn from Pixabay


  1. Because we’re all nerds. ↩︎

  2. I’d love to say it’s not one yet, but this was 11 years ago. RBS is not static typing; I quite frankly don’t know what RBS is, but that’s a topic for another day. ↩︎

Built with ❤ by Paweł J. Wal in 2023. Hugo helped.

Blog contents, except where otherwise noted, are CC BY-SA 4.0. Code of this blog is MIT.

Toggle dark/light mode