Java >> Tutoriel Java >  >> Java

Arrêtez d'utiliser des drapeaux pour contrôler vos boucles

En tant que personne qui regarde beaucoup de code pour débutant, je vois souvent des étudiants utiliser des drapeaux pour terminer des boucles. Bien qu'il n'y ait rien de mal en soi à cette pratique, et parfois cela peut même être nécessaire, je trouve que le code est souvent plus facile à écrire et à lire en utilisant une approche différente. Parlons-en !

Qu'est-ce qu'un drapeau ?

Dans le code, un indicateur est une variable booléenne (cependant, c'est une définition assez vague). C'est ça! Voici quelques exemples :

boolean isHurt = false;
boolean onCooldown = true;
boolean hasStrength = false;

En général, je peux voir pourquoi ils sont attrayants. Après tout, lorsque je crée un drapeau qui a un nom descriptif, cela peut rendre mon code plus facile à lire. Et bien sûr, les booléens ont beaucoup de sens :ils ont deux états. Par exemple, d'après l'un des exemples ci-dessus, soit je suis blessé, soit je ne le suis pas.

Cela dit, le plus souvent, je trouve que les drapeaux sont gênants dans le code, en particulier les boucles. Parlons-en !

Qu'est-ce qui ne va pas avec les drapeaux ?

Il y a plusieurs raisons pour lesquelles je n'aime pas les drapeaux. Premièrement, les variables booléennes en général ne stockent pas beaucoup d'informations :seulement vrai ou faux. En conséquence, ils ne s'adaptent pas bien en tant que variables. Par exemple, que se passerait-il si j'avais un match où je voulais savoir si mon joueur était blessé ? Nous pourrions avoir un indicateur booléen qui nous le dit. Cependant, je dirais qu'il est préférable de créer une méthode qui nous indique si le joueur est blessé en fonction de ses PV :

public boolean isHurt() {
  return this.hp < this.maxHP;
}

Ici, les informations sur les blessures proviennent des HP, et non d'un booléen que nous devons mettre à jour à chaque fois que des événements liés aux HP se produisent (par exemple, chaque fois que vous êtes attaqué, buvez une potion et subissez des dégâts à l'environnement). Cela fait beaucoup de mises à jour !

Bien sûr, c'est ma plainte générale à propos des drapeaux. L'article d'aujourd'hui se concentre sur l'utilisation des drapeaux dans les boucles, en particulier les conditions de boucle. Par exemple, nous pourrions avoir une boucle qui inflige des dégâts décroissants au fil du temps à notre joueur. La boucle ne devrait se terminer que si le joueur meurt ou si la maladie tombe à zéro, nous pourrions donc écrire quelque chose comme ceci :

boolean isDead = false;
boolean isCured = false;
while (!isDead && !isCured) {
  this.hp -= this.maxHP * this.dot;
  this.dot -= .01;
  if (this.hp <= 0) {
    isDead = true;
  }
  if (this.dot <= 0) {
    isCured = true;
  }
}

Maintenant, les développeurs expérimentés pourraient dire :« hé, attendez une minute ! Vous pouvez simplifier les instructions if. (Cependant, ils pourraient aussi souligner la double comparaison avec des égaux…). En conséquence, vous obtenez quelque chose comme ceci :

boolean isDead = false;
boolean isCured = false;
while (!isDead && !isCured) {
  this.hp -= this.maxHP * this.dot;
  this.dot -= .01;
  isDead = this.hp <= 0;
  isCured = this.dot <= 0;
}

Pour être juste, c'est beaucoup plus propre. Cependant, je n'aime pas ça parce que nous devons creuser dans la boucle pour comprendre comment ces conditions sont calculées. Au lieu de cela, je préférerais quelque chose comme ceci :

while (this.hp > 0 && this.dot > 0) {
  this.hp -= this.maxHP * this.dot;
  this.dot -= .01;
}

Et pour en revenir à ce que j'ai dit précédemment, je ferais probablement de ces conditions de boucle des méthodes :

while (!this.isDead() && !this.hasDot()) {
  this.hp -= this.maxHP * this.dot;
  this.dot -= .01;
}

