Why consistency is one of the top indicators of good code
Software engineering is a lot easier in consistent systems.
Consistency is arguably my favorite principle in software engineering.
Far outweighing any other principle as an indicator of maintainability, I’ve experienced that systems built in a consistent manner are more maintainable than systems built inconsistently.
Consistency is defined as: sameness, conformity, uniformity.
Unwavering consistency within a system leads to code where you can make more assumptions and predictions about its behavior and implementation that end up being accurate. On larger, older systems where you can’t fit the codebase in your head, these guesses are critical tools developers use to feel their way around the system.
You increase the number and frequency of correct assumptions by making your code consistent, and these correct assumptions (and verifications) lead to the many actionable steps you take during development.
The code ends up becoming reliable, avoiding surprises.
Ultimately, the thinking around a system that is consistent becomes a matter of intuition instead of pre-knowledge, leading to much more rapid and smoother development.
As a happy side-effect, consistent code is easier to meta-program, automate, refactor, and test.
When improvement is not an improvement
If you interact with me regularly you’ll be guaranteed to have heard this phrase:
Consistently bad is better than inconsistently good.
It’s a polarizing phrase, to be sure. People will ask “What’s wrong with improving things — isn’t it good to continuously improve? Should we continue doing things poorly just because it is consistent?”
Time heals all wounds, except in software
The problem with inconsistently good comes from the unavoidable fact that time destroys software. The longer software exists, and the more hands that work on it, the more chaos and entropy set in.
You can try to make improvements to combat the rot, and in the mid-term and short-term it may improve a slice or two of functionality, but, unless carried out completely and thoughtfully, the improvements ultimately lead to a problem known as the Lava Layer.
Imagine a system initially developed with Technique A. A new software developer joins, and starts using technique B, which is objectively better than Technique A. Years later, that software developer is replaced by another developer, who uses Technique C, an arguably better technique than either Technique A or Technique B. Let this repeat.
What you end up with is a software system with X number of ways of doing the same thing. You use up precious brain-space to accommodate the X different methods of doing things, and you can never really be sure which way you’ll be encountering in your codebase.
Although beneficial when taken individually, the improvements as a group introduced inconsistency which made the system harder to reason about over time.
Avoiding the lava-flow
I’m not saying to never improve: I’m all about continuous improvement. What I am saying is to improve smartly.
Before making a technique or tool change, do what a good senior engineer would do!
Weigh the tradeoffs
Even if a new approach is objectively better, weigh the tradeoffs with the lack of consistency it will introduce. I generally look for an order of magnitude improvement or benefit in some aspect before even considering switching foundational tools or techniques.
Ensure complete adoption, when possible
If you do use a new approach, ensure that you completely swap out the old approach. This may not always be possible, but do this when you can. You’ll avoid the problem of lava-layers altogether and preserve consistency.
Fake the complete adoption, when needed
In some cases, you can pretend that everything got switched over to the new approach but still keep the old approach under-the-hood. You can leverage design patterns like the Adapter Pattern to hide the old implementation with the new approach.
Ensure clear lines of separation
I recognize it is the real world — it’s not always possible to swap things out completely. In these cases, ensure that there are very clear lines of separation between where the new approach is being used and the old approach remains. Preserve consistency within software verticals or modules if you can’t preserve it across the system as a whole.
Consistency in practice
Consistency is a broad term for any action that results in fewer ways to do any given thing in a system. Consistency can be achieved at the individual line level or even in the process level that lives outside of the code.
Stick to a style-guide
They say code should look like it was written by a single individual.
Establishing, following, and enforcing a style-guide can go a long way towards removing the inconsistencies that arise out of individual ways of doing things. There are plenty of good community ones out there for the major languages and frameworks that you can use as a foundation and modify as needed. Evaluate the rules with care and change them where you need to, but don’t let disagreement over a rule or two prevent you from holding your code to a standard.
Individuality and personality quirks simply don’t belong in a codebase, and a style-guide can go a long way towards making everything consistent.
Get your naming down
They say there are only two hard things in programming: cache invalidation and naming things.
Naming things well can make a massive difference in understanding any given piece of code. It imparts meaning and intent, and the best names provide context and describe a tremendous amount of knowledge at a mere glance.
Ensure you follow consistent naming practices, being sure to name things that are the same the same, and name things that are different different to aid discoverability.
For example, one thing I see in many poorly written systems is a plethora of different variable names for counts. Naming like
num_transactions could potentially co-exist chaotically in the system alongside the likes of
transaction_total (an amount),
count_transaction (marks a transaction with a flag), and
transaction_num (unique identifier of the transaction).
Without consistency and care in naming, you never quite know which one you should be reaching for, or whether the naming differences indicate differences in behavior and intent.
Write code for the human, not for the computer.
Have one way of doing something
Whether it means uploading images, setting up routes, or iterating over a collection, try to establish The One True Way of doing something.
All other approaches should be viewed with scrutiny and suspicion by carefully balancing the tradeoff between the benefits they bring and the inconsistencies they introduce.
By following this approach, you end up with a system that has consistent behavior and mechanisms. The consistency introduced by a single approach allow you to massage out frameworks and libraries from your codebase, further improving the maintainability by isolating the “how” from the “what” through modular, testable components.
The consistency also ensures that you aren’t wasting your time having to apply the same change to multiple places or deal with different bugs for the same kind of functionality — keep things DRY.
Make things match
Code rots one line at a time, and many tiny negative changes sneakily add up over the years. Any one change might seem tiny and inconsequential in isolation, but taken as a group you’ll find yourself shaking your head wondering how it got so bad.
Avoid the pain. Ensure tiny inconsistencies aren’t given the potential to build up over time by maintaining high standards.
Get the small stuff right. Make sure file names match their contents. Make sure CSS classes are written in the same style (eg. modifiers, BEM, SMACSS, etc.). Pick a conventions like camelCase vs. snake-cased and stick with them everywhere. Alphabetize, group, or order properties appropriately. Take care of whitespace.
Taking care of the small steps leads is a large step towards maintainable code.
Consistency isn’t the only principle that matters, but it is a high-impact one to a project’s long-term maintainability.