Blog Infos
Author
Published
Topics
Published

taken from https://arrow-kt.io/

 

Today I want to talk about a topic that gets relevant to think about, when working with Arrow’s Either type in combination with domain models – Validation. Putting the validation to the domain models itself can be used to ensure data integrity before it is processed and help reduce errors. Together with Either it can also provide a more organized way of dealing with exceptions, which can increase the maintainability of the code.

During the design process of domain models I always try to only allow valid objects according to the business requirements to be created. It should not be possible to create an object that is treated as invalid. One option is to move this kind of validation to a domain service, which is responsible for the creation of the domain model and afterwards trigger the validation. The problem with this approach, I always need to remember to use the service for object generation and not directly the constructor of the domain model. So for me the main place for validation, that is happening on the basis of the constructor parameters is the init — block of the domain model.

When doing exception handling by using the traditional try-catch blocks together with throws, it is just necessary to put require – functions inside an init – block. These lead to throwing an IllegalArgumentException if one of the requirements is not fulfilled and as a result no object is generated.

In the following example you can see such a case:

data class User(
    val id: UUID,
    val firstName: String,
    val lastName: String,
    val birthDate: LocalDate,
    val address: Address,
    val accounts: List<Account>
) {
    init {
        require(firstName.length in MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH) {
            "Length of firstname must be between '$MINIMUM_NAME_LENGTH' and '$MAXIMUM_NAME_LENGTH'."
        }
        require(lastName.length in MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH) {
            "Length of lastname must be between '$MINIMUM_NAME_LENGTH' and '$MAXIMUM_NAME_LENGTH'."
        }
        require(birthDate.isAfter(MINIMUM_BIRTH_DATE)) {
            "Birthdate must be after '$MINIMUM_BIRTH_DATE'."
        }
        require(accounts.isNotEmpty()) {
            "There must be at minimum one account."
        }
    }

   // Additional business methods
   ...

    companion object {
        private const val MINIMUM_NAME_LENGTH = 3
        private const val MAXIMUM_NAME_LENGTH = 30
        private val MINIMUM_BIRTH_DATE = LocalDate.of(1900, 1, 1)
    }
}

With this implementation it is not possible to create an User object with invalid constructor parameter. This is an easy way to have the validation inside the domain model. It is important to mention that this only includes the validation that is happening on basis of the constructor parameter, there maybe additional validation necessary that needs external information. This kind of validation is mainly done by some kind of domain service.

There is one drawback with this version. The caller of the constructor does not necessarily know that the creation of the object can lead to throwing an exception. So if the consumer does not provide exception handling the exception may be thrown up in the call – stack until there is a place where the exception is handled. This is also related to the fact that Kotlin only has unchecked exceptions (compared to Java), that not need to be handled.

fun main() {
    val user = User(
        id = UUID.randomUUID(),
        firstName = "John",
        lastName = "Doe",
        birthDate = LocalDate.of(1899, 1, 1), // Not valid
        address = Address(
            streetName = "Main Street",
            streetNumber = "1B",
            zipCode = 79108,
            city = "Freiburg",
            countryCode = "DE"
        ),
        accounts = listOf(
            Account(
                id = UUID.randomUUID(),
                name = "Primary Account"
            )
        )
    )
    
    // Exception in thread "main" java.lang.IllegalArgumentException: Birthdate must be after '1900-01-01'.
}

This is one of the reasons that I started to use Either for handling incorrect behavior in the application flow.

Let’s have a look how the above example can be written using Either. There are multiple approaches possible.

Factory

I will start with the first potential solution using a factory to create the domain model instead of the constructor. The first step is to make the constructor private so it is no longer possible to create objects using it.

data class User private constructor(
    val id: UUID,
    val firstName: String,
    val lastName: String,
    val birthDate: LocalDate,
    val address: Address,
    val accounts: List<Account>
)

In the next step I replace the validation inside the init – block by adding a factory function to the companion object of the User class.

