[Q] best practice for redefining methods at runtime?

C

Chuck Remes

I have a class that I would like to "freeze" when it reaches a particular state so that it becomes read-only. I do *not* want to use the existing #freeze method because it raises an exception when there is an attempted mutation on a frozen object and I want the attempt to be silent. It appears that the best way to enforce this (state machine pattern) is by redefining the accessors and other methods that cause mutations to be no ops.

What is the preferred practice for doing that?

Here's an example of my approach:

class Foo
attr_accessor :bar, :baz, :quxxo

def freeze!
do_nothing = Proc.new {|val| nil}
recreate_method:)bar=, do_nothing)
recreate_method:)baz=, do_nothing)
recreate_method:)quxxo=, do_nothing)
end

private

def recreate_method name, meth
self.class.send:)define_method, name, meth)
nil
end
end


ruby-1.9.2-p0 > f=Foo.new
=> #<Foo:0x0000010185f7d0>
ruby-1.9.2-p0 > f.bar=2
=> 2
ruby-1.9.2-p0 > f.baz=9
=> 9
ruby-1.9.2-p0 > f.freeze!
=> nil
ruby-1.9.2-p0 > f.inspect
=> "#<Foo:0x0000010185f7d0 @bar=2, @baz=9>"
ruby-1.9.2-p0 > f.bar=3
=> 3
ruby-1.9.2-p0 > f.inspect
=> "#<Foo:0x0000010185f7d0 @bar=2, @baz=9>"


Is that reasonable or is there a more idiomatic way of accomplishing this task?

cr
 
N

namekuseijin

the only best practice I would advise is to *not* redefine methods at
run-time.
 
J

John W Higgins

[Note: parts of this message were removed to make it a legal post.]

Morning Chuck.

I have a class that I would like to "freeze" when it reaches a particular
state so that it becomes read-only. I do *not* want to use the existing
#freeze method because it raises an exception when there is an attempted
mutation on a frozen object and I want the attempt to be silent. It appears
that the best way to enforce this (state machine pattern) is by redefining
the accessors and other methods that cause mutations to be no ops.

What is the preferred practice for doing that?
You could in the alternative have the ops that mutate do an "unless" at the
top of the method to bake the freeze into the method as opposed to
redefining

Using your example


class Foo
attr_accessor :frozen
attr_reader :bar, :baz, :quxxo

def bar= new_bar
unless @frozen @bar = new_bar
end

def baz= new_baz
unless @frozen @baz = new_baz
end

def quzzo= new_quzzo
unless @frozen @quzzo = new_quzzo
end
end

It's a little more work and a slight amount of overhead for the unless
check. But it does eliminate the need to redefine your methods. It also very
clearly spells out what you are freezing.

John
 
R

Robert Dober

Is that reasonable or is there a more idiomatic way of accomplishing this task?
Reasonable maybe but it is far from achieving what freeze does,
because you still have instance_variable_set and as long as this
available I can redefine the accesors, thus you would need to block
#instance_variable_set, #define_method for the class but I can still
beat that for any given instance by introducing singleton methods....
In other words it is a metaprogramming mess.sub("m","h").gsub("s","l")!

Maybe you should reconsider your approach, what about freezing the
object but accessing it in a wrapper that rescues the exceptions? This
could done via closures, and BasicObject makes the task much easier
than it was in 1.8

class Freezer < BasicObject
freezee = nil
define_method :initialize do | f |
freezee = f
end
define_method :method_missing do | name, *args, &blk |
begin
freezee.send( name, *args, &blk )
rescue ::RuntimeError => re
# Pitty that there is no designated Exception for this BTW
raise unless /can't modify frozen/ === re.message
end
end
end

a = Freezer.new( "" )
a << "1"
puts a
a.freeze
a << "2"
puts a
# But
a.xxx

HTH
R.
 
C

Chuck Remes

Morning Chuck.


You could in the alternative have the ops that mutate do an "unless" at the
top of the method to bake the freeze into the method as opposed to
redefining

Thanks for everyone for their suggestions. Instead of using metaprogramming, which in this case is apparently quite problematic, I'll go with the easiest solution of doing an "unless" check in certain key methods. Obviously that won't prevent a future developer from adding singleton methods to the object(s) or messing with them in some other way, but that will clearly break the contract. The docs will say "don't do this" and leave it at that.

Thanks again.

cr
 
R

Robert Klemme

Morning Chuck.

You could in the alternative have the ops that mutate do an "unless" at t= he
top of the method to bake the freeze into the method as opposed to
redefining

Using your example


class Foo
=A0attr_accessor :frozen
=A0attr_reader :bar, :baz, :quxxo

=A0def bar=3D new_bar
=A0 unless @frozen @bar =3D new_bar
=A0end

=A0def baz=3D new_baz
=A0 unless @frozen @baz =3D new_baz
=A0end

=A0def quzzo=3D new_quzzo
=A0 unless @frozen @quzzo =3D new_quzzo
=A0end
end

It's a little more work and a slight amount of overhead for the unless
check. But it does eliminate the need to redefine your methods. It also v= ery
clearly spells out what you are freezing.

But why do this if you can simply use #freeze and #frozen? as they are
defined? The advantage of the regular #freeze method is that you will
immediately notice if you try to modify a frozen instance. All
approaches present here will silently eat the method call and do
nothing thus making the caller believe the method has worked regularly
while it hasn't. I think this approach to freezing is error prone and
likely to create hard to detect bugs.

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 

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,770
Messages
2,569,583
Members
45,075
Latest member
MakersCBDBloodSupport

Latest Threads

Top