Il punto di uscita singolo incontra i flussi Java

1

Al momento abbiamo una discussione delicata in azienda che tocca un paio di vecchie discussioni basate sull'opinione pubblica. Tuttavia, vorrei discutere il caso in questo forum per stabilire alcune linee guida ragionevoli sul codice. Supponiamo di avere le seguenti due classi di classi legacy non modificabili:

 public class SomeValue {

    private String value;

    public SomeValue(final String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

public class SomeCondition {

    private SomeValue a;
    private SomeValue b;

    public SomeCondition(final SomeValue a, final SomeValue b) {
        this.a = a;
        this.b = b;
    }

    public boolean isA() {
        return a != null;
    }

    public boolean isB() {
        return b != null;
    }

    public String getAOrBValue() {
        if (isA()) {
            return a.getValue();
        }
        if (isB()) {
            return b.getValue();
        }
        return null;
    }
}

Ora, in discussione è lo stile migliore per un metodo che controlla se qualcuno di aob è impostato e restituisce il suo valore (prioritizzando a). Le due versioni in offerta sono quelle:

Versione 1:

    protected String getValue(final List<SomeCondition> conditions) {
    if (isNotEmpty(conditions)) {
        for (SomeConditions conditions : condition) {

            if (condition.isA())) {
                return a.getValue();
            }

            if (condition.isB()) {
                return b.getValue();
            }
        }
    }

    return "default";
}

I critici di questa versione dicono che i ritorni all'interno di più istruzioni if contenute in un ciclo for non solo nascondono istruzioni else ma anche un'istruzione break. Se si tentasse di ottenere un ritorno alla fine del metodo, si vedrebbe tutta questa complessità nascosta che sicuramente costringerebbe uno al refactoring. Se si dovesse risolvere il problema con questo metodo, si sarebbe in guai seri.

Versione 2:

protected String getValue(final List<SomeCondition> conditions) {
    return conditions
    .stream()
    .map(this::getAOrBValue)
    .filter(match -> match != null)
    .findFirst()
    .orElse("default");
}

I critici di questa versione dicono che questo codice è difficile da leggere poiché utilizza in modo errato l'API di java streaming per gestire qualcosa che di solito sarebbe coperto da semplici istruzioni for e if. Inoltre, l'algoritmo di filtraggio è più difficile da capire poiché è distribuito su due classi.

Sono consapevole che parte di questa discussione è basata sull'opinione pubblica. Tuttavia, ritengo che esistano alcune convenzioni di programmazione ben consolidate che potrebbero aiutarci, decidere quale dei due stili vogliamo rendere ufficiali nelle nostre linee guida sul codice. Qualcuno può far luce sull'argomento?

    
posta Fritz Duchardt 21.04.2016 - 10:10
fonte

3 risposte

4

it misuses java streaming api to handle something that usually would be covered by simple for and if statements

Questo è un argomento vacuo. Tutto ciò che l'API di streaming fa tranne la parallelizzazione può essere gestito da per e se le istruzioni. Se siano semplici è una questione di gusti; IMO il tuo primo esempio non è già semplice. (Sei riuscito anche a inserire un errore di compilazione e un ridondante, nella mia esperienza è un segno di complessità non necessaria se la ricreazione di una domanda su SO contiene già bug.)

Detto questo, il confronto non è corretto, dal momento che la prima versione può essere semplificata.

protected String getValue(final List<SomeCondition> conditions) {
    for (SomeCondition condition : conditions) {
        String value = condition.getAOrBValue();
        if (value != null)
            return value;
    }

    return "default";
}

Non ha senso fare un controllo per una raccolta vuota prima del ciclo; se è vuoto, il ciclo semplicemente non funzionerà. E non usare il metodo getAOrBValue , che già confeziona il doppio condizionale con la prioritizzazione di A, sta solo rendendo più difficile il tuo lavoro.

D'altra parte, la versione di streaming può anche essere migliorata molto aggiungendo una funzione di aiuto molto generica da qualche parte e usando quella. (Nota: sono un po 'arrugginito con i generici Java, potrebbero esserci errori di sintassi qui.)

// In some StreamUtility class
public static <T, U> Stream<T> mapNonNull(Stream<U> stream, Function<? super U, T> function) {
  return stream.map(function).filter(x => x != null);
}

// In your class
import static StreamUtility.mapNonNull;

protected String getValue(final List<SomeCondition> conditions) {
  return mapNonNull(conditions.stream(), Condition::getAOrBValue)
    .findFirst().orElse("default");
}

A parte la perdita della sintassi della bella catena, questa versione è molto più leggibile perché impacchetta l'operazione "map and filter", che è la fonte della maggior parte della complessità.

Naturalmente, uno degli argomenti contro la versione non stream è anche privo di senso:

If one tried to have one return at the end of the method, one would see all this hidden complexity definitely forcing one to refactor.

La domanda è: perché vorresti mai fare questo? L'uscita singola non è stata un obiettivo per la maggior parte dei programmatori da molto tempo.

Alla fine, dato che è possibile ridurre la versione loop a un singolo se, e Java non supporta i metodi di estensione piuttosto, andrei per la versione loop in questa particolare istanza. Tuttavia, questo è un giudizio solo per questo caso, e non un'opinione sui flussi in generale.

    
risposta data 21.04.2016 - 11:30
fonte
0

Definirò la tua politica aziendale attorno alla leggibilità e di conseguenza giudicherò tutto basato su questo, piuttosto che una politica specifica di avere un punto di uscita (o meno). Il codice ha molti più lettori che gli scrittori e tu vuoi che il codice sia facilmente comprensibile.

    
risposta data 21.04.2016 - 12:57
fonte
-1

L'ultima opzione è decisamente più difficile da aggiornare o modificare e poiché nasconde ciò che c'è dietro a una funzione simile a linq potrebbe essere difficile eseguire il debug. Per risolvere il problema "Se si tentasse di ottenere un ritorno alla fine del metodo, si vedrebbe tutta questa complessità nascosta costringendo decisamente uno a refactoring" problema avrei fatto qualcosa di simile:

protected String getValue(final List<SomeCondition> conditions) 
{
    if (isNotEmpty(conditions)) 
    {
        List<String> values = new List<String>();

        for (int i = 0; i < conditions.size(); i++) 
        {

            if (condition.get(i).isA())) 
            {
                values.add(a.getValue());
            }
            else if (condition.isB()) 
            {
                values.add(b.getValue());
            }
            else    
                values.add("default");
         }
    }

    return values.get(0);
}

In questo modo la funzione è molto facile da modificare. È possibile aggiungere facilmente una nuova condizione con un altro se e inserire qualsiasi logica prima del ritorno. È anche possibile restituire un altro valore che esegue i criteri anziché il primo o utilizzare quei valori per i calcoli. La mia preoccupazione è un'altra. Innanzitutto non è chiaro dove si ottiene a o b per chiamare il metodo getValue. E secondo nella classe legacy SomeCondition hai già un metodo public String getAOrBValue() che fa la stessa cosa che tu e i tuoi colleghi state cercando di fare con una nuova funzione. Quindi, perché non creare qualcosa di simile?

protected String getValue(final List<SomeCondition> conditions) 
{
    if (isNotEmpty(conditions)) 
    {
        List<String> values = new List<String>();
        for (SomeConditions condition : conditions) 
        {
            values.add(condition.getAOrBValue());         
        }
    }

    //you can add some filter logic on values here
    return values.get(0);
}
    
risposta data 21.04.2016 - 11:17
fonte

Leggi altre domande sui tag