class design issues

S

Spitfire

I have a class which takes an input and produces an object. Let's
say, it takes inputs about specifications of a life-form, and then
creates it (instantiates say an object, 'LifeForm'). Let's call this
factory class 'Creator'. Now, my problem is how do I ensure that once
'Creator' returns a 'LifeForm', any external/requestor class can only
view the properties of LifeForm, that were set during creation, and not
be able to modify them???
How do I design these imaginary classes 'Creator' and 'LifeForm'?
 
R

Robert Klemme

I have a class which takes an input and produces an object. Let's say,
it takes inputs about specifications of a life-form, and then creates it
(instantiates say an object, 'LifeForm'). Let's call this factory class
'Creator'. Now, my problem is how do I ensure that once 'Creator'
returns a 'LifeForm', any external/requestor class can only view the
properties of LifeForm, that were set during creation, and not be able
to modify them???
How do I design these imaginary classes 'Creator' and 'LifeForm'?

First, it is very hard to actually prevent changes of instance variables
(if it is possible at all). For your purposes it is probably sufficient
to define attribute readers only. Second, you do not necessarily need a
second class - basically LifeForm is the factory for LifeForm instances.
So you could do

class LifeForm
attr_reader :age, :name, :foo

def initialize(age,name,foo)
@age = age
@name = name
@foo = foo
end
end

irb(main):010:0> lf1 = LifeForm.new 10, "bla", "buzz"
=> #<LifeForm:0x7ef6f5e4 @name="bla", @foo="buzz", @age=10>
irb(main):011:0> lf1.name
=> "bla"
irb(main):012:0> lf1.name = "ddd"
NoMethodError: undefined method `name=' for #<LifeForm:0x7ef6f5e4
@name="bla", @foo="buzz", @age=10>
from (irb):12
from :0

HTH

robert
 
R

Robert Dober

First, it is very hard to actually prevent changes of instance variables
(if it is possible at all). For your purposes it is probably sufficient
to define attribute readers only. Second, you do not necessarily need a
second class - basically LifeForm is the factory for LifeForm instances.
So you could do

class LifeForm
attr_reader :age, :name, :foo

def initialize(age,name,foo)
@age = age
@name = name
@foo = foo

freeze

and you might add freeze here, now it becomes quite tough to change
the LifeForm object.
Personally I do not know any way to modify it now, but someone will
show us soon, I am quite sure ;)

HTH
Robert
 
D

dblack

Hi --

First, it is very hard to actually prevent changes of instance variables (if
it is possible at all). For your purposes it is probably sufficient to
define attribute readers only. Second, you do not necessarily need a second
class - basically LifeForm is the factory for LifeForm instances. So you
could do

class LifeForm
attr_reader :age, :name, :foo

def initialize(age,name,foo)
@age = age
@name = name
@foo = foo
end
end

irb(main):010:0> lf1 = LifeForm.new 10, "bla", "buzz"
=> #<LifeForm:0x7ef6f5e4 @name="bla", @foo="buzz", @age=10>
irb(main):011:0> lf1.name
=> "bla"
irb(main):012:0> lf1.name = "ddd"
NoMethodError: undefined method `name=' for #<LifeForm:0x7ef6f5e4
@name="bla", @foo="buzz", @age=10>
from (irb):12
from :0

It does raise the danger of:

lf1.name << "more stuff"

so it might be good to dup or freeze the mutable ones (though of
course someone who really wants to can always pry in with
instance_eval).


David

