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
end
But 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
end
I 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
end
The 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
end
The (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
end
That'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
end
Since 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.