Questo uso dei condizionali è un anti-schema?


14

L'ho visto molto nel nostro sistema legacy al lavoro - funzioni che vanno in questo modo:

bool todo = false;
if(cond1)
{
  ... // lots of code here
  if(cond2)
    todo = true;
  ... // some other code here
}

if(todo)
{
  ...
}

In altre parole, la funzione ha due parti. La prima parte esegue una sorta di elaborazione (potenzialmente contenente loop, effetti collaterali, ecc.) E lungo la strada potrebbe impostare il flag "todo". La seconda parte viene eseguita solo se è stato impostato il flag "todo".

Sembra un modo piuttosto brutto di fare le cose, e penso che la maggior parte dei casi che ho davvero avuto il tempo di capire, potrebbe essere riformulata per evitare di usare la bandiera. Ma è un vero anti-pattern, una cattiva idea o perfettamente accettabile?

La prima ovvia rifattorizzazione sarebbe quella di tagliarla in due metodi. Tuttavia, la mia domanda è più se c'è mai la necessità (in un moderno linguaggio OO) di creare una variabile flag locale, potenzialmente impostarla in più posizioni, e poi usarla in seguito per decidere se eseguire il blocco di codice successivo.


2
Come lo rifattori?
Tamás Szelei,

13
Supponendo che todo sia ambientato in più punti, in base a diverse condizioni non banali non esclusive, riesco a malapena a pensare a un refactoring che abbia il minimo senso. Se non c'è refactoring, non c'è antipattern. Tranne la denominazione della variabile todo; dovrebbe essere nominato più espressivo, come "doSecurityCheck".
user281377

3
@ammoQ: +1; se le cose sono complicate, allora è così. Una variabile flag può avere molto più senso in alcune circostanze in quanto rende più chiaro che è stata presa una decisione e puoi cercarla per trovare dove è stata presa quella decisione.
Donal Fellows,

1
@Donal Fellows: se la ricerca del motivo è necessaria, renderei la variabile un elenco; fintanto che è vuoto, è "falso"; ovunque sia impostato il flag, un codice motivo viene aggiunto all'elenco. Quindi potresti finire con un elenco del genere ["blacklisted-domain","suspicious-characters","too-long"]che mostra che sono stati applicati diversi motivi.
user281377,

2
Non penso che sia un anti-schema, ma è sicuramente un odore
Binary Worrier,

Risposte:


23

Non conosco l'anti-pattern, ma ne estrarrei tre metodi.

Il primo eseguiva alcuni lavori e restituiva un valore booleano.

Il secondo eseguirà qualsiasi lavoro eseguito da "qualche altro codice"

Il terzo avrebbe eseguito il lavoro ausiliario se il valore booleano restituito fosse vero.

I metodi estratti sarebbero probabilmente privati ​​se fosse importante che solo il secondo (e sempre) sia chiamato se il primo metodo restituisce true.

Denominando bene i metodi, spero che renderebbe il codice più chiaro.

Qualcosa come questo:

public void originalMethod() {
    bool furtherProcessingRequired = lotsOfCode();
    someOtherCode();
    if (furtherProcessingRequired) {
        doFurtherProcessing();
    }
    return;
}

private boolean lotsOfCode() {
    if (cond1) {
        ... // lots of code here
        if(cond2) {
            return true;
        }
    }
    return false;
}

private void someOtherCode() {
    ... // some other code here
}

private void doFurtherProcessing() {
    // Do whatever is needed
}

Ovviamente c'è da discutere sul fatto che i rendimenti anticipati siano accettabili, ma questo è un dettaglio di implementazione (come lo standard di formattazione del codice).

Il punto è che l'intento del codice diventa più chiaro, il che è buono ...

Uno dei commenti sulla domanda suggerisce che questo modello rappresenta un odore e sarei d'accordo con quello. Vale la pena guardarlo per vedere se riesci a rendere più chiaro l'intento.


Suddividere in 2 funzioni richiederebbe comunque una todovariabile e probabilmente sarebbe più difficile da capire.
Pubblicazione

Sì, lo farei anche io, ma la mia domanda riguardava più l'uso della bandiera "todo".
Kricket,

