Compartir a través de


Este artículo proviene de un motor de traducción automática.

Programación asincrónica

Modelos para aplicaciones de MVVM asincrónicas: Servicios

Stephen Cleary

Este es el tercer artículo de una serie en la combinación de async y esperan con las mismas pautas de modelo-View-ViewModel (MVVM). En el primer artículo, he desarrollado un método para el enlace de datos para una operación asincrónica. En el segundo, consideré unas posibles implementaciones de un ICommand asincrónica. Ahora, podrá recurrir la capa de servicios y servicios asincrónicos de dirección.

No voy a tratar con una interfaz de usuario en absoluto. De hecho, los patrones en este artículo son específicos para MVVM; pertenecen igualmente bien a cualquier tipo de aplicación. El enlace de datos asincrónica y patrones de comando explorados en mis artículos anteriores son bastante nuevos; los patrones de servicio asincrónico en el presente artículo se establecen más. Todavía, incluso establecidos patrones son solo patrones.

Interfaces asíncronas

"Programa para una interfaz, no una implementación". Como esta cita de "patrones de diseño: Elementos de Software orientado a objetos reutilizables"(Addison-Wesley, 1994, p. 18) sugiere, las interfaces son un componente crítico de diseño orientado a objetos. Permiten su código para usar una abstracción en lugar de un tipo concreto, y dan su código puede empalmar en un "punto de Unión" para pruebas unitarias. Pero, ¿es posible crear una interfaz con métodos asincrónicos?

La respuesta es sí. El código siguiente define una interfaz con un método asincrónico:

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

La implementación del servicio es sencilla:

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 muestra cómo el código que consume el servicio llama al método asincrónico definido en la interfaz.

Figura 1 UseMyService.cs: Al llamar al método Async definido en la interfaz

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

Esto puede parecer un ejemplo demasiado simplista, pero ilustra algunas lecciones importantes sobre métodos asincrónicos.

La primera lección es: Métodos no son awaitable, tipos. Es el tipo de expresión que determina si esa expresión es awaitable. En particular, UseMyService.IsLargePageAsync espera el resultado de IMyService.DownloadAndCountBytesAsync. El método de interfaz no es (y no puede ser) async marcada. IsLargePageAsync puede utilizar esperan porque el método de interfaz devuelve una tarea, y las tareas son awaitable.

La segunda lección es: Async es un detalle de la implementación. UseMyService no sabe ni le importa si los métodos de interfaz se implementan mediante async o no. El código de consumo se preocupa solamente que el método devuelve una tarea. Usando async y esperar es una forma común de implementar un método tarea de retorno, pero no es la única manera. Por ejemplo, el código en figura 2 utiliza un patrón común para sobrecargar métodos asincrónicos.

Figura 2 AsyncOverloadExample.cs: Usando un patrón común para sobrecargar métodos asincrónicos

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

Tenga en cuenta que una sobrecarga simplemente llama al otro y devuelve su tarea directamente. Es posible escribir esa sobrecarga usando async y esperan, pero eso sólo agregar encima de la cabeza y no proporcionar ningún beneficio.

Pruebas unitarias asincrónica

Hay otras opciones para la aplicación de métodos devuelven tarea. Task.FromResult es una opción común para recibos de prueba de unidad, porque es la forma más fácil de crear una tarea completada. El código siguiente define una implementación de código auxiliar del servicio:

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

Puede utilizar esta aplicación de código auxiliar para probar UseMyService, como se muestra en la figura 3.

Figura 3 UseMyServiceUnitTests.cs: Implementación del trozo a prueba 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 código de ejemplo utiliza MSTest, pero más otros frameworks de prueba de unidad moderna también apoyan las pruebas unitarias asincrónica. Asegúrate de que tu unidad de pruebas volver tarea; Evite async métodos de prueba de unidad vacía. Mayoría de los entornos de prueba de unidad no soportan los métodos de prueba de unidad vacía async.

Cuando métodos sincrónicos pruebas de unidad, es importante comprobar cómo el código comporta tanto en condiciones de éxito y el fracaso. Métodos asincrónicos añadir una arruga: Es posible que un servicio asincrónico tener éxito o iniciar una excepción, de forma sincrónica o asincrónica. Puede probar los cuatro de estas combinaciones si quieres, pero me parece que es generalmente suficiente probar asincrónica al menos éxito y fracaso asincrónica, además de éxito síncrona si es necesario. La prueba de éxito síncrono es útil porque el operador aguarda actuará diferentemente si ya ha completado su operación. Sin embargo, no encuentro el test falla síncrona como útil, porque el fallo no es inmediata con operaciones asincrónicas más.

