Module 1, Topic 14
In Progress

Utility Function

Module Progress
0% Complete

Let's say we're writing a small web framework. One responsibility of the framework is to generate URLs based on model objects. The code responsible for this makes use of a helper function called #unique_id, which takes an object and returns a short String identifying it.

module ROFLWeb
  module UrlHelpers
    def url_for(object)
      "http://example.com/#{unique_id(object)}"
    end

    def unique_id(object)
      if object.respond_to?(:to_unique_id)
        object.to_unique_id
      else
        "#{object.class.name}+#{object.hash.to_s(16)}"
      end
    end
  end
end

In order to be as flexible as possible, this method can work with both framework-aware objects and "plain-old" Ruby objects that know nothing of the framework. For framework-aware objects, it delegates to a #to_unique_id method on the model object. For everything else, it just combines the classname and object hash value.

This #unique_id method turns out to be useful beyond just generating URLs. It's also helpful for things like storing data in key-value stores, or as a key for cached page content.

module ROFLWeb
  class MemoryRepository
    def initialize
      @repo = {}
    end

    def store(object)
      @repo[unique_id(object)] = object
    end

    def fetch(key, &block)
      @repo.fetch(key, &block)
    end
  end
end
module ROFLWeb
  module UrlHelpers
    def url_for(object)
      "http://example.com/#{unique_id(object)}"
    end

    def unique_id(object)
      if object.respond_to?(:to_unique_id)
        object.to_unique_id
      else
        "#{object.class.name}+#{object.hash.to_s(16)}"
      end
    end
  end
end
module ROFLWeb
  class MemoryRepository
    def initialize
      @repo = {}
    end

    def store(object)
      @repo[unique_id(object)] = object
    end

    def fetch(key, &block)
      @repo.fetch(key, &block)
    end
  end
end

include ROFLWeb::UrlHelpers

repo = ROFLWeb::MemoryRepository.new
repo.store("Hello, World")
puts repo.fetch(unique_id("Hello, World"))

This functionality is useful in many places, and we'd prefer not to duplicate the code. So we decide to put the #unique_id method somewhere centrally accessible. But where should this be?

One possibility is in a ModelUtils module inside the main framework namespace. Every class or module which makes use of it just has to include the ModelUtils module.

module ROFLWeb
  module ModelUtils
    def unique_id(object)
      if object.respond_to?(:to_unique_id)
        object.to_unique_id
      else
        "#{object.class.name}+#{object.hash.to_s(16)}"
      end
    end
  end

  module Urlhelpers
    include ModelUtils
  end

  class MemoryRepository
    include ModelUtils
  end
end

As our framework gathers users, we start seeing more and more of them including the ModelUtils module in their own code. A lot of our users find the #unique_id functionality useful, and they'd like to use the framework version rather than a hand-rolled version of it so as to stay consistent with the unique IDs that the framework uses.

Unfortunately, this comes with some problems. Very often a client only uses the #unique_id method in one place, but in order to gain access to it, they have to mix the ModelUtils module into a whole class. This clutters up their class with methods they either use in a single method, or don't use at all. And these methods become a part of the client class' public interface, even though they are only used internally.

class ClientCode
  include ROFLWeb::ModelUtils

  def log_access(object)
    puts "#{unique_id(object)} accessed at #{Time.now}"
  end
end

In order to better accommodate client coders, we start including two versions of utility methods: an instance-level version, and a module-level version. The instance version delegates to the module version.

module ROFLWeb
  module ModelUtils
    def self.unique_id(object)
      if object.respond_to?(:to_unique_id)
        object.to_unique_id
      else
        "#{object.class.name}+#{object.hash.to_s(16)}"
      end
    end

    def unique_id(object)
      self.class.unique_id(object)
    end
  end
end

Now our framework code can continue to include the ModelUtils module, while client classes can just use the methods they want, when they want them, by calling the module-level versions.

class ClientCode
  def log_access(object)
    puts "#{ROFLWeb::ModelUtils.unique_id(object)} accessed at #{Time.now}"
  end
end

We also decide that, even when the ModelUtils module is included, its methods really shouldn't become part of a class's public methods, since the utility methods are all intended for internal use. To fix this, we make the instance versions of utility methods private.

module ROFLWeb
  module ModelUtils
    def self.unique_id(object)
      if object.respond_to?(:to_unique_id)
        object.to_unique_id
      else
        "#{object.class.name}+#{object.hash.to_s(16)}"
      end
    end

    private

    def unique_id(object)
      ModelUtils.unique_id(object)
    end
  end
end

This seems like an optimal solution except for one thing: every time we add a utility method, we have to add both a module-level version and instance version. This is tedious, non-dry, and easy to forget.

Here, an obscure Ruby feature comes to our aid. The module_function macro will take an instance method and copy it into a class-level version. At the same time, it makes the original instance-level version private, exactly as we had been doing.

module ROFLWeb
  module ModelUtils
    def unique_id(object)
      if object.respond_to?(:to_unique_id)
        object.to_unique_id
      else
        "#{object.class.name}+#{object.hash.to_s(16)}"
      end
    end

    module_function :unique_id
  end
end

Even better, module_function can also be used the same way as Ruby's private and protected macros: call it without an argument, and it will give every method defined after it the module_function treatment. So We can just declare module_function at the beginning of our utility module, and then proceed to define our methods just once.

module ROFLWeb
  module ModelUtils
    module_function

    def unique_id(object)
      if object.respond_to?(:to_unique_id)
        object.to_unique_id
      else
        "#{object.class.name}+#{object.hash.to_s(16)}"
      end
    end
  end
end

We now have a central place to put commonly used utility functions that affords maximum flexibility to our own code as well as to client code. Classes that use more than one of the utility methods, or which use one of the methods over and over again, can include the module to have those utilities added as convenient private methods. Code that just needs to call a utility method once or twice can do so without inheriting from ModelUtils by using the class-level version.

We accomplished all this without repeating ourselves by using module_function. module_function is little-known, probably not least because it's naming is not exactly intuitive. But as we've just seen it can save a lot of code in certain situations. So it deserves a place in our toolbox.

That's it for now. Happy hacking!

Responses