2
Se finisci con if (check_if_needed ()) do_whatever ();, non c'è nessuna bandiera ovvia lì. Penso che questo possa rompere troppo il codice e potenzialmente danneggiare la leggibilità se il codice è ragionevolmente semplice, però. Dopotutto, i dettagli di ciò che fai do_whateverpossono influire sul modo in cui esegui il test check_if_needed, quindi è utile mantenere tutto il codice insieme nella stessa schermata. Inoltre, questo non garantisce che check_if_neededpossa evitare di usare un flag - e se lo fa, probabilmente utilizzerà più returnistruzioni per farlo, probabilmente sconvolgendo i rigorosi sostenitori dell'uscita singola.
Steve314,

3
@ Pubby8 ha detto "estrai 2 metodi da questo" , risultando in 3 metodi. 2 metodi che eseguono l'elaborazione effettiva e il metodo originale che coordina il flusso di lavoro. Questo sarebbe un design molto più pulito.
MattDavey,

Ciò omette il ... // some other code herecaso di ritorno anticipato
Caleth,

6

Penso che la bruttezza sia dovuta al fatto che c'è un sacco di codice in un singolo metodo e / o le variabili sono mal nominate (entrambi sono odori di codice per conto proprio - gli antipattern sono cose più astratte e complesse dell'IMO).

Quindi se estrai la maggior parte del codice in metodi di livello inferiore come suggerisce @Bill, il resto diventa pulito (almeno per me). Per esempio

bool registrationNeeded = installSoftware(...);
if (registrationNeeded) {
  registerUser(...)
}

Oppure puoi persino eliminare completamente la bandiera locale nascondendo il controllo della bandiera nel secondo metodo e utilizzando un modulo simile

calculateTaxRefund(isTaxRefundable(...), ...)

Nel complesso, non vedo che avere una variabile flag locale sia necessariamente negativa di per sé. Quale opzione di cui sopra è più leggibile (= preferibile per me) dipende dal numero di parametri del metodo, dai nomi scelti e da quale forma è più coerente con la logica interna del codice.


4

todo è un nome davvero brutto per la variabile, ma penso che potrebbe essere tutto ciò che è sbagliato. È difficile essere completamente sicuri senza il contesto.

Diciamo che la seconda parte della funzione ordina un elenco, creato dalla prima parte. Questo dovrebbe essere molto più leggibile:

bool requiresSorting = false;
if(cond1)
{
    ... // lots of code here
    if(cond2)
        requiresSorting = true;
    ... // some other code here
}

if(requiresSorting)
{
    ...
}

Tuttavia, anche il suggerimento di Bill è corretto. Questo è ancora più leggibile:

bool requiresSorting = BuildList(list);
if (requiresSorting)
    SortList(list);

Perché non fare un ulteriore passo avanti: if (BuildList (list)) SortList (list);
Phil N DeBlanc,

2

Il modello della macchina a stati mi sembra perfetto. I modelli anti in là sono "todo" (cattivo nome) e "molto codice".


Sono sicuro che è solo a scopo illustrativo, però.
Loren Pechtel,

1
Concordato. Quello che stavo cercando di comunicare è che i buoni schemi annegati in un codice scadente non dovrebbero essere biasimati per la qualità del codice.
ptyx,

1

Dipende davvero. Se il codice custodito da todo(spero che tu non stia usando quel nome per davvero dato che è totalmente non-mnemonico!) È un codice concettualmente pulito, allora hai un anti-pattern e dovresti usare qualcosa come RAII di C ++ o C # usingcostruire invece per gestire le cose.

D'altra parte, se concettualmente non è una fase di pulizia ma piuttosto solo un'ulteriore elaborazione che a volte è necessaria e dove la decisione di farlo deve essere presa prima, ciò che è scritto va bene. Considera se ovviamente i singoli blocchi di codice sarebbero meglio sottoposti a refactoring nelle loro stesse funzioni, e anche se hai acquisito il significato della variabile flag nel suo nome, ma questo modello di codice di base è OK. In particolare, cercando di mettere troppo in altre funzioni potrebbe fare quello che sta succedendo meno chiaro, e che sarebbe sicuramente un anti-modello.


Chiaramente non è una pulizia, non funziona sempre. Ho già riscontrato casi come questo in precedenza: durante l'elaborazione di qualcosa potresti finire per invalidare una sorta di risultato precalcolato. Se il calcolo è costoso, è necessario eseguirlo solo se necessario.
Loren Pechtel,

1

Molte delle risposte qui avrebbero problemi a superare un controllo di complessità, alcune sembravano> 10.

