Software Maintainability

Software Maintainability best practices in general:

Why maintain code at all? Profit. Profit is revenue less expenses. Expenses include time-based wages. Effort is time. The thought is that it will probably take less effort (lower cost, higher profit) to improve an existing product than to start from scratch. If that’s not true, we should start from scratch.

With respect to effort, it is both time and metal burden. Humans fatigue, unfortunately. We can’t hold a high level of focus for long periods of time, day in and day out, especially not in a typical office environment with interruptions galore. Complexity in systems requires higher mental effort. All other things being equal, mistakes are more likely for tasks that demand high levels of focus or mental effort—also termed cognitive load. Additionally, the start-up time to get to the point of focus required for a high cognitive load task is longer than a task requiring a lower cognitive load. Think about following the storyline of a calculus math proof vs. a cartoon. So, ultimately mentally taxing tasks or high cognitive load tasks require more startup time from interruptions, result in more mistakes which consequently take more time correcting, and ultimately result is larger time costs and monetary expense and therefore reduced profits. In other words, mental effort, which is increased by complexity is also cost.

If it were really easier to restart a software product from scratch that does essentially the same things as the existing software, we really ought to ask why. It still seems like it would be easier to take the thousands of hours of work of others as a starting point and make the needed modification to it.

What does it generally take to make a software change?

  • Understanding the change
  • Identifying the subset of code impacted by the change
  • Understanding the existing functionality well enough to implement the change without side-effects
  • Implementing the change

With that in mind, what affects the difficulty (effort required) of accomplishing each of the above tasks?

Understanding the change is likely independent of the particular codebase it affects. We’ll set that aside for now.

Identifying the subset of code impacted by the change

Assuming the codebase is large, my hope, as a developer, would be that I would not be required to understand the entire codebase in order to make a relatively small change. Tightly-coupled, or inter-dependent code requires more effort to change. It generally requires understanding all coupled or inter-dependent parts of the code. If I make a change to object A, how do I know it’s impact to objects B, C, D, through Z that are dependent on or coupled to it in a number a ways? To be certain, I would have to then understand all related objects and their interactions. This takes more time/effort/cost.

On the other hand, if the coupling is loose or there are few or no dependencies between object A and the other objects, I can generally ignore them in making the change. This saves me time/effort/cost.

What constitutes a “coupling” or “dependency”?

This is probably self-evident to most. For tge sake of concreteness, some typical examples include:

  • a function that uses a variable
  • a function that calls another function
  • a class that has a pointer or reference to another class
  • a class derived from another class

Each time one of the above scenarios is created, we have created a linkage, dependency, interaction, or coupling between two components of code. Fewer couplings —> simpler code.

Couplings are necessary. Of course, I’m not suggesting removing required interactions. Rather, removing all unnecessary interactions is the goal.

This concept of minimizing couplings is part of what I refer to as “code modularity”.

One common bad example of unnecessary coupling is passing an entire object to a function, when all that is needed for the function to do it’s job is a single value from the object. Why not instead just pass the one value to the function? Now, we I see this function for the first time as a developer making a change to this function, I can ignore the entire class right of the bat because there is not even a reference to it in the function. Eventually, after reading the function, I would figure out that it just uses the one variable, but wouldn’t it always take less effort to understand if just the variable were passed as an argument rather than the entire object containing it?

Code modularity through minimizing couplings makes code more maintainable by reducing the amount of code a developer has to understand in order to make a change.

Understanding the code well enough to make the change without side-effects

This topic is related the identifying the subset of code, since it takes some level of understanding in order to identify the right right subset. Once I’ve narrowed down the codebase to what appears to be the affected subset, I have to spend some time reading it more carefully in order to understand it well enough to implement the change. The less code I have to read, the less time it will take, so fewer couplings helps with this step as well.

There are a number of other characteristics that make understanding the code take less time and less mental effort or lower cognitive load during that time.

What makes code quicker and easier to understand?

Minimizing required learning. There’s a lot to unpack from that simple phrase, let me explain. The less I have to learn from the code, because I already know it, the quicker it will be for me to read it and understand it. What kinds of things might I have to learn?

  • programming language
  • syntax
  • naming conventions
  • usage of a variable, class, function or other software construct
  • a particular software algorithm
  • software architecture
  • libraries or frameworks
  • integrated development environments (IDE’s)

The less of this I have to learn, because I already know it, the quicker I will be able to absorb the code.

It’s helpful to keep in mind that developers are at different levels of experience. If you use obscure capabilities of a language or do “unusual” things, the developers who come after you may have a more difficult time modifying and maintaining your code. From an individual perspective, it might seem cool to be able to do cool things with the language, but from a software product standpoint, it may make the team suffer. So, please, let’s all become masters at our languages and use them to their fullest while at the same time helping others become masters of the language as well and not going too far beyond the capabilities of our group. By the way, your code might just replaced by a later developer who couldn’t make sense of it and, from their perspective, “cleaned it up” for you.

Here’s some things that make understanding code easier:

  • Simple functions
  • Small functions
  • Pure functions
  • Naming that communicates intent
  • Consistent conventions in style, grammar, naming, etc.

Simple, Small, Pure functions

When I glance at a function, I want to know what it does, inputs and outputs as fast as possible. The next thing I want to know is how is produces the outputs from the inputs. The shorter a function is the faster I can understand it. Short isn’t a explicit measure of the number of lines of code. It’s a metric of complexity of the function. It’s a short function in terms of the number of concepts it has to deal with–how many things it does, how much nesting, how many if’s, how many loops, how many branches in the logic, and yes, how many lines as well. We’ve all seen code that was one or two lines of hell. Why pack so much into one line? Spread it out so it’s easier to follow.

Comments are helpful to communicate intent, but the code actually executes. We need both to be readable and as obvious as possible, within reason.

For concreteness, pure functions have outputs that depend only on the inputs. A pure function doesn’t depend on anything other than the input arguments to the function and has no internal state. The exact same inputs result in the exact same outputs, all the time, no matter what. Pure functions are more independent (e.g. modular) that equivalent non-pure functions.

How long does it take to know exactly what a function, variable, or class does after reading the name? The faster the better. But, naming is hard man. It is. Good naming that can continue to meet this goal as a project grows and new features are added is hard. If I only have to distinguish between dark and light things, I can just call one dark and the other light. But when I create a third darkish thing, I have to distinguish it from the existing dark thing, so “darkish” becomes the name? Then we have to add a lighter than dark thing, so “lightish” sort of makes sense with the existing naming conventions, until we add yet another medium-dark thing. Ok, you can see where this is going and surely you’ve seen this development occur. So deciding on terminology that is stand the test of time and added features can be a bit challenging and get’s clearer with experience.

Scoping helps divide of the global space into subsets that make naming easier.

All of the above can be accomplished in any language and are really a matter of developer discipline and experience.

The following are some characteristics that could be affected by the language:

  • Strong typing
  • Static typing

Pro:

Con:

Non-static, weakly-typed https://dev.to/codemouse92/comment/26p7


by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *