Шаблон Async Try(бла)
Я ищу рекомендации о том, как справиться со следующей ситуацией.
Я создаю методы для получения некоторых данных, следуя этой схеме:
// Typical pattern
public bool TryBlah(string key, out object value)
{
// ... set value and return boolean
}
Я столкнулся с проблемой при попытке следовать этому шаблону на асинхронных версиях, потому что вы не можете использовать out
на асинхронных методах:
// 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
с асинхронными методами все портит.
Один из способов обойти это вернуть 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 при работе с такими асинхронными методами? Я открыт для любых предложений.
5 ответов
Может быть, вы могли бы использовать Action<T>
в качестве заменителя
Пример:
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
}
Вот около 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
в любом случае, так что, может быть, это не так важно. Ваш пробег может отличаться.
Похоже, проблема для дженериков.
public async Task<Tuple<bool, TResult>> TryBlah<TResult>(string key)
{
var resultType = typeof(TResult);
// ... perform some slow io, return new Tuple<bool, TResult>(...)
}
Я бы не использовал метод Try* с TPL. Вместо этого используйте продолжение (Task.ContinueWith) с параметрами OnlyOnFaults.
Таким образом, ваша задача так или иначе завершается, и вызывающая сторона решает, как обрабатывать ошибки, отмены и т. Д.
Это также избавляет от Tuple.
Что касается других проблем проектирования, то каждый раз, когда я вижу, что кто-то говорит: "Я хочу, чтобы этот метод перегружался в зависимости от типа возвращаемого значения", я чувствую плохую идею. Я бы предпочел видеть подробные имена (GetString, GetByte, GetByteArray и т. Д. - посмотрите на SqlDataReader) или чтобы API возвращал очень простой тип (например, byte[] - посмотрите на Stream) и позволил вызывающей стороне создавать преобразования более высокого уровня, такие как StreamReader. / ЧтениеТекст / и т.д..
Похоже, вы пытаетесь создать 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);