Quoi qu'il en soit, le fait est que je devrais pouvoir examiner la condition de la boucle pour savoir quand la boucle se terminera. Avoir des drapeaux vagues ne me donne pas une idée de comment ou quand ils seront mis à jour. Sans oublier qu'ils doivent être mis à jour régulièrement à divers endroits dans le code. Ne pas le faire entraînera des bogues.

Aller un peu plus loin :les invariants de boucle

À ce stade, vous n'êtes peut-être pas totalement convaincu de la suppression des drapeaux de votre code. C'est très bien. Cependant, permettez-moi de partager avec vous un dernier concept :les invariants de boucle.

Un invariant de boucle est une déclaration qui est vraie sur une boucle avant, pendant et après la boucle. D'après mon expérience, ces types de déclarations sont quelque peu difficiles à écrire, mais elles ont un peu de récompense. Voyez, avec un invariant de boucle fort, nous pouvons tracer une boucle en une seule étape (sans jamais voir à l'intérieur de la boucle). Par exemple, voici un simple invariant de boucle :

int x = 3;
int y = 7;
// x + y = 10 
while (x != 6) { ... }

Ici, nous avons une boucle où nous ne savons pas ce qui se passe à l'intérieur. Cependant, nous savons que deux variables changent :x et y . Nous le savons car la condition de boucle dépend de x devenant égal à 6 et x et y doivent totaliser 10. Avec ces informations, nous pouvons garantir que la valeur finale de x est 6 (en supposant que la boucle se termine). Par conséquent, y doit être 4 pour maintenir l'invariant de la boucle.

A résoudre pour y , nous avions besoin de deux informations :l'invariant de boucle et la condition de boucle. Si pour une raison quelconque la condition de boucle manque d'informations critiques, l'invariant de boucle devient inutile. Par exemple, que se passerait-il si nous avions la même boucle mais qu'elle ressemblait à ceci :

int x = 3;
int y = 7;
// x + y = 10 
while (!stop) { ... }

Dans ce cas, nous n'avons aucune idée de ce que stop signifie, comment il est calculé ou comment il est mis à jour. Par conséquent, nous ne savons pas quand la boucle se terminera, nous ne pouvons donc pas résoudre pour x ou y . En fait, tout ce que nous savons, c'est que les deux variables totaliseront 10, donc l'invariant de boucle lui-même est quelque peu inutile.

Cela dit, les gens en général n'écrivent pas d'invariants de boucle, alors qui s'en soucie ? Eh bien, les invariants de boucle nous obligent à écrire des conditions de boucle explicites qui conduisent en fait à des boucles mieux formées. Je sais que la logique est quelque peu circulaire, mais l'idée que la condition de boucle et le corps de la boucle sont séparés suit une meilleure pratique plus large :découplage .

Lorsque nous découplons le corps de la boucle de la condition de la boucle, nous savons toujours où chercher en cas de problème. Plus précisément, nous savons exactement quand notre boucle se terminera strictement en regardant la condition. Par conséquent, si nous avons une boucle infinie, il y a de fortes chances que le problème se situe dans le corps. De même, si jamais nous devons repenser notre condition de boucle, nous n'avons pas besoin de rechercher des branches et des drapeaux dans le corps pour nous assurer que tout fonctionne.

Suivez mes conseils avec un grain de sel

Dans le monde de l'écriture, tout doit suivre la tendance du clickbait. Naturellement, cet article n'est pas différent. Mon approche générale du développement logiciel consiste à faire ce qui a du sens pour vous. Cela dit, les gens lisent mon travail si je n'utilise pas de titres sensationnels comme "NEVER USE FLAGS" ou quelque chose comme ça. Dans ce cas, n'hésitez pas à les utiliser au besoin. Mon seul conseil est que vous trouverez peut-être des boucles plus faciles à écrire et à déboguer si votre condition de boucle est au même endroit .

Cela dit, merci d'être resté dans les parages ! Si vous aimez ce genre de choses, consultez certains de ces articles connexes :

  • Améliorer la lisibilité du code en utilisant les modes de paramètres
  • Résumer les croyances de la communauté autour du code de commentaire

De même, envisagez de montrer votre soutien en vous rendant sur ma liste de moyens de développer le site. Vous y trouverez des liens vers ma newsletter et Patreon. Sinon, prenez soin de vous !


Balise Java