Where you get one of Bill’s thorough lessons in advanced coding.
The object model is an eventful place. Lots of things happen there as your code runs: classes are inherited, modules are mixed into classes, and methods are defined, undefined, and removed. Imagine if you could “catch” these events like you catch GUI mouse-click events. You’d be able to execute code whenever a class is inherited or whenever a class gains a new method.
Well, it turns out you can do all these things. This program prints a notification on the screen when a class inherits from String:
| class String |
| def self.inherited(subclass) |
| puts "#{self} was inherited by #{subclass}" |
| end |
| end |
| |
| class MyString < String; end |
<= | String was inherited by MyString |
The inherited method is an instance method of Class, and Ruby calls it when a class is inherited. By default, Class#inherited does nothing, but you can override it with your own code as in the earlier example. A method such as Class#inherited is called a Spell: because you can use it to hook into a particular event.
Ruby provides a motley bunch of hooks that cover the most newsworthy events in the object model. Just as you override Class#inherited to plug into the lifecycle of classes, you can plug into the lifecycle of modules by overriding Module#included and (in Ruby 2.0) Module#prepended:
| module M1 |
| def self.included(othermod) |
| puts "M1 was included into #{othermod}" |
| end |
| end |
| |
| module M2 |
| def self.prepended(othermod) |
| puts "M2 was prepended to #{othermod}" |
| end |
| end |
| |
| class C |
| include M1 |
| prepend M2 |
| end |
<= | M1 was included into C |
| M2 was prepended to C |
You can also execute code when a module extends an object by overriding Module#extended. Finally, you can execute method-related events by overriding Module#method_added, method_removed, or method_undefined.
| module M |
| def self.method_added(method) |
| puts "New method: M##{method}" |
| end |
| |
| def my_method; end |
| end |
<= | New method: M#my_method |
These hooks only work for regular instance methods, which live in the object’s class. They don’t work for Singleton Methods (), which live in the object’s singleton class. To catch Singleton Method events, you can use BasicObject#singleton_method_added, singleton_method_removed, and singleton_method_undefined.
Module#included is probably the most widely used hook, thanks to a common metaprogramming spell that’s worthy of an example of its own.
VCR is a gem that records and replays HTTP calls. The Request class in VCR includes a Normalizers::Body module:
| module VCR |
| class Request #... |
| include Normalizers::Body |
| #... |
The Body module defines methods that deal with an HTTP message body, such as body_from. After the include, those methods become class methods on Request. Yes, you read that right: Request is gaining new class methods by including Normalizers::Body. But a class usually gets instance methods by including a module—not class methods. How can a mixin like Normalizers::Body bend the rules and define class methods on its includer?
Look for the answer in the definition of the Body module itself:
| module VCR |
| module Normalizers |
| module Body |
| def self.included(klass) |
| klass.extend ClassMethods |
| end |
| |
| module ClassMethods |
| def body_from(hash_or_string) |
| # ... |
The code above pulls off a convoluted trick. Body has an inner module named ClassMethods that defines body_from and other methods as regular instance methods. Body also has an included Hook Method (). When Request includes Body, it triggers a chain of events:
Ruby calls the included hook on Body.
The hook turns back to Request and extends it with the ClassMethods module.
The extend method includes the methods from ClassMethods in the Request’s singleton class. (You might remember this last part of the trick from .)
As a result, body_from and other instance methods get mixed into the singleton class of Request, effectively becoming class methods of Request. How’s that for a complicated code concoction?
This ClassMethods-plus-hook idiom used to be quite common, and it was used extensively by the Rails source code. As you’ll see in Chapter 10, , Rails has since moved to an alternate mechanism—but you can still find examples of the idiom in VCR and other gems.