RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

Roles, Modules, and Mix-Ins in Ruby (POODR: Chapter 7)

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:

  1. Those it implements.
  2. Those implemented in all objects above it in the hierarchy.
  3. Those implemented in any module that has been added to it.
  4. 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 or category, 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.

(may.rb) download
1
2
3
4
5
6
7
8
9
10
module May
    def mondays
    # Array of all Mondays in May
        [5,12,19,26]
    end

    def month_of_year
        5
    end
end
(holiday.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
module Holiday
    def cause
        case name
        when "Memorial Day"
            "remembering the men and women who died while serving in the country's armed forces"
        when "Thanksgiving"
            "giving thanks for the blessing of the harvest and of the preceding year"
        # ...
        end
    end

    # ...
end
(memorial_day.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MemorialDay
    include May
    include Holiday

    def name
        "Memorial Day"
    end

    # Memorial Day always occurs on the last Monday in May
    def day_of
      mondays.last
    end

    def date
        Date.parse("#{month_of_year}/#{day_of}")
    end
end

Happy Memorial Day!