Une méthode doit-elle être indulgente avec les arguments transmis? [fermé]

21

Supposons que nous ayons une méthode foo(String bar)qui ne fonctionne que sur des chaînes qui répondent à certains critères; par exemple, il doit être en minuscules, ne doit pas être vide ou ne comporter que des espaces, et doit correspondre au modèle [a-z0-9-_./@]+. La documentation de la méthode énonce ces critères.

La méthode devrait-elle rejeter tout écart par rapport à ces critères, ou la méthode devrait-elle être plus indulgente à l'égard de certains critères? Par exemple, si la méthode initiale est

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
    }
    this.bar = bar;
}

Et la deuxième méthode de pardon est

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        bar = bar.toLowerCase().trim().replaceAll(" ", "_");
        if (!bar.matches(BAR_PATTERN_STRING) {
            throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
        }
    }
    this.bar = bar;
}

La documentation doit-elle être modifiée pour indiquer qu'elle sera transformée et définie si possible sur la valeur transformée, ou la méthode doit-elle être aussi simple que possible et rejeter toutes les déviations? Dans ce cas, barpourrait être défini par l'utilisateur d'une application.

Le cas d'utilisation principal serait que les utilisateurs accèdent aux objets à partir d'un référentiel par un identificateur de chaîne spécifique. Chaque objet du référentiel doit avoir une chaîne unique pour l'identifier. Ces référentiels pouvaient stocker les objets de différentes manières (serveur sql, json, xml, binaire, etc.) et j'ai donc essayé d'identifier le plus petit dénominateur commun qui correspondrait à la plupart des conventions de dénomination.

Zymus
la source
1
Cela dépend probablement fortement de votre cas d'utilisation. L'une ou l'autre pourrait être raisonnable, et j'ai même vu des classes qui fournissent les deux méthodes et font décider l'utilisateur. Pourriez-vous nous expliquer ce que cette méthode / classe / champ est censé faire, afin que nous puissions offrir de vrais conseils?
Ixrec
1
Connaissez-vous tous ceux qui appellent la méthode? Comme dans, si vous le changez, pouvez-vous identifier de manière fiable tous les clients? Si c'est le cas, j'irais avec autant de permissivité et de pardon que les préoccupations de performance le permettent. Je pourrais également supprimer la documentation. Sinon, et cela fait partie d'une API de bibliothèque, je m'assurerais que le code implémente exactement l'API annoncée, sinon changer le code pour correspondre à la documentation à l'avenir est susceptible de générer des rapports de bogues.
Jon Chesterfield
7
Vous pourriez faire valoir que la séparation des préoccupations dit que si nécessaire, vous devez avoir une foofonction stricte qui est rigoureuse dans les arguments qu'il accepte, et avoir une deuxième fonction d'aide qui peut essayer de "nettoyer" un argument à utiliser foo. De cette façon, chaque méthode a moins à faire seule et peut être gérée et intégrée plus proprement. Si vous vous engagez dans cette voie, il serait probablement également utile de vous éloigner d'une conception exceptionnellement lourde; vous pouvez utiliser quelque chose comme à la Optionalplace, puis avoir les fonctions qui consomment foodes exceptions de levée si nécessaire.
gntskn
1
C'est comme demander "quelqu'un m'a fait du tort, dois-je leur pardonner?" Évidemment, il y a des circonstances où l'un ou l'autre est approprié. La programmation n'est peut-être pas aussi compliquée que les relations humaines, mais elle est certainement suffisamment complexe pour qu'une prescription générale comme celle-ci ne fonctionne pas.
Kilian Foth
2
@Boggin, je voudrais également vous indiquer le principe de robustesse reconsidéré . La difficulté survient lorsque vous devez étendre l'implémentation et l'implémentation indulgente conduit à un cas ambigu avec l'implémentation étendue.

Réponses:

47

Votre méthode doit faire ce qu'elle dit.

Cela empêche les bogues, à la fois de l'utilisation et de la modification ultérieure du comportement des responsables. Cela fait gagner du temps car les responsables n'ont pas besoin de passer autant de temps à comprendre ce qui se passe.

