2013-08-08 16 views
11

Sto cercando consigli su come gestire la seguente situazione.Async Try (blah) pattern

Sto creando metodi per cercare di ottenere alcuni dati, seguendo questo schema:

// Typical pattern 
public bool TryBlah(string key, out object value) 
{ 
    // ... set value and return boolean 
} 

ho incontrato un problema quando si cerca di seguire questo modello in poi Async versioni perché non è possibile utilizzare out sui metodi asincroni:

// Ideal async pattern (not allowed to use an 'out' parameter, so this fails) 
public async Task<bool> TryBlah(string key, out object value) 
{ 
    // ... set value, perform some slow io operation, return bool 
} 

Una soluzione è di restituire una tupla contenente i dati. Funziona per metodi che restituiscono un singolo tipo di dati in questo modo:

// Tuple version 
public async Task<Tuple<bool, object>> TryBlah(string key) 
{ 
    // ... perform some slow io, return new Tuple<bool, object>(...) 
} 

Il problema si verifica quando si desidera restituire tipi di dati diversi. Senza utilizzare async è possibile creare diversi metodi con firme quasi identiche in questo modo:

public bool TryBlah(string key, out byte[] value) 
{ 
    // ... 
} 
public bool TryBlah(string key, out string value) 
{ 
    // ... 
} 

Questo è grande. Questo è quello che sto cercando di fare. Questa API è molto semplice e facile da usare (i nomi dei metodi sono tutti uguali, solo i dati che vengono passati nelle modifiche).

Non essendo in grado di utilizzare out con metodi asincroni, questo però lo rovina.

Un modo per aggirare questo è restituire un Tuple dei dati. Tuttavia ora non è possibile avere firme di metodo pressoché identiche come la seguente:

// The suck... the signatures match, but you want to return different values. 
// You can't do this: 
public async Task<Tuple<bool, byte[]>> TryBlah(string key) 
{ 
    // ... 
} 
public async Task<Tuple<bool, string>> TryBlah(string key) 
{ 
    // ... 
} 

Questi metodi falliscono perché hanno le stesse firme. L'unico modo per ovviare a questo che viene in mente è quello di dare ad ogni metodo un nome diverso, in questo modo:

public async Task<Tuple<bool, byte[]>> TryBlahByteArray(string key) 
{ 
    // ... 
} 
public async Task<Tuple<bool, string>> TryBlahString(string key) 
{ 
    // ... 
} 

Il mio problema è che questo crea ora quello che considero un brutto api dove ora avete un bel po ' di diversi metodi. Sì, non è un grosso problema, ma sento che ci deve essere un modo migliore.

Ci sono altri schemi che si prestano a una API più bella quando si lavora con metodi asincroni come questo? Sono aperto a qualsiasi suggerimento.

risposta

1

Non userei un metodo Try * con TPL. Utilizzare invece una continuazione (Task.ContinueWith) con le opzioni OnlyOnFaulted.

In questo modo la vostra attività viene completata in un modo o in un altro e il chiamante arriva a decidere come gestire gli errori, cancellazioni, ecc

Si ottiene anche liberarsi della tupla.

Come per altri problemi di progettazione, ogni volta che vedo qualcuno che dice "Voglio che questo metodo si sovraccarichi in base al tipo di reso" Sento una cattiva idea. Preferirei vedere nomi dettagliati (GetString, GetByte, GetByteArray, ecc. - guarda SqlDataReader) o fare in modo che l'API restituisca un tipo di base (es. Byte [] - guarda Stream) e permetta al chiamante di creare conversioni di livello superiore come StreamReader/TextReader/etc.

+3

Ho odore di promesse in C#? – cgatian

2

Sembra un problema per i generici.

public async Task<Tuple<bool, TResult>> TryBlah<TResult>(string key) 
{ 
    var resultType = typeof(TResult); 
    // ... perform some slow io, return new Tuple<bool, TResult>(...) 
} 
+0

Ho pensato di suggerire questo, ma dato che sembra interessato a una "cattiva API" non sono sicuro che ci sia molto vantaggio rispetto ai metodi distinti che l'OP aveva suggerito originariamente - in effetti devi digitare 2 caratteri extra: < and > :) Suppongo che ci potrebbero essere dei vantaggi dal punto di vista della creazione di questi metodi ... – mutex

