Полная асинхронность при использовании конвейера
Если у меня есть приложение, которое использует конвейер со многими этапами, которые нужно выполнить, используя foreach
на всех этапах и звоните:
CanExecute
Execute
Интерфейс такой:
public interface IService
{
bool CanExecute(IContext subject);
IContext Execute(IContext subject);
}
Он в основном берет контекст и возвращает контекст, в котором он стал богаче.
В рамках одного из этапов Execute
Мне нужно вызвать службу, и я хочу выполнить асинхронность. Итак, теперьExecute
метод необходимо изменить, например, на
Task<IContext> ExecuteAsync(IContext subject);
с участием await
за звонок в сервис.
Все остальные этапы не имеют асинхронного кода, но их необходимо изменить сейчас, поскольку рекомендуется "полностью асинхронно".
Нормально ли вносить эти изменения при вводе асинхронного кода?
2 ответа
C# 8 предлагает несколько способов избежать изменения синхронных служб. C# 7 также может справиться с этим с помощью операторов сопоставления с образцом.
Члены реализации по умолчанию
Управление версиями интерфейса - один из основных вариантов использования членов интерфейса по умолчанию. Их можно использовать, чтобы избежать изменения существующих классов при изменении интерфейса. Вы можете добавить реализацию по умолчанию дляExecuteAsync
который возвращает результат Execute
как ValueTask.
Допустим, у вас есть эти интерфейсы:
public interface IContext{}
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
}
public class ServiceA:IService
{
public bool CanExecute(IContext subject)=>true;
public IContext Execute(IContext subject){return subject;}
}
Чтобы создать асинхронную службу без изменения синхронных, вы можете добавить реализацию по умолчанию в IService и переопределить ее в новых службах:
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
public ValueTask<IContext> ExecuteAsync(IContext subject)=>new ValueTask<IContext>(Execute(subject));
}
public class ServiceB:IService
{
public bool CanExecute(IContext subject)=>true;
public IContext Execute(IContext subject)=>ExecuteAsync(subject).Result;
public async ValueTask<IContext> ExecuteAsync(IContext subject)
{
await Task.Yield();
return subject;
}
}
ServiceB.Execute
по-прежнему нужно тело, и одна вещь имеет смысл - позвонить ExecuteAsync()
и заблокировать, как бы уродливо это не выглядело. Другая возможность - выбросить, еслиExecute
называется:
public IContext Execute(IContext subject)=>throw new InvalidOperationException("This is an async service");
Соответствие шаблону
Другой вариант - создать второй интерфейс только для асинхронных сервисов:
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
}
public interface IServiceAsync:IService
{
public ValueTask<IContext> ExecuteAsync(IContext subject);
}
Обе реализации сервиса останутся прежними. Код конвейера изменится, чтобы выполнять разные вызовы в зависимости от типа службы:
async Task Main()
{
IService[] pipeline=new[]{(IService)new ServiceA(),new ServiceB()};
IContext ctx=new Context();
foreach(var svc in pipeline)
{
if (svc.CanExecute(ctx))
{
var result=svc switch { IServiceAsync a=>await a.ExecuteAsync(ctx),
IService b => b.Execute(ctx)};
ctx=result;
}
}
}
Выражение сопоставления с образцом вызывает другую ветвь в зависимости от типа текущей службы. Привязка к типу создает строго типизированный экземпляр (a или b), который можно использовать для вызова соответствующего метода.
Выражения переключения являются исчерпывающими - компилятор выдаст предупреждение, если не сможет проверить, что все параметры соответствуют шаблонам.
C# 7
В C# 7 нет выражений переключения, поэтому необходим более подробный оператор переключения для сопоставления шаблонов:
if (svc.CanExecute(ctx))
{
switch (svc)
{
case IServiceAsync a:
ctx=await a.ExecuteAsync(ctx);
break;
case IService b :
ctx=b.Execute(ctx);
break;
default:
throw new InvalidOperationException("Unknown service type!");
}
}
Переключатель заявление не является исчерпывающим, поэтому мы должны добавитьdefault
раздел для обнаружения ошибок во время выполнения.
Это нормально - вносить эти изменения при вводе асинхронного кода?
Когда вы меняете подпись любого метода, это нормально. Если вы хотите переименовать его и изменить тип возвращаемого значения, тогда да, везде, где вызывается этот метод, придется изменить.
Лучший способ изменить их - сделать их также асинхронными на всем протяжении цепочки.