Cela dit, si la logique définie n'est pas conviviale, elle devrait peut-être être améliorée.

Telastyn
la source
8
Telle est la clé. Si votre méthode fait exactement ce qu'elle dit, le codeur utilisant votre méthode compensera son cas d'utilisation spécifique. Ne faites jamais quelque chose de non documenté avec la méthode simplement parce que vous pensez qu'elle est utile. Si vous devez le modifier, écrivez un conteneur ou modifiez la documentation.
Nelson
J'ajouterais au commentaire de @ Nelson que la méthode ne devrait pas être conçue dans le vide. Si les codeurs disent qu'ils vont l'utiliser mais vont compenser et que leurs compensations ont une valeur générale, envisagez de les intégrer à la classe. (Par exemple, avoir fooet fooForUncleanStringméthodes où ce dernier fait les corrections avant de le passer au premier.)
Blrfl
20

Il y a quelques points:

  1. Votre implémentation doit faire ce que stipule le contrat documenté et ne doit rien faire de plus.
  2. La simplicité est importante, à la fois pour le contrat et la mise en œuvre, mais plus pour le premier.
  3. Essayer de corriger une entrée erronée ajoute de la complexité, contre-intuitivement non seulement au contrat et à la mise en œuvre, mais aussi à l'utilisation.
  4. Les erreurs ne doivent être détectées tôt que si cela améliore la débogabilité et ne compromet pas trop l'efficacité.
    N'oubliez pas qu'il existe des assertions de débogage pour diagnostiquer les erreurs logiques en mode débogage, ce qui atténue principalement les problèmes de performances.
  5. L'efficacité, dans la mesure où le temps et l'argent disponibles le permettent sans trop compromettre la simplicité, est toujours un objectif.

Si vous implémentez une interface utilisateur, des messages d'erreur conviviaux (y compris des suggestions et d'autres aides) font partie d'une bonne conception.
Mais rappelez-vous que les API sont destinées aux programmeurs et non aux utilisateurs finaux.


