Paramètre pour contrôler s'il faut lever une exception ou retourner null - bonne pratique?


24

Je rencontre souvent des méthodes / fonctions qui ont un paramètre booléen supplémentaire qui contrôle si une exception est levée en cas d'échec, ou si null est renvoyé.

Il y a déjà des discussions sur lequel est le meilleur choix dans quel cas, alors ne nous concentrons pas sur cela ici. Voir par exemple Retourner une valeur magique, lancer une exception ou retourner faux en cas d'échec?

Supposons plutôt qu'il existe une bonne raison pour laquelle nous voulons soutenir les deux.

Personnellement, je pense qu'une telle méthode devrait plutôt être divisée en deux: l'une qui lève une exception en cas d'échec, l'autre qui renvoie null en cas d'échec.

Alors, quel est le meilleur?

R: Une méthode avec $exception_on_failureparamètre.

/**
 * @param int $id
 * @param bool $exception_on_failure
 *
 * @return Item|null
 *   The item, or null if not found and $exception_on_failure is false.
 * @throws NoSuchItemException
 *   Thrown if item not found, and $exception_on_failure is true.
 */
function loadItem(int $id, bool $exception_on_failure): ?Item;

B: Deux méthodes distinctes.

/**
 * @param int $id
 *
 * @return Item|null
 *   The item, or null if not found.
 */
function loadItemOrNull(int $id): ?Item;

/**
 * @param int $id
 *
 * @return Item
 *   The item, if found (exception otherwise).
 *
 * @throws NoSuchItemException
 *   Thrown if item not found.
 */
function loadItem(int $id): Item;

EDIT: C: Quelque chose d'autre?

Beaucoup de gens ont suggéré d'autres options ou affirment que A et B sont défectueux. Ces suggestions ou opinions sont les bienvenues, pertinentes et utiles. Une réponse complète peut contenir de telles informations supplémentaires, mais répondra également à la question principale de savoir si un paramètre pour modifier la signature / le comportement est une bonne idée.

Remarques

Au cas où quelqu'un se demanderait: Les exemples sont en PHP. Mais je pense que la question s'applique à tous les langages tant qu'ils sont quelque peu similaires à PHP ou Java.


5
Bien que je doive travailler en PHP et que je sois assez compétent, c'est tout simplement un mauvais langage, et sa bibliothèque standard est partout. Lisez eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design pour référence. Il n'est donc pas vraiment surprenant que certains développeurs PHP adoptent de mauvaises habitudes. Mais je n'ai pas encore vu cette odeur de conception particulière.
Polygnome

Il me manque un point essentiel dans les réponses données: quand vous utiliseriez l'un ou l'autre. Vous utiliseriez la méthode d'exception au cas où un élément non trouvé serait inattendu et considéré comme une erreur (le temps de paniquer). Vous utiliseriez la méthode Try ... au cas où un élément manquant n'est pas improbable et certainement pas une erreur mais juste une possibilité que vous incluez dans votre contrôle de flux normal.
Martin Maat


1
@Polygnome Vous avez raison, les fonctions de bibliothèque standard sont partout, et il y a beaucoup de mauvais code, et il y a le problème d'un processus par demande. Mais cela s'améliore avec chaque version. Les types et le modèle d'objet font tout ou la plupart des choses que l'on peut en attendre. Je veux voir les génériques, la co- et la contravariance, les types de rappel qui spécifient une signature, etc. Beaucoup de ceci est en cours de discussion ou en voie d'être implémenté. Je pense que la direction générale est bonne, même si les bizarreries de la bibliothèque standard ne disparaîtront pas facilement.
donquixote

4
Une suggestion: au lieu de loadItemOrNull(id), loadItemOr(id, defaultItem)demandez-vous si cela a du sens pour vous. Si l'élément est une chaîne ou un nombre, c'est souvent le cas.
user949300

Réponses:


35

