È un odore di codice impostare un flag in un loop per usarlo in seguito?


30

Ho un pezzo di codice in cui eseguo l'iterazione di una mappa fino a quando una determinata condizione è vera e successivamente utilizzo quella condizione per fare altre cose.

Esempio:

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
}

Sento di impostare una bandiera e poi usarla per fare più cose è un odore di codice.

Ho ragione? Come posso rimuoverlo?


10
Perché ritieni che sia un odore di codice? che tipo di problemi specifici puoi prevedere nel fare ciò che non si verificherebbe in una struttura diversa?
Ben Cottrell,

13
@ gnasher729 Solo per curiosità, quale termine useresti invece?
Ben Cottrell,

11
-1, il tuo esempio non ha senso. entrynon viene utilizzato da nessuna parte all'interno del ciclo delle funzioni e possiamo solo immaginare cosa listsia. È fillUpListsupposto per riempire list? Perché non lo ottiene come parametro?
Doc Brown,

13
Riconsidererei il tuo uso di spazi bianchi e linee vuote.
Daniel Jour,

11
Non esistono odori di codice. "Odore di codice" è un termine inventato dagli sviluppatori di software che vogliono tenersi il naso quando vedono un codice che non soddisfa i loro standard elitari.
Robert Harvey,

Risposte:


70

Non c'è niente di sbagliato nell'usare un valore booleano per lo scopo previsto: registrare una distinzione binaria.

Se mi dicessero di refactoring questo codice, probabilmente metterei il loop in un metodo a sé stante in modo che l'assegnazione + breaksi trasformi in a return; allora non hai nemmeno bisogno di una variabile, puoi semplicemente dire

