From time to time, I wonder why correctly functioning software is so difficult to write. Software development is something we’ve been doing a long time, that also has a benefit that many other crafts don’t: you can create a repeatable process that does exactly the same thing each time. Given this huge advantage, shouldn’t this allow us to develop software with a high degree of certainty?
Almost like a chess game, there are many ways to reach your desired outcome (“winning”) when developing software. As developers, we’re often free as a bird – even the most simple of processes can be implemented in a myriad of ways within the same language, not to mention the variety of languages and frameworks that are available. I find that, for better or worse, I’ll rarely write the same code the same way twice. There are a number of reasons for this:
- At a very basic level, there is an element of self-expression to software development. Depending on how I feel at a given moment, I may use a different control flow structure, different syntactic sugar, or even a different domain model.
- I’ve often learned something new since last time (yay!), and have found a more elegant, or more performant way to implement the same feature.
- The language or tooling has evolved – if for no other reason than staying current, I will try to implement with the latest patterns unless there is a good reason not to.
- There are usually changes in requirements, even if very minor.
In my opinion, the points above are acceptable reasons to keep mixing up your approach, and this flexibility and fluidity with how we write software has some benefits:
- You can just start developing, and often get to a working solution, without having to consult reams of documentation about the Right Way
- You can develop in a way that suits how you think about software and what your current knowledge of the domain, the programming language, and the craft in general is. Because there is no One True Way to develop a given feature, the barrier to entry is much lower.
Software development truly has a level playing field: the number of hardware platforms and languages available is incredibly vast, and the amount of free knowledge available (assuming an internet connection of some kind) is mind-blowing. Just copy and paste, and off you go.
Despite this lack of consistency, the majority of the code I write has no bugs, when you examine it at a granular level. And I say this not to brag – I think the same is true of the vast majority of developers. Much of the code we write just works.
But, there is a dark side to this flexibility and malleability of software development. A single incorrect or misplaced character can be the difference between success and utter failure. I can’t count how many serious bugs came down to flipping a boolean value, or reversing the direction of an operator. Bugs like these can mean lost data, infinite loops, etc., but more importantly, can cause loss of revenue, customers, or even spaceships.
How do we reconcile these conflicting conditions? How do we add rigor to a process that is inherently free-form, while at the same time having the most rigid requirements for precision?
The real issue is that all software has bugs, because all software has unstated or unknown requirements. Then, if all software has bugs, should I just give up? No, instead, lean into it. Knowing that all software has bugs gives us a clear focus: we can assume those bugs will need to be addressed in the future. The code will need maintenance; therefore, the best thing we can do is write maintainable, not perfect, software.
Borrowing from Michael C. Feathers, I would say maintainable software is software with good test coverage. I am all for code documentation, linting, best practices, etc., but these things can all change or become out of date. A well-written unit test with an interpretable name is worth its weight in gold. Some of the things it can tell you are:
- Whether the code still works or not (green/red)
- What the test is validating (that’s where the good name comes in)
- How common actions get typically performed by the code
In short, it’s a very succinct piece of documentation that can tell you when it’s no longer correct. But, perhaps more importantly, it tells you that someone cared enough while developing this software to write tests.
Unit testing falls under a category of software that I have come to think of as Subversive Design (I plan to write more on this soon). Simply put, Subversive Design is design that is so good that even if you discard the main purported benefit of the design, there is still huge value to be had from having implemented it.
In the case of unit testing, I would argue that if you can get your app to 80% test coverage and then delete your unit tests (don’t do this!), it will still have been entirely worth the effort. That’s because there are benefits to writing tests beyond having the tests themselves, some of which are:
- You have had to make your code testable. At the software level, this can mean:
- Introducing dependency injection
- Breaking up large methods into smaller, single-purpose ones
- Removing heavy reliance on static methods or global variables
- You’ve considered what should/needs to be tested. This in itself is highly valuable information for developers that will need to maintain the code; we rarely unit-test the most non-critical code first.
- You might have re-organized your code into more logical units once you thought about it from a testing perspective.
- You may have identified performance bottlenecks by being able to test your code in an easily-repeatable, granular fashion.
My takeaway here is that all software has bugs and that is not going to change. So, the best thing we can do is focus on maintainability. And have fun!