rss

Lissel's code blogShort rambles about programming


Traceable variables

Do you know where your variables are coming from?

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.

Example A
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:

  1. Find out that it is declared as a member variable of the class.
  2. Find the assignment to the variable in the constructor.
Example B
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.

Example C
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.

Example D
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:

  1. location comes from useLocation()
  2. useLocation is imported from react-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:

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.

Example E
// 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?

Example F
// 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.

  1. We are reading a GPS-coordinate
  2. Doing some string manipulation
  3. Potentially altering the global variable fixes
  4. Updating the member variable rolls.

The benefits

By spending less time chasing variable definitions, you might reap the following benefits:

I wish that you get to create and take part of fun code with easily traceable variables :)