78. Maintainability#

What makes a piece of software good? Sure, it solves a problem. It does what it’s supposed to do. But a truly good piece of software is much more than just its functionality. It’s about how easy it is to change, adapt, and improve over time. It’s about how long it takes to find and fix a bug. It’s about how easily new features can be added or existing features can be improved. These qualities are all parts of a concept we call ‘maintainability’.

important

How do we provide value today without compromising our ability to provide value in the future?

In today’s fast-paced world maintainability is a crucial consideration. Mark Zuckerberg, CEO of Meta (formerly Facebook), famously said, “unless you are breaking stuff, you are not moving fast enough”. This mantra captures the ethos of contemporary software development - it is a never-ending cycle of evolution and adaptation to new requirements. To remain in business we have to adapt. Only the fittest will survive.

This shift in paradigm has reshaped our understanding of software. Rather than a static product, software has transformed into a dynamic service, continually rewritten to meet the shifting needs of the customer base and stay ahead of the competition.

The software isn’t finished until the last user is dead.

—Sidney Markowitz

What’s worse is that it is surprisingly difficult to figure out what we should even build in the first place. This is perhaps best explained by the classic ‘tree swing cartoon’ which has been circulating in various versions since the 60’s. Knowing what to build is often a much harder problem than actually building it.

../_images/tree-swing-cartoon.jpg

Fig. 78.1 Different people tend to have very different things in mind when describing and building a product. [Image source]#

Before the advent of agile software development methods, there was a common belief that the cost of change in software development exponentially increased over time. The bigger the project, the more elements it had, the more difficult - and thus, costly - it was to maintain and adapt. More stuff equals more problems equals lower speed.

It’s like the perfect storm. We have to keep rewriting our software to stay in business, but everytime we rewrite it, it becomes harder and harder to change.

However, agile methodologies, like eXtreme Programming (XP) proposed by Kent Beck, have challenged this notion. Agile methods promote short development cycles, continuous feedback, and code that is written to facilitate future changes. These practices help to flatten the ‘cost of change’ curve, making modifications less costly over time.

../_images/cost-of-change-curves.jpg

Fig. 78.2 Before the advent of agile software development methods it was commonly believed that the cost of change exponentially increases with time (see solid line). Authors of some agile methods such as Kent Beck of eXtreme Programming aruged that the cost of change curve can be flattened out (see dashed line). The truth probably lies somewhere in between (see purple line). [Image source]#

There are many ways to address move fast in the face of uncertainty, but improving maintainability by focusing on how we write code quality certainly is one. How we write our code determines how difficult it is to rewrite it.

Important

How we write our code determines how difficult it is to change.

All these considerations underscore the importance of maintainability in modern software development. While we could have focused solely on the ‘changeability’ of software, this would not be a broad enough view. A well-architected piece of software also needs to be testable to verify and validate its functionality.

Maintainability, therefore, encompasses a broader set of qualities. As per the standards document ISO/IEC 25010:2011, software maintainability is characterized by:

  • Modularity

  • Reusability

  • Analysability

  • Modifiability

  • Testability

In the sections that follow, we will briefly explain each of these facets. In later chapters we will show different techniques to employ and principles to follow in order to improve these facets.

Key point

To provide value today without compromising our ability to provide more value in the future, we must focus on maintainability which can be understood as: modularity, reusability, analysability, modifiability, and testability.

Modularity#

Degree to which a system or computer program is composed of discrete components such that a change to one component has minimal impact on other components.

—ISO/IEC 25010:2011

If we want to change something in a specific part of our code base, how many other areas do we need to adjust for this change to occur? This is the question that modularity deals with.

In essence, modularity manages software complexity by breaking it down into distinct, loosely coupled components. This design approach allows developers to modify or update a specific part of the system without causing the need for widespread changes, fostering more efficient and less error-prone adaptations.

Conversely, software that lacks modularity and is tightly coupled presents challenges when changes need to be implemented. In such instances, modifications can trigger ripple effects across the system, demanding alterations in multiple areas. This interconnectedness can result in increased time, cost, and the potential for errors.

Reusability#

Degree to which an asset can be used in more than one system, or in building other assets.

—ISO/IEC 25010:2011

A high level of modularity does not automatically imply high reusability. If code is not being reused despite its modularity, there may be underlying issues such as code duplication. Ideally, we should strive for fundamental abstractions that enable us to build systems from as few unique components as possible.

However, reusability should not be viewed as an end in itself, but rather as a tool. It needs to be economically justifiable; we should aim for reusability when it aligns with the actual reuse potential. Striving to make our software solve problems we don’t yet need to solve is, arguably, a strategy that will put us out of business.

In software development, the term “premature abstraction” captures the pitfalls of generalizing code too early, before the full scope of problems to be solved becomes clear.