companion object {
       private const val MINIMUM_NAME_LENGTH = 3
       private const val MAXIMUM_NAME_LENGTH = 30
       private val MINIMUM_BIRTH_DATE = LocalDate.of(1900, 1, 1)

       fun createFrom(
           id: UUID,
           firstName: String,
           lastName: String,
           birthDate: LocalDate,
           address: Address,
           accounts: List<Account>
       ): Either<Failure, User> = either {
           ensure(firstName.length in MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH) {
               Failure.ValidationFailure(
                   "Length of firstname must be between '$MINIMUM_NAME_LENGTH' and '$MAXIMUM_NAME_LENGTH'."
               )
           }
           ensure(lastName.length in MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH) {
               Failure.ValidationFailure(
                   "Length of lastname must be between '$MINIMUM_NAME_LENGTH' and '$MAXIMUM_NAME_LENGTH'."
               )
           }
           ensure(birthDate.isAfter(MINIMUM_BIRTH_DATE)) {
               Failure.ValidationFailure("Birthdate must be after '$MINIMUM_BIRTH_DATE'.")
           }
           ensure(accounts.isNotEmpty()) {
               Failure.ValidationFailure("There must be at minimum one account.")
           }
           User(
               id = id,
               firstName = firstName,
               lastName = lastName,
               birthDate = birthDate,
               address = address,
               accounts = accounts
           )
       }
   }

The createFrom – function is returning an Either<Failure, User>. This is the signal for the consumer of the function that it is possible that an failure can occur during object creation. It is necessary to handle it. When calling the code the same way as in the try-catch exception handling example, there is no longer an exception thrown but an Either.Left returned, which is containing the failure, that is created by the ensure – block.

fun main() {
    val user = User.createFrom(
        id = UUID.randomUUID(),
        firstName = "John",
        lastName = "Doe",
        birthDate = LocalDate.of(1899, 1, 1), // Not valid
        address = Address(
            streetName = "Main Street",
            streetNumber = "1B",
            zipCode = 79108,
            city = "Freiburg",
            countryCode = "DE"
        ),
        accounts = listOf(
            Account(
                id = UUID.randomUUID(),
                name = "Primary Account"
            )
        )
    )

    // Either.Left(ValidationFailure(message=Birthdate must be after '1900-01-01'.))
}

This solution seems perfect at first sight. But having a look on the warnings that IntelliJ is showing I can see the below one:

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

By calling the copy– constructor it is still possible to create instances of the User class without triggering the validation.

This problem can be solved in different ways:

I can use a class instead of a data class for the User domain model. This makes it necessary to implement equals and hashCode by myself. IntelliJ is doing the job for me but it makes my class very verbose containing the following part, that I normally don’t want to care about. There is a library called Lombok, that can help in this case, but I’m not a big fan of it.

override fun equals(other: Any?): Boolean {
       if (this === other) return true
       if (javaClass != other?.javaClass) return false

       other as User

       if (id != other.id) return false
       if (firstName != other.firstName) return false
       if (lastName != other.lastName) return false
       if (birthDate != other.birthDate) return false
       if (address != other.address) return false
       if (accounts != other.accounts) return false

       return true
   }

   override fun hashCode(): Int {
       var result = id.hashCode()
       result = 31 * result + firstName.hashCode()
       result = 31 * result + lastName.hashCode()
       result = 31 * result + birthDate.hashCode()
       result = 31 * result + address.hashCode()
       result = 31 * result + accounts.hashCode()
       return result
   }

An other option is to hide the copy-constructor behind an interface.

I create an interface that is defining the available properties that must be provided by the implementing data class. The interface also provides a factory function inside the companion object that is creating a concrete User object by calling the createFrom– function of the UserModel.

interface User {
    val id: UUID
    val firstName: String
    val lastName: String
    val birthDate: LocalDate
    val address: Address
    val accounts: List<Account>

    companion object {
        fun createFrom(
            id: UUID,
            firstName: String,
            lastName: String,
            birthDate: LocalDate,
            address: Address,
            accounts: List<Account>
        ): Either<Failure, User {
            return UserModel.createFrom(
                id, firstName, lastName, birthDate, address, accounts
            )
        }
    }
}

