Scrivere un costruttore valido / leggibile che ha bisogno di molto calcolo per riempire i suoi campi

7

Ho una classe che ha diversi campi che possono essere riempiti solo consecutivamente da un sacco di calcoli. Il 1 ° campo può essere impostato molto facilmente. Per riempire il secondo campo, prendiamo il contenuto del primo campo e calcoliamo molto. Il 3 °, 4 ° e 5 ° campo devono essere calcolati insieme da campo1 e campo2 (non è possibile calcolare 3, 4 e 5 isolati gli uni dagli altri senza gravi perdite di performance). E il sesto campo viene calcolato dal campo 1, 3 e 4.

Userò la sintassi C # per ogni esempio di codice.

class Foo
{
    private Type1 f1; // obtained directly from input parameter of constructor
    private Type2 f2; // from f1 we can calculate f2
    private Type3 f3; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type4 f4; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type5 f5; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type6 f6; // from f1, f3 and f4 we can calculate f6

    public Foo(Type1 input)
    {
        ... // What is a good way to write it?
    }
}

Vorrei avere qualche input su un buon modo per implementare tale costruttore. Sono abbastanza nuovo nell'ingegneria del software della vita reale (ho appena realizzato alcuni progetti sandbox nell'università in precedenza) e non so ancora quali siano gli aspetti più importanti.

Ecco i due modi che ho in mente. Si prega di commentare su di loro, parlami di pro e contro non ho pensato, o dare esempi di come si dovrebbe scrivere un tale costruttore!

Prima versione: metodi statici senza effetti collaterali

public Foo(Type1 input)
{
    f1 = input;
    f2 = GetF2(f1);
    Tuple<Type3, Type4, Type5> bar = GetF3F4F5(f1, f2);
    f3 = bar.Item1;
    f4 = bar.Item2;
    f5 = bar.Item3;
    f6 = GetF6(f1, f3, f4);
}

private static Type2 GetF2(Type1 field1)
{
    Type2 field2 = new Type2();

    ... // calculating a lot, setting the correct value for field2

    return field2;
}

private static Tuple<Type3, Type4, Type5> GetF3F4F5(Type1 field1, Type2 field2)
{
    Type3 field3 = new Type3();
    Type4 field4 = new Type4();
    Type5 field5 = new Type5();

    ... // calculating a lot, setting the correct values for field3, 4 and 5

    return new Tuple<Type3, Type4, Type5>(field3, field4, field5);
}

private static Type6(Type1 field1, Type3 field3, Type4 field4)
{
    Type6 field6 = new Type6();

    ... // calculating a lot, setting the correct value for field6

    return field6;
}

Pro:

  • A ogni riga del costruttore, si sa esattamente quali campi sono già impostati e quali no, rendendolo chiaramente leggibile.
  • I calcoli sono incapsulati in metodi free effect, rendendoli facilmente modificabili.
  • Ogni metodo mostra chiaramente quali campi sono necessari per il calcolo.

con

  • La creazione di tuple ha un sovraccarico.
  • Potrebbe sembrare inutile per il lettore che una variabile sia definita, inizializzata e restituita solo per impostare un campo su questo valore, invece di impostare direttamente il campo.

Seconda versione: metodi non statici che manipolano direttamente i campi

public Foo(Type1 input)
{
    f1 = input;
    GetF2();
    GetF3F4F5();
    GetF6();
}

private void GetF2()
{
    f2 = new Type2();

    ... // calculating a lot with f1, setting the correct value for f2
}

private void GetF3F4F5()
{
    f3 = new Type3();
    f4 = new Type4();
    f5 = new Type5();

    ... // calculating a lot with f1 and f2, setting the correct values for f3, f4 and f5
}

private void Type6()
{
    f6 = new Type6();

    ... // calculating a lot with f1, f3, f4, setting the correct value for f6
}

Pro:

  • Leggermente più corto.
  • Nessun sovraccarico della tupla.
  • Probabilmente un po 'più veloce perché manca la deviazione "initialize- > return- > set". (solo una supposizione)

con

  • Se prendi qualche riga nel costruttore, non puoi dire quali campi sono già inizializzati e quali no. (O forse puoi farlo a causa dei nomi dei metodi, ma per i miei occhi non è chiaro come nella prima versione.)
  • I metodi non indicano quali campi usano per il calcolo.
posta Kjara 18.07.2016 - 14:20
fonte

3 risposte

