web-dev-qa-db-ja.com

非同期初期化を必要とするタイプのすべてのDIアンチパターンを回避する

非同期の初期化が必要なタイプConnectionsがあります。このタイプのインスタンスは、他のいくつかのタイプ(たとえば、Storage)によって消費されます。各タイプには、非同期初期化も必要です(インスタンスごとではなく静的であり、これらの初期化もConnectionsに依存します)。 。最後に、私のロジックタイプ(例:Logic)はこれらのストレージインスタンスを消費します。現在、SimpleInjectorを使用しています。

私はいくつかの異なる解決策を試しましたが、常にアンチパターンが存在します。


明示的な初期化(時間的結合)

私が現在使用しているソリューションには、TemporalCouplingアンチパターンがあります。

public sealed class Connections
{
  Task InitializeAsync();
}

public sealed class Storage : IStorage
{
  public Storage(Connections connections);
  public static Task InitializeAsync(Connections connections);
}

public sealed class Logic
{
  public Logic(IStorage storage);
}

public static class GlobalConfig
{
  public static async Task EnsureInitialized()
  {
    var connections = Container.GetInstance<Connections>();
    await connections.InitializeAsync();
    await Storage.InitializeAsync(connections);
  }
}

Temporal Couplingをメソッドにカプセル化したので、それほど悪くはありません。しかし、それでも、それはアンチパターンであり、私が望むほど保守可能ではありません。


抽象ファクトリ(Sync-Over-Async)

一般的に提案されているソリューションは、AbstractFactoryパターンです。ただし、この場合は非同期初期化を扱っています。したがって、私couldは、初期化を強制的に同期的に実行することでAbstract Factoryを使用できますが、これはsync-over-asyncアンチパターンを採用します。私は複数のストレージを持っていて、現在のコードではそれらがすべて同時に初期化されるため、sync-over-asyncアプローチが本当に嫌いです。これはクラウドアプリケーションであるため、これをシリアル同期に変更すると起動時間が長くなり、リソースを消費するため、パラレル同期も理想的ではありません。


非同期抽象ファクトリ(不適切な抽象ファクトリの使用法)

非同期ファクトリメソッドでAbstractFactoryを使用することもできます。ただし、このアプローチには1つの大きな問題があります。 Mark Seemanがコメントしているように ここ 、「正しく登録すれば、その塩に値するDIコンテナはすべて[ファクトリ]インスタンスを自動配線できます。」残念ながら、これは非同期ファクトリには完全に当​​てはまりません。これをサポートするnoDIコンテナがあります。

したがって、Abstract Asynchronous Factoryソリューションでは、少なくともFunc<Task<T>>で明示的なファクトリを使用する必要があり、 これはどこにでもあることになります ( "私たちは個人的に、Funcデリゲートを登録できるようにすることでデフォルトはデザインの匂いです... Funcに依存するコンストラクターがシステムに多数ある場合は、依存関係の戦略をよく見てください。 "):

public sealed class Connections
{
  private Connections();
  public static Task<Connections> CreateAsync();
}

public sealed class Storage : IStorage
{
  // Use static Lazy internally for my own static initialization
  public static Task<Storage> CreateAsync(Func<Task<Connections>> connections);
}

public sealed class Logic
{
  public Logic(Func<Task<IStorage>> storage);
}

これはそれ自身のいくつかの問題を引き起こします:

  1. 私のすべてのファクトリ登録は、依存関係をコンテナから明示的に引き出し、それらをCreateAsyncに渡す必要があります。つまり、DIコンテナは、依存性注入を実行しなくなりました。
  2. これらのファクトリ呼び出しの結果には、DIコンテナによって管理されなくなった有効期間があります。現在、各工場はDIコンテナではなくライフタイム管理を担当しています。 (同期抽象ファクトリでは、ファクトリが適切に登録されていれば、これは問題になりません)。
  3. これらの依存関係を実際に使用するメソッドはすべて非同期である必要があります。ロジックメソッドでさえ、ストレージ/接続の初期化が完了するのを待つ必要があるためです。とにかく私のストレージメソッドはすべて非同期であるため、これはこのアプリでは大したことではありませんが、一般的なケースでは問題になる可能性があります。

自己初期化(時間的結合)

もう1つの、あまり一般的ではない解決策は、型の各メンバーに独自の初期化を待機させることです。

public sealed class Connections
{
  private Task InitializeAsync(); // Use Lazy internally

  // Used to be a property BobConnection
  public X GetBobConnectionAsync()
  {
    await InitializeAsync();
    return BobConnection;
  }
}

public sealed class Storage : IStorage
{
  public Storage(Connections connections);
  private static Task InitializeAsync(Connections connections); // Use Lazy internally
  public async Task<Y> IStorage.GetAsync()
  {
    await InitializeAsync(_connections);
    var connection = await _connections.GetBobConnectionAsync();
    return await connection.GetYAsync();
  }
}

public sealed class Logic
{
  public Logic(IStorage storage);
  public async Task<Y> GetAsync()
  {
    return await _storage.GetAsync();
  }
}

