Ruby class instance variables and inheritance
I have a Ruby class called LibraryItem
. I want to associate with every instance of this class an array of attributes. This array is long and looks something like
['title', 'authors', 'location', ...]
Note that these attributes are not really supposed to be methods, just a list of attributes that a LibraryItem
has.
Next, I want to make a subclass of Libr开发者_StackOverflow中文版aryItem
called LibraryBook
that has an array of attributes that includes all the attributes of LibraryItem
but will also include many more.
Eventually I will want several subclasses of LibraryItem
each with their own version of the array @attributes
but each adding on to LibraryItem
's @attributes
(e.g., LibraryBook
, LibraryDVD
, LibraryMap
, etc.).
So, here is my attempt:
class LibraryItem < Object
class << self; attr_accessor :attributes; end
@attributes = ['title', 'authors', 'location',]
end
class LibraryBook < LibraryItem
@attributes.push('ISBN', 'pages')
end
This does not work. I get the error
undefined method `push' for nil:NilClass
If it were to work, I would want something like this
puts LibraryItem.attributes
puts LibraryBook.attributes
to output
['title', 'authors', 'location']
['title', 'authors', 'location', 'ISBN', 'pages']
(Added 02-May-2010)
One solution to this is to make @attributes
a simple instance variable and then add the new attributes for LibraryBoot
in the initialize
method (this was suggested by demas in one of the answers).
While this would certainly work (and is, in fact, what I have been doing all along), I am not happy with this as it is sub-optimal: why should these unchanging arrays be constructed every time an object is created?
What I really want is to have class variables that can inherit from a parent class but when changed in the child class do not change in the the parent class.
Another solution would be to use the inherited hook:
class LibraryItem < Object
class << self
attr_accessor :attributes
def inherit_attributes(attrs)
@attributes ||= []
@attributes.concat attrs
end
def inherited(sublass)
sublass.inherit_attributes(@attributes)
end
end
@attributes = ['title', 'authors', 'location',]
end
class LibraryBook < LibraryItem
@attributes.push('ISBN', 'pages')
end
Since you mention that the attributes are "fixed" and "unchanging", I am assuming that you mean that you will never change their value once the object is created. In that case, something like the following should work:
class Foo
ATTRS = ['title', 'authors', 'location']
def attributes
ATTRS
end
end
class Bar < Foo
ATTRS = ['ISBN', 'pages']
def attributes
super + ATTRS
end
end
You are manually implementing a reader method (instead of letting attr_accessor
create it for you) that disguises the internal name of the array. In your subclass, you simply call the ancestor class' reader function, tack on the additional fields associated with the child class, and return that to the caller. To the user, this appears like a read-only member variable named attributes
that has additional values in the sub-class.
Just as a version:
class LibraryItem < Object
def initialize
@attributes = ['one', 'two'];
end
end
class LibraryBook < LibraryItem
def initialize
super
@attributes.push('three')
end
end
b = LibraryBook.new
Out of curiosity, will something like this work?
class Foo
ATTRIBUTES = ['title','authors','location']
end
class Bar < Foo
ATTRIBUTES |= ['ISBN', 'pages']
end
This would seem to produce the desired result - the ATTRIBUTES array is expanded when the class object is created, and the values of ATTRIBUTES varies as expected:
> Foo::ATTRIBUTES
=> ['title','authors','location']
> Bar::ATTRIBUTES
=> ['title','authors','location', 'ISBN', 'pages']
To expand on @Nick Vanderbilt's answer, using active_support you do this, which is exactly the short hand I want for this functionality. Here's a complete example:
require 'active_support/core_ext'
class Foo
class_attribute :attributes
self.attributes = ['title','authors','location']
end
class Bar < Foo
self.attributes = Foo.attributes + ['ISBN', 'pages']
end
puts Foo.attributes.inspect #=> ["title", "authors", "location"]
puts Bar.attributes.inspect #=> ["title", "authors", "location", "ISBN", "pages"]
Shame it's so difficult for ruby to achieve this without needing a library for it. It's the only thing I miss from python. And in my case, I don't mind the dependency on the active_support gem.
ActiveSupport has class_attribute method in rails edge.
In LibraryBook variable @attributes is a new independent variable, instance variable of object LibraryBook, so its not initialized and you get error "undefined method ... for nil"
You should to initialize it by LibraryItem attribut's list before using
class LibraryBook < LibraryItem
@attributes = LibraryItem::attributes + ['ISBN', 'pages']
end
This is for strings (anything really), rather than arrays, but...
class A
def self.a
@a || superclass.a rescue nil
end
def self.a=(value)
@a = value
end
self.a = %w( apple banana chimp )
end
class B < A
end
class C < B
self.a += %w( dromedary elephant )
end
class D < A
self.a = %w( pi e golden_ratio )
end
irb(main):001:0> require 'test2'
=> true
irb(main):002:0> A.a
=> ["apple", "banana", "chimp"]
irb(main):003:0> B.a
=> ["apple", "banana", "chimp"]
irb(main):004:0> C.a
=> ["apple", "banana", "chimp", "dromedary", "elephant"]
irb(main):005:0> D.a
=> ["pi", "e", "golden_ratio"]
irb(main):006:0> A.a = %w( 7 )
=> ["7"]
irb(main):007:0> A.a
=> ["7"]
irb(main):008:0> B.a
=> ["7"]
irb(main):009:0> C.a = nil
=> nil
irb(main):010:0> C.a
=> ["7"]
You can do it using CONSTANTS also. No check though.
class LibraryItem < Object
class << self; attr_accessor :attributes; end
ATTRIBUTES = ['title', 'authors', 'location',]
end
class LibraryBook < LibraryItem
ATTRIBUTES .push('ISBN', 'pages']
end
精彩评论