History of refactoring of Citymobil application
Just over a year ago, I joined the Sitemobil team as an Android developer. I got used to the new project, new approaches and technologies. At that time, Citimobile already had a rather long history, like the project I had adopted, an Android application for ordering a taxi. However, as often happens in such cases, the code carried the characteristic traces of old solutions. And now, after successful refactoring of the code, I want to share ideas that, I believe, can be useful to those who have to refactor an already existing project. And above all, it can be useful for small companies with small development teams.
Businesses often test their ideas, channeling limited resources to this, and try to get feedback, test their hypotheses as quickly as possible. At such times, as a rule, high-quality thinking and implementation of the project's architecture, taking into account the groundwork for the future, fades into the background. Gradually, the project acquires new functionality, new business requirements appear, and all this affects the code base. "Citymobil" in this regard is no exception. The project was consistently developed by several teams in the old office, then, during the move, it was maintained and partially corresponded through outsourcing. Then they started to form a new team, and they handed me work on the project.
At that time, the “development” moved to the Moscow office, the work was in full swing - new interesting and ambitious tasks constantly appeared. However, legacy more and more put sticks into the wheels, and once we realized that it was time for a big change. Unfortunately, then managed to find not so much useful literature. It is understandable, it is experienced, it is hardly possible to invent or find the perfect recipe that works 100% of the time.
The first thing to do is understand, do you really need refactoring? This should be considered if:
- The speed of introducing new features is unreasonably low, despite the high level of specialists in the team.
- Code changes in one part of the program can lead to unexpected behavior in another part.
- Adaptation of new team members is delayed.
- Testing the code is made difficult by strong connectivity.
After understanding the existence of a problem, you should find answers to the following questions:
- What is actually wrong?
- What led to this?
- What needs to be done so that this does not happen again?
- How to correct the situation?
It is almost impossible to build a good long-term project without laying a certain architecture. In our project we decided to implement a “puff” architecture, which has already recommended itself well.
Initially, the project was written, basically, only with the help of tools provided by the Android SDK itself. The approach is undoubtedly a working one, but it forces to write a lot of sample code, which greatly slows down the development. And given that today, many are accustomed to certain stacks of technology, the adaptation of new developers took longer. Gradually, we came to more convenient technologies that many people know and appreciate, and which have proven their reliability and consistency:
- MVP is a user interface design pattern (Model-View-Presenter).
- Dagger 2 is a dependency injection framework.
- RxJava2 - implementation of ReactiveX - libraries for creating asynchronous and event-based programs using the “Observer” pattern, for JVM.
- Cicerone is a library that allows you to simplify navigation in the application.
- A number of specific libraries for working with maps and locations.
It is very important to adopt a common code style for the team, to develop a set of best practices. You should also take care of the infrastructure and processes. It is better to write tests for the new code right away, there is a lot of information about this.
Inside the team, we have become mandatory to conduct a code review, it takes not so much time, but the quality of the code has become much higher. Even if you are alone in the team, I recommend working on Git Flow, creating merge requests and at least checking them yourself.
All the “dirty” work can be delegated to CI - in our case it is TeamCity using fastlane. We configured it to build feature-branches, run tests and display on an internal test. We set up separate builds for the production / staging environment, feature- (we call them by task number with TASK # task_number template) and release branches. This makes testing easier, and if an error occurs, we immediately know what needs to be fixed and where.
After carrying out all the preliminary actions we get to work. We started a new life in the old project by creating a package (cleanarchitecture). It is important not to forget about the activity-aliaswhen moving entry points to the application (a-la ActivitySplash). If you neglect this, then, at best, you will lose the icon in the launcher, and at worst - compatibility with other applications will be broken.
<!-- android:name=".SplashActivity" - old launcher activity --><!-- android:targetActivity=".cleanarchitecture.presentation.SplashActivity" - new launcher activity --><activity-aliasandroid:name=".SplashActivity"android:targetActivity=".cleanarchitecture.presentation.SplashActivity"><intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter></activity-alias>
As experience suggests, it is better to start refactoring from minor small screens and parts of the application. And by the time the time comes to recycle the most complex and voluminous part of the program, a considerable part of the code will already be written for other modules and can be reused.
Also, we then had a big task to complete the redesign of the application, which, at times, resulted in a complete rewriting of screens. We started with the improvement of auxiliary screens, getting ready to proceed to the main.
After rewriting the next part of the application, we searched for parts of the code in the old part of the application and tagged with @Deprecated annotations and analogues of these: https://github.com/VitalyNikonorov/UsefulAnnotation. In them we indicated what should be done when rewriting this part of the program, what kind of functionality and where it is implemented.
/**
* This class deprecated, you have to use
* com.project.company.cleanarchitecture.utils.ResourceUtils
* for new refactored classes
*/@DeprecatedpublicclassResourceHelper{...}
After everything was ready to work on the main screen, they decided not to release new features for 6-8 weeks. We did a global rewriting in our own branch, to which we then added merge requests. At the end of the refactoring, they received the coveted pull request and an almost completely updated application.
After refactoring, changes to the functionality of the application have become much easier. So, recently we were again engaged in the processing of authorization screens.
Initially, they looked like this:
After the first processing and refactoring, they began to look like this:
Now they look like this:
As a result, the first iteration took more than twice as long as the second. Since, in addition to processing the UI, it was necessary to understand the code of business logic located in the same place, although this was not necessary, but the flaw was removed, which reduced the time spent working on the task in the second iteration.
What do we have at the moment?
To make the code convenient for subsequent use and development, we adhere to the principle of "pure architecture". I would not say that we have canonical Clean, but we have adopted many approaches. The presentation layer is written using the MVP pattern (Model-View-Presenter).
- Previously, we had to endlessly discuss each step with each other, to clarify whether the change in one module would affect the functionality of another. And now overhead correspondence has dropped significantly.
- Due to the unification of individual components and fragments, the code base volume has greatly decreased.
- As a result of the same unification and processing of the architecture, the classes have become much more, but now they have a clear division of responsibility, which simplifies the understanding of the project.
- The codebase is divided into layers, the Dagger 2 dependency injection framework is used for their separation and interaction. This reduced the code connectivity and increased the speed of testing.
There are many other interesting points related to legacy code refactoring. If readers are interested, I will write more about them next time. Also I will be glad if you share your experience too.