Ruby's object model forms the foundation of how the language organizes classes, modules, and method resolution. Understanding this system reveals why Ruby behaves the way it does and enables you to write more effective object-oriented code. At its core, Ruby follows a simple principle: everything is an object, and every object has a class.
The method lookup chain determines how Ruby finds and executes methods when you call them on objects. This process involves traversing a specific path through classes, modules, and inheritance hierarchies. When you call a method, Ruby doesn't just look in the obvious place - it follows a well-defined sequence that includes the object's singleton methods, included modules, and ancestor classes.
This systematic approach to method resolution enables Ruby's flexible features like mixins, method overriding, and dynamic method definition. The lookup process happens transparently, but understanding it helps you predict behavior, debug issues, and leverage Ruby's metaprogramming capabilities effectively.
In this article, we’ll explore Ruby’s class hierarchy and how it affects method lookup, and the process of resolving method calls.
Let's dive in!
Prerequisites
You'll need Ruby 2.7 or later installed. The object model examples work consistently across Ruby versions, though newer versions provide better introspection tools:
Basic familiarity with Ruby classes and modules will help you follow the examples, though we'll build concepts from the ground up:
Setting up the environment
To effectively demonstrate Ruby's object model, you'll create a practical example that reveals how method lookup works in real scenarios. This exploration will show you exactly how Ruby resolves method calls and how different language features interact within the object model.
The key insight we'll uncover is that Ruby's apparent simplicity masks a sophisticated system for organizing behavior and data. By starting with concrete examples and then examining Ruby's internal representations, you'll develop an intuitive understanding of how the object model operates.
Create your project directory:
Create a foundation that demonstrates basic object relationships:
This foundation establishes a simple inheritance hierarchy that demonstrates fundamental object model concepts. The AdminUser class inherits behavior from User while defining specialized methods for administrative functionality.
When you call admin.authenticate, Ruby finds the method in AdminUser, which uses super to call the parent method. When you call admin.manage_users, Ruby finds it directly in AdminUser. This automatic traversal forms the basis of Ruby's method lookup system.
Run this to see the basic relationships:
Notice how admin.authenticate combines both the AdminUser and User versions through super, while admin.manage_users calls the method defined only in AdminUser. This demonstrates Ruby's method lookup process in action.
Understanding the method lookup chain
Now that you've seen basic inheritance in action, you'll explore Ruby's complete method lookup process. This system determines exactly how Ruby finds methods when you call them, involving more than just simple inheritance. The lookup chain includes singleton methods, included modules, and the full inheritance hierarchy, creating a sophisticated resolution system.
Ruby's method lookup follows a specific order that prioritizes more specific definitions over general ones. This means singleton methods (methods defined on individual objects) take precedence over class methods, which take precedence over inherited methods. Understanding this order helps you predict method behavior and design effective class hierarchies.
The lookup process happens automatically and efficiently, but you can examine it using Ruby's introspection capabilities. The ancestors method reveals the exact order Ruby follows when searching for methods, while other tools let you examine where specific methods are defined.
Extend your basic example to demonstrate the complete lookup process:
The ancestors method reveals the exact order Ruby follows during method lookup. When you include the Cacheable module, Ruby inserts it between CachedApiClient and ApiClient, allowing the module to enhance inherited behavior using super.
Run the enhanced version to see the complete lookup chains:
The AdminUser.ancestors result reveals that Ruby automatically includes Kernel in every class's lookup chain, providing core methods like puts and require.
The CachedApiClient.ancestors chain shows how Cacheable appears between CachedApiClient and ApiClient. When client.request executes, Ruby finds the method in Cacheable first, which uses super to call the original method in ApiClient.
Exploring singleton methods and eigenclasses
While class-level method definitions affect all instances of a class, Ruby also allows you to define methods on individual objects. These singleton methods exist only on the specific object where they're defined, creating unique behavior without affecting other instances of the same class. This capability enables Ruby's flexible metaprogramming features and explains how class methods actually work.
Behind the scenes, Ruby implements singleton methods using eigenclasses (also called singleton classes). Each object can have an eigenclass that sits at the very beginning of its method lookup chain, before the object's actual class. When you define a singleton method, Ruby creates this eigenclass if it doesn't exist and stores the method there.
This mechanism explains many Ruby features that might seem mysterious at first. Class methods are actually singleton methods defined on class objects. The self keyword in class definitions refers to the class object itself, and defining methods with def self.method_name creates singleton methods on that class object.
Create a new file to explore singleton methods and eigenclasses:
This example shows how singleton methods create unique behavior on individual objects. Both alice and bob are User instances, but only alice has the specialized methods stored in her eigenclass.
Run this to see singleton methods in action:
Now extend the example to show how class methods work through eigenclasses:
Class methods are singleton methods defined on the class object itself. The different syntaxes achieve the same result - creating methods that exist on the class rather than instances.
Run the complete example:
The DatabaseConnection.singleton_methods output confirms that class methods are singleton methods on the class object. Individual instances don't have these methods - they exist only on the class object itself.
Module inclusion and method lookup precedence
Modules provide Ruby's mechanism for sharing code between classes without using inheritance. When you include a module in a class, Ruby inserts it into the method lookup chain, creating a form of multiple inheritance. Understanding how modules integrate into lookup chains helps you design effective mixin strategies and predict method resolution behavior.
The position where modules appear in the lookup chain depends on how they're incorporated. The include keyword places modules between the class and its superclass, while prepend places them before the class itself in the lookup chain. This positioning difference has important implications for method overriding and super calls.
Multiple modules included in the same class create a stack-like structure where the most recently included module appears first in the lookup chain. This ordering affects which version of a method gets called when multiple modules define methods with the same name.
Create a new file to explore module inclusion patterns:
Multiple modules create a stack where the most recently included appears first in lookup. Each module's process method enhances the previous one using super.
Run this to see the module stacking behavior:
Now add an example showing the difference between include and prepend:
With include, the module appears after the class, so the class method runs first. With prepend, the module appears before the class and can wrap the original method.
Run the complete example:
The key difference is positioning: Auditable appears after IncludedDocument but before PrependedDocument. This determines which method gets called first and enables different enhancement patterns.
Introspecting the object model
Ruby provides extensive introspection capabilities that let you examine the object model at runtime. These tools help you understand method resolution, debug issues, and build metaprogramming solutions that work with Ruby's object system rather than against it. Learning to use these introspection methods effectively makes you a more capable Ruby developer.
The key introspection methods reveal different aspects of the object model. Methods like method, source_location, and owner help you trace where methods are defined and how they're resolved. Others like respond_to? and methods help you understand what an object can do without necessarily calling methods on it.
These capabilities become particularly valuable when working with complex inheritance hierarchies, dynamic method definition, or debugging method resolution issues. They provide a window into Ruby's internal decision-making process during method lookup.
Create a comprehensive introspection example:
This demonstrates where Ruby finds each method during lookup, revealing the actual source of method definitions in complex hierarchies.
Run this to see detailed method information:
Now add methods for exploring object capabilities:
Ruby's introspection tools help you understand method resolution and debug complex inheritance scenarios by showing exactly where methods are defined and what an object can do.
Run the complete introspection example:
The results reveal exactly where methods are defined and what capabilities an object has, making introspection essential for debugging complex method resolution scenarios.
Final thoughts
Ruby's object model creates a sophisticated yet predictable system for organizing behavior and resolving method calls. The method lookup chain follows a clear order: singleton methods, included modules (in reverse order of inclusion), the class itself, then up the inheritance hierarchy. Understanding this sequence helps you predict method resolution and design effective class hierarchies.
Ruby's object model balances power with predictability. While the system supports sophisticated metaprogramming patterns, it maintains consistent rules that make behavior understandable. Explore the Ruby Object documentation and experiment with the introspection methods to deepen your understanding of how Ruby organizes and resolves object behavior.