The Empathy of Maintainable Code. This post is primarily intended for…
By: Sebastian Ahmed
Tags: Documentation, Empathy, Software Development, Testing, Silicon

Photo by Sebastian Ahmed

Technology developments that involve coding, such as software, firmware, FPGAs or silicon, typically rely on a substantial amount of “internal” software. This is the software that constitutes “the machinery of development” as opposed to software that is shipped as part of an end-product (tools, scripts, automation frameworks, architectural models, studies and so on). This kind of software is typically not held to the same standard as production software and thus can suffer from the effects of lack of maintainability.

Unmaintainable code is a major source of technical friction and stagnation that holds back individuals and teams which ultimately results in business impacts.

The impacts manifest themselves in insidious ways and tend to be self-perpetuating.

In this post I put forward the notion that making internal code maintainable is an empathetic behavior.

In the world of technology, almost everything is code. Whether you are a front-end UX developer, embedded firmware engineer, data-scientist, DevOps engineer or designing silicon chips, everything is code. Code is the embodiment of ideas that defines products and all the infrastructure around their creation, including automation such as build systems, test frameworks and even documentation.

The need for maintainability applies just as much to an internal software as it does to product software.

If code ever needs to be updated, it meets the bar for needing to be maintainable.

What is unmaintainable code?

Credit: Google

In simple terms, I view unmaintainable code as

code which is “highly resistant to change”.

Unmaintainable code represents an exhausting proposition of a major time investment and inevitable frustration. Such code will put up a fight and likely win in its pursuit to stay unmaintainable. Likely, such code can only be modified by its original author (if they are still around), but even the author of such code will likely suffer (more on that shortly).

This brings me to what I believe are the two key elements which truly make code highly resistant to change:

Lack of helpful source-level documentation

Lack of an automated, informative and extensible test-suite

Good software design, appropriate re-use of known patterns, libraries and language-specific coding practices are indeed fundamental (and possibly enforced by static code analysis or other tools — if they are run on internal software), but the reason I’m not going to cover these is because I believe that

even the most wonderfully crafted code may still be unmaintainable.

Also, consider the possibility that when the two points above are actually addressed, they result in better design and coding practices, because they motivate the author to do things better (test-driven development for example makes the designer think about their design much more comprehensively before they start to code). As such, odds are that when the above two elements are lacking, there is likely some badly designed and implemented code to go along with that void.

Why is unmaintainable code bad?

This could be a long list, but since we’re talking about empathy (caring about others), here is what the damage looks like:

Reduced opportunities for others to learn and grow . Modifying or extending an existing code-base is one of the best ways for either junior designers or designers new to a technology ramp-up quickly and be productive.

. Modifying or extending an existing code-base is one of the best ways for either junior designers or designers new to a technology ramp-up quickly and be productive. The original author becomes a bottleneck as the “go-to” person for every bug-fix or feature request. Initially they may revel in being “valuable”, but over time they may grow to resent their burden when every seemingly small request to modify the code comes to them. They in turn become stifled .

as the “go-to” person for every bug-fix or feature request. Initially they may revel in being “valuable”, but over time they may grow to resent their burden when every seemingly small request to modify the code comes to them. . Re-factoring becomes untenable. Re-factoring is a critical aspect of iteration based design because it actively works towards reducing the “resistance to change” — which makes it easier to quickly add value to the project.

This kind of friction causes stagnation which can even drive people to quit or seek permission to be moved to another team, which will further stifle the team they are departing.

The inability to change a design may also lead to a “fresh-start” scenario which will likely throw away valuable work. There may simply be no other choices, and unfortunately this remake will likely also be unmaintainable.

It should then become obvious that

unmaintainable code actually hurts people and teams, which ultimately hurts the organization.

Helpful source-level documentation

Photo and source-code by Sebastian Ahmed

“Code is more often read than written.” — Guido van Rossum (Creator of the Python programming-language)

This quote says so much, and it was in fact this mindset which drove Guido to make the syntax and semantics of the Python language to be human friendly.

Most people don’t like to write documents, I get that. One of the four tenets specified in the agile manifesto specifically places an emphasis on “working software over comprehensive documentation”, but it also states that the “things on the right” are still valued. Its emphasis, is in regard to delivering value to a customer as the primary objective. But, by no means, is the manifesto suggesting that a delivery is made at “all costs” to any documentation, especially the sort that creates long-term friction and damage we discussed earlier.

We are not talking about “comprehensive” documentation here. We are talking about key semantic information that is required to convey the assumptions and intent of source-code to another human being (not a compiler or interpreter). We are talking about code-comments (or “embedded documentation” as I prefer to look at it). This kind of documentation may cover the following:

Explanations of the design structure, e.g. classes, relationships and patterns if the design is OO based

