Mixins
Overview
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 are ideal when different classes need the same feature but do not belong to the same hierarchy.
Modules used as mixins often end in able to indicate added behavior. For example:
Enumerableadds collection methodsComparableadds comparison methodsMonitorablecan add monitoring featuresSerializableadds ability to convert objects to a storable format.
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
Mixins: Comparable Module
The Comparable module allows objects to be compared easily. By including it in a class and defining the <=> (spaceship) operator, we get methods like <, <=, >, >=, ==, and between?.
The <=> method returns:
-1if the current object is less than the other0if both are equal1if the current object is greater
Here’s an example of using Comparable to compare books based on their number of pages:
class Book
include Comparable
attr_reader :title, :pages
def initialize(title:, pages:)
@title = title
@pages = pages
end
def <=>(other)
self.pages <=> other.pages
end
end
We can create some book instances and compare them easily:
book1 = Book.new(title: "Short Story", pages: 100)
book2 = Book.new(title: "Novel", pages: 300)
book3 = Book.new(title: "Epic", pages: 500)
puts book1 < book2
puts book3 > book2
puts book3.between?(book1, book2)
Output:
true
true
false
Example: Comparing Medals
In this example, we rank the sports medals in tournaments: bronze, gold, and silver. By including Comparable and defining <=>, we can compare the medals easily.
class GamesMedal
include Comparable
attr_reader :type
def initialize(type:)
@type = type
end
def <=>(other)
medal_values = {
gold: 3,
silver: 2,
bronze: 1
}
medal_values[self.type] <=> medal_values[other.type]
end
end
Here we create some medal instances:
medal_gold = GamesMedal.new(type: :gold)
medal_silver = GamesMedal.new(type: :silver)
medal_bronze = GamesMedal.new(type: :bronze)
Note: When creating the instances, we pass a symbol as a keyword argument.
- The
type:is a keyword argument expected byinitialize - The values
:gold,:silver, and:bronzeare symbols
Symbols are identifiers used to represent fixed values. In this example, the symbol stored in @type is used as a key to look up the associated numeric value in the medal_values hash inside the <=> method.
For more information on Symbools, please Symbols as Hash Keys and Hash Values.
Now we can compare them using all the Comparable methods:
puts medal_silver < medal_gold
puts medal_bronze > medal_gold
puts medal_gold.between?(medal_silver, medal_bronze)
Output:s
true
false
false
What happens under the hood: Because Comparable is included, Ruby rewrites the first two puts lines to use the <=> method:
puts medal_silver.<=>(medal_gold) < 0
puts medal_bronze.<=>(medal_gold) < 0
This calls the <=> method, performs the symbol lookup to get the corresponding numeric values, and then compares those numbers:
2 <=> 3
1 <=> 3
For the third comparison, between? is rewritten internally as:
puts medal_gold >= medal_silver && medal_gold <= medal_bronze
This results in the following checks:
-
First check:
medal_gold >= medal_silver
3 <=> 2 # Returns 1
true -
Second check:
medal_gold <= medal_bronze
3 <=> 1 # Returns 1
false -
Because both conditions must be true:
medal_gold <= medal_bronze
true && falseOutput:
false
All comparisons ultimately rely on <=>, where symbol values are mapped to numbers and compared, and the results are then used by Comparable to implement <, >, and between?.
Method Lookup and ancestors
When a module is mixed into a class, Ruby searches for methods in a set order:
- First in the class itself
- Then in any included modules
- Finally up the superclass chain
This determines which method will run if the same method exists in multiple places.
Updating the the Orderable module from the previous section:
- The
Cafeclass overrides theordermethod from the module, so its own version is used. FoodTruckandJuiceStanddon’t define their own method, so they use the one fromOrderable.
module Orderable
def order(item)
"You ordered #{item}"
end
end
class Cafe
include Orderable
def order(item)
"You got a #{item} at the cafe"
end
end
class FoodTruck
include Orderable
end
class JuiceStand < FoodTruck
end
cafe_1 = Cafe.new
food_truck_1 = FoodTruck.new
juice_stand_1= JuiceStand.new
puts cafe_1.order("Latte")
puts food_truck_1.order("Sandwich")
puts juice_stand_1.order("Smoothie")
Output:
You got a Latte at the cafe
You ordered Sandwich
You ordered Smoothie
We can use the ancestors method to check the inheritance and mixin hierarchy:
p Cafe.ancestors
p FoodTruck.ancestors
p JuiceStand.ancestors
Output:
[Cafe, Orderable, Object, Kernel, BasicObject]
[FoodTruck, Orderable, Object, Kernel, BasicObject]
[JuiceStand, FoodTruck, Orderable, Object, Kernel, BasicObject]
The ancestors array shows the order Ruby searches for methods: first the class, then any modules included in the class or its superclasses, and finally the built-in superclasses. This explains why the order method in Cafe was used instead of the module’s.
For more information on ancestors, please see Inheritance.
You can also check if an object includes a module or inherits from a class with is_a?:
puts cafe_1.is_a?(Cafe) # Output: true
puts cafe_1.is_a?(Orderable) # Output: true
puts cafe_1.is_a?(Object) # Output: true
Mixins appear in the ancestor chain and Ruby can find their methods just like it does for superclasses.
Using prepend and extend
There are different ways to mix a module’s methods into a class:
prependmakes the module’s methods run before the class’s methodsextendadds module methods as class-level methods
Using prepend
In the Orderable example below, we are using prepend to override instance methods in the Cafe class:
module Orderable
def order(item)
"You ordered #{item}"
end
end
class Cafe
prepend Orderable
def order(item)
"You got a #{item} at the cafe"
end
end
cafe = Cafe.new
puts cafe.order("Latte")
Output:
You ordered Latte
Here, Orderable comes before Cafe in the method lookup chain, so its order method is called instead of the one defined in Cafe. You can check the lookup order with ancestors:
p Cafe.ancestors
Output:
[Orderable, Cafe, Object, Kernel, BasicObject]
If we comment out the prepend line:
class Cafe
# prepend Orderable
....
And check the method lookup again, Ruby now prioritizes the class’s own methods first. That’s why Orderable no longer appears in the ancestors array:
p Cafe.ancestors
Output:
[Cafe, Object, Kernel, BasicObject]
Using extend
With extend, module methods are added as class methods, not instance methods. This means you call these methods on the class, not on objects created from that class.
In the example below, we are calling the track method on the Cafe and FoodTruck classes, not on the cafe_1 instance.
module Trackable
def track
"Tracking activity for #{self}"
end
end
class Cafe
extend Trackable
end
class FoodTruck
extend Trackable
end
cafe_1 = Cafe.new
# Calls `track` on the classes
puts Cafe.track
puts FoodTruck.track
Output:
Tracking activity for Cafe
Tracking activity for FoodTruck
If we try to call the track method on the instance, it will return an error because track is not an instance method. It doesn’t exist on objects (like cafe_1) created from Cafe.
puts cafe_1.track
Output:
undefined local variable or method 'cafe_1' for main (NameError)
extendUse extend when you want a module’s methods to be called on the class itself. For example, to give different classes a shared way to report their configuration without creating an instance.
Method conflicts when mixing modules
You can mix multiple modules into the same class, but if they define methods with the same name, Ruby will use the method from the last module included. The order of include determines which method is called.
In this example, both the Breakfast and Lunch modules define a serve method. When both are included in the Cafe class, the Lunch module is added last, so its method takes priority.
module Breakfast
def serve(item)
"Serving #{item} for breakfast"
end
def drink
"Serving coffee"
end
end
module Lunch
def serve(item)
"Serving #{item} for lunch"
end
end
class Cafe
include Breakfast
include Lunch
end
cafe = Cafe.new
puts cafe.serve("Pancakes")
Output:
Serving Pancakes for lunch
If we reverse the inclusion order in the Cafe class, the Breakfast module is included last, so its serve method wins:
class Cafe
include Lunch
include Breakfast
end
cafe = Cafe.new
puts cafe.serve("Pancakes")
Output:
Serving Pancakes for breakfast
Methods that don’t conflict are still available, but for conflicting methods, Ruby prioritizes the last included module.
Example: 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"}