skip to content

In Ruby, how does prepend differ from include, and why does only prepend let an AuditSave module wrap Product#save through super?

level: middleimportance: must knowfreq 60%

answer

  1. position relative to the class
  2. included module sits after the class
  3. prepended module sits before it
  4. super reaches the class's own method
  5. replaced alias_method chaining

basics

~20 s

include places a module after the class in method lookup, so the class's own save wins. prepend places it before the class, so AuditSave#save runs first and its super calls Product#save, which makes wrapping possible.

solid answer

~40 s

Both insert a module into the class's ancestors, but at different positions. With `include AuditSave`, the chain starts at `Product`, so `Product#save` is found first and `AuditSave#save` runs only if `Product#save` itself calls `super`. With `prepend AuditSave`, the module sits **in front of** the class: `product.save` finds `AuditSave#save` first, which can do its work before and after `super`, and `super` reaches `Product#save`. That is how a module adds auditing around an existing method without editing the class or renaming the original. Before `prepend` existed, the usual trick was `alias_method` chaining: rename the original and redefine the method to call the renamed one.

code

ruby · 19 lines
ruby
module AuditSave
  def save
    before = price
    result = super            # runs Product#save
    puts "audit: price #{before} -> #{price}" if result
    result
  end
end

class Product
  attr_accessor :price
  def save = true            # persistence stand-in
  prepend AuditSave
end

Product.ancestors.first(2)   # => [AuditSave, Product]
product = Product.new
product.price = 12
product.save                 # prints "audit: price 12 -> 12", returns true

go deeper

for a junior

Remember that prepend puts the module in front of the class and include puts it behind, so only prepend can wrap a method the class defines.

for a middle

Walk through what product.save finds first in each case, show a wrapper calling super, and explain what alias_method chaining did before prepend.

for a senior

Use prepend for cross-cutting behaviour such as auditing across several models, and explain the operational risks: a wrapper that forgets super or raises changes every instance of every class it is prepended to.

for a principal

Decide where wrapping belongs in a codebase: prepended modules keep classes untouched but hide behaviour from readers, so weigh them against explicit calls or a separate service object that makes the flow visible.

## Same mechanism, different position `include` and `prepend` both put a module into the list of places Ruby searches for a method (the class's **ancestors**). The difference is only where: | | `include AuditSave` | `prepend AuditSave` | |---|---|---| | Position | after `Product` | before `Product` | | `product.save` finds first | `Product#save` | `AuditSave#save` | | `super` in the module's `save` reaches | the next ancestor after the module (for example a superclass) | `Product#save` | | Suitable for | adding new methods or defaults | wrapping existing methods | | Callback it triggers | `included` | `prepended` | `Product.ancestors.first(2)` shows it directly: `[Product, AuditSave]` after `include`, `[AuditSave, Product]` after `prepend`. ## Why include cannot wrap Suppose `Product#save` persists a record, and the inventory team wants every save to be audited. With `include AuditSave`: 1. `product.save` looks in `Product` first and finds `Product#save`. 2. `Product#save` runs; `AuditSave#save` is never reached. 3. Only if `Product#save` itself calls `super` would the module's method run, and then it runs **after** the class code has started, which is backwards for a wrapper. An included module is therefore good for **providing** methods the class does not define, or defaults a class may override. It cannot intercept a method the class defines itself. ## Wrapping with prepend With `prepend AuditSave`, the lookup reaches the module first: - code before `super` runs before the class's method (capture the old values); - `super` calls `Product#save`; - code after `super` sees the result (record the change only if the save succeeded); - the method returns whatever it chooses, usually the result of `super`. The class keeps its own `save` untouched, and the wrapper can be shared across `Product`, `Warehouse` and `StockMovement` by prepending the same module to each. ## Choosing include or prepend A quick decision guide for a shared module: | The module should... | Use | |---|---| | add methods the class does not define | `include` | | provide defaults a class may override | `include` | | run code before or after a method the class defines | `prepend` | | replace a class's method for every instance | `prepend`, calling `super` when appropriate | | add behaviour to one object only | `extend` on that object | A module can mix both styles: an audit feature might `include` helpers such as `audit_trail` and `prepend` a separate wrapper module for `save`. Keeping the wrapper in its own module makes the intent clear and lets a class opt in to one without the other. ## What prepend replaced Before `prepend`, wrapping a method meant **alias_method chaining**: ```ruby class Product alias_method :save_without_audit, :save def save result = save_without_audit record_audit if result result end end ``` This works but has costs: it adds a renamed method to the class's public surface, and two libraries chaining the same method with different names can break each other. `prepend` expresses the same wrapper as a module in the lookup chain, and `super` replaces the renamed method. ## Things to keep in mind - `prepend` takes modules only, like `include`; a class argument raises `TypeError`. - Prepending changes behaviour for every instance of the class and its subclasses, so a wrapper that raises or slows down affects all of them. - A prepended module's method can still be skipped only by not calling `super`; forgetting `super` silently disables the class's method. - Patching core classes such as `String` with `prepend` is its own topic with its own risks; the mechanism is the same, but the blast radius is the whole process.

  • Does it matter whether prepend AuditSave appears before or after def save in the class body?
    No. `prepend` changes the lookup chain, not a copy of the method, so the module sits in front of `Product` whatever order the lines appear in. A `save` defined in the class later is still found after the module.
  • What happens if AuditSave#save forgets to call super?
    `Product#save` never runs, because the prepended module's method is found first and nothing passes control on. The wrapper then silently replaces the class's behaviour, which is why a prepended wrapper should call `super` on every path and return its result.

prepend puts a receptionist in front of an office: every visitor meets the receptionist first, who logs the visit and then sends them in with super. include puts a helper in a back room that visitors reach only if the office itself sends them on.

saying these in an interview costs you the question

  • include and prepend are aliases that differ only in name
  • An included module can wrap the class's own method with super
  • prepend copies the module's methods over the class's methods
  • prepend must come after def save in the class body to take effect
  • alias_method chaining is still the preferred way to wrap a method