--
Q. What is THE Ruby book for Rails developers?
A. RUBY FOR RAILS by David A. Black (http://www.manning.com/black)
(See what readers are saying! http://www.rubypal.com/r4rrevs.pdf)
Q. Where can I get Ruby/Rails on-site training, consulting, coaching?
A. Ruby Power and Light, LLC (http://www.rubypal.com)
 
D

dblack

Hi --

Hi David,

Now this should not be available !!

lf1.name << "more stuffs"
this should generate error !! ... (i agree it is working)

Strings response to "<<", so assuming lf1.name returns a string,
there's no error of any kind here.
but is name is an attr_reader then the manipulations with "<<" should not be
supported, as the case of "="
or it should be ?

It's not really that = is or is not supported; it's all a matter of
what methods you define. If you define a method that returns a
string, then you get a string, which is mutable, from that method.
The notion of an "attribute" is really in the mind of the programmer.
Objects don't know whether they're attributes or not; they just exist,
and do what they're told.


David

--
Q. What is THE Ruby book for Rails developers?
A. RUBY FOR RAILS by David A. Black (http://www.manning.com/black)
(See what readers are saying! http://www.rubypal.com/r4rrevs.pdf)
Q. Where can I get Ruby/Rails on-site training, consulting, coaching?
A. Ruby Power and Light, LLC (http://www.rubypal.com)
 
S

Spitfire

Robert said:
freeze

and you might add freeze here, now it becomes quite tough to change
the LifeForm object.
Personally I do not know any way to modify it now, but someone will
show us soon, I am quite sure ;)
Sorry, I'm no expert in Ruby. So you have to explain what 'freeze'
does???

Let me add more hypothetical requirements to my problem (sorry for
not stating these initially!)

Lets consider that LifeForm has a property called 'Rank'. Now, this
is a very critical property that I must make sure to retain consistent.
LifeForm can also have offsprings, which are tied to it, say by a
instance variable that points any LifeForm to its list of offspring
LifeForm objects.

Now, the rank of a LifeForm is its distance from all its ancestors. I
want a functionality such that whenever you create a LifeForm, the rank
is set to '0'. Next, I want to make sure that when I add offsprings to
an existing LifeForm, its depth gets updated automagically, without my
intervention. More specifically, I want to have a feature by which
LifeForm has a mechanism in the class, which allows to it set by itself
the 'rank' of its instances. And, when I add a child, say through a
method 'add_Child' (don't know if this is the ideal solution, but this
is what I can think of!), it does something like this,

for each child in new_children_added
child.depth = child.depth + current.depth
# current refers to parent or current object
end

actually I want this to be carried out to all children newly added,
their children and so on. So that the ranks of a LifeForm is always
consistent! Hope I've conveyed exactly what I want.

Now I want to be able to only 'read' this rank, not modify it from
outside the LifeForm class. Is this possible? If so, how do you design it?
 
A

Alex Young

sur said:
But i think there is a point to discuss here...

as lf1.name is simply an instance method which is(should be) only
capable of
returning the instance variable "@name"

so by defining :name as attr_reader should block everything which is trying
to write the instance variable "@name"

Nothing is trying to write to @name - what's happening is that @name
modifies its own data via the method call '<<' (or gsub!, or...).

Watch:

irb(main):001:0> class A; attr_reader :foo; def initialize; @foo='';
end; end
=> nil
irb(main):002:0> a = A.new
=> #<A:0xb7ca0280 @foo="">
irb(main):003:0> a.foo.object_id
=> -605748948
irb(main):004:0> a.foo = "Foo!"
NoMethodError: undefined method `foo=' for #<A:0xb7ca0280 @foo="">
from (irb):4
from :0
irb(main):005:0> a.foo << "Foo!"
=> "Foo!"
irb(main):006:0> a.foo
=> "Foo!"
irb(main):007:0> a.foo.object_id
=> -605748948

See how despite the method call, a.foo.object_id returns the same value
each time? That's what attr_reader protects. It's not within the scope
of what attr_reader should be doing to define what methods you're
allowed to call on what it returns, because it's got no way to know
which methods can change that object's state.

Hope this helps,
--
Alex
 
R

Robert Dober

Sorry, I'm no expert in Ruby. So you have to explain what 'freeze'
does???
Sure should have been clearer

freeze basically does not allow modification of the object anymore
I will demonstrate in irb
923/425 > irb
irb(main):001:0> class A
irb(main):002:1> attr_reader :a
irb(main):003:1> def initialize
irb(main):004:2> @a=42
irb(main):005:2> end
irb(main):006:1> def change
irb(main):007:2> @a = rand(43)
irb(main):008:2> end
irb(main):009:1> end
=> nil
irb(main):010:0> a=A.new
=> #<A:0xb7d829dc @a=42>
irb(main):011:0> a.change
=> 30
irb(main):012:0> a.instance_variable_set("@a", 1764)
=> 1764
irb(main):013:0> a
=> #<A:0xb7d829dc @a=1764>
irb(main):014:0> a.freeze
=> #<A:0xb7d829dc @a=1764>
irb(main):015:0> a.change
TypeError: can't modify frozen object
from (irb):7:in `change'
from (irb):15
from :0
irb(main):016:0> a.instance_variable_set("@a", 1764)
TypeError: can't modify frozen object
from (irb):16:in `instance_variable_set'
from (irb):16
from :0

David stated that this can be overcome with instance eval, I do not
know how though
irb(main):017:0> a.instance_eval do
irb(main):018:1* @a=1
irb(main):019:1> end
TypeError: can't modify frozen object
from (irb):18
from (irb):17:in `instance_eval'
from (irb):17
from :0
Documentation states that a frozen object cannot be unfrozen.
Unfortunately you cannot freeze your whole object so maybe it would be
good to expose a ProxyObject

class Intern
attr_accessor :a
def initialize
@a = 42
end
end

class Proxy
def initialize protect
@protect = protect
freeze
end
def a *args, &blk
@protect.a *args, &blk
end
freeze
end

i = Intern.new
p = Proxy.new( i )
puts p.a

begin
p.a = 42
rescue
puts "good"
end

begin
class Proxy
def p; @protect; end
end
rescue
puts "very good"
end

begin
Proxy.send:)define_method, :p){
@protect
}
rescue
puts "better"
end

begin
class << Proxy
define_method:)p){
@protect
}
end
rescue
puts "even better"
end



begin
p.a = 42
rescue
puts "still good"
end

now you have pretty much sealed your Intern objects as long as all
access you allow is by Proxy.
That might give an APi which is pretty much clear about access :)
Let me add more hypothetical requirements to my problem (sorry for
not stating these initially!)

