Home > OS >  Redefining ancestor's class method in child class to update 3rd party class dependency
Redefining ancestor's class method in child class to update 3rd party class dependency

Time:09-14

The questions have been placed at the end, as they might not be clear without some context.

The purpose

The idea is that we can dynamically inherit from Foo and redefine some of its class dependencies throughout the inheritance chain of a client class (Bar > Baz). Below a try to reflect this idea:

              Foo
               ▼
Bar.foo  -->  FooChild <id: Bar.id)
 ▼                           ↕
Baz.foo  -->  FooChild <id: Baz.id)

This approach aims to reuse the class Foo by changing its class dependency id. So depending on what client class uses Foo (or children thereof), Foo.id will refer to a different String (as reconfigured by Bar and Baz).

Just for clarity, see below a seemingly more specific example, where the analogy is as follows: Collection is to Foo as Room is to Bar and Library to Baz. If to be at ease we referred to Foo.id, in this case we refer to Collection.item_class; the class variable of Collection that will help to retrieve the Class of the items of a Collection instance object.

                     Collection
                         ▼
Room.things     -->  CollectionChild <item_class: Thing)
 ▼                                                  ↕
Library.things  -->  CollectionChild <item_class: Book)

We want to define the things method (foo) only once in the parent class Room (Bar). Rather than redefining the things (foo) method in the Library class (Baz), the purpose is to redefine item_class dynamically.

Example

The code below synthesizes somehow the context of the problem:

class Foo
  class << self
    attr_accessor :id
    def resolved_id
      id_class, id_method = id.first
      id_class.send(id_method)
    end
  end
  def identify
    puts "Referring to '#{self.class.resolved_id}'"
  end
end

class Bar
  class << self
    def embed(method, klass:, data:)
      define_method "#{method}" do
        Class.new(klass) {|child| child.id = data}.new
      end
    end
    def id; "bar"; end
  end
  embed :foo, klass: Foo, data: {self => :id}
end

class Baz < Bar
  def self.id; "baz"; end
end

Baz.new.foo.identify

The output is:

Referring to 'bar'

I would rather prefer to achieve:

Referring to 'baz'

Explanation

  1. Bar defines a dynamic class method foo that creates a child class of Foo (let's call it FooChild) where FooChild.id is initialized with a reference to the Bar.id class method.
  2. While Foo and Bar are classes that do not belong to the same inheritance chain, Baz is a child class of Bar and redefines the class method id.

The problem

While resolving dependencies from within the same inheritance chain gets easy (i.e. redefining methods such as Bar.id in Baz.id). To resolve dependencies from a non parented class requires to pass the reference to the current class (see data: {self => :id}), not just the sym method (:id), because the method may not exist or may be unrelated in the class that consumes this dependency.

It may be that the entire approach is incorrect and adds unnecessary complexity, when compared to just explicitly redefining things, rather than trying to make everything configurable and reusable. May be configurable classes can be seen as both: a time savior when used in moderation and an aberration and an anti-pattern when used in excess.

What is difficult is to see when you are crossing the line.

The solution I am trying to avoid

In overall, it seems unlikely that this can be resolved without redefining foo in the child class Baz like this:

class Baz < Bar
  def self.id; "baz"; end
  embed :foo, klass: Foo, data: {self => :id}
end

Bar.new.foo.identify
Baz.new.foo.identify

The output:

Referring to 'bar'
Referring to 'baz'

However, if that was the case, that line would look exactly the same as in the parent class Bar; just that self would refer to a different class.

Questions

  1. Is there a way to provide a reference to :id from Bar to FooChild (when the method foo is generated) that can be redefined from the child class Baz? (without redefining foo in the child class).
  2. After skimming through some Rails posts around constant lookup and resolution, I am keen to hear some "simpler" yet effective as, alternative methods.

Thanks in advance.

CodePudding user response:

You seem to be overcomplicating it a bit. The issue stems from the reference to self in the embed call on Bar.

Since that self happens to be on the class level of Bar, it is frozen to be Bar the moment the line is evaluated.

If you call that self inside the foo method instead, it is evaluated at runtime and will be whatever class the method is called on.

class Bar
  class << self
    def id; "bar"; end
  end
  def foo
    klass = self.class
    Class.new(Foo) {|child| child.id = {klass => :id}}.new
  end
end

Everything else unchanged.

Baz.new.foo.identify #=> Referring to 'baz'

CodePudding user response:

@Siim_Liiser answer is the key solution. This answer has been added just to align his with the original code of the question:

Modified lines with comments:

class Bar
  class << self
    def embed(method, klass:, data:)
      define_method "#{method}" do
        # we rather capture the referrer class from the instance object
        referrer = self.class
        Class.new(klass) {|child| child.id = {referrer => data}}.new
      end
    end
    def id; "bar"; end
  end
  # from the referrer class, we just pass the reference :id 
  embed :foo, klass: Foo, data: :id
end

The rest of the code remains untouched.

Bar.new.foo.identify
Baz.new.foo.identify

Output:

Referring to 'bar'
Referring to 'baz'
  • Related