In the first part of this series, “What is a Clean Code?”, we discussed two important best practices: using small functions and meaningful names, and using immutable data. Now we will analyze two more practices with practical examples and complete the list of handy techniques for writing a clean code.
1. Avoid Variables with Null Value
The aim of this is to avoid null exceptions. Luckily, by default Kotlin doesn’t allow variables to be assigned null values, which avoids the NullPointerException problem.
However, if we need to work with nullable variables, Kotlin has operators that can handle them in a safe way.
To indicate that a variable can have a null value, we have to add the operator ? at the end of the sentence.
The compiler will throw an error because it isn’t safe to access the properties of a variable that can be null.
Since Kotlin considers if-else sentences as expressions, they can be part of an assignment.
But doing this type of validation isn’t considered best practice in Kotlin. Instead, it’s better to use the safe call operator, also used to mark a variable as null.
In the previous example, we can see that if the transaction variable isn’t null, it will be assigned balance value in the amount variable. Otherwise, it will be assigned null value.
If we want to assign a default value in order to avoid a null result, we can use the elvis operator ?.
It establishes 0 if the balance or transaction variables are, for some reason, null.
This operator can be used in complex situations such as the following:
In case the user, bankAccount or balance variables are null, they are assigned 0.
But instead of assigning 0, it throws an exception.
Kotlin has many range functions that allow to operate with nullable values. For example, instead of making the next validation:
We can use the let function to avoid making explicit null validations of the variable.
Sometimes, it’s necessary to declare a variable before using it, which means that we have to declare it null and then make safe call validations in order to check that it’s truly valid.
We can avoid this problem by using the lateinit and lazy expressions, to defer initializing an object until it’s truly necessary. For example:
If we use lateinit
Within the GameBoard class, we can define the scenes variable, which isn’t null and doesn’t require initial assignment.
Before using it, we have to make sure it’s been initialized. Otherwise, the compiler will throw the UninitializedPropertyAccessException exception.
If we use lazy
The scenes variable inside the GameBoard class will initialize the first time it’s needed by using the lambda provided, which in this case receives the initScenes function.
2. Error Handling
Using exceptions is the common approach to controlling errors that may occur in a program.
Exceptions have several drawbacks. One is that they can make the code harder to read due to the level of complexity added by the try-catch code block. If multiple consecutive or nested blocks are used, they can make it even more confusing.
If not used properly, these multiple blocks can produce coupling of the application modules and make the execution flow harder to track.
In the next example, a money withdrawal, we can see the method of the Account class. If the amount of money to be withdrawn doesn’t meet conditions, it throws an exception.
To solve this, we can use this method:
This is the traditional way to handle errors. But using Kotlin’s sealed class we can solve the same problem by adopting functional programming approach.
With a sealed class we implement a class that encapsulates the result of the operation, that can be either a Success subtype, if the result was successful, or an Error subtype, if there was any error.
The withdraw function will return an instance of this object that’s dependent of the result.
By using the when expression we can handle both results:
Since result classes are very similar, we could use a generic class representing any type of operation.
Failure could be a base class that allows to model a hierarchy or errors.
- It’s better to avoid using variables that can be null, due to the complexity added when they’re validated.
- For a better error handling, instead of obtaining error codes, exceptions should be used. Another solution is to adopt a functional programming approach.