Ho deciso di scrivere due metodi, uno facendo un sottoinsieme dell'altro. Quale dovrei scrivere prima?

1

Ho deciso di scrivere due metodi, uno che fornisce funzionalità generali e uno che fa qualcosa di più specifico e stretto che può essere fatto con il metodo più complesso, anche se forse con prestazioni peggiori rispetto a un'implementazione dedicata.

Ad esempio, diciamo che voglio scrivere i metodi map e iterate , con il primo che passa sopra ogni elemento in una lista e lo proietta con una funzione, e quest'ultimo sta semplicemente scavalcando ogni elemento nel lista.

map<T, S>(list : List<T>, f : T => S) : List<S>

iterate<T>(list : List<T>, f : T => void) : void

Posso scrivere prima il metodo più complesso e poi chiamarlo usando parametri specifici nel metodo più semplice (se questo ha senso in termini di prestazioni, ecc.), altrimenti scrivi prima il metodo più semplice e fai cose extra nel più complesso uno (se possibile).

Qual è l'opzione migliore?

    
posta GregRos 30.07.2017 - 16:58
fonte

3 risposte

2

Supponendo che la funzione specifica dipenda dalla funzione generale, ma non viceversa:

Scrivere prima la funzione generale consente di eseguirlo e testarlo prima di iniziare a scrivere la funzione specifica. Ciò ti consente di eseguire un processo iterativo per verificare che la base sia corretta prima di crearla.

Ma se scrivi prima la funzione specifica, non puoi eseguirla o testarla finché non hai anche scritto la funzione generale da cui dipende. Questo introduce un rischio più elevato e le ipotesi errate saranno più costose perché richiede più tempo prima di scoprire i problemi.

Quindi inizia con la funzione generale.

    
risposta data 30.07.2017 - 19:34
fonte
1

Per fare un esempio davvero stupido (anche più semplice del tuo), ci sono in realtà quattro pattern che potresti seguire:

Implementato separatamente

int AddNumbers(int a, int b)
{
    return a + b;
}

int AddNumbers(int a, int b, int c)
{
    return a + b + c;
}

Vantaggi: nessuna dipendenza, nessun ulteriore riferimento indiretto (molto leggero) miglioramento delle prestazioni

Contro: più lavoro da scrivere. Se trovi un bug, devi risolverlo in due punti.

Semplice prima

int AddNumbers(int a, int b)
{
    return a + b;
}

int AddNumbers(int a, int b, int c)
{
    return AddNumbers(a, b) + c;
}

Professionisti: se trovi un bug potresti essere in grado di risolverlo in un solo punto

Contro: un leggero colpo alle prestazioni che chiama la versione complessa. Le modifiche alla versione semplice potrebbero avere effetti collaterali non desiderati sulla versione complessa che potrebbero essere persi con il collaudo unitario ordinario che isolerebbe la versione semplice.

Complesso per primo

int AddNumbers(int a, int b, int c)
{
    return a + b + c;
}

int AddNumbers(int a, int b)
{
    return AddNumbers(a, b, 0);
}

O in alcune lingue potresti semplicemente scrivere

int AddNumbers(int a, int b, int c = 0)
{
    return a + b + c;
}

Pro: una funzione sta facendo tutto il lavoro, la semplice funzione sostituisce semplicemente il valore predefinito. Testare la funzione semplice è banale.

Contro: leggero impatto sulle prestazioni. Possibile effetto collaterale creando un parametro predefinito, ad es. se la versione complessa accetta tipi complessi anziché primitivi (devi allocare il terzo parametro). Ma funziona alla grande con i tipi nullable.

Livelli

int AddNumbers(int a, int b, int c)
{
    return AddNumbersInternal(new int[] {a, b, c});
}

int AddNumbers(int a, int b)
{
    return AddNumbersInternal(new int[] {a, b});
}

private int AddNumbersInternal(int[] args)
{
    int accumulator = 0;
    foreach (int n in args) accumulator += n;
    return accumulator;
}

Professionisti: cristallina cosa sta succedendo. Bug corretti in un unico posto. Consente la personalizzazione di ciascun prototipo senza influire sulla logica di base.

Contro: più lavoro.

Secondo me

Tutti gli approcci di cui sopra hanno il loro posto. Se quello che stai facendo è molto semplice, li implementerei separatamente, per essere onesti. se quello che stai facendo è molto complesso, implementerei usando l'approccio a strati.

Ma stai chiedendo i due approcci centrali. Li odio entrambi! Ma mi inclino a "complesso prima" dal momento che tutto il lavoro viene essenzialmente svolto in un unico posto, anche se si aggiungono sempre più prototipi che fanno cose diverse. Con l'approccio "semplice prima" stai dividendo la logica in due funzioni che possono confondere e dovrai dividerle sempre di più man mano che sono necessarie funzioni sempre più complesse.

    
risposta data 01.08.2017 - 00:01
fonte
0

Non importa quale di primo autore fisicamente. Quello che vuoi fare è stabilire (tramite design / architettura) la relazione tra i due.

Se sai in modo statico, quando stai scrivendo un codice che richiama uno di essi, la differenza tra il caso generale e il caso specifico, allora hai il caso di usare firme diverse, ad es. sovraccarichi in alcune lingue. I sovraccarichi con meno parametri vengono implementati richiamando quello generale con tutti i parametri.

È possibile implementare i sovraccarichi utilizzando una copia specializzata del codice generale ottimizzata per quel caso, ma trattarla come ottimizzazione, in cui aumenta la complessità del codice (a titolo di duplicazione) e dovrebbe essere intrapresa in un punto come sai che questa complessità extra offre un vero valore.

Se non puoi / non sai staticamente quale scegliere (ad es. quando scrivi un codice che richiama uno di questi), allora la differenziazione è dinamica. Nella situazione dinamica, l'implementazione verifica efficacemente i parametri in fase di esecuzione e viene utilizzato il caso appropriato (specializzato). Un meccanismo per testare dinamicamente le condizioni dei parametri e scegliere la giusta specializzazione è la gerarchia delle classi (ad esempio metodi virtuali); un'altra è la spedizione multi-metodo (offerta direttamente in alcune lingue e ottenibile come schema in altre lingue).

    
risposta data 30.07.2017 - 18:23
fonte

Leggi altre domande sui tag