Mixins
We saw earlier how using inheritance only to reuse code leads to interface creep, giving subclasses more public behaviors than are appropriate. Sometimes when programmers clamor for multiple inheritance, what they are really asking for is a mechanism to pull in behaviors from multiple sources. They do not need a complex inheritance hierarchy; they just want to write less code. Who can blame them?
A few languages provide a feature called mixins or traits that create reusable plugins of code. Ruby is one of them. A mixin is a structure holding the reusable code. There is no special mixin abstraction in Ruby. Instead, we put the reusable code in a module.
The Orderable
mixin below defines six helpful comparison behaviors built on top of a class's <=>
operator, which is exactly like compareTo
in Java. In a <=> b
, the <=>
operation yields a negative number if a
is less than b
, a positive number if a
is greater than b
, and 0 otherwise.
module Orderable
def <(other)
(self <=> other) < 0
end
def <=(other)
(self <=> other) <= 0
end
def >(other)
(self <=> other) > 0
end
def >=(other)
(self <=> other) >= 0
end
def ==(other)
(self <=> other) == 0
end
def between?(lo, hi)
lo <= self && self <= hi
end
end
The operator !=
is not overloaded because Ruby will automatically invert the result of ==
if we don't define it.
A class may pull in the behaviors of a mixin using the include
method. This Word
class defines only three behaviors itself: an initializer, the <=>
operator, and a length
method:
class Word
include Orderable
def initialize(word)
@word = word
end
def <=>(other)
# length <=> other.length <- a shorter implementation
if length < other.length
-1
elsif length > other.length
1
else
0
end
end
def length
@word.length
end
end
But it also mixes in the behaviors from Orderable
. We can invoke those behaviors as if they were defined by the class itself:
two = Word.new("**")
three = Word.new("***")
four = Word.new("****")
five = Word.new("*****")
puts two < three # prints true
puts five == five # prints true
puts four != two # prints true
puts four.between?(two, five) # prints true
We can include as many mixins as we like. If there are any name collisions, the behavior included later replaces the behavior included earlier.
Note that Ruby has a builtin Comparable
mixin that behaves just like Orderable
.
We will see mixins again when we discuss Rust.
Summary
Objects organize our programs into coherent entities. Classes are the blueprints by which objects are instantiated en masse, but some objects are singletons, allowing only a single instance. Each object manages its own state but provides behaviors for sharing or modifying its state in a controlled manner. Properties appear as state to outside clients but are really behaviors that may do more than read from or write to instance variables. Classes may be arranged in an inheritance relationship, with the root supertype representing the most common or abstract view of all family members and the descendent subtypes representing specializations or extensions of their ancestors. Inheritance reduces code duplication and affords us the chance to write polymorphic code that can operate at the abstract level but serve all subtypes. But inheritance also comes with perils. Subtypes may inadvertently gain behaviors that we don't want them to have. Polymorphic code can't be certain how subtypes will behave. Some subtypes are reasonably defined as a mix of two or more supertypes, but this merging may introduce ambiguities and collisions of state and behaviors. If the supertypes themselves have a common ancestor, then ambiguities and collisions are guaranteed. Some languages provide safer means of reusing behaviors, like mixins, that do not complicate the type hierarchy.