skip to content

In Ruby, how should a mixin require its including class to define a method, and why does checking method_defined? inside self.included usually fail?

level: seniorimportance: should knowfreq 32%

answer

  1. hook fires at the include line
  2. class body not finished yet
  3. stub that raises, host overrides it
  4. class precedes included module
  5. prepend flips who wins

basics

~20 s

The included hook runs at the include line, before the class body defines its methods, so an eager method_defined? check rejects valid hosts. Declare the contract lazily: a stub raising a clear error, overridden by the host's own method.

solid answer

~50 s

A mixin often depends on a method the host must supply — say `export_rows` for an `Exportable` module. Checking `base.method_defined?(:export_rows)` in `self.included` fails because the hook fires at the `include` line, usually at the top of the class body, before the `def` below it has run. The idiomatic contract is lazy: the module defines a stub `export_rows` that raises an error naming the host class, and the host's own definition wins because a class comes before its included modules in `ancestors`. That only holds for `include`: a prepended module sits in front of the class, so its stub would shadow the host's method. Many codebases raise `NotImplementedError` in the stub, but it is a `ScriptError`, not a `StandardError`, so a bare `rescue` does not catch it; a custom `StandardError` subclass avoids that surprise. Back the contract with a test that includes the module into each host.

code

ruby · 11 lines
ruby
module Exportable
  def self.included(base)
    return if base.method_defined?(:export_rows)
    raise ArgumentError, "#{base} must define #export_rows"
  end
end

class Ledger
  include Exportable      # raises ArgumentError: the def below has not run yet
  def export_rows = [[1, 2]]
end

go deeper

for a junior

Remember that a class body runs top to bottom, so methods defined below an include line do not exist yet when the included hook runs.

for a middle

Explain why a raising stub works: the host class precedes the included module in ancestors, so the host's own method is found first.

for a senior

Cover the prepend inversion, the ScriptError ancestry of NotImplementedError, and backing a lazy contract with tests so missing methods fail in CI.

for a principal

Decide how a team documents and enforces mixin contracts — stubs, error classes, contract tests — so implicit interfaces stay discoverable as hosts multiply.

## The contract a mixin carries Many mixins provide behaviour built on top of a method they do not define themselves. An `Exportable` module might offer `export`, which calls `export_rows`, and expect every host class to implement `export_rows`. Ruby has no interface declaration for this, so the mixin has to state the requirement in code. The question is **when** to check. ## Why an eager check in the hook fails A Ruby class body is ordinary code, executed top to bottom. `include Exportable` is a method call on that line; it runs `append_features` and then `Exportable.included(base)` immediately. Any `def` further down the body has **not run yet**. So: - `base.method_defined?(:export_rows)` returns `false` for a perfectly valid host that defines the method below the `include`. - Moving `include` to the bottom of every class "fixes" it but is fragile and surprising. - Reopened classes and methods added later by other code are invisible at hook time as well. `Module` offers no callback for "the class body has finished"; only a `TracePoint` on the `:end` event can observe that moment, which is heavy machinery for a contract check. The hook is simply the wrong moment for it. ## The lazy contract: a stub the host overrides The robust pattern defines the required method in the module as a **stub that raises**: 1. The module defines `export_rows`, whose body raises an error naming `self.class` and the missing method. 2. The host defines its own `export_rows`. 3. Method lookup searches the class before its included modules, so the host's version is found first and the stub is never reached. 4. A host that forgets the method gets a clear error the first time the behaviour is used, instead of a `NoMethodError` from deep inside the mixin. | Mixing call | Where the module sits | Who wins for `export_rows` | |---|---|---| | `include Exportable` | after the class in `ancestors` | the host's method | | `prepend Exportable` | before the class in `ancestors` | the module's stub, which raises | The table shows the trap: the stub pattern depends on `include`. A module designed to be prepended must not define stubs for methods the host provides; it should call `super` or check `defined?(super)` instead. ## Choosing the error class - **`NotImplementedError`** is widely used for this, but it descends from `ScriptError`, not `StandardError`. A bare `rescue` or `rescue => e` does not catch it, which can crash a job runner that expects to rescue application errors. Ruby's own documentation describes it as the error for features not implemented on the current platform. - **A custom subclass of `StandardError`** (for example `Exportable::MissingHookError`) is caught by ordinary rescue clauses and documents the contract by its name. - **`NoMethodError`** raised manually mimics a genuinely missing method; it is a `StandardError` too, but it hides that the mixin, not a typo, caused the failure. ## Making the contract visible early Lazy failure is correct at runtime but late in development. Complement it with: - A test that exercises the mixin against every host, so a missing method fails in CI. - A class-level declaration the host calls **after** its definitions, if an explicit check point is wanted. - Documentation at the top of the module listing the required methods. ## Why not rely on NoMethodError alone? Without a stub, a host that forgets the method still fails — `export` calls `export_rows` and Ruby raises `NoMethodError`. That works, but the message names only the missing method and the receiver, not the mixin that required it, and a reader of the module cannot see its contract without reading every call site. A stub turns an implicit dependency into a named, searchable one, and its message can say exactly which module needs what. The cost is small: one method per requirement, overridden by every correct host. ## Summary - The `included` hook fires at the `include` line, before the rest of the body. - Enforce host methods lazily with a raising stub, relying on class-before-module lookup. - Do not use stubs in modules meant for `prepend`. - Prefer a `StandardError` subclass for the stub's error so ordinary rescue clauses see it.

  • In Ruby, why does a raise NotImplementedError stub escape a bare rescue in the caller?
    `NotImplementedError` inherits from `ScriptError`, which sits directly under `Exception`, not under `StandardError`. A bare `rescue` and `rescue => e` catch only `StandardError` descendants, so the error propagates. Rescue it by name, or raise a custom `StandardError` subclass in the stub instead.
  • In Ruby, how can a prepended module depend on a host method without a stub?
    A prepended module sits before the class, so its methods run first and reach the host's version with `super`. It should not define the host's required method itself; its wrapper can call `super` and, if the host might lack the method, check `defined?(super)` first and raise a clear error when it is missing.

saying these in an interview costs you the question

  • Validates required host methods with method_defined? inside self.included.
  • Believes the module's stub overrides the host's method when the module is included.
  • Uses the raising-stub pattern in a module meant to be prepended.
  • Assumes a bare rescue catches NotImplementedError like any application error.
  • Expects a Module callback like included to fire when the class body ends.