In Ruby, how does a mixin's self.included(base) hook combined with base.extend(ClassMethods) give the including class both instance and class methods?
answer
- include adds instance methods only
- nested module named by convention
- extend targets the singleton class
- subclasses reach the class methods
- hook does not rerun for subclasses
basics
~10 sinclude only gives the host's instances the module's methods. The module's included hook calls base.extend(ClassMethods), mixing a nested module into the class itself, so its methods become class methods such as configuration macros.
solid answer
~40 s`include Plugin` mixes Plugin's instance methods into the class's ancestors, so they are callable on instances only. To also give the host class-level methods, Plugin defines `def self.included(base) = base.extend(ClassMethods)`, where `ClassMethods` is a nested module. `extend` mixes `ClassMethods` into the class object's singleton class, so `Resizer.plugin_option :width, 800` works as a class macro while `Resizer.new.width` uses the instance side. `ClassMethods` is only a naming convention; Ruby gives it no special meaning. Subclasses inherit the class methods, because a subclass's singleton class inherits from its parent's, but the hook itself does not run again for them — so any per-class state the hook sets up must be created lazily.
code
ruby · 27 linesmodule Plugin
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def plugin_option(name, default)
plugin_defaults[name] = default
define_method(name) { @options.fetch(name, default) }
end
def plugin_defaults = (@plugin_defaults ||= {})
end
def initialize(**options)
@options = options
end
end
class Resizer
include Plugin
plugin_option :width, 800
end
Resizer.new(width: 400).width # => 400
Resizer.new.width # => 800
Resizer.plugin_defaults # => {width: 800}go deeper
Remember that include gives instances methods and extend on a class gives class methods; the included hook is where the mixin calls extend for you.
Walk through the sequence: include runs append_features, then included(base), which calls base.extend(ClassMethods), placing the nested module in the class's singleton ancestors.
Discuss subclasses: they inherit ClassMethods through the singleton chain but the hook never reruns, so per-class state must be lazy; also keep load-time hook work minimal.
Weigh the idiom's convenience against hidden coupling: one include silently adds class-level API, so document the macros and keep the mixin's surface small and testable.
## The problem: include is instance-only In Ruby, `include SomeModule` inserts the module into the ancestors of a class, which makes the module's instance methods available on **instances** of that class. It adds no methods callable on the class object itself. A plugin that wants to offer both a class-level macro (`plugin_option :width, 800`) and an instance-level behaviour cannot get both from one `include` without help. Ruby offers a second mixing call, `extend`, which adds a module's instance methods to a **single object**. When that object is a class, the methods become class methods. The idiom stitches the two together with the `included` hook. ## The idiom, step by step 1. The mixin defines a nested module, conventionally named `ClassMethods`, holding the methods meant for the class. 2. The mixin defines `def self.included(base)`, the hook Ruby calls after `include`. 3. Inside the hook it calls `base.extend(ClassMethods)`, where `base` is the including class. 4. The host writes a single `include Plugin` and receives both sets of methods. | Piece | Mixed in with | Callable on | |---|---|---| | Plugin's own instance methods | `include Plugin` | instances of the host | | `Plugin::ClassMethods` | `base.extend(ClassMethods)` in the hook | the host class itself | Key facts about each piece: - **`ClassMethods` is a convention, not a keyword.** Any module name works; the name just tells readers where to look. - **`extend` goes through the singleton class.** `base.extend(M)` inserts `M` into the ancestors of `base`'s singleton class, so `M`'s methods are found when you call a method on the class object. (How singleton classes work is a separate topic.) - **`self` inside a class method is the class.** A macro in `ClassMethods` can therefore call `define_method`, `attr_reader` or set class-level instance variables on the host. - **`include` is a public method** since Ruby 2.1, so `base.include(Other)` from inside the hook also works — but it adds *instance* methods, which is a different result from `base.extend`. ## Subclasses A subclass does not call `include` again, so **the hook does not run for it**. The subclass still reaches the class methods, because its singleton class inherits from the parent's singleton class, and the parent's singleton class has `ClassMethods` mixed in. What the subclass does not get is anything the hook *did* to the parent: if the hook set `base.instance_variable_set(:@defaults, {})`, that instance variable lives on the parent class object only. The usual fix is lazy initialization inside the class method (`@plugin_defaults ||= {}`), which creates separate state the first time each class touches it. Hooks that run when a class is subclassed are covered by a different topic. ## When the mixin is included through another module The hook's `base` is whatever called `include` directly. If a module `Resizable` runs `include Plugin`, then `Plugin.included(Resizable)` fires and `ClassMethods` is extended onto the **module** `Resizable`, not onto any class. A class that later runs `include Resizable` gets Plugin's instance methods through the ancestor chain, but `Plugin.included` never hears about that class, so the class has no `plugin_option`. Nested mixins built this way need the intermediate module to forward the setup in its own `included` hook — for example by calling `base.extend(Plugin::ClassMethods)` itself. This is one of the gaps that framework helpers exist to close. ## Testing the idiom A small test catches most regressions: define an anonymous host with `Class.new { include Plugin }`, then assert that the class responds to the macro and that its instances respond to the generated readers. Anonymous classes keep the test from polluting global constants. ## Common mistakes - Writing `include ClassMethods` or `base.include(ClassMethods)` in the hook: the methods land on instances, and `Resizer.plugin_option` raises `NoMethodError`. - Putting the class methods directly in the mixin as `def self.plugin_option`: those are singleton methods of `Plugin` itself, callable as `Plugin.plugin_option`, and never reach the host. - Expecting the hook to run again for every subclass. - Doing heavy work in the hook: it runs at load time for every host. ## Why the idiom persists The pattern keeps the host's code to a single line and keeps the mixin's surface in one file. Rails packages the same idea as `ActiveSupport::Concern`, which is covered elsewhere; in plain Ruby the hand-written hook above is the whole mechanism.
- In Ruby, what goes wrong if the hook calls base.include(ClassMethods) instead of base.extend(ClassMethods)?`include` adds `ClassMethods` to the host's instance-side ancestors, so its methods become instance methods. `Resizer.plugin_option` then raises `NoMethodError`, while `Resizer.new.plugin_option` would exist, which is the opposite of what was intended. Only `extend` targets the class object.
- In Ruby, why does a subclass of the host see the class methods even though included never ran for it?A subclass's singleton class inherits from its superclass's singleton class. `ClassMethods` was mixed into the parent's singleton class, so method lookup on the subclass object walks up and finds it. The hook's side effects, such as class-level instance variables it set on the parent, are not copied, so state should be initialised lazily.
- In Ruby, why not simply write def self.plugin_option inside module Plugin?Inside `module Plugin`, `def self.plugin_option` defines a singleton method of the `Plugin` module object. It is callable as `Plugin.plugin_option`, but mixing `Plugin` into a class copies only instance methods, so the host never receives it.
A franchise kit: joining the franchise (include) trains every employee in the recipes, and the kit's welcome letter also hands the owner a separate manager's binder (extend ClassMethods) that only the owner uses.
saying these in an interview costs you the question
- Says include adds a module's methods as both instance and class methods.
- Believes ClassMethods is a special name Ruby looks for when including a module.
- Expects the included hook to run again for each subclass of the host.
- Treats base.include(ClassMethods) and base.extend(ClassMethods) as the same thing.
- Defines def self.macro in the mixin and expects hosts to inherit it.