Reputation: 14237
let say I have model
User < ActiveRecord::Base
end
and his STI brother
MasqueradeUser < User
end
masquerade_user = MasqueradeUser.find 123
masquerade_user.class
# => MasqueradeUser
Ridiculous as it sounds, is possible to convert this object back to parent class User
masquerade_user.some_magic.class # => User
I know I can override methods like mode_name
, is_a?(User)
and other so that MasqueradeUser will return values like User
MasqueradeUser < User
def model_name
'User'
end
end
I was just wondering if there is a way to completely downgrade object to parent class instance
Upvotes: 1
Views: 2819
Reputation: 798
If you are needing to convert models then I feel like the Accepted Answer is sufficient.
If you need becomes
like behavior and you either aren't working with ActiveRecord or do not want to include the ActiveRecord::Persistence
module then I'd suggest a variation of this Answer which has good intentions, but would suggest following the logic in becomes
more closely. Also what happens when your initialization method has required params? Then that answer starts to fall apart. The source for becomes
gets around this by passing a block to initialize
, which may be the best approach for you.
def becomes(klass)
became = klass.allocate
became.send(:initialize) do |becoming|
@attributes.reverse_merge!(becoming.instance_variable_get(:@attributes))
becoming.instance_variable_set(:@attributes, @attributes)
becoming.instance_variable_set(:@mutations_from_database, @mutations_from_database ||= nil)
becoming.instance_variable_set(:@new_record, new_record?)
becoming.instance_variable_set(:@destroyed, destroyed?)
becoming.errors.copy!(errors)
end
became
end
I believe there is value in the use of the allocate
method here, which allows us to have a method like becomes
without cluttering up the initialize
method with multiple responsibilities.
initialize
def becomes(wrapper_class)
wrapper_class.allocate.tap do |wrapper_instance|
# your logic here for initializing the wrapper_instance with the calling object's values
# ex:
@attributes.reverse_merge!(becoming.instance_variable_get(:@attributes))
wrapper_instance.instance_variable_set("@attributes", @attributes)
end
end
NOTE: Nothing is stopping you from calling new
or send(:initialize)
as seen in the source or answers, but it isn't required.
This approach gives you an instance of the wrapper class with some of the values from the original object on the wrapper instance. Therefore, this method has the single responsibility to convert classes (without any initialization params) and the initialization method (which may or may not have required params) has the single responsibility of creating an instance from scratch. The distinction may be insignificant or non-existent in some cases, but in the cases where this distinction is important, you will thank yourself for keeping these responsibilities separated.
This leverages the tap
method which does all the work of the first and last lines of the becomes
source and also gives you a block to work within, that is also found in the source. Just seemed more efficient to me. The only major difference here is that the block is not being passed to initialize
with this approach. If you want to do that then use:
initialize
def becomes(wrapper_class)
# `allocate.send(:initialize)` is equivalent to `new` without callbacks for objects like ActiveRecord::Base.
wrapper_class.allocate.send(:initialize) do |wrapper_instance|
# your logic here for initializing the wrapper_instance with the calling object's values
# ex:
@attributes.reverse_merge!(becoming.instance_variable_get(:@attributes))
wrapper_instance.instance_variable_set("@attributes", @attributes)
end
end
Upvotes: 0
Reputation: 16648
You can genericize ActiveRecord
's becomes
method as such:
def becomes(klass)
became = klass.new
became.instance_variable_set("@attributes", @attributes)
became
end
After all it's just a matter of copying variables into another object that supports them (i.e: it can "become" any class, not just the superclass)
Upvotes: 0