Vita Rara: A Life Uncommon

Refactoring: Introduce Local Extension in Ruby


Categories: | |

I was recently reading Martin Fowler's Refactoring. While I was reading Introduce Local Extension (p. 164) all I could think of was how simple it is to implement in Ruby.

Motivation

As Fowler says in the motivation section writers of classes and libraries are not omniscient. Inevitably some class that you're using will be missing some feature you want. Using Introduce Local Extension you can add that additional functionality when you need it.

In a statically typed language, like Java, this refactoring would involve creating either a wrapping class or a descendent of the original class. In Ruby we will accomplish the same thing using a mixin.

Mechanics

  • Create a module and add the new methods to the module.
  • - or -
  • Add new methods to the singleton of the instance you want to extend.

Example Using a Module

In this example we need to determine if a time is inclusively between two times. In a static language utility methods like this frequently end up in utility classes consisting of a collection of static methods. Introduce Local Extension allows us to keep the functionality together with the data, our date object.

First we create a module that has our new method in it.

module TimeUtils
  def between_inclusive? (time1, time2)
      self >= time1 && self <= time2
  end
end

With the module complete we can use it to introduce this extension to any instance of Time.

time = Time.now
time.extend(TimeUtils)
puts time.between_inclusive?(time - 3600, time + 3600)

Using this methodology you can add a significant amount of functionality to an object at runtime. This is a good strategy to use, if you do not wish to add the methods to the original class. (Remember Ruby has open classes. We could just have easily re-opened Time and simply add between_inclusive? to it.)

Example Using the Instance Singleton

We will again add the between_inclusive? method to an instance of Time. In this example we'll use Ruby's ability to add a method to an instance of a class.

time = Time.now
def time.between_inclusive? (time1, time2)
  self >= time1 && self <= time2
end
puts time.between_inclusive?(time - 3600, time + 3600)

This method isn't as DRY or as flexible as using a Module. It is also hard to test. I would personally stay away from it, unless you have a specific need.

With that caution I have used this method for doing a local extension along with Ruby's ability to alias methods to add validations to an ActiveRecord model that covered a rarely used edge case. Using the local extension in that case kept my model class far cleaner, and allowed all of the logic dealing with the edge case to be kept in one place.

model # An instance of an ActiveRecord model.

# Open the singleton class of model.
class << model
  alias_method :old_validate, :validate
  def validate
    old_validate
    # Add local validation as needed
    ...
  end
end

model.validate

In this particular instance we have injected behavior into the validate method that normally would not be there, we have preserved the existing validate method, calling it from our new validate method. This allows us to use introduce local extension and preserve integration with ActiveRecord. (NB: Again I'll caution it should be a rare case where you need to use this technique. If you start using it commonly you're doing something wrong.)

Example Using Open Classes

Since Ruby has open classes you are free to add functionality to any class you want. If you wanted between_inclusive? to be available on every instance of Time in your application you can simply add it.

# Place this in a file you require when your application starts.
class Time
  def between_inclusive? (time1, time2)
      self >= time1 && self <= time2
  end
end

Now you can call between_inclusive? on any instance of Time in your application.

time = Time.now
puts time.between_inclusive?(time - 3600, time + 3600)

Conclusion

Using the Ruby's dynamic features, mixins and singleton classes, we are able to easily introduce local extensions in our code. Unlike in a static language there is no need for a subclass or a wrapper using delegation. Using the module method our code can be kept DRY and testable.