ed3d
ed3d

Reputation: 93

Kotlin null safety and boolean expressions

Is my code well written or is there another way do deal with Kotlin null safety inside of boolean expressions?

class AddressSpecification(private val address: Address) : Specification {
    override fun isSatisfiedBy(): Boolean {
        return (address.municipality.isNotBlank()
                && if (address.neighbourhood == null) true else address.neighbourhood.isNullOrBlank()
                && address.postalCode.isNotBlank()
                && address.stateAbbreviation.isNotBlank())
                && if (address.street.apartment == null) true else address.street.apartment > 0

    }
}

Upvotes: 1

Views: 264

Answers (3)

MaxG
MaxG

Reputation: 1077

IMO the most elegant validation of data is made with "Validator Pattern". There are a lot of versions to the pattern, but the main idea is that you use a general validation scheme for all objects. That way It is all clear and tidy. Anyone who sees the code, can understand exactly what are the validation rules.

The more validations you have to do on an object, the better fit It is to Validator pattern.

Here's an example of a validator-ish pattern I've constructed, first some general definitions:

data class Error(val reason: String, val path: String)

class Address(
    val municipality: String,
    val neighbourhood: String
)

abstract class Validator<T>(val toValidate: T) {
    abstract fun validate(): List<Error?>
}

Now, the (partial) implementation of the validator (this will show you the idea):

class AddressValidator(toValidate: Address) : Validator<Address>(toValidate) {
    override fun validate(): List<Error?> {
         return listOf(
            this::isMunicipalityValid,
            this::isNeighbourhoodValid
         )
         .asSequence()
         .map { it(toValidate) }
         .filterNotNull()
         .toList()
    }

    fun isMunicipalityValid(address: Address): Error? =
        if (address.municipality.isBlank()) Error("municipality is blank", "address.municipality")
        else null

    fun isNeighbourhoodValid(address: Address): Error? =
        if (address.neighbourhood.isBlank()) Error("neighbourhood is blank", "address.neighbourhood")
        else null

}

And now run the example:

fun main() {
    var address = Address(
        municipality = "municipality",
        neighbourhood = "neighbourhood"
    )

    println(AddressValidator(address).validate()) // => []

    var address2 = Address(
        municipality = "",
        neighbourhood = "neighbourhood"
    )

    println(AddressValidator(address2).validate()) // => [Error(reason=municipality is blank, path=address.municipality)]
}

Upvotes: 0

Tenfour04
Tenfour04

Reputation: 93581

A check like this is redundant:

if (address.neighbourhood == null) true else address.neighbourhood.isNullOrBlank()

You're first checking if it's null. If it's not null, you're checking again if it's null or blank. So you could just use the second part address.neighbourhood.isNullOrBlank()

(Also, is this what you really want? Everything else needs to exist, but the neighborhood needs to be null or blank?)

The last line can be collapsed into an logical expression, which is usually simpler to write and comprehend than if/else when you're using if/else to return a Boolean.

And finally, since everything that you're checking is a property of address, you could use the run scope function to avoid having to type it over and over.

override fun isSatisfiedBy(): Boolean {
    return address.run {
        municipality.isNotBlank()
            && neighbourhood.isNullOrBlank()
            && postalCode.isNotBlank()
            && stateAbbreviation.isNotBlank())
            && (street.apartment == null || street.apartment > 0)
    }
}

Upvotes: 0

Raman
Raman

Reputation: 19585

Your null checks can be simplified.

if (address.neighbourhood == null) true else address.neighbourhood.isNullOrBlank()

can just be:

address.neighbourhood.isNullOrBlank()

The reason that works is that Kotlin extension functions can operate on nullable receivers, and isNullOrBlank is defined this way, so there is no need to check for null in advance.

In addition:

if (address.street.apartment == null) true else address.street.apartment > 0

can be re-written as:

address.street.apartment ?: 1 > 0

which uses the Elvis operator to fall-back to 1 if apartment is null, which makes the overall expression true in that case.

Upvotes: 1

Related Questions