本文章是由機器翻譯。

非同步程式設計

非同步 MVVM 應用程式的模式:服務

Stephen Cleary

這是三個系列的文章中結合非同步,等待與既定的模型-視圖-ViewModel (MVVM) 模式。 在第一篇文章,我開發了一個資料繫結到一個非同步作業的方法。 在第二個,我考慮了幾個可能的實現的非同步 ICommand。 現在,我會把對服務層和非同步服務的位址。

我不會在所有處理一個使用者介面。 事實上,這篇文章中的模式不是特定于使用 MVVM ; 他們同樣適用于任何類型的應用程式。 非同步資料繫結和命令模式探討了在我以前的文章都很新 ; 這篇文章中的非同步服務模式更多被建立。 甚至既定的模式還是只是模式。

非同步介面

"到介面,不執行計畫"。從這句話作為"設計模式:可複用的物件導向軟體的元素"(艾迪生-衛斯理,1994 年,p。 18) 表明,介面是適當的物件導向設計的一個關鍵組成部分。 它們允許您的代碼以使用一個抽象的概念,而不是一個具體的類型,和他們給您的代碼"交界點",你可以拼接成的單元測試。 但是它可能與非同步方法創建一個介面嗎?

答案仍是適用的。 下面的代碼與非同步方法定義一個介面:

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

服務實現非常簡單:

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

圖 1 顯示的代碼,使用的服務是如何調用的介面上定義的非同步方法。

圖 1 UseMyService.cs:在介面上定義的非同步方法調用

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

這可能看起來像一個過於簡單化的例子,但它闡釋了一些重要的經驗教訓,關於非同步方法。

第一課是:方法不是 awaitable,類型是。 它是運算式的確定是否該運算式是運算式的 awaitable 的類型。 尤其是,UseMyService.IsLargePageAsync 等待著 IMyService.DownloadAndCountBytesAsync 的結果。 介面方法不是 (和不能) 標記的非同步。 IsLargePageAsync 可以使用等待因為介面方法返回一個任務,任務是 awaitable。

第二個教訓是:非同步是實現詳細資訊。 UseMyService 既不知道也不會在乎使用非同步或不來實現介面方法。 使用代碼只在乎該方法返回一個任務。 使用非同步和等待是一種共同的方法來實現任務返回的方法,但它不是唯一的方法。 例如,在代碼圖 2 超載非同步方法使用一個共同的模式。

圖 2 AsyncOverloadExample.cs:對重載非同步方法使用一種常見模式

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

請注意這一個重載只是調用其他的和直接返回它的任務。 它是可能寫入該重載使用非同步和等待,但這只會增加開銷,提供沒有任何好處。

非同步單元測試

有執行任務返回方法的其他選項。 Task.FromResult 是單元測試存根,共同選擇,因為它是最簡單的方法來創建一個已完成的任務。 下面的代碼定義了服務的存根 (stub) 實施:

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

您可以使用此存根 (stub) 實現測試 UseMyService,如中所示圖 3

圖 3 UseMyServiceUnitTests.cs:存根 (stub) 執行的測試 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);
  }
}

此代碼示例使用 MSTest,但大多數其他現代單元測試框架還支援非同步單元測試。 只需確保您的單元測試返回的任務 ; 避免非同步空的單元測試方法。 大多數單元測試框架不支援非同步空的單元測試方法。

當單元測試的同步方法,它是重要的是要測試代碼的行為方式都在成功和失敗的條件。 非同步方法添加皺:它是可能的一個非同步服務成功或引發異常,同步或非同步。 如果你想要卻通常就足夠了,測試至少非同步成功和非同步失敗,再加上同步成功,如有必要,您可以測試這些組合的所有四個。 同步成功測試非常有用,因為等待操作員將以不同的方式行事,如果其操作已完成。 但是,我找不到同步失敗測試作為有用的因為失敗並不是立即與最非同步作業。

寫這篇文章,一些受歡迎的嘲弄和鉗框架將返回 default (t) 除非你修改此行為。 預設的嘲弄行為不起作用以及與非同步方法因為非同步方法應從不返回空任務 (根據你會發現在基於任務的非同步模式 bit.ly/1ifhkK2)。 正確的預設行為是返回 Task.FromResult(default(T))。 這是一個常見的問題當單元測試非同步代碼 ; 如果你看到意外的 NullReferenceExceptions 您在測試中,確保類比類型正在執行的所有任務返回方法。 我希望嘲弄和鉗框架將在未來成為更加意識到非同步和實現更好地為非同步方法的預設行為。

非同步工廠

模式到目前為止已經說明了如何定義的介面,用一種非同步方法 ; 如何實現它的一項服務 ; 以及如何定義用於測試目的的存根 (stub)。 這些都是足夠最非同步服務,但有整另一個級別的當一個服務實現,必須做一些非同步工作,才可以使用它時,應用的複雜性。 讓我描述如何處理這種情況在您需要非同步建構函式。

