Sunday, July 15, 2018

Why we switched to Kotlin (Pt. 1) - Nullability


As an architect/team lead, I don’t take changing our primary backend language lightly. Moving from Java to Kotlin would mean retraining the entire scrum team. Coming up with new coding standards and tools can be a painful process. I certainly wouldn’t undergo this change for mere syntactic sugar. Removing boilerplate may make us faster developers up front, but clever unreadable code wastes the team’s time in the end.

Change would have to come for some other reason. In this case, the reason was primarily around the preciseness and effectiveness of the language in conveying important architectural and business rules. One of the principles I gleaned from Domain Driven Design was that these important rules should not be buried in implementations but should be first class citizens in the model (see the section on Policy on location 754 of the Kindle version). It turns out that Kotlin supports easy and direct representation of two important decisions that are absent in Java, each with compile time support (violations are discovered at compile time rather than runtime). These two are immutability and nullability. I will deal with nullability here and immutability in my next post.

Kotlin has nullable types where the default is non-nullable (making the better practice the default). Any type can be made a nullable type by adding “?”. This is incredibly useful when trying to express a domain's rules. Take for example the following signature in Java.
public void createUser(String firstName,
                       String lastName,
                       String email,
                       String username)
I can decipher nothing about the rules regarding the identity of a User from this signature. I would naturally assume that either username or email or both will serve as unique natural identifiers to distinguish this user from all others (as certainly first and last name could not be guaranteed to be unique). I would expect that the identifying property would be required but the signature tells me nothing about which fields are required. On the other hand, the engineer that implements this signature, in the best case, buries the business rule as an early null check while checking other preconditions. In the worst case, he or she forgets to implement this check leading to more complex bugs in the software or inconsistencies in datastores (I have seen and caused far too many NullPointerExceptions in my career).

In Kotlin, this same signature might be represented as follows:
fun createUser(firstName: String,
               lastName: String,
               email: String,
               username: String?)
        
Here I can see that email is non-nullable but username is nullable. Rather than testing the method with different parameters to see what the requirements for constructing a user are or reading through the implementation (or trusting Javadocs that may or may not be there, and provided they are, may not be accurate), I merely look at the types. Not only that but the compiler will also enforce these rules, adding another layer of safety and correctness to the code.

Exposing this sort of clarity about the domain rather than burying it in a null check in the method makes it easier to reason about and share these rules with others, including business experts. I think this is worth the cost of transitioning from Java to Kotlin.

No comments:

Post a Comment