Ruby provides a construct called a Module
which is sort of like a thin Class. At first, Ruby devs might look at Modules like a “junk drawer” of code that is just thrown around and used to organize files, but there is a method to this madness. The Ruby class Class
inherits from Module
and adds things like instantiation, properties, etc – all things you would normally think a class would have. Because Module
is literally an ancestor of Class
, this means Modules can be treated like classes in some ways.
Most importantly to understand is how the Ruby ancestor chain works. Like all OOP languages, Ruby supports inheritance. When you inherit from a class, the superclass is added to your ancestor chain. You can view the ancestors of any class by calling ancestors
on it:
class Foo < BarendFoo.ancestors#=> [Foo, Bar, Object, ..., BasicObject]
As mentioned, you can find Module
in the array Class.ancestors
.
When you include
a module into your class, the module is added to your class’s ancestor chain – just like a class. This makes include
just a form of inheritance, there isn’t anything special about it.
module Barendclass Foo include BarendFoo.ancestors#=> [Foo, Bar, Object, ..., BasicObject]
All of the methods in Bar
are added to Foo
as instance methods. Because Bar is in the chain, you can even call super
from it to call methods above it in the chain, whether they are defined on Modules or Classes.
class Baz def hello p 'world' endendmodule Bar def hello p 'hello' super endendclass Foo < Baz include BarendFoo.new.hello#=> hello world
Even though Bar
is a module, super
still calls up the chain and so Baz#hello
is also called. It is worth noting that Bar
is added to the ancestor chain in front of Baz. When a module is included, it is always added directly on top of it’s including class. This can get confusing when adding multiple modules, since they are included in “reverse” order:
class Foo include A include BendFoo.ancestors#=> [Foo, B, A]
When A
is included, it is inserted directly above Foo
. But when B
is included, it is also inserted directly above Foo
, so it ends up landing before A
.
Include vs Extend
include
is easy enough to understand, it adds the module’s methods as instance methods to it’s including class. You can think of extend
doing the same thing, but instead adding the methods as class methods.
module Bar def hello p 'hello' endendclass Foo extend BarendFoo.hello # no .new!# => 'hello'
Prepend
In addition to include/extend, Ruby 2.0+ adds the prepend
method. prepend
is similar to include
, but instead inserts the module before the including class in the inheritance chain.
class Foo include Bar prepend BazendFoo.ancestors#=> [Baz, Foo, Bar]
In Ruby you can re-open any class and redefine the methods on it – it doesn’t seem useful to override methods on a class using a prepended module as opposed to redefining them. Where prepend
comes in handy is when you still need the original implementation of the method, because prepended modules can override methods but still use super
to call the original implementation.