Oh no Is it Map function? ( LOL on Complication )
Hi everyone, In this post, I will share with you my personal opinion maybe you can disagree. But if you do disagree, try to share with me your opinion. I am always open-minded, so perhaps I can learn something from your viewpoint.
I am sure everyone who is writing apps for Android or writing code in any language. They have experience in writing a map function. Today, I am going to share with you my opinion about Mapping. A lot of time, I saw a lot of code which doesn’t make sense, but I need to read, especially in code reviews on my job or in the interview assignments. I am going to share with you some classes, as shown below:
(Note: I used to write this type of code also)
data class GitHubRepository(
val `private`: Boolean = false,
val archive_url: String = "",
val assignees_url: String = "",
val blobs_url: String = "",
val branches_url: String = "",
val collaborators_url: String = "",
val comments_url: String = "",
val commits_url: String = "",
val compare_url: String = "",
val contents_url: String = "",
val contributors_url: String = "",
val deployments_url: String = "",
val description: String = "",
val downloads_url: String = "",
val events_url: String = "",
val fork: Boolean = false,
val forks_url: String = "",
val full_name: String = "",
val git_commits_url: String = "",
val git_refs_url: String = "",
val git_tags_url: String = "",
val git_url: String = "",
val html_url: String = "",
val id: Int = 0,
val issue_comment_url: String = "",
val issue_events_url: String = "",
val issues_url: String = "",
val keys_url: String = "",
val labels_url: String = "",
val languages_url: String = "",
val merges_url: String = "",
val milestones_url: String = "",
val name: String = "",
val node_id: String = "",
val notifications_url: String = "",
val owner: Owner = Owner(),
val pulls_url: String = "",
val releases_url: String = "",
val ssh_url: String = "",
val stargazers_url: String = "",
val statuses_url: String = "",
val subscribers_url: String = "",
val subscription_url: String = "",
val tags_url: String = "",
val teams_url: String = "",
val trees_url: String = "",
val url: String = ""
)data class Owner(
val avatar_url: String = "",
val events_url: String = "",
val followers_url: String = "",
val following_url: String = "",
val gists_url: String = "",
val gravatar_id: String = "",
val html_url: String = "",
val id: Int = 0,
val login: String = "",
val node_id: String = "",
val organizations_url: String = "",
val received_events_url: String = "",
val repos_url: String = "",
val site_admin: Boolean = false,
val starred_url: String = "",
val subscriptions_url: String = "",
val type: String = "",
val url: String = ""
)data class Repository(
val avatar_url: String = "",
val gravatar_id: String = "",
val id: Int = 0,
val url: String = "",
val description: String = "",
val downloads_url: String = "",
val full_name: String = "",
val git_url: String = "",
val html_url: String = "",
val name: String = "",
val releases_url: String = "",
val teams_url: String = ""
)
Now it’s time to define a relationship between these classes. GitHubRepository is a model which contain Owner model. In this model, we will get data from the GitHub API.
A repository is a model on which app code is dependent. Now, we want to convert GitHubRepository to Repository when showing data on the app. Next task is how you will write code. For this example, please do focus only on the map function, not other Rx or Kotlin collection function calls. Also, try to imagine how you will implement.
The first implementation (mostly I saw this implementation but I don’t like in readability perspective)
repo.getItems()
.flatMapIterable<GitHubRepository> { it }
.map {
Repository(
avatar_url = it.owner.avatar_url,
gravatar_id = it.owner.gravatar_id,
id = it.id,
description = it.description,
name = it.name,
downloads_url = it.downloads_url,
full_name = it.full_name,
git_url = it.git_url,
html_url = it.html_url,
releases_url = it.releases_url,
teams_url = it.teams_url,
url = it.url
)
}
.sorted()
.blockingIterable()
.forEach(::println)
Here, this code is readable because that is independent, but when you read this code in the real-world project with other chaining calls or UI related code, this gives you a tough time.
How we can improve this code. In my opinion, anywhere you are going to use a map function always create a secondary constructor into the destination class. Like in our example, I am going to create a secondary constructor in a Repository class, and this will take a GitHubRepository as a single param shown below:
data class Repository(
val avatar_url: String = "",
val gravatar_id: String = "",
val id: Int = 0,
val url: String = "",
val description: String = "",
val downloads_url: String = "",
val full_name: String = "",
val git_url: String = "",
val html_url: String = "",
val name: String = "",
val releases_url: String = "",
val teams_url: String = ""
) {
constructor(gitHubRepository: GitHubRepository) : this(
avatar_url = gitHubRepository.owner.avatar_url,
gravatar_id = gitHubRepository.owner.gravatar_id,
id = gitHubRepository.id,
description = gitHubRepository.description,
name = gitHubRepository.name,
downloads_url = gitHubRepository.downloads_url,
full_name = gitHubRepository.full_name,
git_url = gitHubRepository.git_url,
html_url = gitHubRepository.html_url,
releases_url = gitHubRepository.releases_url,
teams_url = gitHubRepository.teams_url,
url = gitHubRepository.url
)
}
Now it’s time to see the magic of this constructor.
Second and Perfect Implementation in my opinion:
repo.getItems()
.flatMapIterable<GitHubRepository> { it }
.map(::Repository)
.sorted()
.blockingIterable()
.forEach(::println)
Wow, simply readable. Also, the Repository class is easily testable. I can test secondary constructor. Sorry, but I want to say WOW again, :).
There is one more benefit we will get as a bonus. That is a mapping of a list but if I want to map or convert a single object of GitHubRepository to Repository that is also very simple as shown below:
val gitHubRepository = GitHubRepository()
Repository(gitHubRepository)
If you are using Kotlin, you can write this code more readable by using fluent API’s as shown below:
GitHubRepository()
.let(::Repository)
WOW.
Now I feel that look nice. Also, I am delegating the conversion responsibility to the proper class.
There are also some other implementations, which I don’t like but these are better than the first implementation. So we can explore those also.
By writing mapTo function in the origin class. Like in our case, GitHubRepository will contain a mapToRepository method, as shown below:
data class GitHubRepository(
val `private`: Boolean = false,
val archive_url: String = "",
val assignees_url: String = "",
val blobs_url: String = "",
val branches_url: String = "",
val collaborators_url: String = "",
val comments_url: String = "",
val commits_url: String = "",
val compare_url: String = "",
val contents_url: String = "",
val contributors_url: String = "",
val deployments_url: String = "",
val description: String = "",
val downloads_url: String = "",
val events_url: String = "",
val fork: Boolean = false,
val forks_url: String = "",
val full_name: String = "",
val git_commits_url: String = "",
val git_refs_url: String = "",
val git_tags_url: String = "",
val git_url: String = "",
val html_url: String = "",
val id: Int = 0,
val issue_comment_url: String = "",
val issue_events_url: String = "",
val issues_url: String = "",
val keys_url: String = "",
val labels_url: String = "",
val languages_url: String = "",
val merges_url: String = "",
val milestones_url: String = "",
val name: String = "",
val node_id: String = "",
val notifications_url: String = "",
val owner: Owner = Owner(),
val pulls_url: String = "",
val releases_url: String = "",
val ssh_url: String = "",
val stargazers_url: String = "",
val statuses_url: String = "",
val subscribers_url: String = "",
val subscription_url: String = "",
val tags_url: String = "",
val teams_url: String = "",
val trees_url: String = "",
val url: String = ""
) {
fun mapToRepository(): Repository = Repository(
avatar_url = owner.avatar_url,
gravatar_id = owner.gravatar_id,
id = id,
description = description,
name = name,
downloads_url = downloads_url,
full_name = full_name,
git_url = git_url,
html_url = html_url,
releases_url = releases_url,
teams_url = teams_url,
url = url
)
}
Third Implementation:
repo.getItems()
.flatMapIterable<GitHubRepository> { it }
.map { it.mapToRepository() }
.sorted()
.blockingIterable()
.forEach(::println)
Also, some people like to write an extension method, as shown below:
fun GitHubRepository.mapToRepository() = Repository(
avatar_url = owner.avatar_url,
gravatar_id = owner.gravatar_id,
id = id,
description = description,
name = name,
downloads_url = downloads_url,
full_name = full_name,
git_url = git_url,
html_url = html_url,
releases_url = releases_url,
teams_url = teams_url,
url = url
)
I am against this extension method. We should not write an extension method of the class which is under our control. Like in this class, GitHubRepository is our class. So we should add function inside of the class, not as an extension function. We will discuss more extension function in another post.
Instead, I used to create an extension function of a list which will map to new class list as shown below :P. ( Please don’t do this )
fun List<GitHubRepository>.mapToRepositoryList(): List<Repository> = this.map {
Repository(
avatar_url = it.owner.avatar_url,
gravatar_id = it.owner.gravatar_id,
id = it.id,
description = it.description,
name = it.name,
downloads_url = it.downloads_url,
full_name = it.full_name,
git_url = it.git_url,
html_url = it.html_url,
releases_url = it.releases_url,
teams_url = it.teams_url,
url = it.url
)
}
Fourth Implementation:
repo.getItems()
.map { it.mapToRepositoryList() }
.sorted()
.blockingIterable()
.forEach(::println) // In my opinion, please don't do this
Conclusion:
I feel the second implementation is a fantastic implementation of mapping. In case that is not possible maybe we can go with third but fourth and first only create complexity without giving any benefit.