Un'espressione booleana più grande è più leggibile della stessa espressione suddivisa in metodi predicati? [chiuso]


63

Cosa è più facile da capire, una grande dichiarazione booleana (abbastanza complessa) o la stessa istruzione suddivisa in metodi predicati (molto codice extra da leggere)?

Opzione 1, la grande espressione booleana:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {

        return propVal.PropertyId == context.Definition.Id
            && !repo.ParentId.HasValue || repo.ParentId == propVal.ParentId
            && ((propVal.SecondaryFilter.HasValue && context.SecondaryFilter.HasValue && propVal.SecondaryFilter.Value == context.SecondaryFilter) || (!context.SecondaryFilter.HasValue && !propVal.SecondaryFilter.HasValue));
    }

Opzione 2, Le condizioni suddivise in metodi predicati:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {
        return MatchesDefinitionId(context, propVal)
            && MatchesParentId(propVal)
            && (MatchedSecondaryFilter(context, propVal) || HasNoSecondaryFilter(context, propVal));
    }

    private static bool HasNoSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);
    }

    private static bool MatchedSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    }

    private bool MatchesParentId(TValToMatch propVal)
    {
        return (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    }

    private static bool MatchesDefinitionId(CurrentSearchContext context, TValToMatch propVal)
    {
        return propVal.PropertyId == context.Definition.Id;
    }

Preferisco il secondo approccio, perché vedo i nomi dei metodi come commenti, ma capisco che è problematico perché devi leggere tutti i metodi per capire cosa fa il codice, quindi astraggono le intenzioni del codice.


13
L'opzione 2 è simile a quella che Martin Fowler raccomanda nel suo libro di refactoring. Inoltre i nomi dei tuoi metodi servono come intento di tutte le espressioni casuali, il contenuto dei metodi sono solo i dettagli di implementazione che potrebbero cambiare nel tempo.
programmatore

2
È davvero la stessa espressione? "O" ha una precedenza minore di "E", comunque il secondo dice il tuo intento, l'altro (il primo) è tecnico.
thepacker

3
Cosa dice @thepacker. Il fatto che farlo nel primo modo ti abbia fatto commettere un errore è un buon indizio del fatto che il primo modo non è facilmente comprensibile per un settore molto importante del tuo pubblico target. Te stesso!
Steve Jessop,

3
Opzione 3: non mi piace nessuno dei due. Il secondo è ridicolmente dettagliato, il primo non è equivalente al secondo. Le parentesi aiutano.
David Hammen,

3
Questo può essere pedante, ma non hai alcuna if istruzione in nessuno dei blocchi di codice. La tua domanda riguarda le espressioni booleane .
Kyle Strand

Risposte:


88

Cosa è più facile da capire

Quest'ultimo approccio. Non è solo più facile da capire, ma è anche più facile da scrivere, testare, riformattare ed estendere. Ogni condizione richiesta può essere tranquillamente disaccoppiata e gestita a modo suo.

è problematico perché devi leggere tutti i metodi per capire il codice

Non è problematico se i metodi sono nominati correttamente. In effetti sarebbe più facile da capire poiché il nome del metodo descriverà l'intento della condizione.
Per uno spettatore if MatchesDefinitionId()è più esplicativo diif (propVal.PropertyId == context.Definition.Id)

[Personalmente, il primo approccio mi fa male agli occhi.]


12
Se i nomi dei metodi sono validi, è anche più facile da capire.
BЈовић

E per favore, rendili significativi (brevi). 20+ nomi di metodi di chars mi fanno male agli occhi. MatchesDefinitionId()è borderline.
Mindwin

2
@Mindwin Se si tratta di una scelta tra mantenere i nomi dei metodi "brevi" e mantenerli significativi, dico prendere quest'ultimo ogni volta. Corto è buono, ma non a scapito della leggibilità.
Ajedi32

@ Ajedi32 non è necessario scrivere un saggio su cosa fa il metodo sul nome del metodo, o avere nomi di metodi grammaticamente validi. Se si mantengono chiari gli standard delle abbreviazioni (all'interno del gruppo di lavoro o dell'organizzazione) non sarà un problema con nomi brevi e leggibilità.
Mindwin,

Usa la legge di Zipf: rendi le cose più dettagliate per scoraggiarne l'uso.
hoosierEE

44

Se questo è l'unico posto in cui verranno utilizzate queste funzioni di predicato, puoi anche utilizzare le boolvariabili locali :

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    bool matchesDefinitionId = (propVal.PropertyId == context.Definition.Id);
    bool matchesParentId = (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    bool matchesSecondaryFilter = (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    bool hasNoSecondaryFilter = (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);

    return matchesDefinitionId
        && matchesParentId
        && matchesSecondaryFilter || hasNoSecondaryFilter;
}

Questi potrebbero anche essere suddivisi ulteriormente e riordinati per renderli più leggibili, ad esempio con

bool hasSecondaryFilter = propVal.SecondaryFilter.HasValue;

e quindi sostituendo tutte le istanze di propVal.SecondaryFilter.HasValue. Una cosa che sporge immediatamente è che hasNoSecondaryFilterutilizza AND logico sulle HasValueproprietà negate , mentre matchesSecondaryFilterutilizza un AND logico su non negato HasValue, quindi non è esattamente l'opposto.


3
Questa soluzione è abbastanza buona e ho sicuramente scritto un sacco di codice simile. È molto leggibile. L'aspetto negativo, rispetto alla soluzione che ho pubblicato, è la velocità. Con questo metodo, esegui una serie di test condizionali, non importa quale. Nella mia soluzione, le operazioni possono essere notevolmente ridotte in base ai valori elaborati.
BuvinJ

5
@BuvinJ Test come quelli mostrati qui dovrebbero essere abbastanza economici, quindi se non conosco alcune delle condizioni sono costose o se questo è un codice estremamente sensibile alle prestazioni, sceglierei la versione più leggibile.
svick

1
@svick Senza dubbio, è improbabile che ciò introduca un problema di prestazioni il più delle volte. Tuttavia, se è possibile ridurre le operazioni senza perdere la leggibilità, perché non farlo? Non sono convinto che questo sia molto più leggibile della mia soluzione. Dà dei "nomi" autocompensanti ai test - il che è bello ... Penso che dipenda dal caso d'uso specifico e da quanto siano comprensibili i test.
BuvinJ

L'aggiunta di commenti può aiutare anche la leggibilità ...
BuvinJ

@BuvinJ Quello che mi piace di questa soluzione è che ignorando tutto tranne l'ultima riga, posso capire rapidamente cosa sta facendo. Penso che questo sia più leggibile.
svick

42

In generale, è preferito quest'ultimo.

Rende il sito di chiamata più riutilizzabile. Supporta DRY (il che significa che hai meno posti per cambiare quando cambiano i criteri e puoi farlo in modo più affidabile). E molto spesso questi sottocriteri sono cose che verranno riutilizzate indipendentemente altrove, permettendoti di farlo.

Oh, e rende queste cose molto più facili da testare, dandoti la sicurezza di averlo fatto correttamente.


1
Sì, anche se la tua risposta dovrebbe anche risolvere il problema dell'uso di repo, che appare un campo / proprietà statico, cioè una variabile globale. I metodi statistici dovrebbero essere deterministici e non usare variabili globali.
David Arno,

3
@DavidArno - anche se non è eccezionale, sembra tangenziale alla domanda in corso. E senza più codice è plausibile che ci sia un motivo semi-valido per il funzionamento del design in questo modo.
Telastyn,

1
Sì, non importa mai pronti contro termine. Ho dovuto offuscare un po 'il codice, non voglio condividere il codice client così com'è negli interwebs :)
willem

23

Se è tra queste due scelte, quest'ultima è migliore. Queste non sono le uniche scelte, comunque! Che ne dici di suddividere la singola funzione in più if? Prova i modi per uscire dalla funzione per evitare ulteriori test, emulando approssimativamente un "cortocircuito" in un test a linea singola.

È più facile da leggere (potrebbe essere necessario ricontrollare la logica del tuo esempio, ma il concetto è valido):

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    if( propVal.PropertyId != context.Definition.Id ) return false;

    if( repo.ParentId.HasValue || repo.ParentId != propVal.ParentId ) return false;

    if( propVal.SecondaryFilter.HasValue && 
        context.SecondaryFilter.HasValue && 
        propVal.SecondaryFilter.Value == context.SecondaryFilter ) return true;

    if( !context.SecondaryFilter.HasValue && 
        !propVal.SecondaryFilter.HasValue) return true;

    return false;   
}

