Ruby Class Inheritance II: Differences between inheritance and mixin

Guys familiar with Rails are very likely used to the following code, and will not be surprised by it:

ActiveRecord
1
2
3
4
5
6
class User < ActiveRecord::Base
end
first_user = User.find(0)

But actually the code is not as simple as it looks like, especially for the ones from Java or C# world.
In this piece of code, we can figure out that the class User inherited the method find from its parent class ActiveRecord::Base(If you are doubt or interested in how it works, you can check this post Ruby Class Inheritance).

If you write the following code, it should works fine:

Simple Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base
def self.foo
bar_result = new.bar
"foo #{bar_result}"
end
def bar
'bar'
end
end
class Derived < Base
end
Base.new.bar.should == 'bar'
Derived.new.bar.should == 'bar'
Base.foo.should == "foo bar"
Derived.foo.should == "foo bar"

In Ruby’s world, most of the time you can replace a inheritance with a module mixin. So we try to refactor the code as following:

Exract to Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module Base
def self.foo
bar_result = new.bar
"foo #{bar_result}"
end
def bar
'bar'
end
end
class Derived
include Base
end

If we run the tests again, the 2nd test will fail:

Test
1
2
3
4
Dervied.new.bar.should == 'bar' # Passed
Dervied.foo.should == 'foo bar' # Failed

The reason of the test failure is that the method ‘foo’ is not defined!
So it is interesting, if we inherits the class, the class method of base class will be available on the subclass; but if we include a module, the class methods on the module will be available on the host class!

As we discussed before(Ruby Class Inheritance), the module mixed-in is equivalent to include insert a anonymous class with module’s instance methods into the ancestor chain of child class.

So is there any way to make all tests passed with module approach? The answer is yes absolutely but we need some tricky thing to make it happen:

Exract to Module ver 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module Base
module ClassMethods
def foo
bar_result = new.bar
"foo #{bar_result}"
end
end
def bar
'bar'
end
private
def self.included(mod)
mode.extend ClassMethods
end
end
class Derived
include Base
end
Dervied.new.bar.should == 'bar' # Passed
Dervied.foo.should == 'foo bar' # Passed