equivalent8
equivalent8

Reputation: 14237

is there way to convert object into another class in ruby

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

Answers (3)

SMAG
SMAG

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.

Source for: ActiveRecord::Persistence#becomes

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.

Proposed manual implementation, without 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:

Proposed manual implementation, with 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

Yehuda Zargarov
Yehuda Zargarov

Reputation: 277

You can use becomes function of ActiveRecord - see here.

Upvotes: 7

Louis Kottmann
Louis Kottmann

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

Related Questions