On se retrouve aujourd'hui pour la solution du précédent #KataOfTheWeek proposé par Loïc en début de semaine !

Evidemment, il y a plein de manières de répondre à notre ami Pato.

Vous auriez pu faire une méthode de 50 lignes avec toutes ces conditions dans un seul validateur, ou plusieurs méthodes statiques dans une classe ValidationUtils, que vous auriez appelé dans l'ordre. Bref un truc du genre :

class Validation {

public static boolean checkValidDescription(Article a) {
  // CODE ICI...
}

public static boolean isFromDuckInc(Article a) {
  // CODE ICI...
}


class Validator {
  public Result validate(Article a) {
    if (!checkValidDescription(Article a)) {
        return Result.create(false, "wrong-description");
    }
    // ...
  }

}

}

Si vous avez fait ça… Franchement, lisez la suite !

On aurait aussi pu considérer que notre use-case est parfait pour implémenter une spécification. Sauf que se faire un design pattern composite propre en créant tout le boilerplate pour gérer les compositions (et / ou / non etc…)… Alors qu'on est juste en train de filtrer des canards en plastique… Franchement, overkill, et puis le monde souffre suffisamment avec Criteria pour qu'on n'ait pas forcément envie d'augmenter l'entropie de l'univers avec une API conçue sur un bout de feuille après 2 pintes…

Je tiens donc à m'excuser auprès des aficionados de boolean isSatisfiedBy(...), mais la personne qui vous interview en entretien technique me remerciera.

Ici, on va plutôt chercher la simplicité (merci KISS), avec le trade-off suivant: chaque "règle" sera susceptible de concerner plusieurs champs. On va jouter donc avec le design pattern Strategy, et privilégier donc une validation des règles "métier".

Ici, j'ai découpé la spec en 5 "règles" :

  • Une règle qui s'assure de l'honnêteté des vendeurs par rapport à mes critères métier (c'était ma blague nulle de Western Union et sur Kévin. Je sais. C'est cheap.).
  • Une règle plus complexe sur les prix de vente
  • Une règle sur la marque du canard
  • Une règle sur la condition des articles
  • Une règle sur les moyens de livraison

On va créer une jolie petite interface :

public interface ValidationRule {
  fun validate(a: Article, errors: MutableList<String>)
}

Et on va se créer nos 5 petites implems (j'ai mis des magic numbers pour ne pas prendre 2 pages, mais vous voyez l'idée ^^.

import java.math.BigDecimal

class SellerTrustworthinessValidator : ValidationRule {
    override fun validate(a: Article, errors: MutableList<String>) {
        val couldBeAKevin = a.sellerName.containsIgnoreCase("Kevin") || a.email.containsIgnoreCase("Kevin")
        val couldBeAScam = a.description.containsIgnoreCase("Western Union")
        if (couldBeAKevin || couldBeAScam) {
            errors.add("seller_not_trustworthy")
        }
    }
}

class SalePriceValidator : ValidationRule {
    override fun validate(a: Article, errors: MutableList<String>) {
        if (a.price <= BigDecimal.valueOf(10.0)) {
            return
        }
        if (a.condition == "with-blister" && a.price <= BigDecimal.valueOf(35.0)) {
            return
        }
        if (a.ref < 100 && a.price <= BigDecimal.valueOf(120.0)) {
            return
        }
        errors.add("price_too_expensive")
    }
}

class MakeValidator : ValidationRule {
    override fun validate(a: Article, errors: MutableList<String>) {
        if (a.make != "DuckInc") {
            errors.add("wrong_make")
        }
    }
}

class ConditionValidator : ValidationRule {
    override fun validate(a: Article, errors: MutableList<String>) {
        val isGoodCondition = a.condition == "with-blister" || a.condition == "good-as-new"
        if (!isGoodCondition) {
            errors.add("bad_condition")
        }
    }
}

class DuckDeliveryValidator : ValidationRule {
    override fun validate(a: Article, errors: MutableList<String>) {
        val isShippable = a.deliveryMethods.filter { m -> m == "delivery" || m == "relay" }.isNotEmpty()
        if (isShippable) {
            return
        }
        val isSameDepartment = a.zipCode.startsWith("75")
        if (isSameDepartment) {
            return
        }
        errors.add("wrong_delivery_conditions")
    }
}


private fun String.containsIgnoreCase(s: String): Boolean {
    return this.contains(s, true)
}

Voici nos petites data classes

import java.math.BigDecimal

data class Article(val title: String,
                   val description: String,
                   val price: BigDecimal,
                   val deliveryMethods: List<String>,
                   val ref: Integer,
                   val make: String,
                   val condition: String,
                   val email: String,
                   val sellerName: String,
                   val zipCode: String)


data class Result(val valid: Boolean, val reasons: List<String>) {
    companion object {
        fun create(valid: Boolean, reasons: List<String>): Result {
            return Result(valid, reasons)
        }
    }
}

On peut même mettre les règles ensembles pour tout avoir au même endroit, avec la création d'un BigAssValidator (all rights reserved)

class BigAssValidator : ValidationRule {

    private val validators: List<ValidationRule> = listOf(
        ConditionValidator(),
        DuckDeliveryValidator(),
        MakeValidator(),
        SalePriceValidator(),
        SellerTrustworthinessValidator()
    )

    override fun validate(a: Article, errors: MutableList<String>) {
        validators.forEach { v -> v.validate(a, errors) }
    }

}
class Main {

    fun validate(a: Article): Result {
        val reasons = ArrayList<String>()
        BigAssValidator().validate(a, reasons)
        return Result.create(reasons.isEmpty(), reasons)
    }

}

Evidemment, ce n'est qu'une solution parmi d'autres.

A bientôt pour un nouveau #KataOfTheWeek !