Using Ruby Refinements in Rails

Its not very often I get the chance to use refinements in Rails. My past experiences with it in the context of Rails applications has been less than satisfactory due to the way the framework handles code reloading and the way refinements’ scopes work.

However, I feel strongly enough to use refinements in a recent project due to the use / abuse of ActiveSupport#try method.

Supposing we have an Order class that has a single attribute, date. Also assuming that we have a method which calls to_date on that attribute like so:

1
2
3
4
5
6
7
8
9
10
class Order
  attr_accessor :date

  def confirmed_date
    @date.to_date
  end
end

o = Order.new
puts o.date.to_date

If the date attribute is nil, the above would throw an undefined method on NilClass error. A common pattern in Rails application is to wrap the above in a try method from ActiveSupport:

1
2
3
4
5
6
7
8
9
10
require 'active_support'
require 'active_support/core_ext/object/try'

class Order
  attr_accessor :date

  def confirmed_date
    @date.try(:to_date)
  end
end

Using the try method will return nil and not throw an exception. Underneath the hood, ActiveSupport has overridden NilClass with a try method to only return nil, disregarding any arguments passed into it:

1
2
3
4
5
6
7
8
9
10
11
# from lib/active_support/core_ext/object/try.rb

class NilClass
  def try(*args)
    nil
  end

  def try!(*args)
    nil
  end
end

This pattern is common in most Rails applications, and if you study the Rails source code, there are many examples given of this specific usage of try. But from a ruby perspective, this seems like a kind of anti-pattern to me. NilClass is just another object from the ruby ecosystem so why can’t we just define the missing to_date method on it in this case and not have to worry about calling another method from a dependency to deal with it? My immediate thought is to apply the NullObject pattern but I think it is overkill in this use case since there is only 1 method calling try.

Rather than overriding NilClass, my approach is to define to_date on it using Ruby refinements which I can use in the context I decide is applicable like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module NilClassRefinements
  refine NilClass do
    def to_date
      nil
    end
  end
end

using NilClassRefinements

class Order
  attr_accessor :date

  def confirmed_date
    @date.to_date
  end
end

o = Order.new
puts o.date.to_date.inspect

The above also returns nil if @date is nil and is easier to understand with the added benefit of not overriding the core data types in Ruby.

Stay curious and keep hacking!