web-dev-qa-db-fra.com

Est-ce mal d'utiliser un paramètre booléen pour déterminer le comportement?

J'ai vu de temps en temps une pratique qui "se sent" mal, mais je ne peux pas vraiment expliquer ce qui ne va pas. Ou peut-être que c'est juste mon préjugé. Voici:

Un développeur définit une méthode avec un booléen comme l'un de ses paramètres, et cette méthode en appelle un autre, et ainsi de suite, et finalement ce booléen est utilisé, uniquement pour déterminer s'il faut ou non prendre une certaine action. Cela peut être utilisé, par exemple, pour autoriser l'action uniquement si l'utilisateur a certains droits, ou peut-être si nous sommes (ou ne sommes pas) en mode test ou en mode batch ou en direct, ou peut-être seulement lorsque le système est dans un certain état.

Eh bien, il y a toujours une autre façon de le faire, que ce soit en demandant quand il est temps de prendre l'action (plutôt que de passer le paramètre), ou en ayant plusieurs versions de la méthode, ou plusieurs implémentations de la classe, etc. Ma question n'est pas pas tant comment améliorer cela, mais plutôt si c'est vraiment faux (comme je le soupçonne), et si c'est le cas, qu'est-ce qui ne va pas?.

203
Ray

Oui, il s'agit probablement d'une odeur de code, qui conduirait à un code non maintenable, difficile à comprendre et qui a moins de chances d'être facilement réutilisé.

