I still remember the exact moment Ruby’s philosophy clicked for me. I typed 3.times { puts "hi" } into an irb session, half expecting an error, because in every other language I’d used up to that point, 3 was just a dumb number — not something you could call methods on. But it worked. 3 was an object. It had methods. That’s when I understood the phrase people kept repeating: “In Ruby, everything is an object.”
In this article, I want to dig into what that actually means in practice, how methods work under the hood, and how inheritance and modules let you build flexible, reusable object hierarchies. This isn’t just theory — I’ll walk through real code, real output, and the mistakes I made before this stuff felt natural.
Everything Really Is an Object
In many languages, there’s a hard split between “primitive types” (integers, booleans) and “objects” (things with methods). Ruby refuses to make that split. Numbers, strings, true, false, nil, arrays, even classes themselves — all objects, all instances of some class, all capable of responding to methods.
puts 5.class
puts true.class
puts nil.class
puts "hello".class
puts [1, 2, 3].class
puts Integer.class
Output:
Integer
TrueClass
NilClass
String
Array
Class
Notice that last line: Integer.class returns Class. Even a class is an object — an instance of the class Class. This is what people mean when they say Ruby’s object model goes “all the way down.” There’s no escape hatch to some non-object primitive layer.
This matters practically, not just philosophically. Because everything is an object, everything can have methods called on it, be passed around, be reopened and extended, and participate fully in the object-oriented features I’ll cover below.
puts 5.even?
puts (-5).abs
puts nil.to_a.inspect
puts "ruby".upcase
Output:
false
5
[]
RUBY
What Exactly Is a Method?
A method is a named, reusable block of behavior attached to a class. When you call obj.method_name, you’re sending a message to obj, asking it to execute the behavior defined by method_name in its class (or an ancestor of its class).
class Greeter
def hello(name)
"Hello, #{name}!"
end
end
g = Greeter.new
puts g.hello("World")
Output:
Hello, World!
Method Visibility: Public, Private, and Protected
Ruby lets you control which methods can be called from outside an object, using three visibility levels.
class Account
def initialize(balance)
@balance = balance
end
def display_balance
"Balance: #{formatted_balance}"
end
private
def formatted_balance
"$#{@balance}"
end
end
acc = Account.new(500)
puts acc.display_balance
puts acc.formatted_balance
Output:
Balance: $500
The second call raises a NoMethodError because formatted_balance is private — it can only be called from within the object itself, without an explicit receiver. I use private for internal helper methods that support public behavior but shouldn’t be part of the object’s external contract. protected is similar but allows calling the method on other instances of the same class, which is useful for comparison methods:
class Money
def initialize(amount)
@amount = amount
end
def >(other)
amount > other.amount
end
protected
attr_reader :amount
end
puts Money.new(100) > Money.new(50)
Output:
true
Class Methods vs Instance Methods
Instance methods operate on individual objects. Class methods operate on the class itself and are defined with self. or inside class << self.
class Product
@@catalog = []
def initialize(name, price)
@name = name
@price = price
@@catalog << self
end
def self.total_products
@@catalog.size
end
def self.most_expensive
@@catalog.max_by { |p| p.instance_variable_get(:@price) }
end
end
Product.new("Laptop", 1200)
Product.new("Mouse", 25)
puts Product.total_products
Output:
2
I reach for class methods when the behavior conceptually belongs to the class as a whole rather than to a single instance — things like factory methods, counters, or finder methods.
How Method Lookup Actually Works
This is where Ruby’s object-oriented design becomes genuinely elegant. When you call a method on an object, Ruby doesn’t search the object — it searches the object’s ancestor chain, a linear sequence of classes and modules that Ruby walks through in order until it finds a matching method.
class Animal
def speak
"Some generic sound"
end
end
class Dog < Animal
end
puts Dog.ancestors.inspect
puts Dog.new.speak
Output:
[Dog, Animal, Object, Kernel, BasicObject]
Some generic sound
Dog doesn’t define speak, so Ruby walks up the chain to Animal, finds it there, and executes it. This lookup process is deterministic and inspectable, which makes debugging “where did this method come from?” questions much easier than in languages with less transparent dispatch mechanisms.
Inheritance: Building on What Already Exists
Inheritance lets you define a general class and then create more specific classes that automatically get all its behavior, while being free to override or extend it.
class Animal
attr_reader :name
def initialize(name)
@name = name
end
def speak
"#{name} makes a sound"
end
end
class Dog < Animal
def speak
"#{name} barks: Woof!"
end
end
class Cat < Animal
def speak
"#{name} meows: Meow!"
end
end
[Dog.new("Rex"), Cat.new("Whiskers")].each do |animal|
puts animal.speak
end
Output:
Rex barks: Woof!
Whiskers meows: Meow!
This is polymorphism — different classes responding to the same method call (speak) in ways appropriate to their own type. The calling code doesn’t need to know or care whether it’s dealing with a Dog or a Cat; it just trusts that speak will do the right thing.
Calling the Parent’s Implementation with super
Sometimes you don’t want to fully replace a parent method — you want to extend it. super calls the same-named method in the superclass.
class Animal
def initialize(name)
@name = name
end
end
class Dog < Animal
def initialize(name, breed)
super(name)
@breed = breed
end
def info
"#{@name} is a #{@breed}"
end
end
puts Dog.new("Rex", "Labrador").info
Output:
Rex is a Labrador
Calling super without parentheses (just super) automatically forwards all the arguments the current method received. Calling super() with empty parentheses calls the parent method with no arguments at all. This distinction has bitten me more than once, so I always write it explicitly when I mean “no arguments.”
Modules and Mixins: Ruby’s Answer to Multiple Inheritance
Ruby classes can only inherit from one superclass — but Ruby gives you a powerful alternative for sharing behavior across unrelated classes: modules, mixed in using include or extend.
module Swimmable
def swim
"#{name} is swimming"
end
end
module Flyable
def fly
"#{name} is flying"
end
end
class Duck
include Swimmable
include Flyable
attr_reader :name
def initialize(name)
@name = name
end
end
duck = Duck.new("Donald")
puts duck.swim
puts duck.fly
Output:
Donald is swimming
Donald is flying
Duck isn’t a subclass of Swimmable or Flyable — it simply mixes their behavior in. This is how Ruby avoids the complexity of true multiple inheritance while still letting you compose behavior from multiple sources. When you include a module, it gets inserted into the ancestor chain right above the class that included it:
puts Duck.ancestors.inspect
Output:
[Duck, Flyable, Swimmable, Object, Kernel, BasicObject]
Notice modules are included in reverse order of declaration — the most recently included module sits closest to the class in the lookup chain. This matters when two mixed-in modules define the same method name, since it determines which one wins.
extend vs include
While include adds module methods as instance methods, extend adds them as methods on the object (or class) itself.
module Describable
def describe
"I am #{self}"
end
end
class Robot
extend Describable
end
puts Robot.describe
Output:
I am Robot
I use include constantly for shared instance behavior (comparable logic, enumerable behavior, formatting helpers) and extend less often, typically for adding class-level utility methods to a class.
Abstract Base Classes and Duck Typing
Ruby doesn’t have formal abstract class or interface keywords like Java or C#. Instead, Ruby relies on a philosophy called duck typing: “If it walks like a duck and quacks like a duck, treat it like a duck.” What matters is whether an object responds to the methods you need, not what class it officially belongs to.
class PDFExporter
def export
"Exporting as PDF"
end
end
class CSVExporter
def export
"Exporting as CSV"
end
end
def run_export(exporter)
puts exporter.export
end
run_export(PDFExporter.new)
run_export(CSVExporter.new)
Output:
Exporting as PDF
Exporting as CSV
run_export doesn’t care what class it receives — only that the object responds to .export. You can enforce this loosely with respond_to?:
def run_export(exporter)
raise ArgumentError, "must respond to #export" unless exporter.respond_to?(:export)
puts exporter.export
end
If you do want something closer to a formal abstract class, a common convention is to raise NotImplementedError in the base class:
class Exporter
def export
raise NotImplementedError, "#{self.class} must implement export"
end
end
class JSONExporter < Exporter
def export
"Exporting as JSON"
end
end
puts JSONExporter.new.export
Exporter.new.export
Output:
Exporting as JSON
followed by a raised NotImplementedError on the last line, since Exporter itself never provides a real implementation.
Internal Working: Message Passing and send
Under the hood, calling object.method_name(args) is really Ruby sending a message called method_name with args to object. You can do this explicitly with send, which even bypasses private method restrictions — useful for testing or metaprogramming, dangerous if overused.
class Wallet
def initialize(amount)
@amount = amount
end
private
def secret_amount
@amount
end
end
w = Wallet.new(200)
puts w.send(:secret_amount)
Output:
200
This message-passing model is also why method_missing works the way it does — when Ruby can’t find a matching method anywhere in the ancestor chain, it sends a method_missing message instead of immediately failing, giving you a hook to intercept undefined calls dynamically (this is how many Ruby DSLs and ORMs, including parts of Rails, implement dynamic attribute access).
class DynamicResponder
def method_missing(name, *args)
"You called #{name} with #{args.inspect}"
end
end
puts DynamicResponder.new.anything_goes(1, 2, 3)
Output:
You called anything_goes with [1, 2, 3]
Performance Considerations
Method lookup in Ruby involves walking the ancestor chain, which sounds slow but is heavily optimized internally through method caching (Ruby’s MRI implementation caches lookups so repeated calls to the same method on the same class don’t re-walk the chain every time). Still, a few practical performance notes I’ve picked up:
- Deep inheritance hierarchies and heavy module chains add lookup overhead; keep hierarchies as shallow as reasonably possible.
method_missingis convenient but slower than defined methods, since it’s only reached after the full ancestor chain lookup fails. Usedefine_methodto generate real methods dynamically when performance matters.sendandpublic_sendhave a small overhead compared to direct calls; avoid them in hot loops.
class Config
[:host, :port, :timeout].each do |attr|
define_method(attr) { instance_variable_get("@#{attr}") }
end
def initialize
@host = "localhost"
@port = 8080
@timeout = 30
end
end
c = Config.new
puts c.host
puts c.port
Output:
localhost
8080
This define_method pattern generates real, cacheable methods instead of relying on method_missing, giving you dynamic behavior without the performance penalty.
Real-World Application: A Notification System
Here’s a practical example pulling together inheritance, modules, and duck typing the way you’d actually structure this in a production Ruby application.
module Loggable
def log(message)
puts "[LOG] #{Time.now.strftime('%H:%M:%S')} - #{message}"
end
end
class Notifier
include Loggable
def send_notification(message)
raise NotImplementedError
end
def notify(message)
send_notification(message)
log("Notification sent via #{self.class}")
end
end
class EmailNotifier < Notifier
def send_notification(message)
puts "Emailing: #{message}"
end
end
class SMSNotifier < Notifier
def send_notification(message)
puts "Texting: #{message}"
end
end
notifiers = [EmailNotifier.new, SMSNotifier.new]
notifiers.each { |n| n.notify("Server restarted successfully") }
Output:
Emailing: Server restarted successfully
[LOG] 14:02:31 - Notification sent via EmailNotifier
Texting: Server restarted successfully
[LOG] 14:02:31 - Notification sent via SMSNotifier
This is a template method pattern: the base class defines the overall workflow (notify), while subclasses fill in the specific step (send_notification). It’s a pattern I use constantly in real Ruby codebases, and it only works because of the inheritance and polymorphism fundamentals covered above.
Best Practices and Common Mistakes
- Favor composition (modules) over deep inheritance when behavior doesn’t represent a true “is-a” relationship. A
Caris aVehicle(inheritance); aCarisTrackable(mixin). - Don’t overuse
method_missing. It’s powerful but makes code harder to trace and debug; preferdefine_methodfor dynamic-but-known method sets. - Be explicit with
supervssuper()— forgetting the difference is a classic source of “why is this argument nil?” bugs. - Avoid deep module chains that shadow each other’s methods unexpectedly; always check
SomeClass.ancestorswhen you’re unsure which implementation wins. - Don’t reach for
sendto bypass private methods as a routine practice — if you need to call something from outside the object often, it probably shouldn’t be private.
Summary
Ruby earns its reputation as a purely object-oriented language because there is genuinely no escape hatch from the object model — numbers, strings, booleans, and even classes are all objects that respond to methods. Method calls are message sends resolved through an inspectable, predictable ancestor chain, which is what makes inheritance, super, and mixins behave consistently. Modules give you a clean way to share behavior across unrelated classes without the complexity of true multiple inheritance, and duck typing lets you write flexible code that cares about behavior rather than rigid type hierarchies. Once these pieces click together, you start writing Ruby that feels less like following syntax rules and more like designing a small society of cooperating objects.