skip to content

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?

level: middleimportance: must knowfreq 62%

answer

  1. include adds instance methods only
  2. nested module named by convention
  3. extend targets the singleton class
  4. subclasses reach the class methods
  5. hook does not rerun for subclasses

basics

~10 s

include 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 lines
ruby
module 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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.