Ruby Refinements

The Ruby language provides many powerful tools for software engineers to utilize. For instance, classes that have been previously defined and evaluated can be reopened and changed. This is commonly referred to as “monkey patching”, a term which elicits almost universal disdain among Ruby developers.

The Problem

A reason that monkey patched code has issues lies in the scope of the changed code. If previously defined code is changed at an arbitrary time, all other parts of the application suffer from those changes.

Why would someone need to change an existing class? Perhaps an included gem needs to be altered in a small way to behave correctly in a very specific system. Or maybe, there exists a very dark area of a codebase that must not be touched directly for fear that the entire application will go under.

Whatever the case, patching code that has been already defined happens, and it usually happens poorly.

These problems can be especially nefarious if the patches are not in automatically loaded files. For example, say we have a Dog class:

dog.rb

class Dog attr_accessor :trained def bark "woof woof" end end

Then in file loaded later, the bark method changes to be much more formal:

training.rb

class Training def train ( dog ) dog . trained = true dog . bark end end class Dog def bark "Woof woof, good sir." end end

After the Traning class is loaded, any consumers of the Dog class will be in for a surprise whenever the bark method is called. Even worse, when a confused developer opens the dog.rb class to check bark ’s functionality, they will not see the patched version.

dog = Dog . new dog . bark # => woof woof require './training' training = Training . new training . train ( dog ) # => Woof woof, good sir. dog = Dog . new dog . bark # => Woof woof, good sir.

As shown, the second initialized Dog barks the same way as the trained Dog . Globally, the way a Dog barks has been changed after the training file has been included.

Enter Refinements

An alternative way to extend a class’ functionality is by using the built in ruby construct: Refinements . Refinements are context specific alterations to a class’ methods.

To refine the Dog class, a module needs to be written:

module SophisticatedDog refine Dog do def bark "Woof woof, good sir." end end end

The interesting piece here is the refine method. This method returns an overlaid module specific to the class passed into it, allowing a very small scoped change of the Dog class.

To include this Refinement in the place where it is needed, the using method can be added to the Training class.

class Training using SophisticatedDog def train ( dog ) dog . trained = true dog . bark end end

Now, the changes to the Dog method are contained within the Training class:

dog = Dog . new dog . bark # => woof woof require './training' training = Training . new training . train ( dog ) # => Woof woof, good sir.

Neither the passed in dog, nor newly created dogs’ bark method has been permanently changed:

dog . bark # => woof woof dog = Dog . new dog . bark # => woof woof

No more surprise behaviour! Each new Dog is created just as unrefined and unsophisticated as ever.

Super Cool

Another problem with traditional monkey patching is that the patched method is no longer accessible.

class Dog def bark 'woof woof' end end # Then later: class Dog def bark super + ', good sir.' end end Dog . new . bark # => NoMethodError: super: no superclass method `bark' for Dog

The original implementation of Dog had a very specific string that might not want to be duplicated. When the class is monkey patched, the original method is replaced and super is not accessible.

In a less silly example, maybe the method being patched had some valuable code a patch could have used. Since Refinements are not strict code overwrites, they maintain the super functionality found in inheritance:

class Dog def bark 'woof woof' end end module SubWoofer refine Dog do def bark super . split ( ' ' ). first end end end class DogTest using SubWoofer def call dog = Dog . new dog . bark end end DogTest . new . call # => 'woof' Dog . new . bark # => 'woof woof'

The SubWoofer Refinement was able to call the existing Dog#bark method and change it how it saw fit. This keeps the code around barking DRY. Since Refinements are intended to extend or refine existing code, it makes sense that they could use the existing code to their benefit. When that existing code is changed, it would be arduous to make every single Refinement aware.

Using Refinements is a nice alternative to the sledgehammer that is monkey patching. Bending a class to fit a very specific need in an encapsulated manner can mean the difference between a stable system and one riddled with hard to track down bugs. While Refinements still have their own drawbacks, they are much less intrusive than the alternative.