Async Try(blah) pattern [duplicate]

耗尽温柔 提交于 2019-12-21 06:56:20

问题


I'm looking for recommendations on how to handle the following situation.

I'm creating methods for trying to get at some data, following this pattern:

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

I've run into an issue when trying to follow this pattern on they async versions because you cannot use out on async methods:

// 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
}

One workaround is to return a tuple containing your data. This works for methods that return a single data type like so:

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

The issue is when you want to return different data types. Without using async you can create several methods with nearly identical signatures like so:

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

That's great. That's what I'm looking to do. This api is very straightforward and easy to work with (the method names are all the same, only the data that is passed in changes).

Not being able to use out with async methods messes this up though.

One way to get around this is to return a Tuple of your data. However now you can't have nearly identical method signatures like the following:

// 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)
{
    // ...
}

Those methods fail because they have the same signatures. The only way to work around this that comes to mind is to give each method a distinct name, like so:

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

My issue is that this now creates what I consider a nasty api where you now have a whole lot of different methods. Yes, it's not that big of an issue, but I feel that there has to be a better way.

Are there other patterns that lend themselves to a nicer api when working with async methods like this? I'm open to any suggestions.


回答1:


Maybe you could use Action<T> as the out param substitute

Example:

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
}



回答2:


Here is a circa 2017 update with ValueTuples, your sucky option is not so bad.

public async Task<(bool, byte[])> TryBlahByteArray(string key)
{
    // await something
    return (true, new byte[1]);
}
public async Task<(bool, string)> TryBlahString(string key)
{
    // await something
    return (false, "blah");
}

Used as

(bool success, byte[] blahs) = await TryBlahByteArray("key");

And

(bool success, string blah) = await TryBlahString("key");

I don't often want method names that are the same that return different things or a raw object anyway, so maybe this is less of a concern. Your mileage may vary.




回答3:


I would not use a Try* method with TPL. Instead use a continuation (Task.ContinueWith) with a the OnlyOnFaulted options.

This way your task completes one way or another and the caller gets to decide how to handle errors, cancellations, etc.

It also gets rid of the Tuple.

As for other design issues, anytime I see someone saying "I want this method to overload based on return type" I smell a bad idea. I'd rather see verbose names (GetString, GetByte, GetByteArray, etc - look at SqlDataReader) or have the API return a very basic type (e.g., byte[] - look at Stream) and let the caller create higher level conversions like StreamReader/TextReader/etc.




回答4:


Sounds like a problem for generics.

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



回答5:


It looks like you are trying to make an API that takes in a request and then retrieves some data and then processes/converts that data in a particular way and returns it back to the caller. What if you implemented a manager that would handle the different processing methods.

I propose a solution of creating a request and response class that will be passed to the manager, and the manager then returns a result after the processing is complete.

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;
    }
}

The big thing is the Dictionary (or a HashSet) holds your available processors. Then based on type, the correct processor is invoked and results are returned.

The code would be invoked as follows.

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


来源:https://stackoverflow.com/questions/18117287/async-tryblah-pattern

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!