Decorators, presenters, and value objects in Ruby: my thoughts
Decorators, presenters, and value objects in Ruby: my thoughts
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:
1class MyDate
2 attr_reader :year, :month, :day
3 def initialize(year:, month:, day:)
4 @year = year
5 @month = month
6 @day = day
7 end
8end
9
10def format_my_date(input)
11 "#{input.year}-#{input.month}-#{input.day}"
12end
I mean - please don’t do this, but you could. And the output would be indistinguishable from this method’s, which
accepts a Date
:
1def format_date(input)
2 input.strftime('%Y-%-m-%-d')
3end
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.
1class SomeValue
2 def initialize(value)
3 @value = value
4 end
5
6 def value
7 @value.trim
8 end
9
10 def ==(other)
11 value == other.value
12 end
13end
14
15SomeValue.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:
1class User
2 def born_on
3 Date.new(1989, 9, 10)
4 end
5end
6
7require 'delegate'
8
9class UserDecorator < SimpleDelegator
10 def birth_year
11 born_on.year
12 end
13end
14
15decorated_user = UserDecorator.new(User.new)
16decorated_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
:
1require 'delegate'
2
3class FancyString < SimpleDelegator
4 def fancily_formatted
5 trim
6 end
7end
8
9decorated_string = FancyString.new(' asdf ')
10decorated_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:
1class ValueProducer
2 def produce
3 '10'
4 end
5end
6
7class ValueConsumer
8 def consume(value)
9 value.tr('1', '2')
10 end
11end
12
13class ValuePresenter
14 def initialize(value)
15 @value = value
16 end
17
18 def value
19 @value.produce
20 end
21end
22
23presenter = ValuePresenter.new(ValueProducer.new)
24ValueConsumer.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
:
1class ValueProducer
2 def produce
3 100
4 end
5end
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
:
1class ValuePresenter
2 # ...
3
4 def value
5 @value.produce.to_s
6 end
7end
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
is not static typing; I quite frankly don’t know what RBS is, but that’s a topic for another day.
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.
- Because we’re all nerds. [return]
- I’d love to say it’s not one yet, but this was 11 years ago. RBS [return]