Refactoring an Android application represents a substantial challenge, often requiring a commitment ranging from several weeks to many months. Therefore, a solid plan and focused execution are indispensable for the success of any refactoring project.
I’ve been involved in several refactorings of Android applications over the years and learned a great deal about this intricate subject. This article summarizes my experience and insights.
Refactoring
Refactoring is the process of restructuring existing code without changing its external behavior. Simply put, you refactor when the code works, but is no longer optimal in some ways and should be improved.
You can refactor at any level of abstraction, from a single line of code to the entire application. The larger the scope, the more challenging and time-consuming the refactoring task becomes.
In this article, I’ll use the term “refactoring” to refer to an optimization of a relatively large part of an existing Android codebase. For the sake of being quantitative, let’s say that this post applies to refactoring projects that take more than one man-week of effort. For smaller refactoring tasks, you probably don’t need a framework like the one I’ll lay down below.
Refactoring vs Rewrite
Much like refactoring, rewrite of your Android application lets you enhance its source code while preserving the external functionality. However, despite these apparent similarities, refactoring and rewrite are fundamentally distinct processes, each with its own set of goals and methodologies.
The factors affecting the decision whether to refactor or rewrite your Android project are outside the scope of this post. This topic warrants an article on its own. Here, I just want to warn you about “accidental rewrites”. See, it’s surprisingly simple, and even tempting, to set out to refactor your Android application, and end up rewriting it almost from scratch. This happens more often than you’d think.
When refactoring becomes a rewrite? Unfortunately, as far as I know, there is no clear threshold that indicates that you’re in the rewrite territory. What’s certain, though, is that rewrite indication is not as simple as whether you start by creating a new project in an empty directory. You can do that and then copy large chunks of the existing project over to refactor, or you can work in the existing codebase and end up rewriting most of the code.
So, I’ll take the liberty to define a “rewrite threshold” myself:
- If your activities change more than 50% of the code in the project, then it’s a rewrite.
- If the code becomes non-releasable during the refactoring project and disassociates from the legacy code, then it’s a rewrite.
An immediate corollary from the above definition is that refactored code should be integrated into the main code branch continuously.
Now, as we’ll discuss below, there are some refactoring tasks that can take considerable time. For example, changing from one database technology to another is a big step that can’t easily be decomposed into smaller ones. So, it’s OK for this step to “live” on a side branch for a while. However, if you find yourself creating a branch called “refactoring”, or keeping many longer-lived side branches “waiting to be merged”, then you’re at risk of losing compatibility with the existing code and unintentionally shifting to rewrite. Watch out for these warning signs.
Identify Reasons for Refactoring
You must have very good reasons to launch a large refactoring project. Otherwise, you’ll risk wasting a lot of time for very little gain, if any.
In this context, “modernizing the codebase”, “migrating to a new framework”, “adopting the latest architecture”, etc. ARE NOT valid reasons. These are refactoring goals. Reasons are pain points that you experience right now and want to fix. Valid refactoring reasons might include: slowdown in release cadence, degradation in the overall quality of the application, repeated bugs related to specific features in the app, developers are afraid to touch parts of the codebase, and more.
There is also developers’ desire to use new technologies. Nothing bad or shameful about this, but this aspect is rarely ever discussed explicitly as a reason for refactoring. I believe that’s because most developers realize that there is little business value in migrating to newer technologies, so they can’t make a good case for this position. Unfortunately, not discussing this aspect explicitly doesn’t make it go away. It just becomes an implicit bias affecting many other decisions. Therefore, I recommend just addressing this heads on and discussing how much effort can be reasonably allocated to new technologies, independently of any specific pain points.
Once you have a list of reasons for refactoring written down, you’re ready to set refactoring goals.
Set Refactoring Goals
Refactoring project must have a set of end goals. These goals are the desired state of the codebase after refactoring completes, that will address the pain points identified earlier.
If you wouldn’t go through the trouble of identifying and writing down the reasons for refactoring, you’d be at risk of setting unrelated or unimportant goals. But you have your reasons in a list after the previous step, so now you just need to plan how to address each pain point.
After the actual refactoring activities will start, you might be tempted to broaden the scope and “clean up that other part since I’m already here”. Fight off these temptations and only work on tasks that directly correspond to the defined project’s goals. Otherwise, refactoring projects can become much longer and harder than anticipated.
Naming
In my opinion, good naming is the most important feature of a source code. Poorly written code with good names is much simpler to read and work with than a clean code with unfortunate and/or inconsistent naming.
Therefore, I suggest adding “improve naming in the codebase” to the list of your refactoring goals. I make this universal recommendation because, whatever reasons motivated your refactoring project, better naming will likely address at least part of them.
To be clear, by “better naming” I primarily mean using proper business domain terminology. Sure, standardizing your “managers”, “helpers”, “use cases”, “repositories”, etc. can be great, as well as getting rid of kitchen-sink “utils”, but it’s nowhere as important as making sure that the business terms are used correctly and consistently across the codebase.
To align all team members on naming, I recommend starting by compiling a glossary of business domain terms which are used in the existing codebase. Discuss these terms with the team and see which of them need to change. Then write down a new glossary that you want to adopt, so that team members could use it as a reference when carrying out refactoring tasks.
Democracy During Refactoring Project
New developers don’t have the experience and the insight of an experienced tech lead, so software engineering shouldn’t be a democracy.
That said, since refactoring projects aim to resolve the pain points of all team members, I do believe that most of the project’s goals should be up to vote. Whether it’s the choice of a programming language, an architecture, a framework – let the team decide. Tech leads should have a veto right in case they strongly disagree, though.
Once you are done with setting the refactoring goals, there should be no more democracy. Appoint a technical lead for the refactoring project. This can be the tech lead of the entire application, or one of the more senior developers. The important part is that there should be a single team member who has full authority over the refactoring activities. Sure enough, you want someone in that position who is open to feedback from others, but, at the end of a day, they should have the final say on all refactoring-related technical topics.
Not having a “refactoring tech lead” can lead to team members spending much time on arguments over little details. Furthermore, on bigger projects involving multiple teams, “refactoring tech lead” is crucial to ensure that all developers follow the same practices and guidelines.
Allocate Ongoing Effort to Refactoring Instead of Setting Deadlines
Almost all the refactoring project that I was involved in had strict deadlines. Unfortunately, refactoring is a highly unpredictable activity. Therefore, deadlines resulted in either incomplete refactoring and frustrated developers, or deadline misses and tensions with the management.
I think the better model for refactoring project is ongoing effort allocation. For example, the team can decide that they’ll spend 40% of their time, approximately two days a week, working on dedicated refactoring activities. This will allow them to be less stressed about the deadline and do a better job. They’ll also be working on other ongoing tasks, so the application will evolve and external stakeholders will see some progress, instead of a complete halt.
There is a caveat here, though: if you need to accomplish a big monolith refactoring task, splitting the work across multiple weeks is a bad idea. Therefore, if, for example, you want to set up dependency injection in the codebase, at least one developer will need to work on this full-time until it’s done. That developer shouldn’t have a deadline either, of course, because they can’t estimate how long this task will take (unless they did it several times in the past).
Less is More in Refactoring
The most important technical tip in the context of a big refactoring is to make small changes. Sounds simple, but it’s very complicated in practice.
You probably set to refactor your codebase because it’s somewhat messy: excessive coupling, classes with hundreds or even thousands of lines of code, circular dependencies, “clever” abstractions and other issues. In such codebase, you can start refactoring a small piece of code and then discover that it’s coupled to many other parts of the app that require refactoring as well. So, you proceed to refactor those parts and, after a while, you’re buried under a pile of changes, the code is broken and you forgot where you started. This happens all the time.
So, before you make the next refactoring step, spend a bit of time planning it. Draw a mental boundary around the area you’re going to refactor and commit to not going outside of it. Identify inter-dependencies with other parts of the code and decide what you’ll do about them. Draw diagrams that reflect the current and the desired designs of the affected code. Review your plan with the refactoring tech lead.
If you start a refactoring session and it gets messy and long, or the app breaks and you aren’t sure why, don’t sweat it. Just drop all your current changes using Git and start over. In most cases, this will be more efficient than trying to salvage a derailed refactoring step.
Isolated vs Wide-Scope Refactoring
Some refactoring tasks on your project will be relatively small, affecting isolated parts of the codebase. For example, even though migrating the entire app from one MVx architectural pattern to another can be very time-consuming, you can usually do that screen-by-screen. This is a natural level of isolation, so it’s relatively straightforward to break this big refactoring goal of “migrate to MVx” into smaller steps.
Unfortunately, there are wide-scope refactoring tasks that can’t be decomposed into smaller steps so easily. These usually relate to cross-cutting concerns in the application and can affect larger parts of the source code. For example, setting up a Dependency Injection, replacing one database implementation with another, cleanup of navigation logic, etc.
Wide-scope refactoring tasks are complicated and require long periods of continuous, concentrated effort. Therefore, I recommend assigning them to the most experienced developers. I also found great value in doing wide-scope refactorings as pair-programming sessions because it’s very useful to have someone to consult in real time and watch over your shoulder for mistakes. Though, this means drawing effort from two team members, of course.
A major challenge with wide-scope refactorings is merging the changes with the rest of the team. Whether other team members refactor the code or add new features, wide-scope refactorings tend to introduce conflicting changes. That’s another reason to dedicate concentrated effort to these tasks and get them done as quickly as possible – to reduce the number and the severity of merge conflicts.
Dedicated vs Enabling Refactoring
When refactoring your Android application alongside ongoing product development, you’ll have two main types of refactoring tasks: dedicated and enabling refactorings.
Dedicated refactoring is when you refactor a piece of code as a standalone task, unrelated to the ongoing product development.
In contrast, enabling refactoring is when you perform preliminary refactoring as part of a new feature development. For example, when tasked with adding some elements on a screen, you might start by refactoring that screen (if that’s part of the plan), and then add the required feature into the cleaned up code.
Enabling refactoring is a powerful technique because it lets you integrate refactorings into ongoing product development. This means that, instead of jumping into that part of the code twice, you ramp up once and then perform both tasks. That said, you shouldn’t combine the refactoring and the new feature into a single step. Instead, perform the refactoring first, test the app, merge the code, and only then add the new feature. This separation will spare you a lot of time and energy.
Regression Testing
Probably the most important part of any refactoring project is regression testing, also known as verifying that your changes didn’t degrade the existing functionality. In the ideal case, you’ll have a high-quality suite of automated tests, alongside a dedicated QA team to handle the testing. In the more typical scenario that I observed, either one or both of these components are missing.
Whatever your situation is, I recommend doing regression testing after each refactoring step. This is tedious and time-consuming, but the alternative of introducing a bug can be much worse. For reasons that I can’t explain, refactoring bugs tend to be very challenging to find and fix, especially if you encounter them later in the refactoring cycle.
Communication with Non-Technical Stakeholders
Long refactoring projects can add friction between the R&D and non-technical stakeholders, like product and project managers. Even if they support the refactoring initiative in principle, after a month of invisible activity that consumes a significant part of the R&D time, non-technical stakeholders can become impatient.
The best way to prevent this friction is through communication. Make sure the non-technical stakeholder understand the reasons for the refactoring project and give them the list of the project’s goals that you compiled. Update them each time a goal is completed and removed from that list. Make them “feel” the progress and keep them in the loop, as much as possible.
Don’t Aim for Perfection
Last tip is to remain practical and not aim for perfection.
Sure, you started refactoring project to tidy up the codebase, so you don’t want to make any compromises. Clean code and the latest best practices exclusively, please.
Unfortunately, the real world is complex and messy. In any non-trivial application, you’ll find code that is too challenging or risky to refactor. With few notable exceptions, you can probably leave this code in its current form. If other features depend on it and you want to clean up the interfaces between them, you can use Facade or Adapter design patterns to wrap the problematic code and expose a more convenient API.
The same principle applies at lower levels of abstractions, like a single class. You might want to refactor every class that falls within the scope of the refactoring project, but this wouldn’t be optimal. If the implementation of the class is readable and isn’t excessively coupled to other classes, just refactor its external API. It’s not worth spending two hours refactoring something that’s already encapsulated and decoupled, and can be understood and maintained without much trouble.
Conclusion
Huh, I suddenly realized that there is very little discussion of Android in this article. It ended up containing a checklist for a general refactoring project. Well, in retrospect, this makes total sense because all Android applications are unique, so it’s impossible to give a universal advice related to specific tools or patterns.
In conclusion, I want to share one curious observation. Developers are usually very enthusiastic and eager to kick-off new refactoring projects. However, towards the end of these projects, the same developers will often find themselves exhausted, demotivated and willing to just wrap it up in any shape. So, make sure your reasons for refactoring justify this likely outcome. [If you had a different experience, please share it in the comments section below.]
As always, thank you for reading and don’t forget to subscribe to my newsletter if you want to receive notifications about new posts.
Don’t want to come through as grammar-nazi; your articles are great reads and high quality. Just want to play a small part in keeping them that way.
“… consumes a significant part of the R&D time, non-technical stakeholders can become inpatient.”
The last word, inpatient, should be impatient. Inpatient has a different meaning.
This was actually very helpful. Thanks!