Sid
Sid

Reputation: 4995

Enabling changing value of an class instance attribute marked as 'attr_reader'

I have the following code:

class A
   attr_reader :x, :y

   private_class_method :new

   def self.with_data
     a = new
     a.x = 2
     a.y = 'sid'
     a
   end
 end

The intent is to restrict changing values of x and y variables once the class is initialized through the factory method with_data. However, I want this to be allowed when the object is initialized from within the class, as evident from the code above.

But I am getting the following error when I invoke obj = A.with_data:

NoMethodError: undefined method `x='

Should't this be allowed from inside class? Do I need to define attr_writer for this? That would jeopardize encapsulation.

Also, I don't want to define a private setter method for each attribute in the class, as it might run into upto 30 instance level variables. Does ruby provide any feature to get around this?

Versions: Ruby 1.9.3

Upvotes: 1

Views: 3429

Answers (4)

Jörg W Mittag
Jörg W Mittag

Reputation: 369556

I must admit that I don't understand your adversity for using initialize or an attr_writer. I feel the cleanest solution for when you have only one factory method is to use the standard name for factory methods in Ruby, namely new:

class A
   attr_reader :x, :y

   def initialize(x, y) self.x, self.y = x, y end

   def self.new
     super(2, 'sid')
   end

   private

   attr_writer :x, :y
 end

If you have multiple factory methods and want to make absolutely sure that nobody accidentally calls new, this is a good solution:

class A
   attr_reader :x, :y

   def initialize(x, y) self.x, self.y = x, y end

   private_class_method :new

   def self.with_data
     new(2, 'sid')
   end

   private

   attr_writer :x, :y
 end

If you really, really, really must, you can replicate what new is doing in your factory methods. After all, the implementation of new is quite trivial:

class Class
  def new(*args, &block)
    obj = allocate
    obj.__send__(:initialize, *args, &block)
    obj
  end
end

Upvotes: 1

Andrey Deineko
Andrey Deineko

Reputation: 52357

So what you need in your case is Object#instance_variable_set:

class A
  attr_reader :x, :y

  private_class_method :new

  def self.with_data
    a = new
    a.instance_variable_set(:@x, 2)
    a.instance_variable_set(:@y, 'sid')
    a
  end
end

Usage:

a = A.with_data
#=> #<A:0x007ff37c979d30 @x=2, @y="sid">
a.x
#=> 2
a.x = 3
#=> NoMethodError: undefined method `x=' for #<A:0x007ff37c979d30 @x=2, @y="sid">

Upvotes: 2

Andrey Deineko
Andrey Deineko

Reputation: 52357

The intent is to restrict changing values of x and y variables once the class is initialized through the factory method with_data

class Foo
  attr_reader :bar, :baz # <==== assures you only read, not write

  def initialize
    @bar = :bar
    @baz = :baz
  end
end

Now you can only read attributes, not write them:

foo = Foo.new
=> #<Foo:0x007ff6148f0a90 @bar=:bar, @baz=:baz>
foo.bar
#=> :bar
foo.bar = 2
#=> NoMethodError: undefined method `bar=' for #<Foo:0x007ff6148f0a90 @bar=:bar, @baz=:baz

Upvotes: 1

Michael Kohl
Michael Kohl

Reputation: 66837

As the name implies, attr_reader will only define a getter, so you can use accessors inside the class either.

That said, what exactly are you trying to achieve? The following class will initialize attributes, expose them via a reader and not make them easily changeable from "outside". Isn't that just what you wanted to to?

class A
  attr_reader :x, :y

  def initialize
    @x = 2
    @y = 'sid'
  end 
end

Upvotes: 1

Related Questions