9

Il primo approccio è molto, molto meglio del secondo approccio.

La qualità più importante del software è la leggibilità, dal momento che il numero di volte in cui è possibile scrivere e riscrivere una riga di codice è molto più piccolo del numero di volte in cui è possibile leggere una riga di codice. (Scrivi mai codice e poi non lo leggi mai più?) Spero di no e prima di riscrivere una riga di codice, sicuramente la leggerete almeno una volta, giusto?)

La penalizzazione delle prestazioni associata alla restituzione di più valori da una chiamata al metodo è completamente irrilevante, soprattutto perché, per tua stessa ammissione, i calcoli coinvolti sono ampi. Quindi, restituire i risultati non è solo molto economico, ma anche molto più economico di altre operazioni che sai già che devi fare.

Inoltre, dato che i metodi di calcolo saranno privati, il compilatore potrebbe addirittura ottimizzarli, quindi potrebbe non esserci alcuna penalità di prestazioni.

(In ogni caso, se vuoi veramente evitare la creazione della tupla, e dal momento che stai usando la sintassi C #, potresti usare anche out parameters.)

(Inoltre, tieni presente che il secondo approccio non sarebbe nemmeno un'opzione se speri di rendere il tuo oggetto immutabile.)

Esistono due tipi di problemi relativi alle prestazioni: problemi algoritmici e tipi di preoccupazioni "salva il tempo".

Problemi di prestazioni algoritmiche hanno a che fare con l'uso di quicksort invece di bubble sort, usando la ricerca binaria invece della ricerca lineare, desincronizzando le operazioni in modo che possano essere eseguite in parallelo anziché in sequenza, migliorando i protocolli di comunicazione in modo da non richiedere viaggi, ecc.

I problemi di tipo "salva il ciclo dell'orologio" riguardano l'hacking, il tweaking e il torcimento di roba per risparmiare cicli di clock qua e là.

Il mio consiglio è di non considerare mai un tipo di "prestazione del ciclo di salvataggio" come un "pro" o un "contro" quando si decide come strutturare il software. Questo tipo di preoccupazione non farà mai la differenza, e nell'evento estremamente improbabile che un giorno possa essere trovato a fare la differenza, ci sarà sempre un modo molto semplice per risolverlo.

Se vuoi davvero un suggerimento per un approccio alternativo, considera il modello metodo di fabbrica :

  1. Rendi privato il tuo costruttore e accetta che esca esattamente un parametro per ogni variabile membro da inizializzare. (f1, f2, f3, f4, f5 e f6.)

  2. Scrivi una funzione statica pubblica che accetta il parametro Type1 input , esegue tutti i calcoli necessari per archiviare i risultati in variabili locali denominate f1, f2, f3, f4, f5 e f6 e termina con return new Foo( f1, f2, f3, f4, f5, f6); .

risposta data 18.07.2016 - 15:23
fonte
2

Considera separare l'uso dalla costruzione .

Foo foo = new FooInjector(f1).build();

Ora rendere immutabile Foo è banale. Ora posso osservare il comportamento di Foo senza essere distratto dalla costruzione di f #. Certo Foo ha una funzione di costruzione lunga da gestire, ma è nascosta in FooInjector , non distribuita nel codice base.

Se trovi che molte classi (oltre a solo Foo ) devono fare questo per accedere alla famiglia f # potresti voler esaminare introduce oggetto parametro refactoring.

Vorrei anche capire perché un costruttore non dovrebbe svolgere un vero lavoro ma ha già una risposta qui .

    
risposta data 18.07.2016 - 15:16
fonte
1

Non vedo che importi. Tutta l'elaborazione è interna / privata alla classe foo . È comunque un enorme effetto collaterale; il costruttore sta impostando Foo stato dell'oggetto. Se Foo non può fidarsi di se stesso, allora cosa deve fare? Non mantenere lo stato e ricalcolare tutti gli fx oggetti ogni volta?

Tuttavia mi piace la prima opzione non a causa della gestione degli effetti collaterali, ma perché i parametri esplicitamente trasmessi esprimono le dipendenze per la costruzione di un oggetto. Rende l'ordine di costruzione richiesto e gli oggetti partecipanti inequivocabili. E non mi costringe a leggere un sacco di codice, come fa il 2 ° esempio, per capire cosa sta succedendo.

    
risposta data 18.07.2016 - 14:57
fonte

Leggi altre domande sui tag