Come posso modificare una catena di istruzioni if-else if per aderire ai principi del codice pulito di zio Bob?


45

Sto cercando di seguire i suggerimenti sul codice pulito di zio Bob e in particolare di mantenere i metodi brevi.

Mi trovo incapace di abbreviare questa logica però:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

Non riesco a rimuovere gli altri e quindi a separare il tutto in bit più piccoli, causare "altro" in "else if" aiuta le prestazioni - la valutazione di tali condizioni è costosa e se posso evitare di valutare le condizioni di seguito, causa una delle prime è vero, voglio evitarli.

Anche semanticamente parlando, valutare la condizione successiva se la precedente fosse soddisfatta non ha senso dal punto di vista commerciale.


modifica: questa domanda è stata identificata come un possibile duplicato di modi eleganti per gestire if (if else) else .

Credo che questa sia una domanda diversa (puoi vederlo anche confrontando le risposte a tali domande).

  • La mia domanda sta verificando che la prima condizione accettante finisca rapidamente .
  • La domanda collegata sta cercando di avere tutte le condizioni per essere accettata per fare qualcosa. (meglio visibile in questa risposta a questa domanda: https://softwareengineering.stackexchange.com/a/122625/96955 )

46
Cosa c'è di veramente sbagliato o poco chiaro su questo codice nel suo contesto? Non riesco a vedere come possa essere abbreviato o semplificato! Il codice che valuta le condizioni appare già ben ponderato, così come il metodo chiamato come risultato della decisione. Devi solo guardare alcune delle risposte qui sotto, che complicano semplicemente il codice!
Steve,

38
Non c'è niente di sbagliato in questo codice. È molto leggibile e facile da seguire. Qualunque cosa tu faccia per ridurla ulteriormente aggiungerà un riferimento indiretto e renderà più difficile la comprensione.
17 del 26

20
Il tuo codice va bene. Metti la tua energia rimanente in qualcosa di più produttivo che cercare di accorciarla ulteriormente.
Robert Harvey,

5
Se sono davvero solo 4 condizioni, va bene. Se è davvero qualcosa come 12 o 50, probabilmente vorrai fare il refactoring a un livello superiore rispetto a questo metodo.
JimmyJames,

9
Lascia il codice esattamente come è. Ascolta ciò che i tuoi genitori ti hanno sempre detto: non fidarti degli zii che offrono dolci ai bambini per strada. @Harvey Abbastanza divertente, i vari tentativi di "migliorare" il codice lo hanno reso molto più grande, più complicato e meno leggibile.
gnasher729,

Risposte:


81

Idealmente penso che dovresti estrarre la tua logica per ottenere il codice / numero di avviso nel suo metodo. Quindi il tuo codice esistente è ridotto fino a

{
    addAlert(GetConditionCode());
}

e hai GetConditionCode () incapsula la logica per il controllo delle condizioni. Forse anche meglio usare un Enum che un numero magico.

private AlertCode GetConditionCode() {
    if (CheckCondition1()) return AlertCode.OnFire;
    if (CheckCondition2()) return AlertCode.PlagueOfBees;
    if (CheckCondition3()) return AlertCode.Godzilla;
    if (CheckCondition4()) return AlertCode.ZombieSharkNado;
    return AlertCode.None;
}

2
Se possibile incapsulare come descrivi (sospetto che potrebbe non esserlo, penso che OP stia tralasciando le variabili per semplicità), non cambia il codice, che di per sé va bene, ma aggiunge ergonomia del codice e un po 'di leggibilità + 1
opa,

17
Con questi codici di avviso, ringrazio il codice che può essere restituito solo uno alla volta
Josh Part

12
Questa sembra anche una corrispondenza perfetta per l'uso di un'istruzione switch, se disponibile nella lingua di OP.
Frank Hopkins,

4
Probabilmente è solo una buona idea estrarre ottenere il codice di errore con un nuovo metodo se questo può essere scritto in modo da essere utile in più situazioni senza dover ricevere un mucchio di informazioni sulla situazione particolare. In realtà c'è un compromesso e un punto di pareggio quando ne vale la pena. Ma abbastanza spesso vedrai che la sequenza di convalide è specifica per il lavoro in corso ed è meglio tenerli insieme a quel lavoro. In tali casi, inventare un nuovo tipo per dire a un'altra parte del codice che cosa deve essere fatto è una zavorra indesiderata.
PJTraill,

6
Un problema con questa reimplementazione è che rende addAlertnecessaria la funzione per verificare la condizione di avviso fasullo AlertCode.None.
David Hammen,

69

La misura importante è la complessità del codice, non la dimensione assoluta. Supponendo che le diverse condizioni siano in realtà solo chiamate a funzione singola, proprio come le azioni non sono più complesse di quanto mostrato, direi che non c'è nulla di sbagliato nel codice. È già semplice come può essere.

Qualsiasi tentativo di "semplificare" ulteriormente complicherà le cose.

Ovviamente, puoi sostituire la elseparola chiave con una returncome altri hanno suggerito, ma è solo una questione di stile, non un cambiamento nella complessità.


A parte:

Il mio consiglio generale sarebbe di non essere mai religioso su nessuna regola per un codice pulito: la maggior parte dei consigli di codifica che vedi su Internet è buona se applicata in un contesto appropriato, ma applicare radicalmente lo stesso consiglio ovunque può farti entrare a far parte di l' IOCCC . Il trucco è sempre quello di trovare un equilibrio che consenta agli esseri umani di ragionare facilmente sul tuo codice.

Usa metodi troppo grandi e sei fregato. Usa funzioni troppo piccole e sei fregato. Evita le espressioni ternarie e sei fregato. Usa le espressioni ternarie dappertutto e sei fregato. Renditi conto che ci sono posti che richiedono funzioni a una linea e luoghi che richiedono funzioni a 50 linee (sì, esistono!). Comprendi che ci sono luoghi che richiedono if()dichiarazioni e che ci sono luoghi che richiedono l' ?:operatore. Usa l'arsenale completo a tua disposizione e cerca sempre di utilizzare lo strumento più adatto che riesci a trovare. E ricorda, non diventare religioso anche su questo consiglio.


2
Direi che sostituire else ifcon un interno returnseguito da un semplice if(rimuovere il else) potrebbe rendere il codice più difficile da leggere . Quando il codice dice else if, so immediatamente che il codice nel blocco successivo verrà eseguito solo se il precedente non lo ha fatto. Nessuna confusione, nessuna confusione. Se è semplice if, potrebbe essere eseguito o meno, indipendentemente dal fatto che il precedente sia stato eseguito. Ora dovrò dedicare un certo sforzo mentale per analizzare il blocco precedente per notare che termina con a return. Preferirei spendere quello sforzo mentale per analizzare la logica aziendale.
un CVn

1
Lo so, è una piccola cosa, ma almeno per me, else ifcostituisce un'unità semantica. (Non è necessariamente una singola unità per il compilatore, ma va bene.) ...; return; } if (...No; figuriamoci se si sviluppa su più righe. È qualcosa che dovrò davvero guardare per vedere cosa sta facendo, invece di essere in grado di accettarlo direttamente vedendo solo la coppia di parole chiave else if.
un CVn

@ MichaelKjörling Full Ack. Preferirei io stesso il else ifcostrutto, soprattutto perché la sua forma incatenata è un modello così noto. Tuttavia, anche il codice del modulo if(...) return ...;è un modello ben noto, quindi non lo condannerei completamente. Vedo questo in realtà come un problema minore, tuttavia: la logica del flusso di controllo è la stessa in entrambi i casi e un singolo sguardo ravvicinato a una if(...) { ...; return; }scala mi dirà che è effettivamente equivalente a una else ifscala. Vedo la struttura di un singolo termine, ne deduco il significato, mi rendo conto che si ripete dappertutto e so cosa succede.
cmaster

Provenienti da JavaScript / node.js, alcuni userebbero il codice "cintura e bretelle" dell'uso di entrambi else if e return . es.else if(...) { return alert();}
user949300,

1
"E ricorda, non diventare religioso anche su questo consiglio." +1
Words Like Jared,

22

È controverso se questo sia "migliore" del semplice if..else per un dato caso. Ma se vuoi provare qualcos'altro, questo è un modo comune di farlo.

Inserisci le tue condizioni negli oggetti e inseriscili in un elenco

foreach(var condition in Conditions.OrderBy(i=>i.OrderToRunIn))
{
    if(condition.EvaluatesToTrue())
    {
        addAlert(condition.Alert);
        break;
    }
}

Se sono necessarie più azioni a condizione, puoi fare una pazza ricorsione

void RunConditionalAction(ConditionalActionSet conditions)
{
    foreach(var condition in conditions.OrderBy(i=>i.OrderToRunIn))
    {
        if(condition.EvaluatesToTrue())
        {
            RunConditionalAction(condition);
            break;
        }
    }
}

Ovviamente sì. Funziona solo se hai un modello nella tua logica. Se si tenta di eseguire un'azione condizionale ricorsiva super generica, l'impostazione per l'oggetto sarà complicata come l'istruzione if originale. Inventerai il tuo nuovo linguaggio / quadro.

Ma il tuo esempio ha avere un modello

Un caso d'uso comune per questo modello sarebbe la convalida. Invece di :

bool IsValid()
{
    if(condition1 == false)
    {
        throw new ValidationException("condition1 is wrong!");
    }
    elseif(condition2 == false)
    {
    ....

}

diventa

[MustHaveCondition1]
[MustHaveCondition2]
public myObject()
{
    [MustMatchRegExCondition("xyz")]
    public string myProperty {get;set;}
    public bool IsValid()
    {
        conditions = getConditionsFromReflection()
        //loop through conditions
    }
}

27
Questo sposta solo la if...elsescala nella costruzione della Conditionslista. Il guadagno netto è negativo, poiché la costruzione di Conditionsprenderà lo stesso codice del codice OP, ma l'aggiunta indiretta comporta un costo in leggibilità. Preferirei sicuramente una scala codificata pulita.
cmaster

3
@cmaster sì, penso di aver detto esattamente che "quindi l'installazione per l'oggetto sarà complicata come l'istruzione if originale £
Ewan

7
Questo è meno leggibile dell'originale. Per capire quale condizione viene effettivamente verificata, devi scavare in qualche altra area del codice. Aggiunge un livello non necessario di indiretta che rende il codice più difficile da comprendere.
17 del 26

8
Convertire una catena if .. else if .. else .. in una tabella di predicati e azioni ha senso, ma solo per esempi molto più grandi. La tabella aggiunge un po 'di complessità e di riferimento indiretto, quindi sono necessarie voci sufficienti per ammortizzare questo sovraccarico concettuale. Quindi, per 4 coppie predicato / azione, mantieni il semplice codice originale, ma se ne avessi 100, scegli sicuramente la tabella. Il punto di crossover è nel mezzo. @cmaster, la tabella può essere inizializzata staticamente, quindi l'overhead incrementale per l'aggiunta di una coppia predicato / azione è una riga che semplicemente li nomina: difficile fare di meglio.
Stephen C. Steel,

2
La leggibilità NON è personale. È un dovere per il pubblico programmatore. È soggettivo. Questo è esattamente il motivo per cui è importante venire in posti come questo e ascoltare ciò che il pubblico della programmazione ha da dire al riguardo. Personalmente trovo questo esempio incompleto. Fammi vedere come conditionsè costruito ... ARG! Non attributi di annotazione! Perché Dio? Ow miei occhi!
candied_orange,

7

Considera l'utilizzo return;dopo che una condizione è riuscita, ti fa risparmiare tutti elsei messaggi. Potresti anche essere in grado di farlo return addAlert(1)direttamente se quel metodo ha un valore di ritorno.


3
Naturalmente, questo presuppone che non succeda nient'altro dopo la catena di ifs ... Questo potrebbe essere un presupposto ragionevole, e quindi potrebbe non esserlo.
un CVn

5

Ho visto costruzioni come questa considerate a volte più pulite:

switch(true) {
    case cond1(): 
        statement1; break;
    case cond2():
        statement2; break;
    case cond3():
        statement3; break;
    // .. etc
}

Il ternario con la giusta spaziatura può anche essere un'alternativa chiara:

cond1() ? statement1 :
cond2() ? statement2 :
cond3() ? statement3 : (null);

Suppongo che potresti anche provare a creare un array con coppia contenente condizione e funzione e iterare su di esso fino a quando non viene soddisfatta la prima condizione, che a mio modo di vedere sarebbe uguale alla prima risposta di Ewan.


1
ternary è pulito
Ewan

6
@Ewan il debug di un "ternario profondamente ricorsivo" rotto può essere un dolore inutile.
venerdì

5
essa sembra pulito sullo schermo però.
Ewan,

Uhm, quale lingua consente di utilizzare le funzioni con le caseetichette?
undercat

1
@undercat che è un valido ECMAScript / JavaScript
afaik

1

Come variante della risposta di @ Ewan potresti creare una catena (anziché un "elenco semplice") di condizioni come questa:

abstract class Condition {
  private static final  Condition LAST = new Condition(){
     public void alertOrPropagate(DisplayInterface display){
        // do nothing;
     }
  }
  private Condition next = Last;

  public Condition setNext(Condition next){
    this.next = next;
    return this; // fluent API
  }

  public void alertOrPropagate(DisplayInterface display){
     if(isConditionMeet()){
         display.alert(getMessage());
     } else {
       next.alertOrPropagate(display);
     }
  }
  protected abstract boolean isConditionMeet();
  protected abstract String getMessage();  
}

In questo modo è possibile applicare le condizioni in un ordine definito e l'infrastruttura (la classe astratta mostrata) salta i controlli rimanenti dopo il primo incontro.

È qui che è superiore all'approccio "elenco piatto" in cui è necessario implementare il "salto" nel ciclo che applica le condizioni.

È sufficiente impostare la catena di condizioni:

Condition c1 = new Condition1().setNext(
  new Condition2().setNext(
   new Condition3()
 )
);

E inizia la valutazione con una semplice chiamata:

c1.alertOrPropagate(display);

Sì, questo è chiamato il modello della catena di responsabilità
Max

4
Non pretendo di parlare per nessun altro, ma mentre il codice nella domanda è immediatamente leggibile e ovvio nel suo comportamento, non lo considero immediatamente ovvio su ciò che fa.
un CVn

0

Prima di tutto, il codice originale non è terribile IMO. È abbastanza comprensibile e non c'è nulla di intrinsecamente cattivo in esso.

Quindi, se non ti piace, basandoti sull'idea di @ Ewan di usare un elenco ma rimuovendo il suo foreach breakschema in qualche modo innaturale :

public class conditions
{
    private List<Condition> cList;
    private int position;

    public Condition Head
    {
        get { return cList[position];}
    }

    public bool Next()
    {
        return (position++ < cList.Count);
    }
}


while not conditions.head.check() {
  conditions.next()
}
conditions.head.alert()

Ora adatta questo nella tua lingua preferita, trasforma ogni elemento della lista in un oggetto, una tupla, qualunque cosa, e sei bravo.

EDIT: sembra che non sia così chiaro che ho pensato, quindi lasciami spiegare ulteriormente. conditionsè un elenco ordinato di qualche tipo; headè l'elemento corrente che si sta studiando - all'inizio è il primo elemento dell'elenco e ogni volta che next()viene chiamato diventa il seguente; check()e alert()sono il checkConditionX()e addAlert(X)dal PO.


1
(Non ha votato in negativo ma) Non posso seguirlo. Cos'è la testa ?
Belle-Sophie,

@Belle Ho modificato la risposta per spiegare ulteriormente. È la stessa idea di Ewan ma con un while notinvece di foreach break.
Nico,

Una brillante evoluzione di una brillante idea
Ewan,

0

La domanda manca di alcuni dettagli. Se le condizioni sono:

  • soggetto a modifiche o
  • ripetuto in altre parti dell'applicazione o del sistema o
  • modificato in alcuni casi (come build, test, distribuzioni diverse)

o se il contenuto in addAlertè più complicato, allora una soluzione forse migliore in dire che c # sarebbe:

//in some central spot
IEnumerable<Tuple<Func<bool>, int>> Conditions = new ... {
  Tuple.Create(CheckCondition1, 1),
  Tuple.Create(CheckCondition2, 2),
  ...
}

//at the original place
var matchingCondition = Conditions.Where(c=>c.Item1()).FirstOrDefault();
if(matchingCondition != null) 
  addAlert(matchingCondition.Item2)

Le tuple non sono così belle in c # <8, ma scelte per comodità.

I pro con questo metodo, anche se nessuna delle opzioni sopra indicate, è che la struttura è tipizzata staticamente. Non puoi rovinare accidentalmente, diciamo, perdere un else.


0

Il modo migliore per ridurre la complessità ciclomatica nei casi in cui si ha molto if->then statementsè utilizzare un dizionario o un elenco (dipendente dalla lingua) per memorizzare il valore chiave (se il valore dell'istruzione o un valore di) e quindi un risultato valore / funzione.

Ad esempio, anziché (C #):

if (i > 10) { return "Two"; }
else if (i > 8) { return "Four" }
else if (i > 4) { return "Eight" }
return "Ten";  //etc etc say anything after 3 or 4 values

Posso semplicemente

var results = new Dictionary<int, string>
{
  { 10, "Two" },
  { 8, "Four"},
  { 4, "Eight"},
  { 0, "Ten"},
}

foreach(var key in results.Keys)
{
  if (i > results[key]) return results.Values[key];
}

Se stai usando linguaggi più moderni puoi archiviare più logica, quindi anche semplicemente valori (c #). Si tratta in realtà solo di funzioni incorporate, ma puoi anche puntare anche ad altre funzioni se la logica è troppo strana da mettere in linea.

var results = new Dictionary<Func<int, bool>, Func<int, string>>
{
  { (i) => return i > 10; ,
    (i) => return i.ToString() },
  // etc
};

foreach(var key in results.Keys)
{ 
  if (key(i)) return results.Values[key](i);
}

0

Sto cercando di seguire i suggerimenti sul codice pulito di zio Bob e in particolare di mantenere i metodi brevi.

Mi trovo incapace di abbreviare questa logica però:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

Il tuo codice è già troppo breve, ma la logica stessa non dovrebbe essere modificata. A prima vista sembra che tu ti stia ripetendo con quattro chiamate checkCondition(), ed è evidente che ognuno è diverso dopo aver riletto il codice con attenzione. È necessario aggiungere la formattazione e i nomi delle funzioni corretti, ad esempio:

if (is_an_apple()) {
  addAlert(1);
}
else if (is_a_banana()) {
  addAlert(2);
}
else if (is_a_cat()) {
  addAlert(3);
}
else if (is_a_dog()) {
  addAlert(4);
}

Il tuo codice dovrebbe essere leggibile sopra ogni altra cosa. Dopo aver letto diversi libri di zio Bob, credo che sia il messaggio che sta costantemente cercando di trasmettere.


0

Supponendo che tutte le funzioni siano implementate nello stesso componente, è possibile fare in modo che le funzioni mantengano un certo stato al fine di sbarazzarsi dei molteplici rami nel flusso.

EG: checkCondition1()diventerebbe evaluateCondition1(), su cui verificherebbe se le condizioni precedenti fossero soddisfatte; in tal caso, memorizza nella cache un valore da recuperare getConditionNumber().

checkCondition2()diventerebbe evaluateCondition2(), su cui verificherebbe se le condizioni precedenti fossero soddisfatte. Se la condizione precedente non è stata soddisfatta, verifica lo scenario di condizione 2, memorizzando nella cache un valore da recuperare getConditionNumber(). E così via.

clearConditions();
evaluateCondition1();
evaluateCondition2();
evaluateCondition3();
evaluateCondition4();
if (anyCondition()) { addAlert(getConditionNumber()); }

MODIFICARE:

Ecco come dovrebbe essere implementato il controllo delle condizioni costose per far funzionare questo approccio.

bool evaluateCondition34() {
    if (!anyCondition() && A && B && C) {
        conditionNumber = 5693;
        return true;
    }
    return false;
}

...

bool evaluateCondition76() {
    if (!anyCondition() && !B && C && D) {
        conditionNumber = 7658;
        return true;
    }
    return false;
}

Pertanto, se hai troppi controlli costosi da eseguire e le cose in questo codice rimangono private, questo approccio aiuta a mantenerlo, consentendo di modificare l'ordine dei controlli se necessario.

clearConditions();
evaluateCondition10();
evaluateCondition9();
evaluateCondition8();
evaluateCondition7();
...
evaluateCondition34();
...
evaluateCondition76();

if (anyCondition()) { addAlert(getConditionNumber()); }

Questa risposta fornisce solo qualche suggerimento alternativo rispetto alle altre risposte e probabilmente non sarà migliore del codice originale se consideriamo solo 4 righe di codice. Sebbene, questo non sia un approccio terribile (e non rende neppure più difficile la manutenzione come altri hanno già detto) dato lo scenario che ho citato (troppi controlli, solo la funzione principale esposta come pubblica, tutte le funzioni sono dettagli di implementazione della stessa classe).


Non mi piace questo suggerimento: nasconde la logica di test all'interno di più funzioni. Ciò può rendere difficile la manutenzione del codice se, ad esempio, è necessario modificare l'ordine ed eseguire il n. 3 prima del n. 2.
Lawrence,

No. Puoi verificare se sono state valutate alcune condizioni precedenti anyCondition() != false.
Emerson Cardoso,

1
Ok, vedo a cosa stai arrivando. Tuttavia, se (diciamo) le condizioni 2 e 3 vengono entrambe valutate true, l'OP non vuole che la condizione 3 venga valutata.
Lawrence,

Quello che volevo dire è che puoi controllare anyCondition() != falseall'interno delle funzioni evaluateConditionXX(). Questo è possibile implementare. Se l'approccio dell'uso dello stato interno non è desiderato, capisco, ma l'argomento che ciò non funziona non è valido.
Emerson Cardoso,

1
Sì, la mia obiezione è che nasconde inutilmente la logica di test, non che non possa funzionare. Nella tua risposta (paragrafo 3), il controllo per soddisfare la condizione 1 è inserito all'interno di eval ... 2 (). Ma se cambia le condizioni 1 e 2 al livello superiore (a causa di cambiamenti nelle esigenze del cliente, ecc.), Dovresti andare in valutazione ... 2 () per rimuovere il controllo per la condizione 1, oltre a passare in valutazione. ..1 () per aggiungere un controllo per la condizione 2. Questo può essere fatto funzionare, ma può facilmente portare a problemi con la manutenzione.
Lawrence,

0

Non più di due clausole "else" costringono il lettore del codice a passare attraverso l'intera catena per trovare quella di interesse. Utilizzare un metodo come: void AlertUponCondition (Condition condition) {switch (condition) {case Condition.Con1: ... break; case condition.Con2: ... break; etc ...} Dove "Condition" è un enum corretto. Se necessario, restituisce un valore booleano o un valore. Chiamalo così: AlertOnCondition (GetCondition ());

In realtà non può essere più semplice, ed è più veloce della catena if-else una volta superati alcuni casi.


0

Non posso parlare per la tua situazione particolare perché il codice non è specifico, ma ...

codice del genere è spesso un odore per un modello OO carente. Hai davvero quattro tipi di cose, ognuna associata al proprio tipo di segnalatore, ma piuttosto che riconoscere queste entità e creare un'istanza di classe per ognuna, le tratti come una cosa e provi a rimediare in un secondo momento, in un momento in cui davvero devi sapere con cosa hai a che fare per procedere.

Il polimorfismo potrebbe averti adattato meglio.

Diffidare del codice con metodi lunghi contenenti costrutti if-then lunghi o complessi. Spesso vuoi un albero di classe lì con alcuni metodi virtuali.

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.