skip to content

In Ruby, how does overriding a module's append_features differ from defining its included hook, and when would you override append_features?

level: seniorimportance: nice to knowfreq 18%

answer

  1. one inserts, the other notifies
  2. both private on Module
  3. forget super and nothing is mixed in
  4. repeat include still fires included
  5. extend_object and prepend_features mirror it

basics

~20 s

append_features does the mixing: its default inserts the module into the host's ancestors unless already present. included is a notification sent afterwards. Override append_features only to veto or alter insertion, and call super or nothing is mixed in.

solid answer

~40 s

For each module given to `include`, Ruby calls `mod.append_features(host)` and then `mod.included(host)`. The default `append_features` does the real work: it inserts the module into the host's ancestor chain, skipping it if it is already there. `included` is a no-op notification the module can override. Ruby's own documentation recommends `included` for reacting to inclusion; override `append_features` only when you must **refuse or change the insertion** — for example raising `TypeError` for a host that is not a class — and call `super` to keep the normal behaviour. If an override forgets `super`, the module is silently not mixed in, yet `included` still runs and `include` returns normally. The same pairs exist for the other keywords: `prepend_features`/`prepended` and `extend_object`/`extended`.

code

ruby · 22 lines
ruby
module Auditable
  def self.append_features(base)
    unless base.is_a?(Class)
      raise TypeError, "#{self} can only be included into a class, not #{base}"
    end
    super
  end

  def self.included(base)
    puts "#{self} included in #{base}"
  end

  def audit_log = (@audit_log ||= [])
end

class Order
  include Auditable   # prints "Auditable included in Order"
end

module Shared
  include Auditable   # raises TypeError before insertion and before included
end

go deeper

for a junior

Know that include calls two methods on the module: append_features, which does the mixing, and included, which is a notification you may override.

for a middle

Explain the default append_features behaviour: insert unless already an ancestor, then included fires regardless, and the parallel pairs for prepend and extend.

for a senior

Show when a veto belongs in append_features, why a missing super silently disables the mixin, and why included logic must be idempotent under repeated includes.

for a principal

Argue for keeping overrides of core mixing methods rare and reviewed, since a silent failure there changes method lookup for every host without an error.

## Two methods behind every include `Module#include(*modules)` is a short loop. For each argument, in reverse order, it sends two messages to the module: 1. `append_features(host)` — **the action**. The default implementation, defined on `Module`, adds the module's methods, constants and module variables to `host` by inserting it into the host's ancestor chain, unless the module is already an ancestor. 2. `included(host)` — **the notification**. The default does nothing; mixins override it to react. Both are **private** instance methods of `Module`; a module overrides them as its own singleton methods (`def self.append_features(base)`, `def self.included(base)`). Ruby calls them internally regardless of visibility. The other two mixing calls follow the same shape: | Mixing call | Action method | Notification hook | |---|---|---| | `include` | `append_features` | `included` | | `prepend` | `prepend_features` | `prepended` | | `extend` | `extend_object` | `extended` | ## What changes when you override the action Overriding `append_features` replaces the insertion itself. Three behaviours follow directly from the loop above: - **Calling `super`** performs the normal insertion; code before `super` runs before the module is mixed in, code after it runs once it is. - **Not calling `super`** means the module is never inserted. `host.ancestors` does not list it and its methods are missing — but `include` still returns `host`, and `included` still fires, so logging in the hook will claim success. - **Raising before `super`** stops the process before insertion and before `included`, which makes `append_features` a natural place for a guard. ## When to use which - Use **`included`** for nearly everything: extending the host with class methods, registering the host, setting up configuration. The core documentation for `Module#included` says to prefer it over `append_features` for performing some action on inclusion. - Override **`append_features`** only to veto or reshape the insertion: refusing hosts of the wrong kind, or conditionally skipping the mix-in. Always decide explicitly whether `super` runs. - Override **`extend_object`** for the same reasons on `extend`; the core docs show a module that refuses to extend strings. ## Repeated and cyclic includes The default `append_features` quietly ignores a module that is already an ancestor, so including it a second time is not an error — and `included` fires again anyway, because `include` sends it unconditionally. An actual error, `ArgumentError` with "cyclic include detected", appears only when the insertion would make a module its own ancestor, for example when module `A` includes `B` after `B` already included `A`. ## Special cases in the core - `Class` undefines `append_features`, `prepend_features` and `extend_object`, and `include` type-checks its arguments, so a class cannot be mixed into anything; `include SomeClass` raises `TypeError`. - A refinement module also undefines the three action methods, since refinements are activated with `using`, not mixed in. ## A veto, step by step A guarded mixin behaves like this when a class includes it: 1. `include` calls the module's `append_features(host)` override. 2. The override inspects `host` and raises if it is the wrong kind of object; nothing has been inserted yet. 3. Otherwise it calls `super`, and the default implementation inserts the module. 4. `include` then calls `included(host)`, where the usual setup runs. Because the raise happens in step 2, `host.ancestors` is untouched and there is nothing to clean up. The same guard written in `included` would run in step 4, after the insertion, and could not take it back. ## Review checklist - Does every `append_features` override call `super` on the path that should mix in? - Is logic in the notification hook idempotent, given repeated includes? - Would a guard be clearer as a raise in `append_features` than as a raise in `included`, which fires after the module is already inserted?

  • In Ruby, if a guard raises inside included instead of append_features, is the module already mixed in?
    Yes. `include` runs `append_features` first, so by the time `included` raises, the module is already in the host's ancestors. The exception aborts the `include` call, but the insertion is not undone. A guard that must prevent the mix-in belongs in `append_features`, before `super`.
  • In Ruby, which methods play the append_features role for prepend and extend?
    `prepend` calls `prepend_features(host)` then `prepended(host)`; `extend` calls `extend_object(obj)` then `extended(obj)`. The default `extend_object` inserts the module into the object's singleton class. Overriding either follows the same rules: call `super` to keep the default behaviour.

saying these in an interview costs you the question

  • Says included and append_features are two names for the same hook.
  • Claims an append_features override without super still mixes the module in.
  • Believes including an already-included module raises an error.
  • Puts ordinary registration logic in append_features instead of included.
  • Assumes a raise inside included rolls back the module's insertion.