POODR Chapter 7: Sharing Role Behavior with Modules
Tony Duong
Jul 15, 2026 γ» 5 min
Overview
Chapter 6 used classical inheritance to share behavior between classes that are variations of the same thing (a RoadBike is a Bicycle). But sometimes several otherwise-unrelated objects need to play the same role β they all need to do the same thing without being the same thing. Chapter 7 covers sharing that role behavior with Ruby modules (mixins), and then lays out the rules that make any inheritance β classical or module-based β trustworthy.
Understanding Roles
Some problems require unrelated objects to respond to the same message. That shared responsibility is a role. A duck type (chapter 5) is a role defined by an interface; this chapter is about roles that also come with shared code.
A word of caution up front: using a role creates dependencies, and those dependencies raise the risk of your design. Roles are powerful but should be used deliberately, not reflexively.
Finding Roles
The example: a scheduler needs to know whether a target (a Bicycle, a Mechanic, a Vehicle) is available during a proposed time span, respecting a "lead time" between bookings. Every schedulable thing must answer schedulable?, and they all share the same lead-time logic β but bikes, mechanics, and vehicles are otherwise unrelated.
This is a Schedulable role. The interface plus its shared behavior belongs in a module that any class can include.
Writing the Code That Uses a Role β Modules
In Ruby, a module holds a named set of methods that can be mixed into any class with include. Once included, the module's methods become available to instances of that class as if they were defined there β the object gains the behavior.
module Schedulable
attr_writer :schedule
def schedule
@schedule ||= ::Schedule.new
end
def schedulable?(start_date, end_date)
!scheduled?(start_date - lead_days, end_date)
end
def scheduled?(start_date, end_date)
schedule.scheduled?(self, start_date, end_date)
end
# includers may override; template-method style default
def lead_days
0
end
end
class Bicycle
include Schedulable
def lead_days
1
end
end
Schedulable defines the algorithm (schedulable? β scheduled?) and exposes a hook (lead_days) with a sensible default. Each includer specializes only what's different. This is exactly the template-method pattern from chapter 6, now applied through a module instead of a superclass.
Method Lookup and the Antipatterns
When an object receives a message, Ruby searches for the matching method in a specific order:
- the object's own class,
- any modules that class included (last included is searched first),
- the superclass,
- the superclass's included modules,
- β¦up the chain to
Object,Kernel, andBasicObject.
Because included modules are inserted into this lookup path, mixing a module in is a form of inheritance β the same rules and risks apply.
Two antipatterns signal that you should reach for a role or an abstraction:
- An object that uses a variable with a name like
typeorcategoryto decide what message to send toselfβ the classes probably share a role; consider classical inheritance for the is-a part and modules for the behaves-as part. - A message sender that checks the class of the receiver to decide which message to send β you're missing a duck type; the receivers share a role, and that role's interface (and possibly shared code) belongs in a module.
Writing Inheritable Code
The final section gives rules that apply to all inheritance, whether via superclasses or modules. These are what make a hierarchy safe to build on.
Recognize the Antipatterns
(as above) β they tell you when sharing code is appropriate.
Insist on the Abstraction
All code in an abstract superclass or a module must apply to every object that inherits it. Never put code there that only some includers need. If a subclass or includer overrides a method to raise an error ("does not implement this"), that's proof the abstraction is wrong β the method didn't belong in the shared code.
Honor the Contract β Liskov Substitution Principle
Subclasses and includers must be substitutable for the thing they specialize. A subclass should honor the same interface and behave in a way callers expect β accepting the same kinds of inputs and returning the same kinds of outputs. This is the Liskov Substitution Principle (LSP), the "L" in SOLID. Violating it means callers must know which concrete type they're dealing with, which defeats the entire point of the abstraction.
Use the Template Method Pattern
Separate the abstract (the shared algorithm) from the concrete (the per-includer specialization) by having the shared code send messages that includers implement β with defaults provided in the shared code.
Preemptively Decouple Classes β Avoid super in Includers
Wherever possible, avoid writing code that requires includers/subclasses to send super. Use hook methods instead, so specializations plug into the algorithm without needing to know it. This keeps the shared code in control of when, and includers in control of what.
Create Shallow Hierarchies
Deep, wide inheritance hierarchies are hard to understand and expensive to change β method lookup travels far, and it's difficult to reason about where behavior comes from. Prefer shallow, narrow hierarchies. They're easier to comprehend and far less risky to extend.
Key Takeaways
- Modules let unrelated classes share a role β common behavior for objects that do the same thing without being the same thing.
- Using a role introduces dependencies, so apply modules deliberately, not by default.
- Including a module is a form of inheritance β modules join the method-lookup path, so all inheritance rules apply to them too.
- Apply the template-method pattern in modules: define the algorithm, expose hooks with sensible defaults, and let includers specialize.
- Insist on the abstraction β shared code must apply to every includer; an override that raises "not implemented" means the abstraction is wrong.
- Honor the Liskov Substitution Principle β includers/subclasses must be substitutable for the abstraction they extend.
- Avoid forcing
super, and keep hierarchies shallow and narrow to minimize coupling and cognitive load.