Une expérience réelle d'être flou et permissif avec une entrée est le HTML.
Ce qui a amené tout le monde à le faire différemment, et la spécification, maintenant documentée, est un gigantesque tome plein de cas spéciaux.
Voir la loi de Postel (" Soyez conservateur dans ce que vous faites, soyez libéral dans ce que vous acceptez des autres. ") Et un critique qui en parle ( Ou bien mieux que MichaelT m'a fait connaître ).

Déduplicateur
la source
Une autre pièce critique de l'auteur de sendmail: The Robustness Principle Reconsidered
15

Le comportement d'une méthode doit être clair, intuitif, prévisible et simple. En général, nous devrions être très réticents à effectuer un traitement supplémentaire sur l'entrée d'un appelant. De telles suppositions sur ce que l'appelant voulait invariablement ont beaucoup de cas marginaux qui produisent un comportement indésirable. Considérez une opération aussi simple que la jonction d'un chemin de fichier. De nombreuses (ou peut-être même la plupart) des fonctions de jonction de chemin de fichier élimineront silencieusement tous les chemins précédents si l'un des chemins en cours de jonction semble être enraciné! Par exemple, /abc/xyzjoint à /evilentraînera juste /evil. Ce n'est presque jamais ce que j'ai l'intention de joindre aux chemins de fichiers, mais comme il n'y a pas d'interface qui ne se comporte pas de cette façon, je suis obligé d'avoir des bogues ou d'écrire du code supplémentaire couvrant ces cas.

Cela dit, il existe de rares occasions où il est logique qu'une méthode soit "indulgente", mais il devrait toujours être du ressort de l'appelant de décider quand et si ces étapes de traitement s'appliquent à sa situation. Ainsi, lorsque vous avez identifié une étape de prétraitement commune que vous souhaitez appliquer aux arguments dans diverses situations, vous devez exposer les interfaces pour:

  • La fonctionnalité brute, sans aucun prétraitement.
  • L'étape de prétraitement en elle-même .
  • La combinaison de la fonctionnalité brute et du prétraitement.

Le dernier est facultatif; vous ne devez le fournir que si un grand nombre d'appels l'utilisent.

L'exposition de la fonctionnalité brute donne à l'appelant la possibilité de l'utiliser sans l'étape de prétraitement quand il en a besoin. L'exposition de l'étape du préprocesseur par elle-même permet à l'appelant de l'utiliser dans des situations où il n'appelle même pas la fonction ou lorsqu'il souhaite prétraiter une entrée avant d' appeler la fonction (comme lorsqu'il souhaite d'abord la passer dans une autre fonction). Fournir la combinaison permet aux appelants d'invoquer les deux sans tracas, ce qui est utile principalement si la plupart des appelants l'utilisent de cette façon.

jpmc26
la source
2
+1 pour prévisible. Et un autre +1 (je souhaite) pour simple. Je préférerais de beaucoup que vous m'aidiez à repérer et à corriger mes erreurs plutôt que de les cacher.
John M Gant
4

Comme d'autres l'ont dit, rendre la correspondance de chaîne «indulgente» signifie introduire une complexité supplémentaire. Cela signifie plus de travail dans la mise en œuvre de l'appariement. Vous avez maintenant beaucoup plus de cas de test, par exemple. Vous devez effectuer un travail supplémentaire pour vous assurer qu'il n'y a pas de noms sémantiquement égaux dans l'espace de noms. Plus de complexité signifie également qu'il y a plus à se tromper à l'avenir. Un mécanisme plus simple, comme un vélo, nécessite moins d'entretien qu'un mécanisme plus complexe, comme une voiture.

La correspondance de chaîne indulgente vaut-elle donc tout ce coût supplémentaire? Cela dépend du cas d'utilisation, comme d'autres l'ont noté. Si les chaînes sont une sorte d'entrée externe vous ne contrôlez pas, et il y a un avantage certain à la correspondance clémente, il pourrait valoir la peine. Peut-être que les informations proviennent d'utilisateurs finaux qui ne sont peut-être pas très consciencieux sur les caractères de l'espace et la capitalisation, et vous êtes fortement incité à rendre votre produit plus facile à utiliser.

D'un autre côté, si l'entrée provenait, disons, de fichiers de propriétés assemblés par des techniciens, qui devraient comprendre cela "Fred Mertz" != "FredMertz", je serais plus enclin à rendre la correspondance plus stricte et à économiser les coûts de développement.

Je pense que dans tous les cas, il est utile de rogner et d'ignorer les espaces de début et de fin, cependant - j'ai vu trop d'heures gaspillées à déboguer ce genre de problèmes.

mat_noshi
la source
3

Vous mentionnez une partie du contexte d'où vient cette question.

Étant donné que, je voudrais que la méthode fasse juste une chose, elle affirme les exigences sur la chaîne, laissez-la s'exécuter en fonction de cela - je n'essaierais pas de la transformer ici. Restez simple et clair; documentez- le et essayez de synchroniser la documentation et le code.

Si vous souhaitez transformer les données provenant de la base de données utilisateur d'une manière plus indulgente, mettez cette fonctionnalité dans une méthode de transformation distincte et documentez la fonctionnalité qui la lie .

À un moment donné, les exigences de la fonction doivent être mesurées, clairement documentées et l'exécution doit se poursuivre. Le «pardon», à son sens, est un peu muet, c'est une décision de conception et je dirais que la fonction ne mute pas son argument. Le fait que la fonction mute l'entrée masque une partie de la validation qui serait requise du client. Avoir une fonction qui fait la mutation aide le client à bien faire les choses.

La grande importance ici est la clarté et la documentation de ce que fait le code .

Niall
la source
-1
  1. Vous pouvez nommer une méthode en fonction de l'action comme doSomething (), takeBackUp ().
  2. Pour faciliter la maintenance, vous pouvez conserver les contrats communs et la validation sur différentes procédures. Appelez-les selon les cas d'utilisation.
  3. Programmation défensive: votre procédure gère une large gamme d'entrées, y compris (les choses minimales qui sont des cas d'utilisation doivent être couvertes de toute façon)
user435491
la source