[ ].blue

Exploring Multi-Inheritance in Ruby

September 12, 2012

Ruby allows a kind of multi inheritance. First, ruby allows single class to class inheritance found in every traditional OO language.

1
2
3
4
5
class A
end

class B < A
end

In addition to class inheritance, ruby also provides modules or "mixins" which are a kind of interface coupled with implementation. These modules often contain class-agnostic behaviors or traits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Printable
    def print
        puts "Printed!"
    end
end

class A
    include Printable
end

obj = A.new
obj.print

# Output:
# => Printed!

That's all well and good, but what happens if the same method is defined on both the module and the class?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Printable
    def print
        puts "Printed!"
    end
end

class A
    include Printable

    def print
        puts "A!"
    end
end

obj = A.new
obj.print

# Output:
# => A!

In these cases ruby follows the rule of "closest proximity wins". In this case the print method defined on A is closer to obj. Ruby determines this by running a "search path", looking first at the object instance, then at the child-most class, then that class' it's mixins, then the parent classes, and the parent class' mixins and so on up the chain.

Where things begin to get interesting is that ruby provides a super method. When ruby finds a match to a given method, it stops and runs that method. super basically tells ruby to execute the possible match in the search path. If A#print were modified to include super, the output would become:

1
2
3
# Output:
# => A!
# => Printed!

This is because ruby would first find A#print, then calling super would invoke Printable#print.

In considering this, it's a bit different than most OO languages. Typically, super refers only to the parent class. And certainly, if A were inheriting from another class super might refer to that. But in the example above super is actually running code from the mixin. In effect, it's treating the mixin as if it were a parent.

So what happens if both a parent and a mixin have the same method?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module X
    def print
        puts "X"
    end
end

class A
    def print
        puts "A"
    end
end

class B < A
    include X
end

obj = B.new
obj.print

# Output:
# => "X"

In this case the module wins. Why? Because it has the closest proximity (another way of saying this is it's the lowest down on the inheritance chain). What's interesting about ruby's search chain for method precedence is that if a class contains more than one module, it takes the modules in the reverse order they were added to the class.

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 X
    def print
        puts "X"
        super
    end
end

module Y
    def print
        puts "Y"
        super
    end
end

class A
    include X
    include Y
end

obj = B.new
obj.print

# Output:
# => Y
# => X

You can see the order was Y then X where ruby inferred that modules included last were in closer proximity than those included earlier.

So here's a question -- what happens when everything included has the same method and they all call super?

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# Modules

module X
    def print
        puts "X"
        super
    end
end

module Y
    def print
        puts "Y"
        super
    end
end

module Z
    def print
        puts "Z"
        super
    end
end

# Classes

class A

    include X

    def print
        puts "A"
        super
    end
end

class B < A

    include Y
    include Z

    def print
        puts "B"
        super
    end
end

# Execute

obj = B.new
obj.print

# Output:
# => B
# => Z
# => Y
# => A
# => X

Notice that super doesn't mind if the method is on a module or on a class. Also notice that ruby favors methods on the parent class first before methods on the parent class' mixins.

Also of note is that X#print calls super which because it's last in the search chain there is nothing "above" it. However instead of throwing an error it just does nothing.