Lesson 04 / 09
Visibility
Public, protected and private methods; refusal of a call from outside, the difference between send and public_send, the self receiver, a protected method working on another object of the same class, and changing visibility in a subclass.
Published Updated
In this lesson
- Measure that a private method raises NoMethodError when called from outside and can be called from inside the object
- Show the difference between send and public_send on a private method
- Separate the behavior of a protected and a private method on another object
- Measure that changing the visibility of a method to public in a subclass changes the owner of the definition, not the author
Contents
The previous lesson ended by leaving a fact: the call account.fee is refused from outside, yet the net method can call it from inside the same object. The method is the same, the author is the same, but one call is accepted and the other refused. Does the difference lie in the method itself or in whoever makes the call?
The author of this lesson is the programmer: most of the measured methods are written by the classes in their own body with def; accessors that are not written are handled separately in the bounding measurement. The lesson measures a second property that the author does not change: the visibility of a method. In M11/K03 we saw through packages to whom methods are open; here the same question is asked inside a single class.
A Private Method Is Not Called from Outside
Ruby has three visibilities: public, protected and private. A public method is open to everyone. A private method, on the other hand, can be called only from inside the object itself, without an explicit receiver written. The private keyword makes the methods defined after it private.
# author.rb — a helper that classifies who wrote a method
# Classification asks in order: is there a definition in the receiver's chain at all,
# then does the receiver's own class carry it, then how was the definition produced.
def author(receiver, name)
chain = receiver.singleton_class
unless chain.method_defined?(name) || chain.private_method_defined?(name)
return ["nobody", "no definition"]
end
definition = chain.instance_method(name)
owner = definition.owner
place = owner == chain ? "singleton class" : owner.to_s
return ["ancestor", place] unless owner == receiver.class || owner == chain
iseq = RubyVM::InstructionSequence.of(definition)
return ["code", "#{place}, no ruby body"] if iseq.nil?
return ["code", "#{place}, block body"] if iseq.label.start_with?("block")
["programmer", "#{place}, def body"]
end
# private_fee.rb — a private method is refused from outside
require_relative "author"
class Account
def initialize(owner, balance)
@owner = owner
@balance = balance
end
def net = @balance - fee
private
def fee = 2
end
account = Account.new("amy", 100)
p account.net
p author(account, :fee)
begin
account.fee
rescue NoMethodError => e
puts e.class
end
p account.send(:fee)
begin
account.public_send(:fee)
rescue NoMethodError => e
puts e.class
end
p Account.private_method_defined?(:fee)
p Account.public_method_defined?(:fee)
98 ["programmer", "Account, def body"] NoMethodError 2 NoMethodError true false
account.net gave 98: the call from inside found fee. author gave the programmer author, so being private did not change the author. account.fee from outside, however, raised NoMethodError. Calling the same method with send worked and gave 2, whereas public_send was refused. send is the call that ignores visibility, and public_send is the one that enforces it. The last two lines ask visibility at the level of the method definition: fee is registered as private, not as public.
CM9. In this lesson “visibility” is the three-valued property that decides who may call a method: public, protected, private. Visibility does not change the author; the author says where the definition is, and visibility says whether the call is accepted.
The self Receiver and a Private Method
The rule of a private method is a receiver rule: a call with an explicit receiver is refused. When the explicit receiver is self, this rule bends. The file below calls the same method in three forms.
# self_receiver.rb — a self receiver can call a private method
class Account
def initialize(balance)
@balance = balance
end
def bare = fee
def with_self = self.fee
def with_other(other) = other.fee
private
def fee = 2
end
a = Account.new(100)
b = Account.new(50)
p a.bare
p a.with_self
begin
a.with_other(b)
rescue NoMethodError => e
puts e.class
end
2 2 NoMethodError
The call without a receiver and self.fee worked, and the call made through another Account object was refused. A private method belongs not to a class but to an object: even when it is of the same class, another object cannot reach its private method.
A Protected Method
A method that compares two accounts needs the balance of the other side but may not want to open the balance to everyone. A protected method serves exactly this purpose: it cannot be called from outside, but another object of the same class can call it.
# protected.rb — a protected method is open to objects of the same class
class Jar
def initialize(amount)
@amount = amount
end
def greater?(other) = amount > other.amount
def secret_greater?(other) = secret > other.secret
protected
def amount = @amount
private
def secret = @amount
end
a = Jar.new(10)
b = Jar.new(7)
p a.greater?(b)
begin
a.amount
rescue NoMethodError => e
puts e.class
end
begin
a.secret_greater?(b)
rescue NoMethodError => e
puts e.class
end
p Jar.protected_method_defined?(:amount)
true NoMethodError NoMethodError true
Inside greater? the call other.amount worked: a protected method is accepted on another object as long as the calling object is of the same class. a.amount from outside was refused. The similarly built secret_greater? raised NoMethodError on the line other.secret, because a private method never accepts an explicit receiver. The limit of a protected method is the class, the limit of a private method is the object.
What exactly does the “same class” criterion of a protected method cover? The file below changes the caller to a superclass, a subclass and an unrelated class.
# protected_lineage.rb — the limit of a protected method is the lineage of its owner
class Jar
def initialize(amount)
@amount = amount
end
def greater?(other) = amount > other.amount
protected
def amount = @amount
end
class Gold < Jar
end
class Stranger
def ask(jar) = jar.amount
end
p Jar.new(10).greater?(Gold.new(4))
p Gold.new(4).greater?(Jar.new(10))
begin
Stranger.new.ask(Jar.new(1))
rescue NoMethodError => e
puts e.class
end
true false NoMethodError
A superclass object could call the protected method of a subclass object, and a subclass object that of a superclass object (the results are true and false: 10 > 4 and 4 > 10): the criterion is that the caller descends from the class that owns the method. Stranger is not of this lineage, so the same call stayed closed to it. The limit of a protected method is therefore not a narrow match on a class name but the inheritance tree.
CM10. In this lesson “public” is a method open to everyone, “protected” is open to objects of the same class, and “private” is open only to a call without a receiver or with a self receiver. A call from outside with an explicit receiver is refused in every case except public.
private Returns a Value
private is not only a declaration; placed in front of a method definition, it makes that definition private too. The def expression returns the name of the method as a symbol, and private takes that symbol.
# private_def.rb — private def makes a single method private class Access private def only_this = 1 def plain = 2 end access = Access.new p access.plain p Access.private_instance_methods(false) p Access.public_instance_methods(false) begin access.only_this rescue NoMethodError => e puts e.class end
2 [:only_this] [:plain] NoMethodError
private def only_this = 1 made only that method private; the plain after it stayed public. Visibility can thus be written not only for a stretch of lines but for a single method.
To see the value that private takes directly, it is enough to print the return of the def expression and the return of the private call itself. The file below measures both.
# private_value.rb — def returns a symbol, private returns what it received class Access p(def first = 1) p(private def second = 2) p(private(:first)) end
:first :second :first
The def expression returned the name of the method as a symbol, and private def gave back the symbol it received unchanged. private(:first) also returned the name it took. That is why the form private def only_this = 1 works: the inner expression produces a name, and the outer call takes that name and changes its visibility. The same contract shows that changing visibility afterwards is also a call of the form private :name; the place of the definition does not change, only its record does.
Query Methods and Inheritance
Visibility appears in two more places. The first is the respond_to? question: whether an object responds depends on whether the call would be accepted from outside. The second is inheritance: does a private method pass to a subclass? The file below measures both.
# query_and_inheritance.rb — respond_to? and a call from a subclass
class Account
def initialize(owner, balance)
@owner = owner
@balance = balance
end
private
def fee = 2
end
class TermDeposit < Account
def net_term = @balance - fee * 2
def from_outside(other) = other.fee
end
v = TermDeposit.new("tom", 50)
p v.respond_to?(:fee)
p v.respond_to?(:fee, true)
p v.net_term
begin
v.from_outside(TermDeposit.new("a", 1))
rescue NoMethodError => e
puts e.class
end
p TermDeposit.private_method_defined?(:fee)
false true 46 NoMethodError true
respond_to?(:fee) gave false, whereas respond_to?(:fee, true) gave true: the second argument includes private methods in the question. TermDeposit could call fee without a receiver in its own body, and net_term gave 46; a private method passes to the subclass, because the rule looks at the form of the call, not at the owner of the definition. But a call with an explicit receiver such as other.fee was refused even inside the subclass. TermDeposit.private_method_defined?(:fee) is true: visibility too is carried along the chain.
A Private Setter and a Private Class Method
The bending of the receiver rule with self is most useful in setter methods: the form value = 5 defines a local variable and does not call a method; to call the writer, self.value = 5 is needed. The file below calls a private writer this way. The second part measures the visibility of class methods.
# writer_and_class.rb — a self setter and private_class_method
class Counter
def initialize
@value = 0
end
def increment
self.value = value + 1
value
end
private
attr_accessor :value
end
class Gadget
def self.build = new
private_class_method :new
end
s = Counter.new
p s.increment
p s.increment
begin
s.value = 9
rescue NoMethodError => e
puts e.class
end
p Gadget.build.class
begin
Gadget.new
rescue NoMethodError => e
puts e.class
end
1 2 NoMethodError Gadget NoMethodError
increment called the private value= setter in the form self.value = ..., and the counter became 1, then 2; s.value = 9 from outside was refused. For class methods the same rule is applied with private_class_method: Gadget.new was refused, but Gadget.build produced the object because it called new without a receiver from inside the class itself. The only door to producing an object thus becomes a public class method.
The Bounding Measurement
Visibility did not change the author. But when a generator such as attr_reader produces a private accessor, who is the author? And if a subclass makes a private method of its superclass public, whose does the method become?
# bounding.rb — private attr_reader and public in a subclass
require_relative "author"
class Jar
def initialize(amount)
@amount = amount
end
private attr_reader :amount
end
class Account
def initialize(owner, balance)
@owner = owner
@balance = balance
end
private
def fee = 2
end
class Account3 < Account
public :fee
end
p author(Jar.new(3), :amount)
h = Account3.new("amy", 100)
p h.fee
p h.method(:fee).owner
p author(h, :fee)
p Account.new("a", 1).respond_to?(:fee)
["code", "Jar, no ruby body"] 2 Account3 ["programmer", "Account3, def body"] false
Although the accessor produced with private attr_reader :amount is private, author gave the code author: the definition was produced by the attr_reader call and not by def, and in Ruby it has no body. In Ruby 4.0.7, public :fee in Account3 changed two results: h.fee now gave 2 and the owner of the method came out as Account3, not Account. This result is understood by looking at what the visibility call does: the definition of the superclass stays as it is, and public :fee puts a record of the same name in the subclass. author counts the first record found in the chain, that record stands in Account3, so the programmer and Account3, def body came out. The superclass’s own object kept answering false to the respond_to? question: changing visibility affects only the subclass. The measurement below tests the correctness of this explanation.
# bounding_why.rb — public :name puts a record in the subclass
class Account
def initialize(owner, balance)
@owner = owner
@balance = balance
end
private
def fee = 2
end
class Account3 < Account
public :fee
end
h = Account3.new("amy", 100)
p Account3.instance_methods(false)
p Account.instance_method(:fee).owner
p Account3.instance_method(:fee).owner
p h.method(:fee).super_method.nil?
p Account.private_instance_methods(false)
p Account.public_method_defined?(:fee)
[:fee] Account Account3 true [:fee, :initialize] false
Account3.instance_methods(false) listed fee: a record was really added to the subclass, although there is not a single line of definition in its body. The definition of Account stayed owned by Account, Account.private_instance_methods(false) still listed it as private (the initialize in the list is a private method of every class), and Account.public_method_defined? gave false. The super_method of the record in the subclass came out nil: Ruby 4.0.7 holds this record not as a separate body but as a visibility record that looks at the definition of the superclass, and it does not count it as a second link in the chain. Visibility is not a property of the definition; it is a property of the record that finds the definition in the chain.
CM15. In this lesson “record” is the pair of name and visibility in the method table of a class; public and protected records are read with instance_methods(false), private records with private_instance_methods(false). A visibility call puts the record in the subclass; it does not copy the body of the definition.
Which Visibility, When
The choice among the three visibilities depends on whom a method serves. The contract a class gives to the outside is its public methods: code that calls them may exist, and if the class changes them, that code breaks. In the Account example net is such a contract. fee is a detail that explains how net is computed; because it is made private, the class can change it as it likes, since no code calls it from outside.
A protected method fills a narrow gap between the two: the contract that the objects of a class use when talking to one another but that is not opened to the outside. In the Jar example amount is like this. A protected method is needed in methods that handle two objects together, such as a comparison: greater? read the amount of the other object only because it is protected. If only the object itself calls the method, making it private and calling it with a self receiver is enough.
Another result seen in this lesson is that visibility is not a security measure. send could call the private method; so visibility documents a contract and prevents an accidental call, but does not stop a deliberate one. Code that calls a private method with send therefore does knowingly something the class does not allow, and when the class changes that method, it is the first to break.
Summary
- A private method cannot be called from outside with an explicit receiver and raises
NoMethodError; it can be called from inside the object without a receiver or with aselfreceiver. sendignores visibility and gives 2;public_sendenforces it and refuses.- A protected method can be called on another object of the same class; a private method cannot be called on another object.
private def x = 1makes only that method private; visibility is asked withprivate_instance_methods(false).- The author of this lesson is the programmer: for
feeauthorgave the programmer, forprivate attr_reader :amountthe code. public :feein a subclass changes the visibility only in that class; it puts a record in the subclass, the private definition ofAccountstays in place and the owner comes out asAccount3.
Next Step
A Lock module has been mixed into a Safe class, and both define unlock. In the call Safe.new.unlock, which definition runs first, and how does the super chain follow this order? The next lesson, The Method Lookup Chain, measures this question.
To keep your progress and take notes, Log in
My notes
Log in to take notes.