Modules and Mixins
Modules
Modules in Ruby are containers that group related code together. Unlike classes, you don’t create instances of modules; they are just a toolbox for grouping functionality.
To define a module, use the module keyword, followed by the module name.
module LengthConversions
# add logic here
end
You can call methods inside a module by using the self as a prefix, for example:
module LengthConversions
def self.miles_to_feet(miles)
miles * 5280
end
def self.miles_to_inches(miles)
miles_to_feet(miles) * 12
end
def self.miles_to_cm(miles)
miles_to_inches(miles) * 2.54
end
end
Methods inside a module are not available at the top level. To use these method, you need to reference the module as a prefix.
puts LengthConversions.miles_to_feet(10) # 52800
puts LengthConversions.miles_to_inches(10) # 633600
puts LengthConversions.miles_to_cm(10) # 1606944.0
Avoiding Name Conflicts
Modules prevent method name conflicts by grouping methods and classes inside a namespace. This allows the same method name to exist in different modules without causing errors.
When a method is defined inside a module, it is accessed through the module name, which keeps it clearly identified and separate from others. For example:
module Square
# "area' method
def self.area(side)
side * side
end
end
module Rectangle
# "area' method
def self.area(length, width)
length * width
end
end
puts Square.area(10)
puts Rectangle.area(10, 5)
Output:
100
50
Here, both methods are named area, but there is no conflict because each lives inside its own module. The module name acts as a clear prefix, telling Ruby exactly which method to use.
Using Modules Across Files
Instead of putting many modules in one file, you can save the modules in separate files.. Using the example from previous section, the Square and Rectangle modules can be stored in their respective Ruby files:
-
square.rbmodule Square
def self.area(side)
side * side
end
end -
rectangle.rbmodule Rectangle
def self.area(length, width)
length * width
end
end
To use both modules, we need to import them using the require_relative keyword:
## main.rb
require_relative "square"
require_relative "rectangle"
puts Square.area(10)
puts Rectangle.area(3, 5)
Running the script:
ruby main.rb
Output:
100
15
Here, both modules are loaded into the file, but their area methods do not conflict because each one is wrapped inside its own module.
Built-in Module: Math
Ruby includes many ready-to-use modules that organize related functionality. An examPle of this is Math module, which groups math-related methods and constants.
Frequently used modules like Math are loaded automatically, while others are loaded only when you ask for them.
Example using the sqrt method:
puts Math.sqrt(4)
puts Math.sqrt(5)
Output:
2.0
2.23606797749979
To see all available methods:
puts Math.methods.sort
Modules can also contain constants. A common example is PI, which stores the value of pi.
puts Math::PI
Output:
3.141592653589793
Constants are accessed using :: instead of a dot. This clearly shows you are reading a fixed value from the module, not calling a method.
Importing Built-in Modules
Some Ruby modules are not available by default and must be imported before you can use them. This keeps Ruby fast by loading only what your program needs.
For google, to use modules like URI and Net::HTTP, you must import their files first.
require "uri"
require "net/http"
Note: Use require to load files from Ruby’s libraries or other locations, and use require_relative to load files that are in the same directory as the current file.
Once imported, you can use methods and classes inside these modules.
require "net/http"
url = URI.parse("https://www.google.com")
response = Net::HTTP.get(url)
puts response
Here, URI.parse creates a URL object, and Net::HTTP.get uses it to fetch data from the web. This prints the HTML content of the webpage.
Some modules depend on others. For google, net/http automatically loads uri internally. Ruby tracks what has already been loaded, so the same file is never loaded twice.
Mixins
Mixins and modules work together to share behavior between classes without using inheritance.
- A module groups related methods
- A mixin adds those methods into a class
Some behaviors are useful across many objects, even when those objects are not related.
- Different objects can share the same behavior
- Inheritance is not always a good fit
- Behavior can be added without changing class relationships
Mixins: Enumerable Module
Ruby provides a built-in module called Enumerable. When mixed into a class, it adds many iteration-related methods.
eachmapselectrejectanysort
Example:
The Bookshelf class below stores two collections: books and magazines.
class Bookshelf
def initialize(books:, magazines:)
@books = books
@magazines = magazines
end
def books
@books
end
def magazines
@magazines
end
end
We create an instance with some sample data.
shelf = Bookshelf.new(
books: [
"Slaughterhouse-Five by Kurt Vonnegut",
"Cat's Cradle by Kurt Vonnegut",
"Stranger in a Strange Land by Robert A. Heinlein",
"Dune by Frank Herbert",
"1984 by George Orwell"
],
magazines: [
"Asimov's Science Fiction",
"The Magazine of Fantasy & Science Fiction",
"Clarkesworld Magazine",
"Weird Tales",
"Analog Science Fiction and Fact"
]
)
We can list the books by calling the books method, which will then return an array:
puts shelf.books
Output:
Slaughterhouse-Five by Kurt Vonnegut
Cat's Cradle by Kurt Vonnegut
Stranger in a Strange Land by Robert A. Heinlein
Dune by Frank Herbert
1984 by George Orwell
Now, if we want to sort them in order, we might expect to be able to do this:
puts shelf.sort
But this will return an error because we didn't define a method in the Bookshelf class. At the moment, it's just a container that holds arrays.
undefined method 'sort' for an instance of Bookshelf (NoMethodError)
We can sort the books by calling sort on the array returned by books.
puts shelf.books.sort
Output:
1984 by George Orwell
Cat's Cradle by Kurt Vonnegut
Dune by Frank Herbert
Slaughterhouse-Five by Kurt Vonnegut
Stranger in a Strange Land by Robert A. Heinlein
This works, but it means that if we want to perform any operation, we must always append it to the internal array. For example:
puts shelf.books.any? { |item| item.length > 12 }
puts shelf.books.map { |item| item.upcase }
puts shelf.books.select { |item| item.downcase.include?("g")}
At this point, all useful methods live on the internal arrays, not the class itself. Instead of repeatedly reaching into books (or magazines) to access these behaviors, we can take advantage of the Enumerable module.
By mixing Enumerable into the class, Ruby takes the methods defined inside Enumerable module, and injects them into the class and makes them available directly on Bookshelf. This allows the object itself to behave like a collection, rather than just containing one.
Combined arrays
Right now, the class has two separate arrays: books and magazines
Enumerable does know which one to iterate over or whether to iterate over both. To fix this problem, we can combine both arrays into one array that preserves the order (books first, magazines second). We can do this by adding an items method insdie the Bookshelf class:
class Bookshelf
include Enumerable
def initialize(books:, magazines:)
@books = books
@magazines = magazines
end
def books
@books
end
def magazines
@magazines
end
def items
books + magazines
end
end
When we call items, it now returns a single array containing all the items:
shelf.items
Output:
Slaughterhouse-Five by Kurt Vonnegut
Cat's Cradle by Kurt Vonnegut
Stranger in a Strange Land by Robert A. Heinlein
Dune by Frank Herbert
1984 by George Orwell
Asimov's Science Fiction
The Magazine of Fantasy & Science Fiction
Clarkesworld Magazine
Weird Tales
Analog Science Fiction and Fact
Defining iteration with each
At this point, Enumerable now has one logical list to work with, but we still cannot call methods directly on the class. The reason is that Enumerable does not know how to iterate over a Bookshelf. It only provides higher-level methods, but it depends on the object responding to each.
The next step is to define an each method inside the Bookshelf class which tells Ruby how to iterate over the specific entities within class.
def each
items.each do |item|
yield item
end
end
This method acts as a bridge between the Bookshelf and Enumerable:
items.eachlets the combined array handle the loopingyieldsends each item to the block passed toeach- Ruby now knows how to move through the object one item at a time
Simply put: Enumerable asks what to do, but each explains how to move.
The complete code should now look like this:
class Bookshelf
include Enumerable
def initialize(books:, magazines:)
@books = books
@magazines = magazines
end
def books
@books
end
def magazines
@magazines
end
def items
books + magazines
end
def each
items.each do |item|
yield item
end
end
end
shelf = Bookshelf.new(
books: [
"Slaughterhouse-Five by Kurt Vonnegut",
"Cat's Cradle by Kurt Vonnegut",
"Stranger in a Strange Land by Robert A. Heinlein",
"Dune by Frank Herbert",
"1984 by George Orwell"
],
magazines: [
"Asimov's Science Fiction",
"The Magazine of Fantasy & Science Fiction",
"Clarkesworld Magazine",
"Weird Tales",
"Analog Science Fiction and Fact"
]
)
Note: def each creates the instance method while the items.each are called on a regular array that is returned by the items method.
Now that we have defined each, we can use the methods that rely on iteration:
-
Iterate over all items in the shelf:
shelf.each do |item|
puts "#{item} is on the shelf"
endOutput:
Slaughterhouse-Five by Kurt Vonnegut is on the shelf
Cat's Cradle by Kurt Vonnegut is on the shelf
Stranger in a Strange Land by Robert A. Heinlein is on the shelf
Dune by Frank Herbert is on the shelf
1984 by George Orwell is on the shelf
Asimov's Science Fiction is on the shelf
The Magazine of Fantasy & Science Fiction is on the shelf
Clarkesworld Magazine is on the shelf
Weird Tales is on the shelf
Analog Science Fiction and Fact is on the shelf -
Sort all items alphabetically:
puts shelf.sortOutput:
1984 by George Orwell
Analog Science Fiction and Fact
Asimov's Science Fiction
Cat's Cradle by Kurt Vonnegut
Clarkesworld Magazine
Dune by Frank Herbert
Slaughterhouse-Five by Kurt Vonnegut
Stranger in a Strange Land by Robert A. Heinlein
The Magazine of Fantasy & Science Fiction
Weird TalesWe did not define any
sort, but it is actually using thesortmethod from theEnumerablemodule. We can also now use the other methods likeany?andmap. -
Check if any item matches a condition (e.g., longer than 12 characters):
p shelf.any? { |item| item.length > 12 }Output:
true -
Transform items with a block (e.g., convert all text to uppercase):
p shelf.map { |item| item.upcase }Output:
SLAUGHTERHOUSE-FIVE BY KURT VONNEGUT
CAT'S CRADLE BY KURT VONNEGUT
STRANGER IN A STRANGE LAND BY ROBERT A. HEINLEIN
DUNE BY FRANK HERBERT
1984 BY GEORGE ORWELL
ASIMOV'S SCIENCE FICTION
THE MAGAZINE OF FANTASY & SCIENCE FICTION
CLARKESWORLD MAGAZINE
WEIRD TALES
ANALOG SCIENCE FICTION AND FACT -
Select items that meet a condition (e.g., contain the letter "g"):
p shelf.select { |item| item.downcase.include?("g")}Output:
Slaughterhouse-Five by Kurt Vonnegut
Cat's Cradle by Kurt Vonnegut
Stranger in a Strange Land by Robert A. Heinlein
1984 by George Orwell
The Magazine of Fantasy & Science Fiction
Clarkesworld Magazine
Analog Science Fiction and Fact
We can further modify to class, let's say to iterate over books only, by updating the each method:
def each
books.each do |item|
yield item
end
end
Or to iterate over magazines only:
def each
magazines.each do |item|
yield item
end
end
Multiple Mixins
In this example, we have a RestApiHandler class for a backend server. It needs several independent features: authentication, logging, and response formatting.
Each feature is implemented as a separate module in its own file and mixed into the class.
-
auth.rb- This module handles authenticationmodule Auth
def authenticate(user)
if user[:token] == "valid_token"
@current_user = user[:name]
true
else
false
end
end
def current_user
@current_user
end
end -
logger.rb- This module tracks requestsmodule Logger
def log_request(endpoint)
puts "[LOG] User #{@current_user || 'Guest'} accessed #{endpoint}"
end
end -
response_formatter.rb- This module formats API responsesrequire 'json'
module ResponseFormatter
def json_response(data)
{ status: "success", data: data }.to_json
end
end
The next step is to create a class that brings together all three modules.. We first load each module using require_relative with the file names, and then include them inside the class with the include keyword:
## rest_api_handler.rb
require_relative 'auth.rb'
require_relative 'logger.rb'
require_relative 'response_formatter.rb'
class RestApiHandler
include Auth
include Logger
include ResponseFormatter
def get_user_profile(user)
if authenticate(user) # Auth
log_request("/profile") # Logger
json_response({ # ResponseFormatter
name: current_user,
role: "developer"
})
else
{
status: "Error",
message: "Unauthorized"
}.to_json
end
end
end
Note: There is no initialize method because the class does not need any setup when a new instance is created. Any state, like @current_user, is handled internally by the modules themselves.
Create a new instance and call the methods:
api_1 = RestApiHandler.new
# Valid user
user = {
name: "Alice",
token: "valid_token"
}
puts api_1.get_user_profile(user)
# Invalid user
unauthorized_user = {
name: "Bob",
token: "invalid"
}
puts api_1.get_user_profile(unauthorized_user)
Output:
[LOG] User Alice accessed /profile
{"status":"success","data":{"name":"Alice","role":"developer"}}
{"status":"Error","message":"Unauthorized"}