Vous avez raison: deux méthodes sont bien meilleures pour cela, pour plusieurs raisons:

  1. En Java, la signature de la méthode qui déclenche potentiellement une exception inclura cette exception; l'autre méthode ne le fera pas. Il rend particulièrement clair à quoi s'attendre de l'un et de l'autre.

  2. Dans des langages tels que C # où la signature de la méthode ne dit rien sur les exceptions, les méthodes publiques doivent toujours être documentées, et une telle documentation inclut les exceptions. Documenter une seule méthode ne serait pas facile.

    Votre exemple est parfait: les commentaires dans le deuxième morceau de code semblent beaucoup plus clairs, et je raccourcirais même «l'élément, s'il est trouvé (exception sinon)». Jusqu'à «l'élément». - la présence d'une exception potentielle et le la description que vous lui avez donnée va de soi.

  3. Dans le cas d'une méthode unique, il y a peu de cas où vous souhaitez basculer la valeur du paramètre booléen à l'exécution , et si vous le faites, cela signifierait que l'appelant devra gérer les deux cas (une nullréponse et l'exception ), ce qui rend le code beaucoup plus difficile qu'il ne devrait l'être. Étant donné que le choix n'est pas effectué au moment de l'exécution, mais lors de l'écriture du code, deux méthodes sont parfaitement logiques.

  4. Certains frameworks, tels que .NET Framework, ont établi des conventions pour cette situation, et ils le résolvent avec deux méthodes, comme vous l'avez suggéré. La seule différence est qu'ils utilisent un modèle expliqué par Ewan dans sa réponse , int.Parse(string): intlève donc une exception, alors int.TryParse(string, out int): boolque non. Cette convention de dénomination est très forte dans la communauté .NET et doit être suivie chaque fois que le code correspond à la situation que vous décrivez.


1
Merci, c'est la réponse que je cherchais! Le but était de lancer une discussion à ce sujet pour Drupal CMS, drupal.org/project/drupal/issues/2932772 , et je ne veux pas que ce soit à propos de mes préférences personnelles, mais sauvegardez-le avec les commentaires des autres.
donquixote

4
En fait, la tradition de longue date pour au moins .NET est de ne PAS utiliser deux méthodes, mais de choisir le bon choix pour la bonne tâche. Les exceptions concernent des circonstances exceptionnelles, pour ne pas gérer le flux de contrôle attendu. Vous ne parvenez pas à analyser une chaîne en un entier? Pas une circonstance très inattendue ou exceptionnelle. int.Parse()est considéré comme une très mauvaise décision de conception, c'est exactement pourquoi l'équipe .NET est allée de l'avant et a implémenté TryParse.
Voo

