Modules have another, wonderful use. At a stroke, they pretty much
eliminate the need for multiple inheritance, providing a
facility called a
mixin.
In the previous section's examples, we defined module methods, methods
whose names were prefixed by the module name. If this made you think of
class methods, your next thought might well be ``what happens if
I define instance methods within a module?'' Good question. A module
can't have instances, because a module isn't a class. However, you can
include a module within a class definition. When this happens,
all the module's instance methods are suddenly available as methods in
the class as well. They get
mixed in. In fact, mixed-in modules
effectively behave as superclasses.
module Debug
|
def whoAmI?
|
"#{self.type.name} (\##{self.id}): #{self.to_s}"
|
end
|
end
|
class Phonograph
|
include Debug
|
# ...
|
end
|
class EightTrack
|
include Debug
|
# ...
|
end
|
ph = Phonograph.new("West End Blues")
|
et = EightTrack.new("Surrealistic Pillow")
|
|
ph.whoAmI?
|
� |
"Phonograph (#537766170): West End Blues"
|
et.whoAmI?
|
� |
"EightTrack (#537765860): Surrealistic Pillow"
|
By including the
Debug
module, both
Phonograph
and
EightTrack
gain access to the
whoAmI?
instance method.
A couple of points about the
include
statement before we go on.
First, it has nothing to do with files. C programmers use a
preprocessor directive called
#include
to insert the contents of
one file into another during compilation. The Ruby
include
statement simply makes a reference to a named module. If that module
is in a separate file, you must use
require
to drag that
file in before using
include
. Second, a Ruby
include
does
not simply copy the module's instance methods into the class. Instead,
it makes a reference from the class to the included module. If
multiple classes include that module, they'll all point to the same
thing. If you change the definition of a method within a module, even
while your program is running, all classes that include that module
will exhibit the new behavior.
[Of course, we're speaking only
of methods here. Instance variables are always per-object, for
example.]
Mixins give you a wonderfully controlled way of adding functionality
to classes. However, their true power comes out when the code in the
mixin starts to interact with code in the class that uses it. Let's
take the standard Ruby mixin
Comparable
as an
example. The
Comparable
mixin can be used to add the comparison
operators (
<
,
<=
,
==
,
>=
, and
>
), as well as
the method
between?
, to a class. For this to work,
Comparable
assumes that any class that uses it defines the
operator
<=>
. So, as a class writer, you define the one method,
<=>
, include
Comparable
, and get six comparison functions for
free. Let's try this with our
Song
class, by making the songs comparable
based on their duration.
All we have to do is include the
Comparable
module and implement
the comparison operator
<=>
.
class Song
include Comparable
def <=>(other)
self.duration <=> other.duration
end
end
|
We can check that the results are sensible with a few test songs.
song1 = Song.new("My Way", "Sinatra", 225)
|
song2 = Song.new("Bicylops", "Fleck", 260)
|
|
song1 <=> song2
|
� |
-1
|
song1 < song2
|
� |
true
|
song1 == song1
|
� |
true
|
song1 > song2
|
� |
false
|
Finally, back on page 43 we showed an
implementation of Smalltalk's
inject
function, implementing it
within class
Array
. We promised then that we'd make it more generally
applicable. What better way than making it a mixin module?
module Inject
def inject(n)
each do |value|
n = yield(n, value)
end
n
end
def sum(initial = 0)
inject(initial) { |n, value| n + value }
end
def product(initial = 1)
inject(initial) { |n, value| n * value }
end
end
|
We can then test this by mixing it into some built-in classes.
class Array
|
include Inject
|
end
|
[ 1, 2, 3, 4, 5 ].sum
|
� |
15
|
[ 1, 2, 3, 4, 5 ].product
|
� |
120
|
class Range
|
include Inject
|
end
|
(1..5).sum
|
� |
15
|
(1..5).product
|
� |
120
|
('a'..'m').sum("Letters: ")
|
� |
"Letters: abcdefghijklm"
|
For a more extensive example of a mixin, have a look at the
documentation for the
Enumerable
module, which starts
on page 403.