Most software developers understand the relationship between complexity and cost; more complex = more expensive. Unfortunately many equate code complexity with lines of code, slavishly following design patterns that reduce code counts while actually increasing complexity.
For years, engineers have noted that the overall cost of a manufactured device is roughly proportional to the number of parts it has. A whole discipline, known as Value Engineering, is dedicated to assigning a cost to each function of a product so that designers and manufacturers can can make wise choices about which functions to improve and which to throw away. A classic value engineering excercise is to take a common item like a circuit breaker, tear it apart, and then redesign it with less pieces. Removing one part from the design can yield dramatic cost savings and big improvements in reliability.
Figure 1 – Cost and complexity for two competing solutions
In Figure 1, two companies are designing competing products with similar market requirements. The company that solves the problem with the least number of parts (Team A) is the big winner. Note that cost often increases exponentially with the complexity of a particular solution.
In many respects, software engineering bears little resemblance to other engineering disciplines, but in this case, there are real parallels. Just as in circuit breakers, more parts means more $, both in initial cost and ongoing maintenance and operations.
The analogy is useful because code reduction techniques often increase the number of virtual “parts” while decreasing lines of code. My favorite example is indirection. Many popular design patterns aim to reduce code duplication by introducing levels of indirection (for example, adapter or dependency injection patterns). If you think of each indirection as a new part, it is easy to see how some designs can have less code yet more parts. I use adapters and injection all of the time, but I also acknowledge that they complicate the design, development, testing and maintenance of the code.
Let’s say you are adding a new feature to an existing project and there are some similarities to other parts of your code. You must decide whether to build a new common module and refactor your existing code to use it, or ignore the existing code and build the new piece without regard to the existing stuff. Many developers, especially dogmatic adherents to agile development methodologies, would blindly choose the former approach, without regard to the costs involved. A better strategy is to choose based on a reasoned trade off between cost and benefit. And there are many hidden costs to certain complex design patterns:
- Clarity. Some code is just too hard to understand. For example, xml based configuration files can make your code easy to configure and impossible to understand.
- Unit Testing complexity. Multiple levels of indirection require multiple levels of testing. This usually means more support classes, including utility dao’s, mocks, etc.
- Debugging time. You would think that designers would avoid any architecture that hinders efficient debugging, but many architecture decisions are made without even considering the effect on debugging and deployment.
- Operational costs. If it is hard to understand the code, then it will almost always be hard to keep running.
Too Many Notes
In the play, Amadeus, the Austrian King tells Wolfgang Amadeus Mozart that his opera is too complicated, it has too many notes, and he should “take some away.” Mozart, who feels that his opera is perfect, asks the king which notes he would like to take away. It is a moment full of meaning for any creative person. Unfortunately, many creative software developers empathize too much with Mozart, favoring the ornamentation and flash of 18th century classical music. I believe we should all take the tone deaf King’s advice and take some away.