At SoftwarePlant, we like to experiment. Some time ago, we decided to look into writing functions and class methods. The results of our test were quite unexpected. But let’s start from the beginning.
It all starts with a clean code
The code should be written in a way so that it can be read easily and quickly. Obvious, right? This was preached to us by Robert Cecil Martin known better as Uncle Bob. In his book ‘Clean Code’ he noted that during a typical working day, developers spend much more time reading the code than actually writing it. According to the author, this ratio can be as high as 10:1. Conclusion? Again quite obvious: we need to write clean code. Otherwise, we won’t have much time to write the code or to develop the system that’s based on it.
Fortunately, Uncle Bob gave us some easy to follow instructions on this matter. At the lowest level we have four rules for functions:
- Functions should be small
- Functions should do one thing
- Each step of the function’s body should have the same level of abstraction
- All steps of the function should be one level of abstraction below the level of the function name
But what happens if we don’t follow these rules?
And this is where our experiment begins. I invited 23 SoftwarePlant developers to join me. As the first step, I divided them into three even groups. The line-up of each group was determined randomly. Each developer received one method to read. Everyone worked individually.
The methods differed between the groups. In the first group, the method was short. In the second group, the method was of medium length, and in the third group, the method was the longest.
Each of these methods had a placeholder with a name. The name was so vague it was impossible to find out what the method was doing. I asked all participating developers to determine what this method does and how it actually should be called, based only on the body of the method.
Each developer would start a timer and then begin to read and analyze the assigned method. When he or she found a solution, they stopped the timer and sent me the following information:
- his or her time it took to analyze the code,
- the name of the method he or she came up with, and
- the number of his or her group.
Before we dive into the results, I’ll let you know the correct answer. All three methods did one thing:
Yes – they were instructions on how to bake a brownie. So the proper name for the method was ‘bake a brownie.’ Minor variations of this name, such as ‘bake a cake’ or ‘prepare a cake,’ were allowed. As long as they reflected activity and object of the method correctly.
Messing with implementation
Although all three methods do the same, there were differences in their implementations.
The method in group one.
The method in group two – it’s longer and was created by inlining each line of code from the first method.
I developed the third method correspondingly. I created it mainly via inline of the second method. But to complicate it a bit more, I also extracted one of the fragments from the second method to a common method. And thus, in the middle of the third method, we received a line from the first method. Thanks to that, I managed to get quite a mix of levels.
Why did I design the codes like this? As you can probably guess, I wrote it according to or against Uncle Bob’s rules:
The first method meets all the demands Uncle Bob wrote about. The second method does not meet the last postulate, i.e., the steps of which it consists have been inlined and are too detailed. The third method breaks three rules: the method is not small – it has more than 25 lines, there are different levels of abstraction, and the steps are too detailed.
What we’ve learned from our experiment
The first chart shows the average response time for each group. The first group needed, on average, two and a half minutes to establish the name. The second group needed less time – about two minutes. In the third group, it took the developers the longest, i.e., almost four and a half minutes.
Also very interesting are the results from the second chart regarding the percentage of correct answers. Here you can see that all developers in the first and second group correctly named this method. In the last group, three people did not decode these 25 lines and didn’t come up with the correct name for the method.
The easiest explanation for this is the assumption that the third group had the most lines of detailed code to read and so they needed the most time to do it. This is, of course, a correct explanation. Nevertheless, in my opinion, it is not the only factor that affected the results. In my opinion, this was also due to the mixed levels of abstraction in the third method.
In the first method, the name is at the highest level of abstraction. Then each subsequent line of the method’s body is slightly more detailed, but the level of each line is still comparable. In the second method, it’s quite similar. Its name is at the highest level of abstraction, while the body, which was created by the inline of the first method, is more detailed. But the individual lines are at a comparable level to each other. Despite the higher level of detail, the code in the second method still reads well and is very rhythmical.
When analyzing the levels of abstraction, we can observe sort of a reversed ‘L’ in both methods. You could say that we achieve the reversed ‘L’ when the level of abstraction is correctly balanced and in that way, it’s easy to interpret.
Meanwhile, in the third method, it’s not that easy anymore. As you can see, the diagram for this method is no longer a reversed ‘L’ but a kind of zigzag.
Let’s analyze how that happened. The highest level of abstraction is the name of the method. The next two lines are on the same level as they were in the second method. But then we have an inline, which lowers our level of abstraction. If you look closely at the used language, you will quickly notice that it is more technical and detailed than the one used above. The next few lines go back to a higher level. And then we get one line on an even higher level, which contains a lot of details. The next couple of lines are again on a much lower level.
Looking at this code analysis, we can see that the third group had to do two things at once. First, they had to read the code from top to bottom. Secondly, something that the two previous groups did not have to do, they had to read the code from left to right and identify these ‘jumps,’ i.e., changes in abstraction levels. They needed to read the same piece of code repeatedly. Only then, they were able to identify differences in the level of detail between the fragments of the method. They had to create a sort of auxiliary ‘bridges’ in their minds to learn to analyze individual pieces.
On a side note: often, a developer who writes the code tries to warn you that there is a ‘jump’ in abstraction level. He adds a blank line or writes a comment. It’s a convenience, but it’s still just a warning sign. The best solution is to ‘pack’ the fragment into a method and thus even out the level.
To confirm my thesis, I would like to mention that after the experiment, the members of groups one and two were happy they took part in this test. Meanwhile, the members of the third group declared that they were tired. I believe it was mental fatigue, resulting from the need to analyze the code in two dimensions instead of just one.
- ALWAYS write functions so that ALL the steps of the function’s body have the same level of abstraction. Write the code assuming that if you drew the diagram with abstraction levels next to it, you would get a reversed ‘L,’ and not a zigzag. Avoid ‘jumps.’ The only acceptable abstraction level ‘jump’ is between the function’s name and the body.
- Try to make sure that the abstraction level gap, between the function’s name and the body, is a maximum of two. But the truth is, you should always try to make it one.
If you’re interested in taking a closer look at the code used in this experiment, you can download it from my GitHub.