Fixing Ruby’s Inheritance Model with Metamorph

By Loren Segal on March 03, 2010 at 325:120:536 PM

Warning: I'm not really advising anybody to use this hack in production code, but it is an interesting bit of code that may have a few good use cases.

Last night I thought of a really neat trick to solve this long-running “_to alias_method_chain or not to alias_method_chain_” feud. It’s completely transparent, meaning it requires absolutely no modification to your original code. More importantly, it essentially changes Ruby’s inheritance model to what I would consider the “correct” model, with respect to mixin behaviour.

A Little Background

The issue that people bump their heads against can be expressed by the following Ruby example. Say I’m using a library that defines some class ‘A’:

class A
  def foo; "foo" end
end

Being the Rubyist that I am, I want to be able to extend this method and add my own behaviour, but without wiping out the original. This is where alias_method_chain traditionally comes in. I would normally just rename the method and call it myself from my overridden method. The problems with that have been discussed ad infinitum, and people have realized that mixing in a module is proper OO, much cleaner, maintains inheritance, etc.. so let’s do that:

module B; def foo; super + "bar" end end
class A; include B end

Problem! You probably noticed this. “But ‘foo’ is defined directly in A,” you say, “you can’t override it with a mixin!”

This is the ultimate problem with Ruby’s inheritance model, in my opinion. Because mixins always take lower priority than methods defined directly in a class body, A#foo can not be overridden by mixin inclusion. Moreover, because mixins take lower priority than methods, A#foo is now violating encapsulation, but to see this we will need to modify our example slightly.

What is Proper Encapsulation, Anyway?

Encapsulation is the idea that only a component knows about its internal structure. But structure in OOP implies your inheritance tree as much as it implies your data. Suppose our ‘A’ class inherited some class ‘X’ and the A#foo method we’ve been calling really calls the super method X#foo:

class X
  def foo; "foo" end 
end
class A < X; def foo; super end end

When our class ‘A’ calls super, it expects that super should be class ‘X’, because this is its internal structure as defined by A. By this standard, mixing in another module should not change A’s internal expectations. Mixing in a module should create the following encapsulation, where B encapsulates A:

Correct Encapsulation

This is what mixing in a module after A has been created should look like. However, Ruby does not wrap our ‘A’ class, instead it injects itself under A:

Incorrect Encapsulation

Here we have modified the “internal structure” of A, violating proper encapsulation. Whether or not this is “right” or “wrong” depends on how you view the concept of re-opening a class in Ruby. However, pragmatically speaking, this behaviour causes a lot of problems with the way people tend to write code in Ruby-land (and, in general, many languages). Firstly, it requires that developers design for the possibility that a module would be inserted into itself rather than wrapped around, which means to properly accept mixins you should always call “super” in all of your methods since you never know what your inheritance tree will look like. Secondly, it requires you to opt-in to allowing a method to be overridden by a mixin by performing very explicit code transformations that make your code harder to document and add needless complexity. I’m referring to the ClassMethods and InstanceMethods module idioms that Rails likes to use. These are problematic because most documentation tools will show these methods as being “inherited” by the class (if at all), and although this is technically correct, these methods now need to be addressed and looked up as ClassName::InstanceMethods#method_name which is unintuitive for the user. Of course this is in addition to actually organizing your code in this manner, which is, in general, an unintuitive step. These are all workarounds to deal with the failings of a proper inheritance model.

Solving Encapsulation Transparently

So how can we solve the inheritance/encapsulation problem and do so with as small a footprint as possible? This is what I came up with last night:

class Object
  def self.method_added(name)
    return if name == :initialize
    const_set(:InstanceMethods, Module.new) unless defined?(self::InstanceMethods)
    meth = instance_method(name)
    self::InstanceMethods.send(:define_method, name) {|*args, &block| meth.bind(self).call(*args, &block) }
    remove_method(name)
    include self::InstanceMethods
  end
end

This simple hook transparently defines all new methods inside an anonymous inner module, essentially doing this source transformation for us on the fly. As mentioned, it requires no modification to the source code you’re trying to modify, simply add this code before including the rest. For instance, with the above hook, we can now do the following with our original example:

class A; def foo; "foo" end end
module B; def foo; super + "bar" end end

A.new.foo # => "foo"
A.send(:include, B)
A.new.foo # => "foobar"

Note that we need not change the code from our original example and our mixed in module now takes precedence over the explicitly defined A#foo.

Hold on There, Cowboy

“But that looks so inefficient!” you scream. For those who noticed, you will see that we’ve been replacing every method with a delegate Proc object that performs quite a lot of crap before dispatching another method call. Yes, it’s inefficient. There is, however, a better way to do this, but it involves native code. I’ve implemented the native version for 1.9.x, but I’ll spare you the details and code. If you care, you can check the github link (see below).

The Metamorph Gem (1.9 Only)

In 1.9 you can do a gem install metamorph to get this gem. It’s a native gem, so it requires a compile. For 1.8 (or Windows machines) you can use the above non-native version, but it will probably be slower.

The code is on github at: http://github.com/lsegal/metamorph, so if you want to make it work in 1.8, feel free.

PS. It’s not that it’s impossible to implement (efficiently) in 1.8, it’s simply that I don’t personally care about your legacy Rubies the same way DHH never cared about your legacy DBs. Oddly enough, people seem to love their legacy Rubies and still manage to hate legacy databases, but that’s a rant for another day.

Questions? Comments? Follow me on Twitter (@lsegal) or email me.