We all know the cliché situation: a group of developers restlessly waiting for the promised day of refactoring that never comes. But as with any cliché, it is also a very real situation, so chances are, at some point in our developer careers, we’ll all come across a truly messy project. Code, like anything else in this universe, is subject to entropy, and without some energy input to keep it clean, it has a natural tendency to become messier as time goes by.
In our case, the original code-base of the project was actually solid, not perfect, but it clearly had a lot of thought and care put into it. Gradually, over the next 4 years, new features kept being added, and, on many occasions, the time constraints didn’t allow for the architecture to keep up with the new requirements. As the code quality started to decay, the developers became frustrated with their own work, resulting in an even bigger toll on the state of the project, and the snowball kept on growing. This is right about when me and my colleagues happened to take over and quickly realized that the project was actively trying to sabotage any line of code we wrote. We needed to figure out… something. Here are a few short “lessons” we learned during the process of figuring out and implementing that… something.
Elephant in the room
There is no time available. The team is swimming in bugs, the backlog is full for the next three years, you complain constantly about the state of the code; still, time never gets allocated for the project to receive that desperately needed health check. It’s easy to blame the client for not understanding the critical issues you are presenting, but try to view the situation from his point of view. The client gets a working build, relatively regularly, which satisfies the investors and/or succeeds in bringing in money to his company. Allocating resources to remake something that already works doesn’t make any kind of financial sense.
Add to the problem the constant need to add new features, shifting around development resources like this might look like too much a risk. So how do we convince the client that our path is virtuous? With mutual trust and talking a language that all businessmen understand: money. If you can prove that investing resources now will save money in the future, you might find the client being a bit more open to your ideas. This first step is often the hardest for developers, as it requires quite a bit of communication skills and the ability to understand what the client needs. Usually, this part is done by the PO, but it really doesn’t hurt if developers also try to improve a bit in this area.
To make it easier, do your research and properly understand the issues that plague the project. You probably already have a rough mental image of what needs changing, but something more concrete is needed. In case you haven’t already, this is the perfect time to start reading (Uncle Bob’s Clean Code and Clean Architecture are the best place to start). It’s a lot easier to identify code smells once you actually know what they are and how they look like. Use this newfound knowledge to identify and make a list of problem areas detailing each code-smells it has and the problems they cause to the team and the product, and how this translates to lost money. For example, performance issues will lead to unpleasant end-user experience, which will ultimately result in a lower number of users, or, as another example, an inflexible code-base will lead to longer development times, and strongly coupled code will create tens of bugs at even the slightest change, wasting both testing and development time and causing many delayed releases.
When life gives you spaghetti, make lasagna
A good place to start bringing order to the code base is by separating it into layers. No matter what architecture your project uses, it will probably have something that resembles a networking layer, a database layer, a business logic one, etc. Making maps of our classes show how they related to and communicated with one another. In our case, the project had no kind of separation in packages or frameworks so these approaches helped us gain a visceral feel of how classes clumped together naturally and used it as a guide for defining our layers.
In the beginning, we just moved groups of classes to their own separate packages, with no actual significant code change. Depending on the technology used, this can pose enough challenges as it is. The compiler will always be your friend; follow its error trails and you will start to see all kinds of patterns in the code and better understand how the codebase matured over the years and why it ended up in its current state. Use this info to be emotionally attached to the project and to help you take the best course of action moving forward.
Afterwards, we steadily started isolating these packages by restricting all communication to certain classes acting as interfaces to the rest of the project. By creating these communication layers, we basically added rules that the whole project had to respect when trying to access the networking layer, for example. Afterwards, this allowed us to have the certainty that any change we made won’t break the project in unexpected ways as long as all layers still complied with the requirements of the communication layer.
It’s about what the project needs
Given the opportunity, developers get really enthusiastic about adding new technologies to the project. While this energy helps with morale, it is easy to lose perspective. Don’t just delete code, save all of the code that you can. Most times, a full rewrite isn’t needed and large portions of code are good enough to still be reused for the foreseeable future. If you do decide, you need to rewrite, what architecture are you going to use? For example, in the case of user-interface-oriented apps, there have been debates regarding what is better between MVC, MVP, MVVM, VIPER, and what else is the newest trend. In reality, all of them are good and some fit specific situations better than others do. Some features might work better with an MVVM approach while others need nothing more than MVC. Mixing and matching architectures as needed is fine as long as each of them exists in its own little bubble so any issue that might arise won’t impact the rest of the project.
In our case, we went with our own flavor of MVP mainly because we already had some successful attempts in this direction to build upon. The steeper learning curve and extra complexity of other approaches made our decision even easier. We needed a low cost of implementation, meaning that any team member regardless of the level of experience should’ve been able to pick up the new architecture and start using it comfortably in the shortest time span possible. The resulting product also had to be open to future changes, to be backwards compatible with an already complex system, and, most important, to be robust, stable, and easily maintainable. Simplicity guaranteed us all that.
At the danger of repeating myself, good communication with the client and the existence of a roadmap proved to be extremely beneficial for our team. Knowing what technical challenges waited for us, three or four months down the line, helped mold our refactoring plan to the roadmap, and in many cases, it allowed us to create architectures built with specific future requirements already in mind.
It takes time
Proceeding slowly and with caution was mandatory, so any expectations that this is something that can be done in a couple of sprints must be left behind from the very start. Things happen, priorities change, and by the end, it took us a bit over two years to bring the whole project up to standards. This process required planning well in advance, writing lots of documentation, trying to find the perfect balance between the product needs, the client needs, and the needs of the team. Personally, I don’t think any of these “lessons” are groundbreaking, more like common sense ideas, but most things related to clean coding relate simply to common sense.
About Tudor Carare
Tudor’s focus is on creating smooth mobile experiences for users, as well as finding and solving problems within the projects he works on. With a sharp eye for detail and an analytic point of view to every situation, Tudor manages to meld his passion for art and music into software development, to craft products that are not only highly functional, but also creative and inventive.
24 May 2023
Facilitating Innovation in Fintech
22 May 2023
Dotnetdays: A Next Level Tech Event That Ignited the .NET Community
26 April 2023
How Failing Fast Boosts the Resilience of Your Software