Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
190: The Method Resolution Order (Ancestors Chain)
A few years ago, I was mentoring a junior dev who was losing their mind over a bug in a Rails application. They had a User model with a save method, and they were certain the code they wrote was being executed. But in the logs, a weird auditing message was appearing before their logic, and occasionally, their logic wasn't running at all. They had searched the User class, the ApplicationRecord parent class, and the Ruby core docs. Everything looked clean. It turned out a third-party auditing gem had used Module#prepend to hook into the save process. The method was being intercepted before it ever reached the class itself.
This is where most Rubyists hit a wall. We're taught that Ruby looks at the class, then the superclass. But the reality is more like a detailed map—the Method Resolution Order (MRO). When you call a method, Ruby doesn't just jump to the parent; it traverses a very specific "ancestors chain." If you don't know how to read that chain, you're essentially debugging in the dark.
Tracing the Ancestors Chain
The easiest way to stop guessing is to ask Ruby exactly where it's looking. Every class has an .ancestors method that returns an array of all the modules and classes Ruby will check, in the exact order it checks them. I always tell my students: when in doubt, p YourClass.ancestors.
module Trackable
def log_action
puts "Action tracked!"
end
end
class BaseAccount
def identity
"Generic Account"
end
end
class SavingsAccount < BaseAccount
include Trackable
end
p SavingsAccount.ancestors
# Output: [SavingsAccount, Trackable, BaseAccount, Object, Kernel, BasicObject]
In this flow, Ruby starts at SavingsAccount. If the method isn't there, it moves to Trackable, then BaseAccount, and so on. This is the standard behavior for include. The module gets inserted immediately after the class that includes it, but before the superclass. It's a subtle distinction, but it's the difference between a bug and a feature.
The Power Shift of Prepend
Now, here is where things get interesting—and where my junior dev got tripped up. If include puts a module after the class, prepend puts it before the class. This allows a module to override methods defined directly in the class itself, while still being able to call super to trigger the original class logic.
module ValidationHook
def save
puts "Running validations first..."
super # This calls the next person in the ancestors chain
end
end
class User
prepend ValidationHook
def save
puts "Saving user to database..."
end
end
p User.ancestors
# Output: [ValidationHook, User, Object, Kernel, BasicObject]
User.new.save
# Output:
# Running validations first...
# Saving user to database...
Notice the change in the ancestors array. ValidationHook is now at the very front. When you call .save, Ruby hits the module first. Because we used super inside the module, Ruby continues down the chain and finds the save method inside the User class. If we had used include, the User class's own save method would have won, and the validation hook would have been ignored unless the User class explicitly called super.
I've found that mastering the ancestors chain is the "unlock" for understanding how most Ruby gems work. They aren't magic; they're just strategically placing themselves in your MRO to intercept calls.
📋 Practical Task
Exercise: Fixing the Payment Processor Ancestry
You are working on a payment system where a PaymentProcessor class handles transactions. A FraudCheck module was intended to intercept every payment to ensure it's safe before the processor handles it. However, the current implementation is ignoring the fraud check entirely.
Your Task: Modify the provided code so that the FraudCheck logic runs before the PaymentProcessor#process method, ensuring the "Checking for fraud..." message appears first in the console. You must do this by changing how the module is integrated into the class, without changing the method bodies themselves.
module FraudCheck
def process(amount)
puts "Checking for fraud..."
super(amount)
end
end
class PaymentProcessor
include FraudCheck # This is the line causing the issue
def process(amount)
puts "Processing payment of $#{amount}..."
end
end
processor = PaymentProcessor.new
processor.process(100)
# Current Output:
# Processing payment of $100...
# (The fraud check is missing!)
There are no comments for now.