A partir de esta escritura, algunas burlas populares y tropezar Marcos regresará default(T) a menos que usted modificar ese comportamiento. Burlando el comportamiento por defecto no funciona bien con métodos asincrónicos porque métodos asincrónicos nunca deben devolver una tarea nula (según el modelo asincrónico basado en tareas, que encontrará en bit.ly/1ifhkK2). El comportamiento predeterminado correcto sería regresar Task.FromResult(default(T)). Este es un problema común cuando unidad de pruebas de código asíncrono; Si estás viendo NullReferenceExceptions inesperados en sus pruebas, aseguran que los simulacros tipos implementan todos los métodos devuelven tarea. Espero que burlándose de tropezar Marcos consciente más async-en el futuro y aplicar mejor comportamiento predeterminado para métodos asincrónicos.

Fábricas asincrónicas

Los patrones hasta la fecha han demostrado cómo definir una interfaz con un método asincrónico; cómo implementarlo en un servicio; ¿y cómo definir un trozo para propósitos de prueba. Estos son suficientes para los servicios más asincrónicos, pero hay otro nivel de complejidad que se aplica cuando una implementación de servicio debe hacer algún trabajo asincrónico antes de que está listo para usarse. Permítanme describir cómo manejar la situación donde se necesita un constructor asincrónico.

Los constructores no pueden ser async, pero sí métodos estáticos. Una manera de fingir un constructor asincrónico es implementar un método de fábrica asincrónica, como se muestra en figura 4.

Figura 4 servicio con un método de fábrica asincrónica

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

Me gusta mucho el enfoque de fábrica asincrónica ya que no puede ser utilizadas indebidamente. El código de llamada no puede invocar el constructor directamente; debe utilizar el método de fábrica para obtener una instancia, y la instancia esté totalmente inicializada antes de que se la devuelvan. Sin embargo, esto no puede utilizarse en algunos escenarios. A partir de esta escritura, inversión del control (IoC) y marcos de inyección (DI) dependencia no entiende cualquier convenios para métodos de generador asíncrono. Si estás inyectando sus servicios usando un contenedor de IoC/DI, necesitará un enfoque alternativo.

Recursos asincrónicos

En algunos casos, inicialización asíncrono se necesita solamente una vez, para inicializar los recursos compartidos. Stephen Toub desarrolló un Async­Lazy < T > tipo (bit.ly/1cVC3nb), que también está disponible como parte de mi biblioteca AsyncEx (bit.ly/1iZBHOW). AsyncLazy < T > combina Lazy < T > tarea < T >. En concreto, es un vago < < T >> de tarea, un tipo vago que admite métodos asincrónicos. El perezoso < T > capa proporciona inicialización diferida multi-hilo, asegurando que el método de fábrica sólo se ejecuta una vez; la tarea < T > capa proporciona soporte asíncrono, permitiendo que los llamadores que asincrónicamente esperar a que el método de fábrica completar.

Figura 5 presenta una definición ligeramente simplificada de AsyncLazy < T >. Figura 6 muestra cómo AsyncLazy < T > puede ser utilizado dentro de un tipo.

Figura 5 definición 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 > Utilizado en un 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 servicio define un solo "recurso" compartido que debe construirse de forma asincrónica. Cualquier método de todas las instancias de este servicio puede depender de ese recurso y esperan directamente. La primera vez que la AsyncLazy < T > instancia es esperada, se iniciará el método asincrónico fábrica una vez sobre un hilo de piscina. Cualquier otro acceso simultáneo a esa misma instancia desde otro subproceso esperará hasta que el método asincrónico fábrica ha estado en cola en el grupo de subprocesos.

La parte sincrónica, hilo de seguridad de la AsyncLazy < T > Behav­ior es manejado por el Lazy < T > capa. Es muy corto el tiempo de bloqueo: cada subproceso espera únicamente por el método de fábrica estar en cola en el grupo de subprocesos; No esperan a ejecutar. Una vez la tarea < T > es devuelto por el método de fábrica, entonces el Lazy < T > trabajo de la capa está terminado. La misma tarea < T > instancia es compartida con todos los aguarda. Métodos de generador asíncrono ni asincrónica inicialización diferida jamás expondrá una instancia de T hasta que haya completado su inicialización asíncrono. Esto protege contra el uso indebido accidental del tipo.

