Super and dynamically created methods in Ruby
Calling super
from overridden methods that have been created with define_method
is not always easy. It depends on how the original method has been defined.
Let’s take a look at exemplary Sanitizer
module:
module Sanitizer
def attribute(name)
define_method("#{name}=") do |val|
instance_variable_set("@#{name}", val.downcase.gsub(/\s+/, '-'))
end
attr_reader(name)
end
end
It can be consumed in a custom class:
class Article
extend Sanitizer
attribute :link
end
This works fine until there is a need to override a setter (i.e. to add additional logic):
class Article
extend Sanitizer
attribute :link
def link=(val)
super(val[0..40])
end
end
It simply doesn’t work because Sanitizer
module relies on meta-programming - setter is added directly to Article
class and gets overridden when link=
method is re-defined.
To make original methods overrideable, they need to be defined on the anonymous module which gets included in consuming class:
module Sanitizer
def sanitizer
@sanitizer ||= Module.new.tap { |m| include(m) }
end
def attribute(name)
sanitizer.send(:define_method, "#{name}=") do |val|
instance_variable_set("@#{name}", val.downcase.gsub(/\s+/, '-'))
end
sanitizer.send(:attr_reader, name)
end
end
article = Article.new
article.link = "new link"
article.link
#=> "new-link"
Article.ancestors
#=> [Article, #<Module:0x00007f82af192ec0>, Object, Kernel, BasicObject]
What if Sanitizer
module can’t be modified for some reason (i.e. because it comes from 3rd party gem)?
Create anonymous module with overridden method and prepend it to the class:
class Article
extend Sanitizer
attribute :link
prepend(Module.new do
def link=(val)
super(val[0..40])
end
end)
end
article = Article.new
article.link = "new link"
article.link
#=> "new-link"
Article.ancestors
#=> [#<Module:0x00007fab69083128>, Article, Object, Kernel, BasicObject]