Este artigo foi traduzido por máquina.

Programação assíncrona

Padrões para aplicativos MVVM assíncronos: Serviços

Stephen Cleary

Este é o terceiro artigo de uma série sobre a combinação de async e aguardam-se com o padrão Model-View-ViewModel (MVVM) estabelecido. No primeiro artigo, eu desenvolvi um método para vinculação de dados para uma operação assíncrona. Na segunda, eu considerei algumas implementações possíveis de um ICommand assíncrono. Agora, eu vou virar para a camada de serviço e serviços assíncronos de endereço.

Não vou lidar com uma interface do usuário de todo. Na verdade, os padrões neste artigo não são específicos para MVVM; pertencem igualmente bem para qualquer tipo de aplicação. A ligação de dados assíncronos e padrões de comando explorados em meus artigos anteriores são completamente novos; os padrões de serviço assíncrono neste artigo são mais estabelecidos. Ainda assim, padrões estabelecidos nem são apenas padrões.

Interfaces assíncronas

"Programa para uma interface, não uma implementação". Como esta citação de "Design Patterns: Elementos de Software orientado a objeto reutilizável"(Addison-Wesley, 1994, p. 18) sugere, interfaces são um componente crítico do projeto orientado a objeto apropriado. Eles permitem que seu código para usar uma abstração, ao invés de um tipo de concreto, e eles dão seu código um "ponto de junção", em que você pode juntar para testes de unidade. Mas é possível criar uma interface com métodos assíncronos?

A resposta é sim. O código a seguir define uma interface com um método assíncrono:

public interface IMyService
{
  Task<int> DownloadAndCountBytesAsync(string url);
}

A implementação do serviço é simples:

public sealed class MyService : IMyService
{
  public async Task<int> DownloadAndCountBytesAsync(string url)
  {
    await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
    using (var client = new HttpClient())
    {
      var data = await 
        client.GetByteArrayAsync(url).ConfigureAwait(false);
      return data.Length;
    }
  }
}

Figura 1 mostra como o código que consome o serviço chama o método assíncrono definido na interface.

Figura 1 UseMyService.cs: Chamando o método de Async definido na Interface

public sealed class UseMyService
{
  private readonly IMyService _service;
  public UseMyService(IMyService service)
  {
    _service = service;
  }
  public async Task<bool> IsLargePageAsync(string url)
  {
    var byteCount = 
      await _service.DownloadAndCountBytesAsync(url);
    return byteCount > 1024;
  }
}

Isto pode parecer como um exemplo a ser simplista, mas ilustra algumas lições importantes sobre métodos assíncronos.

A primeira lição é: Métodos não são awaitable, são tipos. É o tipo de uma expressão que determina se essa expressão é awaitable. Em particular, UseMyService.IsLargePageAsync aguarda o resultado da IMyService.DownloadAndCountBytesAsync. O método de interface não é (e não pode ser) marcado async. IsLargePageAsync pode usar aguardar porque o método de interface retorna uma tarefa, e as tarefas são awaitable.

A segunda lição é: Async é um detalhe de implementação. UseMyService não sabe nem se importa se os métodos de interface são implementados usando async ou não. O código consumidor preocupa-se apenas que o método retorna uma tarefa. Usando async e esperam é uma maneira comum de implementar um método de retorno de tarefa, mas não é a única maneira. Por exemplo, o código em Figura 2 usa um padrão comum para sobrecarga de métodos assíncronos.

Figura 2 AsyncOverloadExample.cs: Usando um padrão comum para sobrecarga de métodos assíncronos

class AsyncOverloadExample
{
  public async Task<int> 
    RetrieveAnswerAsync(CancellationToken cancellationToken)
  {
    await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
    return 42;
  }
  public Task<int> RetrieveAnswerAsync()
  {
    return RetrieveAnswerAsync(CancellationToken.None);
  }
}

Observe que uma sobrecarga simplesmente chama o outro e retorna a sua tarefa diretamente. É possível escrever essa sobrecarga usando async e aguardam, mas isso só iria adicionar sobrecarga e não fornecer nenhum benefício.