Penso che questa sia la parte "anti-pattern" di ciò che stai guardando. Trova uno strumento che misura la complessità ciclomatica del tuo codice: ci sono plugin per Eclipse. È essenzialmente una misura di quanto sia difficile testare il tuo codice e coinvolge il numero e i livelli dei rami del codice.

Come ipotesi totale su una possibile soluzione, il layout del tuo codice mi fa pensare in "Attività", se ciò accade in molti posti, forse quello che vuoi veramente è un'architettura orientata alle attività - ogni attività essendo la sua oggetto e nel mezzo dell'attività hai la possibilità di accodare l'attività successiva creando un'istanza di un altro oggetto attività e lanciandolo sulla coda. Questi possono essere incredibilmente semplici da configurare e riducono significativamente la complessità di alcuni tipi di codice, ma come ho già detto, questa è una pugnalata totale nel buio.


1

Usando l'esempio di pdr sopra, dato che è un bell'esempio, farò un passo avanti.

Lui aveva:

bool requiresSorting = BuildList(list);
if (requiresSorting)
    SortList(list);

Quindi mi sono reso conto che avrebbe funzionato quanto segue:

if(BuildList(list)) 
    SortList(list)

Ma non è così chiaro.

Quindi, alla domanda originale, perché non avere:

BuildList(list)
SortList(list)

E lascia che SortList decida se richiede l'ordinamento?

Vedete che il vostro metodo BuildList sembra conoscere l'ordinamento, in quanto restituisce un valore booleano che indica tale, ma non ha senso per un metodo progettato per costruire un elenco.


E ovviamente il prossimo passo è chiedere perché questo è un processo in due fasi. Ovunque vedo un codice del genere, faccio riferimento a un metodo chiamato BuildAndSortList (elenco)
Ian

Questa non è una risposta Hai modificato il comportamento del codice.
D Drmmr,

Non proprio. Ancora una volta, non posso credere di rispondere a qualcosa che ho pubblicato 7 anni fa, ma che diavolo :) Quello che stavo sostenendo è che SortList conterrebbe il condizionale. Se si avesse un test unitario che affermava che l'elenco era ordinato solo se la condizione x era soddisfatta, sarebbe comunque passata. Spostando il condizionale in SortList, eviti di dover sempre scrivere (if (qualcosa) quindi SortList (...))
Ian

0

Sì, questo sembra essere un problema perché devi tenere traccia di tutti i punti in cui contrassegni la bandiera ON / OFF. È meglio includere la logica appena dentro come condizione nidificata invece di estrarre la logica.

Anche modelli di dominio ricchi, in quel caso solo un liner farà grandi cose all'interno dell'oggetto.


0

Se il flag viene impostato solo una volta, sposta il codice
...
direttamente dopo il
... // altro codice,
quindi elimina il flag.

Altrimenti dividere qui
... // un sacco di codice qui
... // un altro codice qui
...
se possibile codificare in funzioni separate , quindi è chiaro che questa funzione ha una responsabilità che è la logica del ramo.

Ove possibile separa il codice all'interno di
... // un sacco di codice qui
fuori in due o più funzioni, alcuni che fanno un po 'di lavoro (che è un comando) e altri che restituiscono il valore todo (che è una query) o lo fanno molto esplicito lo stanno modificando (una query che utilizza effetti collaterali)

Il codice stesso non è l'anti-pattern che sta succedendo qui ... Ho il sospetto che mescolare la logica di branching con l'effettivo fare cose (comandi) sia l'anti-pattern che stai cercando.


cosa aggiunge questo post che mancano quelle risposte esistenti?
esoterik,

@esoterik A volte l'opportunità di aggiungere un po 'di CQRS viene spesso trascurata quando si tratta di flag ... la logica per decidere di cambiare un flag rappresenta una query, mentre fare un lavoro rappresenta un comando. A volte separare i due può rendere il codice più chiaro. Inoltre, vale la pena sottolineare nel codice sopra che può essere semplificato perché il flag è impostato in un solo ramo. Sento che le bandiere non sono un antipattern e se il loro nome rende il codice più espressivo sono una buona cosa. Sento dove i flag vengono creati, impostati e utilizzati dovrebbero essere vicini nel codice, se possibile.
Andrew Pate,

0

