Metaprogramming, ancestors chain and super.
Tags: ruby, rails
Published: 2023-03-13 08:07:09

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

```ruby
class Person
  associated_with :account
end

Person.new.account # => "Account associated with a Person"
```

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

```ruby
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"
```

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

`define_method` basically has done this;

```ruby
class Person
  ...

  def account
    "associated account"
  end
end
```

We can easily validate this theory by inspecting the ancestors chain and instance methods:
```ruby
Person.ancestors # => Person,Object,Kernel,BasicObject
Person.instance_methods(false) # => [account]
```


## 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)

```ruby
class Person
  extend Associations

  associated_with :account

  def account
    "overridden"
  end
end

Person.new.account #=> "overridden"
```

But, calling a **super** when overriding will fail

```ruby
def account
  super
  "overridden"
end

Person.new.account # => `account': super: no superclass method `account' for ...
```

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.

```ruby
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
```

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

```ruby
Person.ancestors # => Person,**Person::GeneratedAssociationMethods**, Object,Kernel,BasicObject
Person.instance_methods(false) # => []
```

So calling super will work fine:

```ruby
def account
  super
  "overridden"
end

Person.new.account
  #=> "associated_account"
  #=> "overridden"
```



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

✌️