A Fader Darkly
A Fader Darkly

Reputation: 3636

Ruby private and public accessors

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

Answers (5)

Dave Schweisguth
Dave Schweisguth

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

Dorian
Dorian

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

devpuppy
devpuppy

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

AlexChaffee
AlexChaffee

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

Frederick Cheung
Frederick Cheung

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

Related Questions