I am learning Ruby basic, and trying to do some tests. I encounter an issue to understand the class method accesss scope.
Here is my code.
class User
attr_accessor :name
def initialize(name)
@name = name
@login_status = false
end
def login
puts "user #{name} logged in"
self.login_status = true
end
def logout
puts "user #{name} logged out"
self.login_status = false
end
def login?
self.login_status
end
private
attr_accessor :login_status
end
I got 'no such method error' when I had a test in irb
2.5.9 :001 > load 'user.rb'
=> true
2.5.9 :002 > u = User.new 'Mike'
=> #<User:0x00007ff1a10eb358 @name="Mike", @login_status=false>
2.5.9 :003 > u.login
user Mike logged in
=> true
2.5.9 :004 > u.login?
Traceback (most recent call last):
3: from /Users/sgao/.rvm/rubies/ruby-2.5.9/bin/irb:11:in `<main>'
2: from (irb):3
1: from user.rb:21:in `login?'
NoMethodError (private method `login_status' called for #<User:0x00007ff22f860938 @name="test", @login_status=false>)
2.5.9 :005 >
I am thinking why self.login_status = true
works when I invoke u.login
, but self.login_status
fails when I invoke u.login?
.
CodePudding user response:
TL;DR
The way private methods work in a dynamic language like Ruby is a common source of confusion. In Ruby, private methods primarily work by disallowing an explicit receiver. The underlying problem in your code is that you've declared your accessor methods for :login_status
as private for some reason. I explain more about the problem and the possible solutions below.
How You Unintentionally Broke the Code
You did this to yourself by declaring the :login_status
accessor as private. This prevents your public methods from calling it with an explicit receiver. Although you can still read the created instance variable of @login_status from your public methods as a sort of syntax sugar for the getter method—in Ruby 3.1.1, anyway; I didn't test it with anything else—you don't actually have direct access to the getter method, or an easily-accessible setter method for assigning to the instance variable.
Module#attr_accessor is just shorthand for declaring a pair of getter and setter methods on a object. When you place the accessor declaration after calling Kernel#private, Ruby defines the #login_status and #login_status= getter and setter methods as private methods. You did that here:
private
attr_accessor :login_status
In Ruby, you cannot call a private method with an explicit receiver. Your exception even tells you that this is the problem (bold and emphasis mine):
NoMethodError (private method `login_status' called for #<User:0x00007ff22f860938 @name="test", @login_status=false>)
Possible Fixes
Recommended Fixes
The best solution is probably to change your #login? method to use an implicit receiver by removing self
as the explicit receiver:
def login?
login_status
end
or make the accessor public rather than private.
Possible, Not Recommended: Fixing with Object#Send
Alternatively, you can also use Object#send to more explicitly call private methods. However, without understanding why you think an accessor (which is generally most useful when made part of the public interface) should be defined as a set of private methods, I find it hard to recommend that approach as a generic best practice.
Reasons Not to Rely on Private Methods
Unlike some other languages, Ruby private methods aren't really off-limits. As a result, the use of private or protected methods in Ruby is often more of a set of advisory usage guidelines, or an indication of semantic intent to differentiate code from the public interface, rather than a hard line that users of a class or method can't cross.
That doesn't mean you shouldn't use private methods in your code. It's just that using them, especially when declaring accessors, is a lot less common than in other languages because the use cases are often less about enforcement than they are about communicating programmer intent.
CodePudding user response:
I am thinking why
self.login_status = true
works when I invokeu.login
, butself.login_status
fails when I invokeu.login?
.
For the exact reason that u.login
works but u.fobnicatefoobarbazqux
doesn't: one method is defined, the other isn't.
The method User#login_status=
is defined (attr_writer :login_status
does that for you), but User#login_status
isn't. You need to define it.
Module#attr_writer
essentially looks more or less like this:
class Module
def attr_writer(*meths)
meths.each do |meth|
define_method(:"#{meth}=") do |val|
instance_variable_set(:"@#{meth}", val)
end
end
end
end