Lesson Learned when Implementing 3rd Party Mapper Library for Our Legacy Data Classes

Refactoring a legacy code always provides a valuable experience, especially when creating a mapper for mapping a large data class with hundreds of attributes.

Anang Kurniawan
Staffinc Tech
5 min readApr 6, 2021

--

Developing an application cannot be separated from changes, be it adding features, fixing bugs, or changing the code’s architecture itself. All these changes are needed to improve the user experience, as well as the developer himself.

Due to a lack of knowledge on how to write good code, our code was originally written without any architecture and even ended up producing several large and bloated classes, which is the case for one of our data classes.

Creating a Monster

Over time with the development of business logic and databases, our previously simple data classes have become increasingly complex, with many dependencies and inheritances. And this happens not only to one or two data classes but almost all the data classes in our project.

{
"abc": {
"def": "some nested data",
...
"defN: "some other nested data"
},
...
"abcN": "some other data"
}

While N can be more than 100 attributes, yes, a lot of response payload from our ends.

Can you imagine how complex the JSON above is when it is converted into a data class? Imagine how we create a mapper for that data class. It will be very confusing and exhausting.

Once we had enough knowledge to refactor our code, we realized that what we did before was a big mistake. We wanted to implement MVVM architecture and clean architecture by applying a modular concept, but the data class, which was already very bloated, was one of the obstacles for us to implement this. Since we wanted to separate the data class according to the module layer in the clean architecture, we saw that this was very difficult because our data class was very complicated to create a mapper as an intermediary for each module layer.

Clean Architecture. Image source: https://ahmedadeltito.com/blogs/how-to-architect-an-android-app
Clean Architecture Diagram. Image source: https://ahmedadeltito.com/blogs/how-to-architect-an-android-app

Previously, we tried to create a mapper class manually by changing the data class attributes one by one.

this is our data class in data module (represent response payload from API):

data class ResponseModel(
val abc: NestedResponseModel,
...
val abcN: String,
)
data class NestedResponseModel(
val def: String,
...
val defN: String,
)

this is our data class in domain module (represent business usecase, can be different from data class in data module):

data class DomainModel(
val abc: NestedDomainModel,
...
val abcN: String,
)
data class NestedDomainModel(
val def: String,
...
val defN: String,
)

and this is our mapper class:

class ModelMapper() {
fun mapToDomain(response: ResponseModel): DomainModel {
return DomainModel(
abc = NestedDomainModel(
def = response.abc.def,
...
defN = response.abc.defN
),
...
abcN = response.abcN
)
}
}

Once again, N can be more than 100 attributes.

This is very tiring and will take a lot of time. It is also challenging to refactor again if there is a change in the class data structure one day.

Thus, we are looking for alternatives to make it easier for us to create a mapper for each data class so that the refactor process can run more efficiently and quickly.

Finally, we found a solution

After we tried several mapper libraries, we decided to use MapStruct. We use MapStruct apart from being widely used globally, we feel that MapStruct is relatively easy to use; just add annotation, and the mapper will be generated automatically.

@Mapper
interface ModelMapper {
companion object {
fun getInstance(): ModelMapper =
Mappers.getMapper(ModelMapper::class.java)
}
fun mapToDomain(response: ResponseModel): DomainModel

// if there's special case that You need to map data class to
// other data class that have different attribute,
// use @Mappings annotation

@Mappings(Mapping(target = "ghi"), source = "def2")
fun mapToDomainWithSpecialCase(
response: ResponseModel
): DomainModel
}

As you can see on the snippet above, all we had to do was to create an interface with annotation @mapper and create mapper functions. If we have a specific attribute to map, we can just add annotation @mappings. And that’s it, MapStruct will generate the mapper class for us.

MapStruct has also been supporting the Kotlin data class since version 1.4. By using MapStruct, we felt a significant impact in making the mapper class since there is no need to map each attribute in the data class. Just add the @mapper annotation, and MapStruct will automatically generate the mapper class according to the data class’s attributes.

But, Something happened…

However, MapStruct came with a drawback which was quite a deal-breaker for us. Because MapStruct generates mapper classes during the build process, it contributed significantly to our build time. Plus, we had used many libraries that also generated classes in the build process, such as dagger, glide, and others.

At first, we didn’t feel annoyed, but over time, with more processing done in the build process, we felt a significant increase in build time, and it was quite disruptive to our development process. So, we had to leave some libraries that contributed significantly to improving the build process, one of which was MapStruct.

As an alternative to MapStruct, we used a tool from Kt. Academy called DTO Generator. With this tool, we could create a mapper function using the existing extension in Kotlin.

example of generating mapper with Generate DTO from kt.academy/generate

With this tool we can save more time while creating mapper function. It also generate mapper function in an extension that will make our code more clean.

Lesson Learned

Refactoring legacy data classes was indeed a complicated process and require a lot of effort, but it still had to be done to maintain the quality of the code we produce. Especially if the product we are developing is already established and has many users, it must be made as good as possible so that it is easier to maintain.

Using a 3rd party library or tools can be a solution to simplify the refactoring process. But of course, it is necessary to do in-depth research regarding 3rd party libraries or tools’ advantages and disadvantages. So you can find libraries or tools that suit your needs and don’t even become a double-edged blade that can interfere with the overall development process.

About The Author

I am an Android Developer who loves the world of arts. I work as an Android Developer, but sometimes I do a design challenge with my friends to fill my spare time.

LinkedInMediumDribbbleTwitterInstagram

--

--