0

I've a question about, how would you handle this case? Imagine that you have to do a validation of an object and that validation should have a sort of importance, in this case we only have 3 validations, each one can result Valid or his own QualityCheck enum value.

This is the method example in kotlin and the validations

 sealed class Validation {

        abstract fun validate(bobject: ObjectToCheck): QualityCheck

        object VeryImportantValidation : Validation() {
            override fun validate(bobject: ObjectToCheck): QualityCheck =
                if (isValid(bobject.valueX)) QualityCheck.Valid
                else QualityCheck.VeryImportantInvalid
        }

        object SecondMostImportant : Validation() {
            override fun validate(bobject: ObjectToCheck): QualityCheck =
                if (isValid(bobject.valueNotSoImportant)) QualityCheck.Valid
                else QualityCheck.SecondMostImportantInvalid
        }

       object NotSoImportant : Validation() {
            override fun validate(bobject: ObjectToCheck): QualityCheck =
                if (isValid(bobject.valueNothingImportant)) QualityCheck.Valid
                else QualityCheck.NotSoImportantInvalid
        }
    }

    fun getQualityCheck(object: ObjectToCheck): QualityCheck =
         if (VeryImportantValidation.validate(object) === QualityCheck.Valid) {
            if (SecondMostImportant.validate(object) === QualityCheck.Valid) {
                    NotSoImportant(paymentsRepository.getSystemPayments()).validate(object)
             } else {
                    QualityCheck.SecondMostImportantInvalid
            }
          } else {
                QualityCheck.VeryImportantInvalid
          }

I think this is not scalable neither easy to read/understand or modify if we would want to add a new one.

There is any kind to do this elegant and easier to include more validations?

AlexT
  • 2,524
  • 1
  • 11
  • 23
colymore
  • 11,776
  • 13
  • 48
  • 90

3 Answers3

0

Validation like this is a perfect candidate for the "Rules engine pattern"... mostly known as a for loop.

You just set up a List<Validation> with all of the validations you want to run and iterate over them calling the validate method. You have 2 options, collect all errors (doing a fold on the list), or stop the loop after the first error with a asSequence().map().takeWhile().

I forgot to say, you don't need to seal the Validation class. What is your intent with that?

Augusto
  • 28,839
  • 5
  • 58
  • 88
0

If you invert your Boolean conditions, you can eliminate the nesting. Then you can change it to a when statement for simplicity:

fun getQualityCheck(object: ObjectToCheck): QualityCheck = when {
    VeryImportantValidation.validate(object) !== QualityCheck.Valid -> 
        QualityCheck.VeryImportantInvalid
    SecondMostImportant.validate(object) !== QualityCheck.Valid -> 
        QualityCheck.SecondMostImportantInvalid
    else -> 
        NotSoImportant(paymentsRepository.getSystemPayments()).validate(object)
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
0

Scalability/Extensibility would depend from situation to situation and a code cannot be open to all types of changes. One rule of thumb is to keep it as simple as possible and when a requirement is changed we ensure that the code is open to such kind of changes.

Also, I agree with @Augusto. Your use of the sealed class is not really how it is intended to be used.

Anyways let's look at how it would be easier to add a new validation, change the severity of the violation, or have several validations with the same severity.

Lets define an interface for Validations.

interface Validation {
    fun validate(value: Int): Boolean
}

Now let's define a few Validations

class LimitValidation: Validation{
    override fun validate(value: Int) = value < 100
}

class PositiveValidation: Validation {
    override fun validate(value: Int) = value > 0
}

class EvenValidation: Validation {
    override fun validate(value: Int) = value % 2 == 0
}

Let's say you have the following Violations

enum class Violation {
    SEVERE,
    MODERATE,
    TYPICAL
}

We can make use of sealed class to define the quality.

sealed class Quality {
    object High : Quality()
    data class Low(val violation: Violation) : Quality()
}

We can create a class responsible for checking the Quality.

class QualityEvaluator {

    private val violationMap: MutableMap<KClass<*>, Violation> = mutableMapOf()

    init {
        violationMap[LimitValidation::class] = Violation.SEVERE
        violationMap[PositiveValidation::class] = Violation.MODERATE
        violationMap[EvenValidation::class] = Violation.TYPICAL
    }

    fun evaluateQuality(value: Int, validations: List<Validation>) : Quality {
        val sortedValidations = validations.sortedBy(::violationFor)
        
        sortedValidations.forEach {
             if(!it.validate(value)) {
                 return Quality.Low(violationFor(it))
            }
        }
        
        return Quality.High
    }
    
    private fun <T: Validation> violationFor(validation: T): Violation {
        return if (violationMap.containsKey(validation::class)) {
            requireNotNull(violationMap[validation::class])
        } else {
            Violation.TYPICAL
        }
    }
}

Finally, we can use all this like so:

val validations = listOf(LimitValidation(), PositiveValidation(), EvenValidation())

when(val quality = QualityEvaluator().evaluateQuality(8, validations)) {
    is Quality.High -> println("Quality is High")
    is Quality.Low -> println("Quality is Low. Violation: ${quality.violation}")
}
Xid
  • 4,578
  • 2
  • 13
  • 35