Author: Chris Newton
Published: 10 Jan 2010
Length: 1,472 words
Have you ever noticed how browsing good code is like talking with an old friend, as if it somehow knows what interests you and what you don’t like to talk about? Why does some code seem to open up, like a map unfolding before your eyes, yet looking at other code is like staring into fog? You might instinctively say that some code is more readable, or written by a better programmer. But how do better programmers achieve that better readability?
To answer these questions, we must first visit the world of psychology to study program comprehension, the process of exploring and understanding code.
The first thing to know is that as we read code, we don’t build just one representation of it in our minds. Early research did propose several isolated models, but von Mayrhauser and Vans showed that we can reconcile and combine the main ones.1
In the resulting integrated model, we try to understand code from three different perspectives simultaneously:
Domain concepts are the concrete things our software deals with, the products and transactions on a shopping web site or the characters and weapons in a game. We build a mental map between these concepts and the parts of our code that represent them.
Control flow is the order in which the individual statements in our code are executed, or the expressions evaluated.
Data flow describes where values are determined and used.
Some papers refer to control- and data-based models as the program model and situation model, respectively.
Early research also suggested that readers followed various single strategies to explore code, but the modern view is that we approach the problem from two directions.
Starting with domain concepts of interest, the reader forms hypotheses about how these are likely to be represented in the code, and explores the corresponding areas in an attempt to validate or reject each hypothesis. This process is repeated at increasingly fine levels of detail.
The reader starts examining the control flow at a known point of interest in the code, such as where it generates output or throws an exception. The aim is to “chunk” groups of statements or expressions into progressively larger meaningful abstractions, such as identifying that the code in a loop represents a search of a data structure. As chunking continues, the exploration will move from examining control flow to building a data- and functionality-based model.
Readers usually start by following one approach reasonably systematically, but will switch direction opportunistically as they discover more information. A switch is typically triggered by encountering a beacon, a recognisable element in the code that gives a clue about that code's greater role. The canonical example is finding code to swap two values, which often suggests some kind of sorting algorithm.
The reader’s background often influences the initial approach. Those who are already familiar with the code, or other code like it, will typically start with a top-down examination based on their understanding of the domain concepts. The less experienced will tend to start from a known focal point and work bottom-up.
2 Ko, A.J. et al, Information Needs in Collocated Software Development Teams, in ICSE ’07: Proceedings of the 29th Conference on Software Engineering, 2007.
3 von Mayrhauser, A. and A. M. Vans, From Code Understanding Needs to Reverse Engineering Tool Capabilities, in CASE ’93: Proceeding of the 6th International Workshop on Computer-Aided Software Engineering, 1993.
Now we have an idea of how program comprehension works, but there is another aspect to readability: it’s not just about extracting information, it’s about ease of extracting useful information from the code. What is useful depends on the reader and what they are trying to achieve: the needs of someone fixing a bug may be quite different to the needs of someone trying to implement a new feature. Readability is in the eye of the beholder, and in trying to write readable code, we are always trying to serve several masters.
Several researchers have observed developers performing various tasks and tried to classify the kinds of questions being asked.2,3 Based on that research, here is a simple taxonomy of the main kinds of information needed, with some examples of each:
Discovering and using existing code
- What data structures and algorithms are available?
- How do I use them?
- How can I combine them?
Understanding the purpose of code
- What is the purpose of this code, e.g., how does it relate to domain concepts?
- How does this code fit into the larger design?
Understanding the thinking behind code
- What is the behaviour, as implemented, and why was the code written this way?
Navigating static relationships in existing code
- Where is this function/variable/type defined?
- Where is this function/variable/type used?
Explaining the dynamic behaviour of existing code
- What code could have caused this behaviour?
- How did the program reach this state?
- In what other situations will this failure occur?
Reasoning about changes
- Where do I need to make changes to implement this behaviour?
- What are the implications of making this change?
- What changes have other developers been making?
We have seen the kinds of information we are likely to want, and how we read existing code when we try to discover them, but how successfully do we find what we need from code today?
It turns out that there are huge variations; the generalised statistics below are derived from the results in Ko’s paper.
Top of the class is static navigation. This is, for practical purposes, a 100% solved problem: we have tools to browse around our code, and developers routinely use them.
We do fairly well with discovering and using existing code, though with a 70–80% success rate there is room for improvement here.
Also not bad is understanding the purpose of code, again with a 70–80% success rate.
After that, things look a lot less pretty. In contrast with understanding the purpose of code, understanding the thinking behind code has a mere 30–40% success rate.
Explaining the dynamic behaviour of existing code is another serious problem, also with just a 30–40% success rate.
But the worst of the lot is reasoning about changes. In the study above, this scored a big, fat 0%: developers just gave up on the code at this point, and went straight to asking a colleague for help.
Overall, it looks like our mark is about “6/10, must try harder!”
Two paths to improvement
Knowing what we now do about program comprehension, we can suggest two general plans to improve the readability of our code in the weaker areas, both of which offer a wealth of opportunities.
Make designs more predictable to help top-down exploration
We can adopt system-wide conventions for representing domain concepts in the code. We can implement consistent, systematic policies for error handling, concurrency, resource management, security, and so on. We can build more localised parts of our designs in regular structures as much as possible, and minimise the number of special cases that don’t fit into the expected patterns.
Present implementations with a clear focus to help bottom-up exploration
Someone exploring code bottom-up has to start by wading through all the little details, trying to isolate important parts of the code and piece together the next level of abstractions. We can help them by highlighting important steps in the control flow and the data flow, separating the representation of these two concepts as much as possible, and minimising unnecessary distractions.
1. Mental models
- Readers consider three perspectives on code simultaneously:
- domain concepts
- control flow
- data flow.
2. Exploration strategies
- Readers follow two approaches:
- top-down, starting from domain concepts and testing hypotheses
- bottom-up, chunking related code to understand control and then data flow.
- Readers change direction opportunistically, particularly after identifying a beacon.
- The initial approach typically depends on the reader’s experience.
3. Information needs
- Common information needs are:
- discovering and using existing code
- understanding the purpose of code
- understanding the thinking behind code
- navigating static relationships in existing code
- explaining the dynamic behaviour of existing code
- reasoning about changes.
- Static navigation is a solved problem.
- Reasoning about changes is so hard that developers give up and ask colleagues.
- Other information types are found with some but not universal success.
5. Two paths to improvement
- We can improve readability by:
- making designs more predictable
- presenting implementations with a clear focus.
© 2010 Chris Newton. Redistribution is prohibited without explicit consent from the author.