Override ruby constant in subclass so inherited methods use new constant instead of the old?
I've done this by simply redefining the constant in the subclass, and then referring to it in methods as self.class::CONST
in instance methods and self::CONST
in class methods. In your example:
class SuperClass
CONST = "Hello, world!"
def self.say_hello
self::CONST
end
end
class SubClass < SuperClass
CONST = "Hello, Bob!"
end
SubClass.say_hello #=> "Hello, Bob!"
Modifying a constant for a Subclass
Constants are name spaced within the class or module they are defined. They are resolved through the usual ancestors path. In your subclass you can define a constant of the same name as one in the superclass, and the expression initializing it can reference the superclass's constant as the subclass's constant won't be defined until after the initial assignment. Like this:
$ pry
[1] pry(main)> class A; Items = [[1, 3, 5], [2, 4, 6]]; end
=> [[1, 3, 5], [2, 4, 6]]
[2] pry(main)> class B < A; end
=> nil
[3] pry(main)> class B; Items; end
=> [[1, 3, 5], [2, 4, 6]]
[4] pry(main)> A::Items
=> [[1, 3, 5], [2, 4, 6]]
[5] pry(main)> B::Items
=> [[1, 3, 5], [2, 4, 6]]
[6] pry(main)> class B; Items = Items.dup << [7,8,9]; end
=> [[1, 3, 5], [2, 4, 6], [7, 8, 9]]
[7] pry(main)> A::Items
=> [[1, 3, 5], [2, 4, 6]]
[8] pry(main)> B::Items
=> [[1, 3, 5], [2, 4, 6], [7, 8, 9]]
When deriving the new constant, be careful to dup
the original if you plan to modify it with a mutating method (like Array#<<
). See the trap:
[9] pry(main)> class A; Foo = [[1,2],[3,4]]; end
=> [[1, 2], [3, 4]]
[10] pry(main)> A::Foo
=> [[1, 2], [3, 4]]
[11] pry(main)> class B; Foo = Foo << [5,6]; end
=> [[1, 2], [3, 4], [5, 6]]
[12] pry(main)> B::Foo
=> [[1, 2], [3, 4], [5, 6]]
[13] pry(main)> A::Foo
=> [[1, 2], [3, 4], [5, 6]]
[14] pry(main)> B::Foo.object_id == A::Foo.object_id
=> true
[15] pry(main)> B::Items.object_id == A::Items.object_id
=> false
You can explicitly reference the constant in the parent namespace without naming the superclass using Class#superclass
[16] pry(main)> class B; superclass::Items; end
=> [[1, 3, 5], [2, 4, 6]]
Using overridden class constants in class methods
Because define_method is running in lexical scope, i.e. it's inline in the body of the Foo class definition so there is nothing to cause it to run in Bar.
class Foo
CONST = [:foo, :baz]
def self.define_const_methods(const)
const.each do |c|
define_method("#{c}?") { "#{c} exists" }
end
end
define_const_methods(CONST)
end
class Bar < Foo
CONST = [:foo, :bar]
define_const_methods(CONST)
end
That should do the trick. So you call define_const_methods at the end of the Foo class in it's lexical scope. And you also call it on any class that inherits from it. The inheriting class should find it's own version of that constant.
But this is pretty ugly, so you could dispense with the constants altogether and just use the define_const_methods to define them. Just like ActiveRecord does when it defines the association methods (has_one, has_many etc). So then you can whittle it down to;
class Foo
def self.define_my_methods(meths)
meths.each do |c|
define_method("#{c}?") { "#{c} exists" }
end
end
define_my_methods [:foo, :baz]
end
class Bar < Foo
define_my_methods [:foo, :bar]
end
Dynamic metaprogrammed methods on inheritance
Try something like this
class Animal
def initialize
@name ||= "no name"
end
%w(bark walk).each do |action|
define_method(action) do
"#{@name} #{action}"
end
end
end
class Pig < Animal
def initialize
@name = 'piggie'
end
end
Animal.new.walk # => "no name walk"
Pig.new.walk
Dynamic metaprogrammed methods on inheritance
Try something like this
class Animal
def initialize
@name ||= "no name"
end
%w(bark walk).each do |action|
define_method(action) do
"#{@name} #{action}"
end
end
end
class Pig < Animal
def initialize
@name = 'piggie'
end
end
Animal.new.walk # => "no name walk"
Pig.new.walk
Ruby class constants and inheritance mystery
Constants in ruby are a bit of a misnomer. Reassigning a constant produces a warning:
Foo=1
Foo=2
(irb):5: warning: already initialized constant Foo
But nothing stops you mutating the actual values themselves, which push
does. If you want to prevent this happening, then you can freeze the array, i.e.
class LibraryItem
ATTRIBUTES = ['title', 'authors', 'location'].freeze
end
Attempts to mutate the array will now raise an exception. Only the array is frozen though, so you could do something like
LibraryItem::ATTRIBUTES.first.upcase!
(assuming you haven't got frozen string literals turned on) and that change will be allowed. I'm not aware of a way around that other than freezing the strings individually (or turning on frozen string literals for that file, on ruby 2.3 and above)
Getting the owner of a constant
Like, the below code:
class A
Foo = true
end
class B < A
end
B.ancestors.find { |klass| klass.const_defined? :Foo, false }
# => A
Cloning Classes in Ruby
So it seems the consensus here is that there's no easy way of making clone work the way you might expect in this context. There are a lot of alternative solutions, but none of them work exactly the way you might expect clone
to.
First of all, it's possible to fix the problem with the constants by editing the original class to refer to self::HELLO
in place of just HELLO
:
# Before:
class Foo
HELLO = "Hello, world!"
def self.say_hello
HELLO
end
end
Bar = Foo.clone
Bar.say_hello # Error
# After:
class Foo
HELLO = "Hello, World!"
def self.say_hello
self::HELLO
end
end
Bar = Foo.clone
Bar.say_hello # => "Hello, world!"
Unfortunately, this solution doesn't resolve the problems with class variables, and it requires you to edit the source of Foo, which might not be desirable if Foo is part of a gem or other external library.
Another solution is to subclass Foo instead of cloning it:
class Foo
HELLO = "Hello, world!"
def self.say_hello
HELLO
end
end
class Bar < Foo
end
Bar.say_hello # => "Hello, world!"
The problem with this is that redefining Bar::HELLO won't affect the result of Bar.say_hello
, as you might expect from a cloned class:
Bar.const_set :HELLO, "Hello, Bar!"
Bar.say_hello # => "Hello, world!"
All in all, the most effective solution is probably to just copy the source code of Foo
into another class manually. This isn't dynamic, but the result is exactly the same as what you might expect from clone
.
Related Topics
What Does "<Top (Required)>" Mean in a Ruby Stack Trace
In Ruby, Can You Perform String Interpolation on Data Read from a File
Rspec --Init Not Working/ 'Mkd Ir': Invalid Argument - ./C: (Errno::Einval)
What's the Point of the Prefix? Operator in Ruby 1.9
Having Trouble Installing Libxml-Ruby on Windows
Extract All Email Addresses from Some .Txt Documents Using Ruby
What Do I Need to Do to Get the Blog to Work in Rails 4.2
Add Space After Commas Only If It Doesn't Already
Getting Uninitialized Constant Error When Trying to Run Tests