+0

@mutex Beh, oltre a risolvere il problema iniziale di essere in grado di sovraccaricare (essenzialmente) sul tipo restituito, potrebbe anche essere una soluzione migliore al problema più generale di dover gestire la stessa operazione su diversi tipi di dati diversi. –

+0

Il grosso problema è che non ci sono indicazioni su quali tipi sono consentiti. Idealmente, i generici dovrebbero essere usati con un metodo che può funzionare con qualsiasi tipo, non solo con una mezza dozzina di tipi specifici. – svick

7

Forse si potrebbe usare Action<T> come il sostituto fuori param

Esempio:

public async Task<bool> TryBlah(string key, Action<int> value) 
{ 
    int something = await DoLongRunningIO(); 
    value(something) 
    return true;   
} 

Usage:

int myOutParam = 0; 
if (await TryBlah("Something", value => myOutParam = value)) 
{ 
    // do somthing 
} 
+0

Questo è abbastanza fantastico –

0

Sembra che si sta tentando di fare un'API che accetta una richiesta e quindi recupera alcuni dati e quindi elabora/converte tali dati in in un modo particolare e lo restituisce al chiamante. Cosa succede se hai implementato un gestore che gestirà i diversi metodi di elaborazione.

Propongo una soluzione per la creazione di una classe di richieste e risposte che verrà passata al gestore e il gestore restituirà un risultato al termine dell'elaborazione.

public class Request 
{ 
    public Type ReturnType; 
    public string Key { get; set; } 
    public Request(string Key, Type returnType) 
    { 
     this.Key = Key; 
     this.ReturnType = returnType; 
    } 
} 

public class Response 
{ 
    public object value; 
    public Type returnType; 
} 

//Singleton processor to get data out of cache 
public class CacheProcessor 
{ 
    private static CacheProcessor instance; 

    public static CacheProcessor Process 
    { 
     get 
     { 
      if (instance == null) 
       instance = new CacheProcessor(); 
      return instance; 
     } 
    } 

    private Dictionary<Type, Func<Request, object>> Processors = new Dictionary<Type, Func<Request, object>>(); 

    private CacheProcessor() 
    { 
     CreateAvailableProcessors(); 
    } 

    //All available processors available here 
    //You could change type to string or some other type 
    //to extend if you need something like "CrazyZipUtility" as a processor 
    private void CreateAvailableProcessors() 
    { 
     Processors.Add(typeof(string), ProcessString); 
     Processors.Add(typeof(byte[]), ProcessByteArry); 
    } 

    //Fake method, this should encapsulate all crazy 
    //cache code to retrieve stuff out 
    private static string CacheGetKey(string p) 
    { 
     return "1dimd09823f02mf23f23f0"; //Bullshit data 
    } 

    //The goood old tryBlah... So Sexy 
    public Response TryBlah(Request request) 
    { 
     if (Processors.ContainsKey(request.ReturnType)) 
     { 
      object processedObject = Processors[request.ReturnType].Invoke(request); 
      return new Response() 
      { 
       returnType = request.ReturnType, 
       value = processedObject 
      }; 
     } 
     return null; 
    } 

    //Maybe put these in their own class along with the dictionary 
    //So you can maintain them in their own file 
    private static object ProcessString(Request request) 
    { 
     var value = CacheGetKey(request.Key); 
     //Do some shit 
     return value; 
    } 

    private static object ProcessByteArry(Request request) 
    { 
     var value = CacheGetKey(request.Key); 
     ASCIIEncoding encoding = new ASCIIEncoding(); 
     Byte[] bytes = encoding.GetBytes(value); 
     return bytes; 
    } 
} 

La cosa importante è che il Dizionario (o un HashSet) contiene i processori disponibili. Quindi, in base al tipo, viene richiamato il processore corretto e vengono restituiti i risultati.

Il codice verrà richiamato come segue.

var makeByteRequest = new Request("SomeValue", typeof(byte[])); 
Response btyeResponse = CacheProcessor.Process.TryBlah(makeByteRequest);