Is there a way to make only one class of a variable inherit from another class?

Viewed 46

I'm a newbie and maybe I missed something on my searches, but I went through the documentations and through the other questions here and did not find the answer to my question, so I will put it here and please excuse me if it is already explained anywhere else. I have two classes in ruby, one called Animal and the other one Animal_Group. Inside the initialize of Animal, I have a variable called @family, and I would like to compare this variable with the already existing instances of Animal_Group and, if there's a match, I wanted it to inherit the definitions (including values for Animal_Group class intialize variables itself) of the class Animal_Group and of the instance of it.

Just to ilustrate, it would be something like that:

class Animal
  def initialize(ident, species, family, age)
    @ident = ident
    @species = species
    @family = family
    if family.class == Animal_Group
      super(*args)
    end
    @age = age
  end
end

So it would result on something like that:

  1. I create an animal "kit" that is a 'cat' with age '7' from the 'feline' family.
  2. There is a instance of Animal_Group called 'feline' with the variables @lifespan = '17', @environment_sphere = 'terrestrial' and @breed_max = '6'
  3. 'kit' would them have the variables and it's values as follows:
@ident = 'kit'
@species = 'cat'
@family = 'feline'
@age = '7'
@lifespan = '17'
@environment_sphere = 'terrestrial'
@breed_max = '6'

Is there any way to do that or the only way is to turn everything to new classes?

2 Answers

Basically, without metaprogramming then this isn't possible. Although I could show how to do that with metaprogramming, I will leave that as an exercise to the reader, because when it comes down to it, it's better to keep things simple, and there's definitely a simpler way to do it.

Anyway, what I would recommend is a system like this:

class AnimalGroup
  attr_reader :lifespan, :environment_sphere, :breed_max, :age
end

class Feline < AnimalGroup
  def initialize
    @lifespan = '17'
    @environment_sphere = 'terrestrial'
    @breed_max = '6'
  end
end

class Cat < Feline
  def initialize(age)
    @age = age
    super
  end
end

I understand that this might seem a little less flexible than the code you wanted to write. Instead of storing the "type" of the animal group as a dynamic instance variable, we're basically hard-coding it with the class definition. But hopefully you can see how this approach is simpler.

Suppose

class Animal_Group
  attr_reader :name, :lifespan, :breed_mix
  def initialize(name, lifespan, breed_mix)
    @name = name
    @lifespan = lifespan
    @breed_mix = breed_mix
  end
end
Animal_Group.new("Boots", 18, "Siamese")
  #=> #<Animal_Group:0x000056fa3111a878 @name="Boots", @lifespan=18,
  #     @breed_mix="Siamese"> 
Animal_Group.new("Hank", 14, "Tabby")
  #=> #<Animal_Group:0x000056fa3114f618 @name="Hank", @lifespan=14,
  #     @breed_mix="Tabby"> 

I assume an instance of the argument family in Animal.new will be 'Boots' or 'Hank', as in

Animal.new('kit', 'Hank', 7)

If I misunderstood the question there is no reason to read further.

There are two approaches that could be taken to use information from instances of Animal_Group to assign values to instance variables for instances of Animal. In both cases the trick is to find the instance of Animal_Group whose instance_variable @name has a value equal to the value of the argument family in Animal.new

Use ObjectSpace::each_object

class Animal
  def initialize(ident, family, age)
    @ident = ident
    @family = family
    @age = age
    family_instance = ObjectSpace.each_object(Object).find do |o|
      o.class == Animal_Group && o.name == family
    end
    # raise exception here if family_instance is nil
    @lifespan = family_instance.lifespan
    @breed_mix = family_instance.breed_mix
  end
end
Animal.new('kit', 'Hank', 7)
  #=> #<Animal:0x000056fa31211f10 @ident="kit", @family="Hank",
  #     @age=7, @lifespan=14, @breed_mix="Tabby"> 
Animal.new('kat', 'Boots', 4)
  #=> #<Animal:0x000056fa31174918 @ident="kat", @family="Boots",
  #     @age=4, @lifespan=18, @breed_mix="Siamese">

Maintain a list of instances of Animal_Group

class Animal_Group
  @name_to_instance = {}
  singleton_class.public_send(:attr_reader, :name_to_instance)
  attr_reader :name, :lifespan, :breed_mix
  def initialize(name, lifespan, breed_mix)
    @name = name
    @lifespan = lifespan
    @breed_mix = breed_mix
    self.class.name_to_instance[name] = self
  end
end

Note that

singleton_class.public_send(:attr_reader, :name_to_instance)

(where self #=> Animal_Group) creates a read accessor for the class instance variable @name_to_instance which holds a hash that maps instance names to instances.

Animal_Group.new("Boots", 18, "Siamese")
  #=> #<Animal_Group:0x000056fa311b6110 @name="Boots", @lifespan=18,
  #     @breed_mix="Siamese"> 
Animal_Group.new("Hank", 14, "Tabby")
  #=> #<Animal_Group:0x000056fa311e9998 @name="Hank", @lifespan=14,
  #     @breed_mix="Tabby"> 

Note

Animal_Group.name_to_instance
  #=> {"Boots"=>#<Animal_Group:0x000056fa311b6110 @name="Boots",
  #      @lifespan=18, @breed_mix="Siamese">,
  #    "Hank"=>#<Animal_Group:0x000056fa311e9998 @name="Hank",
  #      @lifespan=14, @breed_mix="Tabby">} 
class Animal
  def initialize(ident, family, age)
    @ident = ident
    @family = family
    @age = age
    family_instance = Animal_Group.name_to_instance[family]
    # raise exception here if family_instance is nil
    @lifespan = family_instance.lifespan
    @breed_mix = family_instance.breed_mix
  end
end
Animal.new('kit', 'Hank', 7)
  #=> #<Animal:0x000056fa311fd9c0 @ident="kit", @family="Hank",
  #     @age=7, @lifespan=14, @breed_mix="Tabby"> 
Animal.new('kat', 'Boots', 4)
  #=> #<Animal:0x000056fa31212cf8 @ident="kat", @family="Boots",
  #     @age=4, @lifespan=18, @breed_mix="Siamese"> 
Related