8

Modifica: risolto diversi problemi di sintassi e coerenza per rendere il codice un po 'più evidente e vicino a quello che sto facendo.Abuso di chiusure? Violazioni di vari principi? O ok?

Ho un codice che assomiglia a questo:

SomeClass someClass; 
var finalResult = 
    DoSomething(() => 
    { 
    var result = SomeThingHappensHere(); 
    someClass = result.Data; 
    return result; 
    }) 
    .DoSomething(() => return SomeOtherThingHappensHere(someClass)) 
    .DoSomething(() => return AndYetAnotherThing()) 
    .DoSomething(() => return AndOneMoreThing(someClass)) 
    .Result; 

HandleTheFinalResultHere(finalResult); 

in cui il metodo DoSomething è un metodo di estensione, e si aspetta un Funz passò in esso. Quindi, ciascuna delle chiamate di metodo in ogni DoSomething => lambda restituisce un tipo di risultato.

questo è simile a un Maybe monad. Tranne che controllare i valori nulli, sto controllando lo stato della classe Result e chiamando il Func passato a DoSomething o restituendo il risultato precedente senza chiamare Func

il problema che devo affrontare è che voglio avere questo tipo di composizione nel mio codice, ma devo anche essere in grado di passare i dati da uno dei risultati di chiamata composti alla chiamata di un altro, come si può vedere con la variabile someClass.

La mia domanda non è se questo sia tecnicamente corretto ... so che questo funziona, perché attualmente lo sto facendo. La mia domanda è se questo è un abuso di chiusure, o una separazione di query di comando, o altri principi ... e poi chiedere quali sono i modelli migliori per gestire questa situazione, perché sono abbastanza sicuro di essere bloccato in una modalità "nuovo martello lucido" con questo tipo di codice, proprio ora.

+4

Non è molto facile da leggere .... –

+0

100% d'accordo. purtroppo, è ancora un miglioramento significativo rispetto a quello che ho iniziato. un po 'più di formattazione potrebbe aiutare ... potrebbe ... –

+2

Ahi, i miei occhi .... –

risposta

11

Come già notato, hai quasi implementato una Monade qui.

Il tuo codice è un po 'poco elegante in quanto i lambdas hanno effetti collaterali. Le Monadi risolvono questo più elegantemente.

Quindi, perché non trasformare il codice in una Monade corretta?

Bonus: è possibile utilizzare la sintassi LINQ!


I presenti:

LINQ to Risultati

 
Esempio:

var result = 
    from a in SomeThingHappensHere() 
    let someData = a.Data 
    from b in SomeOtherThingHappensHere(someData) 
    from c in AndYetAnotherThing() 
    from d in AndOneMoreThing(someData) 
    select d; 

HandleTheFinalResultHere(result.Value); 

Con LINQ to risultati, questa prima esegue SomeThingHappensHere. Se ciò riesce, ottiene il valore della proprietà Data del risultato ed esegue SomeOtherThingHappensHere.Se ciò riesce, esegue AndYetAnotherThing e così via.

Come si può vedere, è possibile eseguire facilmente operazioni a catena e fare riferimento ai risultati delle operazioni precedenti. Ogni operazione verrà eseguita una dopo l'altra e l'esecuzione si interromperà quando viene rilevato un errore.

Il from x in bit ogni riga è un po 'rumoroso, ma IMO nulla di simile complessità sarà più leggibile di così!


Come si fa a fare questo lavoro?

Monadi in C# sono costituiti da tre parti:

  • un tipo Qualcosa-of-T,

  • Select/SelectMany metodi di estensione per esso, e

  • un metodo per convertire un T in un Something-of-T.

Tutto quello che dovete fare è creare qualcosa che assomiglia a una Monade, si sente come un Monade e gli odori come una monade, e tutto funzionerà automagicamente.


I tipi e metodi per LINQ a Risultati sono i seguenti.

Risultato <T> Tipo:

Una classe semplice che rappresenta un risultato. Un risultato è un valore di tipo T o un errore. Un risultato può essere generato da un T o da un Eccezione.

class Result<T> 
{ 
    private readonly Exception error; 
    private readonly T value; 