ここでの問題は、Temporal Couplingに戻ったことです。今回は、システム全体に分散しています。また、このアプローチでは、allパブリックメンバーが非同期メソッドである必要があります。


したがって、ここで対立する2つのDI設計の観点が実際にあります。

  • 消費者は、すぐに使用できるインスタンスを注入できることを望んでいます。
  • DIコンテナー 単純なコンストラクター を強くプッシュします。

問題は、特に非同期初期化の場合、DIコンテナーが「単純なコンストラクター」アプローチに固執する場合、ユーザーに独自の初期化を他の場所で実行させ、独自のアンチパターンをもたらすことです。例: Simple Injectorが非同期関数を考慮しない理由 : "いいえ、このような機能は、依存性に関していくつかの重要な基本ルールに違反しているため、SimpleInjectorやその他のDIコンテナーには意味がありません。注入。"ただし、厳密に「基本ルールに従って」プレイすると、明らかに、はるかに悪いと思われる他のアンチパターンが強制されます。

質問:すべてのアンチパターンを回避する非同期初期化のソリューションはありますか?


更新:AzureConnectionsの完全な署名(上記ではConnectionsと呼ばれます):

public sealed class AzureConnections
{
  public AzureConnections();

  public CloudStorageAccount CloudStorageAccount { get; }
  public CloudBlobClient CloudBlobClient { get; }
  public CloudTableClient CloudTableClient { get; }

  public async Task InitializeAsync();
}
32
Stephen Cleary

あなたが抱えている問題、そして あなたが構築しているアプリケーション は、典型的ではありません。これは、次の2つの理由で非典型的です。

  1. 非同期の起動初期化が必要(または必要)、および
  2. アプリケーションフレームワーク(Azure関数)は、非同期の起動初期化をサポートしています(つまり、それを取り巻くフレームワークはほとんどないようです)。これにより、状況が通常のシナリオとは少し異なり、一般的なパターンについて説明するのが少し難しくなる可能性があります。

ただし、あなたの場合でも、解決策はかなりシンプルでエレガントです。

それを保持するクラスから初期化を抽出し、それを 構成ルート に移動します。その時点で、それらのクラスを作成して初期化しbeforeコンテナに登録し、それらの初期化されたクラスを登録の一部としてコンテナにフィードできます。

これは、特定の場合にうまく機能します。これは、(1回限りの)起動初期化を実行するためです。起動時の初期化は通常、コンテナーを構成する前に(または、完全に構成されたオブジェクトグラフが必要な場合は後で)実行されます。私が見たほとんどの場合、あなたの場合に効果的に行うことができるように、初期化は以前に行うことができます。

私が言ったように、あなたのケースは、標準と比較して、少し独特です。規範は次のとおりです。

  • 起動時の初期化は同期的です。フレームワーク(ASP.NET Coreなど)は通常、起動フェーズでの非同期初期化をサポートしていません
  • 多くの場合、初期化は、アプリケーションごとに事前に行うのではなく、要求ごとにジャストインタイムで行う必要があります。多くの場合、初期化が必要なコンポーネントの有効期間は短いため、通常、最初の使用時にそのようなインスタンスを初期化します(つまり、ジャストインタイム)。

通常、起動時の初期化を非同期で行うことの実際の利点はありません。起動時には、とにかく実行されるスレッドは1つだけであるため、実際のパフォーマンス上の利点はありません(これを並列化する場合もありますが、明らかに非同期は必要ありません)。また、一部のアプリケーションタイプは、synch-over-asyncの実行でデッドロックする可能性がありますが、コンポジションルートでは、使用しているアプリケーションタイプと、これが正確に問題があるかどうか。コンポジションルートは常にアプリケーション固有です。つまり、デッドロックのないアプリケーション(ASP.NET Core、Azure Functionsなど)のコンポジションルートで初期化を行う場合、通常、起動時の初期化を非同期で行うメリットはありません。

コンポジションルートでは、sync-over-asyncが問題であるかどうかがわかっているため、最初の使用時に同期的に初期化を行うこともできます。初期化の量は(要求ごとの初期化と比較して)有限であるため、必要に応じて、同期ブロッキングを使用してバックグラウンドスレッドで初期化を実行しても実際のパフォーマンスへの影響はありません。私たちがしなければならないのは、最初の使用時に初期化が行われることを保証するプロキシクラスをコンポジションルートに定義することです。これは、マーク・シーマンが答えとして提案したアイデアとほぼ同じです。

私はAzureFunctionsにまったく精通していなかったので、これは実際に非同期初期化を実際にサポートしていることを知っている最初のアプリケーションタイプ(もちろんコンソールアプリを除く)です。ほとんどのフレームワークタイプでは、ユーザーがこの起動初期化を非同期で行う方法はまったくありません。たとえば、ASP.NETアプリケーションまたはASP.NETCoreアプリケーションのStartupクラスの_Application_Start_イベント内にいる場合、非同期はありません。すべてが同期している必要があります。

