Software Is Never Finished and Code Can Always Be Improved

“Software: never finished, only abandoned,” John Saddington paraphrased Leonardo da Vinci, around a year ago, in the best blog post I have found for the well-known adage “software is never done”. Where it actually originated I have no idea.

A lot has been written on this saying, but in a lot of cases there is also what I would characterize as confusion of the concept of software, leading to an unconstructive debate. I define two separate terms: software, which ideally helps or solves problems for people, and code, which is ideally the means of producing fault-tolerant, extendable software.

Software consists of code and code builds software, but they are two different things at separate levels of abstraction. I think it is more constructive to discuss each separately, to see if it is true that (a) software is never finished, and (b) code can always be improved.

I define Finished Software as fulfilling the following statements:

This operates very much on the plane of the user, because software is for users. Notice how finished software is not simply software we have delivered.[1] Finished, to me, means without useful improvements to be made.

In contrast, I would define Finished Code as such:

This focuses entirely on technical aspects of the code: how the current of future developers will experience it.

Either of the definitions can, of course, be extended, but they will do for now. They will let us explore whether Software or Code can ever be Finished.

Plans change #

Software should work within the domain of a user, and as such the definition of finished depends on the domain.

Software developers rarely have detailed knowledge of the target domain. This means that the development process is either based on the knowledge of outside experts who are involved as stakeholders, or on experience gained through observing the current processes used in the domain.

It’s hard to know what the optimum solution for a domain problem is, both because users rarely know their own problems, and because manual processes often translate poorly to automated ones.

If you don’t know what the best solution is, you have no way of achieving it, and this gives us a plausible explanation for why software is never really done:[3] more knowledge can always be gained, better solutions found.

In addition, domains themselves are never static. They are ever changeable, and that means that the best practices, processes and conventions change too.

Software is never done because it solves problems that change both while and after they are being solved.

Good Code enables Good Software #

I found a curious response to someone asking why people say that software is never done. “Hello World is pretty complete, it does its job without fail every time,” it read.

Hello World, in my opinion, is not a piece of software. It is an example of a language feature, but it solves no problem and helps no users. Hello World is not software, it is a helpful code snippet that illustrates how a programming language is used. Even accepting Hello World as code and not software, per my definition of finished code, it is not finished either.

Is this a failing of Hello World or is it because code, inherently, can never be finished?

If the code is very brief, and for educational purposes (ie. Hello World) the answer is “it could probably be finished”. This applies in most cases where the code is not part of software.

If the code is used in actual software (which attempts to solve real problems for real users) the answer is a bit more complex.

I will disregard the requirements for code being well-tested and efficient: these seem immediately achievable by some definition — for example, ample unit and integration test coverage. Of course, some people like Jim Coplien would argue that it is impossible to get complete coverage with unit tests, and that they are often wasteful, but even then some other definition of well-tested will be accepted.

This leaves a single requirement from my definition in the start of this post: Finished code has useful abstractions that are easy to extend and reuse.

Useful abstractions are abstractions that capture the domain, and fit with the rest of the system.

If you are mapping bank software you want your code to describe “accounts” and “transferring money”, not “money collections” or “giving money”. You want to use abstractions that capture what the concepts represent in the domain, so you use a terminology that is compatible with the one used by the people you are interacting with to uncover requirements. You want to avoid misunderstandings.

Useful abstractions in code make it easier to make good software. They are by no means a guarantee, but I’d go as far as to say that they are a prerequisite: using the wrong abstractions, or a different terminology from the domain experts is bound to result in a bad solution.

This connection to the domain means that Finished Code inherits many of the challenges of Finished Software. Uncertainty about the domain leads to uncertain abstractions, and changes in the domain leads to changes in what is a good abstraction.

You will get to know your system #

Another challenge for Finished Code is that “useful abstractions” must fit the system, and knowledge about the system is rarely substantial enough at the outset to ensure a good design right off the bat. This applies to every part of the code: architecture, abstractions, naming.

This is one of the reasons iterative development has won out against the Waterfall model. The iterative model ensures a steady flow of feedback, informing future decisions. It is hard (not impossible, but hard) to design a system of good, well-fitting abstractions without first seeing how they interact.

Using iterative development is embracing the fact that changes to our perception will happen. Hopefully, most of these changes will happen outside of the code that is already written, but there will always be some assumptions that turn out to be wrong after implementation.

You get to know a system as you are building it, and, in the process of doing so, realize what fits and what doesn’t. The longer you work on a system, the more you test and stress your early abstractions, the more likely it is that you will find a better abstraction for your system.

Deciding when to refactor is a big problem in itself, so I won’t even begin to discuss that here. Suffice it to say that knowing that you can probably always find something to refactor or some abstraction to improve is knowing that code can always be improved.

Get moving! #

All of this might seem a bit bleak. If software is never finished what is the purpose of building it? Of course, software can be valuable without being finished. Code can be valuable (in that it is part of valuable software) without being finished.

I see the term finished in much the same way as the term perfect. It’s not achievable: nothing is without flaws. However, knowing what steps to take to move towards finished (or perfect) is important, because that moves your software in a positive direction.

Many of the development methods and practices that have developed over the course of Software Development as a field have moved us in a positive direction: we are now much better at handling changing requirements and growing knowledge.

Iterative development lets us get started. The idea of an MVP tells us to deliver a useful (valuable) piece of software in as little time as possible, because we know requirements are changing. If we deliver a big project after four years of development, it won’t fit the domain it was designed for: the domain has changed, the software solves problems of the past (or in ways that are now suboptimal).

My advice is this: focus on writing good code, focus on making good software. Get as close to finished as you can, or as is valuable to your business. Be aware of what makes software or code finished, and move in that direction.

Notes #

  1. I chose the “no meaningful improvements can be made” definition of finished because this makes for meaningful debate.
      Had I instead accepted either of the “it is delivered” or “no future changes will be made” definitions, discussion would be meaningless. If “finished” is down to a choice made by the developers or their managers, then no meaningful discussion of the nature of software can be had on the subject.
      It is in the nature of software — because it is made up of easily editable text — that it can always be changed.
      If a manager insists at some point that no more improvements will be made to a piece of software, and it is declared finished, but then 10 years later it needs to integrate with a new piece of software, and developers start improving it again … was it ever really finished?
      This may be confusing because others discuss finished-ness using the other definition. I wouldn’t be writing this post, however, if I didn’t think my definition was more meaningful.
  2. Ease of extension implies readability. Code that is hard to read is not easy to extend. Readability is therefore an implicit part of my requirements for finished code.
  3. I have used the terms done and finished interchangeably in this post, because both terms have been used about software in the old adage “software is never {finished,done}”.
      The definition of done here collides with the Definition of Done (or DoneDone) used in Scrum and many other development methodologies. In this context Done does not truly denote that a piece of software (or a feature) is done, but rather that it is delivered or no longer being worked on, and a more precise wording could be used. Definition of Ready for Delivery, for example.

You made it to the end. If you want more sign up for my newsletter.


Now read this

Centralized vs. Decentralized Routing in Web Apps

Routing is the act of describing possible URLs and the components of an app that should handle requests to these URLs. Centralized routing # In Ruby on Rails, which is probably the most influential web framework of the present,[citation]... Continue →