Ruby has quite a lot of “exotic” features that are not used that often, but when you need to utilize some metaprogramming magic, you can easily take advantage of them. One of such features is Object.instance_exec which you might be familiar with if you’ve ever built some more advanced DSL.
The great thing about
Object#instance_exec is that it allows to execute code within the context of a given object but it also gives possibility to pass arguments from the current context. Thanks to that, we can build some nice DSLs and other features like this:
1 2 3
An interesting thing is that there is a class equivalent of
Object#instance_exec – Module.class_exec. It would be easy to figure out some theoretical example how it can be used but what could be the real-world use case where this is the best approach to solve the problem?
Anatomy Of The Problem
Imagine that you can have some custom JSON on every instance of some model and this JSON can have very different attributes on every instance depending on various conditions, like some category this model belongs to. To make it more complex, let’s assume that the schema is customizable by the user so we can never really predict what kind of attributes are going to end up there.
Our feature to implement is to provide some wrapper class for this custom JSON so that we don’t need to operate on hashes but we can have some objects where we can access these attributes by invoking methods on this object.
Using OpenStructs sounds like the quickest solution to the problem but this is not going to be that easy in our case – we will need to expose this class to be used with Liquid templates, so that means we will need to inherit from Liquid::Drop.
How about creating some
Wrapper class that would take the
payload as an argument and use Object#define_singleton_method in the constructor to define custom methods based on the keys and values in that payload? Defining singleton methods sounds like the right solution to the problem as indeed each instance might need different methods. Let’s try that:
1 2 3 4 5 6 7 8 9 10 11 12
Looks like it might be the answer to the problem:
1 2 3 4
There is a huge problem with this solution though. These singleton methods are not going to be included in
Do we have any alternative that would be the most robust solution to this problem?
The answer is yes! Although, the solution is going to be more tricky than the previous one.
First, we will need to take advantage of using the constructor of
Class itself and create anonymous classes inheriting from
Liquid::Drop. The next step would be defining the required methods based on
payload. But how can we do that if
payload is not available in the context of this class? We will need to make it available somehow and execute the code within the context of this class.
Fortunately, Ruby has got our back, and we can take advantage of Module.class_exec method which does exactly what we need here.
Here is a potential implementation:
1 2 3 4 5 6 7 8 9 10 11 12
And what about
That means we’ve managed to achieve our goal!
Ruby is widely known for being powerful and allowing to easily do all kinds of things to objects, including modifying them on fly and executing the code within their context. Thanks to that and uncommon methods like Module.class_exec, we can solve some tricky and rare problems with a very elegant solutions.