建構函式不能是非同步,但靜態方法可以。 一種方法假裝的感覺非同步建構函式是實現非同步工廠方法,如中所示圖 4

圖 4 服務與非同步工廠方法

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

我很喜歡非同步出廠的辦法,因為它不會被誤用。 調用代碼不能直接 ; 調用的建構函式 它必須使用工廠方法來獲取的實例,並在返回之前完全初始化該實例。 但是,這不能在某些情況下使用。 寫這篇文章,反演控制 (IoC) 和依賴注入 (DI) 框架不明白任何非同步工廠方法的約定。 如果你注射您使用 IoC/DI 容器的服務,您需要另一種方法。

非同步資源

在某些情況下,非同步初始化需要只有一次,要初始化的共用的資源。 StephenToub 開發非同步­懶人 < T > 類型 (bit.ly/1cVC3nb),也是可用的我的 AsyncEx 圖書館一部分 (bit.ly/1iZBHOW)。 AsyncLazy < T > 結合了懶人 < T > < T > 的任務。 具體地說,它是 < < T >> 的任務,懶懶的類型,支援非同步工廠方法。 懶的 < T > 層提供了執行緒安全的遲緩初始化,確保工廠方法只執行一次 ; < T > 任務 層提供非同步支援,允許調用方以非同步等待了工廠方法來完成。

圖 5 介紹了 AsyncLazy < T > 略為簡化的定義。 圖 6 顯示如何 AsyncLazy < T > 可以使用在一種類型內。

圖 5 定義的 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();
  }
}

圖 6 AsyncLazy < T > 在類型中使用

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

這項服務定義單個共用"資源",必須以非同步方式構造。 這項服務的任何實例的任何方法可以取決於該資源並直接等待著它。 第一次 AsyncLazy < T > 等待實例時,它將執行緒池執行緒上一次啟動非同步工廠方法。 任何其他同時訪問同一實例從另一個執行緒將等待,直到非同步工廠方法已經排隊到執行緒池。

AsyncLazy < T > 的同步、 執行緒安全的一部分 上一篇: 行為­ior 由懶人 < T > 圖層。 花時間阻塞是非常短的:每個執行緒只能等待了工廠方法來排隊到執行緒池 ; 他們不要等待它執行。 一次任務 < T > 返回從工廠方法,然後懶 < T > 圖層的工作就是結束。 同樣的任務 < T > 每個等待共用的實例。 既不是非同步工廠方法,也不是非同步惰性初始化將過公開 T 的實例,直到其非同步初始化已完成。 這樣可以防止意外誤操作的類型。

AsyncLazy < T > 是偉大的一種特殊:非同步初始化的共用資源。 然而,它可以是尷尬,在其他方案中使用。 尤其是,如果一個服務實例需要非同步建構函式,您可以定義一個不會非同步初始化的"內部"的服務類型和使用 AsyncLazy < T > 包裝內"外"服務類型的內部實例。 但是,導致繁瑣而冗長的代碼,與根據相同的內部實例的所有方法。 在這種情況下,一個真正的"非同步建構函式"會更優雅。

一個失足

我首選的解決方案之前,我想指出一個有些共同的失足。 當開發人員都面臨著非同步工作要做在一個建構函式 (不能是非同步),此替代方法可能中的代碼類似圖 7

圖 7 變通辦法面對的建構函式中做非同步工作時

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

但也有一些嚴重的問題,這種方法。 第一,沒有辦法告訴當初始化已完成 ; 第二,通常非同步無效方式,通常崩潰應用程式將處理從初始化任何異常。 如果 InitializeAsync 是而不是非同步不正確非同步任務,將幾乎沒有改善情況:仍然沒有辦法告訴當初始化完成,以及任何異常將被忽略。 有更好的方法 !

非同步初始化模式

大多數基於反射的創建代碼 (IoC/DI 框架、 Activator.CreateInstance 等等) 假定您的類型有一個建構函式,建構函式不能是非同步。 如果你在這種情況,讓你不得不返回沒有 (非同步) 初始化一個實例。 非同步初始化模式的目的是提供一種標準方式處理這種情況,減輕問題的未初始化的實例。

第一,我定義一個"標記"介面。 如果一種類型需要非同步初始化,它實現了此介面:

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

第一眼看著類型任務的屬性感覺很奇怪。 我相信它是適當的不過,因為 (初始化實例) 的非同步作業是實例級操作。 所以初始化屬性屬於該實例作為一個整體。

當我實現此介面時,我更願意這樣做與實際非同步方法,我的名字 InitializeAsync 由公約 》,作為圖 8 顯示:

圖 8 服務實現 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; }
}

該建構函式是相當簡單 ; 它啟動的非同步初始化 (通過調用 InitializeAsync),然後設置初始化屬性。 該初始化屬性提供 InitializeAsync 方法的結果:InitializeAsync 當完成時,初始化任務完成,並且如果有任何錯誤,通過初始化任務,將浮出水面。

