Are ruby instance variables private? If not, how can I make them private?
There is a limited amount of 'privateness' granted to ruby instance variables, you can always access them from the outside through e.g. instance_variable_get
(a public method) as outlined in this question. So depending on your desired level of 'privateness' the answer would be "they are private", "they are protected" or "they cannot be really private"
Get the value of an instance variable given its name
The most idiomatic way to achieve this is:
some_object.instance_variable_get("@#{name}")
There is no need to use +
or intern
; Ruby will handle this just fine. However, if you find yourself reaching into another object and pulling out its ivar, there's a reasonably good chance that you have broken encapsulation.
If you explicitly want to access an ivar, the right thing to do is to make it an accessor. Consider the following:
class Computer
def new(cpus)
@cpus = cpus
end
end
In this case, if you did Computer.new
, you would be forced to use instance_variable_get
to get at @cpus
. But if you're doing this, you probably mean for @cpus
to be public. What you should do is:
class Computer
attr_reader :cpus
end
Now you can do Computer.new(4).cpus
.
Note that you can reopen any existing class and make a private ivar into a reader. Since an accessor is just a method, you can do Computer.new(4).send(var_that_evaluates_to_cpus)
How to get private variables of instance object without calling method
You can use inspect
for p
and to_s
for puts
class Foo
def initialize(arg)
@bar = arg
end
def inspect
@bar
end
def to_s
@bar
end
end
f = Foo.new('test')
puts f #=> "test"
p f #=> "test"
Ruby / IRB: set instance variable to private or otherwise invisible?
After some quick searching, I don't think Ruby supports private instance variables. Your best bet is to override your object's to_s
method (or monkey patch Object#to_s
) to only output the instance variables you want to see. To make things easier, you could create a blacklist of variables you want to hide:
class Foo
BLACK_LIST = [ :@private ]
def initialize(public, private)
@public = public
@private = private
end
def to_s
public_vars = self.instance_variables.reject { |var|
BLACK_LIST.include? var
}.map { |var|
"#{var}=\"#{instance_variable_get(var)}\""
}.join(" ")
"<##{self.class}:#{self.object_id.to_s(8)} #{public_vars}>"
end
end
Note that they will still be accessible through obj.instance_variables
and obj.instance_variable_get
, but at the very least they won't get in the way of your debugging.
How to set private instance variable used within a method test?
As you answered in your question the easiest way to set instance variable is with instance_eval
method:
obj.instance_eval('@a = 1')
Another way is to use instance_variable_set:
obj.instance_variable_set(:@a, 1)
But I would not recommend to do this in your specs. Specs are all about testing behavior of an object and testing behaviour by breaking class encapsulation with instance_eval
will make your tests more fragile and implementation dependent.
Alternative approach to object state isolation is to stub accessor methods:
class UnderTest
attr_accessor :a
def test_this
do_something if a == 1
end
end
#in your test
under_test = UnderTest.new
under_test.stub(:a).and_return(1)
Overriding the method for instance variables
Programming is a lot like real life: it is not a good idea to just run around and let strangers touch your private parts.
You are solving the wrong problem. You are trying to regulate what strangers can do when they play with your private parts, but instead you simply shouldn't let them touch your privates in the first place.
class Example
def initialize(numbers = [])
@numbers = numbers.clone
end
def numbers
@numbers.clone.freeze
end
def <<(number)
validate(number)
@numbers << number
self
end
private
def validate(number)
raise ArgumentError, "number must be non-negative, but is #{number}" unless number >= 0
end
end
example = Example.new([1, 2, 3])
example.numbers # [1, 2, 3]
example << 4
example.numbers # [1, 2, 3, 4]
example << -1 # raise ArgumentError
Let's look at all the changes I made one-by-one.
clone
ing the initializer argument
You are taking a mutable object (an array) from an untrusted source (the caller). You should make sure that the caller cannot do anything "sneaky". In your first code, I can do this:
ary = [1, 2, 3]
example = Example.new(ary)
ary << -1
Since you simply took my array I handed you, I can still do to the array anything I want!
And even in the hardened version, I can do this:
ary = [1, 2, 3]
example = Example.new(ary)
class << ary
remove_method :<<
end
ary << -1
Or, I can freeze
the array before I hand it to you, which makes it impossible to add a singleton method to it.
Even without the safety aspects, you should still do this, because you violate another real-life rule: Don't play with other people's toys! I am handing you my array, and then you mutate it. In the real world, that would be considered rude. In programming, it is surprising, and surprises breed bugs.
clone
ing in the getter
This goes to the heart of the matter: the @numbers
array is my private internal state. I should never hand that to strangers. If you don't hand the @numbers
array out, then none of the problems you are protecting against can even occur.
You are trying to protect against strangers mutating your internal state, and the solution to that is simple: don't give strangers your internal state!
The freeze
is technically not necessary, but I like it to make clear to the caller that this is just a view into the state of the example
object, and they are only allowed to view what I want them to.
And again, even without the safety aspects, this would still be a bad idea: by exposing your internal implementation to clients, you can no longer change the internal implementation without breaking clients. If you change the array to a linked list, your clients are going to break, because they are used to getting an array that you can randomly index, but you can't randomly index a linked list, you always have to traverse it from the front.
The example is unfortunately too small and simple to judge that, but I would even question why you are handing out arrays in the first place. What do the clients want to do with those numbers? Maybe it is enough for them to just iterate over them, in which case you don't need to give them a whole array, just an iterator:
class Example
def each(...)
return enum_for(__callee__) unless block_given?
@numbers.each(...)
self
end
end
If the caller wants an array, they can still easily get one by calling to_a
on the Enumerator
.
Note that I return self
. This has two reasons:
It is simply the contract of
each
. Every other object in Ruby that implementseach
returnsself
. If this were Java, this would be part of theIterable
interface.I would actually accidentally leak the internal state that I work so hard to protect! As I just wrote: every implementation of
each
returnsself
, so what does@numbers.each
return? It returns@numbers
, which means my wholeExample#each
method returns@numbers
which is exactly the thing I am trying to hide!
Implement <<
myself
Instead of handing out my internal state and have the caller append to it, I control what happens with my internal state. I implement my own version of <<
in which I can check for whatever I want and make sure no invariants of my object are violated.
Note that I return self
. This has two reasons:
It is simply the contract of
<<
. Every other object in Ruby that implements<<
returnsself
. If this were Java, this would be part of theAppendable
interface.I would actually accidentally leak the internal state that I work so hard to protect! As I just wrote: every implementation of
<<
returnsself
, so what does@numbers << number
return? It returns@numbers
, which means my wholeExample#<<
method returns@numbers
which is exactly the thing I am trying to hide!
Drop the bang
In Ruby, method names that end with a bang mean "This method is more surprising than its non-bang counterpart". In your case, there is no non-bang counterpart, so the method shouldn't have a bang.
Don't abuse boolean operators for control flow
… or at least if you do, use the keyword versions (and
/ or
) instead of the symbolic ones (&&
/ ||
).
But really, you should void it altogether. do or die
is idiomatic in Perl, but not in Ruby.
Technically, I have changed the return value of your method: it used to return true
for a valid value, now it returns nil
. But you ignore its return value anyway, so it doesn't matter.
validate
is probably not a good name for the method, though. I would expect a method named validate
to return a boolean result, not raise an exception.
An exceptional message
You should add messages to your exceptions that tell the programmer what went wrong. Another possibility is to create more specific exceptions, e.g.
class NegativeNumberError < ArgumentError; end
But that would be overkill in this case. In general, if you expect code to "read" your exception, create a new class, if you expect humans to read your exception, then a message is enough.
Encapsulation, Data Abstraction, Information Hiding
Those are three subtly different but related concepts, and they are among the most important concepts in programming. We always want hide our internal state and encapsulate it behind methods that we control.
Encapsulation to the max
Some people (including myself) don't particularly like even the object itself playing with its internal state. Personally, I even encapsulate private instance variables that are never exposed behind getters and setters. The reason is that this makes the class easier to subclass: you can override and specialize methods, but not instance variables. So, if I use the instance variable directly, a subclass cannot "hook" into those accesses.
Whereas if I use getter and setter methods, the subclass can override those (or only one of those).
Note: the example is too small and simple, so I had some real trouble coming up with a good name (there is not enough in the example to understand how the variable is used and what it means), so eventually, I just gave up, but you will see what I mean about using getters and setters:
class Example
class NegativeNumberError < ArgumentError; end
def initialize(numbers = [])
self.numbers_backing = numbers.clone
end
def each(...)
return enum_for(__callee__) unless block_given?
numbers_backing.each(...)
self
end
def <<(number)
validate(number)
numbers_backing << number
self
end
private
attr_accessor :numbers_backing
def validate(number)
raise NegativeNumberError unless number >= 0
end
end
example = Example.new([1, 2, 3])
example.each.to_a # [1, 2, 3]
example << 4
example.each.to_a # [1, 2, 3, 4]
example << -1 # raise NegativeNumberError
Rspec private method can't access class instance variable?
Your example is a bit confusing due to the various spelling errors in your attempt to generalize your problem. I created the following two files, and the specs ran just fine. Might be a naming error that you're experiencing and not an actual rspec problem.
# test_spec.rb
require 'rspec'
require_relative 'my_class'
describe MyClass do
describe '#initialize' do
let(:configs) { {some_token: '123-FakeToken'} }
let(:loader) { described_class.new(configs) }
context 'when initialized with a set of configs' do
it { expect(loader.instance_variable_get(:@configs)).to eq(configs)}
end
end
end
and
# my_class.rb
class MyClass
#@configs = {}
def initialize(configs)
@configs = configs
check_configs
end
private
def check_configs
if @configs[:some_token].nil?
puts "log message"
raise 'an error'
end
end
end
That said, the bigger question is what are you trying to accomplish with your tests?
Testing private variables is a smell. Ideally, in the case of config variables, they will cause an effect in your instance when set. For example MyClass.foo
will behave differently based on whether some_token
is set or not. Testing whether the behaviour of foo
changes with a some_token
present or not is a high value test that you want. Instead of a low value test of whether you wrote @configs = configs
correctly.
Related Topics
Best Way to Debug Third-Party Gems in Ruby
How to Read the Content of an Excel Spreadsheet Using Ruby
How to Add Migration with Multiple References to the Same Model in One Table? Ruby/Rails
Do Ruby 'Require' Statements Go Inside or Outside the Class Definition
Store the Day of the Week and Time
Run Rspec Tasks in a Specific Order
How to Run a Single Test in Minitest
A Copy of Xxx Has Been Removed from the Module Tree But Is Still Active
Running Pod Setup Gives Me "Bad Interpreter: No Such File or Directory" Error
Ruby on Rails: Can You Put Ruby Code in a Yaml Config File
How to Configure Capistrano to Use My Rvm Version of Ruby
Ruby: How to Convert a String to Boolean
Error: Failed to Build Gem Native Extension When Installing Rails on MAC Mountian Lion Os
Is It Possible for Rspec to Expect Change in Two Tables
Ruby on Rails Rmagick on Windows 7
Shell Out from Ruby While Setting an Environment Variable