Шаблон Async Try (blah)

Я ищу рекомендации относительно того, как обращаться со следующей ситуацией.

Я создаю методы для попыток получить некоторые данные, следуя этому шаблону:

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

У меня возникла проблема при попытке следовать этому шаблону в асинхронных версиях, потому что вы не можете использовать out для методов async:

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

Обходным путем является возврат кортежа, содержащего ваши данные. Это работает для методов, которые возвращают один тип данных, например:

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

Проблема заключается в том, когда вы хотите вернуть разные типы данных. Без использования async вы можете создать несколько методов с почти одинаковыми сигнатурами:

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

Это здорово. Это то, что я ищу. Этот api очень прост и удобен в работе (имена методов все одинаковы, только данные, которые передаются в изменениях).

Невозможность использовать out с помощью методов async все равно испортит это.

Один из способов обойти это - вернуть Tuple ваших данных. Однако теперь вы не можете иметь почти идентичные сигнатуры методов, например:

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

Эти методы терпят неудачу, потому что они имеют одинаковые подписи. Единственный способ обойти это, что приходит на ум, - дать каждому методу отличное имя, например:

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

Моя проблема в том, что теперь это создает то, что я считаю неприятным api, где у вас теперь есть много разных методов. Да, это не так уж и важно, но я считаю, что должен быть лучший способ.

Существуют ли другие шаблоны, которые поддаются лучшему api при работе с такими асинхронными методами? Я открыт для любых предложений.

Ответ 1

Возможно, вы могли бы использовать Action<T> как замену out param

Пример:

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

Использование:

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

Ответ 2

Вот около 2017 года обновление с ValueTuples, ваш вариант не так уж и плох.

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

Используется как

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

и

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

Я не часто хочу, чтобы имена методов были одинаковыми и возвращали разные вещи или необработанный object, так что, возможно, это не так важно. Ваш пробег может варьироваться.

Ответ 3

Я бы не использовал метод Try * с TPL. Вместо этого используйте продолжение (Task.ContinueWith) с опциями OnlyOnFaulted.

Таким образом ваша задача завершается так или иначе, и вызывающий получает решение о том, как обрабатывать ошибки, отмены и т.д.

Он также избавляется от Tuple.

Что касается других проблем дизайна, в любое время я вижу, что кто-то говорит: "Я хочу, чтобы этот метод перегружался на основе типа возврата". Я плохо отношусь к плохой идее. Я бы предпочел увидеть подробные имена (GetString, GetByte, GetByteArray и т.д. - посмотреть на SqlDataReader) или вернуть API очень простой тип (например, byte [] - посмотреть Stream) и позволить вызывающему абоненту создавать более высокие уровни, такие как StreamReader/ЧтениеТекста/др.

Ответ 4

Звучит как проблема для дженериков.

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

Ответ 5

Похоже, вы пытаетесь сделать API, который принимает запрос, а затем извлекает некоторые данные, а затем обрабатывает или конвертирует эти данные определенным образом и возвращает его обратно вызывающему. Что делать, если вы реализовали менеджер, который будет обрабатывать различные методы обработки.

Я предлагаю решение создания класса запроса и ответа, которое будет передано диспетчеру, а затем менеджер вернет результат после завершения обработки.

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

Большая вещь - словарь (или HashSet) содержит ваши доступные процессоры. Затем, основываясь на типе, вызывается правильный процессор и возвращаются результаты.

Код будет вызываться следующим образом.

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