What I learned about prevention in software development
‘Prevent problems before they occur’ is the mantra I try to live by, both in my personal life and at work. I have not been entirely successful, but I am learning to get better at it. I exercise every day, eat healthy, and brush my teeth regularly. I have learned that similar actions done regularly during software development prevent maintenance issues and bugs, and make coding so much easier and faster. Small positive habits pave the way for big positive changes. Much has been written about this (see the writings of Alan Shalloway and his team, Uncle Bob, and Martin Fowler for example), and is what the lean movement is all about. However, I’d like to present a few simple things that I have learned from personal experience and that make life easier for the software developer, maintainer, and the business developer. I hope to build on this theme in my later posts, with concrete examples of problems I have encountered, and how they could have been prevented or at least alleviated with good practices further upstream the development process. I also hope to improve myself as a result of this exercise.
- A good design is the foundation of any software product. This pertains to all layers of the application, the database, the business logic, and the user interface. Towards this end, I have found the following tips to help:
- Modularize the design, so that common code is housed in its own module, and exposes itself with interfaces that clients can consume. Do not use the module in any way but through its interfaces. (I have seen architectures where a module was designed to be consumed as a web service, but since client code had access to the module in other ways, they were exploited, leading to very confusing references)
- Normalize the database – do not store duplicate data, and maintain data integrity by adding all relationships to be enforced to the database. Recently, I worked on an issue where a piece of data, say data1 was linked to another, say data2 through two tables. One of the tables was updated to set the relationship as inactive, but the other table retained the relationship as active, leading to a conflict. Since the problem was detected after much code had been written, it led to work that could have been avoided.
- Let each layer do what it does best. Do not move the work to another layer. For example, databases are designed to work with large pieces of data. Use stored procedures where appropriate, instead of moving the logic to the application layer. Loading or deleting large pieces of data can cause significant performance issues otherwise.
- Choose architectures that can be tested easily. If a piece of software can be tested readily in a repeated manner, it can be maintained more easily without introducing or reintroducing bugs. I learned this the hard way, when I fixed a bug, but did not write a test for it just because of the huge overhead involved. In the next iteration, when changes were made to update the user interface of the application, the bug was reintroduced.
- Design with scalability in mind, especially when the number of users using the product is expected to increase, as with a web application. Once the load exceeds a threshold, rewrite of the product should not be the only option.
- Do not couple the user interface with the business logic. It should be relatively easy to move to a new user interface without spending an inordinate amount of time tailoring the back end to support the new UI.
- Once a solid foundation has been laid, good coding practices build upon the foundation. Some of what I have found useful are:
- Choose good names for classes, methods, and other entities upfront, and spell check them before the names proliferate and it becomes more difficult to correct them.
- Make code simple, readable, and self-documenting. For example, a method should do exactly what it says it does. Recently, while maintaining code, I came across a method that was named ‘getWidth()’ although it actually returned the number of columns.
- Add comments about why something is done when code is not self explanatory. It helps me when I go back to code that I myself have written in the past.
- Automate unit tests, and run them often – ideally with every check-in using continuous integration.
- Automate acceptance tests if possible, and run them nightly.
- Do not pass references of objects around unless they are read-only. Make copies to use locally, or modifications made locally can have unforeseen side effects.
- Do not modify existing interfaces, especially if clients have already been deployed. Extend them for new functionality.
- Prevent people from doing undesirable things using code and scripts instead of word-of-mouth, i.e., automate things where possible.
- Give people a chance to back out of irreversible operations using confirmation dialogs.
- Besides all these are the object oriented principles of cohesion and coupling – making each class do only one thing very well, and avoiding unnecessary coupling between classes.
That’s all my brain comes up with at the moment. Comments are welcome, and hopefully I will be able to contribute more in my later posts.