It happens in the lifecycle of every product—some dead or duplicated code is slowing the project down, the team working on that particular product has changed, the technology and practices used have become outdated or obsolete. Over the next couple of years, the app evolves in an undesired way, getting bigger and scarier, and eventually you end up with a monster app that becomes difficult to handle.
Sound familiar? Believe me, when you work in a software development company, you see this kind of thing happen a lot.
Together with a handful of coworkers, we have decided to start a series of posts dealing with the way we refactor applications. Every project is different and we use different techniques, so we will go on to describe specific examples, our experiences, and the lessons we learned along the way.
In our everyday work, we encounter various difficulties. Some are more complex than others, but all of them have the same thing in common—they have to be solved from scratch. This, in turn, means we have the freedom of choice when it comes to the style, technical matters, and solutions we use to cope with the problem. The situation changes when it comes to long-term projects developed by multiple developers, where every one of them leaves a mark on the codebase. Such projects become unduly incoherent and hard to develop further at some point. But most importantly—different approaches impact app efficiency in various ways.
In a situation like this everyone has to decide whether to develop a new version of the application from scratch or to refactor it.
At this point, we feel we should mention that refactoring is extremely time consuming. You may end up with an app unfit for release for months or even years. And it’s impossible to see the final result before finishing the process. However, you don’t have to necessarily go with one choice or another—sometimes you can implement different solutions for different parts of a single system.
In our series, we would like to explain when and why we do we choose the refactoring as the preferred approach. To begin, we’ll present to you how we handled such a refactor in one of our longest-running projects, one we’ve been working on since 2013. In that time, twenty developers from our company managed to create over 65,000 lines of code. That’s a lot.
Project State Prior to Refactoring
A good look at the codebase clearly said that we’ve had many good ideas and some really bad ones over the application’s lifecycle. At the time, the biggest problem we were facing was that the codebase had a lot of dead code—code which was no longer in use but has never been deleted.
The project had representers and controllers in versions 1, 2, and 3 which may have suggested there were three versions of the entire API. Nothing further from the truth! All the versions were almost the same, but no one from the current team could establish why those different version were used in some cases.
By that time, the application had already been in use by nearly 15,000 users. On the other hand, our development team was relatively small, which made it that much harder to take on the reactor. But there was one positive aspect of the whole thing, despite the poor code quality—the controllers were very light. Outside validation, everything worked within external services defined in various places in the application.
The biggest challenge? We have noticed that some of the classes had too many dependencies which needed to be changed. In such a case, the most important thing is to provide the highest possible test coverage in order to test the changes immediately.
The First Steps of the Refactor
At this point, we should mention that the refactor had to be prosecuted concurrently with handling all our current software duties, which we had to take care of in order to provide the highest possible business benefits for both the client as well as for the developer.
Before we set out, we drew up a couple of principles to guide us through the refactor, including the key one—“If possible, do not use any new gems.” Next, we started to discuss the app architecture or, basically, the lack of it. We decided to discuss the idea of concepts from Trailblazer. At the start, the major issue was to determine the range of the domain and the boundaries of concepts.
After preliminary analyses, we decided that concepts will represent specific application functionalities divided into User, HR or Mutual (rarely occurring) parts. After another thorough analysis, we discovered that the division into Concept Admin, HR, and User (which is basically a separate application) with further divisions happening inside their respective sections.
The first phase, which delivers to highest benefit to the developer, is project „cleaning.” It allows us to reduce codebase size tidying up all of the application parts. As you can guess, this phase results in cleaner code, which in turn facilitates changes, especially during subsequent cleanups or during development of new features.
Transferring code fixes or features is connected with a particular concept. Let’s say something is not working in the user profile. That is when we divide the profile into concepts. This way, development takes a bit longer, but produces mutual benefits.
We rarely reinvent things from scratch. Almost everything is divided into smaller classes and by using existing code. We implement different patterns and although it may be sometimes confusing for other developers, we stick to this approach.
The above-mentioned phase of transferring the necessary code and removing its unused portions is just the first step of the refactorization adventure. Nevertheless, we think of it as a simplification of everyday work because the application becomes more business-driven and less code-driven.
There are still cases when code gets duplicated so this should be the next thing to take care of. It will result in better code readability, higher change resistance or even faster application development.
The next phase crucial for stabilizing the application would be the implementation of the repositories concept. But it is time-consuming and should be performed step by step by one and the same person throughout the whole process.
Implementing the concept of Trailblazer would also be a good idea, but for now this change is too complex. We would also like to decrease the number of external gems up to a minimum. That is when code update and maintenance becomes easier and much neater.