Skip to main content
View
Chapter 10 / 16Modules
1. Variables and data types
2. Strings and symbols
3. Arrays
4. Hashes
5. Conditionals
6. Loops and iterators
7. Methods and blocks
8. Classes and objects
9. Inheritance
10. Modules
11. Extending code with prepend
12. Enumerable
13. Where to go next
14. Error handling
15. Reading and writing files
16. Advanced class features

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 ... end creates a namespace. Pokedex::Pokemon and Combat::Pokemon coexist without issue.
  • :: (double colon) is the scope resolution operator. It navigates through modules: Pokedex::Pokemon means "the class Pokemon inside the module Pokedex".

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 Displayable gives the summary method to Pokemon and Trainer. 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_damage makes the method callable directly on the module.
  • This is the same mechanism as Math.sqrt(4) in Ruby: Math is a module, and sqrt is a module_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::Pokemon vs Combat::Pokemon).
  • include injects a module's methods as instance methods. This is the mixin.
  • extend injects the methods as class methods.
  • module_function makes methods callable directly on the module (like Math.sqrt).
  • :: navigates through modules. ::Module as 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.