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_failure
paramè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.
la source
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.Réponses:
Vous avez raison: deux méthodes sont bien meilleures pour cela, pour plusieurs raisons:
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.
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.
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
null
ré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.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): int
lève donc une exception, alorsint.TryParse(string, out int): bool
que 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.la source
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
.->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.data Maybe a = Just a | Nothing
).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 reveniroptional<Void>
, donc (d) peut être utilisé pour lancer des fonctions renvoyant Void.la source
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:
exemple d'utilisation (out signifie passer par référence non initialisé)
la source
bool TryMethodName(out returnValue)
ressemblerait votre « approche» dans l'exemple original?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.
la source
b
: Au lieu d'appeler,foo(…, b);
vous devez écrireif (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.if (myGrandmaMadeCookies) eatThem() else makeFood()
que je pourrais envelopper dans une autre fonction comme celle-cicheckIfShouldCook(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.Clean Code conseille d'éviter les arguments de drapeau booléen, car leur signification est opaque sur le site d'appel:
alors que des méthodes distinctes peuvent utiliser des noms expressifs:
la source
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'appelle
Optional
, 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:
Dans votre méthode, vous retournez
Optional.of(item)
si vous avez trouvé l'article ouOptional.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'Optional
une action, une action explicite est requise:loadItem(1).get()
jetteraNoSuchElementException
ou retournera l'article trouvé.loadItem(1).orElseThrow(() -> new MyException("No item 1"))
utiliser une exception personnalisée si le retourOptional
est vide.loadItem(1).orElse(defaultItem)
renvoie soit l'élément trouvé, soit l'élément passédefaultItem
(qui peut également l'êtrenull
) sans lever d'exception.loadItem(1).map(Item::getName)
retournerait à nouveau unOptional
avec le nom des éléments, s'il était présent.Optional
.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éponse
try?
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.
la source
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.
la source
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.
TryDoSomething () n'est pas non plus un mauvais modèle.
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.
la source
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:
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:
S'il y a un argument pour savoir comment gérer les erreurs, un seul wrapper serait nécessaire:
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:
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.
la source
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
Vector
classe 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:
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 latryGetUnit()
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!la source