Come possono coesistere le dichiarazioni di protezione e le piccole funzioni?

0

Con dichiarazioni di guardia intendo qualcosa di simile alla prima parte della funzione:

def doSomething(String something)
{
    // Guard Statement
    if(!something)
    {
        return false
    }

    // more stuff
}

Supponiamo che tu possa avere diversi parametri, forse devi registrare che il metodo è stato chiamato con un parametro nullo, o forse lanciare un'eccezione specifica.

Chiarimento : ho letto che entrambe le dichiarazioni di guardia e le funzioni più piccole sono buone scelte di progettazione, ma sembra che includendo le dichiarazioni di guardia le tue funzioni non saranno ridotte.

Con funzioni più piccole, mi riferisco ai suggerimenti più piccoli di funzione di Robert C. Martin in Clean Code.

Credo che quello che sto chiedendo valga la pena di includere le dichiarazioni di guardia in una funzione anche se rende la funzione più lunga. Capisco che la risposta potrebbe basarsi molto se si tratta di un'API pubblica o di una funzione privata, a questo scopo non è una funzione privata ma un servizio in un'applicazione MVC, quindi immagino che lo renda pubblico?

    
posta Josh D 08.06.2015 - 21:58
fonte

4 risposte

2

Non pensare a funzioni brevi in termini di numero di LOC assoluti. È irrilevante, perché:

void demo(int a)
{
    if (a < 0)
    {
        this.dealWithNegative();
    }

    for (int i = 0; i < a; i++)
    {
        yield this.doSomething(i);
    }
}

e

void demo(int a) {
    if (a < 0) this.dealWithNegative();
    for (int i = 0; i < a; i++) yield this.doSomething(i);
}

sono gli stessi in termini di complessità: la seconda variante non è tre volte migliore a causa della sua LOC di 4 linee contro 12 linee nel primo esempio. (In realtà, il secondo esempio è molto più incline agli errori, e quindi peggio, ma questo è un argomento diverso.)

Pensa a brevi funzioni in termini di quanto tempo impiegherebbe a comprenderle . Quando inizi a lavorare con un metodo, dici:

“Oh heck, it would take me two hours to figure out what is all this code about.”

e non:

“Well, there are 124 lines of code, so it should take me from 96 to 112 minutes to figure this code out.”

giusto?

Mentre LOC generalmente più grande porta a metodi che richiedono più tempo per capire, non esiste una stretta correlazione tra due fattori. Ad esempio, per quanto tempo vorresti capire un metodo che associa un valore ad un altro, contenente 50 valori (e quindi 53 LOC)? Questo cambierebbe se la mappa contiene solo 10 valori? Che dire di 300 valori?

Le clausole di protezione non necessariamente rendono i metodi più lunghi, perché sono semplici da capire e non impiegano troppo tempo quando ti prepari a lavorare con il codice. Non possono nemmeno aumentare il numero assoluto di LOC, perché la maggior parte delle cose che trattano è dispersa nel codice. Ad esempio, cosa è più semplice:

  • Il codice con clausole di guardia:

    void demo(int a, int b)
    {
        if (a <= 0) throw new ArgumentOutOfRangeException(...);
        if (b < a) throw new ArgumentException(...);
    
        var c = b / a;
        this.doSomething(c);
    }
    
  • o la logica artificiosa del codice simile senza di essi:

    void demo(int a, int b)
    {
        if (a != 0)
        {
            // The division is safe: we won't have division by zero here.
            var c = (double)b / a;
            if (c < 1.0)
            {
                // We shouldn't have 'c' inferior to 1.
                throw new NotImplementedException();
            }
    
            this.doSomething((int)c);
        }
        else
        {
            throw new ArgumentOutOfRangeException();
        }
    }
    
risposta data 10.06.2015 - 16:22
fonte
3