Lets consider that LifeForm has a property called 'Rank'. Now, this
is a very critical property that I must make sure to retain consistent.
LifeForm can also have offsprings, which are tied to it, say by a
instance variable that points any LifeForm to its list of offspring
LifeForm objects.

Now, the rank of a LifeForm is its distance from all its ancestors. I
want a functionality such that whenever you create a LifeForm, the rank
is set to '0'. Next, I want to make sure that when I add offsprings to
an existing LifeForm, its depth gets updated automagically, without my
intervention. More specifically, I want to have a feature by which
LifeForm has a mechanism in the class, which allows to it set by itself
the 'rank' of its instances. And, when I add a child, say through a
method 'add_Child' (don't know if this is the ideal solution, but this
is what I can think of!), it does something like this,
Of course you cannot freeze the whole object anymore but only some parts.
for each child in new_children_added
child.depth = child.depth + current.depth
# current refers to parent or current object
end

actually I want this to be carried out to all children newly added,
their children and so on. So that the ranks of a LifeForm is always
consistent! Hope I've conveyed exactly what I want.

Now I want to be able to only 'read' this rank, not modify it from
outside the LifeForm class. Is this possible? If so, how do you design it?
--
_ _ _]{5pitph!r3}[_ _ _
__________________________________________________
"I'm smart enough to know that I'm dumb."
- Richard P Feynman
 
D

dblack

Hi --

But i think there is a point to discuss here...

as lf1.name is simply an instance method which is(should be) only capable of
returning the instance variable "@name"

so by defining :name as attr_reader should block everything which is trying
to write the instance variable "@name"

attr_reader just writes a wrapper method for you. If the default
wrapper method doesn't do what you want, you can write a different
one:

def x
@x.dup
end

If you want to do that a lot, you could write a new attr_* method:

class Module
def attr_reader_dup(*attrs)
attrs.each do |attr|
define_method(attr) { instance_variable_get("@#{attr}").dup }
end
end
end

class C
attr_reader_dup:)x)
def initialize
@x = "don't change me"
end
end