Testes de unidade assíncrona

Existem outras opções para a implementação de métodos de retorno de tarefa. Task.FromResult é uma escolha comum para stubs de teste de unidade, porque é a maneira mais fácil para criar uma tarefa concluída. O código a seguir define uma implementação do esboço do serviço:

class MyServiceStub : IMyService
{
  public int DownloadAndCountBytesAsyncResult { get; set; }
  public Task<int> DownloadAndCountBytesAsync(string url)
  {
    return Task.FromResult(DownloadAndCountBytesAsyncResult);
  }
}

Você pode usar essa implementação de esboço para testar o UseMyService, conforme mostrado no Figura 3.

Figura 3 UseMyServiceUnitTests.cs: Esboço de implementação de teste UseMyService

[TestClass]
public class UseMyServiceUnitTests
{
  [TestMethod]
  public async Task UrlCount1024_IsSmall()
  {
    IMyService service = new MyServiceStub { 
      DownloadAndCountBytesAsyncResult = 1024 
    };
    var logic = new UseMyService(service);
    var result = await 
      logic.IsLargePageAsync("http://www.example.com/");
    Assert.IsFalse(result);
  }
  [TestMethod]
  public async Task UrlCount1025_IsLarge()
  {
    IMyService service = new MyServiceStub { 
      DownloadAndCountBytesAsyncResult = 1025 
    };
    var logic = new UseMyService(service);
    var result = await 
      logic.IsLargePageAsync("http://www.example.com/");
    Assert.IsTrue(result);
  }
}

Este exemplo de código usa MSTest, mas a maioria das outras estruturas de teste de unidade moderna também oferecem suporte a testes de unidade assíncrona. Apenas certifique-se de sua unidade testes retornam a tarefa; Evite métodos de teste de unidade vazio async. A maioria das estruturas de teste de unidade não oferecem suporte a métodos de teste de unidade vazio async.

Quando métodos síncronos testes de unidade, é importante testar como o código se comporta tanto em condições de sucesso e fracasso. Métodos assíncronos adicionar uma ruga: É possível para um serviço assíncrono suceder ou lançar uma exceção, de forma síncrona ou assíncrona. Se você quiser, mas acho que é suficiente testar o sucesso pelo menos assíncrono e assíncrona falha, mais sucesso síncrono se necessário, você pode testar todos os quatro dessas combinações. O teste de sucesso síncrono é útil porque o operador await agirá de forma diferente se sua operação já foi concluída. No entanto, não acho o teste falha síncrona como útil, porque o fracasso não é imediato com mais assíncronas operações.

Como desta escrita, alguns populares zombando e arrancar quadros retornará default (t) a menos que você modificar esse comportamento. O padrão de comportamento a gozar não funciona bem com métodos assíncronos porque métodos assíncronos nunca devem retornar uma tarefa nula (de acordo com o padrão assíncrono baseado em tarefas, que você encontrará em bit.ly/1ifhkK2). O comportamento padrão adequado seria retornar Task.FromResult(default(T)). Este é um problema comum quando unidade de teste de código assíncrono; Se você está vendo NullReferenceExceptions inesperados em seus testes, certifique-se de que tipos de simulação são implementar todos os métodos de retorno de tarefa. Espero que zombando e arrancar quadros irão tornar-se mais ciente de async no futuro e implementar melhor comportamento padrão para métodos assíncronos.

Fábricas assíncronas

Os padrões até agora demonstraram como definir uma interface com um método assíncrono; como implementá-lo em um serviço; e como definir um esboço para fins de teste. Estas são suficientes para serviços mais assíncronos, mas há um outro nível de complexidade que se aplica quando uma implementação de serviço deve fazer algum trabalho assíncrono antes de que está pronto para ser usado. Deixe-me descrever como lidar com a situação onde você precisa de um Construtor assíncrono.

