
Written July 31, 2013. Tagged Ruby, Metaprogramming.
When you want to override methods defined by a library, super is more convenient than alias_method:
class Post < SomeORM
attributes :title, :body
def title
super || "No title"
end
endBut as these things are commonly implemented, you need to use alias_method (or Rails' alias_method_chain) instead, which is less convenient:
class Post
attributes :title, :body
alias_method :old_title, :title
def title
old_title || "No title"
end
endI would encourage library writers to make their dynamically defined methods super-overridable whenever possible; this post explains how.
The common implementation uses define_method straight on your own class:
class SomeORM
def self.attributes(*names)
names.each do |name|
define_method(name) do
# Stuff
end
end
end
endThe library puts the method right in your class; right in Post. So if you define your own method by the same name in Post, you completely replace that old method, with no way to reach it. Thus you need alias_method.
But if the library could instead put the method further up the inheritance chain, we could define our own method and just call super.
So what's further up the inheritance chain? The SomeORM superclass of course, and its chain of superclasses. But we can't define methods there, or those methods would be available to every SomeORM subclass, not just Post.
What else is further up the inheritance chain? Modules.
Say we have this Ruby class:
class Post < SomeORM
include SomeModule
endThe (slightly simplified) inheritance chain would then be [Post, SomeModule, SomeORM, Object]. Included modules go between the class itself and its parent class, and super will look in those modules before it gets to the parent class.
So we simply create a module, define methods on that, and include it:
class SomeORM
def self.attributes(*names)
mod = Module.new
include mod
names.each do |name|
mod.module_eval do
define_method(name) do
# Stuff
end
end
end
end
endThat's all it takes.
What does the inheritance chain look like now? Something along the lines of [Post, #<Module:0x007fa0fea7fcf0>, SomeORM, Object].
We defined an anonymous module, which works fine but could be confusing in a later debugging session.
Also, the current implementation will create a separate module for each attributes declaration. If you define ten attributes each on their own line, you'll add ten modules to the inheritance chain.
While this works, it makes for a messier inheritance chain.
We can solve both these issues in one fell stroke:
class SomeORM
MODULE_NAME = :DynamicAttributes
def self.attributes(*names)
if const_defined?(MODULE_NAME, _search_ancestors = false)
mod = const_get(MODULE_NAME)
else
mod = const_set(MODULE_NAME, Module.new)
include mod
end
names.each do |name|
mod.module_eval do
define_method(name) do
# Stuff
end
end
end
end
end(I provided a somewhat different solution earlier; this improved solution is due to Avdi Grimm.)
The added code does a few things. It names the constant Post::DynamicAttributes, so now the inheritance chain will be something like [Post, Post::DynamicAttributes, SomeORM, Object].
It checks if Post::DynamicAttributes already exists. If it does, it's reused. This means we only add a single module to the inheritance chain even if Post declares ten attributes each on their own line.
The second argument in const_defined?(MODULE_NAME, _search_ancestors = false) is important, by the way. Say we introduce a VideoPost that inherits from Post:
class VideoPost < Post
attribute :timestamp
endSince we passed false as the second argument of const_defined?, we will not reuse Post::DynamicAttributes from the superclass, but will instead get a new VideoPost::DynamicAttributes module. This ensures attributes inherit correctly.
You could of course write just const_defined?(MODULE_NAME, false) without the _search_ancestors local variable. I feel it makes a cryptic argument more obvious, but it does trigger an "unused variable" warning in Ruby <2.0 with the -w flag.
I've implemented this in Traco and Minimapper.
Implementing it in Traco last December was inspired by points brought up in Ruby Rogues episode 80, in turn based on the talk "The Polite Programmer's Guide to Ruby Etiquette" by Jim Weirich et al. (which I haven't watched).
I'd love to hear about other libraries that get this right, alternative solutions, or problems with this approach.