當建構函式完成後時,在初始化可能還不會完成,所以使用代碼一定要很小心。 使用該服務的代碼有責任確保在調用任何其他方法之前初始化已完成。 下面的代碼創建並初始化一個服務實例:

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

在更現實的政府間海洋學委員會/DI 方案中,使用代碼只獲取執行 IUniversalAnswerService,一個實例,並已測試是否它實現了 IAsyncInitialization。 這是一個非常有用的技術 ; 它允許非同步初始化該類型的實現細節。 例如,存根 (stub) 類型可能不會使用非同步初始化 (除非你在實際測試使用代碼將等待要進行初始化的服務)。 下面的代碼是我的答案服務更切合實際使用:

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

在繼續之前非同步初始化模式,我要指出一個重要選擇。 它是可以將服務成員公開為內部等待著他們自己的物件的初始化的非同步方法。 圖 9 顯示此類型的物件會是什麼樣子。

圖 9 服務,等待著自己的初始化操作

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

我喜歡這種方法,因為它不可能濫用沒尚未初始化的物件。 然而,它限制的 API 服務,因為任何取決於初始化的成員必須公開為非同步方法。 在前面的示例中,GetAnswerAsync 方法取代答案屬性。

構成非同步初始化模式

讓我們假設我要定義一種服務,取決於幾個其他服務。 當我介紹非同步初始化模式為我的服務時,這些服務中的任何可能需要非同步初始化。 檢查是否這些服務實現 IAsyncInitialization 的代碼可以獲取有些繁瑣,但我可以輕鬆地定義一個説明器類型:

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

説明器方法以任意數量的任何類型的實例,篩選出不執行 IAsyncInitialization,然後非同步等待所有的初始化任務,要完成的任何。

與這些説明器方法在的地方,創建複合服務是簡單的。 在服務圖 10 採用答案服務的兩個實例作為依賴項,並計算其結果的平均值。

圖 10 服務的平均數作為依賴項的答案服務的結果

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

有幾個重要的優點,在撰寫服務時要牢記。 第一,因為非同步初始化是實現細節,組成的服務不能知道是否其依賴項的任何需要非同步初始化。 如果沒有任何依賴關係需要非同步初始化,然後既不會複合服務。 但是,因為它無法知道的複合服務必須聲明本身作為需要非同步初始化。

Don不太擔心這 ; 性能影響 會有一些額外的記憶體分配對於非同步結構,但該執行緒的行為將不以非同步方式。 等待了一進來,每當代碼等待任務已經完成的"快速通道"優化。 如果依賴關係的複合服務不需要非同步初始化,傳遞給 Task.WhenAll 的序列為空,從而導致 Task.WhenAll 返回一個已完成的任務。 當這項任務正在等待 CompoundService.InitializeAsync 時,它不會產生執行,因為任務已完成。 在此方案中,InitializeAsync 同步,完成建構函式完成之前。

第二個外賣是重要的是要複合 InitializeAsync 返回之前初始化所有依賴項。 這可確保該複合類型的初始化是完全完成。 另外,錯誤處理是自然 — — 如果依賴的服務已初始化錯誤,這些錯誤會傳播了從 EnsureInitializedAsync,造成的複合類型 InitializeAsync,同樣的錯誤而失敗。

最後的外賣是類型的複合服務不是類型的一種特殊。 它是服務的只是服務的一種服務,支援非同步初始化,就像任何其他類型。 這些服務中的任何可以進行測試,是否支援或不支援非同步初始化嘲弄。

總結

這篇文章中的模式可以適用于任何類型的應用程式 ; 我用過ASP.NET和主控台,以及使用 MVVM 應用程式中。 我自己最喜歡非同步建設模式是非同步工廠方法 ; 它非常簡單,不能被濫用,消費的代碼,因為它從來沒有公開未初始化的實例。 然而,我也發現了非同步初始化模式很有用在哪裡我不能 (或不想) 的方案中工作時創建我自己的實例。 AsyncLazy < T > 模式也有它的地方,當有需要非同步初始化的共用的資源。

非同步服務模式就是比我早些時候在這個系列介紹的使用 MVVM 模式更成熟。 為非同步資料繫結模式和非同步命令的各種方法都是相當新的,他們肯定有改進的餘地。 非同步服務模式,相比之下,已使用更廣泛。 然而,通常一些注意事項:這些模式並不是福音 ; 他們只是技術,我已經發現有用,想要分享。 如果您可以改善對他們或他們根據您應用程式的需要來定制,請去做吧 ! 我希望這些文章有説明把你介紹給非同步使用 MVVM 模式,和甚至更多,他們都鼓勵你它們進行擴展和教科文組織統計研究所為探索自己的非同步模式。

Stephen Cleary 是一個丈夫、 父親和程式師生活在北密歇根。他曾與多執行緒和非同步程式設計 16 年並已在 Microsoft.NET 框架以來,第一次的 CTP 使用非同步支援。他的主頁,包括他的博客,是在 stephencleary.com

感謝以下 Microsoft 技術專家對本文的審閱:James麥卡弗裡和StephenToub