Here’s a question for you lovely developers (and non-developers, too!) out there: how can we dynamically include behavior from a specific module at runtime? Put differently, is it possible to include one specific module’s behavior after a
class definition has already been read in? The typical Ruby include-behavior-from-a-module pattern looks something like this:
1 2 3 4 5 6 7 8 9
Trainable might extend the behavior of
Employee instances something like this:
1 2 3 4 5 6
This is a contrived example, clearly, but the pattern should look familiar: in Ruby, we love to encapsulate behavior into modules and
include them - the name for this practice is separation of concerns. However, since we write the
include directive within a class-level scope, we cannot for example use an instance of
Employee itself to determine which module’s behavior we’d like to include. Let me explain with a quick example…
Different Modules for Different Training
Assume that instead of one
Trainable module, we really have multiple modules that encapsulate distinct forms of employee training like follows:
1 2 3 4
(NB: As you can see, I’ve omitted any actual methods from these modules above, but use your imagination! e.g.
ExecutiveTrainable#soul_suck!, or maybe
These modules may have also been written to reflect the various real-world types of
Employee that are modeled in our Ruby application:
Each of these
employees also need to be able to go through some form of “training”. So, in order to ensure that all of
Employee types can be
train!‘ed in the appropriate manner, we can simply
include all modules at the class level:
1 2 3 4 5 6 7 8 9 10
Unforutnately, this has a nubmer of problems. First, it doesn’t communicate the developer’s intent very clearly. Someone coming along and reading this code in some months’ time might ask, “What’s going on - why all these variations of
Trainable?” Second, we risk overwriting methods if the modules share the same/similar interfaces (for example, if each module implements
#train! independently, you would have to change the method names, or apply logic to the ordering of our 4
include directives, or… well, you’d have a real problem on your hands to solve at that point).
extend at instance-level scope to simulate
Ruby thankfully offers a very elegant, dynamic solution to this problem. Since the logic of which
Trainable module should be included is a function of the instance’s
employee.type, we can use a lifecycle hook in combination with
extend like so:
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10 11 12
Code Example: add
Human#moniker behavior with both class-level
include and instance-level
In the following code example (where all humans are named
"Matt!", ha), you can see how either
extend can be used (at different scopes, of course) to tell all
Humans how to report their
name. Note that console/IO output is indicated by commented out lines with a single
# - you’d get the same output by copy-pasting
this code into a live REPL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
Black magic?! How does it work?!!
To understand why
extending at an instance scope works the way it does, it will help to understand the concept of “eigenclasses” in Ruby. First, let’s define eigenclass and then write a sort of helper method that tells us any object’s eigenclass on demand.
Eigenclass: a dynamically created anonymous class that Ruby inserts into the method lookup path any time at least one singleton method is added to an object. (I added the last bit in italics… I think it’s right, ha.)
1 2 3 4 5 6 7
To tie the eigenclass concept back into our previous code example, let’s try to answer the question: Where does
extended_human.moniker actually “live”?
If we query both of our instances’ classes, it’s not immediately clear where our
#moniker method lives…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
I typically query an object’s class’s ancestors when I want to see its inheritance hierarchy (where ‘inheritance’ is a combination of mixins and classical inheritance, i.e.
class Foo < Bar) as part of finding where a particular method might have come from. This exercise reminds us that in Ruby, the
Kernel module (responsible for
puts, etc.) is quite high up in the object hierarchy, and as such its methods/behaviors are made available practically everywhere in a Ruby program.
Notably absent from
extended_human.class.ancestors, however, is any reference to our
So - where is
extended_human.moniker coming from then? Let’s instead look through the instances’ eigenclasses’ ancestors:
Ruby’s method lookup path flows through eigenclasses first!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Aha - found it! Note that for our
Nameable is the very first ancestor in its method-lookup hierarchy. This is because we
Nameable directly into
extended_human’s eigenclass, as opposed to
include‘ing it in its containing class definition. And once again, to display this point differently:
1 2 3 4
By making use of an anonymous class under the hood at runtime, Ruby gives us the ability to dynamically mixin behaviors at all levels of our programs - namely in our example, at both the class and instance level! Wo-man, I <3 Ruby :)