In Ruby, which hook method runs on a module when a class includes, extends or prepends it, and what does each hook receive?
answer
- one callback per mixing keyword
- defined with def self. on the module
- runs after the module is inserted
- extend passes the object, not its class
- include A, B fires B's hook first
basics
~10 sRuby calls a singleton method on the module: included(base) after include, prepended(base) after prepend and extended(obj) after extend. Each receives whatever took the module in, and runs once the module is already in place.
solid answer
~40 sA module reacts to being mixed in by defining singleton methods named after the keyword: `def self.included(base)`, `def self.prepended(base)` and `def self.extended(obj)`. `Module` ships all three as private no-op defaults, so defining one simply overrides it. Ruby calls the hook right after the insertion step (`append_features`, `prepend_features` or `extend_object`), so `base.ancestors` already lists the module inside the hook. The argument is the receiver of the keyword: the class or module for `include`/`prepend`, and for `extend` the object itself — a class when you write `Report.extend(M)`, a plain instance when you write `obj.extend(M)`. The hook's return value is ignored, and `include A, B` processes its arguments last-first, so `B.included` fires before `A.included`.
code
ruby · 24 linesmodule Tracked
def self.included(base)
puts "included in #{base}"
end
def self.prepended(base)
puts "prepended to #{base}"
end
def self.extended(obj)
puts "extended #{obj.inspect}"
end
end
class Invoice
include Tracked # prints "included in Invoice"
end
class Receipt
prepend Tracked # prints "prepended to Receipt"
end
note = Object.new
note.extend(Tracked) # prints "extended #<Object:0x...>"go deeper
Recall the three names — included, extended, prepended — and that each is written as def self.name on the module and receives whatever mixed the module in.
Explain the two-step sequence: an insertion method such as append_features runs first, then the hook, and include with several modules processes them last-first.
Point out that hooks fire at the include line, before the rest of the class body, and on every repeated include, so hook code must be idempotent and must not assume later definitions.
Frame hooks as import-time side effects: keep them small and predictable, since every host pays for them at load time and surprises there are hard to trace.
## What an inclusion hook is A Ruby **module** is a bag of methods and constants that can be mixed into classes, other modules or single objects. Mixing in is done with three keywords-that-are-really-methods: `include`, `prepend` and `extend`. Each of them, after it has done its work, sends a **callback** (a "hook") to the module being mixed in, so the module can react: register the host, add class-level methods, set up configuration, or refuse the host. | Call | Insertion step Ruby runs first | Hook Ruby calls next | Argument the hook receives | |---|---|---|---| | `Host.include(M)` | `M.append_features(Host)` | `M.included(Host)` | the including class or module | | `Host.prepend(M)` | `M.prepend_features(Host)` | `M.prepended(Host)` | the prepending class or module | | `obj.extend(M)` | `M.extend_object(obj)` | `M.extended(obj)` | the extended object itself | ## Where the hook must be defined The hook is called **on the module object**, so it has to be a singleton method of the module: - `def self.included(base)` inside `module M` is correct. - `def included(base)` without `self.` defines an ordinary *instance* method. Ruby then copies it to the host's instances like any other mixin method, and never calls it as a hook. - Defining `self.included` on the *host* class does nothing useful either: Ruby asks the module, not the class. `Module` already defines `included`, `extended` and `prepended` as **private no-op methods** that return `nil`. Your definition overrides them. Ruby calls them internally regardless of their visibility, and it discards whatever they return, so a hook cannot cancel the mixing by returning `false`. ## The order of events `Module#include` accepts several modules and handles them **in reverse order**: 1. For the last argument, call its `append_features(host)`, which inserts the module into the host's ancestor chain unless it is already there. 2. Call that same module's `included(host)`. 3. Repeat for the previous argument, and so on back to the first. So `include A, B` runs `B.append_features`, `B.included`, then `A.append_features`, `A.included`, and ends with `A` nearer to the class than `B` in `ancestors`. `prepend` and `extend` follow the same two-step pattern with their own pair of methods. Two consequences are worth remembering: - **Inside the hook the module is already mixed in.** `base.ancestors` includes it, and `base.include?(M)` is true. - **The rest of the class body has not run yet.** The hook fires at the line where `include` appears, so methods the class defines further down do not exist yet. ## What the argument is for extend `extend` works on any object. `obj.extend(M)` inserts `M` into the ancestors of `obj`'s singleton class, which makes `M`'s instance methods callable on that one object. The hook still receives **the object you extended**, not its class and not the singleton class: - `Report.extend(M)` — `M.extended(Report)`, and `M`'s methods become class-level methods of `Report`. - `invoice.extend(M)` — `M.extended(invoice)`, and only that invoice gains the methods. ## What people use the hooks for - Adding class-level methods to the host (`base.extend(ClassMethods)`), the most common use. - Registering every host in a list the module keeps, for plugins or serializers. - Validating the host early and raising if it is the wrong kind of object. - Logging or instrumentation during development. A hook runs **every time** `include` is called with the module, even when the module is already an ancestor: the insertion step skips the duplicate, but the notification is still sent. Registration code therefore needs to be idempotent. The hooks do not fire for subclasses: a subclass reaches the module through its superclass without anyone calling `include` again. ## When the host is another module The `base` argument is not always a class. If module `Billing` runs `include Tracked`, then `Tracked.included(Billing)` fires once, with the module `Billing` as its argument. When a class later runs `include Billing`, Ruby sends `Billing.included(thatClass)` — not `Tracked.included` — even though `Tracked` also ends up in the class's ancestors. Each hook hears only about the direct include of its own module. A mixin that must react to every final class therefore cannot rely on being included indirectly through another module; either every class includes it directly, or the intermediate module's own hook forwards the work. ## Mistakes interviewers listen for - Defining the hook without `self.`, so it becomes a stray instance method on every host instance. - Expecting `extended` to receive a class when an individual object was extended. - Assuming methods defined below the `include` line are visible inside the hook. - Relying on the hook's return value, which Ruby ignores.
- In Ruby, does included fire again if one class includes the same module twice?Yes. `Module#include` calls `append_features` and then `included` for every argument, every time. The default `append_features` notices the module is already an ancestor and skips the insertion, but the hook is still sent. Any registration the hook performs should therefore check before appending, or it will record the same host twice.
- In Ruby, is the module already in base.ancestors when included runs?Yes. `include` runs `append_features` first, which inserts the module, and only then calls `included`. Inside the hook `base.ancestors` lists the module and its instance methods are already callable on instances of `base`. What is not there yet is anything the class body defines below the `include` line.
saying these in an interview costs you the question
- Writes def included(base) without self. and expects Ruby to call it as a hook.
- Says extended receives the class of the object that called extend.
- Claims the included hook runs before the module's methods are added to the host.
- Believes returning false from included stops the module from being mixed in.
- Thinks the hook fires only the first time a module is included anywhere.