3
Perché ho ricevuto un downvote per questo pochi secondi dopo averlo pubblicato? Si prega di aggiungere un commento quando si vota! Questa risposta funziona altrettanto rapidamente ed è più facile da leggere. Allora, qual'è il problema?
BuvinJ

2
@BuvinJ: assolutamente niente di sbagliato. Lo stesso del codice originale, tranne per il fatto che non devi combattere con una dozzina di parentesi e una sola riga che si estende oltre la fine dello schermo. Posso leggere quel codice dall'alto verso il basso e capirlo immediatamente. Conteggio WTF = 0.
gnasher729

1
La restituzione diversa da quella alla fine della funzione rende il codice meno leggibile, non più leggibile, IMO. Preferisco il punto di uscita singolo. Alcuni buoni argomenti in entrambi i modi su questo link. stackoverflow.com/questions/36707/…
Brad Thomas

5
@ Brad Thomas Non posso essere d'accordo con il singolo punto di uscita. Di solito porta a parentesi annidate profonde. Il ritorno termina il percorso, quindi per me è molto più facile da leggere.
Borjab

1
@BradThomas Sono pienamente d'accordo con Borjab. Evitare gli annidamenti profondi è in realtà il motivo per cui utilizzo questo stile più spesso che per spezzare lunghe dichiarazioni condizionali. Mi ritrovo a scrivere codice con tonnellate di annidamenti. Quindi, ho iniziato a cercare modi per non superare quasi mai uno o due annidamenti in profondità e, di conseguenza, il mio codice è diventato MOLTO più facile da leggere e mantenere. Se riesci a trovare un modo per uscire dalla tua funzione, fallo il prima possibile! Se riesci a trovare un modo per evitare annidamenti profondi e lunghi condizionali, fallo!
BuvinJ

