Sunday, January 4, 2015

Ruby and instance_variable_get - when it's useful?

When working on my book about refactoring Rails controllers, I've collected a number of "unusual" Rails solutions from real-world projects. They were either sent to me from the readers or I found them in some open-source Rails projects.

One of them was the idea of using instance_variable_get.

instance_variable_get in the Redmine project

I was working on one of the most important recipes from my book - "Explicitly render views with locals". It's important, as it opens the door to many other recipes - it makes them much simpler.

When I was practising this recipe, I tried to use it in many different projects. One of the open-source ones was Redmine. I applied this refactoring to one of the Redmine actions. I've searched through the view to find all @ivars and replaced them with an explicit object reference (passed to the view through the locals collection). This time it didn't work. The tests (Redmine has really high test coverage) failed.

The reason was a helper named error_messages_for(*objects). It's used in many Redmine views.

The implementation looks like this:

The idea behind this is to display error messages for multiple @ivars in a view. As you see, it uses the instance_variable_get calls to retrieve the @ivar from the current context. It's only used in cases, when a string is passed and not an @ivar directly.

This situation taught me 3 lessons.

1. I need to test my recipes in as many projects as possible to discover such edge cases.
2. A high test coverage gives a very comfortable situation to experiment with refactorings.
3. Ruby developers are only limited by imagination. The language doesn't impose any constraints. It's both good and bad.

I've extended the recipe chapter with a warning about the instance_variable_get usage:

The lifecycle of a Rails controller

After some time, I was working on another chapter called "Rails controller - the lifecycle". In that chapter I describe step by step what is happening when a request is made, starting from the routes, ending with rendering the view. Each step is associated with a snippet of the Rails code.

Everybody knows that if you have assign an @ivar in a controller, it's automatically available as an @ivar in the view object, even though they are different objects.

The trick here is that the Rails code uses instance_variable_get as well. It grabs all the instance variables and creates their equivalents in the view object using the instance_variable_set. Here is part of the chapter:

Easier debugging in rubygems code

In the rubygems code you will find this piece of code. It's a way of displaying the current state (@ivars) of the object. At first, it may look useful. I'm sure it served well for this purpose. However, the root problem here is not how to display the state. The root problem is that the state is so big here that it needs a little bit of meta programming to retrieve it easily. Solving the root problem (splitting it into more classes?) would probably reduce the need for such tricks.

When it's useful?

instance_variable_get is a way of bypassing the object encapsulation. There's a good reason that in Ruby we hide some state using @ivars. Such a hidden state may or may not be exposed in some way. I'd compare instance_variable tricks to using the "send" method.

My rule of thumb is that it's sometimes useful when you build a framework-like code. Code that will be used by other people via some kind of DSL. Rails is such an example. I'm not a big fan of copying the @ivars to the view, but I admit it plays some role in attracting new people to Rails.

However, it's a rare case, that instance_variable_get may be a good idea to use in your typical application.

The temptation to use it is often related to reducing the amount of code. That's a similar temptation to using the "send"-based tricks. I'm guilty of that in several cases. I learnt my lessons here. It might have been "clever" for me, but it was painful for others to understand. It's rarely worth the confusion.

I've seen enough legacy Rails projects to know, that sometimes the instance_variable_set trick is useful in tests. It's still "cheating", but it may be a good temporary step to get the project on the right path. Some modules/objects may be hard to test in isolation in a legacy context. Sometimes, we may need to take an object (like a controller) and set some of its state via instance_variable_set at the beginning of the test. It's similar to stubbing. This way we may be able to build a better test coverage. Test coverage on its own doesn't mean anything. However, good test coverage enables us to refactor the code, so that it no longer needs meta programming tricks.

When in doubt, don't use instance_variable_get.

No comments: