DEV Community

Dino for Wizard Health

Posted on

Metaprogramming, ancestors chain and super.

Let's imagine we are building DSL similar to ActiveRecord associations.

class Person associated_with :account end Person.new.account # => "Account associated with a Person" 
Enter fullscreen mode Exit fullscreen mode

In order to build this feature, we will create a new module that dynamically defines association methods.

module Associations def associated_with(name) define_method(name) do puts "associated #{name}" end end end class Person extend Associations associated_with :account end Person.new.account #=> "associated account" 
Enter fullscreen mode Exit fullscreen mode

define_method creates an instance method on a receiver, which is exactly what we need.

define_method basically has done this;

class Person ... def account "associated account" end end 
Enter fullscreen mode Exit fullscreen mode

We can easily validate this theory by inspecting the ancestors chain and instance methods:

Person.ancestors # => Person,Object,Kernel,BasicObject Person.instance_methods(false) # => [account] 
Enter fullscreen mode Exit fullscreen mode

Overwriting dynamically defined method

If we wish to overwrite a dynamically defined method we can do it without any problems since this is just a "regular" instance method (albeit defined with some metaprogramming)

class Person extend Associations associated_with :account def account "overridden" end end Person.new.account #=> "overridden" 
Enter fullscreen mode Exit fullscreen mode

But, calling a super when overriding will fail

def account super "overridden" end Person.new.account # => `account': super: no superclass method `account' for ... 
Enter fullscreen mode Exit fullscreen mode

This makes sense since we are calling super on the method we've completely overwritten.

In order for super to work the method need to be defined in Persons ancestors chain.

We can do this by generating a new module on the fly, including that module in the class and define dynamic methods on that module instead of the class itself.

module Associations # Create new module on the fly. # Include that module in the ancestor chain def generated_association_methods @generated_association_methods ||= begin mod = const_set(:GeneratedAssociationMethods, Module.new) include mod mod end end def associated_with(name) mixin = generated_association_methods # define methods on the newly created module mixin.define_method(name) do puts "associated #{name}" end end end class Person extend Associations associated_with :account end 
Enter fullscreen mode Exit fullscreen mode

Now dynamically defined methods live inside the Person::GeneratedAssociationMethods, which is part of ancestors chain.

Person.ancestors # => Person,**Person::GeneratedAssociationMethods**, Object,Kernel,BasicObject Person.instance_methods(false) # => [] 
Enter fullscreen mode Exit fullscreen mode

So calling super will work fine:

def account super "overridden" end Person.new.account #=> "associated_account" #=> "overridden" 
Enter fullscreen mode Exit fullscreen mode

I've seen this pattern used in Rails codebase in multiple places where this kind of behaviour is needed.

✌️

Top comments (1)

Collapse
 
mahendrachoudhary profile image
Mahendra Choudhary

A great insight how ruby magic works .