c = C.new
c.x << "trying to change"
p c.x # don't change me


David

P.S. Please don't top-post. Just quote what you're responding to and
add your response.

--
Q. What is THE Ruby book for Rails developers?
A. RUBY FOR RAILS by David A. Black (http://www.manning.com/black)
(See what readers are saying! http://www.rubypal.com/r4rrevs.pdf)
Q. Where can I get Ruby/Rails on-site training, consulting, coaching?
A. Ruby Power and Light, LLC (http://www.rubypal.com)
 
R

Robert Klemme

Sorry, I'm no expert in Ruby. So you have to explain what 'freeze'
does???

It prevents further manipulation of an instance:

irb(main):001:0> %w{foo bar bx}.shift
=> "foo"
irb(main):002:0> x=Struct.new:)name).new("foo")
=> #<struct #<Class:0x7ef96ce8> name="foo">
irb(main):003:0> x.name = "bar"
=> "bar"
irb(main):004:0> x.freeze
=> #<struct #<Class:0x7ef96ce8> name="bar">
irb(main):005:0> x.name = "foo"
TypeError: can't modify frozen Struct
from (irb):5:in `name='
from (irb):5
from :0
irb(main):006:0> x
=> #<struct #<Class:0x7ef96ce8> name="bar">
irb(main):007:0> s="foo"
=> "foo"
irb(main):008:0> s << "bar"
=> "foobar"
irb(main):009:0> s
=> "foobar"
irb(main):010:0> s.freeze
=> "foobar"
irb(main):011:0> s << "xxx"
TypeError: can't modify frozen string
from (irb):11:in `<<'
from (irb):11
from :0
Let me add more hypothetical requirements to my problem (sorry for not
stating these initially!)

Right. Your new set of requirements rules out "freeze" as that won't
allow for adding of children etc.
Lets consider that LifeForm has a property called 'Rank'. Now, this is
a very critical property that I must make sure to retain consistent.
LifeForm can also have offsprings, which are tied to it, say by a
instance variable that points any LifeForm to its list of offspring
LifeForm objects.

Now, the rank of a LifeForm is its distance from all its ancestors. I
want a functionality such that whenever you create a LifeForm, the rank
is set to '0'. Next, I want to make sure that when I add offsprings to
an existing LifeForm, its depth gets updated automagically, without my
intervention. More specifically, I want to have a feature by which
LifeForm has a mechanism in the class, which allows to it set by itself
the 'rank' of its instances. And, when I add a child, say through a
method 'add_Child' (don't know if this is the ideal solution, but this
is what I can think of!), it does something like this,

for each child in new_children_added
child.depth = child.depth + current.depth
# current refers to parent or current object
end

You have a tree data structure here (if you have multiple roots it's
called a "forest"). This is one of the well researched and understood
structures. You'll find plenty of implementations and information on
the web.

Actually when adding a child, I would go up recursively to the root of
the tree and update the rank. Kind of:

def add_child(ch)
delta = rank + 1

# BFS because Ruby is not good at recursion
q = [ch]

until q.empty?
obj = q.shift
obj.rank += delta
q.concat obj.children
end
end

You could make method rank= protected to prevent accidental invocation
from the outside.
actually I want this to be carried out to all children newly added,
their children and so on. So that the ranks of a LifeForm is always
consistent! Hope I've conveyed exactly what I want.

Yes. One solution is to calculate the rank on demand and only optimize
this to a local variable if you have performance issues. That solution
is much easier because then you can simply do

def rank
obj, r = self, 0
while obj
r += 1
obj = obj.parent
end
r
end

This is slow but always consistent.
Now I want to be able to only 'read' this rank, not modify it from
outside the LifeForm class. Is this possible? If so, how do you design it?

See above for ideas. Although I would probably not spend too much
efforts in making it impossible to change the value from the outside.
There is always a way.

Kind regards

robert
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,774
Messages
2,569,596
Members
45,134
Latest member
Lou6777736
Top