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
Bar
defines a dynamic class methodfoo
that creates a child class ofFoo
(let's call itFooChild
) whereFooChild.id
is initialized with a reference to theBar.id
class method.- While
Foo
andBar
are classes that do not belong to the same inheritance chain,Baz
is a child class ofBar
and redefines the class methodid
.
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
- Is there a way to provide a reference to
:id
fromBar
toFooChild
(when the methodfoo
is generated) that can be redefined from the child classBaz
? (without redefiningfoo
in the child class). - 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'