Can Kotlin do it better?

Davide Cerbo
4 min readFeb 15, 2020

In this post I will do a simple refactoring of a real small piece of code that I found in a legacy code base and after I will translate the example to Java to Kotlin. Why? Only in order to demonstrate why Kotlin can be useful for writing more maintainable code and not only a fashionable language for old Java developer, like me :)

Every day we face simple problems like splitting a list into two new lists.

public void myMethod(A source, B destination) {
List<Credentials> list = source.getCredentials();
Set<String> users = new HashSet<>();
Set<String> groups = new HashSet<>();
for (Credentials item : list) {
if (item.isActive()) {
if (CredentialsType.USER.equals(item.getType())) {
users.add(item.getName());
} else {
groups.add(item.getName());
}
}
}
destination.setGroups(users);
destination.setUsers(groups);
}

Ok, above we solve our problem, but our code is doing too much stuff and its hard to change when the requirement will change. Moreover, we are not trendy because we are not using at Java Stream (Java 8 it is released in 2014, come on!!!) to iterate our list. So we can refactor our code in:

public void myMethod(A source, B destination) {
Set<String> users = new HashSet<>();
Set<String> groups = new HashSet<>();
source.getCredentials()
.stream()
.filter(Credentials::isActive)
.forEach(item -> {
if (CredentialsType.USER.equals(item.getType())) {
users.add(item.getName());
if (CredentialsType.GROUP.equals(item.getType())) {
groups.add(item.getName());
}
});
destination.setGroups(users);
destination.setUsers(groups);
}

But nothing is changed, we just use a different API, but our code logic is still the same.

PS: Always write a test before starting the refactoring in order to validate that the original behavior doesn’t change, otherwise a kitten will die!

Serious refactoring in Java

Even though write less code is better, we are doing two things in the same piece of code, so we can make our code cleaner, just filling a list at a time.

public void myMethod(A source, B destination) {    Set<String> users = source.getCredentials()
.stream()
.filter(Credentials::isActive)
.filter(it -> CredentialsType.USER.equals(item.getType()))
.map(item -> item.getName())
.collect(Collectors.toSet());
Set<String> groups = source.getCredentials()
.stream()
.filter(Credentials::isActive)
.filter(it -> CredentialsType.GROUP.equals(item.getType()))
.map(item -> item.getName())
.collect(Collectors.toSet());
destination.setGroups(users);
destination.setUsers(groups);
}

After we can extract a method to avoid duplications and to have a decent code.

public void myMethod(A source, B destination) {
destination.setGroups(extractFrom(source.getCredentials(),
item -> CredentialsType.GROUP.equals(item.getType())));
destination.setUsers(extractFrom(source.getCredentials(),
item -> CredentialsType.USER.equals(item.getType())));
}
public Set<String> extractFrom(List<Credentials> credentials, Predicate<Credentials> predicate) {
return credentials
.stream()
.filter(Credentials::isActive)
.filter(predicate)
.map(Credentials::getName)
.collect(Collectors.toSet());
}

Now, using our shining new method extractFrom its very simple to add new features in our method, for instance, if we want to collect adult users, we can simply add a line in our code.

destination.setAdultUsers(extract(source.getCredentials(),
item -> item.getAge() > 18
&& CredentialsType.USER.equals(item.getType())));

We already moved some responsibilities from one method to another, but, we can move the responsibility about the type of the credential, or if the user is Adult in the Credentials class and use a cool method reference:

destination.setUsers(extractFrom(source.getCredentials(), 
Credentials::isUser //or ::isGroup or ::isAdult));

We can do more refactoring? Of course! But for our target, it is enough.

Kotlin can do it better

In Koltin we can do even better, as shown below:

fun myMethod(source: A, destination: B): B = destination.copy(
groups = extractFrom(source.credentials){ GROUP == it.type},
users = extractFrom(source.credentials){ USER == it.type },
adultUsers = extractFrom(source.credentials){
USER == it.type && it.age >= 18
}
)
fun extractFrom(credentials: List<Credentials>,
predicate: (Credentials) -> Boolean) = credentials
.filter { it.isActive }
.filter { predicate(it) }
.map { it.name }
.toSet()

The code above is exactly the same that we did in Java, but using some Kotlin features:

  • Implicit name of a simple parameter: we can remove unuseful code using the keyword “it” and improving code readability. This keyword stands for the default single parameter of a function, so we don’t have to declare it explicitly.
  • Passing trailing lambdas: in Kotlin we can pass a lambda expression outside function parameters when the last parameter of a function is a function, and if it is the only argument, the parentheses can be omitted entirely.
  • Data classes: are classes whose main purpose is to hold data and they provide useful methods like copy that can create a new copy of the object. The method accepts as parameters any field of the class, using named arguments, so we can customize our copy, reaching immutability principle, because of our data class is immutable because it will use val modifier to define class fields:
data class B(val users: Set<String>, val groups: Set<String>, val adultUsers: Set<String>)

Further reading

In the past, I wrote two articles like that, unfortunately, they are in Italian, but there is a lot of code, so you can understand them even just by reading the code ;)

--

--

Davide Cerbo

Open-space invader, bug hunter and software writer