Alcune opzioni:

  1. Non usare guardie. Seriamente, se un parametro nullo sta per esplodere, fallo esplodere. Inoltre, mentre è possibile per alcuni programmatori cattivi per chiamare direttamente funzioni / servizi interni con dati non corretti, forse non è necessario proteggere tutto.
  2. Condizionatori a linea singola. Questo è un posto in cui un singolo come if(string.IsNullOrWhitespace(something)){ return; } è buono. Tiene lontana la strada.
  3. Impostazioni predefinite sane. something = something ?? string.Empty; (o simile) è molto più breve e, inoltre, rende la tua funzione meno fragile a input errati. Alcuni lo odiano però.
  4. Helpers. In C #, puoi usare gli alberi di espressione per fare qualcosa come Guard.Range(()=>x,1,10); che verrà lanciata pur essendo più conciso e più SECCO.
  5. Non importa. "Piccole funzioni" può significare "meno in corso" oltre a "occupare meno spazio sullo schermo". Il primo è più importante del secondo. Mentre le guardie che mangiano grandi spazi sono fastidiose, sono facili da leggere. Se riesci a superare questo fastidio, allora puoi concentrarti su ciò che conta davvero, mantenendo il tuo codice di facile manutenzione mantenendo l'ambito (non la dimensione) delle funzioni di piccole dimensioni.
risposta data 08.06.2015 - 22:11
fonte
0

"Le funzioni dovrebbero essere piccole" non è un requisito assoluto. Il tempo di leggere e comprendere una funzione potrebbe essere modellato come una costante, più alcune piccole e costanti volte il numero di righe di codice quadrato. Molte piccole funzioni sono difficili da capire. Poche enormi funzioni sono difficili da capire. C'è una via di mezzo che devi cercare.

Ma poi, non stai cercando il codice meno complesso. Stai cercando il codice meno complesso che risolva il tuo problema in modo affidabile e in un tempo accettabile. Se le istruzioni di protezione sono necessarie per rendere affidabile il tuo codice, allora non importa quanto aggiungono alla dimensione di una funzione.

È stato detto "non usare guardie". Determina cosa succede senza guardie e quanto è male. Se capisci "se non uso una guardia, il codice si bloccherà in fase di sviluppo" - non usare la guardia. Attendi il crash in fase di sviluppo, correggi il motivo del crash. Definisci l'interfaccia per la tua funzione, documentala e se il chiamante non la rispetta, sentiti libero di andare in crash (a volte puoi usare una guardia per invocare l'arresto). Se capisci "se quella guardia si innesca, allora sono in una situazione totalmente inaspettata". Sentiti libero di andare in crash se è meglio che eseguire il codice senza alcuna idea sul risultato.

Dove dovresti avere guardie è quando ti aspetti che le cose potrebbero essere sbagliate. Tipico: elaborazione dei dati JSON da un'origine sconosciuta. Qualunque cosa potrebbe andare storta, e tu vuoi usare guardie per dire al chiamante "i tuoi dati erano spazzatura" invece di "Ho elaborato tutti i tuoi bei dati".

    
risposta data 10.06.2015 - 15:58
fonte
0

Codifica le tue precondizioni nel sistema di tipi in modo che un programma che violi la guardia non possa essere compilato. Ad esempio (scala)

object PositiveInt {
  /** Returns a positiveInt wrapper or None */
  def apply(i: Int): Option[PositiveInt] = if (i >= 0) Some(PositiveInt(i)) else None
}
case class PositiveInt private (value: Int) extends AnyVal // Value class to avoid extra object creation

def squareRoot(i: PositiveInt): Double = math.sqrt(i.value)

In questo caso, faccio in modo che non sia possibile richiamare squareRoot su un int che non abbiamo verificato come positivo. Il costruttore è privato quindi l'unico modo per ottenerne uno è utilizzare il metodo restituendo Option e gestisci il caso in cui restituisce None - l'errore nel fare ciò significa che il programma non viene compilato.

    
risposta data 10.06.2015 - 16:22
fonte

Leggi altre domande sui tag