Construtores não podem ser assíncronos, mas métodos estáticos podem. Uma maneira de simular um Construtor assíncrono é implementar um método assíncrono de fábrica, conforme mostrado no Figura 4.

Figura 4 serviço com um método assíncrono de fábrica

interface IUniversalAnswerService
{
  int Answer { get; }
}
class UniversalAnswerService : IUniversalAnswerService
{
  private UniversalAnswerService()
  {
  }
  private async Task InitializeAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(2));
    Answer = 42;
  }
  public static async Task<UniversalAnswerService> CreateAsync()
  {
    var ret = new UniversalAnswerService();
    await ret.InitializeAsync();
    return ret;
  }
  public int Answer { get; private set; }
}

Eu realmente gosto da abordagem de fábrica assíncrono porque ele não pode ser usurpado. Código de chamada não pode invocar o construtor diretamente; Ele deve usar o método de fábrica para obter uma instância e a instância é inicializada totalmente antes de ele é retornado. No entanto, isso não pode ser usado em alguns cenários. Como desta escrita, inversão de controle (IoC) e frameworks de injeção (DI) de dependência não entendo qualquer convenções para os métodos de fábrica assíncrono. Se você está injetando seus serviços usando um contêiner IoC/DI, você precisará de uma abordagem alternativa.

Recursos assíncronos

Em alguns casos, a inicialização assíncrona é necessária apenas uma vez, para inicializar recursos compartilhados. Stephen Toub desenvolvido Async­Lazy < T > tipo (bit.ly/1cVC3nb), que também está disponível como uma parte da minha biblioteca de AsyncEx (bit.ly/1iZBHOW). AsyncLazy < T > combina Lazy < T > com tarefa < T >. Especificamente, é um preguiçoso < < T >> de tarefa, um tipo de preguiçoso que suporta os métodos de fábrica assíncrono. O preguiçoso < T > camada fornece inicialização ociosa segura para thread, garantindo que o método de fábrica é executado apenas uma vez; a tarefa < T > camada fornece suporte assíncrono, que permite chamadores de esperar de forma assíncrona para o método de fábrica seja concluída.

Figura 5 apresenta uma definição ligeiramente simplificada de AsyncLazy < T >. Figura 6 mostra como AsyncLazy < T > pode ser usado dentro de um tipo.

Figura 5 definição de AsyncLazy < T >

// Provides support for asynchronous lazy initialization.
// This type is fully thread-safe.
public sealed class AsyncLazy<T>
{
  private readonly Lazy<Task<T>> instance;
  public AsyncLazy(Func<Task<T>> factory)
  {
    instance = new Lazy<Task<T>>(() => Task.Run(factory));
  }
  // Asynchronous infrastructure support.
// Permits instances of this type to be awaited directly.
public TaskAwaiter<T> GetAwaiter()
  {
    return instance.Value.GetAwaiter();
  }
}

Figura 6 AsyncLazy < T > Usado em um tipo

class MyServiceSharingAsyncResource
{
  private static readonly AsyncLazy<int> _resource =
    new AsyncLazy<int>(async () =>
    {
       await Task.Delay(TimeSpan.FromSeconds(2));
       return 42;
    });
  public async Task<int> GetAnswerTimes2Async()
  {
    int answer = await _resource;
    return answer * 2;
  }
}

Este serviço define um único "recurso compartilhado" que deve ser construído de forma assíncrona. Quaisquer métodos de todas as instâncias deste serviço podem depender desse recurso e esperam por ele diretamente. A primeira vez que o AsyncLazy < T > instância é aguardada, começará o método assíncrono de fábrica uma vez em um thread do pool. Qualquer outro acesso simultâneo a essa mesma instância de outro thread aguardará até que o método assíncrono de fábrica foi enfileirado para o pool de segmentos.