10

Mi piace l'opzione 2 meglio, ma suggerirei un cambiamento strutturale. Combina i due controlli sull'ultima riga del condizionale in un'unica chiamata.

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    return MatchesDefinitionId(context, propVal)
        && MatchesParentId(propVal)
        && MatchesSecondaryFilterIfPresent(context, propVal);
}

private static bool MatchesSecondaryFilterIfPresent(CurrentSearchContext context, 
                                                    TValToMatch propVal)
{
    return MatchedSecondaryFilter(context, propVal) 
               || HasNoSecondaryFilter(context, propVal);
}

Il motivo per cui suggerisco questo è che i due controlli sono una singola unità funzionale e la nidificazione tra parentesi in un condizionale è soggetta a errori: sia dal punto di vista della scrittura iniziale del codice sia dal punto di vista della persona che lo legge. Questo è particolarmente vero se gli elementi secondari dell'espressione non seguono lo stesso modello.

Non sono sicuro che MatchesSecondaryFilterIfPresent()sia il nome migliore per la combinazione; ma non viene subito in mente niente di meglio.


Molto bello, cercare di spiegare cosa si sta facendo all'interno dei metodi è in realtà meglio della semplice ristrutturazione delle chiamate.
klaar,

2

Sebbene in C #, il codice non sia molto orientato agli oggetti. Sta usando metodi statici e quelli che sembrano campi statici (ad es repo.). Si ritiene generalmente che la statica renda difficile il refactoring del codice e difficile da testare, ostacolando al contempo la riusabilità e, alla tua domanda: un utilizzo statico come questo è meno leggibile e mantenibile della costruzione orientata agli oggetti.

È necessario convertire questo codice in una forma più orientata agli oggetti. Quando lo fai, scoprirai che ci sono posti ragionevoli per mettere il codice che fa il confronto di oggetti, campi, ecc. È probabile che potresti quindi chiedere agli oggetti di confrontarsi, il che ridurrebbe la tua grande istruzione if in un semplice richiesta di confronto (ad esempio if ( a.compareTo (b) ) { }, che potrebbe includere tutti i confronti tra campi.)

C # ha un ricco set di interfacce e utility di sistema per fare confronti su oggetti e loro campi. Al di là della ovvia .Equalsmetodo, per cominciare, guardare in IEqualityComparer, IEquatablee le utility come System.Collections.Generic.EqualityComparer.Default.


0

Quest'ultimo è sicuramente preferito, ho visto casi con il primo modo ed è quasi sempre impossibile da leggere. Ho commesso l'errore di farlo nel primo modo e mi è stato chiesto di cambiarlo in metodi predicati.


0

Direi che i due sono più o meno gli stessi, SE aggiungi uno spazio bianco per la leggibilità e alcuni commenti per aiutare il lettore nelle parti più oscure.

Ricorda: un buon commento dice al lettore cosa stavi pensando quando hai scritto il codice.

Con modifiche come ho suggerito, probabilmente andrei con il primo approccio, poiché è meno ingombrante e diffuso. Le chiamate di subroutine sono come note a piè di pagina: forniscono informazioni utili ma interrompono il flusso di lettura. Se i predicati fossero più complessi, li suddividerei in metodi separati in modo che i concetti che rappresentano possano essere costruiti in blocchi comprensibili.


