Leo
Leo

Reputation: 2103

inheriting attr_accessor inside a constant

I have a class with attr_accessor set like this:

class Human
    ATTRIBUTES = [:name]
    attr_accessor *ATTRIBUTES
end

it works like a charm, allows me to keep attributes inside ATTRIBUTES constant. Problem is I would like to have a class Student inheriting from a Human class, without the need to put attr_accessor every time. Basically what i would like to have is this:

class Student < Human
    ATTRIBUTES = [:name, :school]
end

unfortunately when i do

Student.new.school

i get no method error, because attr_accessor is loaded from Human and not a Student. What construction should i use to accomplish my goal?

Upvotes: 2

Views: 1522

Answers (2)

lcguida
lcguida

Reputation: 3847

Well, while I don't get the need to keep the attributes in a array, Student class will already inherit the attr_accessor's defined in it's parent class.

For example:

class Human
  attr_accessor :name, :gender
end

class Student < Human
  attr_accessor :school
end

Student class now has :name, :gender and :school attr_accessor's:

 > Student.new.respond_to?(:name)
 => true
 > Student.new.respond_to?(:name=)
 => true
 > Student.new.respond_to?(:school)
 => true
 > Student.new.respond_to?(:school=)
 => true

Human also responds to :name and :gender

> Human.new.respond_to?(:name)
=> true
> Human.new.respond_to?(:gender)
=> true

But not to school

> Human.new.respond_to?(:school)
=> false

It's cleaner, it's the ruby way, easier to understand.

Upvotes: 2

Alexandre Angelim
Alexandre Angelim

Reputation: 6753

I personally agree with @lcguida's answer, but I came up with a little experiment if you insist on following the pattern you proposed. The other answers already covered why your solution didn't work, so I'm not getting into that here.

The first thing that came to mind was to call attr_accessor on the self.inherited callback on the parent class, but unfortunately the child's body is not loaded until later. Even so, where there's a will, there's a way. If you're using Ruby 2.0 or later, the following implementation will work.

module LazyAttrAccessorizer
  def self.extended(obj)
    TracePoint.trace(:end) do |t|
      if obj == t.self
        obj.send :attr_accessor, *obj::ATTRIBUTES
        t.disable
      end
    end
  end
end

class Human
  extend LazyAttrAccessorizer
  ATTRIBUTES = [:name]
  def self.inherited(subclass)
    subclass.extend LazyAttrAccessorizer
  end
end

class Student < Human
  ATTRIBUTES = [:name, :school]
  # ATTRIBUTES = [:school] would also work as expected, but I think you'd like to be literal there.
end

> Student.new.respond_to?(:name)
=> true
> Student.new.respond_to?(:school)
=> true

Upvotes: 3

Related Questions