I am working my way through programming-related reading, currently with Sandi Metz’s “Practical Object-Oriented Design in Ruby”. Click here for more posts about POODR.
Summary: When multiple objects play a common role, modules allow objects to share behavior. Ruby does not support multiple inheritance directly, but does so through mixins, or classes mixed with modules. The Memorial Day example below illustrates modules and mixins.
Remember the strategy of sharing behavior through classical inheritance, which I discussed in this post? It turns out there’s a problem: inheritance cannot combine two existing subclasses. For that, we need to understand roles and modules.
Quick test
How many types of methods can an object respond to in Ruby? That is, in how many different ways can methods be implemented?
Answer (read on)
Four. An object can respond to the following messages:
- Those it implements.
- Those implemented in all objects above it in the hierarchy.
- Those implemented in any module that has been added to it.
- Those implemented in all modules added to any object above it in the hierarchy.
Modules add two of the four kinds of messages that objects can respond to in Ruby. Kind of a big deal. They enable objects to share roles.
Roles
Some problems require sharing behavior among otherwise unrelated objects. This common behavior is orthogonal to class; it’s a role an object plays. […] When formerly unrelated objects begin to play a common role, they enter into a relationship with the objects for whom they play the role. These relationships are not as visible as those created by the subclass/superclass requirements of classical inheritance but they exist nonetheless.
Furthermore, Metz writes:
When a role needs shared behavior you’re faced with the problem of organizing the shared code. […] Many object-oriented languages provide a way to define a named group of methods that are independent of class and can be mixed in to any object. In Ruby, these mix-ins are called modules. Methods can be defined in a module and then the module can be added to any object. Modules thus provide a perfect way to allow objects of different classes to play a common role using a single set of code.”
Inheritable Code
The whole point of modules is to enable us to write good code. But Metz warns:
…you are equipped to write some truly frightening code. Imagine the possibilities. You can write modules that include other modules. You can write modules that override the methods defined in other modules. You can create deeply nested class inheritance hierarchies and then include these various modules at different levels of the hierarchy. You can write code that is impossible to understand, debug, or extend.
She follows this warning, however, by saying “this very same power is what allows you to create simple structures of related objects that elegantly fulfill the needs of your application, your task is not to avoid these techniques but to learn to use them for the right reasons, in the right places, in the correct way.” The first step in this direction, Metz argues, is to write properly inheritable code.
A few practical object-oriented design tips for writing and maintaining inheritance hierarchies and modules:
-
“Recognize the Antipatterns”: if using variables like
type
orcategory
, look to classical inheritance. If checking the class of receiving objects to determine which message to send, duck typing. - “Insist on the Abstraction”: all code in an abstract superclass should apply to every class that inherits it.
- “Honor the Contract”: subclasses agree to a contract, promising to be substitutable for their superclasses. Maintain that substitutability.
- “Use the Template Method Pattern”: separate the abstract from the concrete.
- “Preemptively Decouple Classes”: avoid writing code that requires its inheritors to send super; instead use hook messages.
- “Create Shallow Hierarchies”: The shallower and narrower, the better.
Code Example
Being Memorial Day and all, a simple example of modules and mix-ins, in which the MemorialDay
class mixes in both the May
and Holiday
modules.
This code allows an instance of MemorialDay
to call (1) methods that have been implemented in the MemorialDay
class itself, (2) methods implemented in all objects above it in the hierarchy (in this case, Object
), and (3) methods implemented in any module that has been added, namely May
and Holiday
.
Note: there is a difference between include
and extend
when mixing in modules—include
is for instance methods, extend
for class methods.
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Happy Memorial Day!