Merita un +1. Buon cibo per pensare, anche se non opinione popolare basata sulle altre risposte. Grazie :)
willem,

1
@willem No, non merita +1. Due approcci non sono gli stessi. I commenti aggiuntivi sono stupidi e non necessari.
BЈовић

2
Un buon codice non dipende MAI dai commenti per essere comprensibile. In effetti i commenti sono il peggior disordine che un codice possa avere. Il codice dovrebbe parlare da solo. Inoltre, i due approcci che OP vuole valutare non possono mai essere "più o meno gli stessi", indipendentemente da quanti spazi bianchi si aggiungano.
Wonderbell,

È meglio avere un nome di funzione significativo che dover leggere il commento. Come indicato nel libro "Clean Code", un commento è un errore nell'espressione del codice di lancio. Perché spiegare cosa stai facendo quando la funzione avrebbe potuto dichiararlo in modo molto più chiaro.
Borjab

0

Bene, se ci sono parti che potresti voler riutilizzare, separarle in funzioni separate con nome appropriato è ovviamente la migliore idea.
Anche se potresti non riutilizzarli mai, farlo potrebbe permetterti di strutturare meglio le tue condizioni e dare loro un'etichetta che descriva cosa significano .

Ora, diamo un'occhiata alla tua prima opzione, e ammettiamo che né il tuo rientro e l'interruzione di linea sono stati così utili, né il condizionale è stato strutturato così bene:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal) {
    return propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue
        || repo.ParentId == propVal.ParentId
        && propVal.SecondaryFilter.HasValue == context.SecondaryFilter.HasValue
        && (!propVal.SecondaryFilter.HasValue || propVal.SecondaryFilter.Value == context.SecondaryFilter.Value);
}

0

Il primo è assolutamente orribile. Hai utilizzato || per due cose sulla stessa linea; questo è un bug nel tuo codice o un intento di offuscare il tuo codice.

    return (   (   propVal.PropertyId == context.Definition.Id
                && !repo.ParentId.HasValue)
            || (   repo.ParentId == propVal.ParentId
                && (   (   propVal.SecondaryFilter.HasValue
                        && context.SecondaryFilter.HasValue 
                        && propVal.SecondaryFilter.Value == context.SecondaryFilter)
                    || (   !context.SecondaryFilter.HasValue
                        && !propVal.SecondaryFilter.HasValue))));

Questo è almeno a metà decentemente formattato (se la formattazione è complicata, è perché la condizione if è complicata) e hai almeno la possibilità di capire se qualcosa dentro non ha senso. Rispetto alla tua immondizia formattata se, qualcos'altro è meglio. Ma sembra che tu sia in grado di fare solo estremi: o un pasticcio completo di un'istruzione if o quattro metodi completamente inutili.

Si noti che (cond1 && cond2) || (! cond1 && cond3) può essere scritto come

cond1 ? cond2 : cond3

che ridurrebbe il disordine. Scriverei

if (propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue) {
    return true;
} else if (repo.ParentId != propVal.ParentId) {
    return false;
} else if (propVal.SecondaryFilter.HasValue) {
    return (   context.SecondaryFilter.HasValue
            && propVal.SecondaryFilter.Value == context.SecondaryFilter); 
} else {
    return !context.SecondaryFilter.HasValue;
}

-4

Non mi piace nessuna di queste soluzioni, sono entrambe difficili da ragionare e difficili da leggere. L'astrazione con metodi più piccoli solo per motivi di metodi più piccoli non risolve sempre il problema.

Idealmente, penso che dovresti confrontare metaprogrmaticamente le proprietà, quindi non hai un nuovo metodo definito o se ramo ogni volta che vuoi confrontare un nuovo set di proprietà.

Non sono sicuro di c #, ma in JavaScript qualcosa del genere sarebbe MOLTO migliore e potrebbe almeno sostituire MatchesDefinitionId e MatchesParentId

function compareContextProp(obj, property, value){
  if(obj[property])
    return obj[property] == value
  return false
}

1
Non dovrebbe essere un problema implementare qualcosa di simile in C #.
Snoop

Non vedo come una combinazione booleana di ~ 5 chiamate compareContextProp(propVal, "PropertyId", context.Definition.Id)sarebbe più facile da leggere rispetto alla combinazione booleana del PO di ~ 5 confronti del modulo propVal.PropertyId == context.Definition.Id. È significativamente più lungo e aggiunge un ulteriore livello senza nascondere realmente la complessità del sito di chiamata. (se è importante, non ho
votato in negativo
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.