Imagine that you are implementing some form object because you are fed up with treating ActiveRecord models as such, and you need some extra flexibility. You start with a straightforward implementation for a base class of a form object where you can just whitelist attributes. That could look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Since the base class is ready, you can create a first form object that would inherit from this class:
1 2 3
Initially, it does the job, but then it turns out that you might need a default value if
some_attribute turns out to be nil. So you try something like that:
1 2 3 4 5 6 7
After checking if the default value works, this is what you get:
Whoops! How did it happen? The method was defined in the superclass so it should be inheritable, right?
Well, this is not really true. However, the problem is easy to fix.
Anatomy Of The Problem
The primary question we should answer in the first place is: where are all those new methods defined using
define_method in that particular way? Is it a superclass?
It’s definitely not a superclass – there are no any instance methods defined there, there are only the ones inherited from
Object. What about
Now the error that we initially got makes way more sense. The entire issue is caused by the fact that the declaration of the attribute happens in
MyFormWithDefaultValue, if it were defined in a base class, there would be no any issue. We can verify it with a simple example:
1 2 3 4 5 6 7 8 9
Now that we fully understand the problem let’s think about the solution. Ideally, about the one, that doesn’t require defining explicitly an intermediate class that we can inherit from.
How about defining a module instead? Modules are also included in the inheritance chain, and for using
super, it doesn’t matter if the method is defined in the class or a module.
The exact solution to the problem would be wrapping the definition of new methods that happens in
FormObject inside some module, it could be even an anonymous one, and including it right away:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Let’s verify if the new solution works:
Yay! It does exactly what we wanted to achieve.
Metaprogramming in Ruby is a powerful tool, however; it can lead to some issues that might not be obvious why they happen. Fortunately, with enough knowledge of the language, those problems can be solved elegantly.