Reputation: 4995
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
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
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
Reputation: 52357
The intent is to restrict changing values of
x
andy
variables once the class is initialized through the factory methodwith_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
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