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