Override Ruby Constant in Subclass So Inherited Methods Use New Constant Instead of the Old

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



Leave a reply



Submit