In Ruby, what happens when you call extend on one StockMovement instance instead of on the StockMovement class?
answer
- one object, not the class
- goes into that object's singleton class
- other instances unchanged
- returns the extended object
- frozen object: FrozenError
basics
~10 smovement.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 linesmodule 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) # FrozenErrorgo deeper
Know that extend on one object gives only that object the module's methods, and that the class and other instances are unaffected.
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.
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.
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