Simulating Multiple Inheritance with Mixins

Problem

You want to create a class that derives from two or more sources, but Ruby doesn't support multiple inheritance.

Solution

Suppose you created a class called Taggable that lets you associate tags (short strings of informative metadata) with objects. Every class whose objects should be taggable could derive from Taggable.

This would work if you made Taggable the top-level class in your class structure, but that won't work in every situation. Eventually you might want to do something like make a string taggable. One class can't subclass both Taggable and String, so you'd have a problem.

Furthermore, it makes little sense to instantiate and use a Taggable object by itselfthere is nothing there to tag! Taggability is more of a feature of a class than a fullfledged class of its own. The Taggable functionality only works in conjunction with some other data structure.

This makes it an ideal candidate for implementation as a Ruby module instead of a class. Once it's in a module, any class can include it and use the methods it defines.

require 'set' # Deals with a collection of unordered values with no duplicates # Include this module to make your class taggable. The names of the # instance variable and the setup method are prefixed with "taggable_" # to reduce the risk of namespace collision. You must call # taggable_setup before you can use any of this module's methods. module Taggable attr_accessor :tags def taggable_setup @tags = Set.new end def add_tag(tag) @tags << tag end def remove_tag(tag) @tags.delete(tag) end end

Here's a taggable string class: it subclasses String, but it also includes the functionality of Taggable.

class TaggableString < String include Taggable def initialize(*args) super taggable_setup end end s = TaggableString.new('It was the best of times, it was the worst of times.') s.add_tag 'dickens' s.add_tag 'quotation' s.tags # => #

 

Discussion

A Ruby class can only have one superclass, but it can include any number of modules. These modules are called mixins. If you write a chunk of code that can add functionality to classes in general, it should go into a mixin module instead of a class.

The only objects that need to be defined as classes are the ones that get instantiated and used on their own (modules can't be instantiated).

If you come from Java, you might think of a module as being the combination of an interface and its implementation. By including a module, your class implements certain methods, and announces that since it implements those methods it can be treated a certain way.

When a class includes a module with the include keyword, all of the module's methods and constants are made available from within that class. They're not copied, the way a method is when you alias it. Rather, the class becomes aware of the methods of the module. If a module's methods are changed later (even during runtime), so are the methods of all the classes that include that module.

Module and class definitions have an almost identical syntax. If you find out after implementing a class that you should have done it as a module, it's not difficult to translate the class into a module. The main problem areas will be methods defined both by your module and the classes that include it: especially methods like initialize.

Your module can define an initialize method, and it will be called by a class whose constructor includes a super call (see Recipe 9.8 for an example), but sometimes that doesn't work. For instance, Taggable defines a taggable_setup method that takes no arguments. The String class, the superclass of TaggableString, takes one and only one argument. TaggableString can call super within its constructor to trigger both String#initialize and a hypothetical Taggable#initialize, but there's no way a single super call can pass one argument to one method and zero arguments to another.

That's why Taggable doesn't define an initialize method.[1] Instead, it defines a taggable_setup method and (in the module documentation) asks everyone who includes the module to call taggable_setup within their initialize method. Your module can define a _setup method instead of initialize, but you need to document it, or your users will be very confused.

[1] An alternative is to define Taggable#initialize to take a variable number of arguments, and then just ignore all the arguments. This only works because Taggable can initialize itself without any outside information.

It's okay to expect that any class that includes your module will implement some methods you can't implement yourself. For instance, all of the methods in the Enumerable module are defined in terms of a method called each, but Enumerable never actually defines each. Every class that includes Enumerable must define what each means within that class before it can use the Enumerable methods.

If you have such undefined methods, it will cut down on confusion if you provide a default implementation that raises a helpful exception:

module Complaint def gripe voice('In all my years I have never encountered such behavior…') end def faint_praise voice('I am pleased to notice some improvement, however slight…') end def voice(complaint_text) raise NotImplementedError, "#{self.class} included the Complaint module but didn't define voice!" end end class MyComplaint include Complaint end MyComplaint.new.gripe # NotImplementedError: MyComplaint included the Complaint module # but didn't define voice!

If two modules define methods with the same name, and a single class includes both modules, the class will have only one implementation of that method: the one from the module that was included last. The method of the same name from the other module will simply not be available. Here are two modules that define the same method:

module Ayto def potato 'Pohtayto' end end module Ahto def potato 'Pohtahto' end end

One class can mix in both modules:

class Potato include Ayto include Ahto end

But there can be only one potato method for a given class or module.[2]

[2] You could get both methods by aliasing Potato#potato to another method after mixing in Ayto but before mixing in Ahto. There would still only be one Potato#potato method, and it would still be Ahto#potato, but the implementation of Ayto#potato would survive under a different name.

Potato.new.potato # => "Pohtahto"

This rule sidesteps the fundamental problem of multiple inheritance by letting the programmer explicitly choose which ancestor they would like to inherit a particular method from. Nevertheless, it's good programming practice to give distinctive names to the methods in your modules. This reduces the risk of namespace collisions when a class mixes in more than one module. Collisions can occur, and the later module's method will take precedence, even if one or both methods are protected or private.

See Also

Категории