In my learning of Ruby, here is a simple code I'm trying to make it work.
It is an implementation of instance_counter using class level variables (@instance_count
).
Please note, I'm not using class variable (@@
), because I want every layers of
class hierarchy (Foo
and Bar
) to have its own counter.
An important point is that I don't want the counters to be accessible in write from the outside of the class hierarchy. This is the purpose of defining the setter private.
class Foo
@instance_count = 0
class << self
attr_accessor :instance_count
end
private_class_method :instance_count=
def initialize
Foo.instance_count += 1
end
end
class Bar < Foo
@instance_count = 0
def initialize
super
Bar.instance_count += 1
end
end
p "Foo count #{Foo.instance_count}, Bar count #{Bar.instance_count}"
# => Foo count 0, Bar count 0
f = Foo.new # ERROR 'Foo#initialize': private method 'instance_count=' called for class Foo (NoMethodError)
b = Bar.new
p "Foo count #{Foo.instance_count}, Bar count #{Bar.instance_count}"
# => Foo count 2, Bar count 1 (that i would have expected without the error)
The only way I can make it "working" was to by-pass the privacy in initialize
and use Foo.send(:instance_count=, Foo.instance_count + 1)
. But, this looks
more as a patch than a solution from my point of view.
My goal here is more to understand what I'm missing about the Ruby concepts around Class Level Variable and privacy definition.
Attempting to call a private method with an explicit receiver (other than self
) results in a NoMethodError
(see the docs on visibility). Class method are no exception to this rule and instances don't have special privileges for accessing private class methods, even for their very own class.
You could move the whole logic into the class, e.g. by overriding new
. This way the counting is handled entirely by the class and doesn't depend on the instances.
class Foo
class << self
def new(...)
self.instance_count += 1
super
end
def instance_count
@instance_count ||= 0
end
private
def instance_count=(count)
@instance_count = count
end
end
def initialize
# nothing to do here
end
end
class Bar < Foo
# will work just fine
end
This gives:
Foo.new
Foo.new
Bar.new
Foo.instance_count #=> 2
Bar.instance_count #=> 1
As well as:
Foo.instance_count = 3 # NoMethodError: private method 'instance_count=' called for class Foo
Note that I'm lazily initializing the class instance variable inside the getter. That's because modules and classed don't have the typical initialize
mechanic. (you can implement that method but it won't be called)