Since Kotlin 1.5, the long time awaited inline value classes have finally become stable. They were first introduced in Kotlin 1.2.30 as an experimental feature.
Some of Kotlin’s classes are already very popular like data class
and sealed class
.
The new value class
comes to solve a different problem.
What are inline classes?
Kotlin has a variety of class types: class
, data class
, sealed class
, enum class
and the “new” value class
.
Value classes are value-based classes that wrap primitive values.
As of now, Kotlin value classes only support wrapping one single primitive value.
Value classes provide type safety. However, wrapping a primitive type can be very costly in terms of performance.
These type of classes are called inline classes because the compiler will “unbox” them to directly use the underlying type for performance improvement.
How are they different from type aliases?
Type aliases introduce an “alias” for an existing type.
This means that a Name
type alias is just another way to call the type String
.
Therefore, we could assign a String
value to a Name
type alias.
However, value classes introduce a new type and cannot be assigned.
A Name
type will not be the same type as a String
.
The Kotlin documentation has a nice example.
Domain-Driven Design
First, let us rewind and talk about Domain-Driven Design and its role in order to have a better understanding of the “value” (pun intended 😜) that value classes bring.
Domain-Driven Design A.K.A. DDD is a software development approach that was first introduced by Eric Evans on his well known book Domain-Driven Design: Tackling Complexity in the Heart of Software.
This approach claims that the software’s structure and the language it uses should be deeply connected to the business domain. For example, if a software manages a cargo fleet of vessels, there will be concepts like “voyage”, “leg”, “vessel”, “load” or “distance” represented by classes and methods.
In DDD, there are multiple concepts to represent domain models. This time we will talk about Value objects. Let us see their main features.
No identity. Value equality
Value objects do not have an identity. Therefore, two objects with the same set of property values are considered equal.
For example, if we have two books with the same title, author, editor, etc., they are the same book and we cannot distinguish them. Within our software system they will be value objects.
They are immutable
Once a value object is created, it cannot change. The only way to change its value is by creating a new object. Therefore, classes representing value objects must not have setters.
Validation
The fact that a value object exists should be proof that it contains valid data. This means that before creating an instance, a value object must verify its validity.
For example, when creating a value object representing an e-mail address, if the e-mail address is not valid, the value object will not be created and an error may be thrown.
What are the benefits of using value objects?
Type safety
A common code smell is Primitive Obsession, where primitive data types are used to represent domain models. For example, we use a Long to represent a timestamp or a String to represent an e-mail.
Because of this, a common flaw is that, there may be multiple properties of the same type:
class Person(
val firstName: String,
val lastName: String,
val age: Int,
val siblings: Int,
...
)
or we can make mistaked by mixing up arguments on a method:
fun setPersonData(firstName: String, lastName: String, age: Int, siblings: Int)...
setPersonData("Smith", "John", 0, 24)
In Kotlin we could use named parameters
to prevent easy mistakes, but mistakes can still happen.
How would it look if we use value objects?
class Person(
val firstName: FirstName,
val lastName: LastName,
val age: Age,
val siblings: Siblings,
...
)
and method calls cannot be mixed up anymore:
fun setPersonData(firstName: FirstName, lastName: LastName, age: Age, siblings: Siblings)...
setPersonData("Smith", "John", 0, 24) // This is not be possible anymore
// instead it will be:
setPersonData(
FirstName("John"),
LastName("Smith"),
Age(24),
Siblings(0),
)
This types make our code easier to read too!
Validation
As mentioned before:
The fact that a value object exists should be proof that it contains valid data.
Therefore, an Age
object will never contain a negative value and a FirstName
or LastName
will never be empty or blank.
This brings tremendous benefits to our codebase as we only need to check validity of data upon object creation and invalid objects are simply impossible to exist (unless we have a bug 🤷♂️).
Flexibility
Another big benefit that is not so obvious is the fact that value objects provide us flexibility.
Imagine we have a userId
represented as an integer.
If we were to change the type from an Int in Kotlin to a String (to use UUID, for instance), we would need to change it everywhere it is used, having great impact in our codebase.
Using a value object, a userId
of type UserId
will always be the same, regardless of the underlying primitive type.
We could switch from an integer to a string with minimum impact to our codebase.
Readability
At this point, it goes without saying that readability improves as:
fun sendMessage(email: Email, subject: Subject, message: Message)
is easier to read than:
fun sendMessage(email: String, subject: String, message: String)
What about the drawbacks?
Obviously, creating a value object for every single primitive can easily bloat our codebase. Therefore, it is probably smart to use them at the most critical places.
In Kotlin and other programming languages for instance, serialization of non-primitive types comes with some overhead.
We usually have to provide a TypeAdapter
so that our serialization tool knows how to handle our own types.
In this case, it is more pragmatic to convert our value objects to their primitive counterparts in the Data Trasfer Objects (DTOs).
For example, a userId
will be of type UserId
across our whole domain, but when declaring a UserDto
to send to an API or to store in a database, we could declare it as a String
.
How can I start using value classes?
To declare a value class
we can do it like this:
value class UserId(val value: String)
If we are using Kotlin in the JVM, we have to use the @JvmInline
annotation before the class declaration.
@JvmInline
value class UserId(val value: String)
The reason why we need to use the @JvmInline
annotation is that Kotlin wants to provide value classes
as a feature early on and by adding this annotation it will compile to primitive JVM Value Types whenever Project Valhalla is released.
What can we use them for?
Since value classes
support some functionality from regular classes, we can take advantage of this to enforce them as value objects from the DDD perspective.
In the init
block we can validate the value so that the object is never created with invalid data.
As an example:
@JvmInline
value class Age(
val value: Int,
) {
init {
require(value >= 0) { "age must be >= 0: '$value'" }
}
}
In this case, the Age
object will always represent a positive number.
Trying to create an object with a negative value will throw an IllegalArgumentException
.
Additionally, we can have meaningful error messages that directly point to the cause of error.
As we can see, we are passing the error message to use in the lambda parameter of the require
method.
Another example where we can have extra validation is when representing a country code:
@JvmInline
value class CountryCode(
val value: String,
) {
init {
require(value.length == 2) { "Country code must be 2 characters" }
val validCountryCodeValue = Locale.getISOCountries()
.firstOrNull { it.equals(value, ignoreCase = true) }
requireNotNull(validCountryCodeValue) { "Invalid country code: '$value'" }
}
}
In this case we make sure that the code has only 2 characters and that it is a real country code.
Because we have two require
verifications, we can write two different messages which will point to the specific issue.
The extra mile
Kotlin is great programming language. In my opinion it offers a very developer-friendly API.
Like many other languages, Kotlin allows us to overload operators.
One of this operators is the less known invoke
operator.
Objects with an invoke
method can be called as a function.
With this, there is a way to create a factory method using the invoke
operator and make it look like a normal constructor for the caller.
Because the companion object is a special object in Kotlin, we can overload its invoke
operator.
How can we use it in our value class
to make them more powerful and go the extra mile?
@JvmInline
value class CountryCode private constructor(
val value: String,
) {
init {
require(value.length == 2) { "Country code must be 2 characters" }
val validCountryCodeValue = Locale.getISOCountries()
.firstOrNull { it == value }
requireNotNull(validCountryCodeValue) { "Invalid country code: '$value'" }
}
companion object {
operator fun invoke(value: String?): CountryCode {
requireNotNull(value) { "Country code cannot be null" }
return CountryCode(value.uppercase())
}
}
}
In this example, we want to expose a factory method that accepts a nullable type and checks if it is null.
Why do we want to do this?
Because we want to just call the factory method with whatever value we are provided without a need for null checks or ?
in the call chain.
Also because we want to isolate the logic that knows what to do if we want to create this class with a null
value.
In the example we also modify the value before we create an instance of the CountryCode value class
.
Modifying val
values during object creation is not possible when calling regular constructors.
In this case we want our underlying value to contain only uppercase letters.
We can also notice that the primary constructor is now private
.
How would it look now when we create an instance of CountryCode?
val countryCode = CountryCode("es")
It looks the same!
This is because Kotlin allows us to omit the name of the companion object
and invoke
method.
This is a list of calls to our factory method; they are all equivalent:
// Method 1
CountryCode.Companion.invoke("es")
// Method 2
CountryCode.Companion("es")
// Method 3
CountryCode("es")
What do we have now?
From now on in our codebase, whenever we have an instance of CountryCode, we know that:
- It is valid
- It is a 2-chars country code
- It is uppercase
Summary
Value classes are a very nice and practical addition to our Kotlin toolbox. They provide stronger type safety, validation capabilities, readability and flexibility.
Thank you JetBrains for the Value Classes in Kotlin!
We have also seen how we can provide a factory method using the invoke
operator and go the extra mile.
Like every tool, we must use it for the right problem. So we should use them carefully, otherwise we risk bloating our codebase.
Personally, I like to use them whenever I have a type that I want to validate and to remove ambiguity and prevent mistakes.
Thanks for reading!
References
If you found this article interesting, share it!