Better Understanding Ruby Modules Through Zombies
Modules in Ruby are a great place to store shared behavior and data used throughout a program. Writing a module is a convenient way to abstract out shared methods from multiple classes, thereby making the classes more DRY and manageable.
Some of the most popular modules in the Ruby core are Comparable
, Math
, and Enumerable
. Yes, a module is the reason Rubyists across the world can enjoy each
and other higher level iterators like all?
, collect
, and inject
! ActiveRecord
and Sinatra
are two other popular modules used in the Rails and Sinatra web frameworks, respectively.
Why Use Modules?
DHH likes modules. And they’re cool.
How to Name a Module
Names are important in programming. And since modules are known in technical parlance as “namespaces”, module names are doubly important… and doubly spacious.
Like classes, modules in Ruby are capitalized in CamelCase. Whereas classes tend to be singular nouns (Person
, Dog
, Artist
), module names tend to be adjectives (Magical
, Persistable
, Catlike
). Modules often have unique names that describe the behaviors, characteristics, or qualities shared by multiple classes.
These naming conventions make sense because, unlike classes, modules cannot be instantiated. You can make a new Person
, but you cannot make a new Magical
.
In your Rails app, file your modules in the app/models/concerns
, app/controllers/concerns
, and lib/concerns
directories.
Ineritance via Modules
Direct class-to-class inheritance in Ruby is a tricky subject: only single inheritance is allowed. For example, this is valid Ruby:
class Mammal
end
class Animal < Mammal
end
class Dog < Animal
end
Dog.ancestors
#=> [Dog, Animal, Mammal, Object, Kernel, BasicObject]
but this is not:
class Animal
end
class MansBestFriend
end
class Dog < Animal
end
class Dog < MansBestFriend
end
#=> TypeError: superclass mismatch for class Dog
Dog.ancestors
#=> [Dog, Animal, Object, Kernel, BasicObject]
Modules solve the problem of multiple inheritance in an interesting way. A class can call the extend
method to inherit a module’s class methods, and a class can call the include
method to inherit a module’s instance methods.
As a result, it is good practice to write modules in the following way:
module Generic
module ClassMethods
# class methods go here
end
module InstanceMethods
# instance methods go here
end
end
The methods in the Generic
module can be mixed in to a class as follows:
class Student
extend Generic::ClassMethods
include Generic::InstanceMethods
end
Enter Zombies
Zombies provide a useful analogy for thinking about the relationship between classes and modules. Consider the Person
class:
class Person
attr_accessor :name
def initialize(name)
self.name = name
end
def occupation
"Being a human."
end
end
We can instantiate a new person as follows:
chris = Person.new("Chris")
#=> #<Person:0x0000010185eba0 @name="Chris">
chris.name
#=> "Chris"
chris.occupation
#=> "Being a human."
Now let’s say that a zombie pandemic breaks out. First thing’s first: how do we refactor our code?
We could rename our Person
class to Zombie
or write a new Zombie
class, but neither of these options feel totally right. Instead, it would seem more appropriate for instances of the Person
class to adopt zombie behavior in addition to the original behaviors of the Person
class.
A Zombified
module is the obvious choice here: it is an abstraction encapsulating shared behavior across (potentially) multiple classes, and it does not need to be instantiated.
module Zombified
module ClassMethods
def status_of_the_human_race
"doomed"
end
end
module InstanceMethods
def preferred_source_of_energy
"BRAAAAIIINNNNSSSS!!!!!"
end
def is_alive?
false
end
end
end
include
When a module is include
d into a class, instances of that class have access to the instance methods of both the class and the module mixed in to the class.
class Person
include Zombified::InstanceMethods
end
chris = Person.new("Brains")
#=> #<Person:0x00000102250618 @name="Brains">
chris.name
#=> "Brains"
chris.preferred_source_of_energy
#=> "BRAAAAIIINNNNSSSS!!!!!"
chris.is_alive?
#=> false
Now chris
has access to both the instance methods of the Person
class and the instance methods provided by the Zombified::InstanceMethods
module. In chris
’s method lookup chain, the methods in Person
come before the methods in the Zombified::InstanceMethods
module.
Person.ancestors
#=> [Person, Zombified::InstanceMethods, Object, Kernel, BasicObject]
An important takeaway is that method names in a module should not conflict with those of the method names in the classes they are mixed into. Instead, choose more abstract and granular method names.
extend
Uh oh. The zombie pandemic has spread across the planet. What’s the status of the human race, then?
Person.status_of_the_human_race
#=> NoMethodError: undefined method `status_of_the_human_race' for Person:Class
Right. Time to refactor our code!
class Person
extend Zombified::ClassMethods
end
Person.status_of_the_human_race
#=> "doomed"
Humanity is doomed, thanks to our handy module.
From here it’s easy to zombify objects in other classes:
class Dog
include Zombified::InstanceMethods
end
fido = Dog.new
#=> #<Dog:0x0000010326e428>
fido.preferred_source_of_energy
#=> "BRAAAAIIINNNNSSSS!!!!!"
fido.is_alive?
#=> false
Conclusion
Modules are fun! I hope to do another blog post about some of the other cool and more metaprogramm-y things you can do with modules, but for now check out some of the links below.