The UserModel is a private data class with a private constructor. Because I again using a data class there is no need to implement equals() and hashCode() by myself.

private class UserModel private constructor(
    override val id: UUID,
    override val firstName: String,
    override val lastName: String,
    override val birthDate: LocalDate,
    override val address: Address,
    override val accounts: List<Account>
) : User

With this implementation the resulting User object is no longer leaking the copy – constructor to the outside.

Both options seems to be an overhead on code, that need to be written in order to achieve the same behavior as with the try-catch exception handling solution but with a more explicit propagation of potential failure. Let’s have a look if there are other options available…

Composition

There is also a different approach possible that is moving properties, that need validation to its own type and composing the domain model out of these custom types.

There is a firstname and a lastname property that both need to fulfill the same requirments for a name. So instead of validating them inside the User domain model I can do this in a custom type that only contains a single property. For this I can use the value class that is provided by Kotlin.

@JvmInline
value class Name private constructor(val value: String) {
    companion object {
        private const val NAME_MIN_LENGTH: Int = 3
        private const val NAME_MAX_LENGTH: Int = 20

        fun from(rawString: String): Either<Failure, Name> = either {
            ensure(rawString.length in NAME_MIN_LENGTH..NAME_MAX_LENGTH) {
                Failure.ValidationFailure(
                    "Name length must be between $NAME_MIN_LENGTH and $NAME_MAX_LENGTH but is ${rawString.length}"
                )
            }
            Name(rawString)
        }
    }
}

The same I can do for the birthdate property and also for the non-empty list for accounts.

@JvmInline
value class BirthDate private constructor(val value: LocalDate) {
    companion object {
        private val MINIMUM_BIRTH_DATE = LocalDate.of(1900, 1, 1)

        fun from(birthDate: LocalDate): Either<Failure, BirthDate> = either {
            ensure(birthDate.isAfter(MINIMUM_BIRTH_DATE)) {
                Failure.ValidationFailure("Birthdate must be after '${MINIMUM_BIRTH_DATE}'.")
            }
            BirthDate(birthDate)
        }
    }
}

@JvmInline
value class Accounts private constructor(val value: List<Account>) {
    companion object {
        fun from(accounts: List<Account>): Either<Failure, Accounts> = either {
            ensure(accounts.isNotEmpty()) {
                Failure.ValidationFailure("There must be at minimum one account.")
            }
            Accounts(accounts)
        }
    }
}

With this all the validation is no longer part of the User domain model and it can be changed back to a simple data class.

data class User(
    val id: UUID,
    val firstName: Name,
    val lastName: Name,
    val birthDate: BirthDate,
    val address: Address,
    val accounts: Accounts
)

Looks great, but the creation of the User object is no longer such easy than it was before. The creation of the NameBirthDate and Accounts type returns an Either type. So for the creation of a User I need to unwrap the results and cancel the creation of the object if an Either.Left is returned by one of them.

val firstName = Name.from("John").bind()
val lastName = Name.from("Doe").bind()
val birthDate = BirthDate.from(LocalDate.of(1899, 1, 1)).bind()
val accounts = Accounts.from(
            listOf(
                Account(
                    id = UUID.randomUUID(),
                    name = "Primary Account"
                )
            )
        ).bind()

val user = User(
   id = UUID.randomUUID(),
   firstName = firstName,
   lastName = lastName,
   birthDate = birthDate, // Not valid
   address = Address(
        streetName = "Main Street",
        streetNumber = "1B",
        zipCode = 79108,
        city = "Freiburg",
        countryCode = "DE"
    ),
   accounts = accounts
)

I move this functionality to a factory that returns an Either<Failure, User> so I can easily handle the invalid input.

Validate by Domain Service

There is a last option available that I can use for creation of of valid User domain objects. Its a solution that, as I already mentioned at the beginning, I don’t like that much because it not guarantees that it is impossible to create invalid User objects. Instead of moving the validation of the domain model to a factory function it is also possible to use a validate – function for this. Advantage of this approach is that it is still easy to create instances of the User model.

