Flexible Ruby Value Object Initialisation

MetaDave 🇪🇺 on January 21, 2019

Problem Value objects in Ruby are lovely things, and invaluable in a complex application. But one issue that always seems to trip me... [Read Full]
markdown guide
 

If your OO approach means to be omnivorous,

class ISBN
  def initialize(value)
    @isbn_string = value.to_s.gsub /\D/, ''
  end
end

If you want to write idiomatic ruby code, use triple-equals instead of nasty elsifs:

class ISBN
  def initialize(value)
    @isbn_string =
      case value
      when String then value.gsub /\D/, ''
      when Integer, NilClass then value.to_s
      when ->(v) { v.respond_to?(:to_isbn) } then v.to_isbn
      else raise
      end
  end
end
 

Indeed, Ruby does conversions very well, as you show in the first code sample.

I do like case, but again it's a type check. I'd rather avoid them if I can. It's good code that, though.

 

I do like case, but again it's a type check.

Except the last clause is literally a duck check you wanted.

Yes, it is a duck check, but I don't want a type check or a duck check. Or any kind of check, really.

What I'm describing is how to achieve the aim of not doing those checks. If that is not an aim that someone shares then they of course are free to implement a different methodology.

You gotta be kidding. Your code explicitly depends on several classes refinements. If this is not a type check, I do not know what is.

I don't see it as anything other than enabling 100% duck typing in the application. I'm ensuring that the application depends only on the ability to respond, not on the underlying object type.

Negative. Proc clause in the case example I provided is depending only on the ability to respond.

Refinements are required to exist upfront: no 3rd party code that responds, but does not have a clue about them is accepted.

Hiding the type check in the module does not make it magically stop being a type check.

In itself, adding responses to a class clearly does not constitute type checking, it is simply "adding responses to a class".

So are you saying that needing to reference the module that adds the behaviour constitutes a form of type checking?

I am saying refinements depend on the type. 3 types in your example were explicitly checked and extended with some functionality. Other types, no matter whether they respond to / implement this functionality are rejected.

There is no way to supply my own type that mimics the functionality to the constructor, without monkey patching your code.

This is exactly how we do it in some Java, but this is not what I would call idiomatic ruby.

Refinements depend on the type

Everything in some way depends on the type, because the type defines the messages that can be received and the responses. That is not type checking, though, that is just code.

Other types, no matter whether they respond to / implement this functionality are rejected

If they respond to the correct message then they are not rejected by my ISBN class, because my ISBN class is not type checking. The refinements are just a way of adding the required responses that is safer than monkey patching, but ultimately they just define behaviour.

I would be keen on hearing other opinions, though.

 
 

Nice use of refinements. I've not seen them around much, but this is a good example of how they can really clean up an approach.

 

Thanks Phil. I like the way they put the Ruby core objects on a level playing field with your own application objects, and let them add the behaviour you want.

Having said that, I'd probably draw a line at doing anything like:

module EasyFinder
  refine Integer do
    def to_book
      Book.find(self)
    end
  end

  refine String do
    def to_book
      Book.find(self.to_i)
    end
  end
end

... if I thought anyone was ever going to see my code.

I like the semantics, but it's a bit "out there".

code of conduct - report abuse