if(fill_list_from_map()) {
  ...

6
In realtà l'odore nel suo codice è la funzione lunga che deve essere suddivisa in funzioni più piccole. Il tuo suggerimento è la strada da percorrere.
Bernhard Hiller il

2
Una frase migliore che descrive l'utile funzione della prima parte di quel codice è scoprire se il limite verrà superato dopo aver accumulato qualcosa da quegli elementi mappati. Possiamo anche presumere che fillUpList()sia un codice (che OP decide di non condividere) che utilizza effettivamente il valore entrydall'iterazione; senza questo presupposto, sembrerebbe che il corpo del ciclo non abbia usato nulla dall'iterazione del ciclo.
rwong

4
@Kilian: ho solo una preoccupazione. Questo metodo riempie un elenco e restituirà un valore booleano che rappresenta che la dimensione dell'elenco supera o meno un limite, quindi il nome 'fill_list_from_map' non chiarisce che cosa rappresenta il valore booleano restituito (non è riuscito a riempire, un il limite supera, ecc.). Poiché il booleano è tornato è per un caso speciale che non è ovvio dal nome della funzione. Qualche commento? PS: possiamo prendere in considerazione anche la separazione delle query di comando.
Siddharth Trikha,

2
@SiddharthTrikha Hai ragione, e ho avuto la stessa identica preoccupazione quando ho suggerito quella linea. Ma non è chiaro quale elenco debba compilare il codice. Se è sempre lo stesso elenco, non hai bisogno della bandiera, puoi semplicemente controllarne la lunghezza in seguito. Se hai bisogno di sapere se un singolo riempimento ha superato il limite, devi in ​​qualche modo trasportare tali informazioni al di fuori, e il principio di separazione dei comandi / query IMO non è un motivo sufficiente per rifiutare il modo ovvio: tramite il reso valore.
Kilian Foth,

6
Lo zio Bob dice a pagina 45 di Clean Code : "Le funzioni dovrebbero o fare qualcosa o rispondere a qualcosa, ma non entrambe. O la tua funzione dovrebbe cambiare lo stato di un oggetto, oppure dovrebbe restituire alcune informazioni su quell'oggetto. Fare entrambe le cose porta spesso a confusione."
CJ Dennis,

25

Non è necessariamente male, e talvolta è la soluzione migliore. Ma impostare flag come questo in blocchi nidificati può rendere il codice difficile da seguire.

Il problema è che hai dei blocchi per delimitare gli ambiti, ma poi hai dei flag che comunicano tra gli ambiti, interrompendo l'isolamento logico dei blocchi. Ad esempio, limitFlagsarà falso se mapè null, quindi il codice "fai qualcosa" verrà eseguito se lo mapè null. Questo può essere quello che intendi, ma potrebbe essere un bug che è facile perdere, perché le condizioni per questo flag sono definite altrove, all'interno di un ambito nidificato. Se riesci a mantenere le informazioni e la logica all'interno dell'ambito più stretto possibile, dovresti provare a farlo.


2
Questo è il motivo per cui ho sentito che è un odore di codice, poiché i blocchi non sono completamente isolati e possono essere difficili da rintracciare in seguito. Quindi immagino che il codice nella risposta di @Kilian sia il più vicino che possiamo ottenere?
Siddharth Trikha,

1
@SiddharthTrikha: È difficile da dire poiché non so cosa dovrebbe fare il codice. Se vuoi solo verificare se la mappa contiene almeno un elemento con un elenco più grande del limite, penso che puoi farlo con una singola espressione anyMatch.
Jacques B

2
@SiddharthTrikha: il problema dell'ambito può essere facilmente risolto cambiando il test iniziale in una clausola di protezione come if(map==null || map.isEmpty()) { logger.info(); return;}This, tuttavia funzionerà solo se il codice che vediamo è il corpo completo di una funzione e la // Do somethingparte non è richiesta nel caso in cui la mappa è nullo o vuoto.
Doc Brown,

14

Vorrei sconsigliare il ragionamento su "odori di codice". Questo è solo il modo più pigro possibile di razionalizzare i tuoi pregiudizi. Nel tempo svilupperai molti pregiudizi, e molti di loro saranno ragionevoli, ma molti di loro saranno stupidi.

Invece, dovresti avere ragioni pratiche (cioè non dogmatiche) per preferire una cosa a un'altra ed evitare di pensare che dovresti avere la stessa risposta per tutte le domande simili.

Gli "odori di codice" sono per quando non stai pensando. Se penserai davvero al codice, fallo nel modo giusto!

In questo caso, la decisione potrebbe davvero andare in entrambi i modi a seconda del codice circostante. Dipende davvero da quello che pensi sia il modo più chiaro di pensare a quello che sta facendo il codice. (il codice "pulito" è un codice che comunica chiaramente cosa sta facendo agli altri sviluppatori e rende facile per loro verificare che sia corretto)

Molte volte, le persone scriveranno metodi strutturati in fasi, in cui il codice determinerà prima ciò che deve sapere sui dati e quindi agirà su di essi. Se la parte "determina" e la parte "agisci su di essa" sono entrambe un po 'complicate, allora può avere senso farlo, e spesso il "ciò che deve sapere" può essere portato tra le fasi nelle bandiere booleane. Preferirei davvero che tu abbia dato un nome migliore alla bandiera. Qualcosa come "largeEntryExists" renderebbe il codice molto più pulito.

Se, d'altra parte, il codice "// Do Something" è molto semplice, può avere più senso inserirlo all'interno del ifblocco invece di impostare un flag. Ciò avvicina l'effetto alla causa e il lettore non deve scansionare il resto del codice per assicurarsi che il flag mantenga il valore che imposteresti.


5

Sì, è un odore di codice (cue downvotes da chiunque lo faccia).

La cosa fondamentale per me è l'uso breakdell'affermazione. Se non lo avessi usato, avresti ripetuto più elementi del necessario, ma utilizzarlo fornisce due possibili punti di uscita dal loop.

Non è un grosso problema con il tuo esempio, ma puoi immaginare che man mano che il condizionale o i condizionali all'interno del ciclo diventano più complessi o l'ordinamento dell'elenco iniziale diventa importante, allora è più facile che un bug si insinui nel codice.

Quando il codice è semplice come il tuo esempio, può essere ridotto a un whileciclo o a una mappa equivalente, costruire un filtro.

Quando il codice è abbastanza complesso da richiedere flag e interruzioni, sarà soggetto a bug.

Come per tutti gli odori di codice: se vedi un flag, prova a sostituirlo con a while. Se non è possibile, aggiungere ulteriori test unitari.


+1 da me. È sicuramente un odore di codice e tu dici bene perché e come gestirlo.
David Arno,

@Ewan: SO as with all code smells: If you see a flag, try to replace it with a whilepuoi approfondire questo con un esempio?
Siddharth Trikha,

2
Avere più punti di uscita dal loop può rendere più difficile ragionare, ma in questo caso lo farebbe refactoring per far dipendere la condizione del loop dal flag - significherebbe sostituirlo for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())con for (Iterator<Map.Entry<BigInteger, List<String>>> iterator = map.entrySet().iterator(); iterator.hasNext() && !limitFlag; Map.Entry<BigInteger, List<String>> entry = iterator.next()). Questo è un modello abbastanza insolito che avrei più difficoltà a capirlo di una pausa relativamente semplice.
James_pic,

