Local variables - the new global variables?
The topic of tonight’s Melbourne Ruby meetup was “things I wish I’d known earlier”.
This was my contribution.
Last year I read the Refactoring book and it gave me some surprising new insights into local variables.
If you look through the contents of the book you’ll quickly find the refactoring “Introduce explaining variable”.
Imagine you have a method that does some processing that is not particularly obvious. Here’s an example that selects a certain number of evenly spaced elements from an array:
You can make it more readable by assigning an intermediate value to a well-named local variable, like so:
There are times when that will be a great help, and you should definitely perform such refactorings.
Tonight, however, I want to focus on the more general case, which is that local variables stink.
Local variables are bad because they are often introduced as a premature optimisation.
Take this Library
class for example, which can prepare a report on overdue books for the library detective. It uses the total number of overdue books twice, but it manages to only calculate it once, by storing the value in a local variable. The same is done with the total count of all books.
These are premature optimisations that don’t aid readability or have any perceptible influence on performance, yet they do add additional lines of code.
They should be removed by repeating the calculation call wherever its result is required.
The example of counting elements in an array is perhaps trivial. The mistake is easier to make with other kinds of calculations. Data on disk or across a network may require further consideration, but when it is in memory you should assume that the results of sorting, arithmetic, string manipulation and most other processing can be instantly recalculated as required.
Our #detective_report
method still uses the explaining variable percent_overdue
. The problem with this is that the logic that went into calculating that value is not ready for reuse. When we add a method to tell us whether late fees need to be reviewed (which they obviously should be if more than 50% of our books are overdue), we end up with duplication.
If we replace that explaining local variable with a method, we significantly improve readability as well as reducing duplication.
Local variables are bad because they hide Law of Demeter violations and other complexity.
The #can_borrow?
method says a borrower can borrow a book if they don’t already have a book out and their membership record shows no fees are due.
It’s a fairly short and clear method. We just make a couple of simple method calls, and we don’t have the long method chains that Law of Demeter violations are often recognised by.
However, if we remove our local variables, the equivalent code reveals itself as ridiculously inappropriate. It is reaching deep into the record object - coupling itself to fee structures and introducing duplication of fee handling logic.
ActiveSupport’s #try
method would shorten this, but it’s still a clear candidate for refactoring.
We can fully resolve these issues by introducing a new method, good?
, which shows whether a borrower’s record finds them in good standing.
Local variables are bad because they go together with big methods.
Big methods usually do several things, and local methods help them store the intermediate values that appear in their multi-part processing.
As well as being involved in the work of methods that are already big, local variables make methods even bigger because they usually add at least one line of code each as they come into existence.
So, are they the new global variables?
Yes!
Local variables are the new global variables because as you write better code, you will use them less, and because when they appear in existing code, it is usually a sign that you should refactor.
The next time you discover or create a local variable, consider extracting a method instead. You’ll find yourself with cleaner code and a clearer mind in no time flat.