Code understanding is a task we are always doing, though we are not even aware that we're doing it. It's an obvious need when a colleague is leaving and another gets his code. However, we have to learn code in lots of cases, i.e. when:
- we continue developing the code next day
- bug fixing
- coding new features
- extending or improving available features
- refactoring the code
- reuse code
- migrate code
As a consequence, it is not a surprise that code understanding is at least 50% of the entirety of coding. The problem is, however, that we are not able to fully understand our code. Full code understanding means to know every state of all possible execution steps; this is obviously impossible. If we understood our code, it would be bug free. Unfortunately, it isn't.
Why is code comprehension so important? If our knowledge on our code is vague, then the quality of our code will decrease during maintenance of the code. The different parts of the code may influence each other and the lack of knowledge results in tricky, hard to catch bugs. High-level regression testing or retesting mitigates the problem. However, we have to know that even good testing detects only 85% of the bugs. If our knowledge is vague, the defect potential becomes huge, and even after testing lots of bugs will remain.
The lack of appropriate code comprehension leads to poor code quality and thus increasing technical debt. Technical debt is a term introduced by Ward Cunningham to denote the amount of work required to put a piece of software into that state which it should have had from the beginning. Technical debt today is huge. It is assumed that the level of technical dept (of an agile project) is $ 3.61 per statement on average. One of the main reasons of the technical debt is just the poor quality of the code. One of the causes of poor quality is that developers make compromises while coding to release software earlier and keep deadlines. This is intentional and developers believe that once they complete the feature they can escape from time pressure. This almost never happens.
However, the other side of poor quality is unintentional. Developers believe they understand their code well enough, but it's not true. One of the reasons is that most of the programmers have no deep knowledge on code comprehension in general, thinking about it as a simple and natural thing which can be obtained by some simple code search. However, code understanding is really difficult.
There are two main approaches of code understanding: full or as-needed. Full code understanding is usually needed when a programmer has left and another should continue his or her work. The newcomer should systematically read the code in detail, tracing through the control-flow and data-flow in the program to gain a global understanding. The developer builds a mental model of the program which makes it possible to fix, modify or improve the code while assuring high quality. At least in theory. The problem is that code is huge and understanding itself is boring. I never suggest doing code comprehension like this.
In practice we almost always apply the as-needed approach, focusing only on the code relating to a particular task needs to be addressed. Applying the as-needed approach only leads to vague knowledge resulting in a weaker mental model of how the program works. More defects will occur as the developer fails to recognize relevant dependencies among components in the code. The advantage of the as-needed approach is its time efficiency, since any code out of the scope is ignored.
The Code Comprehension Process
The obvious solution is to merge the advantages of these approaches, i.e. build a full mental model while understanding only the necessary part of the code. Our model we outline here is an attempt for this. We always use the as-needed method. As code reading is boring we should start from an interesting and applicable task as a first step of code comprehension.
Having a task, we can start code understanding. If the task is to improve a feature which location is unknown, the next step is to map the feature to the related code. This is not an easy task either, but efficient methods and tools are available. To learn more read our recent article in DZone: Code Understanding Step by Step - How to Find a Feature in the Code?
The next step is to learn the code related to the feature. The best way is to execute the code and watch what happens while running. We can investigate variables, declarations, class hierarchy and other things necessary for understanding. In the next article we demonstrate the details of this step.
After following the execution we obtain a certain level of understanding. However, there are two problems. First, one execution doesn't cover the whole feature, only a fraction of it. Second, even if we follow the data flow for our input, the interactions of the whole feature with other code parts remain hidden. This lack of knowledge may or may not cause problems, but we should check. Unfortunately, investigating possible data flow is very difficult by code reading only. We address this issue in a subsequent article.
When the code we need is known, it’s time to solve our task. If we do this and test the correctness, we can state that we understand the code - at least the location related to the feature. Because it's easy to forget this new knowledge, it is advisable to document it.
Finally, I summarize the understanding process - some steps may be omitted as appropriate:
- select an interesting and realistic task related to the feature to be understood
- search for the code location of the feature
- execute and debug the code to understand it
- check, and if necessary, explore the data flow to obtain a full mental model
- solve the task, and test the result
- document your understanding