その上、アプリケーションフレームワークでは、フレームワークのルートコンポーネントを非同期で構築することはできません。したがって、DIコンテナが非同期解決を行うという概念をサポートする場合でも、アプリケーションフレームワークのサポートが「不足」しているため、これは機能しません。 ASP.NETCoreのIControllerActivatorを例にとってみましょう。そのCreate(ControllerContext)メソッドを使用すると、Controllerインスタンスを作成できますが、Createメソッドの戻り値の型はobjectであり、_Task<object>_ではありません。言い換えると、DIコンテナがResolveAsyncメソッドを提供する場合でも、ResolveAsync呼び出しは同期フレームワークの抽象化の背後でラップされるため、ブロッキングが発生します。

ほとんどの場合、初期化はインスタンスごとまたは実行時に行われることがわかります。たとえば、SqlConnectionは通常、リクエストごとに開かれるため、各リクエストは独自の接続を開く必要があります。 「ジャストインタイム」で接続を開きたい場合、必然的にアプリケーションインターフェイスが非同期になります。ただし、ここでは注意してください。

同期の実装を作成する場合、決して別の実装が存在しないことが確実な場合にのみ、その抽象化を同期にする必要があります(または、非同期のプロキシ、デコレータ、インターセプタなど)。抽象化を無効に同期化した場合(つまり、_Task<T>_を公開しないメソッドとプロパティがある場合)、Leaky Abstractionが手元にある可能性があります。これにより、後で非同期実装を取得するときに、アプリケーション全体で抜本的な変更を行う必要が生じる可能性があります。

言い換えると、asyncの導入により、アプリケーションの抽象化の設計にさらに注意を払う必要があります。これはあなたの場合にも当てはまります。今は起動時の初期化だけが必要かもしれませんが、定義した抽象化(およびAzureConnectionsも)では、ジャストインタイムの非同期初期化が必要になることはありませんか? AzureConnectionsの同期動作が実装の詳細である場合は、すぐに非同期にする必要があります。

これの別の例はあなたの INugetRepository です。そのメンバーは同期的ですが、それが同期的である理由はその実装が同期的であるため、それは明らかにリーク抽象化です。ただし、同期APIのみを持つレガシーNuGet NuGetパッケージを使用するため、その実装は同期的です。 INugetRepositoryの実装は同期的ですが、完全に非同期である必要があることは明らかです。

非同期を適用するアプリケーションでは、ほとんどのアプリケーション抽象化にはほとんど非同期メンバーが含まれます。この場合、この種のジャストインタイム初期化ロジックも非同期にするのは簡単です。すべてがすでに非同期です。

要約する:

  • 起動時の初期化が必要な場合:コンテナーを構成する前または後に実行します。これにより、オブジェクトグラフの作成自体が高速で信頼性が高く、検証可能になります。
  • コンテナーを構成する前に初期化を行うと、 Temporal Coupling が防止されますが、初期化を必要とするクラスから初期化を移動する必要がある場合があります(これは実際には良いことです)。
  • ほとんどのアプリケーションタイプでは、非同期起動の初期化は不可能です。他のアプリケーションタイプでは、通常は不要です。
  • リクエストごとまたはジャストインタイムの初期化が必要な場合、非同期インターフェイスを使用する方法はありません。
  • 非同期アプリケーションを構築している場合は、同期インターフェースに注意してください。実装の詳細が漏洩している可能性があります。
16
Steven

以下はあなたが探しているものではないと私はかなり確信していますが、なぜそれがあなたの質問に対処しないのか説明できますか?

public sealed class AzureConnections
{
    private readonly Task<CloudStorageAccount> storage;

    public AzureConnections()
    {
        this.storage = Task.Factory.StartNew(InitializeStorageAccount);
        // Repeat for other cloud 
    }

    private static CloudStorageAccount InitializeStorageAccount()
    {
        // Do any required initialization here...
        return new CloudStorageAccount( /* Constructor arguments... */ );
    }

    public CloudStorageAccount CloudStorageAccount
    {
        get { return this.storage.Result; }
    }
}

デザインを明確にするために、クラウドプロパティの1つだけを実装しましたが、他の2つも同様の方法で実装できました。

AzureConnectionsコンストラクターは、さまざまなクラウドオブジェクトの初期化にかなりの時間がかかる場合でも、ブロックしません。

一方、作業は開始され、.NETタスクはpromiseのように動作するため、最初に値にアクセスしようとすると(Resultを使用)、InitializeStorageAccount

これはあなたが望んでいることではないという強い印象を受けますが、あなたが解決しようとしている問題がわからないので、少なくとも議論することがあるので、この答えを残したいと思いました。

5
Mark Seemann

プロキシシングルトンクラスで私がしていることをあなたがやろうとしているようです。

                services.AddSingleton<IWebProxy>((sp) => 
                {
                    //Notice the GetService outside the Task.  It was locking when it was inside
                    var data = sp.GetService<IData>();

                    return Task.Run(async () =>
                    {
                        try
                        {
                            var credentials = await data.GetProxyCredentialsAsync();
                            if (credentials != null)
                            {
                                return new WebHookProxy(credentials);
                            }
                            else
                            {
                                return (IWebProxy)null;
                            }
                        }
                        catch(Exception ex)
                        {
                            throw;
                        }
                    }).Result;  //Back to sync
                });
0
T Brown