Comme d'autres affiches l'ont noté le contexte est tout (ne faites pas preuve de fermeté s'il s'agit d'un cas isolé ou si la pratique a été reconnue comme une dette technique délibérément contractée à refactoriser plus tard) mais d'une manière générale, si un paramètre est transmis à une fonction qui sélectionne un comportement spécifique à exécuter, un raffinement supplémentaire par étapes est nécessaire; La division de cette fonction en fonctions plus petites produira des fonctions plus fortement cohésives.

Alors, quelle est une fonction hautement cohésive?

C'est une fonction qui fait une chose et une seule chose.

Le problème avec un paramètre transmis comme vous le décrivez est que la fonction fait plus de deux choses; il peut ou non vérifier les droits d'accès des utilisateurs en fonction de l'état du paramètre booléen, puis en fonction de cet arbre de décision, il exécutera un élément de fonctionnalité.

Il serait préférable de séparer les préoccupations du contrôle d'accès de celles de la tâche, de l'action ou du commandement.

Comme vous l'avez déjà noté, l'entrelacement de ces préoccupations semble éteint.

Ainsi, la notion de cohésion nous aide à identifier que la fonction en question n'est pas très cohésive et que nous pourrions refactoriser le code pour produire un ensemble de fonctions plus cohésives.

La question pourrait donc être reformulée; Étant donné que nous convenons tous que le passage des paramètres de sélection comportementale est préférable d'éviter comment améliorer les choses?

Je me débarrasserais complètement du paramètre. La possibilité de désactiver le contrôle d'accès même pour les tests est un risque potentiel pour la sécurité. À des fins de test, stub ou mock le contrôle d'accès pour tester à la fois les accès autorisés et les accès refusés.

Réf: Cohésion (informatique)

112
Rob Kielty

J'ai cessé d'utiliser ce modèle il y a longtemps, pour une raison très simple; coût de maintenance. Plusieurs fois, j'ai trouvé que j'avais une fonction disons frobnicate(something, forwards_flag) qui était appelée plusieurs fois dans mon code, et avait besoin de localiser tous les endroits du code où la valeur false était passée comme valeur de forwards_flag. Vous ne pouvez pas facilement les rechercher, cela devient donc un casse-tête de maintenance. Et si vous avez besoin de faire un bug sur chacun de ces sites, vous pourriez avoir un problème malheureux si vous en manquez un.

Mais ce problème spécifique est facilement résolu sans changer fondamentalement l'approche:

enum FrobnicationDirection {
  FrobnicateForwards,
  FrobnicateBackwards;
};

void frobnicate(Object what, FrobnicationDirection direction);

Avec ce code, il suffit de rechercher des instances de FrobnicateBackwards. Bien qu'il soit possible qu'il y ait du code qui l'assigne à une variable, vous devez donc suivre quelques threads de contrôle, je trouve en pratique que c'est assez rare pour que cette alternative fonctionne correctement.

Cependant, il y a un autre problème à passer le drapeau de cette manière, du moins en principe. C'est que certains (seulement certains) systèmes ayant cette conception peuvent exposer trop de connaissances sur les détails d'implémentation des parties profondément imbriquées du code (qui utilise l'indicateur) aux couches externes (qui doivent savoir quelle valeur passer dans ce drapeau). Pour utiliser la terminologie de Larry Constantine, cette conception peut avoir un fort couplage entre le passeur et l'utilisateur du drapeau booléen. Franky est difficile à dire avec certitude sur cette question sans en savoir plus sur la base de code.

Pour répondre aux exemples spécifiques que vous donnez, j'aurais une certaine inquiétude dans chacun, mais principalement pour des raisons de risque/correction. Autrement dit, si votre système doit contourner des indicateurs qui indiquent dans quel état il se trouve, vous pouvez constater que vous disposez d'un code qui aurait dû en tenir compte mais ne vérifie pas le paramètre (car il n'a pas été transmis à cette fonction). Vous avez donc un bug parce que quelqu'un a omis de passer le paramètre.

Il convient également d'admettre qu'un indicateur d'état du système qui doit être transmis à presque toutes les fonctions est en fait essentiellement une variable globale. De nombreux inconvénients d'une variable globale s'appliqueront. Je pense que dans de nombreux cas, il est préférable d'encapsuler la connaissance de l'état du système (ou des informations d'identification de l'utilisateur ou de l'identité du système) au sein d'un objet qui est chargé d'agir correctement sur la base de ces données. Ensuite, vous passez une référence à cet objet par opposition aux données brutes. Le concept clé ici est encapsulation .

153
James Youngman

Ce n'est pas nécessairement faux mais cela peut représenter un odeur de code .

Le scénario de base à éviter concernant les paramètres booléens est le suivant:

public void foo(boolean flag) {
    doThis();
    if (flag)
        doThat();
}

Ensuite, lors de l'appel, vous appelez généralement foo(false) et foo(true) selon le comportement exact que vous souhaitez.

C'est vraiment un problème car c'est un cas de mauvaise cohésion. Vous créez une dépendance entre les méthodes qui n'est pas vraiment nécessaire.

Dans ce cas, vous devez laisser doThis et doThat en tant que méthodes distinctes et publiques, puis faire:

doThis();
doThat();

ou

doThis();

De cette façon, vous laissez la bonne décision à l'appelant (exactement comme si vous passiez un paramètre booléen) sans créer de couplage.

Bien sûr, tous les paramètres booléens ne sont pas utilisés de manière aussi mauvaise, mais c'est définitivement une odeur de code et vous avez raison de vous méfier si vous en voyez beaucoup dans le code source.

Ce n'est qu'un exemple de la façon de résoudre ce problème sur la base des exemples que j'ai écrits. Il existe d'autres cas où une approche différente sera nécessaire.

Il y a une bonne article de Martin Fowler expliquant plus en détail la même idée.

PS: si la méthode foo au lieu d'appeler deux méthodes simples avait une implémentation plus complexe, alors tout ce que vous avez à faire est d'appliquer un petit refactoring extraction des méthodes pour que le code résultant ressemble à l'implémentation de foo que j'ai écrit.

39
Alex

Tout d'abord: la programmation n'est pas tant une science qu'un art. Il y a donc très rarement une "mauvaise" et une "bonne" façon de programmer. La plupart des normes de codage ne sont que des "préférences" que certains programmeurs jugent utiles; mais finalement ils sont plutôt arbitraires. Je ne qualifierais donc jamais un choix de paramètre de "mauvais" en soi - et certainement pas quelque chose d'aussi générique et utile qu'un paramètre booléen. L'utilisation d'un boolean (ou d'un int, d'ailleurs) pour encapsuler l'état est parfaitement justifiable dans de nombreux cas.

Les décisions de codage, dans l'ensemble, doivent être basées principalement sur les performances et la maintenabilité. Si la performance n'est pas en jeu (et je ne peux pas imaginer comment elle pourrait jamais être dans vos exemples), alors votre prochaine considération devrait être: quelle sera la facilité pour moi (ou un futur rédacteur) à maintenir? Est-ce intuitif et compréhensible? Est-ce isolé? Votre exemple d'appels de fonction chaînés semble en fait potentiellement fragile à cet égard: si vous décidez de changer votre bIsUp en bIsDown, combien d'autres emplacements dans le code devront également être modifiés? En outre, votre liste de paramétres monte-t-elle en flèche? Si votre fonction a 17 paramètres, la lisibilité est un problème et vous devez reconsidérer si vous appréciez les avantages de l'architecture orientée objet.

30
kmote

Je pense que Robert C Martins Clean code article déclare que vous devez éliminer les arguments booléens lorsque cela est possible car ils montrent qu'une méthode fait plus d'une chose. Une méthode devrait faire une chose et une seule chose, je pense, est l'une de ses devises.

16
dreza

Je pense que la chose la plus importante ici est d'être pratique.

Lorsque le booléen détermine le comportement entier, faites simplement une deuxième méthode.

Lorsque le booléen ne détermine qu'un peu de comportement au milieu, vous voudrez peut-être le garder en un pour réduire la duplication de code. Dans la mesure du possible, vous pourriez même être en mesure de diviser la méthode en trois: deux méthodes d'appel pour l'une ou l'autre des options booléennes, et une qui effectue la majeure partie du travail.

Par exemple:

private void FooInternal(bool flag)
{
  //do work
}

public void Foo1()
{
  FooInternal(true);
}

public void Foo2()
{
  FooInternal(false);
}

Bien sûr, dans la pratique, vous aurez toujours un point entre ces extrêmes. Habituellement, je vais juste avec ce qui me semble bien, mais je préfère pécher par excès de duplication de code.

8
Jorn

J'aime l'approche de personnalisation du comportement via des méthodes de génération qui retournent des instances immuables. Voici comment Guava Splitter l'utilise:

private static final Splitter MY_SPLITTER = Splitter.on(',')
       .trimResults()
       .omitEmptyStrings();

MY_SPLITTER.split("one,,,,  two,three");

Les avantages de ceci sont:

  • Lisibilité supérieure
  • Séparation claire de la configuration et des méthodes d'action
  • Favorise la cohésion en vous forçant à réfléchir à ce qu'est l'objet, à ce qu'il doit et ne doit pas faire. Dans ce cas, c'est un Splitter. Vous ne mettriez jamais someVaguelyStringRelatedOperation(List<Entity> myEntities) dans une classe appelée Splitter, mais vous pensez à le mettre comme méthode statique dans une classe StringUtils.
  • Les instances sont préconfigurées, donc facilement injectables dans les dépendances. Le client n'a pas besoin de se soucier de passer true ou false à une méthode pour obtenir le comportement correct.
7
oksayt

Certainement un odeur de code . S'il ne viole pas principe de responsabilité unique alors il viole probablement Dites, ne demandez pas . Considérer:

S'il s'avère qu'il ne viole pas l'un de ces deux principes, vous devez toujours utiliser une énumération. Les drapeaux booléens sont l'équivalent booléen de nombres magiques . foo(false) a autant de sens que bar(42). Les énumérations peuvent être utiles pour Modèle de stratégie et ont la flexibilité de vous permettre d'ajouter une autre stratégie. (N'oubliez pas de nommez-les de façon appropriée .)

Votre exemple particulier me dérange particulièrement. Pourquoi ce drapeau passe-t-il par tant de méthodes? Il semble que vous deviez diviser votre paramètre en sous-classes .

5
Eva

TL; DR: n'utilisez pas d'arguments booléens.

Voir ci-dessous pourquoi ils sont mauvais, et comment les remplacer (en gras).


Les arguments booléens sont très difficiles à lire et donc difficiles à maintenir. Le problème principal est que l'objectif est généralement clair lorsque vous lisez la signature de la méthode où l'argument est nommé. Cependant, l'attribution d'un nom à un paramètre n'est généralement pas requise dans la plupart des langues. Vous aurez donc des anti-modèles comme RSACryptoServiceProvider#encrypt(Byte[], Boolean) où le paramètre booléen détermine le type de cryptage à utiliser dans la fonction.

Vous recevrez donc un appel comme:

rsaProvider.encrypt(data, true);

où le lecteur doit rechercher la signature de la méthode simplement pour déterminer ce que l'enfer true pourrait réellement signifier. Passer un entier est bien sûr tout aussi mauvais:

rsaProvider.encrypt(data, 1);

vous en dirait autant - ou plutôt: aussi peu. Même si vous définissez des constantes à utiliser pour l'entier, les utilisateurs de la fonction peuvent simplement les ignorer et continuer à utiliser des valeurs littérales.

La meilleure façon de résoudre ce problème est d'utiliser une énumération . Si vous devez passer une énumération RSAPadding avec deux valeurs: OAEP ou PKCS1_V1_5, Vous pourrez immédiatement lire le code:

rsaProvider.encrypt(data, RSAPadding.OAEP);

Les booléens ne peuvent avoir que deux valeurs. Cela signifie que si vous avez une troisième option, vous devrez refactoriser votre signature. En général, cela ne peut pas être facilement effectué si la compatibilité descendante est un problème, vous devez donc étendre n'importe quelle classe publique avec une autre méthode publique. C'est ce que Microsoft a finalement fait en introduisant RSACryptoServiceProvider#encrypt(Byte[], RSAEncryptionPadding) où ils ont utilisé une énumération (ou au moins une classe imitant une énumération) au lieu d'un booléen.

Il peut même être plus facile d'utiliser un objet complet ou une interface comme paramètre, au cas où le paramètre lui-même doit être paramétré. Dans l'exemple ci-dessus, le remplissage OAEP lui-même peut être paramétré avec la valeur de hachage à utiliser en interne. Notez qu'il existe maintenant 6 algorithmes de hachage SHA-2 et 4 algorithmes de hachage SHA-3, de sorte que le nombre de valeurs d'énumération peut exploser si vous n'utilisez qu'une seule énumération plutôt que des paramètres (c'est probablement la prochaine chose que Microsoft va découvrir ).


Les paramètres booléens peuvent également indiquer que la méthode ou la classe n'est pas bien conçue. Comme avec l'exemple ci-dessus: toute bibliothèque cryptographique autre que celle .NET n'utilise pas du tout d'indicateur de remplissage dans la signature de la méthode.


Presque tous les gourous du logiciel que j'aime mettent en garde contre les arguments booléens. Par exemple, Joshua Bloch met en garde contre eux dans le livre très apprécié "Effective Java". En général, ils ne devraient tout simplement pas être utilisés. Vous pourriez faire valoir qu'ils pourraient être utilisés si le cas est qu'il existe un paramètre facile à comprendre. Mais même alors: Bit.set(boolean) est probablement mieux implémenté en utilisant deux méthodes : Bit.set() et Bit.unset().


Si vous ne pouvez pas refactoriser directement le code, vous pouvez définir des constantes pour au moins les rendre plus lisibles:

const boolean ENCRYPT = true;
const boolean DECRYPT = false;

...

cipher.init(key, ENCRYPT);

est beaucoup plus lisible que:

cipher.init(key, true);

même si vous préférez:

cipher.initForEncryption(key);
cipher.initForDecryption(key);

au lieu.

4
Maarten Bodewes

Je suis surpris que personne n'ait mentionné named-parameters.

Le problème que je vois avec les booléens est qu'ils nuisent à la lisibilité. Par exemple, qu'est-ce que true dans

myObject.UpdateTimestamps(true);

faire? Je n'ai aucune idée. Mais qu'en est-il:

myObject.UpdateTimestamps(saveChanges: true);

Maintenant, c'est clair ce que le paramètre que nous passons est censé faire: nous disons à la fonction d'enregistrer ses modifications. Dans ce cas, si la classe n'est pas publique, je pense que le paramètre booléen est correct.


Bien sûr, vous ne pouvez pas forcer les utilisateurs de votre classe à utiliser des paramètres nommés. Pour cette raison, une enum ou deux méthodes distinctes sont généralement préférables, selon la fréquence de votre cas par défaut. C'est exactement ce que fait .Net:

//Enum used
double a = Math.Round(b, MidpointRounding.AwayFromZero);

//Two separate methods used
IEnumerable<Stuff> ascendingList = c.OrderBy(o => o.Key);
IEnumerable<Stuff> descendingList = c.OrderByDescending(o => o.Key); 

Je ne peux pas vraiment expliquer ce qui ne va pas.

Si cela ressemble à une odeur de code, ressemble à une odeur de code et - enfin - sent comme une odeur de code, c'est probablement une odeur de code.

Ce que vous voulez faire, c'est:

1) Évitez les méthodes avec effets secondaires.

2) Gérez les états nécessaires avec une machine à états centrale et formelle (comme this ).

1
BaBu

Je suis d'accord avec toutes les préoccupations liées à l'utilisation des paramètres booléens pour ne pas déterminer les performances afin de; améliorer, lisibilité, fiabilité, réduire la complexité, réduire les risques de mauvaise encapsulation et cohésion et réduire le coût total de possession avec la maintenabilité.

J'ai commencé à concevoir du matériel au milieu des années 70 que nous appelons maintenant SCADA (contrôle de supervision et acquisition de données) et il s'agissait de matériel affiné avec le code machine en EPROM exécutant des télécommandes macro et collectant des données à haute vitesse.

La logique s'appelle ( Mealey & Moore machines que nous appelons maintenant Machines à états finis . Celles-ci doivent également être effectuées selon les mêmes règles que ci-dessus, sauf s'il s'agit d'une machine en temps réel avec un temps d'exécution fini, puis des raccourcis doivent être effectués pour atteindre l'objectif.

Les données sont synchrones mais les commandes sont asynchrones et la logique de commande suit une logique booléenne sans mémoire mais avec des commandes séquentielles basées sur la mémoire de l'état précédent, présent et suivant souhaité. Pour que cela fonctionne dans le langage machine le plus efficace (seulement 64 Ko), un grand soin a été pris pour définir chaque processus de manière heuristique IBM HIPO. Cela signifiait parfois passer des variables booléennes et faire des branches indexées.

Mais maintenant avec beaucoup de mémoire et la facilité d'OOK, l'encapsulation est un ingrédient essentiel aujourd'hui mais une pénalité lorsque le code a été compté en octets pour le temps réel et le code machine SCADA.

Ce n'est pas nécessairement faux, mais dans votre exemple concret de l'action en fonction d'un attribut de l '"utilisateur", je passerais par une référence à l'utilisateur plutôt qu'un indicateur.

Cela clarifie et aide de plusieurs façons.

Quiconque lit la déclaration invoquante se rendra compte que le résultat changera en fonction de l'utilisateur.

Dans la fonction qui est finalement appelée, vous pouvez facilement implémenter des règles métier plus complexes car vous pouvez accéder à n'importe quel attribut utilisateur.

Si une fonction/méthode dans la "chaîne" fait quelque chose de différent selon un attribut utilisateur, il est très probable qu'une dépendance similaire sur les attributs utilisateur sera introduite pour certaines des autres méthodes de la "chaîne".

0
James Anderson

La plupart du temps, je considérerais ce mauvais codage. Je peux penser à deux cas, cependant, où cela pourrait être une bonne pratique. Comme il y a déjà beaucoup de réponses disant pourquoi c'est mauvais, j'offre deux fois quand ça pourrait être bon:

La première est de savoir si chaque appel de la séquence a un sens en soi. Cela aurait du sens si le code appelant pourrait être changé de vrai en faux ou faux en vrai, o si la méthode appelée pourrait être changée en utilisez directement le paramètre booléen plutôt que de le transmettre. La probabilité de dix appels de ce type d'affilée est faible, mais cela pourrait arriver, et si c'était le cas, ce serait une bonne pratique de programmation.

Le deuxième cas est un peu exagéré étant donné que nous avons affaire à un booléen. Mais si le programme a plusieurs threads ou événements, la transmission de paramètres est le moyen le plus simple de suivre les données spécifiques aux threads/événements. Par exemple, un programme peut obtenir des entrées de deux sockets ou plus. Le code en cours d'exécution pour un socket peut avoir besoin de produire des messages d'avertissement, et celui en cours d'exécution pour un autre peut ne pas. Il est alors (en quelque sorte) logique qu'un ensemble booléen à un niveau très élevé passe par de nombreux appels de méthode aux endroits où des messages d'avertissement pourraient être générés. Les données ne peuvent pas être enregistrées (sauf avec de grandes difficultés) dans une sorte de global car plusieurs threads ou événements entrelacés auraient chacun besoin de leur propre valeur.

Pour être sûr, dans ce dernier cas, je créerais probablement une classe/structure dont le seul contenu était un booléen et je passerais plutôt that à la place. Je serais presque certain d'avoir besoin d'autres champs avant trop longtemps - comme pour envoyer les messages d'avertissement, par exemple.

0
RalphChapin