Mixins
Ruby, Iteration 5
The Problem: Multiple Inheritance
One of the smartest things that the Java (and C#) inventors did was to not allow multiple inheritance. It's not that useful, and it typically causes more problems than it fixes. There are a variety of reasons for this which we'll see in a different iteration (and maybe a different context).
Ruby does allow multiple inheritance, but in a strange way. Technically, you can extend only one class (just as in Java and C#), but you can mix in a module to gain extra behaviors.
Remember: OOP is more about behaviors than about objects.
Normal inheritance looks like this:
class Integer < Numeric ... end
Integer
is said to extend the
Person
class. Everything Numeric
has is automatically given to Integer
, including
instance variables.
Modules and mixins work a little differently. Modules can be used for other reasons (mostly for creating namespaces), but when they define instance methods, they can be included in a class and those instance methods are mixed in with the original methods of the class.
Ruby provides a few incredibly useful modules. We'll look at the
Comparable
module.
The Comparable Module
Typically there are seven comparison methods that Ruby lets you
define. First, Ruby has the standard comparisons (e.g.,
<
, <=
, ==
,
>=
, >
) from basic mathematics:
4 < 5
and 8 >= -4
.
Second, Ruby has a between?
method:
3.between?(1, 5)
(which is true) and
6.between?(1, 5)
(which is false).
The <=>
operator is affectionally referred to
as "the spaceship operator".
Third, Ruby has a ternary comparison (similar to
compareTo(T)
in Java and strcmp(s1,s2)
in
C/C++): 1 <=> 3
returns -1, 1 <=>
1
returns 0, 3 <=> 1
returns +1. (Other
languages say that the results will be less than zero, zero, or
greater than zero, but Ruby seems to insist on returning -1, 0, or
+1. However, don't count on it.)
You could write definitions for all seven methods yourself. You could write them in terms of the others making some of the computations easier, but there would still be seven definitions.
You could write a superclass and use normal inheritance, but then you use up your one official superclass.
Keep in mind that these seven methods are all behavior.
What if you only had to define just one of these methods and let a module take care of the rest?
Running Time
To keep track of the running time of different programs, I write this class:
running_time.rb
class RunningTime include Comparable def initialize(name, running_time) @name = name @running_time = running_time end def running_time @running_time end def <=>(other) running_time <=> other.running_time end end
Note the highlighted line of code.
Create a new folder ruby/running_time
. Create the
running_time.rb
file with these contents. Create a
running_time_test.rb
.
running_time_test.rb
class RunningTimeTest < Test::Unit::TestCase def setup @pokey = RunningTime.new("Pokey", 12) @speedy = RunningTime.new("Speedy", 3) end def test_ternary assert_equal +1, @pokey <=> @speedy assert_equal 0, @pokey <=> @pokey assert_equal 0, @speedy <=> @speedy assert_equal -1, @speedy <=> @pokey end def test_less_than assert (@speedy < @pokey) assert !(@pokey < @pokey) assert !(@pokey < @speedy) end end
RunningTime
is a class for keeping track of running
time (for cross country, swim meets, program analysis, etc.).
Instead of defining all seven methods for comparisons, I define
only <=>
because all of the others can
be defined in terms of it.
In order to get the other six methods, I included the
Comparable
module with include
Comparable
. This is not the same as a C/C++
#include
(which is evil and despised by everyone).
Ruby's include
mixes in all of the instance methods
from the named module, as if they were defined in the class itself.
It's actually a bit more efficient and flexible than that.
(Efficient: it's not a textual include, it's virtual. Flexible:
since it's virtual, changes to the module are virtually updated for
everyone and everything.)
The real key is that the RunningTime
objects get
all those methods for free.
Java and C# don't allow you to do this; you'll typically have to reimplement those methods or use adapter pattern (which only reduces the amount of thinking you have to do, not the number of methods you have to write). C++ allows you to inherit from multiple classes, and it can turn into a right terrible mess without too much trying.
Your Turn
Create a new folder ruby/name
, a new file
name.rb
with a Name
class, and a new file
name_test.rb
with a test-case class. Write tests and
computations based on the description below.
The Name
class should keep track of first name and
last name separately. (There should be Name#first_name
and Name#last_name
methods.) Using the
Comparable
module, implement the seven comparison
methods so that names are ordered properly—compare last names
first; if equal, use the first name.
Name.new('Lee', 'Adama')
is less than
Name.new('Kara', 'Thrace')
(because 'Adama' is less
than 'Thrace'). Name.new('Sarah Jane', 'Smith')
is
greater than Name.new('Dr. Zachary', 'Smith')
(because
'Smith' equals 'Smith' and 'Sarah Jane' is greater than 'Dr.
Zachary'). Since you have two things to consider, it makes your
<=>
a little more interesting. But it doesn't
make the other methods any harder!
Work hard on some good test cases.