I did a little experiments with Ruby class dynamic loading/unloading/updating as implementing plugins infrastructure. I found a few points:
My question is, is there an easy way to make existent object created from the old class version 'switch' to the new version (but not a merged version of the old & new version)? It seems to me the possible way to do it is to re-create the object after unloading/loading, which is not suitable for plugins (don't want it be destroyed).
Update: my intent was to have existent objects updated with new version, without the problem of merging old version with new version (like change of number of arguments, or the removal of a method). Unloading and then reloading again seems to be the cleanest way of doing this, though you must keep track of all such objects and recreate them when needed. Also, expensive objects might not be suitable for re-creation. This leaves me with the second option, prohibiting unexpected merging from happening. As long as no method removed, no method signature changed, the merging should work just fine.
Below is my test program:
$ cat test.rb
load 'v1.rb'
puts "=> 'v1.rb' loaded"
a1 = A.new
puts "=> object a1(#{a1}) created"
a1.common
a1.method_v1
load 'v2.rb'
puts '',"=> class A updated by 'v2.rb'"
a1.common
a1.method_v1
a1.method_v2
a2 = A.new
puts '',"=> object a2(#{a2}) created"
a2.common
a2.method_v1
a2.method_v2
Object.send(:remove_const, 'A')
puts '',"=> class A unloaded"
A.new rescue puts $!
puts '',"=> class A does not exist now"
a1.common
a1.method_v1
a1.method_v2 rescue puts $!
a2.common
a2.method_v1
a2.method_v2
load 'v3.rb'
puts '',"=> 'v3.rb' loaded"
a1.common
a1.method_v1
a1.method_v2 rescue puts $!
a1.method_v3 rescue puts $!
a2.common
a2.method_v1
a2.method_v2
a2.method_v3 rescue puts $!
a3 = A.new
puts '',"=> object a3(#{a3}) create"
a3.common
a3.method_v1 rescue puts $!
a3.method_v2 rescue puts $!
a3.method_v3
The sample output:
$ ruby test.rb
=> 'v1.rb' loaded
=> object a1(#<A:0x1042d4b0>) created
#<A:0x1042d4b0>: common: v1
#<A:0x1042d4b0>: method v1
=> class A updated by 'v2.rb'
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2
=> object a2(#<A:0x1042cec0>) created
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2
=> class A unloaded
uninitialized constant A
=> class A does not exist now
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2
=> 'v3.rb' loaded
#<A:0x1042d4b0>: common: v2
#<A:0x1042d4b0>: method v1
#<A:0x1042d4b0>: method v2
undefined method `method_v3' for #<A:0x1042d4b0>
#<A:0x1042cec0>: common: v2
#<A:0x1042cec0>: method v1
#<A:0x1042cec0>: method v2
undefined method `method_v3' for #<A:0x1042cec0>
=> object a3(#<A:0x1042c3f8>) create
#<A:0x1042c3f8>: common: v3
undefined method `method_v1' for #<A:0x1042c3f8>
undefined method `method_v2' for #<A:0x1042c3f8>
#<A:0x1042c3f8>: method v3
Below is the 3 versions of class A:
$ cat v1.rb
class A
def common
puts "#{self}: common: v1"
end
def method_v1
puts "#{self}: method v1"
end
end
$ cat v2.rb
class A
def common
puts "#{self}: common: v2"
end
def method_v2
puts "#{self}: method v2"
end
end
$ cat v3.rb
class A
def common
puts "#{self}: common: v3"
end
def method_v3
puts "#{self}: method v3"
end
end
Obviously there's a danger in totally replacing the class definition with a new class definition, whether you're merging the new version or deleting the old version and expecting objects to automatically get updated. That danger is in the fact that the old version of the object may be in an invalid state for the new version. (For example, instance variables that the new version of the class initializies in its initialize
method may not have been defined by the old version, but there could aso be subtler bugs than this). So care (and a well-planned upgrade path) is needed no matter how you pull this off.
Given that you know what the version you're upgrading from looks like (which you need in order to upgrade sensibly anyway), it's dead simple to have the new version of the class remove unneeded methods from the old version of the class:
class A
remove_method :foo
end
And I'm not sure what you're talking about when you say there's problems redefining a method to take a different number of parameters. It works fine for me:
class A
def foo a
a
end
end
ainst=A.new
p(ainst.foo 1) rescue puts($!)
p(ainst.foo 1,2) rescue puts($!)
class A
def foo a,b
[a,b]
end
end
p(ainst.foo 1) rescue puts($!)
p(ainst.foo 1,2) rescue puts($!)
The only thing you can't do (AFAIK) is change the class's superclass. That's defined the first time you define the class, and you're not allowed to change it (though you can specify the same ancestor class again).
class A < Object
end
class A < Object
end
class A < String #TypeError: superclass mismatch for class A
end
In short, there is no way to do this without some serious hacking. What I suggest you to do is to make a to_serialized
method that returns an array that the initialize
method accepts to get the same state. If you simply want to copy all instance variables over, you could do this:
class A
def initialize(instance_variables)
instance_variables.each do |key, value|
self.instance_variable_set(key, value)
end
end
def to_serialized
iv = {}
self.instance_variables.each do |key|
iv[key] = self.instance_variable_get(key)
end
end
end
And to reload the method, you could do this:
obj_state = object.to_serialized
Object.send(:remove_const, 'A')
load 'file.rb'
object = A.new(obj_state)
Note that this doesn't nest, so if any of the objects the instance variables refer to is reloaded too, you need to "serialize" them yourself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With