Ruby - Can a subclass object receive and modify instance variables from a parent object

I have a system in which the user can create a new object of the class Map that has a subclass of SubMap. The thing is, there can be multiple Map objects, and for each one of those, there can be multiple SubMap objects. I need to be able to create a SubMap object that can both get and change the Map object's @b value of the unique ID passed during the SubMap instantiation. Ex:

map1=Map.new(10,5) #Map is assigned unique ID of 1 and has a few instance variables (@b = 5)
point=Map::SubMap.new(1) #Assigned to the map object with an ID of 1, which is map1
point.change_b(8) #Change map1's @b value to 8.

Here is an expanded code example:

class Map
@@id=1
def change_b(b)
    @b=b
end
def initialize(a,b)
    @id=@@id
    @@id+=1
    @a=a
    @b=b
    @map="#{a}_#{b}"
    end
end

class SubMap < Map
    def initialize(mapId)
        @mapID=mapId
    end
    def change_b(b)
        super
    end
end

map=Map.new(5,2) #Create New Map            ID: 1, @b = 2
second_map=Map.new(6,1) #Create Test Map    ID: 2, @b = 1
point=Map::SubMap.new(1) #Just affect the map with the ID of 1
point.change_b(5) #Change map's (Instance of Map with ID of 1) instance variable of @b to 5
##second_map's @b value is unchanged.

I'll welcome any other methods for doing this (no pun intended), Thanks in advance. Also, I apologize for any formatting errors (Missing indents and such that I may have missed), lets just say me and the SO code format didn't agree on a few things.

Upvotes: 1

Views: 411

Answers (4)

Cary Swoveland
Cary Swoveland

Reputation: 110755

I suggest doing that as follows.

class Map
  @id=1
  singleton_class.send(:attr_accessor, :id)
  @id_to_instance = {}
  singleton_class.send(:attr_accessor, :id_to_instance)
  attr_reader :b

  def initialize(a,b)
    self.class.id_to_instance[self.class.id] = self
    self.class.id += 1
    @a=a
    @b=b
  end

  def change_b(b)
    @b=b
  end
end

class SubMap < Map
  attr_reader :mapID
  def initialize(mapID)
    @mapID=mapID
  end
  def change_b(b)
    self.class.superclass.id_to_instance[@mapID].change_b b
  end
end

map        = Map.new(5,2)
second_map = Map.new(6,1)
point      = SubMap.new(1)
point.change_b(5)

Confirm the value of the correct instance variable @b was changed

Map.id_to_instance[point.mapID].b
  #=> 5
  • I've used a class instance variable @id (in Map) rather than a class variable @@id because the former is shielded from subclasses, which is good practice.
  • I've created a class instance variable @id_to_instance (in Map) that maps values of the counter @id to the associated instance of Map.
  • I've created a getter and setter for the class instance variable @id (in Map), so that instances of Map can read and increment the instance variable's value.
  • I've created a getter and setter for the class instance variable @id_to_instance (in Map), so that instances of Map can set its value and instances of SubMap can read its value.
  • I've created a getter for the instance variable @b (for instances of Map) for use by SubMap.change_b.
  • Map#initialize adds a key-value pair to the hash @id_to_instance that maps the current value of the class instance variable @id to the instance of Map being created, and increments the counter @id.
  • SubMap#change obtains the value of Map's instance variable id_to_instance for the key equal to the value of the SubMap instance's instance variable @mapID and invokes the instance method :change_b, with argument b, on that instance of Map.

Upvotes: 1

sbagdat
sbagdat

Reputation: 850

Both answers are true, but old objects will garbage collected and will be lost. I think you should hold the created objects in a class array.

class Map
  @@maps = []

  def change_b(b)
    @b=b
  end

  def initialize(a,b)
    @id=@@maps.size + 1
    @a=a
    @b=b
    @map="#{a}_#{b}"
    @@maps << self
  end

  def self.find_by_id(id)
    @@maps.select {|m| m.instance_variable_get(:@id) == id}.first
  end
end

class SubMap < Map
    def initialize(mapId)
      @map = Map.find_by_id(mapId)
    end

    def change_b(b)
      @map.change_b(b)
    end
end

Map.new(5,2) # => #<Map:0x007ff94a822800 @id=1, @a=5, @b=2, @map="5_2">
Map.new(6,1) # => #<Map:0x007ff94a821fb8 @id=2, @a=6, @b=1, @map="6_1">
point=SubMap.new(1) # => #<SubMap:0x007ff94a820938 @map=#<Map:0x007ff94a822800 @id=1, @a=5, @b=2, @map="5_2">>
point.change_b(5) # => 5

Also you can instantiate SubMap directly, no need Map::SubMap.new(1).

Upvotes: 1

T. Claverie
T. Claverie

Reputation: 12256

An alternative way to do that, use your instance of map as a factory to create submaps (no complicated code here):

class Map
    @@id=1

    def change_b(b)
        @b=b
    end

    def initialize(a,b)
        @id=@@id
        @@id+=1
        @a=a
        @b=b
        @map="#{a}_#{b}"
    end

    def sub_map
        return SubMap.new(self)
    end

    def to_s
        "#{@a}_#{@b}"
    end
end

class SubMap
    def initialize(map)
        @map=map
    end

    def change_b(b)
        @map.change_b(b)
    end
end

map=Map.new(5,2) #Create New Map            ID: 1, @b = 2
second_map=Map.new(6,1) #Create Test Map    ID: 2, @b = 1
puts second_map
# 6_1

puts map
# 5_2

point = second_map.sub_map
point.change_b(5)
puts second_map
# 6_5

Upvotes: 0

Pholochtairze
Pholochtairze

Reputation: 1854

My (probably highly suboptimal) solution is to get all instances of Map, find the one who has an @id equal to the submap's @mapID. Once we have it, we set it's @b to b.

So it works as follows :

  1. ObjectSpace.each_object(Map)

It gets an enumarator of all instances of Map, you can find more infos here : How do I list all objects created from a class in Ruby?

  1. Enumerator.select {|m| m.instance_variable_get(:@id) == @mapID}

From the Enumerator we got in step 1, we select only the instances where the instance variable @id is equal to the current subMap's @mapID.

  1. MapInstance.instance_variable_set(:@b, b)

Now that we have the map whose @id is equal to the submap's @mapID, then we just have to set this map's instance variable @b to the parameter b of the method. See here for more infos : How to set private instance variable used within a method test?

Putting it all together we obtain :

class Map
  @@id=1

  def change_b(b)
      @b=b
  end

  def initialize(a,b)
    @id=@@id
    @@id+=1
    @a=a
    @b=b
    @map="#{a}_#{b}"
  end
end

class SubMap < Map
  def initialize(mapId)
      @mapID=mapId
  end

  def change_b(b)
    #https://stackoverflow.com/questions/14318079/how-do-i-list-all-objects-created-from-a-class-in-ruby
    #https://stackoverflow.com/questions/9038483/how-to-set-private-instance-variable-used-within-a-method-test
    ObjectSpace.each_object(Map).select {|m| m.instance_variable_get(:@id) == @mapID}.instance_variable_set(:@b, b)
  end
end

map=Map.new(5,2) #Create New Map            ID: 1, @b = 2
second_map=Map.new(6,1) #Create Test Map    ID: 2, @b = 1
point=Map::SubMap.new(1) #Just affect the map with the ID of 1
point.change_b(5) #Change map's (Instance of Map with ID of 1) instance variable of @b to 5
p map
#<Map:0x00000002276f80 @id=1, @a=5, @b=5, @map="5_2">

Upvotes: 0

Related Questions