Justification of the design including assumed use-cases and referenced requirements. Use of libraries.

Calling out programming patterns and why they were chosen

Key design assumptions and caveats that may not be obvious (e.g. “had to use a uint64_t here because the input is offset binary”)

References to relevant articles, tickets or other code-bases

Discussions about performance-centric code or design decisions (e.g. “Used a thread pool here because the API is I/O bound”)

Detailed descriptions-of or URLs-to major bug-fix tickets. Some obscure piece of code may have been the result of a very corner-case bug fix

Recommendations on how to extend the code, e.g. “guide on extending the Foo class”

Highlighting areas that are somewhat suspect or fragile, e.g. “This class uses a static variable and may not be thread-safe”. Tell the reader what landmines are hiding in this code.

Highlight areas that frankly you are not too sure about “This recursion seems to work, but has not been extensively tested and I am still not sure why I had to subtract 2 and not 1 at the last iteration”

This is by no means a complete list (hardly so), but this sort of information is helpful to anyone trying to modify the code. It may even go a long way in making a badly written piece of code maintainable because it will provide enough information to consider a re-factor.

This also takes some humility and that is indeed being kind, and it is being empathetic. The mental state of writing code should always be “will another human understand this? What information will help them?”.

This is different from user-documentation, it is maintainer-documentation.

All languages have mechanics to embed helpful documentation (including ways to render it to other formats such as HTML). It takes empathy to prioritize its importance.

Automated, informative and extensible test-suite

Drawing by Sebastian Ahmed

This is perhaps the most critical element for making code maintainable and yet it is not the code itself.

Nothing creates conditions for maximum fear of change more than the inability to easily test and iterate locally.

This is the key element that I believe is most often missed when dealing with “internal” types of software such as automation, infrastructure and models.

Such software is in fact very valuable because it represents the “machinery” of product development. Often, such code is written by developers of the product. Since it is not considered to be the actual product, it may be written in haste, with poor embedded “maintainer-documentation” and likely no test support.

Recall that “If code ever needs to be updated, it meets the bar for needing to be maintainable”.

So what does an automated, informative and extensible test-suite look like? Here are some suggestions to get started.

One simple command-line script should execute a full suite of tests. Finer run-controls should be available via command-line arguments, but by default, one simply runs a test-program/script as defined in a top-level README.md file or similar.

should execute a full suite of tests. Finer run-controls should be available via command-line arguments, but by default, one simply runs a test-program/script as defined in a top-level README.md file or similar. Any dependencies for running the main test-script should be easy to acquire and this should be explained in the README.md. Ideally the test-script pulls down the dependencies or dependencies are already included in the project under a “test” directory

for running the main test-script should be easy to acquire and this should be explained in the README.md. Ideally the test-script pulls down the dependencies or dependencies are already included in the project under a “test” directory The failure modes of each test need to be informative . Depending on the test-framework, each test should be well described and failures should steer the user to a particular function or piece of code with relevant information why the test failed

. Depending on the test-framework, each test should be well described and failures should steer the user to a particular function or piece of code with relevant information why the test failed The code-base should ideally be instrumented with asserts that are enabled during testing at the very least. Asserts provide localized information

that are enabled during testing at the very least. Asserts provide localized information Ideally a test-framework should be used. For example, if you are writing a Python script or program, using something simple such as unittest would suffice (although more advanced frameworks exist). If you have never used a unit-testing framework, I highly encourage you to look at these. They provide a significant amount of infrastructure to let you focus on the semantics of testing rather than the infrastructure.

should be used. For example, if you are writing a Python script or program, using something simple such as unittest would suffice (although more advanced frameworks exist). If you have never used a unit-testing framework, I highly encourage you to look at these. They provide a significant amount of infrastructure to let you rather than the infrastructure. If using a test-framework, this should make testing extensible. Make sure to embed documentation in your testing programs as well (i.e. “how to add a unit-test”). If someone is extending your code, they need to add a test for it. Make it easy to do by way of a starting point and examples. Having these tests also sets the standard for the next person.

Without such a testing infrastructure (which is not a large time investment especially when using a testing framework), you are essentially

asking another human to fend for themselves in the dark.

Providing a sufficiently comprehensive, informative and extensible test framework, gives the next person the necessary tools to have the courage and ability to improve and extend the code in a productive manner.

Summary

Providing automated tests and helpful source-level documentation is empathetic because it involves thinking beyond oneself. It is a pattern of “paying it forward”. Providing these key elements is a demonstration of leadership because it empowers others and enables you to tackle new problems.

If you are a leader, and your team develops internal software, make sure to expect this and give your team members the time to develop internal software in this way. It will allow your team to thrive and build a culture of empathy.