Traceable variables
tl;dr
It is easier to maintain code where variables are clearly declared, and preferably close to the place they are being used.
Anything that is implicit is your enemy: inheritance, injections, global state...
Codebase hopping is norm
As a professional programmer, you seldom have the luxury of working on the one and same codebase. You are switching between frontend, backend, that overgrown batch job, the backoffice, the app, the search-integrations-project, etc.
Different projects might use different versions of libraries, different code styles, or even different programming languages. You do not have the time to master every aspect of every system. Thus:
Code must be easy to read and grasp, even if you are not a master of the framework in use.
Traceable variables
To make your code readable, it is important that variables are easily traceable. If you are trying to modify a specific function, you want to spend as little time as possible outside of that function.
public class PlayButton
{
MatchMaker service;
public PlayButton()
{
service = new RoundRobinMatchMaker();
}
// ... some code ...
// row 153
public void Play()
{
service.execute();
}
}
In Example A, trying to understand Play()
requires you to first trace the variable service
:
- Find out that it is declared as a member variable of the class.
- Find the assignment to the variable in the constructor.
public class PlayButton
{
// ... some code ...
//row 153
public void Play()
{
var service = new RoundRobinMatchMaker();
service.execute();
}
}
In Example B no tracing is needed of service
. Clearly, the Play()
-function in Example B is much easier to reason about!
Traceability index
Let's define a measurement for how hard it is to trace a variable:
Traceability Index
The number of times you have to scroll away or switch context in order to understand the variable.
In Example A above, the variable service
has traceability index 2; while its index is 0 in Example B.
Composition vs. inheritance
Hopefully you are already aware of the bad strong coupling that comes with inheritance. But there are more subtle problems.
class AVector : InstancedVector
{
Smurf()
{
Console.WriteLine("" + list.Isberg);
}
}
Where does the variable list
come from? The superclass? Which one?
class InstancedVector : DeepVector ...
class DeepVector : FullVector ...
class FullVector : ArrayVector ...
The tracability index of the variable list
is potentially greater than 4! Even with a modern development environment, you might catch the declaration but miss a mutation to the variable "deeper down" the inheritance chain.
React hooks
React has recently taken a big step in traceable variables: hooks. Example C is an old style React class.
import 'react-redux';
import 'react-router';
const mapStateToProps = (state) => ({
user: state.user,
matchesPlayed: state.matchesPlayed
});
@injectRouter
@connect(mapStateToProps)
class Dude extends React.Component {
render() {
return <p>{this.props.user.name} is on {this.props.match.location} </p>
}
}
match.location
is not traceable at all! The traceability index is infinity.
import { useLocation } from 'react-router';
import { useSelector } from 'react-redux';
function Dude() {
const user = useSelector(store => store.user);
const matchesPlayed = useSelector(store => store.matchesPlayed);
const location = useLocation();
return <p>{user.name} is on {location} </p>
}
In Example D, you can trace the variable location
, with traceability index 2:
location
comes fromuseLocation()
useLocation
is imported fromreact-router
Even if you have never seen React before, you can still figure out that location
is probably related to the location of the URL (and not the city of the user, for instance).
Since there are no rows of code between the call to useLocation()
and the usage of location
, you could argue that the traceability index of location
is 1.
Trapped by technology
We can not always chose which technology to use. But you can still do your best to increase readability and traceability:
- Naming conventions
- Comments
- Variable and function names
For instance, you can make sure to always prefix global variables with g_, or member variables with _.
If you have helper functions, you can make sure to include a prefix indicating where they originate from.
// No conventions
class Greeter {
char *rolls;
void Hello(const char* str) {
intersperse(location, str);
fixes->count();
rolls++;
}
}
In order to reason about the code in Example E, you would have to trace all variables. Even worse, you will have to trace some functions to. For instance: can intersperse
change the database?
// Better
class Greeter {
char *_rolls;
void Hello(const char* str) {
char *loc = location; // From GPS-library
str_utils_intersperse(loc, str);
g_fixes->count();
_rolls++;
}
}
In Example F, we can actually draw conclusions from the code, just by reading the Hello
-function.
- We are reading a GPS-coordinate
- Doing some string manipulation
- Potentially altering the global variable fixes
- Updating the member variable rolls.
The benefits
By spending less time chasing variable definitions, you might reap the following benefits:
- You can keep more focus on solving your actual problem...
- ...which will make the job more fun! Most people do not become developers to dechipher old peoples' code.
- You will save money by doing the job faster.
I wish that you get to create and take part of fun code with easily traceable variables :)