AsyncLazy < T > es ideal para un determinado tipo de problema: inicialización asíncrona de un recurso compartido. Sin embargo, puede ser incómodo de usar en otros escenarios. En particular, si una instancia de servicio necesita un constructor asincrónico, puede definir un tipo de servicio "interna" que hace la inicialización asíncrona y usar AsyncLazy < T > para envolver la instancia interna dentro del tipo de servicio "externo". Pero eso lleva a código engorroso y tedioso, con todos los métodos dependiendo de la misma instancia interna. En tales casos, un verdadero "constructor asincrónica" sería más elegante.

Un paso en falso

Antes de entrar en mi solución preferida, quiero señalar un error relativamente común. Cuando los desarrolladores se enfrentan con asincrónica trabajo que hacer en un constructor (que no puede ser asincrónico), la solución puede ser algo así como el código de figura 7.

Figura 7 solución cuando se enfrentan con Async trabajo que hacer en un Constructor

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

Pero hay problemas serios con este enfoque. En primer lugar, es imposible saber cuándo se ha completado la inicialización; en segundo lugar, las excepciones de la inicialización se manejarán de manera nula generalmente async, comúnmente chocando la aplicación. Si InitializeAsync era tarea async en lugar de async vacío, apenas se mejorarían la situación: Aún no habría hay manera de saber cuando la inicialización completa y las excepciones que silenciosamente ignorarse. Hay una mejor manera.

El patrón de inicialización asíncrono

La mayoría código creación basada en la reflexión (IoC/DI Marcos, Activator.CreateInstance y así sucesivamente) asume el tipo tiene un constructor, y los constructores no pueden ser asincrónicos. Si estás en esta situación, te obligan a devolver una instancia que no ha sido inicializada (asincrónicamente). El propósito del patrón asincrónico inicialización es proporcionar una forma estándar de manejar esta situación, para mitigar el problema de los casos sin inicializar.

En primer lugar, definir una interfaz "marcador". Si un tipo necesita inicialización asíncrono, que implementa esta interfaz:

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

A primera vista, una propiedad de tipo tarea se siente rara. Creo que es apropiado, sin embargo, porque la operación asincrónica (inicializar la instancia) es una operación de la instancia. Así la propiedad de inicialización se refiere a la instancia como un todo.

Cuando implemento esta interfaz, prefiero hacerlo con un método async reales, que nombro a InitializeAsync por la Convención, como figura 8 muestra:

Figura 8 servicio implementa el 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; }
}

El constructor es bastante sencillo; comienza la inicialización asíncrona (llamando al InitializeAsync) y luego se establece la propiedad de inicialización. Esa propiedad de inicialización proporciona los resultados del método InitializeAsync: Cuando InitializeAsync completa, completa la tarea de inicialización, y si hay algún error, será ser surgidos a través de la tarea de inicialización.

Cuando se completa el constructor, la inicialización no todavía sería completa, tanto el código de consumo tiene que ser cuidadoso. El código utilizando el servicio tiene la responsabilidad de garantizar la inicialización completa antes de llamar a cualquier otro método. El siguiente código crea e inicializa una instancia de servicio:

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

En un escenario más realista de la COI/DI, el código de consumo sólo obtiene una instancia de aplicación de IUniversalAnswerService y tiene que probar si implementa IAsyncInitialization. Esta es una técnica útil; permite la inicialización ser un detalle de la implementación del tipo asíncrona. Por ejemplo, trozo tipos probablemente no utilizará inicialización asincrónico (a menos que realmente estás probando que el código de consumo esperará el servicio ser inicializado). El siguiente código es un uso más realista de mi servicio de respuesta:

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

Antes de continuar con el patrón asincrónico inicialización, debo señalar una alternativa importante. Es posible exponer a los miembros del servicio como métodos asincrónicos que esperan internamente la inicialización de sus propios objetos. Figura 9 muestra cómo quedaría este tipo de objeto.

Figura 9 servicio que espera su inicialización

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

Me gusta este enfoque porque no se puede abusar de un objeto que todavía no se ha inicializado. Sin embargo, limita la API del servicio, porque cualquier miembro que depende de inicialización debe ser expuesto como un método asincrónico. En el ejemplo anterior, la propiedad de respuesta fue reemplazada con un método GetAnswerAsync.

Componiendo el patrón asincrónico inicialización