This idea has parallels in business. Authors like John Warrillow (see “Built to sell” argue that businesses should develop robust systems for selling a few products or services before branching out into new verticals (which would require an even broader system). For software developers, this suggests a focus on solving current, known problems instead of designing for hypothetical future scenarios. What is the point of solving future problems if we can’t stay in business by solving our current ones?

Analysability#

Degree of effectiveness and efficiency with which it is possible to assess the impact on a product or system of an intended change to one or more of its parts, or to diagnose a product for deficiencies or causes of failures, or to identify parts to be modified.

—ISO/IEC 25010:2011

When writing code, think of it as a real-world model. The more straightforward your abstractions and the fewer you have, the simpler it is to reason about your model. Note the emphasis on “simplicity” over “size” – small code is not always easy to analyze, but simple code is.

The goal should be for software components to be modular, reusable, and analysable, not just small. This is where the concept of ‘code golf’, a programming competition where the goal is to solve problems with the smallest possible source code, falls short. While these solutions may be concise, they are practically indecipherable to humans and thus not analyzable.

“Why would I need to analyse my code?” you might ask. “If I write it correctly the first time around, there’s no need for analysability.” This mindset overlooks the complex nature of software development, which often involves incrementally building upon code and adjusting for unforeseen issues. Remember, survival of the fittest.

Tip

Write code under the assumption that we will get it wrong.

Unreadable code becomes a liability when changes are required – it may need to be rewritten from scratch, or a significant amount of time must be invested in examining it line by line, or even character by character. When we write unreadable code, we say that we are incurring ‘technical debt’. Just like monetary debt, we will at some point have to pay our dues.

Analysability should be a priority for any piece of code that we wouldn’t be willing to discard and rewrite from scratch for minor changes or bug fixes. This doesn’t mean that every level of a system must be equally analysable. However, if we’re not ready to throw it away, it should be analysable.

Modifiability#

Degree to which a product or system can be effectively and efficiently modified without introducing defects or degrading existing product quality.

—ISO/IEC 25010:2011

In a world where requirements change, technologies evolve, and businesses scale, software must adapt seamlessly to maintain its value. Modifiability ensures that software can be updated and adjusted without compromising its overall quality or functionality.

Design decisions made early in the software development cycle greatly impact future modifiability. Careful selection of programming languages, frameworks, and architectures can help create a foundation for modifiable software. Likewise, good programming practices, such as clear documentation, consistent coding standards, and extensive testing, facilitate later modifications.

Back to the same question: “Why would I need to change my code?”. “If I write it correctly the first time around, there’s no need for modifiability.” This is not how the real world works. To provide value, we have to adapt to the ever-changing environment. Remember, it is wise to write code under the assumption that we’ll get it wrong at least the first time.

However, a high degree of modifiability doesn’t mean that changes should be made carelessly. Every modification comes with the risk of introducing new bugs or unexpected behaviors. For this reason, modifiability works hand-in-hand with other maintainability factors like analysability and testability. Analysability ensures that the impact of changes is understood before they are implemented, and testability allows those changes to be verified, ensuring they work as intended without introducing new issues.

Modifiability is also inherently linked to the principles of reusability and modularity. Code that is written to be reusable tends to be more modular and abstract, which in turn allows for easy modifications with minimal impact on the overall system.

In summary, striving for modifiability is about striving for code that can be changed.

Testability#

Degree of effectiveness and efficiency with which test criteria can be established for a system, product or component and tests can be performed to determine whether those criteria have been met.

—ISO/IEC 25010:2011

To stay in business we need to rewrite our code quickly and often. But every time we rewrite our code we also need to make sure that the code still works. This is what our tests should tell us.

Warning

Changing code quickly without automated testing is an accident waiting to happen.

Highly testable software is a foundation for the long-term sustainability of any software project. Software that can be easily tested promotes confidence when changes are made, assists in identifying issues quickly, and ultimately ensures that the software works as expected. Key characteristics of testable code include being deterministic, isolated, and observable.

Deterministic: Deterministic code is reliable and consistent in its behavior. Given the same inputs, it will always produce the same outputs, and it will not exhibit different behaviors in different runs or under different conditions. This predictability simplifies testing significantly because it means that any test we write today will still be valid tomorrow.

Isolated: Highly testable code is independent, or isolated, meaning that it can be tested without having to also test a lot of dependencies at the same time. This is important because it helps us narrow down where a problem might be when a test fails. Isolation can be achieved through techniques such as dependency injection, mocking, and designing for interfaces.

Observable: We must be able to observe what actually happens when the code is executed. This could be through returned results, changes in the state of the system, or observable effects. Observable code allows us to validate that our code is functioning as expected.

By striving for testability, we build software that is resilient to changes and more reliable for the end user.