Selon Est-ce mal d'utiliser un paramètre booléen pour déterminer un comportement? , je sais qu'il est important d'éviter d'utiliser des paramètres booléens pour déterminer un comportement, par exemple:
version originale
public void setState(boolean flag){
if(flag){
a();
}else{
b();
}
c();
}
nouvelle version:
public void setStateTrue(){
a();
c();
}
public void setStateFalse(){
b();
c();
}
Mais qu'en est-il du cas où le paramètre booléen est utilisé pour déterminer des valeurs plutôt que des comportements? par exemple:
public void setHint(boolean isHintOn){
this.layer1.visible=isHintOn;
this.layer2.visible=!isHintOn;
this.layer3.visible=isHintOn;
}
J'essaie d'éliminer le drapeau isHintOn et de créer 2 fonctions distinctes:
public void setHintOn(){
this.layer1.visible=true;
this.layer2.visible=false;
this.layer3.visible=true;
}
public void setHintOff(){
this.layer1.visible=false;
this.layer2.visible=true;
this.layer3.visible=false;
}
mais la version modifiée semble moins maintenable car:
il a plus de codes que la version originale
il ne peut pas clairement montrer que la visibilité de layer2 est opposée à l'option hint
lorsqu'un nouveau calque (par exemple: layer4) est ajouté, je dois ajouter
this.layer4.visible=false;
et
this.layer4.visible=true;
dans setHintOn () et setHintOff () séparément
Donc ma question est, si le paramètre booléen est utilisé pour déterminer uniquement les valeurs, mais pas les comportements (par exemple: pas de if-else sur ce paramètre), est-il toujours recommandé d'éliminer ce paramètre booléen?
La conception de l'API doit se concentrer sur ce qui est le plus utilisable pour un client de l'API, du côté appelant.
Par exemple, si cette nouvelle API nécessite que l'appelant écrive régulièrement du code comme celui-ci
if(flag)
foo.setStateTrue();
else
foo.setStateFalse();
alors il devrait être évident qu'éviter le paramètre est pire que d'avoir une API qui permet à l'appelant d'écrire
foo.setState(flag);
L'ancienne version produit juste un problème qui doit ensuite être résolu du côté appelant (et probablement plus d'une fois). Cela n'augmente ni la lisibilité ni la maintenabilité.
Le côté implémentation, cependant, ne devrait pas dicter à quoi ressemble l'API publique. Si une fonction comme setHint
avec un paramètre nécessite moins de code dans l'implémentation, mais qu'une API en termes setHintOn
/setHintOff
semble plus facile à utiliser pour un client, on peut l'implémenter ceci façon:
private void setHint(boolean isHintOn){
this.layer1.visible=isHintOn;
this.layer2.visible=!isHintOn;
this.layer3.visible=isHintOn;
}
public void setHintOn(){
setHint(true);
}
public void setHintOff(){
setHint(false);
}
Ainsi, même si l'API publique n'a pas de paramètre booléen, il n'y a pas de logique en double ici, donc un seul endroit à changer lorsqu'une nouvelle exigence (comme dans l'exemple de la question) arrive.
Cela fonctionne également dans l'autre sens: si la méthode setState
ci-dessus doit basculer entre deux morceaux de code différents, ces morceaux de code peuvent être refactorisés en deux méthodes privées différentes. Donc à mon humble avis, il n'est pas logique de rechercher un critère pour décider entre "un paramètre/une méthode" et "zéro paramètres/deux méthodes" en regardant les internes. Regardez cependant la façon dont vous aimeriez voir l'API dans le rôle de consommateur de celle-ci.
En cas de doute, essayez d'utiliser le "développement piloté par les tests" (TDD), qui vous obligera à réfléchir à l'API publique et à son utilisation.
Martin Fowler cite Kent Beck dans recommandant des méthodes séparées de setOn()
setOff()
, mais dit également que cela ne devrait pas être considéré comme inviolable:
Si vous extrayez des données [sic] d'une source booléenne, comme un contrôle d'interface utilisateur ou une source de données, je préfère avoir
setSwitch(aValue)
queif (aValue) setOn(); else setOff();
C'est un exemple qu'une API doit être écrite pour faciliter la tâche de l'appelant, donc si nous savons d'où vient l'appelant, nous devons concevoir l'API en tenant compte de ces informations. Cela fait également valoir que nous pouvons parfois fournir les deux styles si nous obtenons des appelants des deux manières.
Une autre recommandation est de tiliser une valeur énumérée ou un type de drapeaux pour donner à true
et false
de meilleurs noms spécifiques au contexte. Dans votre exemple, showHint
et hideHint
pourraient être mieux.
Je pense que vous mélangez deux choses dans votre article, l'API et l'implémentation. Dans les deux cas, je ne pense pas qu'il y ait une règle forte que vous puissiez utiliser tout le temps, mais vous devriez considérer ces deux choses indépendamment (autant que possible).
Commençons par l'API, à la fois:
public void setHint(boolean isHintOn)
et:
public void setHintOn()
public void setHintOff()
sont des alternatives valides en fonction de ce que votre objet est censé offrir et de la façon dont vos clients vont utiliser l'API. Comme Doc l'a souligné, si vos utilisateurs ont déjà une variable booléenne (à partir d'un contrôle d'interface utilisateur, d'une action utilisateur, d'un externe, d'une API, etc.), la première option a plus de sens, sinon vous forcez simplement une instruction if supplémentaire sur le code du client . Cependant, si par exemple vous changez le conseil en vrai au début d'un processus et en faux à la fin, la première option vous donne quelque chose comme ceci:
setHint(true)
// Do your process
…
setHint(false)
tandis que la deuxième option vous donne ceci:
setHintOn()
// Do your process
…
setHintOff()
quel IMO est beaucoup plus lisible, donc je vais aller avec la deuxième option dans ce cas. Évidemment, rien ne vous empêche d'offrir les deux options (ou plus, vous pouvez utiliser une énumération comme Graham l'a dit si cela a plus de sens par exemple).
Le fait est que vous devez choisir votre API en fonction de ce que l'objet est censé faire et de la façon dont les clients vont l'utiliser, et non en fonction de la façon dont vous allez l'implémenter.
Ensuite, vous devez choisir la façon dont vous implémentez votre API publique. Supposons que nous avons choisi les méthodes setHintOn
et setHintOff
comme API publique et qu'elles partagent cette logique commune comme dans votre exemple. Vous pouvez facilement résumer cette logique via une méthode privée (code copié depuis Doc):
private void setHint(boolean isHintOn){
this.layer1.visible=isHintOn;
this.layer2.visible=!isHintOn;
this.layer3.visible=isHintOn;
}
public void setHintOn(){
setHint(true);
}
public void setHintOff(){
setHint(false);
}
Inversement, supposons que nous avons choisi setHint(boolean isHintOn)
a notre API, mais inversons votre exemple, pour une raison quelconque, la définition de l'indice est complètement différente de sa désactivation. Dans ce cas, nous pourrions l'implémenter comme suit:
public void setHint(boolean isHintOn){
if(isHintOn){
// Set it On
} else {
// Set it Off
}
}
Ou même:
public void setHint(boolean isHintOn){
if(isHintOn){
setHintOn()
} else {
setHintOff()
}
}
private void setHintOn(){
// Set it On
}
private void setHintOff(){
// Set it Off
}
Le fait est que, dans les deux cas, nous avons d'abord choisi notre API publique, puis adapté notre implémentation pour l'adapter à l'API choisie (et aux contraintes que nous avons), et non l'inverse.
Soit dit en passant, je pense que la même chose s'applique au message que vous avez lié sur l'utilisation d'un paramètre booléen pour déterminer le comportement, c'est-à-dire que vous devriez décider en fonction de votre cas d'utilisation particulier plutôt que d'une règle stricte (bien que dans ce cas particulier généralement la bonne chose à faire est de le casser en plusieurs fonctions).
Tout d'abord: le code n'est pas automatiquement moins maintenable, simplement parce qu'il est un peu plus long. La clarté est ce qui compte.
Maintenant, si vous êtes vraiment juste en train de traiter des données, alors vous avez un setter pour une propriété booléenne. Dans ce cas, vous souhaiterez peut-être simplement stocker cette valeur directement et dériver les visibilités de la couche, c'est-à-dire.
bool isBackgroundVisible() {
return isHintVisible;
}
bool isContentVisible() {
return !isHintVisible;
}
(J'ai pris la liberté de donner aux couches des noms réels - si vous n'avez pas ceci dans votre code d'origine, je commencerais par ça)
Cela vous laisse toujours la question de savoir si vous avez une méthode setHintVisibility(bool)
. Personnellement, je recommanderais de le remplacer par une méthode showHint()
et hideHint()
- les deux seront vraiment simples et vous n'aurez pas à les changer lorsque vous ajoutez des couches. Cependant, ce n'est pas clairement le bien/le mal.
Maintenant, si l'appel de la fonction doit réellement modifier la visibilité de ces couches, vous avez en fait un comportement. Dans ce cas, je recommanderais certainement des méthodes distinctes.
Les paramètres booléens sont corrects dans le deuxième exemple. Comme vous l'avez déjà compris, les paramètres booléens ne sont pas en eux-mêmes problématiques. C'est un comportement de commutation basé sur un indicateur, ce qui est problématique.
Le premier exemple est cependant problématique, car la dénomination indique une méthode de définition, mais les implémentations semblent être quelque chose de différent. Vous avez donc l'anti-modèle de changement de comportement, et une méthode nommée de manière trompeuse. Mais si la méthode est en fait un setter normal (sans changement de comportement), alors il n'y a pas de problème avec setState(boolean)
. Ayant deux méthodes, setStateTrue()
et setStateFalse()
ne fait que compliquer inutilement les choses sans aucun avantage.
Une autre façon de résoudre ce problème consiste à introduire un objet pour représenter chaque indice et à faire en sorte que l'objet soit responsable de la détermination des valeurs booléennes associées à cet indice. De cette façon, vous pouvez ajouter de nouvelles permutations plutôt que d'avoir simplement deux états booléens.
Par exemple, en Java, vous pouvez faire:
public enum HintState {
SHOW_HINT(true, false, true),
HIDE_HINT(false, true, false);
private HintState(boolean layer1Visible, boolean layer2Visible, boolean layer3Visible) {
// constructor body and accessors omitted for clarity
}
}
Et puis votre code d'appel ressemblerait à ceci:
setHint(HintState.SHOW_HINT);
Et votre code d'implémentation ressemblerait à ceci:
public void setHint(HintState hint) {
this.layer1Visible = hint.isLayer1Visible();
this.layer2Visible = hint.isLayer2Visible();
this.layer3Visible = hint.isLayer3Visible();
}
Cela permet de garder le code d'implémentation et le code de l'appelant concis, en échange de la définition d'un nouveau type de données qui mappe clairement les caractères fortement typés, nommés intentions aux ensembles d'états correspondants. Je pense que c'est mieux tout autour.
Séparément de la question setOn() + setOff()
vs set(flag)
, je considérerais soigneusement si un type booléen est le meilleur ici. Êtes-vous sûr qu'il n'y aura jamais de troisième option?
Il pourrait être utile d'envisager une énumération au lieu d'un booléen. En plus de permettre l'extensibilité, cela rend également plus difficile d'obtenir le booléen dans le mauvais sens, par exemple:
setHint(false)
contre
setHint(Visibility::HIDE)
Avec l'énumération, il sera beaucoup plus facile d'étendre lorsque quelqu'un décide qu'il veut une option `` si nécessaire '':
enum class Visibility {
SHOW,
HIDE,
IF_NEEDED // New
}
contre
setHint(false)
setHint(true)
setHintAutomaticMode(true) // New
Selon ..., je sais qu'il est important d'éviter d'utiliser des paramètres booléens pour déterminer un comportement
Je suggère de réévaluer ces connaissances.
Tout d'abord, je ne vois pas la conclusion que vous proposez dans la question SE que vous avez liée. Ils parlent principalement de la transmission d'un paramètre sur plusieurs étapes d'appels de méthode, où il est évalué très loin dans la chaîne.
Dans votre exemple, vous évaluez le paramètre directement dans votre méthode. À cet égard, il ne diffère en rien de tout autre type de paramètre.
En général, il n'y a absolument rien de mal à utiliser des paramètres booléens; et évidemment tout paramètre déterminera un comportement, ou pourquoi l'auriez-vous en premier lieu?
Votre question de titre est "Est-ce mal de [...]?" - mais que voulez-vous dire par "mauvais"?.
Selon un C # ou Java, ce n'est pas faux. Je suis certain que vous en êtes conscient et ce n'est pas ce que vous demandez. J'ai peur à part ça, nous avons seulement n
programmeurs n+1
opinions différents. Cette réponse présente ce que le livre Clean Code a à dire à ce sujet.
Clean Code présente des arguments solides contre les arguments de fonction en général:
Les arguments sont difficiles. Ils prennent beaucoup de pouvoir conceptuel. [...] nos lecteurs auraient dû l'interpréter à chaque fois qu'ils le voyaient.
Un "lecteur" ici peut être le consommateur d'API. Il peut également s'agir du prochain codeur, qui ne sait pas encore ce que fait ce code - qui pourrait être vous dans un mois. Ils passeront soit par 2 fonctions séparément soit par 1 fonction deux fois , une fois en gardant true
et une fois false
en tête.
En bref, tilisez le moins d'arguments possible.
Le cas spécifique d'un argument indicateur est ensuite traité directement:
Les arguments de drapeau sont moches. Passer un booléen dans une fonction est une pratique vraiment terrible. Cela complique immédiatement la signature de la méthode, proclamant haut et fort que cette fonction fait plus d'une chose. Cela fait une chose si le drapeau est vrai et une autre si le drapeau est faux!
Afin de répondre directement à vos questions:
Selon Clean Code , il est recommandé d'éliminer ce paramètre.
Votre exemple est assez simple, mais même là, vous pouvez voir la simplicité se propager à votre code: les fonctions sans paramètre ne font que des affectations simples, tandis que l'autre fonction doit faire de l'arithmétique booléenne pour atteindre le même objectif. C'est une arithmétique booléenne triviale dans cet exemple simplifié, mais peut être assez complexe dans une situation réelle.
J'ai vu beaucoup d'arguments ici selon lesquels vous devriez le rendre dépendant de l'utilisateur de l'API, car avoir à le faire à de nombreux endroits serait stupide:
if (isAfterSunset) light.TurnOn();
else light.TurnOff();
Je suis d'accord que quelque chose de non optimal se produit ici. C'est peut-être trop évident à voir, mais votre toute première phrase mentionne "l'importance d'éviter [sic] d'utiliser des paramètres booléens pour déterminer un comportement" et c'est la base de toute la question. Je ne vois pas de raison de rendre cette chose mauvaise à faire plus facilement pour l'utilisateur de l'API.
Je ne sais pas si vous faites des tests - dans ce cas, pensez également à ceci:
Les arguments sont encore plus difficiles du point de vue des tests. Imaginez la difficulté d'écrire tous les cas de test pour vous assurer que toutes les différentes combinaisons d'arguments fonctionnent correctement. S'il n'y a pas d'arguments, c'est trivial.
Donc ma question est, si le paramètre booléen est utilisé pour déterminer uniquement les valeurs, mais pas les comportements (par exemple: pas de if-else sur ce paramètre), est-il toujours recommandé d'éliminer ce paramètre booléen?
Quand j'ai des doutes sur de telles choses. J'aime imaginer à quoi ressemblerait la trace de la pile.
Pendant de nombreuses années, j'ai travaillé sur un projet PHP qui utilisait la même fonction qu'un setter et getter. Si vous avez réussi null il retournerait la valeur, sinon le définir. C'était horrible de travailler avec.
Voici un exemple de trace de pile:
function visible() : line 440
function parent() : line 398
function mode() : line 384
function run() : line 5
Vous n'aviez aucune idée de l'interne état et cela a rendu le débogage plus difficile. Il y a un tas d'autres effets secondaires négatifs, mais essayez de voir qu'il y a valeur dans un verbose nom de la fonction et clarté lorsque les fonctions effectuent un - nique action.
Maintenant, imaginez travailler avec la trace de pile pour une fonction qui a un comportement A ou B basé sur une valeur booléenne.
function bar() : line 92
function setVisible() : line 120
function foo() : line 492
function setVisible() : line 120
function run() : line 5
C'est déroutant si vous me demandez. Les mêmes lignes setVisible
produisent deux chemins de trace différents.
Revenons donc à votre question. Essayez d'imaginer à quoi ressemblera la trace de la pile, comment elle communique à une personne ce qui se passe et demandez-vous si vous aidez une future personne à déboguer le code.
Voici quelques conseils:
Parfois, le code semble excessif sous un microscope, mais lorsque vous revenez à l'image plus grande, une approche minimaliste le fera disparaître. Si vous en avez besoin pour vous démarquer afin de pouvoir le maintenir. L'ajout de nombreuses petites fonctions peut sembler trop verbeux, mais il améliore la maintenabilité lorsqu'il est utilisé largement dans un contexte plus large.
Dans presque tous les cas où vous passez un paramètre boolean
à une méthode comme indicateur pour changer le comportement de quelque chose, vous devriez considérer une manière plus explicite et sécurisée de le faire.
Si vous ne faites rien de plus que d'utiliser un Enum
qui représente l'état, vous avez amélioré la compréhension de votre code.
Cet exemple utilise la classe Node
de JavaFX
:
public enum Visiblity
{
SHOW, HIDE
public boolean toggleVisibility(@Nonnull final Node node) {
node.setVisible(!node.isVisible());
}
}
est toujours meilleur que, comme on le trouve sur de nombreux objets JavaFX
:
public void setVisiblity(final boolean flag);
mais je pense que .setVisible()
et .setHidden()
sont la meilleure solution pour la situation où l'indicateur est un boolean
car c'est le plus explicite et le moins verbeux.
dans le cas de quelque chose avec plusieurs sélections, il est encore plus important de procéder de cette façon. EnumSet
existe juste pour cette raison.
Ed Mann a un très bon article de blog sur ce sujet. J'étais sur le point de paraphraser exactement ce qu'il dit, donc pour ne pas redoubler d'efforts, je posterai simplement un lien vers son article de blog comme addendum à cette réponse.
Lorsque vous décidez entre une approche pour une interface qui transmet un paramètre (booléen) et des méthodes surchargées sans ce paramètre, regardez les clients consommateurs.
Si toutes les utilisations passent des valeurs constantes (par exemple, vrai, faux), cela plaide pour les surcharges.
Si tous les usages passent une valeur variable, cela plaide pour la méthode avec l'approche des paramètres.
Si aucun de ces extrêmes ne s'applique, cela signifie qu'il y a un mélange d'utilisations des clients, vous devez donc choisir de prendre en charge les deux formulaires ou de faire en sorte qu'une catégorie de clients s'adapte à l'autre (ce qui pour eux est un style moins naturel).
Il y a deux considérations à concevoir:
Ils ne doivent pas être confondus.
Il est parfaitement possible de:
En tant que tel, tout argument qui tente d'équilibrer les coûts/avantages d'une conception d'API par les coûts/avantages d'une conception d'implémentation est douteux et doit être examiné attentivement.
Côté API
En tant que programmeur, je privilégierai généralement les API programmables. Le code est beaucoup plus clair lorsque je peux transmettre une valeur que lorsque j'ai besoin d'une instruction if
/switch
sur la valeur pour décider quelle fonction appeler.
Ce dernier peut être nécessaire si chaque fonction attend des arguments différents.
Dans votre cas, donc, une seule méthode setState(type value)
semble meilleure.
Cependant, il n'y a rien de pire que les noms sans nom true
, false
, 2
, Etc ... ces valeurs magiques n'ont aucune signification à elles seules. Évitez l'obsession primitive et adoptez un typage fort.
Ainsi, à partir d'un POV API, je veux: setState(State state)
.
Côté implémentation
Je recommande de faire tout ce qui est plus facile.
Si la méthode est simple, il vaut mieux la garder ensemble. Si le flux de contrôle est alambiqué, il est préférable de le séparer en plusieurs méthodes, chacune traitant d'un sous-cas ou d'une étape du pipeline.
Enfin, considérons groupement.
Dans votre exemple (avec des espaces supplémentaires pour plus de lisibilité):
this.layer1.visible = isHintOn;
this.layer2.visible = ! isHintOn;
this.layer3.visible = isHintOn;
Pourquoi layer2
Contrecarre la tendance? Est-ce une fonctionnalité ou un bug?
Serait-il possible d'avoir 2 listes [layer1, layer3]
Et [layer2]
, Avec un nom explicite indiquant la raison de leur regroupement ensemble, puis parcourez ces listes.
Par exemple:
for (auto layer : this.mainLayers) { // layer2
layer.visible = ! isHintOn;
}
for (auto layer : this.hintLayers) { // layer1 and layer3
layer.visible = isHintOn;
}
Le code parle de lui-même, il est clair pourquoi il y a deux groupes et leur comportement diffère.