user200783
user200783

Reputation: 14346

In Ruby, how to count the number of instances created (including subclasses)?

The following class Point contains a class instance variable @count that counts the number of instances created. It seems to work fine:

class Point
    @count = 0
    class << self
        attr_accessor :count
    end

    def initialize(position)
        @position = position
        self.class.count += 1
    end
end

However, if I add a subclass ColoredPoint and I want @count to include the number of these created too:

class ColoredPoint < Point
    def initialize(position, color)
        super(position)
        @color = color
    end
end

This gives an error when calling ColoredPoint.new(1, 'red'):

undefined method '+' for nil (NoMethodError)
    self.class.count += 1

How can I make this work? Do I have to use a class variable (@@count), even though I've read that "you should avoid them at all costs"?

Upvotes: 0

Views: 167

Answers (4)

Stefan
Stefan

Reputation: 114237

You could approach it like this:

class Point
  class << self
    def count
      @count ||= 0
    end

    def increment_count
      superclass.increment_count if superclass <= Point
      @count = count + 1
    end
  end

  def initialize(position)
    self.class.increment_count
    @position = position
  end
end

The line superclass.increment_count if superclass <= Point is for subclasses of Point. It will call increment_count on the superclass if the superclass is Point or one of its subclasses. This way, the count gets propagated up the inheritance chain:

Point.new(123)

Point.count        #=> 1
ColoredPoint.count #=> 0

ColoredPoint.new(456, "red")

Point.count        #=> 2 
ColoredPoint.count #=> 1

If you want to handle things a little more fundamentally, you could override the .new class method and do the counting there. This is a bit unusual but if you want to have the class keep track of its instances, it might be a good fit.

With this approach the explicit call inside initialize isn't needed anymore. Likewise, it makes super calls from initialize in other subclasses optional. You can also declare the counting method protected which limits the caller to subclasses of Point (the classes, not their instances):

class Point
  class << self
    def count
      @count ||= 0
    end

    def new(...)
      increment_count
      super
    end

    protected

    def increment_count
      superclass.increment_count if superclass <= Point
      @count = count + 1
    end
  end

  def initialize(position)
    @position = position # <- no counting logic here
  end
end

class ColoredPoint < Point
  def initialize(position, color)
    @position = position # <- no call to super needed (although recommended)
    @color = color
  end
end

Upvotes: 1

engineersmnky
engineersmnky

Reputation: 29588

Another option would be to use a separate class or module to track the count.

For instance something like this might work for you:

module ClassCounter 

  def self.extended(base)
    base.prepend(InstanceMethods)
    base.extend(ClassMethods)
    _counter[base.counter_name] = 0
  end

  def self.counter
    _counter.dup
  end 

  module ClassMethods 
    def count
      ClassCounter.counter[counter_name]
    end 
    def counter_name 
      self.name 
    end
    def counter_names
      @counter_names ||= ancestors.map(&:name) & ClassCounter.counter.keys
    end
    def inherited(subclass)
      ClassCounter._counter[subclass.counter_name] = 0
    end
  end

  module InstanceMethods 
    def initialize(*)
      self.class.counter_names.each do |name|
        ClassCounter._counter[name] += 1
      end 
      super(*)
    end
  end

  def self._counter
    @_counter ||= {} 
  end
end 

This will count subclasses separately while tracking all of the subclass instances as instances of the parental class as well:

class Point
    extend ClassCounter 
end
class ColoredPoint < Point; end
class OpaquePoint < ColoredPoint; end 

Result:

Point.new
Point.count 
#=> 1 
ColoredPoint.new
ColoredPoint.count
#=> 1 
Point.count
#=> 2
OpaquePoint.new
OpaquePoint.count 
#=> 1
ColoredPoint.count
#=> 2
Point.count
#=> 3 

Upvotes: 1

Rajagopalan
Rajagopalan

Reputation: 6064

Use this way. Remember Point objects are counted separately from ColoredPoint objects.

class Point
  @count = 0

  class << self
    attr_accessor :count
  end

  def initialize(position)
    @position = position
    self.class.count += 1
  end
end

class ColoredPoint < Point
  @count = 0

  class << self
    attr_accessor :count
  end

  def initialize(position, color)
    super(position)
    @color = color
  end
end

Point.new([0, 0])
Point.new([0, 1])
ColoredPoint.new([1, 1], 'red')
ColoredPoint.new([2, 2], 'blue')

puts Point.count # Output: 2
puts ColoredPoint.count # Output: 2

Upvotes: 1

nik0x1
nik0x1

Reputation: 1461

Try to define class variable in the Point:

@@count = 0

And increment it in the Point constructor:

@@count += 1

Full example:

class Point
    @@count = 0
    
    def self.number
        @@count
    end

    def initialize(position)
        @position = position
        @@count += 1
    end
end


class ColoredPoint < Point
    def initialize(position, color)
        super(position)
        @color = color
    end
end

Point.new(1)
ColoredPoint.new(1, 'red')
ColoredPoint.new(1, 'blue')

# ptint 3
puts Point.number

Upvotes: 0

Related Questions