Digamos que estoy definiendo un servicio que depende de muchos otros servicios. Cuando introduzco el patrón asincrónico inicialización por mis servicios, cualquiera de esos servicios pueden requerir inicialización asíncrono. El código para comprobar si los servicios de implementación de IAsyncInitialization puede ser algo tedioso, pero fácilmente puedo definir un tipo de ayuda:

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

Los métodos auxiliares tomar cualquier número de instancias de cualquier tipo, filtrar cualquier que no implementan IAsyncInitialization y luego esperar asincrónicamente para que todas las tareas de inicialización completar.

Con estos métodos auxiliares en el lugar, creando un servicio compuesto es sencilla. El servicio en figura 10 toma dos instancias del servicio de respuesta como dependencias, y un promedio de sus resultados.

Figura 10 servicio que un promedio de resultados del servicio de respuesta como dependencias

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

Hay algunos puntos importantes a tener en cuenta al redactar los servicios. En primer lugar, porque inicialización asíncrono es un detalle de la implementación, el servicio compuesto no puede saber si alguna de sus dependencias requieren inicialización asíncrono. Si ninguna de las dependencias requiere inicialización asíncrono, yo tampoco haría el servicio compuesto. Pero porque no puede saber, el servicio compuesto debe declararse que requiere inicialización asíncrono.

Donpreocupen demasiado sobre las implicaciones de rendimiento de esto; habrá algunas asignaciones de memoria adicional para las estructuras asincrónicas, pero el hilo no se comportará de forma asincrónica. Esperan tiene una optimización de la "vía rápida" que entran en juego cuando código le espera una tarea que ya está completa. Si las dependencias de un servicio compuesto no requieren inicialización asíncrono, pasada a Task.WhenAll la secuencia está vacía, causando Task.WhenAll devolver una tarea ya terminada. Cuando esa tarea es esperada por CompoundService.InitializeAsync, no ceder ejecución porque ya ha completado la tarea. En este escenario, InitializeAsync síncrono, completa antes de que finalice el constructor.

Un segundo punto es que es importante inicializar todas las dependencias antes de que regrese el compuesto InitializeAsync. Esto asegura la que inicialización del tipo compuesto es completo. Además, manejo de errores es natural — si un servicio dependiente tiene un error de inicialización, esos errores se propagan hasta de EnsureInitializedAsync, causando InitializeAsync del tipo compuesto que el mismo error.

El punto final es que el servicio compuesto no es un tipo especial de. Es un servicio que soporta inicialización asíncrono, al igual que cualquier otro tipo de servicio. Cualquiera de estos servicios puede ser burlado para las pruebas, si apoyan inicialización asíncrono o no.

Resumen

Los patrones en este artículo se pueden aplicar a cualquier tipo de aplicación; Los he usado en ASP.NET y consola, así como aplicaciones de MVVM. Mi propio patrón favorito para construcción asincrónico es el método asincrónico fábrica; es muy sencillo y no puede ser abusada por consumir código porque nunca expone una instancia sin inicializar. Sin embargo, también encontré el patrón asincrónico inicialización muy útil cuando se trabaja en escenarios donde yo no puede (o no) crear mis propias instancias. El AsyncLazy < T > patrón también tiene su lugar, cuando hay recursos compartidos que requieren inicialización asíncrono.

Los patrones de servicio asíncrono son más establecidos que los patrones MVVM que presenté anteriormente en esta serie. El modelo de enlace de datos asincrónica y los diferentes enfoques para comandos asincrónicos son bastante nuevo, y ciertamente tienen margen de mejora. Los patrones de servicio asíncrono, en cambio, se han utilizado más ampliamente. Sin embargo, se aplican las advertencias usuales: Estos patrones no son Evangelio; son técnicas sólo he encontrado útil y quería compartir. Si puedes mejorarlas o adaptarlos a las necesidades de su aplicación, por favor, hazlo! Espero que estos artículos han sido útiles para presentarles a patrones MVVM asincrónicos y, más aún, que alentaron a extenderlas y explorar sus propios patrones asincrónicas para UIs.

Stephen Cleary es esposo, padre, programador y vive en el norte de Michigan. Trabaja en la programación multithreading y asincrónica desde hace 16 años y ha usado las funciones asincrónicas de Microsoft .NET Framework desde la primera CTP. Su página principal y su blog se encuentran en stephencleary.com.

Gracias a los siguientes expertos técnicos de Microsoft por su ayuda en la revisión de este artículo: James McCaffrey y Stephen Toub