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