1
Fournir deux méthodes au lieu de penser au type de cas d'utilisation auquel nous avons affaire est la solution la plus simple: ne pensez pas à ce que vous faites, mais donnez la responsabilité à tous les utilisateurs de votre API. Bien sûr, cela fonctionne, mais ce n'est pas la marque d'une bonne conception d'API (ou n'importe quelle conception pour le sujet. Lisez par exemple le point de vue de Joel à ce sujet .
Voo

@Voo Point valide. En effet, fournir deux méthodes met le choix sur l'appelant. Peut-être que pour certaines valeurs de paramètre, un élément devrait exister, tandis que pour d'autres valeurs de paramètre, un élément inexistant est considéré comme un cas normal. Peut-être que le composant est utilisé dans une application où les éléments devraient toujours exister et dans une autre application où les éléments non existants sont considérés comme normaux. Peut-être que l'appelant appelle ->itemExists($id)avant d'appeler ->loadItem($id). Ou peut-être que c'est juste un choix fait par d'autres personnes dans le passé, et nous devons le prendre pour acquis.
donquixote

1
Raison 1b: Certaines langues (Haskell, Swift) nécessitent la nullité pour être présentes dans la signature. Plus précisément, Haskell a un type entièrement différent pour les valeurs nullables ( data Maybe a = Just a | Nothing).
John Dvorak

9

Une variante intéressante est la langue Swift. Dans Swift, vous déclarez une fonction comme "lancer", c'est-à-dire qu'elle est autorisée à lancer des erreurs (similaire mais pas tout à fait la même chose que les exceptions Java). L'appelant peut appeler cette fonction de quatre manières:

une. Dans une instruction try / catch, capture et gestion de l'exception.

b. Si l'appelant est lui-même déclaré comme étant lancé, il suffit de l'appeler, et toute erreur renvoyée se transforme en erreur lancée par l'appelant.

c. Marquer l'appel de fonction avec try!ce qui signifie "Je suis sûr que cet appel ne va pas être lancé". Si la fonction appelée ne lancer, l'application est garantie à l' accident.

ré. Marquer l'appel de fonction avec try?ce qui signifie "Je sais que la fonction peut lancer mais je ne me soucie pas de l'erreur qui est lancée". Cela modifie la valeur de retour de la fonction du type " T" au type " optional<T>", et une exception levée est transformée en renvoyant nil.

(a) et (d) sont les cas dont vous parlez. Que faire si la fonction peut retourner nil et peut lever des exceptions? Dans ce cas, le type de la valeur de retour serait " optional<R>" pour certains types R, et (d) changerait cela en " optional<optional<R>>", ce qui est un peu cryptique et difficile à comprendre mais tout à fait correct. Et une fonction Swift peut revenir optional<Void>, donc (d) peut être utilisé pour lancer des fonctions renvoyant Void.


2

J'aime l' bool TryMethodName(out returnValue)approche.

Il vous donne une indication concluante de la réussite ou non de la méthode et les correspondances de nommage enveloppent la méthode de levée d'exceptions dans un bloc try catch.

Si vous retournez simplement null, vous ne savez pas s'il a échoué ou si la valeur de retour était légitimement nulle.

par exemple:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

exemple d'utilisation (out signifie passer par référence non initialisé)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}

Désolé, vous m'avez perdu ici. À quoi bool TryMethodName(out returnValue)ressemblerait votre « approche» dans l'exemple original?
donquixote

modifié pour ajouter un exemple
Ewan

1
Vous utilisez donc un paramètre par référence au lieu d'une valeur de retour, vous avez donc la valeur de retour libre pour le succès booléen? Existe-t-il un langage de programmation qui prend en charge ce mot clé "out"?
donquixote

2
oui, désolé je n'ai pas réalisé que 'out' est spécifique à c #
Ewan

1
pas toujours non. Mais que faire si le point 3 est nul? vous ne sauriez pas s'il y a eu une erreur ou non
Ewan

2

Habituellement, un paramètre booléen indique qu'une fonction peut être divisée en deux, et en règle générale, vous devez toujours la diviser plutôt que de passer dans un booléen.


1
Je ne ferais pas exception à cela en règle générale sans ajouter une certaine qualification ou nuance. La suppression d'un booléen en tant qu'argument peut vous obliger à écrire un idiot if / else à un autre endroit et donc à encoder des données dans votre code écrit et non dans des variables.
Gerard

@Gerard pouvez-vous me donner un exemple s'il vous plait?
Max

@Max Lorsque vous avez une variable booléenne b: Au lieu d'appeler, foo(…, b);vous devez écrire if (b) then foo_x(…) else foo_y(…);et répéter les autres arguments, ou quelque chose comme ((b) ? foo_x : foo_y)(…)si le langage a un opérateur ternaire et des fonctions / méthodes de première classe. Et cela peut même se répéter à plusieurs endroits différents du programme.
BlackJack

@BlackJack bien ouais je pourrais être d'accord avec toi. J'ai pensé à l'exemple, if (myGrandmaMadeCookies) eatThem() else makeFood()que je pourrais envelopper dans une autre fonction comme celle-ci checkIfShouldCook(myGrandmaMadeCookies). Je me demande si la nuance que vous avez mentionnée a à voir avec le fait de savoir si nous passons un booléen sur la façon dont nous voulons que la fonction se comporte, comme dans la question d'origine, par rapport à ce que nous transmettons quelques informations sur notre modèle, comme dans le exemple de grand-mère que je viens de donner.
Max

1
La nuance mentionnée dans mon commentaire fait référence à "vous devriez toujours". Peut-être le reformuler comme "si a, b et c sont applicables alors [...]". La "règle" que vous mentionnez est un grand paradigme, mais très différent du cas d'utilisation.
Gerard

1

Clean Code conseille d'éviter les arguments de drapeau booléen, car leur signification est opaque sur le site d'appel:

loadItem(id, true) // does this throw or not? We need to check the docs ...

alors que des méthodes distinctes peuvent utiliser des noms expressifs:

loadItemOrNull(id); // meaning is obvious; no need to refer to docs

1

Si je devais utiliser A ou B, j'utiliserais B, deux méthodes distinctes, pour les raisons énoncées dans la réponse d'Arseni Mourzenkos .

Mais il existe une autre méthode 1 : en Java, on l'appelleOptional , mais vous pouvez appliquer le même concept à d'autres langages où vous pouvez définir vos propres types, si le langage ne fournit pas déjà une classe similaire.

Vous définissez votre méthode comme ceci:

Optional<Item> loadItem(int id);

Dans votre méthode, vous retournez Optional.of(item)si vous avez trouvé l'article ou Optional.empty()si vous ne le faites pas. Vous ne lancez jamais 2 et ne revenez jamaisnull .

Pour le client de votre méthode, c'est quelque chose comme ça null, mais avec la grande différence que cela oblige à penser au cas des éléments manquants. L'utilisateur ne peut pas simplement ignorer le fait qu'il peut n'y avoir aucun résultat. Pour retirer l'élément d' Optionalune action, une action explicite est requise:

  • loadItem(1).get()jettera NoSuchElementExceptionou retournera l'article trouvé.
  • Il faut également loadItem(1).orElseThrow(() -> new MyException("No item 1"))utiliser une exception personnalisée si le retour Optionalest vide.
  • loadItem(1).orElse(defaultItem)renvoie soit l'élément trouvé, soit l'élément passé defaultItem(qui peut également l'être null) sans lever d'exception.
  • loadItem(1).map(Item::getName)retournerait à nouveau un Optionalavec le nom des éléments, s'il était présent.
  • Il existe d'autres méthodes, voir Javadoc pourOptional .

L'avantage est que maintenant c'est au code client ce qui doit se passer s'il n'y a pas d'élément et il vous suffit de fournir une seule méthode .


1 Je suppose que c'est quelque chose comme la réponsetry? dans gnasher729s mais ce n'est pas tout à fait clair pour moi car je ne connais pas Swift et cela semble être une fonctionnalité de langue donc c'est spécifique à Swift.

2 Vous pouvez lever des exceptions pour d'autres raisons, par exemple si vous ne savez pas s'il y a un élément ou non parce que vous avez eu des problèmes de communication avec la base de données.


0

Comme autre point d'accord avec l'utilisation de deux méthodes, cela peut être très facile à mettre en œuvre. J'écrirai mon exemple en C #, car je ne connais pas la syntaxe de PHP.

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}

0

Je préfère deux méthodes, de cette façon, vous me direz non seulement que vous renvoyez une valeur, mais quelle valeur exactement.

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething () n'est pas non plus un mauvais modèle.

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

Je suis plus habitué à voir le premier dans les interactions de base de données tandis que le second est plus utilisé dans les trucs d'analyse.

Comme d'autres vous l'ont déjà dit, les paramètres d'indicateur sont généralement une odeur de code (les odeurs de code ne signifient pas que vous faites quelque chose de mal, juste qu'il y a une probabilité que vous le fassiez). Bien que vous le nommiez précisément, il y a parfois des nuances qui rendent difficile de savoir comment utiliser ce drapeau et vous finissez par regarder à l'intérieur du corps de la méthode.


