LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 47: Breach of non-performance

The original article was published on October 24, 2024.

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Breach of non-performance

Let's assume that a data model meaning User is defined as follows.

data class UserModel(
    val id: Int,
    val name: String,
    val profileImageUri: Uri?,
    val birthDate: Date?,
    ...
)

Here, let's assume that it is necessary to create a Null Object that represents an "invalid user" as UserModel(0, "", null, null, ...). (In reality, whether or not to create a Null Object should be carefully considered, but here we assume it is unavoidable due to the historical background of the product. Naturally, definitions such as "if id is 0, then the user is invalid" should be avoided if possible. For more on Null Object, please refer to No smoke without null.)

Furthermore, to easily create a Null Object, default parameters are defined as follows.

data class UserModel(
    val id: Int = 0, // 0 represents an invalid user
    val name: String = "",
    val profileImageUri: Uri? = null,
    val birthDate: Date? = null,
    ...
)

In Kotlin and many other languages, default parameters can be used, and if actual arguments are omitted, default parameters are used. With these default parameters, a Null Object of UserModel can be created as follows.

val invalidUserModel = UserModel()

val isInvalid = userModel == invalidUserModel

Is there any problem with this definition of UserModel?

Fulfill what can be fulfilled

Basically, it is better to avoid using default parameters to create a Null Object. In this case, there is a possibility that an instance of UserModel like the following may be mistakenly created.

val user = UserModel(
    name = "John Doe",
    profileImageUri = imageUrl,
    birthDate = date,
    ...
)

If you forget to specify id like this, the id of that user will be considered "invalid". On the other hand, the result of user == UserModel() will be false, which could cause bugs. The cause of such a situation is that the default parameter value id = 0 is not a widely used value, but a value with a special meaning.

If a Null Object is necessary, it should be created without using default parameters, as follows.

data class UserModel(
    val id: Int,
    val name: String,
    val profileImageUri: Uri?,
    val birthDate: Date?,
    ...
) {
    companion object {
        val INVALID = UserModel(0, "", null, null)
    }
}

Although it is off-topic, there is also a way to represent a Null Object with a sum type. This is particularly effective when it is necessary to distinguish Invalid from a normal UserModel.

sealed interface UserModel {
    data class Valid(
        val id: Int,
        val name: String,
        ...
    ) : UserModel

    data object Invalid : UserModel
}

Also, in languages where null can be distinguished by type checking, using null directly instead of a Null Object is also an option.

val userModel: UserModel? = ... // null represents an invalid user.

In any case, the value used for default parameters should be a widely used general value, and special-purpose values should not be specified. When using default parameters, check whether "not providing a value" is a common situation and whether it is easy to imagine what will happen if a value is not provided.

In a nutshell

The value used for default parameters should be a commonly used "normal" value, and special values should not be used.

Keywords: default parameter, null object, special value

List of articles on techniques for improving code quality