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

  • If you want a real-life implementation of a Taggable-like mixin, see Recipe 13.18, "Adding Taggability with a Database Mixin"


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net