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
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 class Order 2 attr_accessor :date 3 4 def confirmed_date 5 @date.to_date 6 end 7 end 8 9 o = Order.new 10 puts o.date.to_date
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 require 'active_support' 2 require 'active_support/core_ext/object/try' 3 4 class Order 5 attr_accessor :date 6 7 def confirmed_date 8 @date.try(:to_date) 9 end 10 end
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 # from lib/active_support/core_ext/object/try.rb 2 3 class NilClass 4 def try(*args) 5 nil 6 end 7 8 def try!(*args) 9 nil 10 end 11 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
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 module NilClassRefinements 2 refine NilClass do 3 def to_date 4 nil 5 end 6 end 7 end 8 9 using NilClassRefinements 10 11 class Order 12 attr_accessor :date 13 14 def confirmed_date 15 @date.to_date 16 end 17 end 18 19 o = Order.new 20 puts o.date.to_date.inspect
The above also returns
@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!