A parte síncrona de thread-safe do AsyncLazy < T > comportar­ior é tratada pelo Lazy < T > camada. O bloqueio de tempo gasto é muito curto: cada thread aguarda apenas o método de fábrica ser enfileirado para o pool de segmentos; Eles não esperam por ele para executar. Uma vez a tarefa < T > é retornado a partir do método de fábrica, então o Lazy < T > Acabou-se o emprego da camada. A mesma tarefa < T > instância é compartilhada com todos os espera. Métodos assíncronos fábrica nem a inicialização assíncrona de preguiçosa nunca irá expor uma instância de T até que a inicialização assíncrona foi concluída. Isto protege contra uso indevido acidental do tipo.

AsyncLazy < T > é ótimo para um tipo particular de problema: inicialização assíncrona de um recurso compartilhado. No entanto, pode ser complicado de usar em outros cenários. Em particular, se uma instância de serviço precisa de um Construtor assíncrono, você pode definir um tipo de serviço "interna" que faz a inicialização assíncrona e usar AsyncLazy < T > para embrulhar a instância interna dentro do tipo de serviço "exterior". Mas isso leva ao código complicado e tedioso, com todos os métodos dependendo da mesma instância interna. Em tais situações, uma verdadeira "Construtor assíncrono" seria mais elegante.

Um passo em falso

Antes de eu ir para minha solução preferida, quero assinalar um erro banal. Quando os desenvolvedores são confrontados com trabalho assíncrono para fazer em um Construtor (que não pode ser assíncrona), a solução podem ser algo como o código em Figura 7.

Figura 7 solução quando confrontado com Async trabalho para fazer em um construtor

class BadService
{
  public BadService()
  {
    InitializeAsync();
  }
  // BAD CODE!!
private async void InitializeAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(2));
    Answer = 42;
  }
  public int Answer { get; private set; }
}

Mas há sérios problemas com essa abordagem. Em primeiro lugar, não há nenhuma maneira de saber quando a inicialização foi concluída; em segundo lugar, quaisquer exceções da inicialização serão tratadas da maneira void async habitual, comumente, falhando o aplicativo. Se InitializeAsync era tarefa assíncrona em vez de async void, a situação seria mal maior: Ainda não haveria nenhuma maneira de saber quando a inicialização completada e todas as exceções seria silenciosamente ignorado. Há uma maneira melhor!

O padrão de inicialização assíncrona

A maioria dos código de criação baseada em reflexão (IoC/DI quadros, Activator e assim por diante) assume seu tipo tem um construtor e construtores não podem ser assíncronos. Se você está nessa situação, você é forçado a retornar uma instância que ainda não foi inicializada (assíncrona). O objetivo do padrão de inicialização assíncrona é fornecer uma forma padrão de lidar com essa situação, para atenuar o problema das instâncias não inicializados.

Primeiro, eu definir uma interface de "marcador". Se precisa de um tipo de inicialização assíncrona, ele implementa essa interface:

/// <summary>
/// Marks a type as requiring asynchronous initialization and
/// provides the result of that initialization.
/// </summary>
public interface IAsyncInitialization
{
  /// <summary>
  /// The result of the asynchronous initialization of this instance.
/// </summary>
  Task Initialization { get; }
}

À primeira vista, uma propriedade de tipo de tarefa se sente estranha. Acredito que é apropriado, no entanto, porque a operação assíncrona (inicializar instância) é uma operação de nível de instância. Então a propriedade de inicialização refere-se à instância como um todo.

Ao implementar essa interface, eu prefiro fazê-lo com um método de async real, que eu InitializeAsync o nome por convenção, como Figura 8 mostra:

Figura 8 serviço implementando o método InitializeAsync

class UniversalAnswerService : 
  IUniversalAnswerService, IAsyncInitialization
{
  public UniversalAnswerService()
  {
    Initialization = InitializeAsync();
  }
  public Task Initialization { get; private set; }
  private async Task InitializeAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(2));
    Answer = 42;
  }
  public int Answer { get; private set; }
}

O construtor é bastante simples; começa a inicialização assíncrona (chamando-se InitializeAsync) e em seguida, define a propriedade de inicialização. Essa propriedade de inicialização fornece os resultados do método InitializeAsync: Quando InitializeAsync for concluída, a tarefa de inicialização for concluída, e se houver algum erro, eles vão ser reproduzidos através da tarefa de inicialização.

