Modules
This chapter introduces modules, a tool for organizing code and sharing behavior between classes. Modules are the natural complement to classes and inheritance.
Principle
In chapter 9, we saw that inheritance allows specializing a class. But it has a limitation: in Ruby, a class can only inherit from one parent. What if we want to give display capabilities to Pokemon and Team, two classes with no parent-child relationship?
That is the role of modules. A module is a container for methods and constants. It looks like a class, but with two major differences:
- You cannot create an instance of a module (no
.new) - You can include it in as many classes as you want
A module has two main uses:
- Namespace: grouping classes and constants under the same roof to avoid name conflicts
- Mixin: sharing methods between multiple classes without inheritance
Namespaces
Imagine we have a Pokemon class for the Pokedex and another Pokemon for combat. Without a module, this is a name conflict. Modules solve this problem:
module Pokedex
class Pokemon
attr_reader :name, :types
def initialize(name, types)
@name = name
@types = types
end
end
end
module Combat
class Pokemon
attr_reader :name, :hp
def initialize(name, hp)
@name = name
@hp = hp
end
end
end
# Two Pokemon classes, no conflict
entry = Pokedex::Pokemon.new('Pikachu', [:electric])
fighter = Combat::Pokemon.new('Pikachu', 55)
module Pokedex ... endcreates a namespace.Pokedex::PokemonandCombat::Pokemoncoexist without issue.::(double colon) is the scope resolution operator. It navigates through modules:Pokedex::Pokemonmeans "the classPokemoninside the modulePokedex".
Constants in a module
A module can contain constants:
module Pokedex
VERSION = '1.0.0'
MAX_TEAM_SIZE = 6
end
puts Pokedex::VERSION # => 1.0.0
puts Pokedex::MAX_TEAM_SIZE # => 6
- Constants are accessible from the outside with
::. - Inside the module, they are accessed directly by their name.
Nested modules
Modules can be nested inside each other:
module Pokedex
module Data
class Pokemon
attr_reader :name
def initialize(name)
@name = name
end
end
end
end
pikachu = Pokedex::Data::Pokemon.new('Pikachu')
Pokedex::Data::Pokemon: we chain::to navigate through nesting levels.
You can also reopen a module to add content later:
module Pokedex
VERSION = '1.0.0'
end
# Later in the code (or in another file)
module Pokedex
MAX_TEAM_SIZE = 6
end
puts Pokedex::VERSION # => 1.0.0
puts Pokedex::MAX_TEAM_SIZE # => 6
- Ruby merges the declarations. This is very common when code is spread across multiple files.
include — sharing behavior (mixin)
include injects a module's methods into a class. The module's methods become instance methods of the class:
module Displayable
def summary
return "#{@name} Lvl.#{@level}"
end
end
class Pokemon
include Displayable
def initialize(name, level)
@name = name
@level = level
end
end
class Trainer
include Displayable
def initialize(name, level)
@name = name
@level = level
end
end
pikachu = Pokemon.new('Pikachu', 25)
sacha = Trainer.new('Ash', 10)
puts pikachu.summary # => Pikachu Lvl.25
puts sacha.summary # => Ash Lvl.10
include Displayablegives thesummarymethod toPokemonandTrainer. We shared behavior without inheritance.- The module accesses the instance variables (
@name,@level) of the object that includes it. So the class must define those variables. - This is what we call a mixin: we "mix" the module's methods into the class.
include and the ancestor chain
When you include a module, it appears in .ancestors:
puts Pokemon.ancestors.inspect
# => [Pokemon, Displayable, Object, Kernel, BasicObject]
- Ruby looks for methods in the order of
.ancestors: first in the class, then in the included modules, then in the parent class. - If multiple modules define the same method, the last included one takes priority.
extend — class methods
extend works like include, but the methods become class methods instead of instance methods:
module Searchable
def find_by_type(team, type)
return team.select { |pokemon| pokemon.types.include?(type) }
end
end
class Pokemon
extend Searchable
attr_reader :name, :types
def initialize(name, types)
@name = name
@types = types
end
end
team = [
Pokemon.new('Pikachu', [:electric]),
Pokemon.new('Charizard', [:fire, :flying]),
Pokemon.new('Blastoise', [:water])
]
# Called on the class (not on an object)
fire_pokemon = Pokemon.find_by_type(team, :fire)
puts fire_pokemon.first.name # => Charizard
include-> instance methods (called on an object:pikachu.summary)extend-> class methods (called on the class:Pokemon.find_by_type(...))
module_function — utility functions
module_function makes methods callable directly on the module, like utility functions:
module Pokedex
module Combat
def calculate_damage(attack, defense, power)
return ((power * attack) / (defense + 1.0)).round
end
module_function :calculate_damage
end
end
puts Pokedex::Combat.calculate_damage(55, 40, 60) # => 80
module_function :calculate_damagemakes the method callable directly on the module.- This is the same mechanism as
Math.sqrt(4)in Ruby:Mathis a module, andsqrtis amodule_function.
You can also write module_function without an argument so that all methods that follow become module functions:
module Pokedex
module Combat
module_function
def calculate_damage(attack, defense, power)
return ((power * attack) / (defense + 1.0)).round
end
def effectiveness(attack_type, defense_type)
# ...
end
end
end
Absolute resolution with
If an inner module has the same name as an outer module, you can force resolution from the root with a :: prefix:
module Pokedex
REGION = 'Kanto'
module Data
REGION = 'Johto'
def self.display_regions
puts REGION # => Johto (relative resolution)
puts Pokedex::REGION # => Kanto (explicit resolution)
puts ::Pokedex::REGION # => Kanto (absolute resolution from the root)
end
end
end
Pokedex::Data.display_regions
::Pokedex::REGION: the leading::forces Ruby to search from the top level. This is rarely needed, but useful in case of ambiguity.
Conclusion
- A module is a container for methods and constants. You cannot create instances of it.
- Modules serve as namespaces to avoid conflicts (
Pokedex::PokemonvsCombat::Pokemon). includeinjects a module's methods as instance methods. This is the mixin.extendinjects the methods as class methods.module_functionmakes methods callable directly on the module (likeMath.sqrt).::navigates through modules.::Moduleas a prefix forces resolution from the root.- Modules can be nested and reopened freely. Ruby merges the declarations.
- Included modules appear in
.ancestors, which influences the method lookup order.