Visibility and Encapsulation
|
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); features newer than Ruby 3.0 name the version that introduced them.
Encapsulation is the practice of exposing a small, deliberate interface and keeping everything else free to change.
Ruby supports it with three method visibilities (public, protected and private), a separate visibility for
constants (private_constant), and a handful of conventions around them. Unlike Java, C# or Swift, none of this is a
security boundary: visibility is a rule about how a method is called, it is checked when the call happens, and
reflection can step around it on purpose. This page covers the rules exactly, the syntax that sets them (and what each
form returns), the traps in the scope of the private mode switch, class-level and constant visibility,
module_function, top-level methods, the ways round the rules, the reflection queries, and how to design a narrow public
surface that stays easy to refactor.
It builds on Classes and Objects (instance variables, attr_*,
initialize), Methods and Parameters (what def returns)
and Modules and Mixins. The lookup rules that decide which
method is found are in Method Lookup and super, class-level
methods in
Singleton Classes and Class-Level Behavior,
and the reflection that bypasses visibility in
Dynamic Dispatch and Method Definition and
Reflection, Introspection and Tracing.
Refinements, the controlled alternative to monkey patching, are in
Refinements and Monkey Patching; immutable value
objects, which need far less hiding, are in
Struct, Data and Value Objects. Design advice
continues in Idioms, Style and Clean Code and
SOLID and Design Patterns. For how other languages
control access, see Java,
C#,
Kotlin,
Swift access control,
PHP visibility,
Rust modules and visibility and
Python (which has only naming conventions).
Every example on this page 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). Two cops are relaxed on purpose: Style/AccessModifierDeclarations is disabled because the examples show the
inline private def form next to the grouped one, and a few lines carry a rubocop:disable comment where an example
shows a redundant form deliberately. The output shown in # => comments, and in the output blocks, is the real output.
Each file is self-contained and uses the names of the shelf library catalogue used throughout this section (Shelf,
Book, Isbn, Author, Catalog), run with ruby -Ilib so that require "shelf" finds the gem (see the shelf gem source).
Encapsulation in Ruby
What is hidden, and what is not
Ruby has no package, internal, friend or final keyword and no per-file privacy. What it has is a small set of
mechanisms, each with a different reach:
-
Instance variables are always private to the object. There is no syntax for reading
obj.@namefrom outside, so the only way to expose state is a method:attr_reader,attr_writer,attr_accessor, or one you write. -
Methods are public by default and can be made
protectedorprivate. The visibility belongs to the method entry in a class or module, not to the name. -
Constants are public by default and can be hidden from scoped lookup (
Mod::NAME) withprivate_constant. -
Local variables are visible only in their lexical scope; blocks and
define_methodbodies capture them, which makes closures the strongest hiding mechanism Ruby has (see Real information hiding).
The consequence is that "private" in Ruby describes the interface of an object: which messages other objects are
meant to send it. The guard exists to stop accidents and to document intent, and it is checked against the form of the
call (the receiver), never against the identity of the caller’s class, with one exception, protected.
The three visibilities
| Visibility | Who may call it | Typical use |
|---|---|---|
|
Anyone, with any receiver. The default for methods defined with |
The contract: the messages callers are meant to send. |
|
A method call whose receiver is explicit, when the caller’s |
Comparison operators and similar methods that need the other object’s internals without making them public. |
|
A method call with no explicit receiver, or with the literal |
Helpers, steps of an algorithm, template-method hooks, setters used internally. |
The next figure is the complete decision for a call written as receiver.name(args). Reflective calls (send,
__send__, Method#call, instance_eval and friends) skip the visibility check, which is why the rules are advisory.
Three details in the figure matter in practice. The check is made against the method that lookup finds first, so a
private method in a subclass hides a public method of the same name in the parent. A failed check goes through
method_missing, which is how a catch-all method_missing can accidentally answer for a private method (shown in the
NoMethodError example below). And the check happens at call time: you can define a method that calls a private method
wrongly, and nothing complains until it runs.
A first complete example
A Member of the library has a small public interface (borrow, books, owes_more_than?), a protected reader that
only other members may use, and a private guard method. The class diagram uses the UML markers for the three levels
(+ public, # protected, - private).
# frozen_string_literal: true
require "shelf"
module Shelf
# A library member: the public API is small, the details are hidden.
class Member
LIMIT = 2
attr_reader :name
def initialize(name, fines_cents: 0)
@name = name
@fines_cents = fines_cents
@books = []
end
# Public: part of the contract with callers.
def borrow(book)
ensure_below_limit
@books << book
self
end
def books = @books.dup
# Callable on another Member, because the caller is a Member too.
def owes_more_than?(other) = fines_cents > other.fines_cents
protected
attr_reader :fines_cents
private
def ensure_below_limit
raise Shelf::Error, "#{name} has reached the limit of #{LIMIT} books" if @books.size >= LIMIT
end
end
end
author = Shelf::Author.new("Ursula K. Le Guin")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("978-0-306-40615-7"), title: "The Dispossessed", author:)
ana = Shelf::Member.new("Ana", fines_cents: 250)
ben = Shelf::Member.new("Ben")
ana.borrow(book).books.size # => 1
ana.owes_more_than?(ben) # => true
begin
ana.fines_cents
rescue NoMethodError => e
e.message # => "protected method 'fines_cents' called for an instance of Shelf::Member"
end
begin
ana.ensure_below_limit
rescue NoMethodError => e
e.message # => "private method 'ensure_below_limit' called for an instance of Shelf::Member"
end
begin
ana.borrow(book).borrow(book)
rescue Shelf::Error => e
e.message # => "Ana has reached the limit of 2 books"
end
Notice what the example relies on. books returns a copy (dup), so callers cannot mutate the internal Array through
the reader: hiding the variable is not enough if a method hands out the object. LIMIT is an ordinary public
constant, and the private keyword above ensure_below_limit does not affect constants (see
Constants: private_constant, public_constant and deprecate_constant).
public: the default, and the methods Ruby makes private for you
A method defined with def is public unless the surrounding class body has switched mode (see
Choosing a visibility: the four syntactic forms). public with names re-publishes methods, and public with no
arguments switches the mode back after a private or protected section. A public method is a promise: anything
that can be called from outside will eventually be called from outside, and removing it later is a breaking change.
Five method names are an exception. initialize, initialize_copy, initialize_clone, initialize_dup and
respond_to_missing? are always private, however they are written and wherever the mode switch stands. They are hooks
that the runtime calls (new calls initialize, dup calls initialize_dup and then initialize_copy), and calling
them directly would corrupt an object’s state or bypass the protocol. Most of the infrastructure that Ruby gives every
object is private for the same reason: Kernel#puts, format, raise, require and Integer() are private instance
methods of Kernel, so "text".puts is a NoMethodError, while puts "text" works from anywhere because it has no
receiver.
# frozen_string_literal: true
require "shelf"
module Shelf
# A receipt printed when a book is returned.
class Receipt
# No access modifier anywhere: Ruby makes these hooks private by itself.
def initialize(book) = @book = book
def initialize_copy(source)
super
@book = source.instance_variable_get(:@book)
end
def respond_to_missing?(name, include_private = false) = name == :total || super
def to_s = "Receipt for #{@book.title}"
end
end
Shelf::Receipt.private_instance_methods(false).sort # => [:initialize, :initialize_copy, :respond_to_missing?]
Shelf::Receipt.public_instance_methods(false).sort # => [:to_s]
author = Shelf::Author.new("Octavia E. Butler")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "Kindred", author:)
receipt = Shelf::Receipt.new(book)
begin
receipt.initialize(book)
rescue NoMethodError => e
e.message # => "private method 'initialize' called for an instance of Shelf::Receipt"
end
receipt.dup.to_s # => "Receipt for Kindred"
receipt.respond_to?(:total) # => true
Object.private_method_defined?(:puts) # => true
Kernel.private_method_defined?(:format) # => true
BasicObject.private_instance_methods(false).sort.first(2) # => [:initialize, :method_missing]
The same listing shows the other side of the rule: BasicObject itself defines private hooks (initialize,
method_missing, singleton_method_added and two more), which is why you override method_missing as a private
method by convention even though a public one also works (a public method_missing lets callers invoke it directly).
private
The rule: no explicit receiver
A private method can be called only in the function-call form, name(args) or just name, where the receiver is
implicitly self. The consequence that surprises programmers from Java and C# is that private is per call form,
not per class: another instance of the same class is still "someone else", so other.balance fails even inside
Account. Ruby’s protected exists for exactly that case.
# frozen_string_literal: true
require "shelf"
module Shelf
# Private is a rule about the call site, not about the object: a second
# Account cannot use the first one's private methods.
class Account
def initialize(cents) = @cents = cents
def same_balance_as?(other) = balance == other.balance
private
def balance = @cents
end
# A catch-all that also answers for names that exist but are private.
class Forgiving
def method_missing(name, *args)
name.start_with?("fetch_") ? "ghost #{name} #{args.inspect}" : super
end
def respond_to_missing?(name, include_private = false) = name.start_with?("fetch_") || super
private
def fetch_title = "real private method"
end
end
mine = Shelf::Account.new(500)
begin
mine.same_balance_as?(Shelf::Account.new(500))
rescue NoMethodError => e
[e.name, e.receiver.class, e.args, e.private_call?] # => [:balance, Shelf::Account, [], false]
e.message # => "private method 'balance' called for an instance of Shelf::Account"
end
begin
mine.instance_eval { missing_helper(1) }
rescue NoMethodError => e
[e.private_call?, e.message] # => [true, "undefined method 'missing_helper' for an instance of Shelf::Account"]
end
forgiving = Shelf::Forgiving.new
forgiving.fetch_title # => "ghost fetch_title []"
forgiving.send(:fetch_title) # => "real private method"
forgiving.fetch_author # => "ghost fetch_author []"
forgiving.respond_to?(:fetch_title) # => true
begin
forgiving.other
rescue NoMethodError => e
e.message # => "undefined method 'other' for an instance of Shelf::Forgiving"
end
NoMethodError carries the details: name is the method, receiver the object, args the arguments given, and
private_call? tells how the call was written, which is true only for a function-style call (no receiver) that
failed, not for a private method called with an explicit receiver. That is why the first handler printed false. The
second part of the example shows the interaction with method_missing: a failed visibility check dispatches to
method_missing like any unknown name, so a catch-all that answers every name starting with fetch_ also answers for
the private fetch_title. send finds the real private method because it skips the check and never reaches
method_missing. Guard such handlers with a narrow pattern, call super for everything else (as the example does), and
keep respond_to_missing? consistent.
The literal self exception (Ruby 2.7)
Before Ruby 2.7 the only private methods that could be called with self. in front were setters (self.position = 1),
because the assignment syntax needs an explicit receiver. Since Ruby 2.7 any private method can be called with the
literal self as receiver, which is why self.advance_position is no longer an error. The rule is syntactic: it applies
to the keyword self written directly as the receiver. A variable that holds the same object, a parenthesised
(self), and a method that returns self do not qualify. The scope operator form self::name and the safe navigation
form self&.name are parsed with a literal self and are accepted.
# frozen_string_literal: true
require "shelf"
module Shelf
# A reservation queue position; the setter is private.
class Hold
attr_reader :title
def initialize(title, position)
@title = title
self.position = position
end
def advance
self.position -= 1
self
end
def position_text = "#{title}: place #{position}"
# Different ways of writing the receiver of a private call. RuboCop flags the
# redundant forms, so its cops are switched off for this one method.
# rubocop:disable Style/RedundantSelf, Style/ColonMethodCall, Lint/RedundantSafeNavigation
# rubocop:disable Style/RedundantParentheses, Metrics/MethodLength
def receivers
me = self
forms = {
implicit: -> { position },
literal_self: -> { self.position },
scope_operator: -> { self::position },
safe_navigation: -> { self&.position },
parenthesised: -> { (self).position },
through_variable: -> { me.position }
}
forms.transform_values do |form|
form.call
rescue NoMethodError => e
e.message
end
end
# rubocop:enable Style/RedundantSelf, Style/ColonMethodCall, Lint/RedundantSafeNavigation
# rubocop:enable Style/RedundantParentheses, Metrics/MethodLength
# A local variable shadows the private setter: it does NOT call position=.
def reset_wrongly
position = 0
[position, self.position]
end
private
attr_accessor :position
end
end
hold = Shelf::Hold.new("Kindred", 3)
hold.advance.position_text # => "Kindred: place 2"
results = hold.receivers
results.values_at(:implicit, :literal_self, :scope_operator, :safe_navigation) # => [2, 2, 2, 2]
results[:parenthesised] # => "private method 'position' called for an instance of Shelf::Hold"
results[:through_variable] # => "private method 'position' called for an instance of Shelf::Hold"
begin
hold.position
rescue NoMethodError => e
e.message # => "private method 'position' called for an instance of Shelf::Hold"
end
begin
hold.position = 1
rescue NoMethodError => e
e.message # => "private method 'position=' called for an instance of Shelf::Hold"
end
hold.reset_wrongly # => [0, 2]
hold.position_text # => "Kindred: place 2"
hold.send(:position) # => 2
Source: Ruby 2.7.0 released: calling a private method with a literal self and Modules and Classes, Visibility
Two lessons come out of the example. First, a private writer needs the explicit self., both for self.position = x
and for compound assignment, self.position -= 1, because without self the parser reads position = x as the
creation of a local variable and never calls the writer; that is what reset_wrongly shows with its [0, 2] result.
Second, RuboCop’s Style/RedundantSelf knows this and flags self. only where it is not required, which is why the demonstration of
the redundant forms had to switch the cops off. In normal code the only self. you write is on a setter, on a call to
a method that shares its name with a local variable, or on a method such as class whose name is also a keyword.
Private methods and inheritance
Since private looks only at the call form, a private method of a parent class is visible to every subclass: the
subclass body calls fee_cents or helper without a receiver and it works. This is the opposite of Java and C++ (where
private stops at the class boundary and protected is the inheritance level) and is why Ruby code is full of private
methods that subclasses call and override. The rest of this page keeps it in mind: a private method is part of the
protocol between a class and its subclasses, not a sealed detail.
protected
A protected method may be called with an explicit receiver, but only from code whose self is an instance of the class
or module that defines the method. The official documentation puts the typical use plainly: protected visibility is most
often applied to == and other comparison methods, where the author does not want to expose an object’s state to every
caller but still needs to read it from another object of the same kind.
The check compares the caller’s self with the owner of the method, using kind_of?. That has three consequences,
all shown in the next example:
-
Instances of different subclasses of the owner can call each other’s protected methods (a
Studentcan read the rank of aTeacherwhen both inherit it fromPatron). -
A module’s methods run with the includer as
self, so a module included into the owner can call the protected method (rank_of). -
An unrelated class, such as
Auditor, cannot, even if it holds aPatron.
# frozen_string_literal: true
require "shelf"
module Shelf
# Members with a protected rank: comparable, but the rank is not public API.
class Patron
include Comparable
attr_reader :name
def initialize(name, loans_count)
@name = name
@loans_count = loans_count
end
def <=>(other) = other.is_a?(Patron) ? rank <=> other.rank : nil
protected
def rank = @loans_count
end
class Student < Patron; end
class Teacher < Patron; end
# Not a Patron: the same call is rejected.
class Auditor
def peek(patron) = patron.rank
end
# A module included into Patron: its methods run with a Patron as self.
module Ranking
def rank_of(other) = other.rank
end
Patron.include(Ranking)
end
ana = Shelf::Student.new("Ana", 4)
ben = Shelf::Teacher.new("Ben", 9)
[ana < ben, ben < ana] # => [true, false]
[ben, ana].min.name # => "Ana"
ana.between?(ana, ben) # => true
ana.rank_of(ben) # => 9
begin
ana.rank
rescue NoMethodError => e
e.message # => "protected method 'rank' called for an instance of Shelf::Student"
end
begin
Shelf::Auditor.new.peek(ana)
rescue NoMethodError => e
e.message # => "protected method 'rank' called for an instance of Shelf::Student"
end
method_name = :rank
begin
ana.public_send(method_name)
rescue NoMethodError => e
e.message # => "protected method 'rank' called for an instance of Shelf::Student"
end
ana.send(method_name) # => 4
ana.respond_to?(:rank) # => false
ana.respond_to?(:rank, true) # => true
Shelf::Patron.protected_instance_methods(false) # => [:rank]
Shelf::Patron.instance_methods(false).sort # => [:<=>, :name, :rank]
public_send honours protected as well as private, send does not, and respond_to? answers false for a protected
method unless the second argument asks for private and protected methods too. protected_instance_methods lists
protected names; instance_methods and methods include them, public_instance_methods and public_methods do not.
When is protected the right tool? It is useful when two objects of the same type must see each other’s internals, and
no more. Most designs get along without it:
-
Value objects made with
Data.defineorStructexpose their members through public readers and compare by value, so there is nothing to protect (see Struct, Data and Value Objects). -
A comparison can be built on a public
to_hor a public key method (sort_key), which also works for callers that are not Patrons. -
Where
protectedis used, keep the protected surface to one or two readers. Every protected method is reachable from every subclass instance and from every module that is later mixed in, which is a wider door than it looks.
The Comparable mix-in fits this pattern well: it needs only the public <=>, and the protected reader is read
inside <=> where the caller’s self is a Patron. The protected detail never leaks into the public interface.
Choosing a visibility: the four syntactic forms
All the visibility methods live on Module (so they are available in every class and module body) and accept method
names as Symbols or Strings. public, protected, private and module_function are private methods of Module,
which is why they are written without a receiver inside the body, and why calling SomeClass.private(:name) from
outside fails (see Changing visibility in subclasses and after the fact). private_class_method,
public_class_method, private_constant and public_constant are public.
-
The mode switch:
private(orprotected,public) with no arguments. It changes the default visibility of everydefthat follows in the same class body, up to the end of the body, or until another switch. -
Names after the definition:
private :name,private :first, :second,private [:name](an Array is accepted since Ruby 3.0). The methods must already exist orNameErroris raised. -
Inline:
private def name ... end. This works becausedefreturns the Symbol of the method it defines (since Ruby 2.1), andprivatethen receives that Symbol. It reads well for a single method and is the standard way to protect a single endless method (private def helper = 1). -
With attribute macros:
private attr_reader :serial. Since Ruby 3.0attr_reader,attr_writer,attr_accessorandattrreturn an Array of the Symbols they define, so they compose with every modifier.
Since Ruby 3.1 the modifiers also return something useful: with one argument the argument itself, with several
arguments an Array of them, and with none, nil. Before 3.1 they returned nil in every case, which is why older
code and older books treat the return value as meaningless.
| Call | Returns (Ruby 3.1 and later) | Notes |
|---|---|---|
|
|
Changes the mode for the following |
|
|
One argument is returned unchanged. |
|
|
Several arguments come back as an Array. |
|
|
Array argument, Ruby 3.0 and later. |
|
|
The inline form: |
|
|
The |
|
The same rules |
The example runs every form and prints the return values.
# frozen_string_literal: true
module Shelf
# Every way of choosing a visibility, with the value each call returns (Ruby 3.1+).
class Stamp
def by_name = "private by symbol"
def by_array = "private by array (Ruby 3.0)"
def pair_one = "private with two names"
def pair_two = "private with two names"
private :by_name # => :by_name
private [:by_array] # => [:by_array]
private :pair_one, :pair_two # => [:pair_one, :pair_two]
# The mode switch: affects every `def` that follows, up to the end of the class body.
private # => nil
def mode_one = "private from the switch"
def mode_two = "private from the switch"
public # => nil
def visible = "public again"
# Inline forms take the Symbol that `def` returns.
private def inline_private = "private, inline" # => :inline_private
protected def inline_protected = "protected, inline" # => :inline_protected
public def inline_public = "public, inline" # => :inline_public
# attr_* return the defined names (Ruby 3.0), so they compose with the modifiers.
private attr_reader :serial # => [:serial]
public attr_accessor :label # => [:label, :label=]
end
end
stamp = Shelf::Stamp
stamp.public_instance_methods(false).sort # => [:inline_public, :label, :label=, :visible]
stamp.protected_instance_methods(false) # => [:inline_protected]
private_names = stamp.private_instance_methods(false).sort
private_names.first(4) # => [:by_array, :by_name, :inline_private, :mode_one]
private_names.last(4) # => [:mode_two, :pair_one, :pair_two, :serial]
Source: Ruby 4.0 documentation: Module#private, #protected and #public
Which form to use
The Ruby Style Guide asks you to assign proper visibility levels instead of leaving everything public, to indent the
modifier as deep as the method definitions it governs, and to put a blank line before and after it. RuboCop enforces the
indentation (Layout/AccessModifierIndentation) and the blank lines (Layout/EmptyLinesAroundAccessModifier). Which
form the project uses is a style decision that Style/AccessModifierDeclarations encodes: its default
EnforcedStyle: group wants the mode switch (private on a line of its own), and allows the symbol, attribute and
alias forms by default (AllowModifiersOnSymbols, AllowModifiersOnAttrs, AllowModifiersOnAliasMethod). Setting it to
inline makes private def the norm. Both are fine; mixing them in one class is the only wrong answer. The grouped
form has the advantage of showing the public interface at the top of the file and the private tail below it, in the
"stepdown" order: public methods first, then protected, then private, each private helper below the method that calls it
first (the example of the Ruby Style Guide groups protected and private methods near the end of the class too).
The scope of the mode switch
The mode switch is lexically scoped to the class body (more exactly, to the current frame of the class definition), and it reaches less than people expect. The next example collects the cases.
# frozen_string_literal: true
module Shelf
# What the `private` mode switch does and does not reach.
class Scope
LIMIT = 6
def self.define_later(name, &) = define_method(name, &)
private
def plain_def = 1
attr_reader :reader
alias copy_of_plain plain_def
define_method(:direct_define) { 3 }
# A method that calls define_method does it from another scope: the new method is public.
define_later(:indirect_define) { 4 }
# Singleton methods defined with class << self have their own mode.
class << self
def singleton_default = 7
private
def singleton_hidden = 8
end
# A block given to Data.define has its own default visibility.
Point = Data.define(:x) do
def public_in_block = 9
private
def private_in_block = 10
end
end
# Reopening the class starts again in public mode.
class Scope
def after_reopen = 11
end
end
scope = Shelf::Scope
scope.private_instance_methods(false).sort # => [:copy_of_plain, :direct_define, :plain_def, :reader]
scope.public_instance_methods(false).sort # => [:after_reopen, :indirect_define]
scope.singleton_class.private_instance_methods(false) # => [:singleton_hidden]
scope.singleton_default # => 7
scope::Point.private_instance_methods(false) # => [:private_in_block]
scope.constants.sort # => [:LIMIT, :Point]
What the example shows, point by point:
-
defandattr_*obey the mode, andaliascopies the visibility of the original (here the alias is private becauseplain_defwas private at the time).alias_methoddoes the same. -
define_methodobeys the mode only when it is called directly in the class body (direct_defineis private). A helper method that callsdefine_method(define_later) runs in a different frame, so the method it creates is public. When you generate methods in a loop or a hook, set the visibility explicitly withprivate nameafterwards. -
A
class << selfblock starts its own scope in public mode, sosingleton_hiddenneeds its ownprivatethere. Aprivateabovedef self.namehas no effect on the class method (see the cops below). -
Constants are never affected by the mode switch (
LIMITwas defined before it, but one defined after would be public too). -
The block of
Data.define,Struct.newandClass.newis a fresh class body with its own mode. -
Reopening the class resets the mode to public: the
privatein the firstclass Scopedoes not carry over to the second.
Calling private without arguments inside a method (for example in a helper such as def self.setup; private; end)
is unreliable, and Ruby warns about it: "calling private without arguments inside a method may not have the intended
effect". Use the modifier directly in the class body, or pass names.
The mistakes RuboCop finds
Four of the commonest mistakes about visibility are caught by RuboCop’s Lint cops, which is the strongest reason to
run it. The file below contains all of them on purpose.
# frozen_string_literal: true
module Shelf
# Four mistakes about visibility, kept on purpose so that RuboCop can report them.
class Mistakes
private
def self.helper = 1
TOKEN_LENGTH = 16
def hidden = helper
public
def visible = hidden
private
protected
end
end
Running RuboCop on it prints these offences (the cop names are the part to remember):
Offenses:
e18.rb:8:5: W: Lint/IneffectiveAccessModifier: private (on line 6) does not make singleton methods private. Use private_class_method or private inside a class << self block instead.
def self.helper = 1
^^^
e18.rb:10:5: W: Lint/UselessConstantScoping: Useless private access modifier for constant scope.
TOKEN_LENGTH = 16
^^^^^^^^^^^^^^^^^
e18.rb:18:5: W: [Correctable] Lint/UselessAccessModifier: Useless private access modifier.
private
^^^^^^^
e18.rb:20:5: W: [Correctable] Lint/UselessAccessModifier: Useless protected access modifier.
protected
^^^^^^^^^
1 file inspected, 4 offenses detected, 2 offenses autocorrectable
Source: RuboCop: Lint/IneffectiveAccessModifier, Lint/UselessConstantScoping and Lint/UselessAccessModifier
The fixes: put private below the def self. methods and use private_class_method (or private inside
class << self) for the class methods; write constants above the private line and use private_constant to hide
them; delete a modifier that has nothing after it.
Changing visibility in subclasses and after the fact
Visibility is a property of the method entry, so a subclass can change what it inherits, and a private method takes part in dynamic dispatch like any other.
-
A private method is looked up and overridden like a public one. If a parent’s public
runcalls a privatefee_cents, a subclass that defines its ownfee_centsreplaces it, which is the foundation of the template-method pattern (the parent defines the skeleton, the subclasses fill in private hooks). There is nofinal. -
The same fact makes accidental overriding possible: a subclass that happens to define a method with the same name as one of the parent’s private helpers silently replaces it, and the parent’s code then calls the subclass method. Give helpers distinctive names, keep the inheritance hierarchy shallow, and prefer composition (see SOLID and Design Patterns).
-
An override is public unless it is marked otherwise, even if the parent’s method was private. Forgetting
privatein the override turns an implementation hook into public API. -
private :namein a subclass for a method inherited from a parent creates a new entry that forwards to the parent’s implementation (supersemantics) but is private.public :namedoes the opposite for a private method. In both casesinstance_method(:name).owneris the subclass, andpublic_instance_methods(false)orprivate_instance_methods(false)lists the name. -
Narrowing the visibility of an inherited public method breaks substitutability: code written against the parent type fails with
NoMethodErroron the subclass.
# frozen_string_literal: true
require "shelf"
module Shelf
# A template method: `fee_cents` is a private hook that subclasses override.
class Pricing
def quote(days) = "#{format('%<euros>.2f', euros: days * fee_cents / 100.0)} EUR"
def code = "BASE"
private
def fee_cents = 10
end
# Overriding a private hook is normal. Without `private` the override is public.
class WeekendPricing < Pricing
private
def fee_cents = 25
end
# Here the override is public, so the hook is now part of the API.
class LeakyPricing < Pricing
def fee_cents = 50
end
# Narrowing an inherited public method: callers of the parent type break.
class SealedPricing < Pricing
private :code
end
# Widening a private method: `public` copies the entry into the subclass.
class OpenPricing < Pricing
public :fee_cents
end
end
Shelf::Pricing.new.quote(3) # => "0.30 EUR"
Shelf::WeekendPricing.new.quote(3) # => "0.75 EUR"
Shelf::LeakyPricing.new.fee_cents # => 50
Shelf::OpenPricing.new.fee_cents # => 10
begin
Shelf::SealedPricing.new.code
rescue NoMethodError => e
e.message # => "private method 'code' called for an instance of Shelf::SealedPricing"
end
Shelf::Pricing.new.code # => "BASE"
Shelf::SealedPricing.instance_method(:code).owner # => Shelf::SealedPricing
Shelf::SealedPricing.private_instance_methods(false) # => [:code]
Shelf::OpenPricing.public_instance_methods(false) # => [:fee_cents]
Shelf::OpenPricing.instance_method(:fee_cents).owner # => Shelf::OpenPricing
# Module#private is itself a private method, so it needs send or class_eval from outside.
begin
Shelf::Pricing.private(:code)
rescue NoMethodError => e
e.message # => "private method 'private' called for class Shelf::Pricing"
end
Shelf::Pricing.class_eval { private :code } # => :code
Shelf::Pricing.private_method_defined?(:code) # => true
begin
Shelf::Pricing.send(:private, :nope)
rescue NameError => e
e.message # => "undefined method 'nope' for class 'Shelf::Pricing'"
end
Source: Ruby 4.0 documentation: Module#public and Module#private
Two methods remove a method instead of hiding it. undef_method :name blocks the name in this class, so a call raises
NoMethodError even if an ancestor defines it, while remove_method :name deletes only this class’s entry, so lookup
falls through to the ancestor’s definition. Use private to hide an inherited method from callers while keeping it for
super, undef_method to forbid it completely (for example in an adapter that must not expose the full API of its
parent), and remove_method to undo an override. All of them are covered together with alias_method and define_method
in Dynamic Dispatch and Method Definition.
Class-level visibility
The mode switch never affects def self.name, so class methods have their own tools:
-
private_class_method :namemakes class methods private, and accepts several names or an Array. It returns the module it was called on. -
public_class_method :namereverses it, most commonly fornew. -
A
class << selfblock withprivateinside it (orprivate definside it) hides several singleton methods at once. -
private_class_method def self.helper ... endworks, becausedefreturns a Symbol.
The classic use is the factory with a private constructor: private_class_method :new and a public factory method
(from_isbn, for_book) that calls new from inside the class, where the receiver is implicit. The standard
library’s Singleton module uses exactly this and adds instance.
# frozen_string_literal: true
require "shelf"
require "singleton"
module Shelf
# A factory with a private constructor.
class Barcode
PREFIX = "SH"
def self.for_book(book) = new("#{PREFIX}-#{book.isbn}")
private_class_method :new
attr_reader :text
def initialize(text) = @text = text
end
# Private class methods are called without a receiver from other class methods.
class Registry
def self.reset = clear_all
def self.snapshot = items.dup
class << self
private
def items = (@items ||= [])
def clear_all = items.clear
end
private_class_method def self.audit = :audited
def self.run_audit = audit
end
# The standard library's Singleton makes `new` private and adds `instance`.
class Clock
include Singleton
def now = Time.at(0).utc.year
end
class ManualBarcode < Barcode
public_class_method :new
end
end
author = Shelf::Author.new("Ursula K. Le Guin")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "The Dispossessed", author:)
Shelf::Barcode.for_book(book).text # => "SH-9780306406157"
begin
Shelf::Barcode.new("x")
rescue NoMethodError => e
e.message # => "private method 'new' called for class Shelf::Barcode"
end
Shelf::Registry.snapshot # => []
Shelf::Registry.run_audit # => :audited
Shelf::Registry.reset # => []
begin
Shelf::Registry.items
rescue NoMethodError => e
e.message # => "private method 'items' called for class Shelf::Registry"
end
Shelf::Clock.instance.equal?(Shelf::Clock.instance) # => true
begin
Shelf::Clock.new
rescue NoMethodError => e
e.message # => "private method 'new' called for class Shelf::Clock"
end
# The private `new` is inherited by subclasses until they reopen it.
Shelf::ManualBarcode.new("custom").text # => "custom"
Shelf::Barcode.singleton_class.private_instance_methods(false) # => [:new]
Shelf::Barcode.private_methods(false) # => [:new, :initialize, :inherited]
# A private `new` is a convention, not a lock: allocate, dup, clone and Marshal still make instances.
barcode = Shelf::Barcode.for_book(book)
Shelf::Barcode.allocate.text # => nil
barcode.dup.text # => "SH-9780306406157"
barcode.clone.equal?(barcode) # => false
Marshal.load(Marshal.dump(barcode)).text # => "SH-9780306406157"
Three limits apply to a private new. Subclasses inherit the privacy (the singleton classes inherit too), so each
subclass that should be constructible has to public_class_method :new itself. The privacy is a convention that stops
callers from writing Barcode.new, not a lock: allocate, dup, clone and Marshal.load still create instances
without going through the factory, and allocate skips initialize entirely. The standard Singleton module closes
those holes by also making dup and clone raise and by customising marshalling; if uniqueness really matters, use
it rather than re-implementing it. Finally, a private class method is still a regular method, so send (or a reopened singleton class) reaches it.
Constants: private_constant, public_constant and deprecate_constant
Constants have their own visibility, set per name with private_constant, public_constant and deprecate_constant.
The private keyword, the mode switch and private_class_method never apply to constants, which is why RuboCop’s
Lint/UselessConstantScoping flags a constant written below a private line.
-
A private constant can be referenced by its bare name from inside the class, its subclasses and nested classes (the lexical and inheritance lookup works), but not with a scope:
Limits::SECRET_SALTraisesNameError("private constant Shelf::Limits::SECRET_SALT referenced"). That is the mechanism for hiding helper classes and internal tables inside a namespace. -
constantsandconst_defined?-style listings behave accordingly:constantsomits private constants, andconst_getignores the restriction, which is the reflective back door. -
public_constantreversesprivate_constant. Both return the module in Ruby 4.0, so they can be chained, but their return value is rarely used. -
deprecate_constantkeeps a constant working and prints a warning when it is referenced. The warning belongs to the:deprecatedcategory, which has been off by default since Ruby 2.7.2, so it appears only with-w,-W:deprecatedorWarning[:deprecated] = true. It is the supported way to retire a public constant over several releases.
# frozen_string_literal: true
module Shelf
# Constants have their own visibility: private_constant, public_constant, deprecate_constant.
class Limits
DEFAULT_DAYS = 14
MAX_RENEWALS = 3
SECRET_SALT = "not-a-real-salt"
OLD_DAYS = 21
class Helper
def salt = SECRET_SALT
end
private_constant :SECRET_SALT, :Helper
deprecate_constant :OLD_DAYS
def self.salt_length = SECRET_SALT.length
def self.helper = Helper.new
def describe = "#{DEFAULT_DAYS} days, #{MAX_RENEWALS} renewals, salt #{SECRET_SALT.length}"
end
class StrictLimits < Limits
def bare_reference = SECRET_SALT.length
end
end
Shelf::Limits.new.describe # => "14 days, 3 renewals, salt 15"
Shelf::Limits.salt_length # => 15
Shelf::StrictLimits.new.bare_reference # => 15
Shelf::Limits.helper.salt # => "not-a-real-salt"
Shelf::Limits.constants.sort # => [:DEFAULT_DAYS, :MAX_RENEWALS, :OLD_DAYS]
begin
Shelf::Limits::SECRET_SALT
rescue NameError => e
[e.class, e.name, e.message] # => [NameError, :SECRET_SALT, "private constant Shelf::Limits::SECRET_SALT referenced"]
end
begin
Shelf::StrictLimits::SECRET_SALT
rescue NameError => e
e.message # => "private constant Shelf::Limits::SECRET_SALT referenced"
end
Shelf::Limits.const_get(:SECRET_SALT) # => "not-a-real-salt"
Shelf::Limits.const_defined?(:SECRET_SALT) # => true
Shelf::Limits.private_constant(:DEFAULT_DAYS) # => Shelf::Limits
Shelf::Limits.public_constant(:SECRET_SALT) # => Shelf::Limits
Shelf::Limits::SECRET_SALT.length # => 15
# Deprecated constants warn only when the :deprecated category is on (it is off by default).
Warning[:deprecated] = true
Shelf::Limits::OLD_DAYS.succ # => 22
# stderr: e10.rb:54: warning: constant Shelf::Limits::OLD_DAYS is deprecated
The first handler’s output shows the three fields of the NameError: the class, the name of the constant and the
message. Notice StrictLimits::SECRET_SALT fails too: a subclass reaches the constant only through its bare name inside
its own body. When a constant is both private and referenced from outside the class by design, the real problem is that
the constant belongs elsewhere.
module_function and extend self
A module is a bag of methods, and a bag of utility functions (no state, no instances) is the case where visibility
needs special handling. module_function turns the methods it is applied to into module functions: a public
singleton method on the module (Slug.for_title) and, at the same time, a private instance method (so a class that
includes the module can call for_title without a receiver but does not expose it to its own callers). Like the other
modifiers it has the mode form (everything below it), and the form with names (module_function :dashes), and it
returns its arguments by the rules of Ruby 3.1. It exists only on modules, not on classes (Class undefines it).
# frozen_string_literal: true
require "shelf"
module Shelf
# module_function: every function is a public singleton method and a private instance method.
module Slug
module_function
def for_title(title) = squeeze(title.downcase.gsub(/[^a-z0-9]+/, "-"))
def squeeze(text) = text.gsub(/\A-+|-+\z/, "")
end
# The same module with explicit names, chosen after the definitions.
module Dashes
def dashes(text) = text.tr(" ", "-")
def internal = :not_a_function
module_function :dashes # => :dashes
end
# extend self: the methods stay public instance methods, and are also singleton methods.
module Plain
extend self # rubocop:disable Style/ModuleFunction
def shout(text) = "#{text.upcase}!"
end
class Label
include Slug
def initialize(book) = @book = book
def to_s = for_title(@book.title)
end
end
author = Shelf::Author.new("Italo Calvino")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "If on a Winter's Night", author:)
Shelf::Slug.for_title("The Left Hand of Darkness") # => "the-left-hand-of-darkness"
Shelf::Label.new(book).to_s # => "if-on-a-winter-s-night"
Shelf::Slug.singleton_methods.sort # => [:for_title, :squeeze]
Shelf::Slug.private_instance_methods(false).sort # => [:for_title, :squeeze]
Shelf::Dashes.singleton_methods # => [:dashes]
Shelf::Dashes.public_instance_methods(false) # => [:internal]
Shelf::Plain.shout("hey") # => "HEY!"
Shelf::Plain.instance_method(:shout).owner # => Shelf::Plain
begin
Shelf::Label.new(book).for_title("x")
rescue NoMethodError => e
e.message # => "private method 'for_title' called for an instance of Shelf::Label"
end
# The built-in modules use the same pattern: Math.sqrt is a singleton method, sqrt a private instance method.
Math.sqrt(16) # => 4.0
Math.private_method_defined?(:sqrt) # => true
ratio = 3.14159
Kernel.format("%05.1f", ratio) # => "003.1"
Kernel.respond_to?(:format) # => true
# module_function copies the method: the two sides are different method objects.
Shelf::Slug.method(:squeeze).owner # => #<Class:Shelf::Slug>
Shelf::Slug.instance_method(:squeeze).owner # => Shelf::Slug
The two sides are copies: module_function defines the singleton method as a copy of the instance method at that moment,
so the instance method and the singleton method are different method objects with different owners. Redefine one and the
other does not change. Math and Kernel are built this way, which is why you can write both Math.sqrt(16) and, after
include Math, a bare sqrt(16), and why Kernel.format works next to the private format that every object has.
| Technique | Singleton (module-level) call | Instance method visibility | Effect of include |
Notes |
|---|---|---|---|---|
|
Public copy |
Private |
Functions callable without receiver, hidden from the includer’s callers |
The default recommendation of RuboCop ( |
|
The same method object |
Public |
Methods become public API of the includer |
One definition, so changes apply to both sides. RuboCop flags it by default. |
|
Public |
None |
Nothing: not part of an includer |
Plain namespacing; not reusable by mixing in. |
|
Public |
None |
Nothing |
The place for |
The mixed-in private methods of a module follow the same private rule as everything else. A module included into a class can define private helper methods, the class calls them without a receiver, and nobody else can. This is the cleanest way to share an implementation detail between classes without enlarging the public interface of either. The cost is the usual mixin one: two modules that define the same private helper name collide silently, and the one included later wins (see Method Lookup and super).
Top-level methods and Kernel
A method defined with def at the top level of a script or in the irb console is not a function in the sense of
other languages. It becomes a private instance method of Object, and since every class inherits from Object,
every object can call it without a receiver. That is why banner("x") works from inside a method of a class that
knows nothing about the script, and why "text".banner("x") is a NoMethodError: the method is private.
# frozen_string_literal: true
# A method defined at the top level becomes a private instance method of Object.
def banner(text) = "== #{text} =="
banner("Shelf") # => "== Shelf =="
Object.private_method_defined?(:banner) # => true
begin
"text".banner("x")
rescue NoMethodError => e
e.message # => "private method 'banner' called for an instance of String"
end
# Every object can call it implicitly, because Object is an ancestor of every class.
class Report
def title = banner("Report")
end
Report.new.title # => "== Report =="
# `public` and `private` also exist at the top level (as singleton methods of main) and make the next def public.
public def loud(text) = text.upcase
"quiet".loud("hey") # => "HEY"
to_s # => "main"
Object.public_method_defined?(:loud) # => true
Source: Ruby 4.0 documentation: Module#public and Kernel
At the top level, self is a special object called main, which has its own public and private singleton methods
(since Ruby 3.0 they also accept an Array of names). public def loud made the method callable on any receiver, so
"quiet".loud("hey") worked: you have added a method to every object in the process, the most extreme form of monkey
patching. The Ruby Style Guide’s advice for "global" helper methods is the opposite: if you really need them, add them to
Kernel and keep them private, or better, put them in a module as module functions and include that module only where
needed. A script with a handful of top-level helpers is fine; a library must never define any, because they leak into
every object of the application that loads it.
Visibility is advisory: the ways round the rules
Ruby trusts the programmer: visibility is a communication tool rather than a lock, and send exists to call a method
whatever its visibility. Every one of the following reaches a private method, a protected method, a private constant or an instance variable.
| Mechanism | What it reaches | Honours visibility? |
|---|---|---|
|
Private and protected methods |
No |
|
Public methods only (raises for private and protected) |
Yes |
|
Any method found in the ancestors |
No ( |
|
Instance variables |
Not applicable: variables have no visibility |
|
Runs the block with the object as |
No |
|
Runs the block in the class body: change visibility, define methods |
No |
|
Private constants |
No (the scoped |
|
Instances built without calling a private |
No |
# frozen_string_literal: true
module Shelf
# Something with a private method, a private constant and a hidden instance variable.
class Vault
PIN = "0000-not-a-real-pin"
private_constant :PIN
def initialize = @opened = 0
attr_reader :opened
private
def open_door(who = "guest")
@opened += 1
"opened by #{who}"
end
end
end
vault = Shelf::Vault.new
name = :open_door
# Reflection is the first way round the rule: send ignores visibility, public_send honours it.
vault.send(name, "ana") # => "opened by ana"
vault.__send__(name) # => "opened by guest"
begin
vault.public_send(name)
rescue NoMethodError => e
e.message # => "private method 'open_door' called for an instance of Shelf::Vault"
end
# A Method object remembers its receiver and calls without any visibility check.
vault.method(name).call("ben") # => "opened by ben"
Shelf::Vault.instance_method(name).bind_call(vault) # => "opened by guest"
begin
vault.public_method(name)
rescue NameError => e
e.message # => "method 'open_door' for class 'Shelf::Vault' is private"
end
# Instance variables are reachable by reflection, or by running code with the object as self.
vault.instance_variable_get(:@opened) # => 4
vault.instance_variable_set(:@opened, 100) # => 100
vault.instance_eval { @opened } # => 100
vault.instance_eval { open_door("cara") } # => "opened by cara"
vault.opened # => 101
vault.instance_variables # => [:@opened]
# Reopening the class and class_eval go through the same door.
Shelf::Vault.class_eval { public :open_door }
vault.open_door("dan") # => "opened by dan"
# Constants: const_get ignores private_constant.
Shelf::Vault.const_get(:PIN).length # => 19
When going round the rule is legitimate
-
Tests and debugging. A debugger or an IRB session needs to look inside. For tests, prefer to exercise the public interface (see the design section below); reach a private method with
sendonly as a last resort and never for protected state that the public interface already exposes. -
Frameworks and metaprogramming. Serialization, dependency injection and DSL libraries use
instance_variable_get,instance_evalandsendas their building blocks (see Hooks, eval and DSLs). -
Dynamic dispatch on a method name you control:
sendover a fixed table of known names is fine.
When it is dangerous
send and public_send on a user-supplied name are an injection point. With send, a request parameter such as
"exit", "system" or "instance_eval" reaches private Kernel methods; even public_send exposes every public method
of the receiver (delete, destroy, freeze). Always check the name against an allow-list first (see
Security). The Ruby Style Guide and RuboCop’s Style/Send cop (disabled
by default) prefer public_send precisely because it does not circumvent private and protected, and because a class
that defines its own method named send (a mail-sending object, say) breaks plain send; use __send__ in that case.
In short: treat the rule as a contract with the reader of the code. A caller who uses send(:internal) takes
responsibility for breakage in the next release.
Reflection: asking about visibility
Visibility can be queried per object and per class. The reflection methods are the tools that serialisers, test doubles and documentation generators use, and the table below lists what each includes.
# frozen_string_literal: true
require "shelf"
module Shelf
# One method at each visibility, to compare the reflection API.
class Probe
def pub = :pub
protected def prot = :prot
private def priv = :priv
end
end
probe = Shelf::Probe.new
own = ->(list) { list & %i[pub prot priv] }
own.call(probe.methods) # => [:pub, :prot]
own.call(probe.public_methods) # => [:pub]
own.call(probe.protected_methods) # => [:prot]
own.call(probe.private_methods) # => [:priv]
own.call(probe.singleton_methods) # => []
Shelf::Probe.instance_methods(false) # => [:pub, :prot]
Shelf::Probe.public_instance_methods(false) # => [:pub]
Shelf::Probe.protected_instance_methods(false) # => [:prot]
Shelf::Probe.private_instance_methods(false) # => [:priv]
%i[pub prot priv].map { |name| probe.respond_to?(name) } # => [true, false, false]
%i[pub prot priv].map { |name| probe.respond_to?(name, true) } # => [true, true, true]
Shelf::Probe.method_defined?(:prot) # => true
Shelf::Probe.method_defined?(:priv) # => false
Shelf::Probe.public_method_defined?(:pub) # => true
Shelf::Probe.protected_method_defined?(:prot) # => true
Shelf::Probe.private_method_defined?(:priv) # => true
Shelf::Probe.private_method_defined?(:puts) # => true
Shelf::Probe.private_method_defined?(:puts, false) # => false
Shelf::Probe.instance_method(:priv).name # => :priv
probe.public_method(:pub).call # => :pub
Shelf::Probe.public_instance_method(:pub).owner # => Shelf::Probe
| Query | Answers |
|---|---|
|
Public and protected methods ( |
|
Public methods only |
|
Protected methods only |
|
Private methods only (without |
|
Public and protected singleton methods |
|
|
|
Public or protected. A second argument |
|
One visibility each; same optional |
|
Any visibility, returning an |
|
Public only; |
Method#public?, private? and protected? were added in Ruby 3.1 and removed again in Ruby 3.2, so do not use
them; ask the owner (Klass.private_method_defined?(name)) instead.
Real information hiding
If the goal is to prevent access rather than to signal intent, private is the wrong tool. Four techniques give
stronger guarantees:
-
Closures. A variable captured by a lambda is not an instance variable and cannot be listed by
instance_variables. TheCounterbelow keeps its count in a local variable of the factory;instance_variableslists only the@stepthat holds the lambda, and the only way to change the count is to call it. -
Immutability. Freezing (
freeze) stops mutation but not reading.Data.defineobjects are frozen and expose only their readers (see Object Lifecycle, Freezing and Copying). -
Isolation. Ractors isolate objects between execution contexts, so a non-shareable object cannot be referenced directly from another Ractor (see Ractors). Process boundaries are stronger still.
-
Controlling what is printed.
inspect,pp, error messages and logs print every instance variable, which is the usual way a token ends up in a log file. Ruby 4.0 letsObject#inspectask a method namedinstance_variables_to_inspectwhich instance variables to show; define it private and return an Array of names (ornilfor the default).
# frozen_string_literal: true
require "shelf"
module Shelf
# Plain object: inspect, pp and error messages print every instance variable.
class Session
def initialize(user, token)
@user = user
@token = token
end
end
# Ruby 4.0: a private instance_variables_to_inspect hook chooses what inspect shows.
class QuietSession
def initialize(user, token)
@user = user
@token = token
end
private
def instance_variables_to_inspect = [:@user]
end
# State kept only in a closure: there is no instance variable to reach.
class Counter
def self.build
count = 0
new(-> { count += 1 })
end
def initialize(step) = @step = step
def next = @step.call
end
end
token = "fake-token"
open = Shelf::Session.new("ana", token)
open.inspect.sub(/0x\h+/, "0x0") # => "#<Shelf::Session:0x0 @user=\"ana\", @token=\"fake-token\">"
quiet = Shelf::QuietSession.new("ana", token)
quiet.inspect.sub(/0x\h+/, "0x0") # => "#<Shelf::QuietSession:0x0 @user=\"ana\">"
counter = Shelf::Counter.build
[counter.next, counter.next] # => [1, 2]
counter.instance_variables # => [:@step]
# Freezing protects against mutation, not against reading.
session = Shelf::Session.new("ben", token).freeze
session.frozen? # => true
session.instance_variable_get(:@user) # => "ben"
begin
session.instance_variable_set(:@user, "eve")
rescue FrozenError => e
e.message.include?("fake-token") # => true
end
begin
quiet.freeze.instance_variable_set(:@user, "eve")
rescue FrozenError => e
e.message.include?("fake-token") # => false
end
The last two handlers show the other lesson: a FrozenError message is built with inspect, so a frozen object with a
secret prints it in the exception message (and from there into logs and error trackers). Defining
instance_variables_to_inspect fixes both the direct inspect and the message. The @token values in the example
are made-up placeholders; real credentials belong in the environment or a secrets store, never in an object that gets
inspected or logged (see Security).
Designing the public surface
The mechanics above support a few design habits. They follow the guidance of the object-oriented chapters of the books listed on the section landing page, restated for current Ruby.
-
Start narrow. Make methods private by default and widen them when a caller needs them. The direction matters: turning a private method public never breaks anyone, while turning a public method private breaks every caller. A useful trick when refactoring: make a suspicious public method private and run the tests. The
NoMethodErrorlist is the exact set of external callers. -
Hide the variables, then hide the objects. Expose state with
attr_reader, notattr_accessor, unless callers really set it, and make setters private when only the class mutates the field. Return copies or frozen values for mutable collections (booksreturned@books.dup). -
Tell, do not ask. An object that exposes many readers invites callers to make decisions with its data. Put the behaviour that needs the data inside the object, and the callers need fewer public methods.
-
Order the file by call depth ("stepdown"): the public interface first, each private method below the first public method that uses it, so a reader goes from the contract down to the details.
-
Keep constructors passive.
initializeshould assign state and validate it, not read files or call services. Factory class methods (Isbn.parse,Barcode.for_book) with a privatenewmake the construction path explicit. -
Test through the public interface. If a private method seems to need its own tests, it is usually a class waiting to be extracted, with its own public interface (the refactoring approach of breaking up a large class in small, tested steps).
-
Do not use
protectedas a default for "private but visible to subclasses". In Ruby a private method is already visible to subclasses.protectedis for other instances. -
Prefer composition over deep inheritance when the private protocol between parent and child grows: every private method a subclass calls or overrides is coupling.
-
Document the boundary. YARD’s
@api privatetag and the# @!visibility privatedirective, and RDoc’s:nodoc:, keep internals out of the generated documentation (see Documentation with RDoc and YARD).
A narrow public interface in the shelf scenario looks like this. The LoanDesk has three public methods; every step of the
checkout is a private method defined below the public one that calls it.
# frozen_string_literal: true
require "shelf"
module Shelf
# A narrow public interface (checkout, receipt) over private steps, in stepdown order:
# each private method sits below the first method that calls it.
class LoanDesk
Receipt = Data.define(:title, :due_day, :fee_cents)
attr_reader :catalog
def initialize(catalog: Catalog.new, today: 1)
@catalog = catalog
@today = today
@lent = {}
end
def checkout(book, member: "guest")
ensure_available(book)
record_loan(book, member)
receipt_for(book)
end
def lent?(book) = @lent.key?(book.isbn)
private
def ensure_available(book)
raise Shelf::Error, "#{book.title} is already lent" if lent?(book)
end
def record_loan(book, member)
@catalog.add(book)
@lent[book.isbn] = member
end
def receipt_for(book) = Receipt.new(title: book.title, due_day: due_date, fee_cents: daily_fee_cents)
def due_date = @today + Shelf.config.loan_days
def daily_fee_cents = 10
end
end
author = Shelf::Author.new("Ursula K. Le Guin")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "The Dispossessed", author:)
desk = Shelf::LoanDesk.new
receipt = desk.checkout(book, member: "ana")
receipt.to_h # => {title: "The Dispossessed", due_day: 15, fee_cents: 10}
desk.lent?(book) # => true
desk.public_methods(false).sort # => [:catalog, :checkout, :lent?]
private_names = desk.private_methods(false).sort
private_names.first(3) # => [:daily_fee_cents, :due_date, :ensure_available]
private_names.last(3) # => [:initialize, :receipt_for, :record_loan]
begin
desk.checkout(book)
rescue Shelf::Error => e
e.message # => "The Dispossessed is already lent"
end
initialize shows up in private_methods(false), as the section on the automatically private hooks explained. The Receipt data class is a
public nested constant: it is part of the interface, because callers receive and read it.
Retiring a public method
Making a published method private is a breaking change. Libraries that follow semantic versioning do it in two releases:
first keep the method public and make it warn, then make it private in the next major release. Kernel#warn takes
uplevel: (the caller’s file and line) and category: :deprecated, so the message points at the code that must change
and is silent unless deprecation warnings are on.
# frozen_string_literal: true
require "shelf"
require "stringio"
module Shelf
# Release 1.1: the old public method still works but warns. Release 2.0: it becomes private.
class Inventory
def initialize(books) = @books = books
def total = @books.size
def book_count
warn "Inventory#book_count is deprecated, use #total", uplevel: 1, category: :deprecated
total
end
end
class NextInventory < Inventory
private :book_count
end
end
def capture_stderr
original = $stderr
$stderr = StringIO.new
yield
$stderr.string
ensure
$stderr = original
end
Warning[:deprecated] = true
inventory = Shelf::Inventory.new(%w[a b c])
capture_stderr { inventory.book_count } # => "e17.rb:36: warning: Inventory#book_count is deprecated, use #total\n"
Warning[:deprecated] = false
capture_stderr { inventory.book_count } # => ""
begin
Shelf::NextInventory.new(%w[a]).book_count
rescue NoMethodError => e
e.message # => "private method 'book_count' called for an instance of Shelf::NextInventory"
end
Source: Ruby 4.0 documentation: Kernel#warn and Warning
The second version of the class is the 2.0 release: private :book_count hides it, and the former callers now get
NoMethodError. For constants the same job is done by deprecate_constant (see
Constants: private_constant, public_constant and deprecate_constant); see
Creating and Publishing Gems, and Rake for
version numbering.
RuboCop cops for visibility
| Cop | What it checks | Default |
|---|---|---|
|
|
Enabled |
|
A blank line before and after the modifier |
Enabled |
|
Mode switch ( |
Enabled, |
|
A modifier that governs no method |
Enabled |
|
|
Enabled |
|
A constant written under |
Pending (enabled with |
|
|
Enabled |
|
|
Enabled, |
|
|
Disabled |
Where the books are outdated
Much of the printed advice on visibility was written for Ruby 1.9 to 2.x and is still repeated. The current rules:
-
"A private method can never be called with a receiver." The rule changed in Ruby 2.7: the literal
selfis allowed as the receiver for any private method. Before that only setters could use it. Books that warn againstself.methodfor private methods are describing Ruby 2.6 and older. -
Return values of the modifiers. Before Ruby 3.0
attr_readerreturnednil, so the inline formprivate attr_reader :namecould not work; before 3.1private,public,protectedandmodule_functionreturnednil. Today they return their arguments, so the inline forms are idiomatic. -
Array arguments (
private [:a, :b]) arrived in Ruby 3.0, and theprivate defidiom needs Ruby 2.1 or later. -
Error messages. The wording of
NoMethodErrorchanged twice, and tests that match the text must follow:Ruby 3.2 and older: private method `x' called for #<Foo:0x000071c4> Ruby 3.3: private method `x' called for an instance of Foo Ruby 3.4 and later: private method 'x' called for an instance of FooUntil 3.2 the message printed the receiver’s
inspect, which was slow and could leak data. Preferassert_raises(NoMethodError)and checkerror.nameinstead of matching the message. -
sendas the universal tool. Older tutorials callsendfor everything. Todaypublic_sendis the default for dynamic dispatch on public methods, andsendor__send__is reserved for the cases that intend to bypass visibility. -
protectedeverywhere. Some books recommend protected readers for all "internal but shared" state. Value objects built withDataorStructmake that unnecessary. -
Hiding constants by convention.
private_constanthas existed since Ruby 1.9.3; leading underscores or comments are no longer needed. -
Method#public?was added in 3.1 and removed in 3.2, so articles that use it are wrong for current Ruby. -
inspectalways shows everything. True before Ruby 4.0. Nowinstance_variables_to_inspectcontrols it.
Version notes
| Ruby | Change |
|---|---|
2.1 |
|
2.5 |
|
2.7 |
Calling a private method with a literal |
3.0 |
|
3.1 |
|
3.2 |
|
3.3 |
|
3.4 |
Error messages quote method names with an apostrophe instead of a backtick. |
4.0 |
|
Common errors
| Message | Cause | Fix |
|---|---|---|
private method 'x' called for an instance of Foo |
Explicit receiver on a private method |
Call it without receiver or with literal |
protected method 'x' called for an instance of Foo |
Caller is not an instance of the method’s owner |
Compare through public readers, or call from a method of the owner |
undefined local variable or method 'x' for an instance of Foo |
Local-variable-looking call to a missing method |
Check the name; a private setter needs |
private constant Foo::BAR referenced |
|
Add a public accessor, or use the bare name inside the class |
private method 'new' called for class Foo |
|
Call the factory method |
|
|
Define the method first |
References
-
Ruby 4.0 documentation: Modules and Classes (Visibility)
-
Module (
public,protected,private,module_function,private_class_method,public_class_method,private_constant,public_constant,deprecate_constant,private_instance_methods,private_method_defined?) -
Object (
send,public_send,respond_to?,methods,inspect,instance_variable_get), BasicObject (__send__,instance_eval,method_missing) and NoMethodError -
Ruby 2.7.0 release notes, 3.0.0, 3.1.0, 3.2.0, 3.3.0, 3.4.0 and 4.0.0
-
Ruby Style Guide: Leverage Access Modifiers, Indent access modifiers, module_function and Prefer public_send
-
RuboCop Style/AccessModifierDeclarations, Lint/UselessAccessModifier, Lint/IneffectiveAccessModifier and Layout/AccessModifierIndentation