In today’s world, software tends to live on and on with many features added over the course of its lifetime. To deal with perpetual software, software engineers must write code that is maintainable. Maintainable code has two properties we care about: readability, the ease of understanding code, and extensibility, the ease of extending code later. As software developers, we want to keep our code following these properties so we can deliver features to our customers faster and make our lives simpler in the long-run.
A quick note about performance. Often programmers worry about trading readability or extensibility for performance. I recommend prioritizing readability and extensibility over performance 99% of the time. I have another post for those curious on when to consider performance.
The most crucial goal for maintainable code is to keep it readable. Why is readability so important? It boils down to being able to find what you are looking for when you need to. Let’s say there’s a bug in your system that millions of people are relying on and developers must fix it ASAP. If the code is unreadable, you will spend too much time trying to find out what the problem is, even before you can fix it. The delay harms your customer’s experience because they are relying on you to fix your bug quickly.
Readable code is code that people can understand just from looking at it. You should think of future programmers maintaining or extending your code as your customers, too. We want to make it easy for future programmers (which likely includes you) to understand our code so that they can fix bugs and add new features with ease. When we understand that people prefer skimming over reading and that they don’t look for optimal solutions, it gives us a reason and a way to make readable code.
What makes code readable:
- Great, descriptive naming
- Splitting code into smaller functions with great names
- Logical organization of functions and classes
- Following global conventions (e.g., using design patterns by name)
- Have consistent style and structure throughout your code base
- Avoiding comments (use descriptive naming instead)
- Separation of Concerns principle
- Avoiding implementation inheritance (often it’s hard to find and understand base class methods)
Requirements change frequently. It could be the customer who changes the requirements. In game development, requirements often change after testing your game and finding pieces that aren’t very fun. It could be that we just want to add new requirements later to launch a new feature for our customers.
Thus, our second goal for maintainable code is to make code resistant to change. This is called extensibility. Extensible code will allow you to add or remove features quickly without introducing bugs.
In a perfect extensible world, we’d be able to add new features without changing any code that already exists. If you don’t have to change old code, there’s no way you can introduce bugs into that old code. In practice, this is hard to achieve, but it’s still the goal to shoot for in all your units of code.
What helps code be extensible:
- Open-closed principle – Open for extension, closed for modification. Make units of code (classes, functions) such that they never have to be touched again.
- Separation of Concerns principle – keep code cohesive with a single responsibility
- Keeping code loosely coupled
- Minimizing redundancy
- Using unit tests to find code smells
- Using polymorphism
- Favoring composition over inheritance
- Use dependency injection to separate use from creation
Balancing Readability and Extensibility
The most extensible solutions can often be unreadable. For example, if all you need to do is encrypt a string, and you don’t anticipate future changes that will require more types of string manipulation, it would just confuse people to have a complicated StringDecorator interface. You could just have a function that’s called encryptString. It would be much easier to read and quicker to implement. You can always refactor later if you need more functionality.
How can we make code that is readable and as extensible as it needs to be?
- KISS – Keep It Simple, Stupid – Your code should only be as complicated as it needs to be
- Descriptive naming
- Make sure your team understands the tools you are using for extensibility (e.g., design patterns, language features like java lambdas)
- Avoid implementation inheritance. It kills both of these goals. Composition is preferred.
Some design patterns are simple and increase extensibility dramatically without readability degradation. Using the strategy pattern is a great way to keep your code extensible with minimal readability loss. In the above example, you can have a StringEncryptionStrategy, and leave your code open for changing which encryption algorithm you are using. The code will still be readable because you’ll know you’re encrypting a string. The only readability you might be losing is which encryption strategy you are using in the moment, but often you won’t need to know that information while looking at the other class (it’s really in the wrong abstraction layer).
In the modern day of software development where old software is continuously changing, I suggest prioritizing in this way:
- Make it work
- Make it readable
- Make it extensible
- Make it performant