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"
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"
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
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]
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"
But, calling a super when overriding will fail
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.
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.
Person.ancestors # => Person,**Person::GeneratedAssociationMethods**, Object,Kernel,BasicObject Person.instance_methods(false) # => []
So calling super will work fine:
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.
✌️
Top comments (1)
A great insight how ruby magic works .