Often with RubyOnRails (and other Ruby frameworks) we rely on instance variables and the or-equals operator (
||=) to store some expensive data in memory to avoid costly database or API queries : this is called memoization.
As this post is fairly short I decided to not record an audio version of it, apologies.
class Group def users @users ||= expensive_query end end
For those unfamiliar with this construct : if a value is already present in
expensive_query will not be called and the value will be returned directly.
Keeping things limited
There is a clear interest in using memoization. Queries to a database can be costly at times. Even with proper indexes, some queries are sometimes too much to be done often.
Instance variables are available throughout the instance once they have been defined. They are quite well adapted to be used within models or services objects has the context won’t be easily exposed beyond what we would want.
But with controllers, it's different. Defining an instance variable within a controller will also expose that variable to the rest of chain such as in the view or the template. It's expected as we use this mechanism in RoR to allow views and templates to work.
Usually, for the view or template, we only rely on instance variables that are defined within the controller’s action. Relying on instance variables from methods called by the controller is risky as it extend the dependency chain from the view to what ever defines those methods in the first place : a module, or a parent controller.
If one is then refactoring the way the module or parent controller work there might be an impact beyond what was expected.
The view and template should not have to know details of the implementation of the backend. They should be provided with just the data needed by the action rendering them.
There is an idea that Controllers should be pretty light in content. Actions should be fairly short and pass on any major work to service objects, or background workers.
Instance variables needed by views and templates can then be prepared with the return values of those objects. Using such separate objects explicitly avoid bleeding of extra instance variables downstream.
There is also an argument to be made to keep whatever is needed to be done to fill the instance variable to a separate method.
class Group def users @users ||= expensive_query end private def expensive_query # do something expensive end end
This allows to keep the first method smaller and more readable. It also means that the
expensive_query can be tested separately.
Keep things simple and limited in responsability
I recently encountered a case of misuse of memoization while reworking a controller. It reminded me of some readings such as Sandy Metz ‘POODR’.
Overall I am a fan of keeping things simple and limit responsibility of objects and their methods as much as possible. Sometimes one has to balance this with the complexity it can add but it’s often worth it. The main interests with such approaches are to ensure code is readable, easy to test and to maintain.
You can read more on such topics in the following links :