Reputation: 3636
When defining accessors in Ruby, there can be a tension between brevity (which we all love) and best practice.
For example, if I wanted to expose a value on an instance but prohibit any external objects from updating it, I could do the following:
class Pancake
attr_reader :has_sauce
def initialize(toppings)
sauces = [:maple, :butterscotch]
@has_sauce = toppings.size != (toppings - sauces).size
...
But suddenly I'm using a raw instance variable, which makes me twitch. I mean, if I needed to process has_sauce before setting at a future date, I'd potentially need to do a lot more refactoring than just overriding the accessor. And come on, raw instance variables? Blech.
I could just ignore the issue and use attr_accessor
. I mean, anyone can set the attribute if they really want to; this is, after all, Ruby. But then I lose the idea of data encapsulation, the object's interface is less well defined and the system is potentially that much more chaotic.
Another solution would be to define a pair of accessors under different access modifiers:
class Pancake
attr_reader :has_sauce
private
attr_writer :has_sauce
public
def initialize(toppings)
sauces = [:maple, :butterscotch]
self.has_sauce = toppings.size != (toppings - sauces).size
end
end
Which gets the job done, but that's a chunk of boilerplate for a simple accessor and quite frankly: ew.
So is there a better, more Ruby way?
Upvotes: 22
Views: 14795
Reputation: 37607
Ruby 3.0 made access modifiers and attr_*
work with each other, so you can just write
private attr_reader :has_sauce
Upvotes: 12
Reputation: 9065
You can put attr_reader
in a private
scope, like this:
class School
def initialize(students)
@students = students
end
def size
students.size
end
private
attr_reader :students
end
School.new([1, 2, 3]).students
This will raise an error as expected:
private method `students' called for #<School:0x00007fcc56932d60 @students=[1, 2, 3]> (NoMethodError)
Upvotes: 5
Reputation: 832
There's nothing wrong with referencing instance variables directly within your class. attr_accessor
is just doing that indirectly anyway, whether you make those methods public or private.
In this particular example, it may help to recognize that toppings
are likely an attribute you want to save for other purposes, and has_sauce
is a "virtual attribute", a characteristic of the model that's dependent on the underlying toppings attribute.
Something like this might feel cleaner:
class Pancake
def initialize(toppings)
@toppings = toppings
end
def has_sauce?
sauces = [:maple, :butterscotch]
(@toppings & sauces).any?
end
end
Up to you whether or not to expose attr_accessor :toppings
as well. If you're just throwing the toppings away, your class is less of a Pancake
and more of a PancakeToppingDetector
;)
Upvotes: 2
Reputation: 8252
private
can take a symbol arg, so...
class Pancake
attr_accessor :has_sauce
private :has_sauce=
end
or
class Pancake
attr_reader :has_sauce
attr_writer :has_sauce; private :has_sauce=
end
etc...
But what's the matter with "raw" instance variables? They are internal to your instance; the only code that will call them by name is code inside pancake.rb
which is all yours. The fact that they start with @
, which I assume made you say "blech", is what makes them private. Think of @
as shorthand for private
if you like.
As for processing, I think your instincts are good: do the processing in the constructor if you can, or in a custom accessor if you must.
Upvotes: 15
Reputation: 84114
attr_reader
etc are just methods - there's no reason you can define variants for your own use (and I do share your sentiment) For example:
class << Object
def private_accessor(*names)
names.each do |name|
attr_accessor name
private "#{name}="
end
end
end
Then use private_accessor
as you would attr_accessor
(I think you need a better name than private_accessor though)
Upvotes: 6