skip to content

In Ruby, what happens when you call extend on one StockMovement instance instead of on the StockMovement class?

level: middleimportance: should knowfreq 35%

answer

  1. one object, not the class
  2. goes into that object's singleton class
  3. other instances unchanged
  4. returns the extended object
  5. frozen object: FrozenError

basics

~10 s

movement.extend(ManualReview) adds the module's methods to that one object only; other StockMovement instances and the class are unchanged. Calling StockMovement.extend(ManualReview) instead adds them to the class object as class methods.

solid answer

~40 s

`Object#extend` adds a module to the lookup of **one object**, through that object's own singleton class, and returns the object. So `movement.extend(ManualReview)` gives just that movement methods such as `flag_for_review`, and they take precedence over the class's methods of the same name for that object; other instances and `StockMovement` itself are untouched. `StockMovement.extend(ManualReview)` is the same call on a different object, the class, so the methods become class methods. Extending is permanent for the object's lifetime, raises `FrozenError` on a frozen object and `TypeError` on objects that cannot have a singleton class, such as Integers. It suits giving a role to one object at run time; for behaviour every instance needs, include the module in the class.

code

ruby · 21 lines
ruby
module ManualReview
  def approved? = false            # overrides the class's method for this object
  def flag_for_review = "review: #{quantity} units"
end

class StockMovement
  attr_reader :quantity
  def initialize(quantity)
    @quantity = quantity
  end
  def approved? = true
end

routine = StockMovement.new(5)
large   = StockMovement.new(5_000).extend(ManualReview) # extend returns the object

large.approved?        # => false
large.flag_for_review  # => "review: 5000 units"
routine.approved?      # => true
routine.respond_to?(:flag_for_review) # => false
StockMovement.new(1).freeze.extend(ManualReview) # FrozenError

go deeper

for a junior

Know that extend on one object gives only that object the module's methods, and that the class and other instances are unaffected.

for a middle

Explain that extend works through the object's singleton class, so the module's methods win over the class's for that object, and list the errors: FrozenError, TypeError.

for a senior

Judge when per-object extend is appropriate, such as giving a role to one record, and when its costs, a singleton class per object and hidden behaviour, make a decorator or an include better.

for a principal

Set guidance on run-time role assignment: allow per-object extend only where the role is rare and permanent for the object, and prefer explicit wrappers where behaviour must be visible in the class.

## extend works on exactly one object `Object#extend(module, ...)` is defined on every object. It adds the module's instance methods to the receiver **only**, and returns the receiver. There is no separate "class-level" and "instance-level" extend; the difference is just which object you call it on. | Call | Object that gains the methods | Effect on others | |---|---|---| | `movement.extend(ManualReview)` | this one `StockMovement` instance | none | | `StockMovement.extend(ManualReview)` | the `StockMovement` class object | instances do not gain them | | `class StockMovement; include ManualReview; end` | every `StockMovement` instance | the class object does not gain them | Internally, Ruby gives the object its own hidden class (its **singleton class**) and includes the module there. Because that hidden class sits in front of the object's normal class, a method from the extended module takes precedence over the class's method of the same name, for that object only. The rdoc example for `extend` shows exactly this: `k.hello` returns the class's greeting, and after `k.extend(Mod)` it returns the module's. ## An inventory example In an inventory system, most stock movements are routine, but a movement larger than a threshold needs manual review. Rather than adding review methods to every movement: 1. Detect the large movement where it is created. 2. Call `movement.extend(ManualReview)` on that object. 3. Code that later handles the object can call `movement.flag_for_review` or override `movement.approved?` for that record only. Every other `StockMovement` still behaves exactly as the class defines it. This pattern, attaching a role to one object for part of its life, is the main legitimate use of per-object extend. ## Rules and limits - **Modules only.** `extend` with a class raises `TypeError`. - **Several modules.** `obj.extend(A, B)` is allowed and still returns the object. - **Callbacks.** Each module's `extend_object` and `extended` hooks run, which is how a module can react to being attached. - **Frozen objects.** A frozen object's singleton class is frozen too, so extending it raises `FrozenError`. - **Immediates.** Integers, Floats and Symbols cannot have singleton classes, so extending them raises `TypeError` (`can't define singleton`). - **No undo.** Ruby has no method that removes an extended module; the object keeps the methods until it is garbage collected. To end a role, drop the object or wrap it instead. ## Inspecting an extended object When an object behaves differently from its siblings, per-object extension is one of the first things to check: - `movement.singleton_class.include?(ManualReview)` is `true` only for the extended object. - `movement.singleton_class.ancestors.first(3)` lists the singleton class, then the extended module, then `StockMovement`. - `movement.singleton_methods` includes methods that come from modules the object was extended with. - `StockMovement.include?(ManualReview)` stays `false`, confirming that the class was never changed. These checks make hidden per-object behaviour visible during debugging, which matters because nothing in the class's source mentions the module. ## Costs and alternatives Per-object extend is convenient, but it is not free: - Every extended object gets its own singleton class, which costs memory and makes extending thousands of objects in a loop a poor fit. - Behaviour that depends on which objects were extended is hard to follow when reading the class, since nothing in `StockMovement` mentions the module. - A wrapper object that delegates to the movement (a decorator) keeps the original untouched and can be discarded; it is often the better choice when the role is temporary. When the behaviour belongs to every instance, include the module in the class; when it belongs to the class as a whole, extend the class; when it belongs to one object, extend that object.

  • How can code check whether one particular object was extended with ManualReview?
    `movement.singleton_class.include?(ManualReview)` returns `true` only for the extended object, because extend includes the module in that object's singleton class. `StockMovement.include?(ManualReview)` stays `false`, since the class itself was never changed.
  • Why would you pick a decorator object over extend for a temporary role?
    `extend` cannot be undone and changes the original object for everyone holding a reference to it. A decorator wraps the object, adds the role's methods, and can be dropped when the role ends, leaving the original untouched.

saying these in an interview costs you the question

  • Extending one instance adds the methods to every instance of its class
  • Object#extend returns the module, not the extended object
  • An extended module's method loses to the class's own method on that object
  • You can remove an extended module later with a built-in method
  • Extending a frozen object works because freezing only protects instance variables