Attr_accessor for Subclass inside Parent Class - ruby

I'm trying to figure out a way to dynamically generate subclasses based on a parent class. In my specific case I'd want to have attr_accessor for every instance variable, initialized in my Parent class and inherited on the SubClasses.
My classes are three different models representing three different tables in a DB.
"Record" is my parent class where I want to store and write all of my code.
"Post" and "User" are the Subclasses inheriting.
My code
class Record
attr_reader :id
# attr_accessor
def initialize(**params)
#id = params[:id]
instance_variable_set("##{params.keys[0]}", params.values[0])
instance_variable_set("##{params.keys[1]}", params.values[1])
instance_variable_set(:#votes, params["votes"] || 0) if instance_of?(Post)
# p self.title
end
Want I want to achieve is setting attr_accessor as for example in my Subclass "Post" I want to call
post = Post.new(title: "New post", url: "some url")
puts post.title
I can access the title instance variable without raising a NoMethodError
Could someone guide me, or give me some hint?
Thanks

You're going about it backwards. A parent class should not have to know about or implement specific logic for its subclasses.
class Record
attr_reader :id
def initialize(**attributes)
attributes.each do |key, value|
send("#{key}=", value)
end
end
end
class Post < Record
attr_accessor :title
attr_accessor :votes
end
irb(main):066:0> Post.new(id: 1, votes: 10, title: "Hello World").title
=> "Hello World"
attr_accessor is just a metaprogramming convenience for defining methods so your accessor methods are inherited anyways. But if you're writing something like an Object Relational Manager you'll want to define your own macro method for defining attributes that lets you keep track of the attributes of a class:
module Attributes
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
#attributes ||= {}
end
end
# assigns the passed attributes to the instance
def initialize(**attributes)
attributes.each do |key, value|
send "#{key}=", value
end
end
# gets all the attributes of an instance
def attributes
self.class.attributes.keys.each_with_object(Hash.new) do |key, hash|
hash[key] = send(key)
end
end
module ClassMethods
# Inherits the attributes of the parent class
def inherited(subclass)
attributes.tap do |parent_attributes|
subclass.class_eval do
#attributes ||= {}.merge(parent_attributes)
end
end
end
# defines an attribute that is inherited
def attribute(name, type = nil, **kwargs)
#attributes[name] = { type: type }.merge(kwargs)
attr_accessor name
end
def attributes
#attributes
end
end
end
class Record
include Attributes
attribute :id, Integer
end
class Post < Record
attribute :title, String
attribute :votes, Integer
end
irb(main):101:0> Post.new(votes: 10, title: "Hello World").attributes
=> {:id=>nil, :title=>"Hello World", :votes=>10}
This stores the attribute definitions in a class instance variable which lets you attach "metadata" which opens up for features that you will want later such as typecasting, serialization and dirty tracking.

Related

How to Best Factor Out Common Class Methods

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).

create instance variable dynamically in ruby unknown number variables needed