@James_pic my java è un po 'arrugginito, ma se stiamo usando le mappe, vorrei usare un raccoglitore per riassumere il numero di elementi e filtrare quelli dopo il limite. Tuttavia, come ho detto l'esempio è "non così male" un odore di codice è una regola generale che ti avverte di un potenziale problema. Non è una legge sacra a cui devi sempre obbedire
Ewan,

1
Non vuoi dire "stecca" piuttosto che "coda"?
psmears

0

Basta usare un nome diverso da limitFlag che indica ciò che si sta effettivamente verificando. E perché registri qualcosa quando la mappa è assente o vuota? limtFlag sarà falso, tutto ciò che ti interessa. Il ciclo va bene se la mappa è vuota, quindi non è necessario verificarlo.


0

Impostare un valore booleano per trasmettere informazioni che già possedevi è una cattiva pratica secondo me. Se non esiste un'alternativa facile, probabilmente è indicativo di un problema più grande come l'incapsulamento scadente.

È necessario spostare la logica del ciclo for nel metodo fillUpList per interromperla se viene raggiunto il limite. Quindi controlla la dimensione dell'elenco subito dopo.

Se questo rompe il tuo codice, perché?


0

Prima di tutto il caso generale: l'uso di un flag per verificare se alcuni elementi di una raccolta soddisfano una determinata condizione non è raro. Ma lo schema che ho visto più spesso per risolvere questo problema è spostare il controllo in un metodo aggiuntivo e tornare direttamente da esso (come descritto da Kilian Foth nella sua risposta ):

private <T> boolean checkCollection(Collection<T> collection)
{
    for (T element : collection)
        if (checkElement(element))
            return true;
    return false;
}

Da Java 8 esiste un modo più conciso di utilizzare Stream.anyMatch(…):

collection.stream().anyMatch(this::checkElement);

Nel tuo caso questo probabilmente assomiglierebbe a questo (supponendo list == entry.getValue()nella tua domanda):

map.values().stream().anyMatch(list -> list.size() > limit);

Il problema nel tuo esempio specifico è la chiamata aggiuntiva a fillUpList(). La risposta dipende molto da cosa dovrebbe fare questo metodo.

Nota a margine: così com'è, la chiamata a fillUpList()non ha molto senso, perché non dipende dall'elemento che stai ripetendo. Immagino che questa sia una conseguenza della riduzione del codice effettivo per adattarlo al formato della domanda. Ma esattamente ciò porta a un esempio artificiale che è difficile da interpretare e quindi difficile da ragionare. Perciò è importante fornire un minimo, completa e verificabile esempio .

Quindi presumo che il codice effettivo passi la corrente entryal metodo.

Ma ci sono altre domande da porre:

  • Gli elenchi nella mappa sono vuoti prima di raggiungere questo codice? In tal caso, perché esiste già una mappa e non solo l'elenco o il set di BigIntegerchiavi? Se non sono vuoti, perché è necessario compilare gli elenchi? Quando ci sono già elementi nell'elenco, non è un aggiornamento o qualche altro calcolo in questo caso?
  • Cosa fa sì che un elenco diventi più grande del limite? Si tratta di una condizione di errore o si prevede che accada spesso? È causato da un input non valido?
  • Hai bisogno degli elenchi calcolati fino al punto in cui raggiungi un elenco più grande del limite?
  • Cosa fa la parte " Fai qualcosa "?
  • Riavvia il riempimento dopo questa parte?

Queste sono solo alcune domande che mi sono venute in mente quando ho cercato di capire il frammento di codice. Quindi, secondo me, questo è il vero odore del codice : il tuo codice non comunica chiaramente l'intento.

Potrebbe significare questo ("tutto o niente" e raggiungere il limite indica un errore):

/**
 * 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.
}

Oppure potrebbe significare questo ("aggiornamento fino al primo problema"):

/**
 * 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.
}

O questo ("aggiorna tutti gli elenchi ma mantieni l'elenco originale se diventa troppo 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
}

O anche il seguente (usando computeFoos(…)dal primo esempio ma senza eccezioni):

/**
 * 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);
}

Oppure potrebbe significare qualcosa di completamente diverso ... ;-)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.