Bad code can be the result of any of the following:
- Inconsistent coding styles
- Steadily increasing technical debt
- Poor software architecture and design
- Obsolete development methods that do not leverage Test-Driven Development, code review, and unit testing
- Lack of coding skills and little drive for operational excellence
- Deficient process governance
- Legacy code with poor documentation and lack of familiarity with the product
Naturally, it is best if all your codebase is clean and tidy, but the reality is messy, and we must live with it. So how do we reconcile a messy reality with a desire to keep the product tidy and orderly?
In his seminal work on the topic, Adam Tornhill argues that not all technical debt needs to be eliminated, especially when it does not make sense financially. We can say the same for low-quality code.
His analysis consistently showed that only a tiny fraction of files in a code base are touched by new features or bug fixes (changes follow a Pareto distribution). It is generally sufficient to handle files forming such bottlenecks only (through what he refers to as Behavioral Analysis).
So, where exactly do we draw the line on quality and are there any simple actions we can take to keep low-quality code under control?
This article will attempt to answer these questions in some detail. The discussions will revolve around the following ideas:
- What is bad code, and what is good code — Under this section, we look at software design, productivity, testability, and extensibility.
- How to write clean code — here we investigate code readability, cognitive complexity, and coding style.
- Long-term implications of poor code — this discussion will revolve around increasing and uncontrolled technical debt and how it can turn a product from an asset into a liability.
2. Table of Contents
- 1. Overview
- 2. Table of Contents
- 2. State of Software Development
- 3. Quality Code
- 4. Consequences of Bad Code
- 5. Guide to Maintaining Quality Code
- 6. Final Words
- 7. Further Reading
- 8. Featured Articles
2. State of Software Development
Software development has changed a lot since it kicked off in the 1950s and has expanded exponentially and in every direction.
The technological expansion of software has been so fast that coding standards and best practices have trouble keeping up.
Bob Martin recounts an interesting story in many of his talks. It goes like this: The number of software developers worldwide doubles every five years, so looking at a random sample of software developers, chances are that half the population will have less than five years of experience in development!
This fact is remarkable as five years is just enough time for software developers to acquire the necessary skills, especially since they will also move up the corporate ladder sooner or later. These two facts make it harder to find experienced developers.
Under these conditions, users today are happy to have applications that run just about right instead of perfectly all right.
3. Quality Code
This section will look at three criteria for assessing code quality: productivity, extensibility, and testability.
Productivity refers to how easy or difficult it is to maintain the current code. Two factors primarily impact productivity as far as coding is concerned: readability and accessibility.
Productivity in this context focuses on the analysis and coding stages (not the entire software delivery chain), measuring how easily a developer can read and interpret a piece of code.
Code is like humour. When you have to explain it, it’s bad.
Short and explicit statements are easier to believe than those that are hard to read. This phenomenon is called cognitive ease and is one of many cognitive biases our minds possess.
Should we deliberately strain our cognitive faculties by writing hard-to-read code so that developers will spend more effort making sense of it? I believe that would be counterproductive.
A better approach, in my view, is to make code more legible so that developers can focus their thoughts on understanding and solving the problem.
But how do you make code more legible? Some people like to see the whole function on a single page; others would like to see enough white spaces and an aesthetically pleasing structure.
Most people would agree on consistency in style, which is where style-checking becomes an essential aspect of your development processes, and, therefore, must be included in your IDE or continuous integration pipeline.
Two factors influence accessibility: cognitive complexity and language-specific skills.
Cognitive complexity is a mathematical measure created by G. Ann Campbell and used in SonarQube to detect complex code. This measure scores a piece of code based on criteria that would make it either hard or easy to interpret. Examples of these criteria are:
- Breaking of the linear flow
- Operator sequence
Cognitive complexity addresses many of the shortcomings of its predecessor, cyclomatic complexity.
As for the second factor, language-specific skills, recruiting developers with expertise in the language immediately augments accessibility.
One of the principles of Operational Excellence is using mature and proven technology where experienced developers can be quickly recruited, and online material easily found.
Of the many mature technologies available today, some are low-level while others are high-level. Low-level programming languages require more expertise and lines of code per feature to achieve the same result.
One of Google’s early engineering decisions is “Python where you can, C++ where you must“, and a very wise one indeed.
The ultimate objective of developing software is to create business value for your customers. The latter may not care which programming language is used or how fast it runs, as long as it solves their business needs cost-efficiently.
You also want to acquire a winning value proposition. There is little point in creating a perfect product that never makes it to your customers.
Extensibility measures how easy it is to extend the functionality in a piece of code.
Two elements govern the ease with which software code is extensible: good software architecture and low technical debt.
3.2.1 Software Design Patterns and Architecture
Software design patterns fall into four categories: structural, creational, behavioural, and concurrency, and serve as textbook answers to known problems.
For example, if you want to implement parcel tracking software, where packets move between well-defined states (like Ordered, Shipping, Delivered), you would use a state machine pattern.
The subtle relationship between design patterns and software architecture can only be understood by looking at it from an evolutionary perspective. The below ideas illustrate the point:
- Software design patterns are universal, and every seasoned developer must be familiar with them, making the code more accessible. They are designed in a way that allows functionality to be naturally extended.
- Software architecture can never be completely defined in a top-to-bottom manner unless all the business requirements are known at design time. This is hardly ever the case, and users only know what they want after seeing what the technology can offer. Therefore, software architecture emerges as new code is added, and design patterns are perfect for introducing orderliness into the programming process.
- Appropriately implemented, software design patterns are crafted using programming best practices, like the SOLID principles. They provide developers with flexible boundaries within which good architecture can emerge.
3.2.2 Low Technical Debt
Technical debt is the amount of work you leave behind when you sacrifice software quality for faster delivery.
Sometimes, these sacrifices are justifiable, making technical debt a problem you must manage and control rather than eliminate.
Technical debt can be kept under control by refactoring, a process devised by Martin Fowler in 2000 with contributions from notable experts like Kent Beck:
Refactoring is a controlled technique for improving the design of an existing code base. Its essence is applying a series of small behavior-preserving transformations […] the cumulative effect of each of these transformations is quite significant. By doing them in small steps you reduce the risk of introducing errors. […] which allows you to gradually refactor a system over an extended period of time.
A prerequisite to refactoring large pieces of code is a test suite that provides you with enough confidence to perform the refactoring exercise.
Technical debt beyond a certain threshold would become too expensive, disruptive, and challenging to manage. This is when you get legacy code. It is much easier and far less risky to refactor code constantly.
Testability is a measure of how easy it is to test new code.
Many factors can influence testability, but our primary focus is on code-specific ones:
- Methods that do too much or have a high degree of cognitive complexity cannot be easily unit tested
- Not enough modularity so that components can be isolated and any dependencies accounted for. In this case, considerable mocking is required, raising the product’s cost of ownership and the cost of change.
- The new code is buried behind layers of other code and is not accessible externally through APIs, for example. While this is not necessarily a faulty design, it makes any new code challenging to test.
These issues are less prominent if you use Test-Driven Development or code with automation in mind. Both approaches will push developers to write code that can be easily tested.
Code just developed must be ready to be tested; otherwise, if you leave it till later, chances are it will accumulate and eventually get dropped. Quality code should come with a complete suite of test cases.
4. Consequences of Bad Code
Recognizing the long-term impact of poor quality code on the product and the business is vital. Equally important is recognizing which factors play a role in the assessment and which do not.
4.1 High Cost of Ownership
When discussing Waterfall and large software project management, Winston Royce rightly stipulates that customers are happy to pay only for analysis and coding as this is where business value is created. Everything else, from unit testing to documentation, deployment, and bug fixing, are costs to be avoided.
Businesses must implement Agile processes and develop quality code to maintain a low cost of ownership.
4.2 Decreasing Value Proposition
The margins between competing firms are usually not wide enough to allow for slacking regarding value proposition, and the latter must be demonstrable.
Flawless deliveries enhance your firm’s market reputation and bring about more business. You can have the best people and processes. Still, if your solution requires more time and budget to implement because of obsolete technology or legacy code, you will soon find yourself unable to compete.
4.3 From Asset to Liability
Naturally, some products in a company portfolio will have a poorer performance than others, but this is not the same as when a product turns from an asset to a liability.
Technical debt and poor quality can slowly erode your software delivery pace, making projects less profitable.
Poor-performing products cannot be quickly abandoned without an appropriate replacement strategy. This might leave the clients in a difficult position and open the door for competitors to place a foot in the door.
5. Guide to Maintaining Quality Code
In this section, we will address two broad topics: first, maintaining quality code, and second, some examples of code styling for keeping your code up to scratch.
5.1 Investing in Design
5.1.1 What it means
- The first sacrifice is design and documentation when software teams are pressed for time. But that is not ideal. Regardless of the deadlines, guarantee a minimum level of design and documentation; these things have a notorious way of biting back!
- Proper design by experienced architects eliminates the potential of introducing redundant code, unusable features, or unreliable solutions.
- Design investments become more important when projects are more significant or new changes have a system-wide impact.
- Investing in design keeps the product foundations rock-solid while preventing quick-and-dirty fixes, short-term hacks, subpar implementations, and unreliable solutions from creeping into the codebase.
5.1.2 How to Do It
- Invest in a solution design document
- Follow the 70-30 rule for design/development effort
- Have the design reviewed by architects and developers for feedback and buy-in
- Allocate a portion of resources for Research and Development (R&D)
- Commit to one major upgrade every 1-2 years
- Encourage constant refactoring of old code
5.2 Diligent Code Review
5.2.1 What it means
- Code review ranked number 1 in a survey on maintaining quality code.
- Its main objective is to review code against internally published guidelines and industry best practices.
- Software teams today are actively using project tracking tools like JIRA. These usually offer built-in process flows that allow the Code Review set up as a step within the ticket lifecycle.
- Code styling is an important area to review. A Code Style comprises an extensive set of rules governing the writing style (commenting, naming, indentation, spacing, vertical alignment, indentation, usage of global variables or static functions, and so on). Having an internally published Code Style to help keep the code consistent across the board.
5.1.2 How to Do It
- Make sure code review is an integral part of your SDLC
- Make efficient use of enterprise collaboration tools such as JIRA for implementing proper code review processes
- Publish code styling rules and guidelines (Google’s style guides include various topics and languages. A great place for inspiration).
- Train new staff on the product and industry best practices
- Make sure code reviews happen against a well-described and documented design.
5.3 Testing and Automation
5.3.1 What it means
- Regression test suites with decent coverage make constant refactoring of old code a practical exercise by eliminating any fears of breaking existing functionality.
- Test automation compounds the advantage of regression test suites by providing a mechanism for shorter and more reliable feedback.
- Automated software testing is, in my view, essential for Agile practices.
- When writing new code, make sure to add the necessary test cases. Do not commit code that does not have unit tests.
5.3.2 How to Do It
- Publish a test strategy
- Develop a test automation framework that is suitable for your team and product
- Embrace some form of Test-Driven Development (TDD)
5.4 Sound Architecture
5.4.1 What it means
See the previous section on software architecture to understand its impact on quality code.
5.4.2 How to Do It
- Choose standard, time-tested architecture and design patterns suitable for your product requirements.
- Encourage collaboration between architects and developers when designing new functionality or modifying architecture.
- Use Other People’s Experience (or OPE) when implementing solutions for existing problems.
- Hire architects with the required skill sets and implement the necessary processes to oversee significant design decisions.
- Make sure new functionality is in line with the overall software architecture of the product.
- Constant refactoring and investment in R&D help keep the product up-to-date with the latest technological standards.
5.5 Publish a Styling Guide
5.5.1 What it means
- Publish an internal Styling Guide that everyone can read and follow
- Make sure its part of the code review process
- Code Styling should be geared for maximum readability
5.5.2 How to Do It
- Get inspiration from online content (Google Styling Guide is a great example)
- Have a look at the below subsections for some practical examples which we found useful
6. Final Words
Producing quality code should never be a spurious exercise left to the staff’s discretion, and it is best viewed as a collective effort enforced through appropriate internal policies and processes.
The quality of a codebase has far-reaching consequences on the business. Bugs are just the tip of the iceberg, while performance issues, huge backlogs, costlier projects, poor customer experience, and a suffering reputation follow shortly.
Software development companies are increasingly pressured to produce better products in shorter and shorter timeframes.
7. Further Reading
- An awesome article on modular design can be found here.
- Great content from Bob Martin on clean code.
- Another set of great videos that will help you start with design patterns