-1

Alors que d'autres personnes suggèrent le contraire, je dirais qu'il est préférable d'avoir une méthode avec un paramètre pour indiquer comment les erreurs doivent être traitées que d'exiger l'utilisation de méthodes distinctes pour les deux scénarios d'utilisation. Bien qu'il puisse être souhaitable d' avoir également des points d'entrée distincts pour les utilisations «erreur jette» par rapport à «erreur renvoie erreur d'indication», avoir un point d'entrée qui peut servir les deux modèles d'utilisation évitera la duplication de code dans les méthodes d'encapsuleur. À titre d'exemple simple, si l'on a des fonctions:

Thing getThing(string x);
Thing tryGetThing (string x);

et on veut des fonctions qui se comportent de la même façon mais avec x encapsulé entre crochets, il faudrait écrire deux ensembles de fonctions wrapper:

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

S'il y a un argument pour savoir comment gérer les erreurs, un seul wrapper serait nécessaire:

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

Si l'encapsuleur est assez simple, la duplication de code n'est peut-être pas trop mauvaise, mais si jamais elle doit être modifiée, la duplication du code nécessitera à son tour la duplication de toutes les modifications. Même si l'on opte pour ajouter des points d'entrée qui n'utilisent pas l'argument supplémentaire:

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

on peut garder toute la "logique métier" dans une seule méthode - celle qui accepte un argument indiquant quoi faire en cas d'échec.


Vous avez raison: les décorateurs et les wrappers, et même l'implémentation régulière, auront une certaine duplication de code lors de l'utilisation de deux méthodes.
donquixote

Assurément mettre les versions avec et sans levée d'exception dans des packages séparés, et rendre les wrappers génériques?
Will Crawford

-1

Je pense que toutes les réponses qui expliquent que vous ne devriez pas avoir un indicateur qui contrôle si une exception est levée ou non sont bonnes. Leurs conseils seraient mes conseils à tous ceux qui ressentent le besoin de poser cette question.