    public Result(Exception error) 
    { 
     if (error == null) throw new ArgumentNullException("error"); 
     this.error = error; 
    } 

    public Result(T value) { this.value = value; } 

    public Exception Error 
    { 
     get { return this.error; } 
    } 

    public bool IsError 
    { 
     get { return this.error != null; } 
    } 

    public T Value 
    { 
     get 
     { 
      if (this.error != null) throw this.error; 
      return this.value; 
     } 
    } 
} 

metodi di estensione:

implementazioni per i metodi Select e SelectMany. Le firme del metodo sono fornite nella specifica C#, quindi tutto ciò di cui ti devi preoccupare sono le loro implementazioni. Questi vengono abbastanza naturalmente se si tenta di combinare tutti gli argomenti del metodo in modo significativo.

static class ResultExtensions 
{ 
    public static Result<TResult> Select<TSource, TResult>(this Result<TSource> source, Func<TSource, TResult> selector) 
    { 
     if (source.IsError) return new Result<TResult>(source.Error); 
     return new Result<TResult>(selector(source.Value)); 
    } 

    public static Result<TResult> SelectMany<TSource, TResult>(this Result<TSource> source, Func<TSource, Result<TResult>> selector) 
    { 
     if (source.IsError) return new Result<TResult>(source.Error); 
     return selector(source.Value); 
    } 

    public static Result<TResult> SelectMany<TSource, TIntermediate, TResult>(this Result<TSource> source, Func<TSource, Result<TIntermediate>> intermediateSelector, Func<TSource, TIntermediate, TResult> resultSelector) 
    { 
     if (source.IsError) return new Result<TResult>(source.Error); 
     var intermediate = intermediateSelector(source.Value); 
     if (intermediate.IsError) return new Result<TResult>(intermediate.Error); 
     return new Result<TResult>(resultSelector(source.Value, intermediate.Value)); 
    } 
} 

È possibile modificare liberamente il Risultato <T> classe ed i metodi di estensione, ad esempio, per implementare regole più complesse. Solo le firme dei metodi di estensione devono essere esattamente come indicato.

+0

Son-of-a ... Ho sprecato un'ora a lavorare su questo. Roba buona. –

+0

Sì, lo adoro. –

+0

Fantastico. Sono state per un po 'sconcertate le monadi e questo ha avuto molto senso e ha sicuramente aiutato la mia comprensione. Non potresti spostare gran parte della logica in Select/SelectMany extensions in un Bind (Func ) o simile? Non completamente sicuro della sintassi, ma rimuoverebbe molto della duplicazione nei metodi di estensione. – Neal

2

Mi sembra che tu abbia costruito qualcosa di molto simile a una monade qui.

si potrebbe fare una vera e propria monade, rendendo digitare il delegato un Func<SomeClass, SomeClass>, avere un modo per impostare il valore iniziale SomeClass a passare in, e hanno DoSomething passare il valore di ritorno di uno come il parametro del prossimo - questo renderebbe il concatenamento esplicito piuttosto che basarsi su uno stato condiviso con scope lessicale.

+0

sì, ho studiato/apprendo le monadi di recente e stavo cercando di creare qualcosa in questo senso. problema con il tuo suggerimento è che ho bisogno di restituire sempre la classe "risultato" dalla funzione, e in un caso ho bisogno anche di ottenere i dati "someClass" da una funzione alla successiva. –

+0

+1 Gli effetti collaterali sono difficili da ragionare e testare. Ti ringrazierai più tardi se rifattori su 'Func' invece di' Action' e esegui operazioni in modo esplicito. –

+0

Perché lasciare il tipo di risultato così critico? Viene trasmesso attraverso il codice di qualcun altro che non è possibile modificare prima di tornare ad avere un altro link nella catena di composizione attaccata? –

0

La debolezza di questo codice è l'accoppiamento implicito tra il primo e il secondo lambda. Non sono sicuro del modo migliore per risolverlo.

+0

sicuramente. è un cattivo design e un accoppiamento semantico - devi sapere cosa sta succedendo sotto il cofano per comprendere le ramificazioni di tirare fuori il valore da uno e nell'altro. –