This is the problem "I am trying to create a generic object, could be thought of as a "dynamic schema
object" each schema object will have a different number of instances variables." and this approach doesn't work.
class GenericObjectArray
def initialize
#data_fields = []
end
def data_fields(t)
#data_fields << t
end
def initialize(attrs = {})
attrs.each { |attr,val| instance_variable_set "##{attr}", val }
end
end
p GenericObjectArray.new(:data_fields=> "may_sales", :data_fields=>"june_sales", :data_fields=>"july_sales")
This is my approach, bu it doesnt work. I would like to set may_sales, june_sales, july_sales as an instance variables. Set all three as instance variables. It only returns that last one.
GenericObjectArray:0x007f8c5b883cd8 #data_fields="july_sales"
Think it from this approach:
You have objects (lets say GenericObject)
Objects have many attributes (GenericObject#attributes => [GenericObject::Attribute])
Attributes have a name, a value, and a type (GenericObject::Attribute#value, #name and #type)
Which translates into code like this:
class GenericObject
attr_accessor :attributes
def add_attribute(name, value, type)
(#attributes ||= []) << Attribute.new(name, value, type)
end
class Attribute
attr_accessor :name, :value, :type
def initialize(name, value, type)
#name, #value, #type = name, value, type
end
end
end
# so...
cat = GenericObject.new
cat.add_attribute :leg_number, 4, :integer
cat.add_attribute :fur_color, 'Orange', :color
cat.add_attribute :name, 'Garfield', :string
cat.attributes.each { |attr| puts "My cat's #{attr.name} is #{attr.value} (#{attr.type})" }
# My cat's leg_number is 4 (integer)
# My cat's fur_color is Orange (color)
# My cat's name is Garfield (string)
You can make a fancy initializer for GenericObject or whatever you see fit.
Or you can just to a little fix
class GenericObjectArray
def initialize(attrs = {})
attrs.each { |attr,val| instance_variable_set "##{attr}", val }
end
end
GenericObjectArray.new(:data_fields=> ["may_sales", "june_sales", "july_sales"])

Ruby metaprogramming: accessing class name

I'm fairly new to Ruby metaprogramming. I'm trying to write code which generates the
"dup" function for a class when it's created, using a list of fields which should be passed into the constructor. However, I can't figure out how to get access to the name of the class I'm creating, while I'm creating it.
So for example, if I had this code:
class Example
make_dup :name, :value
attr_accessor :name, :value
def initialize(name,value)
#name, #value = name, value
end
end
I'd want it to create the method:
def dup
Example.new(name,value)
end
I'm just getting stuck on how it would figure out to insert Example there.
Note that all classes have built-in dup and clone methods. You can customize what happens in them by adding an initialize_copy method, e.g.:
class Foo
attr_accessor :bar
def initialize_copy(orig)
super
#bar = #bar.dup
end
end
In case that isn't what you're truly looking for, you can access an object's class using its class method:
class Foo
def p_class
p self.class # Foo.new.p_class => Foo ; self is *a* `Foo'
end
def self.p_class
p self.class # Foo.p_class => Class ; self *is* `Foo'
end
end
def dup
self.class.new(name,value)
end
Maybe you can implement it this way:
module MyDup
def make_dup(*args)
define_method(:my_dup) do
obj = self.class.new(nil, nil)
args.each do |arg|
obj.send(arg.to_s + "=", self.send(arg))
end
obj
end
end
end
class Example
extend MyDup
make_dup :name, :value
attr_accessor :name, :value
def initialize(name,value)
#name, #value = name, value
end
end
e = Example.new("John", 30)
p e
d = e.my_dup
p d
Execution result as follows:
#<Example:0x000000022325d8 #name="John", #value=30>
#<Example:0x00000002232358 #name="John", #value=30>

Defining class methods in Ruby

Typically when writing a model in Rails you use a DSL to setup various aspects of derived objects, for example:
class Question < ActiveRecord::Base
has_one :category
validates_presence_of :category
end
In this case, "has_one" and "validates_presence_of" create associations and validation call backs on models instantiated from Question.
I want to add a new method called "parent" to be used when defining a class:
class Question
attr_accessor :category
parent :category
end
q = Question.new
q.category = 'a category'
puts q.parent
-> 'a category'
So when objects are instantiated from class, they should have the method "parent" defined.
How do I do this? My first thought was to use a module, but this isn't an instance method, or a class method.
I believe this is what you are looking for:
module QuestionParent
module ClassMethods
def inherited(descendant)
descendant.instance_variable_set(:#parent, parent.dup)
super
end
def parent(args=nil)
#parent ||= args
end
end
module InstanceMethods
def parent
self.send self.class.parent.to_sym
end
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
end
end
class Question
include QuestionParent
attr_accessor :category
parent :category
end
Which produces:
q = Question.new
q.category = 'a category'
puts q.parent
a category
What this does is add a class method parent that will define the class variable #parent, when an instance calls the parent in the InstanceMethod the #parent symbol (here is category) is called.

Creating a class dynamically

I'm trying to create a new class, without knowing the name of the class until it's supposed to be created.
Something like this;
variable = "ValidClassName"
class variable
end
Test = ValidClassName.new
If possible, i'd also appreciate som hints on how to dynamically add attributes (and methods) to this new class.
I'll be retreiving 'settings' for the class, and they will look something like this:
title :Person
attribute :name, String
attribute :age, Fixnum
But should not be designed to accept only that explicit file, the attributes might differ in number end type.
Which in the end will generate a class that should look something like:
class Person
def initialize(name, age)
#name_out = name
#age_out = age
end
end
Help?
A class gains its name when it is assigned to a constant. So It's easy to do in a generic fashion with const_set.
For example, let's say you want to use Struct to build a class with some attributes, you can:
name = "Person"
attributes = [:name, :age]
klass = Object.const_set name, Struct.new(*attributes)
# Now use klass or Person or const_get(name) to refer to your class:
Person.new("John Doe", 42) # => #<struct Person name="John Doe", age=42>
To inherit from another class, replace the Struct.new by Class.new(MyBaseClass), say:
class MyBaseClass; end
klass = Class.new(MyBaseClass) do
ATTRIBUTES = attributes
attr_accessor *ATTRIBUTES
def initialize(*args)
raise ArgumentError, "Too many arguments" if args.size > ATTRIBUTES.size
ATTRIBUTES.zip(args) do |attr, val|
send "#{attr}=", val
end
end
end
Object.const_set name, klass
Person.new("John Doe", 42) # => #<Person:0x007f934a975830 #name="John Doe", #age=42>
Your code would look something akin to this:
variable = "SomeClassName"
klass = Class.new(ParentClass)
# ...maybe evaluate some code in the context of the new, anonymous class
klass.class_eval { }
# ...or define some methods
klass.send(:title, :Person)
klass.send(:attribute, :name, String)
# Finally, name that class!
ParentClass.send(:const_set, variable, klass)
...or you could just use eval:
eval <<DYNAMIC
class #{name}
title :Person
attribute :name, String
# ...or substitute other stuff in here.
end
DYNAMIC

Resources