Classes and Objects
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18). Ruby is an object-oriented language in the
strictest sense: every value is an object, every object is an instance of a class, and a class is itself an object (an
instance of Class). This page covers how classes are defined and instantiated, what new really does, how instance
state is stored and exposed, how objects print, what reopening and inheriting a class mean, how to ask an object what it
is, the three roots of the hierarchy (BasicObject, Object and Kernel), and how to design small, collaborating classes
instead of large ones.
It builds on Methods and Parameters (every method in a class
body is defined with def), Variables, Constants and Scope
(instance variables, class variables and constants) and
Exceptions and Error Handling. The pages that follow
go deeper on each topic that is only introduced here:
Modules and Mixins,
Singleton Classes and Class-Level Behavior,
Method Lookup and super,
Visibility and Encapsulation,
Refinements and Monkey Patching,
Equality, Hashing and Comparison,
Object Lifecycle, Freezing and Copying and
Struct, Data and Value Objects. For how other languages
model the same ideas, see Java,
Python and Kotlin.
Every example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using
the configuration of the running shelf gem (double-quoted strings, NewCops: enable, Style/Documentation off, and
Style/OneClassPerFile off because several small classes share one example file). The output shown is the real output;
memory addresses such as 0x000000012035b6e8 differ from run to run. The examples that need the gem are run with
ruby -Ilib, so require "shelf" finds it (see the shelf gem source).
Objects and classes in one page
| Term | Meaning |
|---|---|
Object |
A value with an identity, some state (instance variables) and behaviour (methods found through its class). |
Class |
A blueprint and a factory: the object that holds the instance methods shared by its instances and that creates them with |
Instance |
An object created by a class. Its class is available as |
Instance variable |
A value stored in one object, named with a leading |
Method |
Code stored in a class (or module) and run with a receiver. |
Constant |
The name a class is stored under ( |
Superclass |
The class a class inherits from. Every class has exactly one, except |
Singleton class |
A hidden per-object class that holds methods defined for that single object. Class methods live in the singleton class of the class. See Singleton Classes and Class-Level Behavior. |
The Ruby reference says that the keyword defines a class "in the current scope", and that the name must be a constant:
conventionally CamelCase, with each word capitalised and no underscores (LibraryCard, not library_card). A class
body is ordinary executable code: it runs top to bottom when Ruby reaches it, with self set to the class, and the
value of the whole class ... end expression is the value of the last expression in the body (like a method).
# frozen_string_literal: true
require "shelf"
# A class is an object, created by the class keyword and stored in a constant.
class Member
attr_reader :name, :loans
def initialize(name)
@name = name
@loans = []
end
def borrow(book)
@loans << book
self
end
def to_s = "#{name} (#{loans.size} loans)"
end
author = Shelf::Author.new("Ursula K. Le Guin")
isbn = Shelf::Isbn.new("978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "The Dispossessed", author:)
ada = Member.new("Ada").borrow(book)
puts ada
p ada.class
p Member.class
p Member.name
p ada.instance_variables
p ada.is_a?(Member)
# A class definition is an expression: its value is the last expression.
result =
class Probe
42
end
p result
Ada (1 loans)
Member
Class
"Member"
[:@name, :@loans]
true
42
Source: Modules and Classes: Classes
Member.class is Class, and Member.name is the string "Member": a class knows the constant it was first assigned
to (see Anonymous classes and Class.new). ada.instance_variables lists the two variables the constructor
assigned, in the order they were first assigned.
The rest of the page is organised around the life of an object: how a class is shaped (reading and writing state, printing), how objects are created, and how classes relate to each other.
Creating objects
new, allocate and initialize
Member.new("Ada") is not syntax: new is an ordinary public method of Class (the instance method Class#new). It
does two things, which can be written by hand:
-
allocateasks the class for a fresh, empty object: right class, no instance variables, noinitializecall. -
It calls the private instance method
initializeon that object, passing on every argument, keyword and block it was given. The valueinitializereturns is ignored:newalways returns the new object.
Because initialize is private, it cannot be called from outside (Member.new("Ada").initialize("Bob") is a
NoMethodError); it runs only through new (or send, as the helper below does). BasicObject defines an empty
initialize that accepts no arguments, which is why Object.new(1) raises ArgumentError. A class without its own
initialize therefore takes no arguments at all.
# frozen_string_literal: true
class Member
attr_reader :name
def initialize(name)
@name = name
@name.length # the value of initialize is discarded
end
end
# Class#new is allocate followed by a call to the private initialize method.
def build(klass, ...)
object = klass.allocate
object.__send__(:initialize, ...)
object
end
p Member.allocate
p build(Member, "Ada")
p Member.new("Grace")
p Member.method(:new).owner
p Member.instance_method(:initialize).owner
p Member.private_instance_methods(false)
begin
Member.new("Ada").initialize("Bob")
rescue NoMethodError => e
puts e.message
end
begin
Member.new
rescue ArgumentError => e
puts e.message
end
begin
Integer.new
rescue NoMethodError => e
puts e.message
end
begin
Integer.allocate
rescue TypeError => e
puts e.message
end
#<Member:0x000000012467b8b0>
#<Member:0x000000012467b6d0 @name="Ada">
#<Member:0x000000012467b590 @name="Grace">
Class
Member
[:initialize]
private method 'initialize' called for an instance of Member
wrong number of arguments (given 0, expected 1)
undefined method 'new' for class Integer
allocator undefined for Integer
Source: Class#new; Class#allocate
Member.allocate shows an object with no @name: initialize never ran. Reading an instance variable that was never
assigned returns nil (see Instance variables). Two more details of this output are worth knowing:
-
Member.private_instance_methods(false)lists[:initialize]: Ruby makes a method namedinitialize(andinitialize_copy,initialize_clone,initialize_dupandrespond_to_missing?) private automatically, whatever its position relative to aprivatecall. -
The core classes that cannot have instances created the normal way undefine
newon their singleton (Integer.newisundefined method 'new' for class Integer) andallocateraisesTypeError(allocator undefined for Integer). Integers,Symbol,nil,trueandfalseexist only as literals or results of operations.Class.allocate.new(an uninitialised class) raisesTypeError: can't instantiate uninitialized class.
The wrong-arity message wrong number of arguments (given 0, expected 1) is ArgumentError, raised before the body of
initialize runs. For the parameter kinds initialize can declare (required, optional, keyword, rest, block) see
Methods and Parameters; the constructor is a method like any
other.
Constructors without I/O
A constructor should leave the object ready to use and nothing else: assign instance variables, validate the arguments,
possibly freeze. It should not read files, open sockets, query a database or print. Three reasons make this a rule
rather than a taste:
-
Testability. A constructor that does only in-memory work can be called in a test with plain values.
-
Failure.
newhas no way to say "the file was missing" except by raising from the constructor, and the half-built object is then unreachable. A named class method that does the I/O can choose its error, retry, or returnnil. -
Clarity.
Settings.newreads as "make an object from these values";Settings.load(path)says that a file is read.
The idiom is a class method that performs the effect and ends by calling new. Keyword parameters with defaults let the
file be sparse, and freeze at the end of initialize makes the object immutable (see
Object Lifecycle, Freezing and Copying).
# frozen_string_literal: true
require "tmpdir"
# The constructor only stores values. Anything that touches the outside world
# is a named class method that ends by calling new.
class Settings
attr_reader :loan_days, :currency
def self.parse(text)
pairs = text.each_line.filter_map { |line| line.chomp.split("=", 2) if line.include?("=") }
new(**pairs.to_h { |key, value| [key.to_sym, value] })
end
def self.load(path) = parse(File.read(path))
def initialize(loan_days: "14", currency: "EUR")
@loan_days = Integer(loan_days)
@currency = currency
freeze
end
end
Dir.mktmpdir do |dir|
path = File.join(dir, "shelf.conf")
File.write(path, "loan_days=21\ncurrency=USD\n")
settings = Settings.load(path)
p settings
p settings.frozen?
p Settings.new
end
# A private new plus named factories closes every other way in.
class LoanPolicy
attr_reader :days, :renewals
def self.standard = new(14, 2)
def self.strict = new(7, 0)
private_class_method :new
def initialize(days, renewals)
@days = days
@renewals = renewals
end
end
p LoanPolicy.strict
begin
LoanPolicy.new(1, 1)
rescue NoMethodError => e
puts e.message
end
#<Settings:0x00000001055eff50 @loan_days=21, @currency="USD">
true
#<Settings:0x00000001055e96c8 @loan_days=14, @currency="EUR">
#<LoanPolicy:0x0000000124bcecb8 @days=7, @renewals=0>
private method 'new' called for class LoanPolicy
Settings.parse is pure and Settings.load is the only method that touches the file system, so a test can exercise
parse with a string. LoanPolicy goes one step further: private_class_method :new removes the public constructor, so
the only ways to obtain a policy are the named factories standard and strict. A private new can still be called
from inside the class, with an implicit receiver, which is how the factories use it. The same technique is the basis of
the singleton pattern (see Singleton Classes and Class-Level Behavior).
Two cautions apply to constructors. First, new is a normal method, so it can be overridden (def self.new(...)) to
return an instance of a different class or a cached object, but that surprises readers: prefer a named factory. Second,
a subclass that defines initialize without calling super skips the parent’s setup; see Inheritance and super.
Instance variables
An instance variable is a name starting with a single @. It belongs to one object, is created by its first assignment,
and is visible only inside that object’s methods. There is no declaration, and two instances of the same class may carry
different sets of variables.
# frozen_string_literal: true
class Member
def initialize(name)
@name = name
end
def nickname? = !@nickname.nil?
end
ada = Member.new("Ada")
p ada.instance_variable_get(:@nickname)
p ada.nickname?
p ada.instance_variables
p ada.instance_variable_get(:@name)
ada.instance_variable_set(:@nickname, "Countess")
p ada.instance_variables
p ada.instance_variable_defined?(:@email)
p ada.remove_instance_variable(:@nickname)
p ada.instance_variables
begin
ada.instance_variable_get("name")
rescue NameError => e
puts e.message
end
ada.freeze
begin
ada.instance_variable_set(:@name, "Bob")
rescue FrozenError => e
puts e.message
p e.receiver.equal?(ada)
end
nil
false
[:@name]
"Ada"
[:@name, :@nickname]
false
"Countess"
[:@name]
'name' is not allowed as an instance variable name
can't modify frozen Member: #<Member:0x000000012462bb58 @name="Ada">
true
The facts to remember:
-
Unset reads are
nil. Reading@nicknamebefore assigning it returnsnil. Before Ruby 3.0 this printed the warninginstance variable @nickname not initializedunder-w; the warning was removed in 3.0, so a typo in an instance variable name now fails silently (anilappears later). RuboCop and the tests have to catch it. -
The reflection methods are public.
instance_variables,instance_variable_get,instance_variable_set,instance_variable_defined?andremove_instance_variable(all fromObject) work from outside, so instance variables are not private data in the strict sense: they are only unreachable without these methods. The name must be a symbol or string that starts with@;"name"raisesNameError: 'name' is not allowed as an instance variable name. -
Freezing blocks writes. A frozen object raises
FrozenError(can't modify frozen Member: ...) on any assignment, including throughinstance_variable_set; itsreceiveris the object. -
The order is the assignment order.
instance_variablesreturns the names in the order they were first assigned. Since Ruby 3.2 the VM stores variables using object shapes keyed by that order, so assigning the same variables in the same order ininitialize(the usual style) is also the fastest layout. Define every variable in the constructor, even asnil, so all instances share one shape. -
Variables are per object, not per class. A class has its own instance variables too (
@issuedin the example in Class methods and class-level state), unrelated to the variables of its instances. -
Subclasses share the namespace. An instance variable set by a superclass method and one set by a subclass method with the same name are the same variable. Prefix or choose names carefully when inheriting from a class you do not own.
The related variable kinds are covered elsewhere: @@class_variables, constants and globals in
Variables, Constants and Scope.
Attribute methods
Instance variables are not readable from outside, so objects expose state through methods. Three Module methods
generate the boring ones:
| Call | Defines |
|---|---|
|
|
|
|
|
both |
|
A legacy alias of |
Each of them accepts symbols or strings, and since Ruby 3.0 returns an array of the defined method names as symbols
(attr_accessor :three returns [:three, :three=]), which is what makes private attr_reader :x possible: private
accepts that array. The generated methods are ordinary public methods unless a visibility is in effect (see
Visibility and Encapsulation).
# frozen_string_literal: true
class Copy
attr_reader :condition
attr_writer :shelf_mark
attr_accessor :notes
def initialize(condition: :good)
@condition = condition
end
def condition=(value)
raise ArgumentError, "unknown condition: #{value.inspect}" unless %i[new good worn].include?(value)
@condition = value
end
def mark_worn
self.condition = :worn # the explicit receiver calls the writer
condition
end
def mark_worn_wrongly
condition = :worn # a new local variable: the object is untouched
"#{condition} locally"
end
def shelf_mark? = !@shelf_mark.nil?
private attr_reader :shelf_mark
end
result = Class.new do
attr_reader :one, :two
attr_accessor :three
end
p result.instance_methods(false)
copy = Copy.new
p(copy.public_methods(false).sort)
p copy.shelf_mark?
copy.shelf_mark = "QA76"
p copy.shelf_mark?
p copy.mark_worn_wrongly
p copy.condition
p copy.mark_worn
p copy.condition
copy.condition = :new
p copy.condition
begin
copy.condition = :torn
rescue ArgumentError => e
puts e.message
end
begin
copy.shelf_mark
rescue NoMethodError => e
puts e.message
end
copy.notes ||= []
copy.notes << "spine cracked"
p copy.notes
p(copy.then { Copy.instance_method(:notes=).arity })
p Copy.send(:attr_reader, :extra)
[:one, :two, :three, :three=]
[:condition, :condition=, :mark_worn, :mark_worn_wrongly, :notes, :notes=, :shelf_mark=, :shelf_mark?]
false
true
"worn locally"
:good
:worn
:worn
:new
unknown condition: :torn
private method 'shelf_mark' called for an instance of Copy
["spine cracked"]
1
[:extra]
Points the output makes concrete:
-
The setter pitfall. Inside a method,
condition = :worncreates a local variable; onlyself.condition = :worncalls the writer. The second call in the example (mark_worn_wrongly) returns"worn locally"and the object’sconditionstays:good. This is the one place whereself.is required on a method call (see the implicit-receiver rule in Visibility and Encapsulation), and a classic source of "my setter does nothing" bugs. A related slip is a class with onlyattr_readerwhose code later calls a setter, which raisesNoMethodError: the cure is the rightattr_*(or a method that changes the state on purpose). -
Writers can be written by hand.
condition=above validates its argument; a hand-written writer, a reader that computes a value, or a reader with a default are all normal.attr_*is only for the trivial case. -
||=works through accessors.copy.notes ||= []expands tocopy.notes || (copy.notes = []). -
Predicates return truthy or falsy values.
shelf_mark?returns a boolean here becausenil.!produces one, but the convention for?methods is "answers a question", not "returns exactlytrueorfalse`" (`5.nonzero?returns5). -
private attr_reader :shelf_markkeeps the reader for internal use while the writer stays public; the call from outside raisesprivate method 'shelf_mark' called for an instance of Copy. -
Grouping. RuboCop’s
Style/AccessorGroupingprefers oneattr_reader :a, :bover two calls, andStyle/TrivialAccessorsprefersattr_reader :nicknametodef nickname = @nickname.
to_s and inspect
Two methods turn an object into a string, and they have different audiences:
| Method | Used by and meant for |
|---|---|
|
|
|
|
Overriding one does not change the other (inspect has not called to_s since Ruby 2.0). to_s must return a
String: if it returns anything else, both interpolation and puts silently print the default #<A:0x...> form instead. The shelf gem follows the split: Shelf::Book#to_s returns "Title by Author" while
inspect of a Data object shows #<data Shelf::Book isbn=..., title=..., author=...>.
# frozen_string_literal: true
class Member
def initialize(name, email)
@name = name
@email = email
@loans = []
end
def to_s = "member #{@name}"
end
ada = Member.new("Ada", "ada@example.com")
puts ada
puts "Hello, #{ada}"
puts ada.inspect.sub(/0x\h+/, "0x...")
puts [ada].to_s.sub(/0x\h+/, "0x...")
# Ruby 4.0: inspect asks which instance variables to show.
class Account
def initialize(owner, token)
@owner = owner
@token = token
end
private
def instance_variables_to_inspect = [:@owner]
end
puts Account.new("Ada", "s3cr3t").inspect.sub(/0x\h+/, "0x...")
# Overriding inspect changes p, irb and the output of collections.
class Isbn10
def initialize(digits) = @digits = digits
def inspect = "#<Isbn10 #{@digits}>"
end
p Isbn10.new("0306406152")
p [Isbn10.new("0306406152")]
x = p(1, 2)
p x
member Ada
Hello, member Ada
#<Member:0x... @name="Ada", @email="ada@example.com", @loans=[]>
[#<Member:0x... @name="Ada", @email="ada@example.com", @loans=[]>]
#<Account:0x... @owner="Ada">
#<Isbn10 0306406152>
[#<Isbn10 0306406152>]
1
2
[1, 2]
What to take from it:
-
puts adaprinted the overriddento_s, but theinspectof the same object (and of the array that contains it) is still the default, listing every instance variable. Aninspectthat dumps@emailor a token into a log line is a common leak. -
Ruby 4.0:
instance_variables_to_inspect.Kernel#inspectnow checks whether the object has a method namedinstance_variables_to_inspectand, if it does, shows only the instance variables it returns. The method may be private (the NEWS example makes it so, and theAccountabove does the same), and there is no need to overrideinspectitself. The result forAccountis#<Account:0x... @owner="Ada">: the token is not printed. It is a convenience for hiding secrets and large values, not a security boundary (instance_variable_getstill reads them). -
Before 4.0 the same effect needs a hand-written
inspect, asIsbn10does. A hand-writteninspectshould stay short and should not include secrets. -
pwith one argument returns it, with several returns an array, and with none returnsnil; it printsinspectfor each argument.putsprintsto_sand flattens arrays.ppispwith line breaking for nested structures. -
The 3.4
Hash#inspectformat ({name: "Ada"}) and the 3.4Set/Dataformats are what the examples print; older tutorials show{:name=>"Ada"}.
Reopening classes
A class statement for a name that already holds a class does not replace it: it reopens it, and the new body adds
methods (or redefines existing ones) on the same object. This is how a class is split across files (a gem’s
lib/shelf/book.rb and a later lib/shelf/book/extras.rb), and how the core classes can be extended. The superclass,
if written, must match the existing one, and the name must be a class.
# frozen_string_literal: true
require "shelf"
# The first definition creates Shelf::Book (it is a Data class in the gem).
# Another class block with the same name reopens it instead of replacing it.
module Shelf
class Book
def isbn_digits = isbn.digits
end
end
author = Shelf::Author.new("N. K. Jemisin")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "The Fifth Season", author:)
puts book
puts book.isbn_digits
p Shelf::Book.instance_methods(false).sort
# Reopening a core class is legal too, and is monkey patching.
class Integer
def days = self * 24 * 60 * 60
end
p 14.days
p Integer.instance_method(:days).owner
p Integer.instance_method(:days).source_location.first.end_with?("e07.rb")
The Fifth Season by N. K. Jemisin
9780306406157
[:author, :isbn, :isbn_digits, :title, :to_s]
1209600
Integer
true
Source: Modules and Classes: Classes
Reopening has sharp edges:
-
Mismatched superclass. Writing
class Member < StringwhenMemberalready exists with another superclass raisesTypeError: superclass mismatch for class Member. Omit the< Superwhen reopening. -
Not a class.
class ModwhenModis a module raisesTypeError: Mod is not a class, andmodule MemberwhenMemberis a class raisesMember is not a module(both with a pointer to the previous definition). -
Redefinition warns under
-w. Defining a method that already exists on the same class printswarning: method redefined; discarding old hifollowed bywarning: previous definition of hi was here, a useful tripwire for accidental overwrites. RuboCop’sLint/DuplicateMethodscatches the same mistake statically. -
Monkey patching is global.
Integer#daysabove now exists for every library in the process, and two libraries that add a method with the same name silently overwrite each other. The alternatives are a refinement, a wrapper object or a prepended module, covered in Refinements and Monkey Patching. Redefining a basic operator such asInteger#+makes the VM abandon its fast paths for it, which slows every caller. -
Reopening also reopens a typo.
class Shelf::Bokcreates a brand-new class instead of failing. In a gem, define classes inside nestedmoduleblocks (as above) and let a test or an autoloader notice stray constants.
The example below is a script that shows the first of those errors, so it ends with an uncaught exception:
# frozen_string_literal: true
class Member
def name = "Ada"
end
class Member < String
end
e07b.rb:7:in '<main>': superclass mismatch for class Member (TypeError)
Source: Modules and Classes: Inheritance
Class methods and class-level state
A method defined with def self.name (or inside class << self) is a class method: it belongs to the class object
itself and is called on it (LibraryCard.issue("Ada")). Technically it is an instance method of the class’s singleton
class, which is why class methods are inherited by subclasses and why private_class_method exists
(see Singleton Classes and Class-Level Behavior
for the mechanics). Inside a class body self is the class, which makes a class a place for state of its own:
| State | Where it lives and how it behaves |
|---|---|
A constant ( |
Looked up lexically, then through the ancestors. Visible to subclasses ( |
A class-level instance variable ( |
An instance variable of the class object. Each class, including each subclass, has its own. Reached with |
A class variable ( |
One variable shared by the class and all its subclasses. It leaks between hierarchies, and reading it before it is set raises |
A global ( |
One per process. Avoid. |
# frozen_string_literal: true
# self inside a class body is the class itself.
class LibraryCard
puts "defining #{inspect} (#{self.class})"
MAX_ACTIVE = 3
@issued = 0 # an instance variable of the class object
class << self
attr_reader :issued
def issue(holder)
@issued += 1
new(holder, @issued)
end
end
def self.reset! = @issued = 0
attr_reader :holder, :number
def initialize(holder, number)
@holder = holder
@number = number
end
def to_s = "card ##{number} for #{holder} (max #{MAX_ACTIVE})"
end
puts LibraryCard.issue("Ada")
puts LibraryCard.issue("Grace")
p LibraryCard.issued
p LibraryCard.singleton_methods.sort
p LibraryCard.instance_variables
p LibraryCard.new("Alan", 99).instance_variables
# Subclasses have their own class-level instance variables.
class GuestCard < LibraryCard
end
begin
GuestCard.issue("Guest")
rescue NoMethodError => e
puts e.message
end
p GuestCard.issued
p LibraryCard.constants
p GuestCard::MAX_ACTIVE
defining LibraryCard (Class)
card #1 for Ada (max 3)
card #2 for Grace (max 3)
2
[:issue, :issued, :reset!]
[:@issued]
[:@holder, :@number]
undefined method '+' for nil
nil
[:MAX_ACTIVE]
3
GuestCard.issue inherited the class method and ran it with self set to GuestCard, whose own @issued is nil: the
error undefined method '+' for nil is the classic result of class-level state that a subclass never initialised. The
fixes are an inherited hook that initialises each subclass (see
Hooks, eval and DSLs) or reading the value with || 0.
Instance methods can reach class state through self.class, as LibraryCard does for MAX_ACTIVE (a bare constant name
is lexically scoped and needs no receiver).
The two usual ways to write a class method, equivalent in effect:
# frozen_string_literal: true
class Library
# a single method on the class
def self.open = new
# many methods at once, and the only form where private and attr_* apply
class << self
def close = nil
end
end
p Library.singleton_methods.sort
[:close, :open]
A third spelling, def Library.reopen, names the class instead of self; it works but RuboCop’s Style/ClassMethods asks
for self., so that the method survives renaming the class.
def self.x is the usual choice for one or two methods; class << self is clearer for many, and it is the only form that
lets private, attr_reader and alias_method apply to the class level. Both define methods on the singleton class, so
LibraryCard.singleton_methods lists them. Class methods are inherited: GuestCard.singleton_methods includes issue,
while GuestCard.singleton_methods(false) is empty.
A good habit: put behaviour that needs no instance state (parsing a string, building an object from a different representation) in a class method, and keep instance methods for behaviour that depends on the object’s own variables. A class method that takes an instance as an argument is usually an instance method in disguise.
Inheritance and super
class Dvd < Media makes Media the superclass of Dvd. Instances of Dvd get every instance method of Media and
may override them; Dvd also inherits the class methods and constants of Media. Ruby has single inheritance: a class has
exactly one superclass, and the rest of the reuse is done with modules (see
Modules and Mixins). When no superclass is written it is Object.
Inside a method, super calls the same-named method in the next class up the chain, and it has three forms:
| Form | What it passes |
|---|---|
|
The current values of the method’s parameters, positional and keyword, and the block (an implicit pass-through). |
|
Nothing: no arguments, but the block is still passed (use |
|
Exactly those arguments (and the block). |
The lookup path that super follows, the way modules take part in it, and Method#super_method are the subject of
Method Lookup and super.
# frozen_string_literal: true
class Media
attr_reader :title
def initialize(title)
@title = title
end
def to_s = "#{self.class.name.downcase}: #{title}"
def loan_days = 14
end
class Dvd < Media
attr_reader :minutes
def initialize(title, minutes:)
super(title) # explicit arguments
@minutes = minutes
end
# bare super passes the current arguments (none here)
def loan_days = super - 7
end
class AudioBook < Media
def initialize(title, narrator)
super # bare super passes title and narrator: too many for Media#initialize
@narrator = narrator
end
end
dvd = Dvd.new("Metropolis", minutes: 153)
puts dvd
p dvd.loan_days
p Dvd.superclass
p Dvd.ancestors.take(3)
p Media.subclasses.sort_by(&:name)
p Dvd.instance_method(:to_s).owner
p dvd.method(:loan_days).super_method.owner
begin
AudioBook.new("Dune", "Scott Brick")
rescue ArgumentError => e
puts e.message
end
dvd: Metropolis
7
Media
[Dvd, Media, Object]
[AudioBook, Dvd]
Media
Media
wrong number of arguments (given 2, expected 1)
Reading the output:
-
Dvd#to_sis not defined;Dvd.instance_method(:to_s).ownerisMedia, the class that supplies it.to_susesself.class.name, so a subclass gets a correct label without overriding the method: the template method idea in one line. -
Dvd.ancestors.take(3)is[Dvd, Media, Object]: the chainsuperwalks. -
Class#subclasses(Ruby 3.1) returns the direct subclasses currently loaded, in no guaranteed order, so the example sorts them. It is the supported replacement for scanningObjectSpace.each_object(Class), and subclasses that were garbage collected are absent. There is nodescendantsin 4.0. -
The
AudioBookerror is the best-knownsupertrap. A baresuperininitialize(title, narrator)forwards both arguments, whileMedia#initializetakes one.ArgumentErroris raised, with the usual message, from insideMedia. When the signatures differ, writesuper(title). This is also how to correct tutorials that claim baresuperpasses the original arguments: it passes the current values of the parameters (including reassigned ones, and keywords). -
If a subclass defines
initializeand never callssuper, the parent’s instance variables are never set. RuboCop’sLint/MissingSuperflags a missingsuperininitialize(and in the callbacksinherited,included, …), except in classes with no explicit superclass.
Inheritance means "is a": Dvd is a Media everywhere a Media is expected, and callers may rely on that
(the Liskov substitution idea in SOLID and Design Patterns).
Ruby does not enforce it, so overriding with a different return type or stricter arguments breaks callers silently.
Ruby also does not let you inherit from everything: Class.new(Class) and class Foo < Integer; end; Foo.new fail,
and inheriting from core classes with their own allocation (Array, Hash, String) needs care, see
Designing classes.
Abstract methods
Ruby has no abstract keyword. The usual way to define a method that subclasses must provide is to raise from the base class. The
exception to pick matters, because the first reflex, NotImplementedError, is the wrong one for most code:
-
NotImplementedErroris a subclass ofScriptError, not ofStandardError. It was designed to signal that a feature is unavailable on the platform (forkon Windows). A barerescue(which rescuesStandardError) does not catch it, nor doesrescue => e, so a missing method in a subclass bypasses the application’s error handling and ends the program. The Ruby documentation describes it as the error for a feature that the current platform does not implement. -
NoMethodErrorsays exactly what has happened (a required method is absent), is aStandardError, and is what Ruby raises when a method is not defined at all. -
A custom error under the library’s base error (
Shelf::Error, see Exceptions and Error Handling) lets callers rescue the whole library.
# frozen_string_literal: true
class Exporter
# The abstract operation: subclasses must supply it.
def render(_books) = raise(NoMethodError, "#{self.class} must implement #{__method__}")
def export(books) = render(books).strip
end
class CsvExporter < Exporter
def render(books) = books.join(",")
end
class BrokenExporter < Exporter
end
p CsvExporter.new.export(%w[Dune Emma])
begin
BrokenExporter.new.export([])
rescue NoMethodError => e
puts e.message
end
# NotImplementedError is a ScriptError, not a StandardError.
p NotImplementedError.ancestors.take(4)
begin
begin
raise NotImplementedError, "not yet"
rescue StandardError
puts "bare rescue caught it"
end
rescue ScriptError => e
puts "only ScriptError caught #{e.class}"
end
"Dune,Emma"
BrokenExporter must implement render
[NotImplementedError, ScriptError, Exception, Object]
only ScriptError caught NotImplementedError
Source: NotImplementedError; ScriptError; NoMethodError
Exporter#export is the stable skeleton and render the hook, a small instance of the template method pattern.
Many books and tutorials show raise NotImplementedError in the base class. That is the pattern to avoid, for the reason
the last lines of the output demonstrate. A related trap is raise NotImplementedError, "..." inside code that is
wrapped in rescue => e: the rescue does not trigger and the process exits with the error. An alternative that needs no
base-class method at all is duck typing: call the method and let the NoMethodError of the missing one speak.
Anonymous classes and Class.new
Class.new(superclass = Object) { body } creates a class at run time. The block runs with self set to the new
class (it is evaluated like a class body), so def, attr_reader and include work in it, but constants and @@vars
resolve lexically at the block, not in the class. The class has no name until it is assigned to a constant:
# frozen_string_literal: true
anonymous = Class.new do
def hello = "hello from #{self.class.inspect}"
end
p anonymous.name
puts anonymous.new.hello.sub(/0x\h+/, "0x...")
puts anonymous.inspect.sub(/0x\h+/, "0x...")
Object.const_set(:Greeter, anonymous) # the first constant assignment names the class
p anonymous.name
puts anonymous.new.hello
Named = Class.new { def hello = "I know my name" }
p Named.name
p Class.new(StandardError).superclass
# Ruby 3.3: give an anonymous class a temporary name for error messages.
temporary = Class.new.set_temporary_name("fake<Report>")
p temporary
# Class.new with a parent and a body, as a parameterised subclass.
def error_class(prefix)
Class.new(StandardError) do
define_method(:message) { "#{prefix}: #{super()}" }
end
end
ShelfError = error_class("shelf")
begin
raise ShelfError, "no copies left"
rescue ShelfError => e
puts e.message
p e.class.name
end
begin
Class.new(Comparable)
rescue TypeError => e
puts e.message
end
begin
Class.new(Class)
rescue TypeError => e
puts e.message
end
begin
Class.new(anonymous.singleton_class)
rescue TypeError => e
puts e.message
end
nil
hello from #<Class:0x...>
#<Class:0x...>
"Greeter"
hello from Greeter
"Named"
StandardError
fake<Report>
shelf: no copies left
"ShelfError"
superclass must be an instance of Class (given an instance of Module)
can't make subclass of Class
can't make subclass of singleton class
Things to notice:
-
The name appears at the first assignment to a constant (
Named = Class.new { ... },const_set, or aclassstatement later).Class.newalone yieldsnilasnameand#<Class:0x...>asinspect. -
Module#set_temporary_name(Ruby 3.3) gives an anonymous class a readable label for messages without making it a real constant; the label may not look like a constant name, which is why it is writtenfake<Report>. -
Class.new(parent)takes a class: a module raisessuperclass must be an instance of Class (given an instance of Module),Classitself cannot be subclassed (can't make subclass of Class), and neither can a singleton class (can't make subclass of singleton class). -
define_methodand the closure overprefixare what aclassstatement cannot do: the class is parameterised by a local variable. Remember thatsuper()insidedefine_methodneeds explicit parentheses, because implicitsuperis not allowed there. -
Anonymous classes appear in error messages as
an instance of #<Class:0x...>, which is hard to read in logs. Useset_temporary_nameor name them. -
Struct.newandData.definereturn anonymous classes in the same way, which is whyBook = Data.define(...)gives the class its name. See Struct, Data and Value Objects.
Asking about classes and objects
Ruby code usually asks what an object can do rather than what it is (see Design Philosophy and Coming from Other Languages), but there are a few tools to ask what it is, from the strict to the loose:
| Test | True when |
|---|---|
|
|
|
|
|
The same as |
|
|
|
|
|
The module |
|
The object has a public method |
|
The public and protected methods defined in |
# frozen_string_literal: true
require "shelf"
class Media
def kind = "media"
end
class Dvd < Media
end
module Lendable
end
class AudioBook < Media
include Lendable
end
dvd = Dvd.new
audio = AudioBook.new
p [dvd.is_a?(Dvd), dvd.is_a?(Media), dvd.is_a?(Object), dvd.is_a?(Lendable)]
p dvd.method(:kind_of?) == dvd.method(:is_a?)
p [dvd.instance_of?(Dvd), dvd.instance_of?(Media)]
p [audio.is_a?(Lendable), audio.instance_of?(AudioBook)]
p [Media === dvd, Dvd === audio, Lendable === audio] # rubocop:disable Style/CaseEquality
same = Dvd
p [Dvd < Media, Media < Dvd, same <= Dvd, same < Dvd, Dvd < AudioBook]
p [Dvd <=> Media, Media <=> Dvd, Dvd <=> AudioBook, Dvd <=> 3]
p Dvd.ancestors.include?(Media)
p Dvd.include?(Lendable)
p AudioBook.include?(Lendable)
p [dvd.respond_to?(:to_s), dvd.respond_to?(:initialize), dvd.respond_to?(:initialize, true)]
p dvd.class.instance_methods(false)
p AudioBook.included_modules.first
p 3.is_a?(Comparable)
label =
case audio
when Dvd then "disc"
when Lendable then "lendable"
else "other"
end
puts label
p Shelf::Isbn.ancestors.take(2)
p Shelf::Isbn.superclass
p Shelf::Book.superclass
[true, true, true, false]
true
[true, false]
[true, true]
[true, false, true]
[true, false, true, false, nil]
[-1, 1, nil, nil]
true
false
true
[true, false, true]
[]
Lendable
true
lendable
[Shelf::Isbn, Data]
Data
Data
Remarks:
-
is_a?follows modules:audio.is_a?(Lendable)is true becauseAudioBookincludes it, whiledvd.is_a?(Lendable)is false.dvd.instance_of?(Media)is false even though aDvdis aMedia;instance_of?is rarely what you want (RuboCop’sStyle/ClassEqualityComparisonsteers away fromobj.class == Kfor the same reason). -
Lendable === audioin acaseis what allowswhen Lendableto select by role. The copStyle/CaseEqualityobjects to calling===directly; the line is annotated withrubocop:disablebecause the example is about the operator. -
Dvd <=> 3isnil: comparing a class with a non-module is not an error. -
respond_to?(:initialize)is false becauseinitializeis private; the second argumenttrueincludes private methods. -
Shelf::Isbn.ancestors.take(2)is[Shelf::Isbn, Data]: classes made withData.defineinherit fromData, andComparabledoes not appear because onlyInteger,String,Timeand a few others include it. -
A type check at a boundary is fine (
raise ArgumentError unless value.is_a?(Shelf::Isbn)); a chain ofis_a?tests in the middle of logic usually means a method should be polymorphic. Acaseover classes is the idiom when the types are genuinely closed. See Pattern Matching forcase/inon objects.
BasicObject, Object and Kernel
Three names sit at the root of every class hierarchy. They are easy to mix up, and each has a distinct job:
| Name | Role |
|---|---|
|
The root class and the only class with no superclass ( |
|
The default superclass: a class declared without |
|
A module, included by |
Class is a subclass of Module, which is a subclass of Object: a class is an object, and a module is a class’s
parent in the sense that a class is a module that can be instantiated. This is why Member.class is Class,
Class.class is Class too (the chain loops back on itself), and why Class.ancestors is
[Class, Module, Object, Kernel, BasicObject].
# frozen_string_literal: true
p Class.ancestors
p Class.superclass
p Class.class
p [Object.superclass, BasicObject.superclass]
p BasicObject.instance_methods.sort
p BasicObject.private_instance_methods.sort
p Kernel.instance_method(:puts).owner
p Kernel.private_method_defined?(:puts)
p Object.new.respond_to?(:puts)
p Object.new.respond_to?(:then)
# A BasicObject subclass is a blank slate: no inspect, no is_a?, no puts.
class Proxy < BasicObject
def initialize(target) = @target = target
def method_missing(name, ...)
::Kernel.puts "-> #{name}"
@target.public_send(name, ...)
end
def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private)
end
proxy = Proxy.new("shelf")
p proxy.upcase
p proxy.length
p proxy.class
begin
BasicObject.new.inspect
rescue NoMethodError => e
puts e.message
end
p Proxy.instance_method(:__id__).owner
puts Object.new.inspect.sub(/0x\h+/, "0x...")
puts Object.new.to_s.sub(/0x\h+/, "0x...")
p Object.new.frozen?
p(5.then { it + 1 })
p Kernel.respond_to?(:puts)
p Kernel.singleton_class.include?(Kernel)
[Class, Module, Object, Kernel, BasicObject]
Module
Class
[BasicObject, nil]
[:!, :!=, :==, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]
[:initialize, :method_missing, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined]
Kernel
true
false
true
-> upcase
"SHELF"
-> length
5
-> class
String
undefined method 'inspect' for an instance of BasicObject
BasicObject
#<Object:0x...>
#<Object:0x...>
false
6
true
true
Source: BasicObject; Object; Kernel; BasicObject#method_missing
What the output shows:
-
BasicObjectis nearly empty. Its eight public instance methods are!,!=,==,__id__,__send__,equal?,instance_evalandinstance_exec. A subclass of it (Proxy) is therefore a blank slate: callinginspecton a plainBasicObjectraisesundefined method 'inspect' for an instance of BasicObject. Inside such a class, constants resolve lexically, so core classes need a::prefix (::Kernel.puts). -
method_missingis where the blank slate pays off.Proxyforwards every other call to the wrapped object; evenproxy.classis forwarded, so it reports the target’s classString. A proxy that must answerrespond_to?correctly also needsrespond_to_missing?, see Dynamic Dispatch and Method Definition.DelegatorandSimpleDelegator(Modules and Mixins) are the usual way; subclassingBasicObjectis for the rare cases that must forward eveninspector==. -
Kernelfunctions are private.Object.new.respond_to?(:puts)isfalse, yetputsworks from inside any object because the call has an implicit receiver. Because they aremodule_function-style,Kernel.puts "x"also works with an explicit receiver. Callingobj.putsfrom outside raisesprivate method 'puts' called for an instance of .... -
Objectmethods are public.then,tap,itself,frozen?,instance_of?and the reflection methods can be called from anywhere.Object#then(aliasedyield_self) passes the receiver to a block and returns the block’s value, handy withit. -
Classes are
Object`s.Everything an `Object can do, a class can:Member.respond_to?(:then),Member.frozen?,Member.instance_variables,Member.inspect. That is why class-level state (see Class methods and class-level state) needs no special mechanism.
Outside a BasicObject subclass you almost never need to think about this layering. It matters when you override
method_missing, build a proxy, define a DSL base class (a BasicObject subclass prevents puts or exit from
leaking into the DSL’s scope; see Hooks, eval and DSLs), or read
Object.instance_methods to see why a name is already taken. When you define your own to_s you override
Kernel#to_s/Object#to_s; super still reaches it.
The shelf classes
The running shelf gem is a small library catalogue whose classes cover the page’s ideas: value objects made with
Data.define (Isbn, Author, Book), a state-driven Loan, a duck-typed Store::Memory, an Enumerable
Catalog, a configuration Struct, and an error class that inherits from StandardError.
The solid diamonds mean that a Book is made of an Isbn and an Author (composition: they have no life apart from it), the open
diamonds that Loan and Catalog merely hold references to collaborators passed in. Catalog talks to its store through
two methods (all and add), not through a class: any object that answers both can be substituted, which is the
duck-typing and composition pattern developed in the next section. Inheritance appears only where it means "is a" (Error is a
StandardError), and the three Data classes inherit from Data.
Designing classes
A class is a promise about a small set of responsibilities. The following rules are the practical output of the object-oriented material above.
Keep constructors cheap and honest
As in Constructors without I/O: initialize assigns, validates and freezes; it does not read files, open
connections, print or start threads. When an object needs one of those, take it as an argument and let a factory
(Settings.load) do the effect. When validation is non-trivial, as in Shelf::Isbn, raise ArgumentError from
initialize so no invalid object is ever visible.
Prefer composition over inheritance
Inheritance fixes the relationship at definition time, exposes the parent’s whole interface and couples the child to the
parent’s internals. Composition lets the collaborator be chosen when the object is built. The rule of thumb:
inherit when the child is a kind of parent for all of the parent’s clients (Dvd is a Media); otherwise hold the
collaborator in an instance variable and forward the few methods needed. Deep chains are a design smell: each
level adds methods the reader must know, and a change in a base class can break every descendant (the "fragile base
class" problem). Two or three levels is normal; more usually means reuse via modules or composition is wanted.
# frozen_string_literal: true
require "shelf"
require "stringio"
# One job: turn a loan into text. No I/O, easy to test.
class Receipt
def initialize(book, member, days)
@book = book
@member = member
@days = days
end
def to_s = "#{@member} borrowed #{@book.title} for #{@days} days"
end
# One job: deliver text. Any object with #deliver(text) can replace it.
class StreamNotifier
def initialize(io) = @io = io
def deliver(text) = @io.puts(text)
end
# The desk coordinates collaborators that are passed in, not created inside.
class Desk
def initialize(store: Shelf::Store::Memory.new, notifier: StreamNotifier.new($stdout))
@store = store
@notifier = notifier
end
def check_out(book, member)
@store.add(book)
receipt = Receipt.new(book, member, Shelf.config.loan_days)
@notifier.deliver(receipt.to_s)
receipt
end
def count = @store.all.size
end
author = Shelf::Author.new("Italo Calvino")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "Invisible Cities", author:)
log = StringIO.new
desk = Desk.new(notifier: StreamNotifier.new(log))
desk.check_out(book, "Ada")
p log.string
p desk.count
p Desk.public_instance_methods(false).sort
# Subclassing a core class couples you to its whole interface.
class Stack < Array
end
stack = Stack.new([1, 2, 3])
p stack.class
p stack.map { it * 2 }.class
p stack.select(&:odd?).class
p stack.dup.class
p Stack.instance_methods.size > 100
"Ada borrowed Invisible Cities for 14 days\n"
1
[:check_out, :count]
Stack
Array
Array
Stack
true
Source: Modules and Classes: Classes; Array
Desk has two public methods (check_out and count), and it never creates its collaborators inside check_out: the store and the
notifier arrive through the constructor with defaults (constructor injection, the pattern the SOLID page names
dependency inversion; see SOLID and Design Patterns). A test passes a StringIO and reads log.string; production passes $stdout. Neither Receipt nor StreamNotifier
knows about the others. Adding an EmailNotifier needs no change to Desk.
The Stack < Array lines show the cost of inheriting from a core collection. The subclass inherits the entire Array interface (Stack.instance_methods.size
is well over a hundred), so stack << :anything, sort! and clear all remain public even when a stack should offer
only push and pop. map and select on an Array subclass return a plain Array (and since Ruby 3.0 so do take, drop, slice, uniq, flatten and *), while dup returns a Stack: the rules differ by method and by version. Wrapping the array in an instance
variable and exposing push, pop and each is cleaner. The same warning applies to Hash and String.
Small public surface, one reason to change
A God class is one that knows and does too much: it formats, stores, validates, sends e-mail and reports. It collects
every change request and every bug. The symptoms are a long list of unrelated instance variables, methods that use disjoint
subsets of them, a name ending in Manager, Helper or Handler, and a test file that needs elaborate set-up. The cure is to extract
one collaborator per responsibility (Receipt, StreamNotifier) and let the original class coordinate. A related rule is to limit the
public methods: everything that is not part of the contract is private, because every public method is something clients
may call and you must keep working. See Visibility and Encapsulation.
Further conventions that make a class easy to read:
-
Order. Put constants and
include`s first, then the class methods, `initialize, the public methods, andprivateat the end, with each private helper after the first method that uses it (the "stepdown" order, so that the file reads top to bottom from the general to the detail). RuboCop’sLayout/ClassStructurecan enforce an order; it is disabled by default. -
Name by role. A class name is a noun (
Receipt,Catalog); a method name is a verb or a property (check_out,count); a predicate ends in?(nickname?) and the dangerous twin of a method ends in!. Constants areSCREAMING_SNAKE_CASE, files aresnake_case.rbmatching the class (library_card.rbforLibraryCard). -
Extract mixins for reusable behaviour. When two unrelated classes need the same method, put it in a module rather than in a shared superclass (see Modules and Mixins).
-
Value objects are immutable. A small class that is defined by its values should freeze itself in the constructor and implement equality on the values;
Data.definedoes both (see Struct, Data and Value Objects and Equality, Hashing and Comparison). -
Document with comments that say why. A class comment states the responsibility in a sentence (RuboCop’s
Style/Documentationrequires one; the shelf gem turns it off). See Documentation with RDoc and YARD.
Where older books and tutorials are outdated
Ruby books and articles written for Ruby 2.x and early 3.x still describe classes correctly in outline, but several details moved:
| Older statement | Current behaviour |
|---|---|
|
It is a |
Error messages quote the method name with a backtick and end in |
Ruby 3.3 changed the receiver text and 3.4 the quote: |
|
Ruby 3.4 prints |
|
Assign the generated class to a constant, |
A class with only |
That raises |
Bare |
It passes the current values of the parameters, with keywords and the block. When the parent takes fewer arguments, write |
`@instance |
|
= new` in a class method makes a singleton |
It is not thread-safe; use the |
Reading an uninitialised instance variable prints a warning under |
The warning was removed in Ruby 3.0. A typo now yields |
A list of the object’s instance variables in |
Ruby 4.0 asks the object for |
|
|
Naming an anonymous class requires a constant |
|
|
|
Two further points where careless advice circulates. safe_constantize, constantize and classify are ActiveSupport
methods (Rails), not core Ruby: in plain Ruby the way to look a class up from a string is Object.const_get("Shelf::Book"),
which raises NameError for an unknown name, and which should not be given untrusted input. And Class objects are
not special syntax: Class.new, Module.new and Struct.new are ordinary method calls.
Common exceptions and warnings
All of these were reproduced on Ruby 4.0.7 (a scratch Member class with no members unless noted).
| Error or warning | Message (4.0) | Cause and fix |
|---|---|---|
|
|
|
|
|
The receiver’s class has no such method. A typo, a missing |
|
|
Class without |
|
|
|
|
|
Calling a private reader with an explicit receiver (outside the object). |
|
|
|
|
|
An instance variable (often a class-level one in a subclass) was never initialised. |
|
|
A bare identifier that is neither a local variable nor a method. |
|
|
Typo, missing |
|
|
A class variable was read before being set. |
|
|
|
|
|
|
|
|
Reopening a class with a different |
|
|
The name already holds a module (or class). Rename. |
|
|
|
|
|
Not allowed. |
|
|
|
|
|
|
|
|
Writing an instance variable of a frozen object (including |
|
message of your choice |
Not a |
Warning ( |
|
The same method defined twice in a class. |
Warning |
|
A constant was reassigned. |
Warning ( |
|
|
RuboCop cops for classes
| Cop | What it checks |
|---|---|
|
Every class and module has a comment above it (off in the shelf configuration). |
|
Nested style ( |
|
A file defines at most one top-level class or module (added in RuboCop 1.85; the example configuration turns it off because several short classes share a file). |
|
Forbids |
|
Prefers |
|
Prefers |
|
Prefers |
|
Flags direct |
|
Flags |
|
Prefers |
|
Groups accessors of the same kind in one call. |
|
Flags |
|
A class or metaclass without a body. |
|
One consistent style for empty classes: the |
|
A subclass |
|
The same method defined twice in one class. |
|
|
|
A constant defined in a block (such as |
|
Methods that return a boolean end with |
|
Predicates should not start with |
|
Methods named |
|
Size limits that detect God classes and fat methods. |
|
Optional: enforces the order of constants, includes, class methods, |
Pitfalls
-
Forgetting
self.on a setter.name = "x"inside a method sets a local variable;self.name = "x"calls the writer. -
A subclass that skips
superininitialize. The parent’s variables arenil, and the failure appears much later. -
Bare
superwith a different signature. It forwards all current arguments; usesuper(a)orsuper(). -
Typos in instance variable names. No warning since 3.0; the variable reads as
nil. Cover the class with tests. -
Class variables.
@@xis shared by the whole hierarchy; use a class-level instance variable (and remember it is per class: initialise it ininheritedor read it with||=). -
Subclassing core collections. The subclass inherits the whole interface and surprising return types; wrap instead.
-
NotImplementedErrorfor abstract methods. It escapesrescue => e. UseNoMethodErroror a library error. -
Monkey patching core classes. The method exists for every library in the process; prefer a refinement or a wrapper.
-
Heavy constructors. I/O, threads or network in
initializemake objects hard to test and failures hard to recover. -
Mutable defaults.
def initialize(items = [])is fine (a new array each call), but a constantDEFAULT = []shared across instances is mutated by all of them;freezeit or build a new one. -
Public readers of mutable state.
attr_reader :loanshands out the internal array; return@loans.dupor a frozen copy when callers must not change it. -
to_sreturning a non-String. Interpolation andputssilently fall back to the default#<Class:0x...>form. -
God classes and deep chains. Extract collaborators; keep inheritance shallow.
Version notes
| Version | Change |
|---|---|
2.0 |
|
3.0 |
|
3.1 |
|
3.2 |
|
3.3 |
|
3.4 |
Error messages use |
4.0 |
|
References
-
Class, Module, Object, BasicObject and Kernel
-
Class#new, Class#allocate, Class#superclass, Class#subclasses and Class#attached_object
-
Module#attr_accessor, Module#private_class_method, Module#set_temporary_name and Module#ancestors
-
NotImplementedError, ScriptError, NoMethodError, NameError and FrozenError
-
Data, Struct, Comparable and Forwardable