In an iterative software development process, you work on designing and developing a solution in repeated cycles. At the end of each iteration, working code is demonstrated and is subject of potential change requests.
In addition, improved and more elaborated business definitions become available each iteration. There are many reasons for that: business requirements may change, new stakeholders may get involved, new laws and regulations to observe, organizational changes, improvements on the current understanding of requirements, and so on.
The purpose of working iteratively is to have the flexibility for those changes. Consequently, the source code that you create today is likely to change in future iterations, either to extend it for new features or adjust some current ones.
If this dynamic process is disregarded during the design phase, source code ends up not being as readable and maintainable as it could and should be. That creates a huge obstacle for the development team in achieving steady and continuously improved performance, compromising the amount and quality of delivered value on each iteration.
Decisions and goals
As a developer, you’re making decisions pretty much all the time —either small decisions related to some piece of code, or bigger ones that allow you to identify proper design alternatives and select the right one for the problem you’re solving.
Those decisions have a direct impact on the simplicity and cleanliness of the source code and therefore, on its maintainability properties. What makes this job so thrilling is that there’s not a single source of truth that reveals what the available decisions are, or which is the best option to take each time. So, how do we optimize the ultimate result by making the right decisions and selecting the best option every time?
Of course, there is no answer. Building software is a complex job.
A good way to start is by having in your pocket some of the following important principles and practices for software design and development. Then, depending on the problem and the context, you’ll be able to cherry pick those that best fit your goals at any given moment.
In this article, I’m assuming that building clean and maintainable code is amongst your goals.
‘Keeping it simple’ is not always a simple thing to do
The single-most important programming principle there is, I think, is simplicity. Systems work best when they’re kept simple. In contrast, complexity makes the code base harder to understand and to maintain.
Failing to build simple code is sometimes due to over-engineering. When a developer tries to foresee extensibility points or features that the solution might require (but which it probably won’t), the source code ends up getting unnecessarily complex. Too many components and layers that won’t actually be needed have now become part of the source code, making it difficult to understand and harder to change. This becomes a burden for maintainability and should be avoided.
Another cause for failing to achieve simple code is, conversely, under-engineering. This is a situation in which a developer writes some code and either totally ignores basic programming principles and practices or isn’t aware of them. The actual problem is that the code is pushed to the product’s repository as soon as it does the job, without a proper code review. This leads to the worst possible kind of maintainability burden a solution can have: buggy, hard to understand, not testable, inconsistent and poorly designed code.
Simple does not necessarily mean easy to achieve. Achieving simplicity takes judgment and evaluation skills to apply the right programming principle every time.
Having the following principles in your toolbox is the first step to writing simple code.
You aren’t gonna need it
“You aren’t gonna need it” (YAGNI) establishes that you should only deliver what’s strictly needed. Sometimes, in an attempt to anticipate future necessities, a developer feels the urge to write some extra code. What ends up happening, most of the times, is that extra piece of code is not needed at all, resulting in higher costs for your codebase (development, drag and maintenance of a non-required feature).
Likewise, it’s important to avoid premature optimization. Optimization is good at the right time. Doing it when definitions are not mature enough and you still don’t have the full picture of the solution, is going to slow you down.
S.O.L.I.D. design principles
“S.O.L.I.D.”: Consists of the five most fundamental principles for object-oriented design (OOD). They were first described by Robert C. Martin (popularly known as Uncle Bob).
By following S.O.L.I.D. principles, your solution gains massive maintainability, since it results in loosely coupled, highly cohesive, testable and extensible source code. Also, it significantly increases the isolation of future changes, which reduces the impact on existing source code.
Writing Clean Code
“Clean Code: A Handbook of Agile Software Craftsmanship” (by Uncle Bob) promotes a set of rules and standards to follow for the source code to be readable, maintainable and extensible. This book is probably one of the most fundamental ones that every software professional must read. I think that recommending this book (just like Natalia did for this and other great books on this post) is mandatory for helping professionals write simple and maintainable code.
In addition to understanding and applying Clean Code guidelines, you can use static code analyzers in order to help detect deviations from naming rules, code duplication, high complexity, the presence of commented-out code, etc. This practice does not replace code reviews, but it effectively complements them.
Refactor from the very beginning
Once you write a piece of code, you may identify some code smells, or maybe you realize that you should have applied some principle you did not. That’s totally Ok — you just refactor it from the very beginning. To take this practice to the next level, use Test-Driven Development (TDD), which we’ll briefly describe next.
Test-Driven Development (TDD)
TDD is a development process that describes a cycle called ‘Red-Green-Refactor’, in which you first have your unit test failing (because of the lack of implementation), then you achieve the expected result (by writing some code), and finally (if the code can be simplified or improved), you refactor it, run your tests again and get immediate feedback on whether the refactor was correct or not.
Not only does this help in improving the design of your code (by calling your code from unit tests you get a feeling of how clear and simple the interfaces are), but it also acts as a safety net that in most cases prevents further changes from introducing bugs. In addition, it’s concrete proof that your code units do what they are expected to do.
TDD requires a totally different mindset than the traditional “write code-debug-done” cycle.
In order for this practice to be effective and not cause frustration for developers, it requires them to have proficient knowledge and experience in programming principles (eg, S.O.L.I.D.), and advanced design skills. Since it’s a discipline, it requires practice and improvement over time. Once it’s mastered, however, you acquire a truly powerful skill.
A final note on this is, TDD does not replace other testing practices, like for example, integration tests. Its focus is on code units, not on the interaction between them.
Empowering people is key
Have design sessions with your team. By approaching the problem in groups, you’ll have different insights to solve it. This not only enriches the decision making process but also helps in tutoring every team member with different approaches in software design, therefore improving everyone’s skills.
Perform interactive code reviews and discuss improvements. Talk in terms of programming principles and clean code standards, and suggest doing it in a simpler way whenever you can.
No attempt at improving your development deliverables will succeed if there is no clear communication and agreement related to standards, principles and practices with your team. Just saying ‘we should do this’ is not as effective as taking the time to present the issue, propose a solution, and highlight its benefits. Always do this using a concrete example of your source code. Iterate on this often.
Simple gives a totally different experience
We didn’t mention all the principles and practices there are, nor did we elaborate on the most important ones, since they deserve a comprehensive study and that goes beyond this article’s goal. But I strongly recommend learning all of them.
Principles are at developers service, but not the other way round. Your context and goals should primarily drive your software design decision making. If that’s done in a sensible way, you are going to come up with simple and extensible source code that’s easy to maintain. And that contributes to an increasingly better team performance.
There is another benefit, too. For a developer, working on a clean and testable code base is a way better experience than dealing with the high risks of change in a messy source code, with no unit tests nor proper design principles whatsoever.
This better experience effect does not have a quantifiable result, but it certainly impacts in a positive way on every team member. Everyone can now read any piece of code (there is no need for ‘specific code experts’), any programming task is executed successfully, any issue is quickly diagnosed and fixed, and product quality improves. In other words, you can now move mountains.