Ruby / Contracts: callables, evaluation, failures, and value models / Lesson 10
Compose modules through a visible method lookup contract
Distinguish namespaces, include, extend, prepend, and super while avoiding broad monkey patches and hidden lifecycle changes.
On this page
Lesson goals
- Explain where mixed-in methods appear and which receiver gains them.
- Preserve arguments, return values, visibility, and failures when wrapping behavior.
A module can name code or provide behavior
A module can namespace constants and classes, provide instance methods for mixins, or expose module-level operations. Those roles are related but not interchangeable. include adds module instance methods into a class’s instance lookup chain; extend adds behavior to a particular object’s singleton side. Since a class is itself an object, extending a class commonly creates class-facing methods rather than methods on its future instances. Keep the receiver explicit when reviewing a call. A method being available somewhere in a namespace does not imply it was mixed into every contained class.
Require the capability a mixin actually uses
The Displayable module calls id on its receiver and assumes it has useful display meaning. Record supplies that reader. Ruby does not statically enforce this interface, so a class including the module without the expected method can fail when display is called. Document dependencies and keep the mixin cohesive. A large shared module that reaches into many unrelated instance variables makes ownership difficult to inspect. Prefer an explicit collaborator when the behavior needs a separate resource lifetime, configuration policy, or independently replaceable implementation. The following expected output was not executed.
module Displayable
def display
"[#{id}]"
end
end
class Record
include Displayable
attr_reader :id
def initialize(id)
@id = id
end
end
puts Record.new('A-4').display
[A-4]
Expected output is [A-4]. The record supplies id and the module supplies display. No interpreter or dynamic interface test was run.
Prepend changes where wrapping behavior is found
prepend places a module before the class in the relevant ancestor lookup chain, allowing a wrapper to call super into the class implementation. include and prepend therefore have different override consequences. Multiple modules and inheritance add ordering that should be inspected rather than guessed from file position alone. A wrapper must preserve its intended argument, return, and exception contracts. Bare super forwards the current call’s arguments, while super() sends none; explicit positional and keyword forwarding is often clearer at an API boundary, especially under Ruby 3 keyword separation.
Keep the wrapper’s result visible
The wrapper below returns a new display string around the class result. It does not mutate the receiver, start a background task, or hide an exception. If instrumentation were added, keep the final expression intentional so the method does not begin returning a log result. Repeated application, module conflicts, and inherited implementations need tests in a real library. The example is a small source-level explanation of lookup, not evidence that a framework hook or a production patch has been integrated safely.
module WrappedLabel
def label
"wrapped:#{super}"
end
end
class Presenter
def label
'plain'
end
prepend WrappedLabel
end
puts Presenter.new.label
wrapped:plain
Expected output is wrapped:plain. Lookup reaches the module before Presenter, and super continues to the class method. The script was not executed.
Avoid changing unrelated callers through global patches
Ruby permits reopening classes and replacing methods, which can affect libraries that never opted into the change. Prefer a named wrapper, composition, or a carefully scoped technique when extending behavior. Refinements have lexical activation rules and are not a universal global override replacement. Loading code, applying a mixin, and managing a resource are separate actions with different lifetimes. Review ancestor order, visibility, keyword behavior, and failure propagation in the selected runtime before relying on a wrapper. This course performs no monkey patch on its host, framework loading, or Ruby execution.
Common errors and fixes
A method appears on the class but not its instances.
- Why it happens
- extend was used where instance mixing with include was intended.
- How to fix it
- Choose the operation for the intended receiver and public contract.
- Prevent it next time
- Check class-side and instance-side calls separately.
A wrapper returns nil instead of the original value.
- Why it happens
- Instrumentation became the final expression after super.
- How to fix it
- Preserve and return the intended composed result.
- Prevent it next time
- Test result semantics as well as whether super was called.
A shared patch breaks an unrelated library.
- Why it happens
- A global class change altered callers outside its intended scope.
- How to fix it
- Use an explicit wrapper or narrower collaboration boundary.
- Prevent it next time
- Review affected receivers and lookup order before changing core behavior.
Check your understanding
1. Why is including a logging module not enough to define who closes its output resource?
Show worked solution
Method reuse does not assign lifecycle ownership. The logger may borrow a caller-owned sink or own a separate one, and those contracts require different cleanup. State acquisition, completion, failure, and closure responsibilities explicitly. A mixin should not silently close shared resources merely because it can call their methods.
2. What should be checked when replacing bare super with super()?
Show worked solution
The latter forwards no arguments, so parent methods that require positional or keyword input can fail or use different defaults. Inspect the parent contract and wrapper signature. Preserve explicit keywords under Ruby 3 rules, and verify return and failure behavior rather than treating the change as stylistic punctuation.
Key takeaways
- Namespace, receiver, lookup order, and lifecycle must remain explicit.
- A wrapper preserves a behavior contract only when its results and failures are preserved.