Modules: Namespaces and Mixins
Prerequisites: Polymorphism and duck typing
A module is a container for methods and constants. Unlike a class, you can't create objects from a module. Modules have two main jobs.
1. Namespaces — grouping related code
A module keeps names from clashing by giving them a prefix:
module Billing
TAX_RATE = 0.2
class Invoice
def initialize(amount) = @amount = amount
def total = @amount * (1 + TAX_RATE)
end
def self.format(amount) = "$#{'%.2f' % amount}"
end
invoice = Billing::Invoice.new(50)
puts Billing.format(invoice.total)
$60.00
Billing::Invoice can't collide with some other library's Invoice
class. Almost every gem wraps its code in a module named after itself.
2. Mixins — sharing behavior
include copies a module's methods into a class as instance methods.
This is called a mixin, and it's how Ruby shares abilities between
unrelated classes without inheritance:
module Greetable
def greet
"Hi, I'm #{name}"
end
end
module Timestamped
def created_label
"created #{created_on}"
end
end
class User
include Greetable
include Timestamped
attr_reader :name, :created_on
def initialize(name, created_on)
@name = name
@created_on = created_on
end
end
class Robot
include Greetable
def name = "R2"
end
user = User.new("Ada", "2025-01-15")
puts user.greet
puts user.created_label
puts Robot.new.greet
p User.ancestors.first(3)
Hi, I'm Ada
created 2025-01-15
Hi, I'm R2
[User, Timestamped, Greetable]
Notice that Greetable#greet calls name — the module assumes the
class provides it. That's duck typing again: a module can rely on any
method the including class has.
ancestors shows the method lookup chain: Ruby looks for a method
in User first, then in its included modules (most recently included
first), then up the superclass chain.
extend adds a module's methods to a single object (or to a class as
class methods) instead of to every instance.
Common mistakes
- Using inheritance just to share a helper method. If two classes aren't "kinds of" the same thing, a mixin is a better fit.
- Mixing in large modules with lots of hidden assumptions about the class. Keep mixins small and document what they expect.
Practice
Write a Describable module with a describe method that returns
"#{self.class.name}: #{title}". Include it in Book and Movie
classes that each have a title.
Show solution
module Describable
def describe = "#{self.class.name}: #{title}"
end
class Book
include Describable
attr_reader :title
def initialize(title) = @title = title
end
class Movie
include Describable
attr_reader :title
def initialize(title) = @title = title
end
puts Book.new("Dune").describe
puts Movie.new("Up").describe
Book: Dune
Movie: Up