The 7 habits of high-quality software
Developers who follow these rules will produce applications worth keeping
I have been doing a lot of Java programming recently on a free version of a knowledge base editor called EZKB — for Easy Knowledge Base. I am just finishing the second of four user scenarios required for me to consider the software minimally useful and thereby ready for public release. E-mail me — my e-mail address is at the end of this column — if you would like to be an alpha tester before that public release.
This recent intense period of programming brought a flood of memories and old insights related to the discipline required to craft high-quality code. Like most endeavors, there is a strong desire in programming to quickly complete some flashy new feature at the expense of programming discipline, which, in turn, means at the expense of high-quality code.
In this article, we will explore seven of the best habits I have learned to create quality software. That will be an important preparatory discussion before we revisit my more controversial — to some — warning against agile programming in a previous column.
Now, with apologies to Stephen Covey, on to the seven habits.
1. Detailed user scenarios. A user scenario is a populated use case that walks through how the software solves a specific user problem. The user scenario should be formal, and developers should write it down. Much of Apple’s success, for example, comes from its laser-like focus on things that matter to the user, not the programmer.
2. Automated regression tests. Software testing has evolved over the years, and there are now good frameworks, like JUnit, to help you build tests as you go. I was recently shocked to learn that a major Army software system had no automated regression testing, which is the process of retesting software to ensure that no new errors were introduced while fixing other errors or adding new functionality. This is a question all government information technology managers should immediately ask their system developers: How many automated tests were run against the latest build, and what percentage of those tests are functional regression tests?
In general, many, if not most, software errors are caused by the left hand not knowing what the right hand is up to. More specifically, this means changing existing code in a manner that breaks old code because the ramifications of the change are not understood or the existing code is complex or poorly coded. With regards to software maturity, which is highly correlated to software reliability, change is the enemy. But it also is necessary, and there’s the rub.
3. Polymorphism, patterns and good design. Given a complex problem, novice developers often implement the easiest, and typically the first, solution that comes to mind. Unfortunately, those initial, obvious solutions usually perform poorly in real-world conditions. This is where design comes into play.
Given a detailed user scenario, a senior developer will know which elements of the scenario are easy to implement, such as collecting data from the user, and which elements will be difficult, such as relating incoming data to existing data. Additionally, given multiple user scenarios — you should require a minimum of three — a senior developer will know how the scenarios reuse parts of the software, and that begins to form the building blocks, dependencies and layers of the software architecture.
Make no mistake; every software application resembles a physical building with plumbing, wiring, insulation and a foundation. If you plan on living in your software/building for a while, I recommend a good set of architectural blueprints. A specific example of this is the use of polymorphism for extensible code. Polymorphism enables a parent/child relationship between code, where a child module, called a class, can override a parent’s function of the same name. This allows you to seamlessly plug in a new child class whose overridden functions will be automatically invoked by the existing software without changing the invoking code.
Remember how we said that change is the enemy? Well, this is a technique that allows new functionality with minimal change to existing code. Some forethought in that regard goes a long way to software reliability.
4. Refactor poor design. Even in well-designed software, and especially in complex software coded by multiple developers, there will be some poorly designed code. That is where, in terms of seasoned developers, we separate the men from the boys and the women from the girls.
Refactoring is when you purposefully change or even rewrite existing software to improve its design, quality or performance. I learned this in 1994 by watching an exceptional developer, named J. Keller, recode recently submitted code from novice developers because it was inefficient or failed to reuse previously developed code libraries. In other words, he was refactoring code well before that term became popular.
At first glance, it might seem risky to purposefully change code that is working, especially because we said that change is the enemy. However, the reality is that although poorly designed things can work in the short run, they do more harm than good during the lifespan of the system. As a simple analogy, I can use wood to build a bridge over raging waters, but that won’t perform well under stress in the long term. I can also use a screwdriver as a hammer with the same inherent problems. Senior developers are always vigilant to protect the code base from poorly designed code. Fortunately, modern Integrated Development Environments have excellent built-in support for refactoring.
5. Practice defensive programming. A defensive programmer is one who seeks to create functionality while minimizing the probability of error. Specific techniques for this are loose coupling, catching exceptions early and often and localizing the impact of an error, such as bad data. Like defensive driving, defensive programming is the only way to safely navigate potentially hostile environments.
6. Always increase code maturity. This is a critical habit that is often overlooked. This habit is the direct opposite of seeking instant gratification, whereby you are willing to always eliminate redundant code.
Many times in development, a new feature has parts that are similar or identical to an existing feature. The temptation is to simply cut and paste the code from the existing part of the program into the new feature’s code body. However, that duplication of code creates more new code instead of reusing code, which would increase the maturity of the existing code.
The reason many developers choose to cut and paste code instead of the better route is that refactoring the existing code into a reusable, encapsulated new method or class is more work. It takes more forethought, more time and even more courage. In other words, with a little extra effort we can increase code maturity via reuse instead of creating redundant — or nearly redundant — code. This discipline can pay huge dividends during the life of a project. Mature code leads to reliable software. Mature code is code that has been executed countless times under various stresses and is thereby more trustworthy than new code.
7. Always think about the user. Keeping your user scenarios and user satisfaction as your primary goal gives you the motivation to code it right instead of coding it fast. A programmer confronts this choice often, especially in the mundane task of crafting error messages. It's funny how doing the mundane tasks right shows your true mettle.
A good error message is written to help the user, not confuse him or her. All messages to the user should be polite and upbeat. You must always keep in mind that the holy grail of a good product is lots of happy users. Finally, always give the user lots of feedback on what the program is doing. Never leave the user hanging or wondering whether the program is working. Finally, in user interface design, always make the state of the program visible and the controls as intuitive as possible. Provide good documentation, which by now should include screen shots of all key functions. Making the life of the user easier is a goal for all software.
Practicing these seven habits of high-quality software will keep IT as a strategic mission multiplier for federal, state, local and tribal governments.