Rails Value Objects – One Validator to Rule Them All

Problem

Value objects in Ruby are lovely things, and invaluable in a complex application.

But one issue that always seems to trip me up is correctly initialising from other objects, so while we might want to write:

class ISBN def initialize ( value ) @isbn_string = value end def etc end end

... it turns out in practice that sometimes you're creating a value object based on string input, and sometimes it might make sense to store the value in the database as a numeric but you can be sure that a controller will receive it as a string, and other times it already is a value object.

Enough of all that nonsense – you need to be able to initialise your value object based on almost anything that might represent its value.

Making it so

So what do you think of this?

class ISBN def initialize ( value ) @isbn_string = if value . is_a? String value . gsub ( /[^[:digit:]]/ , "" ) elsif value . is_a? Integer value . to_s elsif value . is_a? NilClass "" end end end

Pretty bad.

One of the principles of object-oriented design that you might pick up along the way is that this sort of code is A Bad Thing, because we're not supposed to care about what type/class an object is, only about what it can respond to.

Now if we were sending the a message #address to an object, we'd be fine – we'd just expect the Person/Company/Owner/Invoice etc classes to implement that method, and because they are our own domain objects and we write the code for them, we can define whatever behaviour we want on them.

But what do we do when the object is one of the Ruby core classes? String , Integer , or even NilClass ?

One answer is to monkey patch, but by now I think we all know that that is Also A Bad Thing and that we should instead be implementing a refinement.

How about this?

module ISBNInitializerExtensions refine String do def to_isbn_string gsub ( /[^[:digit:]]/ , "" ) end end refine Integer do def to_isbn_string to_s end end refine NilClass do def to_isbn_string "" end end end

It's not too bad, as the ISBN initialisation then becomes:

class ISBN using ISBNInitializerExtensions def initialize ( value ) @isbn_string = value . to_isbn_string end def etc end end

We can scatter ISBN.new(obj) around the system and the ISBN class doesn't need insights into the cleansing for a string or a number. It assumes that the input value has implemented it.

But we don't do Integer.new("78") very much, do we? We like "78".to_i instead. (OK, we can do the explicit method, but only when we need the specific behaviour that that brings with it. Otherwise we just send #to_i ).

We also need to know the name of the class that implements the ISBN object, and how to initialise it, but maybe that's not such a big deal.

Taking it a little further, what about:

module ISBNInitializerExtensions refine String do def to_isbn ISBN . new ( gsub ( /[^[:digit:]]/ , "" )) end end refine Integer do def to_isbn ISBN . new ( to_s ) end end refine NilClass do def to_isbn ISBN . new ( "" ) end end end

This module becomes the location in the system where we convert core Ruby classes to the application-defined class ISBN, and the only place where we need to do refactoring if we wanted to change the name of the ISBN class.

And we could implement similar logic for #to_zip_code , #to_country , #to_currency , etc., with similar benefits.

And then, secure in the knowledge that the classes which might need to return it have already implemented the clean-up, we can write:

class ISBN def initialize ( value ) @isbn_string = value end def etc end end

And we're back where we started, with a nice, clean initialisation and the ability to:

"978-3-16-148410-0" . to_isbn 9783161484100 . to_isbn nil . to_isbn

.. as long as we have first added using ISBNInitializerExtensions to the class or module where we want to use it (which admittedly seems to make it tricky in Rails views for some reason).

For completeness, we can ...

class ISBN def initialize ( value ) @isbn_string = value end def to_isbn self end def to_s @isbn_string end end

This does make value object definitions in Rails a thing of very little code:

class Book using ISBNInitializerExtensions def isbn @_isbn ||= self [ :isbn ]. to_isbn end def isbn = ( obj ) self [ :isbn ] = obj . to_isbn . to_s end end

Summary

So there are two options here:

Refine the clean-up of the parameters, taking them out of the initialiser, and letting us do ISBN.new(obj) for fairly arbitrary classes of obj. Refine the complete transformation, allowing obj.to_isbn within any class or module where we have invoked the use of the appropriate refinement.