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 evenSize
est 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' if
instruction 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 padToEvenWithIsEven
toujours la vérification des paramètres, qui retournera toujours vrai, comme nous l'avons déjà appelé cette fonction.
Maintenant, isEven
bien 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 isEven
et effectuer une double vérification des paramètres? J'aimerais que la solution ne fasse pas appel padToEven
avec 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).
la source
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.Réponses:
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.
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:
Vous pouvez maintenant déclarer
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
FirstName
de aLastName
. J'utilise fréquemment ce modèle dans des langues typées statiquement.la source
EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
unsafePadStringToEven
opération qui n'effectue aucune vérification, mais cela semble être une mauvaise idée juste pour éviter la validation.En tant qu'extension de la réponse de @ amon, nous pouvons combiner la sienne
EvenInteger
avec 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 unOptional
(ou équivalent) pour rendre la fonction plus composable.On peut alors facilement utiliser des
Optional
méthodes standard pour écrire la logique d'entrée:Vous pouvez également écrire
keepAsking()
dans un style Java plus idiomatique avec une boucle do-while.Ensuite, dans le reste de votre code, vous pouvez compter
EvenInteger
avec certitude qu'il sera vraiment pair et notre chèque pair n'a été écrit qu'une seule fois, enEvenInteger::of
.la source
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.
la source