Quando o construtor é concluída, a inicialização não ainda esteja completa, então o código consumidor tem que ter cuidado. O código usando o serviço tem a responsabilidade de assegurar que a inicialização for concluída antes de chamar outros métodos. O código a seguir cria e Inicializa uma instância de serviço:

async Task<int> AnswerTimes2Async()
{
  var service = new UniversalAnswerService();
  // Danger!
The service is uninitialized here; "Answer" is 0!
await service.Initialization;
  // OK, the service is initialized and Answer is 42.
return service.Answer * 2;
}

Em um cenário mais realista de IoC/DI, o código consumidor só Obtém uma instância em execução IUniversalAnswerService e tem que testar se ele implementa IAsyncInitialization. Esta é uma técnica útil; Ele permite que a inicialização assíncrona de ser um detalhe de implementação do tipo. Por exemplo, tipos de esboço provavelmente não usará inicialização assíncrona (a menos que você realmente está testando que o código consumido irá aguardar o serviço ser inicializado). O código a seguir é um uso mais realista do meu serviço de resposta:

async Task<int> 
  AnswerTimes2Async(IUniversalAnswerService service)
{
  var asyncService = service as IAsyncInitialization;
  if (asyncService != null)
    await asyncService.Initialization;
  return service.Answer * 2;
}

Antes de continuar com o padrão de inicialização assíncrona, deve assinalar uma alternativa importante. É possível expor os membros do serviço como métodos assíncronos que internamente, aguardar a inicialização de seus próprios objetos. Figura 9 mostra como ficaria este tipo de objeto.

Figura 9 serviço que aguarda sua própria inicialização

class UniversalAnswerService
{
  private int _answer;
  public UniversalAnswerService()
  {
    Initialization = InitializeAsync();
  }
  public Task Initialization { get; private set; }
  private async Task InitializeAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(2));
    _answer = 42;
  }
  public Task<int> GetAnswerAsync()
  {
    await Initialization;
    return _answer;
  }
}

Eu gosto dessa abordagem porque não é possível fazer mau uso de um objeto que ainda não foi inicializado. No entanto, limita a API do serviço, porque qualquer membro que depende de inicialização deve ser exposto como um método assíncrono. No exemplo anterior, a propriedade de resposta foi substituída com um método de GetAnswerAsync.

Compondo o padrão de inicialização assíncrona

Digamos que eu estou definindo um serviço que depende de vários outros serviços. Quando apresento o padrão de inicialização assíncrona de meus serviços, qualquer um desses serviços pode requerer inicialização assíncrona. O código para verificar se esses serviços implementam IAsyncInitialization pode ter um pouco tedioso, mas posso facilmente definir um tipo de auxiliar:

public static class AsyncInitialization
{
  public static Task 
    EnsureInitializedAsync(IEnumerable<object> instances)
  {
    return Task.WhenAll(
      instances.OfType<IAsyncInitialization>()
        .Select(x => x.Initialization));
  }
  public static Task EnsureInitializedAsync(params object[] instances)
  {
    return EnsureInitializedAsync(instances.AsEnumerable());
  }
}

Os métodos auxiliares ter qualquer número de instâncias de qualquer tipo, filtrar qualquer um que não implementar IAsyncInitialization e em seguida de forma assíncrona espera todas as tarefas de inicialização completar.

Com esses métodos auxiliares no lugar, criar um serviço composto é simples. O serviço no Figura 10 leva duas instâncias do serviço resposta como dependências, e a média de seus resultados.

Serviço de figura 10 que a média de resultados do serviço resposta como dependências

