I am building an in-memory instance model in Ruby. There are a bunch of classes that each get instantiated and managed by class methods on that class. There are a bunch of those class methods, e.g. list all instances, retrieve all instances, etc.
The code for these methods is common across all classes and does not need to take any account of any particularities of those classes. Hence, I would like that code to live in a common place. See the list
method below. My question: How to best achieve this.
class A
attr_reader :value
@@instances = []
def initialize(value:)
@value = value; @@instances << self
end
def self.list
@@instances.each { |i| puts "#{i.value}"}
end
end
class B
attr_reader :value
@@instances = []
def initialize(value:)
@value = value; @@instances << self
end
def self.list
@@instances.each { |i| puts "#{i.value}"}
end
end
A.new(value: '100')
A.new(value: '101')
B.new(value: '200')
B.new(value: '201')
A.list
B.list
Ideally, I define the list method only once. I have also tried moving that to a super-class:
class Entity
def self.list
@@instances.each { |i| puts "AB: #{i.value}"}
end
end
class A < Entity
attr_reader :value
@@instances = []
def initialize(value:)
@value = value; @@instances << self
end
end
class B < Entity
attr_reader :value
@@instances = []
def initialize(value:)
@value = value; @@instances << self
end
end
...but as one would expect the super-class cannot access the @@instances
array of its sub-classes. Moving the @@instances
array to the super-class results in the array being common to all classes, which is not what I need.
The main change you need to make is to use class instance variables rather than class variables. For reasons explained here class variables should be used sparingly; class instance variables are generally a better choice, as is illustrated nicely by this question.
class Entity
attr_reader :value
class << self
attr_reader :ins
end
def self.inherited(klass)
klass.instance_variable_set(:@ins, [])
end
def initialize(value:)
@value = value
self.class.ins << self
end
def self.list
@ins.each { |i| puts "#{i.value}"}
end
end
class A < Entity; end
class B < Entity; end
A.new(value: '100')
#=> #<A:0x00005754a59dc640 @value="100">
A.new(value: '101')
#=> #<A:0x00005754a59e4818 @value="101">
A.list
# 100
# 101
B.new(value: '200')
#=> #<B:0x00005754a59f0910 @value="200">
B.new(value: '201')
#=> #<B:0x00005754a59f8b88 @value="201">
B.list
# 200
# 201
I defined a getter for the class instance variable @ins
in Entity
's singleton class1:
class << self
attr_reader :ins
end
When subclasses of Entity
are created the callback method Class::inherited is executed on Entity
, passing as an argument the class that has been created. inherited
creates and initializes (to an empty array) the class instance variable @ins
for the class created.
Another way of doing that, without using a callback method, is as follows.
class Entity
attr_reader :value
class << self
attr_accessor :ins
end
def initialize(value:)
@value = value
(self.class.ins ||= []) << self
end
def self.list
@ins.each { |i| puts "#{i.value}"}
end
end
The fragment:
(self.class.ins ||= [])
sets @ins
to an empty array if @ins
equals nil
. If @ins
is referenced before it is created, nil
is returned, so either way, @ins
is set equal to []
. In order to execute this statement I needed to change attr_reader :ins
to attr_accessor :ins
in order to perform the assignment @ins = []
(though I could have used instance_variable_set
instead).
Note that if I were to add the line @ins = []
to Entity
(as th first line, say), the instance variable @ins
would be created for every subclass when the subclass is created, but that instance variable would not be initialized to an empty array, so that line would serve no purpose.
1. Alternatively, one could write, singleton_class.public_send(:attr_reader, :ins)
.