A volte scopro di dover implementare questo modello. A volte si desidera eseguire più controlli prima di procedere con un'operazione. Per motivi di efficienza, i calcoli che comportano determinati controlli non vengono eseguiti a meno che non sia assolutamente necessario verificarli. Quindi in genere vedi codice come:

// Check individual fields for proper input

if(fieldsValidated) {
  // Perform cross-checks to see if input contains values which exist in the database

  if(valuesExist) {
    try {
      // Attempt insertion
      trx.commit();
    } catch (DatabaseException dbe) {
      trx.rollback();
      throw dbe;
    }
  } else {
    closeConnection(db);
    throwException();
  }
} else {
  closeConnection(db);
  throwException();
}

Ciò potrebbe essere semplificato separando la convalida dall'effettivo processo di esecuzione dell'operazione, quindi vedresti più simili:

boolean proceed = true;
// Check individual fields for proper input

if(fieldsValidated) {
  // Perform cross-checks to see if input contains values which exist in the database

  if(!valuesExist) {
    proceed = false;
  }
} else {
  proceed = false;
}

// The moment of truth
if(proceed) {
  try {
    // Attempt insertion
    trx.commit();
  } catch (DatabaseException dbe) {
    trx.rollback();
    throw dbe;
  }
} else {
  if(db.isOpen()) {
    closeConnection(db);
  }
  throwException();
}

Ovviamente varia in base a ciò che stai cercando di ottenere, sebbene scritto in questo modo, sia il tuo codice "successo" che il tuo codice "fallimento" vengono scritti una volta, il che semplifica la logica e mantiene lo stesso livello di prestazioni. Da lì, sarebbe una buona idea adattarsi a interi livelli di validazione all'interno di metodi interni che restituiscono indicazioni booleane di successo o fallimento che semplificano ulteriormente le cose, anche se ad alcuni programmatori piace metodi estremamente lunghi per qualche strana ragione.


Nell'esempio che hai dato, penso che preferirei avere una funzione shouldIDoIt (fieldsValidated, valoriExist) che restituisce la risposta. Questo perché la determinazione sì / no viene presa tutta in una volta, in contrasto con il codice che vedo qui al lavoro, in cui la decisione di procedere è sparsa in alcuni punti diversi non contigui.
Kricket,

@KelseyRider, quello era esattamente il punto. La separazione della convalida dall'esecuzione consente di inserire la logica in un metodo al fine di semplificare la logica generale del programma in if (isValidated ()) doOperation ()
Neil

0

Questo non è uno schema . L'interpretazione più generale è che stai impostando una variabile booleana e ramificando il suo valore in un secondo momento. Questa è la normale programmazione procedurale, niente di più.

Ora, il tuo esempio specifico può essere riscritto come:

if(cond1)
{
    ... // lots of code here
    ... // some other code here
    if (cond2)
    {
        ...
    }
}

Potrebbe essere più facile da leggere. O forse no. Dipende dal resto del codice che hai omesso. Concentrati sul rendere quel codice più conciso.


-1

I flag locali utilizzati per il flusso di controllo devono essere riconosciuti come una forma gotomascherata. Se un flag viene utilizzato solo all'interno di una funzione, può essere eliminato scrivendo due copie della funzione, etichettandone uno come "flag è vero" e l'altro come "flag è falso" e sostituendo ogni operazione che imposta il flag quando è chiaro o lo cancella quando è impostato, con un salto tra le due versioni della funzione.

In molti casi, il codice che utilizza l'uso di un flag sarà più pulito di ogni possibile alternativa che utilizza gotoinvece, ma non è sempre vero. In alcuni casi, l'uso gotoper saltare un pezzo di codice può essere più pulito rispetto all'utilizzo di flag per farlo [anche se alcune persone potrebbero inserire un determinato fumetto di rapace qui].

Penso che il principio guida principale dovrebbe essere che il flusso di logica del programma dovrebbe assomigliare nella misura della logica di business nella misura del possibile. Se i requisiti della logica aziendale sono descritti in termini di stati che si dividono e si fondono in modi strani, avere la logica del programma allo stesso modo può essere più pulito rispetto al tentativo di utilizzare i flag per nascondere tale logica. D'altra parte, se il modo più naturale di descrivere le regole aziendali sarebbe dire che un'azione dovrebbe essere fatta se fossero state fatte altre azioni, il modo più naturale di esprimere che potrebbe essere quello di usare una bandiera che viene impostata durante l'esecuzione quest'ultima azione ed è altrimenti chiara.

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.