private data class User(
    val id: UUID,
    val firstName: String,
    val lastName: String,
    val birthDate: LocalDate,
    val address: Address,
    val accounts: List<Account>
){

    fun validate(): Either<Failure, Unit> = either{
        ensure(firstName.length in MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH) {
            Failure.ValidationFailure(
                "Length of firstname must be between '$MINIMUM_NAME_LENGTH' and '$MAXIMUM_NAME_LENGTH'."
            )
        }
        ensure(lastName.length in MINIMUM_NAME_LENGTH..MAXIMUM_NAME_LENGTH) {
            Failure.ValidationFailure(
                "Length of lastname must be between '$MINIMUM_NAME_LENGTH' and '$MAXIMUM_NAME_LENGTH'."
            )
        }
        ensure(birthDate.isAfter(MINIMUM_BIRTH_DATE)) {
            Failure.ValidationFailure("Birthdate must be after '$MINIMUM_BIRTH_DATE'.")
        }
        ensure(accounts.isNotEmpty()) {
            Failure.ValidationFailure("There must be at minimum one account.")
        }
    }
    
    companion object{
        private const val MINIMUM_NAME_LENGTH = 5
        private const val MAXIMUM_NAME_LENGTH = 30
        private val MINIMUM_BIRTH_DATE = LocalDate.of(1900, 1, 1)
    }
}

This can be used together with a domain service to create valid User model objects (just a simplified example):

private fun createUser(): Either<Failure, User> = either {
    val user = User(
        id = UUID.randomUUID(),
        firstName = "John",
        lastName = "Doe",
        birthDate = LocalDate.of(1899, 1, 1), // Not valid
        address = Address(
            streetName = "Main Street",
            streetNumber = "1B",
            zipCode = 79108,
            city = "Freiburg",
            countryCode = "DE"
        ),
        accounts = listOf(
            Account(
                id = UUID.randomUUID(),
                name = "Primary Account"
            )
        )
    )
    user.validate().bind()
    user
}

The big disadvantage of this solution is that the creation of User objects should only be done by using the domain service. So I always need to remember this, but still an other developer can create invalid users by directly calling the constructor of the class.

Conclusion

In todays article I explained how the validation of domain models can be done when using the Either type for the exception handling. In the world of throwing exceptions during the model object creation, I can add the validation for domain models inside the init– block of the corresponding data class by adding require – functions, that are throwing an exception in case the requirement is not fulfilled. This is an easy way. The consumer that is creating a domain object is not forced to do some exception handling at all. Only by having a look on the implementation I can see that it is possible that something can fail. This is not only the case for domain model validation but also for the whole application. Because Kotlin only knows unchecked exception it’s on the developer to handle invalid behavior correctly (often by a generic try-catch block using the Exception or Throwable type).

Switching to the exception handling using Either does change the way of dealing with exceptional behavior. I just talked about the case of creating and validating domain models, not the exception handling with Either in general. Preventing to throw exceptions and instead returning an Either changes the way how objects are created. The constructor does not allow to return an other type as the type of the class itself. So I need other strategies.

The easiest solution is to move the validation logic to a method of the domain model and call it after creation of the object. This changes nothing in the creation process of objects but makes it necessary to call the constructor and the validate method always together e.g. by a domain service. In my opinion no good solution.

There are two other options both ensuring that it is not possible to create domain models without valid data. The first one is using a factory method for creation and additionally set the constructor to private. So I’m forced to use the factory. The whole validation for the domain model is happening inside the factory method and an Either is returned. This signals to the developer calling the method that something can go wrong and the result needs to be handled in some way. Drawback of this solution – the creation of a domain model isn’t such easy I need to unwrap the Either to further work with object. The second variant is similar but instead of doing all validation in the domain model itself the domain model is composed of custom types (represented by value classes), that are all responsible for validating their own property). With this approach the creation of the domain model itself is easy but the parts it is composed of need to deal with an Either type during creation.

As a hint. My journey working with Either started recently so all you see is my current state of doing things, it’s a process of learing and revise solutions. There is no claim that there are no other ways in dealing with domain model validation and Either.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu