J'ai un morceau de code où j'itère une carte jusqu'à ce qu'une certaine condition soit vraie et plus tard j'utilise cette condition pour faire plus de choses.
Exemple:
Map<BigInteger, List<String>> map = handler.getMap();
if(map != null && !map.isEmpty())
{
for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())
{
fillUpList();
if(list.size() > limit)
{
limitFlag = true;
break;
}
}
}
else
{
logger.info("\n>>>>> \n\t 6.1 NO entries to iterate over (for given FC and target) \n");
}
if(!limitFlag) // Continue only if limitFlag is not set
{
// Do something
}
Je sens que mettre un drapeau et ensuite l'utiliser pour faire plus de choses est une odeur de code.
Ai-je raison? Comment pourrais-je supprimer cela?
Il n'y a rien de mal à utiliser une valeur booléenne pour son objectif: enregistrer une distinction binaire.
Si on me disait de refactoriser ce code, je mettrais probablement la boucle dans une méthode qui lui est propre afin que l'affectation + break
se transforme en return
; alors vous n'avez même pas besoin d'une variable, vous pouvez simplement dire
if(fill_list_from_map()) {
...
Ce n'est pas nécessairement mauvais, et parfois c'est la meilleure solution. Mais définir des indicateurs comme celui-ci dans des blocs imbriqués peut rend le code difficile à suivre.
Le problème est que vous avez des blocs pour délimiter les étendues, mais vous avez ensuite des indicateurs qui communiquent entre les étendues, brisant l'isolement logique des blocs. Par exemple, le limitFlag
sera faux si le map
est null
, donc le code "faire quelque chose" sera exécuté si map
est null
. C'est peut-être ce que vous envisagez, mais ce pourrait être un bogue qui est facile à manquer, car les conditions de cet indicateur sont définies ailleurs, à l'intérieur d'une portée imbriquée. Si vous pouvez conserver les informations et la logique dans l'étendue la plus étroite possible, vous devriez essayer de le faire.
Je déconseille de raisonner sur les "odeurs de code". C'est juste la façon la plus paresseuse possible de rationaliser vos propres préjugés. Au fil du temps, vous développerez de nombreux préjugés, et beaucoup d'entre eux seront raisonnables, mais beaucoup d'entre eux seront stupides.
Au lieu de cela, vous devriez avoir des raisons pratiques (c'est-à-dire non dogmatiques) de préférer une chose à une autre, et éviter de penser que vous devriez avoir la même réponse pour toutes les questions similaires.
Les "odeurs de code" sont quand vous n'êtes pas en train de penser. Si vous pensez vraiment au code, faites-le bien!
Dans ce cas, la décision pourrait vraiment aller dans les deux sens en fonction du code environnant. Cela dépend vraiment de ce que vous pensez être la façon la plus claire de penser à ce que fait le code. (Un code "propre" est un code qui communique clairement ce qu'il fait aux autres développeurs et leur permet de vérifier facilement qu'il est correct)
Souvent, les gens écriront des méthodes structurées en phases, où le code déterminera d'abord ce qu'il doit savoir sur les données, puis agira en conséquence. Si la partie "déterminer" et la partie "agir sur elle" sont toutes les deux un peu compliquées, alors il peut être judicieux de le faire, et souvent "ce qu'il doit savoir" peut être transporté entre les phases dans les drapeaux booléens. Je préférerais vraiment que vous donniez un meilleur nom au drapeau. Quelque chose comme "largeEntryExists" rendrait le code beaucoup plus propre.
Si, en revanche, le code "// Do Something" est très simple, il peut être plus judicieux de le placer dans le bloc if
au lieu de définir un indicateur. Cela rapproche l'effet de la cause, et le lecteur n'a pas à analyser le reste du code pour s'assurer que l'indicateur conserve la valeur que vous définiriez.
Oui, c'est une odeur de code (cue downvotes de tous ceux qui le font).
L'essentiel pour moi est l'utilisation de l'instruction break
. Si vous ne l'utilisez pas, vous itérerez sur plus d'éléments que nécessaire, mais son utilisation donne deux points de sortie possibles de la boucle.
Ce n'est pas un problème majeur avec votre exemple, mais vous pouvez imaginer que lorsque le conditionnel ou les conditionnels à l'intérieur de la boucle deviennent plus complexes ou que l'ordre de la liste initiale devient important, il est plus facile pour un bogue de se glisser dans le code.
Lorsque le code est aussi simple que votre exemple, il peut être réduit à une boucle while
ou à une construction de filtre équivalente.
Lorsque le code est suffisamment complexe pour nécessiter des indicateurs et des ruptures, il sera sujet à des bugs.
Donc, comme pour toutes les odeurs de code: si vous voyez un indicateur, essayez de le remplacer par un while
. Si vous ne le pouvez pas, ajoutez des tests unitaires supplémentaires.
Définir une valeur booléenne pour transmettre des informations que vous aviez déjà est une mauvaise pratique à mon avis. S'il n'y a pas d'alternative facile, cela indique probablement un problème plus important comme une mauvaise encapsulation.
Vous devez déplacer la logique de la boucle for dans la méthode fillUpList pour la faire rompre si la limite est atteinte. Vérifiez ensuite la taille de la liste directement après.
Si cela casse votre code, pourquoi?
Utilisez simplement un nom autre que limitFlag qui indique ce que vous vérifiez réellement. Et pourquoi enregistrez-vous quelque chose lorsque la carte est absente ou vide? limtFlag sera faux, tout ce dont vous vous souciez. La boucle est très bien si la carte est vide, donc pas besoin de vérifier cela.
Commençons par le cas général: l'utilisation d'un indicateur pour vérifier si un élément d'une collection remplit une certaine condition n'est pas rare. Mais le modèle que j'ai vu le plus souvent pour résoudre ce problème consiste à déplacer le chèque dans une méthode supplémentaire et à en revenir directement (comme Kilian Foth décrit dans sa réponse ):
private <T> boolean checkCollection(Collection<T> collection)
{
for (T element : collection)
if (checkElement(element))
return true;
return false;
}
Depuis Java 8, il existe un moyen plus concis en utilisant Stream.anyMatch(…)
:
collection.stream().anyMatch(this::checkElement);
Dans votre cas, cela ressemblerait probablement à ceci (en supposant list == entry.getValue()
dans votre question):
map.values().stream().anyMatch(list -> list.size() > limit);
Le problème dans votre exemple spécifique est l'appel supplémentaire à fillUpList()
. La réponse dépend beaucoup de ce que cette méthode est censée faire.
Note latérale: En l'état, l'appel à fillUpList()
n'a pas beaucoup de sens, car il ne dépend pas de l'élément que vous êtes en train d'itérer. Je suppose que c'est une conséquence de la suppression de votre code réel pour l'adapter au format de la question. Mais cela donne exactement un exemple artificiel difficile à interpréter et donc difficile à raisonner. Par conséquent, il est si important de fournir un Minimal, Complete , and Verifiable exemple .
Je suppose donc que le code réel transmet le entry
actuel à la méthode.
Mais il y a plus de questions à poser:
BigInteger
? S'ils ne sont pas vides, pourquoi devez-vous remplir les listes? Lorsqu'il y a déjà des éléments dans la liste, n'est-ce pas un update ou un autre calcul dans ce cas?Ce ne sont que quelques questions qui me sont venues à l'esprit lorsque j'ai essayé de comprendre le fragment de code. Donc, à mon avis, c'est la vraie odeur de code: Votre code ne communique pas clairement l'intention.
Cela peut signifier cela ("tout ou rien" et atteindre la limite indique une erreur):
/**
* Computes the list of all foo strings for each passed number.
*
* @param numbers the numbers to process. Must not be {@code null}.
* @return all foo strings for each passed number. Never {@code null}.
* @throws InvalidArgumentException if any number produces a list that is too long.
*/
public Map<BigInteger, List<String>> computeFoos(Set<BigInteger> numbers)
throws InvalidArgumentException
{
if (numbers.isEmpty())
{
// Do you actually need to log this here?
// The caller might know better what to do in this case...
logger.info("Nothing to compute");
}
return numbers.stream().collect(Collectors.toMap(
number -> number,
number -> computeListForNumber(number)));
}
private List<String> computeListForNumber(BigInteger number)
throws InvalidArgumentException
{
// compute the list and throw an exception if the limit is exceeded.
}
Ou cela peut signifier cela ("mise à jour jusqu'au premier problème"):
/**
* Refreshes all foo lists after they have become invalid because of bar.
*
* @param map the numbers with all their current values.
* The values in this map will be modified.
* Must not be {@code null}.
* @throws InvalidArgumentException if any new foo list would become too long.
* Some other lists may have already been updated.
*/
public void updateFoos(Map<BigInteger, List<String>> map)
throws InvalidArgumentException
{
map.replaceAll(this::computeUpdatedList);
}
private List<String> computeUpdatedList(
BigInteger number, List<String> currentValues)
throws InvalidArgumentException
{
// compute the new list and throw an exception if the limit is exceeded.
}
Ou ceci ("mettre à jour toutes les listes mais conserver la liste d'origine si elle devient trop grande"):
/**
* Refreshes all foo lists after they have become invalid because of bar.
* Lists that would become too large will not be updated.
*
* @param map the numbers with all their current values.
* The values in this map will be modified.
* Must not be {@code null}.
* @return {@code true} if all updates have been successful,
* {@code false} if one or more elements have been skipped
* because the foo list size limit has been reached.
*/
public boolean updateFoos(Map<BigInteger, List<String>> map)
{
boolean allUpdatesSuccessful = true;
for (Entry<BigInteger, List<String>> entry : map.entrySet())
{
List<String> newList = computeListForNumber(entry.getKey());
if (newList.size() > limit)
allUpdatesSuccessful = false;
else
entry.setValue(newList);
}
return allUpdatesSuccessful;
}
private List<String> computeListForNumber(BigInteger number)
{
// compute the new list
}
Ou même ce qui suit (en utilisant computeFoos(…)
du premier exemple mais sans exception):
/**
* Processes the passed numbers. An optimized algorithm will be used if any number
* produces a foo list of a size that justifies the additional overhead.
*
* @param numbers the numbers to process. Must not be {@code null}.
*/
public void process(Collection<BigInteger> numbers)
{
Map<BigInteger, List<String>> map = computeFoos(numbers);
if (isLimitReached(map))
processLarge(map);
else
processSmall(map);
}
private boolean isLimitReached(Map<BigInteger, List<String>> map)
{
return map.values().stream().anyMatch(list -> list.size() > limit);
}
Ou cela pourrait signifier quelque chose de complètement différent… ;-)