Published July 22, 2022
by Doug Klugh
Making Good Software Better
Evolve your software through iterative development with continuous improvements to its internal design. This will help preserve the structure and integrity of the code, while progressively gaining flexibility, maturity and viability. Be sure you have a good Safety Net of tests so you can refactor the code without fear of breaking it. Refactoring should be used to add structure (where structure is lacking) prior to extending functionality. As with other Agile methods, refactoring is best employed with continuous integration to minimize merge conflicts.
Building flexibility into your software enables your team to easily adapt to changing requirements. However, you should avoid building constructs in anticipation of future changes. Instead of speculating as to what changes may come, build flexibility for the changes you know will be required. Adopt YAGNI to avoid over-engineering and maintain a Simple Design. As you better understand the needs of your users, employ refactoring to adapt your architecture to best support those new requirements.
Build software that is flexible, reliable, and easy to understand. In other words, build software that is simple. Do not over-engineer the initial design (YAGNI). Start with the simplest solution, then work towards complexity (only as warranted). Ensure your code passes all of its tests, reveals its intent, contains no duplication, and is as small as possible. Writing simple code can often be difficult. It is not about writing code by the simplest means. Simple != easy.
Build comprehensive test suites so you can refactor code without fear of breaking it. This safety net enables you to work more quickly and adopt a more experimental style of changing software. It will give you the confidence to improve existing code and discover immediately if something breaks in the process. Missing tests are like holes in your safety net and bad assertions equate to broken strands. Building mature test suites will increase both agility and speed to market.
Keep your functions small by extracting code fragments into their own functions. If you have to spend even a small amount of time figuring out what a code fragment does, then it should be extracted into its own function and given a name that describes what it does. Then when you read it again, the purpose of the code should be obvious. This will clarify the intention of your code (separate from the implementation), while making it self-documenting, more maintainable, and more testable.
Use local variables to name complex expressions (or portions of them) to help decompose those expressions into more manageable pieces of logic and provide clarity around the purpose of that code. These simple abstractions help to simplify those expressions, making them easier to read and easier to understand — not to mention how easy it is to provide a hook for debugging.
Refactor code before optimizing to make your software more adaptable to performance tuning. Building software that is well-factored without attention to performance will produce finer granularity for performance analysis — providing effective identification of performance hot spots. Even when refactoring impacts performance, you can apply more effective performance-tuning enhancements with well-factored code. First, write clean code that facilitates performance tuning, then optimize for time and footprint.
Refactor your code by grouping related statements together. This will promote Readability while setting you up for the next refactor — usually Extract Function. It is important to examine the code you are sliding and the code you are sliding over to ensure they do not interfere with each other in a way that would change the observable behavior of the system. As always, ensure reliable test coverage prior to refactoring and rerun tests often.
When a loop is doing multiple things, split it into multiple loops with each doing just one thing. If you need to modify one of the loops, this will ensure that you need to understand only the behavior that you need to modify. This also sets you up for the next refactor — usually Slide Statements or Extract Function. While this forces iterations of multiple loops, it is important to first Refactor, Then Optimize. If the loops turn out to be a performance bottleneck (which is rarely the case), it is easy enough to merge them back together.
As software is developed by making behavioral and structural changes, do not attempt to do both at the same time. Wear one hat to add new capabilities (including tests) without changing existing code, then wear the other hat to restructure the code without changing behavior. Build software by swapping hats frequently to develop and test very small capabilities, then refactor to improve the design and the overall quality of the code.