Comment effectuer la validation des entrées sans exception ni redondance

11

Lorsque j'essaie de créer une interface pour un programme spécifique, j'essaie généralement d'éviter de lever des exceptions qui dépendent d'une entrée non validée.

Donc, ce qui se produit souvent, c'est que j'ai pensé à un morceau de code comme celui-ci (ce n'est qu'un exemple pour un exemple, ne me dérange pas la fonction qu'il exécute, exemple en Java):

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, alors disons que cela evenSizeest dérivé de l'entrée utilisateur. Je ne suis donc pas sûr que ce soit égal. Mais je ne veux pas appeler cette méthode avec la possibilité qu'une exception soit levée. Je fais donc la fonction suivante:

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

mais maintenant j'ai deux contrôles qui effectuent la même validation d'entrée: l'expression dans l' ifinstruction et le contrôle explicite isEven. Duplicata, pas sympa, alors refactorisons:

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, cela l'a résolu, mais maintenant nous entrons dans la situation suivante:

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

nous avons maintenant une vérification redondante: nous savons déjà que la valeur est paire, mais nous effectuons padToEvenWithIsEventoujours la vérification des paramètres, qui retournera toujours vrai, comme nous l'avons déjà appelé cette fonction.

Maintenant, isEvenbien sûr, cela ne pose pas de problème, mais si la vérification des paramètres est plus lourde, cela peut entraîner des coûts trop élevés. En plus de cela, effectuer un appel redondant ne se sent tout simplement pas bien.

Parfois, nous pouvons contourner cela en introduisant un "type validé" ou en créant une fonction où ce problème ne peut pas se produire:

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

mais cela nécessite une réflexion intelligente et un grand refactor.

Existe-t-il un moyen (plus) générique pour éviter les appels redondants isEvenet effectuer une double vérification des paramètres? J'aimerais que la solution ne fasse pas appel padToEvenavec un paramètre non valide, déclenchant l'exception.


Sans aucune exception, je ne veux pas dire une programmation sans exception, je veux dire que l'entrée utilisateur ne déclenche pas une exception par conception, tandis que la fonction générique elle-même contient toujours la vérification des paramètres (ne serait-ce que pour se protéger contre les erreurs de programmation).

Maarten Bodewes
la source
Essayez-vous simplement de supprimer l'exception? Vous pouvez le faire en faisant une hypothèse sur la taille réelle. Par exemple, si vous passez 13, passez à 12 ou 14 et évitez simplement le contrôle. Si vous ne pouvez pas faire l'une de ces hypothèses, vous êtes bloqué avec l'exception car le paramètre est inutilisable et votre fonction ne peut pas continuer.
Robert Harvey
@RobertHarvey Cela - à mon avis - va directement à l'encontre du principe de la moindre surprise ainsi que du principe de l'échec rapide. C'est un peu comme retourner null si le paramètre d'entrée est null (et oublier ensuite de gérer le résultat correctement, bien sûr, bonjour NPE inexpliqué).
Maarten Bodewes
Ergo, l'exception. Droite?
Robert Harvey
2
Attends quoi? Vous êtes venu ici pour des conseils, non? Ce que je dis (à ma manière apparemment pas si subtile), c'est que ces exceptions sont intentionnelles, donc si vous voulez les éliminer, vous devriez probablement avoir une bonne raison. Soit dit en passant, je suis d'accord avec amon: vous ne devriez probablement pas avoir de fonction qui ne peut pas accepter des nombres impairs pour un paramètre.
Robert Harvey
1
@MaartenBodewes: Vous devez vous rappeler que la validation des entrées utilisateur padToEvenWithIsEven n'est pas effectuée. Il effectue un contrôle de validité sur son entrée pour se protéger contre les erreurs de programmation dans le code appelant. L'étendue de cette validation dépend d'une analyse coût / risque dans laquelle vous comparez le coût de la vérification au risque que la personne qui écrit le code appelant passe le mauvais paramètre.
Bart van Ingen Schenau

Réponses:

7

Dans le cas de votre exemple, la meilleure solution consiste à utiliser une fonction de remplissage plus générale; si l'appelant veut remplir une taille uniforme, il peut le vérifier lui-même.

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Si vous effectuez à plusieurs reprises la même validation sur une valeur, ou si vous souhaitez autoriser uniquement un sous-ensemble de valeurs d'un type, les microtypes / types minuscules peuvent être utiles. Pour les utilitaires à usage général comme le remplissage, ce n'est pas une bonne idée, mais si votre valeur joue un rôle particulier dans votre modèle de domaine, l'utilisation d'un type dédié au lieu de valeurs primitives peut être un grand pas en avant. Ici, vous pouvez définir:

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

Vous pouvez maintenant déclarer

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

et ne pas avoir à faire de validation interne. Pour un test aussi simple, encapsuler un int à l'intérieur d'un objet sera probablement plus cher en termes de performances d'exécution, mais l'utilisation du système de type à votre avantage peut réduire les bogues et clarifier votre conception.

L'utilisation de ces types minuscules peut même être utile même lorsqu'ils n'effectuent aucune validation, par exemple pour lever l'ambiguïté d'une chaîne représentant a FirstNamede a LastName. J'utilise fréquemment ce modèle dans des langues typées statiquement.

amon
la source
Votre deuxième fonction ne lève-t-elle pas toujours une exception dans certains cas?
Robert Harvey
1
@RobertHarvey Oui, par exemple en cas de pointeurs nuls. Mais maintenant, le contrôle principal - que le numéro est pair - est forcé de quitter la fonction sous la responsabilité de l'appelant. Ils peuvent alors traiter l'exception d'une manière qu'ils jugent appropriée. Je pense que la réponse se concentre moins sur l'élimination de toutes les exceptions que sur l'élimination de la duplication de code dans le code de validation.
amon
1
Très bonne réponse. Bien sûr, en Java, la pénalité pour la création de classes de données est assez élevée (beaucoup de code supplémentaire) mais c'est plus un problème de langage que l'idée que vous avez présentée. Je suppose que vous n'utiliseriez pas cette solution pour tous les cas d'utilisation où mon problème se produirait, mais c'est une bonne solution contre la surutilisation des primitives ou pour les cas de bord où le coût de la vérification des paramètres est exceptionnellement difficile.
Maarten Bodewes
1
@MaartenBodewes Si vous voulez faire une validation, vous ne pouvez pas vous débarrasser de quelque chose comme des exceptions. Eh bien, l'alternative est d'utiliser une fonction statique qui retourne un objet validé ou null en cas d'échec, mais cela n'a fondamentalement aucun avantage. Mais en déplaçant la validation de la fonction dans le constructeur, nous pouvons maintenant appeler la fonction sans possibilité d'erreur de validation. Cela donne à l'appelant tout le contrôle. Par exemple:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon
1
@MaartenBodewes La validation en double se produit tout le temps. Par exemple, avant d'essayer de fermer une porte, vous pouvez vérifier si elle est déjà fermée, mais la porte elle-même ne permettra pas de la refermer si elle l'est déjà. Il n'y a tout simplement aucun moyen de s'en sortir, sauf si vous avez toujours confiance que l'appelant n'effectue aucune opération non valide. Dans votre cas, vous pouvez avoir une unsafePadStringToEvenopération qui n'effectue aucune vérification, mais cela semble être une mauvaise idée juste pour éviter la validation.
plalx
9

En tant qu'extension de la réponse de @ amon, nous pouvons combiner la sienne EvenIntegeravec ce que la communauté de programmation fonctionnelle pourrait appeler un "constructeur intelligent" - une fonction qui enveloppe le constructeur stupide et s'assure que l'objet est dans un état valide (nous rendons le muet classe constructeur ou module / package dans des langages non basés sur la classe privé pour vous assurer que seuls les constructeurs intelligents sont utilisés). L'astuce consiste à renvoyer un Optional(ou équivalent) pour rendre la fonction plus composable.

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

On peut alors facilement utiliser des Optionalméthodes standard pour écrire la logique d'entrée:

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

Vous pouvez également écrire keepAsking()dans un style Java plus idiomatique avec une boucle do-while.

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

Ensuite, dans le reste de votre code, vous pouvez compter EvenIntegeravec certitude qu'il sera vraiment pair et notre chèque pair n'a été écrit qu'une seule fois, en EvenInteger::of.

walpen
la source
Facultatif en général représente assez bien les données potentiellement invalides. Ceci est analogue à une pléthore de méthodes TryParse, qui renvoient à la fois des données et un indicateur indiquant la validité des entrées. Avec vérification du temps de conformité.
Basilevs
1

Faire la validation deux fois est un problème si le résultat de la validation est le même ET si la validation est effectuée dans la même classe. Ce n'est pas votre exemple. Dans votre code refactorisé, la première vérification isEven qui est effectuée est une validation d'entrée, un échec entraîne la demande d'une nouvelle entrée. La deuxième vérification est complètement indépendante de la première, car elle se trouve sur une méthode publique padToEvenWithEven qui peut être appelée de l'extérieur de la classe et a un résultat différent (l'exception).

Votre problème est similaire au problème de code accidentellement identique qui est confondu pour non sec. Vous confondez l'implémentation et la conception. Ce ne sont pas les mêmes, et le simple fait que vous ayez une ou une douzaine de lignes identiques ne signifie pas qu'elles servent le même but et peuvent toujours être considérées comme interchangeables. De plus, il est probable que votre classe en fasse trop, mais sautez cela car il ne s'agit probablement que d'un exemple de jouet ...

S'il s'agit d'un problème de performances, vous pouvez résoudre le problème en créant une méthode privée, qui ne fait aucune validation, que votre padToEvenWithEven public a appelée après avoir effectué sa validation et que votre autre méthode appellerait à la place. Si ce n'est pas un problème de performances, laissez vos différentes méthodes effectuer les vérifications dont elles ont besoin pour effectuer les tâches qui leur sont assignées.

jmoreno
la source
OP a déclaré que la validation d'entrée est effectuée pour empêcher le lancement de la fonction. Les contrôles sont donc totalement dépendants et intentionnellement les mêmes.
D Drmmr
@DDrmmr: non, ils ne sont pas dépendants. La fonction lance parce que cela fait partie de son contrat. Comme je l'ai dit, la réponse est de créer une méthode privée qui fait tout sauf lever l'exception et que la méthode publique appelle la méthode privée. La méthode publique conserve la vérification, où elle sert un objectif différent - pour gérer les entrées non validées.
jmoreno