Two months ago, I was a guest on the Maintainable podcast. The first question the host Robby Russell asks is “What are a few characteristics of well-maintained software?”. This is such a great question, and I thought I would expand a bit on my answer from the show.
That software is well-maintained only matters if you need to change it. If you never have to change it, it doesn’t matter how it is done, as long as it works. However, in pretty much all cases you do need to change it. For me, well-maintained means that it is easy to change. And for the software to be easy to change, it must first be easy to understand. These are the characteristics that most help me understand how a program works:
Being able to see the different steps done by the program is the key to understanding what it does. Methods with descriptive names help in explaining the flow. For example, processing a message may be done like this on the top level: parseInput(), removeDuplicates(), processRequest(), sendResponse(). In processRequest(), the same pattern is used recursively. The work that needs to be done is again separated into different methods, until we end up with reasonably-sized methods of only statements.
I believe this is how a lot of software is initially written. However, as time goes by, code is added into the methods without adding new sub-methods as needed. The result is methods with hundreds of statements after each other, without structure. It still works, but it is now much harder to understand the overall flow. When you read this code for the first time, it is difficult to see the bigger picture, and thus harder to understand.
There are many reasons this happens. Following the existing pattern (just adding into an existing method, instead of creating a new one), wanting to make the smallest possible change, time pressure, or not being familiar enough with the code. I’ve written more about this in Code Rot. The solution is usually to add more methods.
When I worked at Nasdaq, I was taking over a middleware system when the last original developer quit. The system routed messages between the trading system components, and also handled failovers. It was written in Erlang, and was quite compact in size. It was enormously helpful that there were plenty of test cases for the system. To understand how it worked, I could both read the code, and run the code. There were unit tests, integration tests and end-to-end tests (running a complete system). The end-to-end tests were the most helpful in the beginning, because they allowed me to see how all the pieces worked together.
Tests are useful for automatically checking that the code does what it is supposed to do. But a side-effect is that it is possible to execute parts of the code without much effort. The work of setting up the state needed to run the code has already been done by the author of the test. So in addition to reading the code, I can also run the part of the code I am currently interested in. This greatly helps in understanding how it works
Descriptive names of variables, methods, classes, files, database tables and so on really help with understanding. Ideally, reading the name of a method should make it clear to you what it does. But even when the names are clear and descriptive, there can be problems. One common case is that there are several names for the same concept. Another is when the names in the program are not the names used by the non-programmers. Getting this right is harder than it sounds.
Being able to run the program helps a lot to understand how it works. However, just getting the result back, without knowing how it arrived at it, is not enough. This is where logging comes in. It allows you to “see” the execution of the program. Good logging helps you understand the flow of the program. Too much logging doesn’t help, since that obscures rather than clarifies.
If there are errors, logging can help you find what went wrong. Note that an error doesn’t have to mean that an exception was thrown. Arriving at the wrong result is also an error, even if the program executes without exceptions. I have written more about logging in Good Logging.
I used to think that code should be self-explanatory, so that there would be no need for comments. But I have changed my mind. I think comments are often very helpful in understanding how a program works, and why it is written the way it is. It is most useful when explaining tricky or unusual cases. More about it in On Comments in Code.
One of the topics we covered in the podcast was There is No Software Maintenance. My argument is that we should not think of software development as having the phases “development” and “maintenance” – it is all just software development. Is it a paradox then that I talk about well-maintained software? Not really, well-maintained software in my mind is the same as well-written software.
So why is the combination of traits above important? Because they help making the existing program easier to understand. In my mind, this is the essence of well-written software! I am interested in hearing what other developers think characterizes well-maintained software. Let me know in the comments.