Je tenais cependant à souligner qu'il existe une exception significative à cette règle. Une fois que vous êtes suffisamment avancé pour commencer à développer des API que des centaines d'autres personnes utiliseront, il peut arriver que vous souhaitiez fournir un tel indicateur. Lorsque vous écrivez une API, vous n'écrivez pas seulement aux puristes. Vous écrivez à de vrais clients qui ont de vrais désirs. Une partie de votre travail consiste à les rendre satisfaits de votre API. Cela devient un problème social, plutôt qu'un problème de programmation, et parfois des problèmes sociaux dictent des solutions qui ne seraient pas idéales comme solutions de programmation à elles seules.

Vous pouvez constater que vous avez deux utilisateurs distincts de votre API, dont l'un souhaite des exceptions et l'autre non. Un exemple concret où cela pourrait se produire est avec une bibliothèque qui est utilisée à la fois en développement et sur un système embarqué. Dans les environnements de développement, les utilisateurs voudront probablement avoir des exceptions partout. Les exceptions sont très populaires pour gérer des situations inattendues. Cependant, ils sont interdits dans de nombreuses situations intégrées, car ils sont trop difficiles à analyser autour des contraintes en temps réel. Quand vous vous souciez réellement non seulement de la moyenne temps nécessaire à l'exécution de votre fonction, mais du temps qu'il faut pour un plaisir individuel, l'idée de dérouler la pile à n'importe quel endroit arbitraire est très indésirable.

Un exemple réel pour moi: une bibliothèque de mathématiques. Si votre bibliothèque de mathématiques a une Vectorclasse qui prend en charge l'obtention d'un vecteur unitaire dans la même direction, vous devez être en mesure de diviser par l'amplitude. Si la magnitude est 0, vous avez une situation de division par zéro. En développement, vous voulez les attraper . Vous ne voulez vraiment pas surprendre la division par des 0 qui traînent. Lancer une exception est une solution très populaire pour cela. Cela n'arrive presque jamais (c'est un comportement vraiment exceptionnel), mais quand c'est le cas, vous voulez le savoir.

Sur la plate-forme intégrée, vous ne voulez pas de ces exceptions. Il serait plus logique de faire une vérification if (this->mag() == 0) return Vector(0, 0, 0);. En fait, je vois cela dans du vrai code.

Pensez maintenant du point de vue d'une entreprise. Vous pouvez essayer d'enseigner deux façons différentes d'utiliser une API:

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

Cela satisfait les opinions de la plupart des réponses ici, mais du point de vue de l'entreprise, cela n'est pas souhaitable. Les développeurs intégrés devront apprendre un style de codage et les développeurs de développement devront en apprendre un autre. Cela peut être assez embêtant et contraint les gens à une façon de penser. Potentiellement pire: comment allez-vous prouver que la version intégrée n'inclut pas de code d'exception? La version de lancement peut facilement se fondre, se cachant quelque part dans votre code jusqu'à ce qu'une carte d'examen des échecs coûteuse la trouve.

Si, en revanche, vous avez un indicateur qui active ou désactive la gestion des exceptions, les deux groupes peuvent apprendre exactement le même style de codage. En fait, dans de nombreux cas, vous pouvez même utiliser le code d'un groupe dans les projets de l'autre groupe. Dans le cas de l'utilisation de ce code unit(), si vous vous souciez réellement de ce qui se passe dans un cas de division par 0, vous devriez avoir écrit le test vous-même. Ainsi, si vous le déplacez vers un système embarqué, où vous obtenez simplement de mauvais résultats, ce comportement est "sain". Maintenant, d'un point de vue commercial, je forme mes codeurs une fois, et ils utilisent les mêmes API et les mêmes styles dans toutes les parties de mon entreprise.

Dans la grande majorité des cas, vous souhaitez suivre les conseils des autres: utilisez des idiomes similaires tryGetUnit()ou similaires pour les fonctions qui ne génèrent pas d'exceptions et renvoient à la place une valeur sentinelle comme null. Si vous pensez que les utilisateurs peuvent vouloir utiliser à la fois le code de gestion des exceptions et le code de non-exception côte à côte dans un même programme, respectez la tryGetUnit()notation. Cependant, il y a un cas d'angle où la réalité des affaires peut améliorer l'utilisation d'un drapeau. Si vous vous retrouvez dans ce cas d'angle, n'ayez pas peur de jeter les "règles" au bord du chemin. Voilà à quoi servent les règles!

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.