interface ICompoundService
{
  double AverageAnswer { get; }
}
class CompoundService : ICompoundService, IAsyncInitialization
{
  private readonly IUniversalAnswerService _first;
  private readonly IUniversalAnswerService _second;
  public CompoundService(IUniversalAnswerService first,
    IUniversalAnswerService second)
  {
    _first = first;
    _second = second;
    Initialization = InitializeAsync();
  }
  public Task Initialization { get; private set; }
  private async Task InitializeAsync()
  {
    await AsyncInitialization.EnsureInitializedAsync(_first, _second);
    AverageAnswer = (_first.Answer + _second.Answer) / 2.0;
  }
  public double AverageAnswer { get; private set; }
}

Existem alguns argumentos importantes para manter em mente ao redigir serviços. Primeiro, porque a inicialização assíncrona é um detalhe de implementação, o serviço composto não pode saber se qualquer uma de suas dependências requer inicialização assíncrona. Se nenhuma das dependências requer inicialização assíncrona, então nem teria o serviço composto. Mas porque não pode saber, o serviço composto deve declarar-se como exigindo a inicialização assíncrona.

Don' t se preocupar muito com as implicações de desempenho desta; haverá algumas alocações de memória extra para as estruturas assíncronas, mas o thread não irá se comportar de forma assíncrona. Aguardam tem uma otimização de "caminho rápido" que entra em jogo, sempre que o código espera por uma tarefa que já está completa. Se as dependências de um serviço composto não requerem inicialização assíncrona, a seqüência passada para Task.WhenAll está vazia, causando Task.WhenAll retornar uma tarefa já concluída. Quando essa tarefa é aguardada pelos CompoundService.InitializeAsync, não o rendimento execução porque a tarefa já foi concluída. Nesse cenário, InitializeAsync completa sincronicamente, antes que o construtor é concluída.

Um segundo argumento é que é importante inicializar todas as dependências, antes que o composto InitializeAsync retorna. Isso garante que a inicialização do tipo composto é totalmente completa. Além disso, a manipulação de erro é natural — se um serviço dependente tem um erro de inicialização, esses erros propagam acima de EnsureInitializedAsync, fazendo com que InitializeAsync os compostos do tipo de falhar com o erro mesmo.

O argumento final é que o serviço composto não é um tipo especial de tipo. É apenas um serviço que oferece suporte a inicialização assíncrona, como qualquer outro tipo de serviço. Qualquer um destes serviços pode ser ridicularizados por testes, se eles oferecem suporte a inicialização assíncrona ou não.

Conclusão

Os padrões no presente artigo podem aplicar a qualquer tipo de aplicação; Já os usei em ASP.NET e console, bem como aplicativos MVVM. Meu favorito padrão para construção assíncrona é o método assíncrono de fábrica; é muito simples e não pode ser abusado por consumir código porque ele nunca expõe uma instância não inicializada. No entanto, também achei o inicialização assíncrona padrão bastante útil quando se trabalha em cenários onde não pode (ou não) criar meus próprios casos. O AsyncLazy < T > padrão também tem seu lugar, quando há recursos compartilhados que requerem inicialização assíncrona.

Os padrões de serviço assíncrono são mais estabelecidos do que os padrões MVVM que apresentei no início desta série. O padrão para vinculação de dados assíncrono e nas várias abordagens para comandos assíncronos são ambos bastante novo, e eles certamente têm espaço para melhorias. Os padrões de serviço assíncrono, em contrapartida, têm sido utilizados mais amplamente. No entanto, as advertências usuais aplicam-se: Estes padrões não são Evangelho; Eles são apenas técnicas eu achei útil e queria compartilhar. Se você pode melhorá-los ou adequá-los às necessidades do seu aplicativo, por favor, vá em frente! Espero que estes artigos foram úteis para apresentá-lo para padrões de MVVM assíncrona e, ainda mais, que eles te encorajei a estendê-las e explorar seu próprios padrões de assíncrono para interfaces de usuário.

Stephen Cleary é marido, pai e programador que mora no norte de Michigan. Ele trabalha com multithreading e programação assíncrona há 16 anos e tem usado o suporte assíncrono no Microsoft .NET Framework desde o primeiro CTP. Seu site, incluindo seu blog, é stephencleary.com.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffrey e Stephen Toub