Alebrijes are brightly colored Mexican folk art sculptures of fantastical creatures. Alebrijes are very strange and unknown animals.
A new user story comes for grooming, apparently a very small change, but as part of the requirements you have to change the legacy class, yes that class that no one wants to touch and everyone is scared of, sounds familiar? After reading the story, everyone agreed on 3 points, you think you can do it faster, but you want to be cautious so you say 3.
When the sprint starts you are very excited to do the change, you open the code, and boom, you found an alebrije. What the falafal is going on here?, this story requires more things than I initially thought, how I will be able to finish this? and I’m not even able to understand the code.
Wait, how did we end up with this implementation? Why the code is written in this way? Ummm, there are multiple reasons why this piece of falafal ended up like this, but now is not the time to find a culprit; as one of my managers use to say, “you touch it, you own it” or “Es tu perro y tu lo bañas”, now you are the owner of this code and you are responsible for this.
Let’s be honest, every single project around has code like this, a lot of code is already a mess, and we need to learn how to live with it, so the real question is what can we do to make it better? How can we improve the quality of this code?
Every situation is different, and there is no silver bullet to deal with this mythical beasts, but let’s start with the premise that you got a very old piece of code with zero documentation, zero unit testing, and not following the architecture patterns, conventions and best practices for Android. With that in mind here are some of the things that you can do:
Improving Documentation
How much documentation do we need? Only whatever you can keep updated. Keep it simple, but not too simple. Add enough detail to understand how the implementation works.
Start by creating a simple diagram. It can be a simple diagram with boxes and their relationships. This will help to understand what components are involved. If the authors of the code are still available, involve them.
After the basic diagram, you can create a navigation flow, a high-level class diagram, and sequence diagrams to understand the flow, dependencies, behaviors, and responsibilities of each component.
When the feature includes complex business logic, use the Debugger and logger to understand it better. Document all your findings.
Start documenting the code
- Public methods. Start with public methods. Those are the ones expose to external invokers and require good documentation.
- Exceptions. Document possible exceptions to be thrown by methods.
- Executable specifications through unit testing. This will continuously increase with each iteration. Try to follow a pattern where we clearly specify the scenarios under test and the expected behavior. One popular naming convention Should_ExpectedBehavior_When_StateUnderTest.
Make a list of to-do items to improve the code. TODOs can be included in the code or using some wiki page. Next time someone has some free time, they can come to the list and pick one of the items.
Improving Estimation
Don’t shout yourself in the foot, be prepared ahead of the grooming. You need enough time to work on legacy code. We need to be as accurate as possible. We don’t want to introduce more technical debt or make the code worst.
Start by reading and understanding the user story.
Identify the changes needed to accomplish the new functionality.
When the story has a big impact on the code, split it into small to-do tasks.
Estimate the required tech tasks to complete the user story. Don’t forget to include integration testing or data preparation when needed.
Improving Testing
Without tests, there is no way to tell if we are breaking existing functionality. Don’t trust code, not including tests.
But where do I start if most of the code is in the views, in private methods, static functions, and final classes?
Robolectric
Robolectric lets you run view tests without an emulator or physical device. This will help you to write tests for the views without installing the app. You can test activities or fragments in isolation. Robolectric extends Android framework with a large set of test APIs. Robolectric uses the concept of shadows to extend the behavior of an Android OS component. You can create your own custom shadow implementations.
Kotlin and Mockk
With the help of Kotlin and Mockk you should be able to test static utility methods, static initializers, final classes, private methods. Powerbock is a really good option if you are allowed to use only java.
Improving the code Quality
We can start with small refactors to improve the readability and maintainability of the code. We can start with changes like renaming classes, variables, and methods. This will help us to get a better understanding of the implementation so we can proceed with bigger refactors.
Please make small PRs when making your changes. If you send a big PR, is very likely that reviewers will not be able to follow the changes. Additionally, include in the PR description the problem you are trying to solve, and the solution.
Problem/Requirement
Include a description of the problem or requirement to solve
Solution
Description of the solution
Refactoring
The next refactors will require a full regression testing by your QA, be very careful about when to do these changes.
- Separate UI logic from application logic. Move logic outside of the Views. Nowadays we have MVP, MVVM, MVI, but all of them have the same intent Separation of Concerns. Try to keep the view classes lean to avoid life-cycle related problems.
- Life Cycle Aware components. Move logic triggered by lifecycle events out of the activity/fragment. This will help you to keep your code more organized and maintainable.
- Use LiveData and Flow to notify data changes to the View.
- Use Coroutines to simplify your background operation handling.
- Move data access logic to repositories.
- Extract code to new classes using kotlin. Kotlin will help to simplify and made the code safer with java support.
- Use DI. Encapsulate related behavior and expose it using interfaces. This will help you with the tests.
Architecture Components and Guide to App architecture
Android already has a guide with the best practices and recommended architecture to help you design robust, testable, and maintainable apps. You don’t need to reinvent the wheel, use the Android guide to App Architecture and Architecture Components collection library. The code works consistently across Android versions and devices so that we can focus on the related to our business domain.Follow the Boy scout rule
We keep doing small changes to the code to improve it. The code gradually will get better and better. With this rule, we will see the team caring for the system as a whole, rather than individually caring for the parts they build.
And that’s all, I hope little by little we get rid of those alebrijes, and we don’t add more technical debt during the process :).