How does bad code slow us down?
Maybe you’ve come across a lot of people talking about how we should write clean code, how code should be easy to maintain, or how we must repay our technical debt and such. The reason for why we should strive for maintainable code is because we want to keep a steady velocity through the whole lifetime of the project. But, how exactly does bad code slow us down? It is important to not just blindly follow what anyone says without actually knowing why we do what we do. I am going to talk about a few factors which lots of people commonly talk about – and rightfully so, because those factors does address the core problem with bad code.
I am going to discuss the following attributes (as popularized by Robert Martin, aka Uncle Bob) of our code, one by one:
- Rigidity
- Fragility
- Readability
- Reusability
- Needless complexity
- Flexibility
Rigidity: resistance to change
Rigidity describes the degree of resistance to change in our code. Have you ever tried to make a small change in some code only to have the compiler complaining about some other code that is no longer working? I bet you have. If you have read my post about technical debt, you know that we have to constantly refactor our design as new requirements appear. And if we can’t do this easily, we will spend much more time changing our code than actually producing anything of value.
Rigidity usually increases when implementation details of some module or class leaks into client code, which is something I have discussed as well. With other words, as code gets more tightly coupled, it gets more rigid. You’ve probably heard of the idea of “loose coupling, high cohesion”; well, there’s a reason for that. Code gets harder to change the tighter the coupling is between classes or modules.
Fragility: the easiness of introducing defects
Fragility describes the risk of breaking something as you change code. This attribute is closely related to rigidity, but the difference is that the compiler won’t necessarily tell you some code no longer works. Instead, you might unknowingly introduce bugs as you change your code. And they may not be noticed until much later; it may be noticed after release, which is something we’d like to avoid.
Fragile code may appear, just like rigidity, due to tight coupling. Not only that, however, fragile code appears when responsibilities are tightly coupled as well. Think of classes, modules or procedures that does more than one thing. Maybe you notice something that appears to be duplicate code in either of those, but in reality, those two (or several) duplicated code segments may be used for entirely different purposes. You decide to merge those, and later someone comes along and changes this code in order to fulfill some new requirement that affects one of the responsibilities. As this code changes, the other part of the code may no longer function as expected. You may have heard of the principles “Single Responsibility Principle” and “Separation of Concerns”, those ideas address this problem. And they deserve a blog post of their own.
Readability: the easiness of understanding code
Readability is not something we should underestimate. We read code much more than we write it. Whenever we need to change something, such as fixing a bug or adding a new feature, we need to understand the surrounding code. Not only that, we need to know where to change things. And readable code doesn’t only consist of well-named things (don’t get me wrong, this is important as well), it also has to do with the design itself. How easy is it to see how things fit together in the big picture? What responsibilities do each module or class have? How do they interact with each other? Those are bits of information we need to know in order to know where to apply changes. We may read the bug report which has been written in some domain language perhaps, while our code doesn’t use the same language at all. Instead, the code may be using technical terms heavily, such as “ContextWidgetVisitor” and “XYZComponentObserver” and whatnot. This is problematic, because it makes it much more difficult for us to even know where to change the code. We want the code to guide us towards the place we might be interested in changing.
Our code should be readable. It should communicate not only how it does something, but also what it does. It should name things by using a domain language that you and your colleagues use when verbally discussing features and problems.
Unreadable code can impose high costs, and since we read code a lot more than we write it, why not spend some extra time making the code readable? It is certainly worth it. Don’t go about overanalyzing when naming things though. Having trouble assigning a concise and describing name to a class, for example, may be an indication that the class’s responsibility isn’t defined well enough; it may have too many responsibilities (or even be unnecessary in some cases).
Reusability: the degree at which we can reuse existing code to fulfill new requirements
If there is something that can save us tons of time, it is reusable pieces of code. If we are about to make a change to our software, and we notice some logic in some other part of the code we’d like to use, we use it. Unless this particular logic is intertwined with lots of other code that isn’t interesting to us, at all. Usually this happens when you mix responsibilities together in classes or procedures, for example. At this point, what will usually happen is that we’ll simply copy all or parts of this code and paste it at some place we’ve deemed to be adequate. However, the problem is that this may spawn duplicate code. And we all know what duplicate code means: if one of those duplicated code segments change, so must the other code segments change. How will you know where those duplicated code segments are? How can you ensure anyone else knows about them and changes them accordingly?
If responsibilities are heavily coupled together in code, the reusability of those parts of the code will decrease while increasing the risk of duplicate code entering our codebase. Follow the single responsibility principle. (Unless you have deemed it to be more of a benefit to not follow it in some case, of course. That should be considered an exception to the rule, though.)
Needless complexity: unnecessary structures in code
Overengineering things can be just as bad as not caring about the maintainability of the code at all. It reminds me of a certain quote by some real wise guy, “The road to hell is paved with good intentions.”
Just like technical debt, complexity should be kept at a reasonable level. Adding too much complexity means decreasing the readability, and possibly increasing the rigidity, while not really providing any value in return. There is only so much things we can have in our head at the same time when we read and try to understand code. If you blindly slap design patterns all over the place, along with some pretty abstract classes (or interfaces in Java or C#), the code will quickly become horrible to work with. And worst of all, there’s no return on investment for those complexities. Only design for what you need right now.
As I said earlier, unnecessarily complex code has a tendency to become rigid as well. We shouldn’t design for longevity, but rather for change.
Flexibility: the easiness of changing the design
Last, but certainly not least, is flexibility. This attribute describes how easy it is for us to change the design in order to fulfill new requirements, while not making quick hacks (i.e. letting the code rot) that reduces the maintainability of our code. Sure, sometimes we choose to go this way, but it should be a conscious decision. And there has to be some benefit from it. And.. it should be refactored eventually. Code rot in this case is basically what you call the interest on technical debt, which I also discussed in my post about technical debt.
If your code isn’t flexible, it will get much harder to refactor it, meaning that it’ll be hard to pay back the technical debt.
Summary
I think those attributes address the issue of bad code very well. When making design decisions, you should consider thinking about the design in terms of those attributes, because those attributes describe how unmaintainable code slows us down. Don’t religiously follow certain design principles and such, although they are important as well since they address how code might affect the attributes we’ve discussed. They should be used as a guideline when making design decisions; the hard part is knowing how those problems we’ve discussed appear.