42 votes

Éviter tous les anti-modèles DI pour les types nécessitant une initialisation asynchrone

J'ai un type Connections qui nécessite une initialisation asynchrone. Une instance de ce type est consommé par plusieurs autres types (par exemple, Storage), chacun de qui exigent également initialisation asynchrone (statique, pas à par exemple, et ces initialisations dépendent également de l' Connections). Enfin, ma logique types (par exemple, Logic) consomme de stockage de ces instances. Actuellement à l'aide de Simples Injecteur.

J'ai essayé plusieurs solutions, mais il y a toujours un antipattern présent.


L'Initialisation Explicite (Temporelle D'Attelage)

La solution que j'utilise actuellement a le temps de Couplage antipattern:

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

J'ai encapsulé le temps de Couplage dans une méthode, il n'est donc pas aussi mauvaise qu'il pourrait être. Mais pourtant, c'est un antipattern et pas aussi facile à gérer comme je le voudrais.


Abstract Factory (Sync-Sur-Async)

Une commune de la solution proposée est un Résumé de l'Usine modèle. Toutefois, dans ce cas, nous avons affaire avec initialisation asynchrone. Donc, j'ai pu utiliser Abstrait Usine en forçant l'initialisation à exécuter de manière synchrone, mais ce n'adopte ensuite la synchronisation-sur-async antipattern. Je n'aime vraiment pas le sync-sur-async approche, car j'ai plusieurs stockages, et dans mon code actuel, ils sont tous initialisés simultanément; puisque c'est une application en nuage, la modification de cette série synchrone permettrait d'augmenter le temps de démarrage, et en parallèle synchrone est pas idéal en raison de la consommation de ressources.


Asynchrone Abstract Factory (Mauvaise Résumé De L'Usine D'Utilisation)

Je peux aussi utiliser Abstrait Usine avec asynchrone méthodes de fabrique. Cependant, il y a un gros problème avec cette approche. En tant que Marque de Seeman commentaires ici, "Tout Conteneur d'injection de dépendances qui vaut son sel sera capable d'auto-fils d'un [usine] exemple pour vous si vous vous inscrivez correctement." Malheureusement, ceci est complètement faux pour asynchrones usines: autant que je sache, il n'y a pas de conteneur d'injection de dépendances qui prend en charge ce.

Donc, le Résumé Asynchrone Usine solution m'obligerait à utiliser des usines, à tout le moins, Func<Task<T>>, et cela finit par être partout ("Nous pense personnellement que ce qui permet d'inscrire Func délégués par défaut est une conception de l'odeur... Si vous avez de nombreux constructeurs dans votre système qui dépendent d'un Func, veuillez jeter un oeil à votre dépendance à la stratégie."):

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

Cela entraîne plusieurs problèmes qui lui sont propres:

  1. Tous mes usine les inscriptions pour tirer des dépendances du conteneur explicitement et de les transmettre CreateAsync. De sorte que le conteneur d'injection de dépendances n'est plus à faire, vous le savez, l'injection de dépendance.
  2. Les résultats de ces usine appels ont une durée de vie qui ne sont plus gérés par le conteneur d'injection de dépendances. Chaque usine est maintenant responsable de la gestion de durée de vie au lieu de la DI conteneur. (Avec la machine synchrone Résumé de l'Usine, ce n'est pas un problème, si l'usine est enregistré de façon appropriée).
  3. Toute méthode fait l'utilisation de ces dépendances doivent être asynchrone puisque même la logique des méthodes, il faut attendre pour le stockage/connexions initialisation. Ce n'est pas une grosse affaire pour moi sur cette application depuis mes méthodes de stockage sont tous asynchrone de toute façon, mais il peut être un problème dans le cas général.

Auto-Initialisation (Temporelle D'Attelage)

Un autre, moins commun, la solution est de demander à chaque membre d'un type qui attendent son propre initialisation:

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

Le problème ici est que nous sommes de retour à un temps de Couplage, cette fois, réparties dans l'ensemble du système. Aussi, cette approche exige que tous les membres du public à des méthodes asynchrones.


Donc, il y a vraiment deux DI design de points de vue qui s'opposent ici:

  • Les consommateurs veulent être en mesure d'injecter des instances qui sont prêts à l'emploi.
  • DI conteneurs pousser dur pour les constructeurs simples.

Le problème est - en particulier avec initialisation asynchrone - que si DI conteneurs de prendre une ligne dure sur la "simple constructeurs" approche, puis ils sont juste de forcer les utilisateurs à faire leur propre initialisation d'ailleurs, ce qui amène son propre antipatterns. E. g., pourquoi la Simple Injecteur n'envisage pas de fonctions asynchrones: "Non, cette fonctionnalité n'a pas de sens pour un Simple Injecteur ou tout autre conteneur d'injection de dépendances, parce qu'elle viole les quelques règles de base quand il s'agit de l'injection de dépendance." Cependant, la lecture strictement "par les règles de base" apparemment, d'autres forces antipatterns qui semblent bien pire.

La question: est-il une solution pour l'initialisation asynchrone qui évite tous les antipatterns?


Mise à jour: Terminée signature pour AzureConnections (mentionnée ci-dessus en tant que Connections):

public sealed class AzureConnections
{
  public AzureConnections();

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

  public async Task InitializeAsync();
}

24voto

Steven Points 56939

Le problème que vous avez, et l'application que vous êtes en train de construire, est une typique. Il est typique pour deux raisons:

  1. vous avez besoin (ou plutôt le souhaitez) asynchrone démarrage de l'initialisation, et
  2. Votre application framework (azure fonctions) prend en charge asynchrone démarrage de l'initialisation (ou plutôt, il semble y avoir peu de cadre qui l'entoure). Cela rend votre situation un peu différente à partir d'un scénario typique, ce qui peut le rendre un peu plus difficile à discuter en commun des habitudes.

Cependant, même dans votre cas, la solution est assez simple et élégant:

Extrait de l'initialisation de l'classes qui le tenir, et de le déplacer dans la Composition de la Racine. À ce stade, vous pouvez créer et initialiser les classes avant de les enregistrer dans le récipient et de le nourrir ceux qui sont initialisées des classes dans le conteneur en tant que partie des enregistrements.

Cela fonctionne bien dans votre cas particulier, parce que vous voulez faire (une seule fois) démarrage de l'initialisation. Démarrage de l'initialisation se fait généralement avant de configurer le conteneur (ou parfois après si elle nécessite une entièrement composée d'objet graphique). Dans la plupart des cas que j'ai vu, l'initialisation peut être fait avant, comme peut être fait de manière efficace dans votre cas.

Comme je l'ai dit, votre cas est un peu particulière, par rapport à la norme. La norme est:

  • La Start-up de l'initialisation synchrone. Cadres (comme ASP.NET de Base) généralement ne prennent pas en charge initialisation asynchrone dans la phase de démarrage
  • L'initialisation doit souvent être fait par demande et juste-à-temps plutôt que par l'application et à l'avance. Souvent les composants qui ont besoin d'initialisation ont une durée de vie courte, ce qui signifie que nous sommes généralement d'initialiser une telle instance, lors de la première utilisation (en d'autres termes: juste-à-temps).

Il n'y a généralement pas de réel avantage de faire de la start-up de l'initialisation asynchrone. Il n'y a aucun avantage en termes de performance car, à l'heure de démarrage, il n'y aura qu'un seul thread en cours d'exécution de toute façon (si l'on peut paralléliser ce, qui n'a évidemment pas besoin async). Notez également que, bien que certains types d'application peuvent faire l'impasse sur synch-sur-async, dans la Composition de la Racine, nous savons exactement ce qui type d'application que nous utilisons et si oui ou non ce sera un problème ou pas. Une Composition de la Racine est toujours spécifique à une application. En d'autres termes, lorsque nous avons initialisation dans la Composition de la Racine d'un non-blocage de l'application (par ex. ASP.NET de Base, d'Azur, Fonctions, etc), il n'y a généralement aucun avantage de faire de la start-up de l'initialisation asynchrone.

Parce que dans la Composition de la Racine de savoir si ou de ne pas synchroniser-sur-async est un problème ou pas, on pourrait même décider de faire l'initialisation lors de la première utilisation et de manière synchrone. Parce que le montant de l'initialisation est finie (par rapport à par-l'initialisation de la requête), il est possible, en pratique, l'impact sur les performances de le faire sur un thread d'arrière-plan avec synchrone bloquant si nous le souhaitons. Tout ce que nous avons à faire est de définir une classe de Proxy dans notre Composition de la Racine qui permet de s'assurer que l'initialisation est effectuée lors de la première utilisation. C'est à peu près l'idée que la Marque Seemann proposé comme réponse.

Je n'étais pas familier avec Azure Fonctions, c'est en fait le premier type d'application (à l'exception de la Console apps, bien sûr) que je connais qui supporte l'initialisation asynchrone. Dans la plupart des types de cadre, il n'existe aucun moyen pour les utilisateurs de faire de cette start-up de l'initialisation asynchrone à tous. Lorsque nous sommes à l'intérieur d'un Application_Start événement dans un ASP.NET application ou dans la Startup de la classe d'un ASP.NET application de Base, par exemple, il n'est pas asynchrone. Tout doit être synchrone.

En plus de cela, l'application des cadres de ne pas nous permettre de construire leur cadre de racine de composants de manière asynchrone. Donc, même si DI Récipients de soutenir le concept de faire asynchrone résout, ce ne serait pas travailler en raison de "l'absence" du soutien de l'application de cadres de. Prendre ASP.NET Core IControllerActivator par exemple. Ses Create(ControllerContext) méthode nous permet de composer un Controller instance, mais le type de retour de la Create méthode object, pas Task<object>. En d'autres termes, même si DI Conteneurs serait de nous fournir un ResolveAsync méthode, il serait encore la cause de blocage, car ResolveAsync des appels étaient enveloppés derrière synchrone cadre des abstractions.

Dans la majorité des cas, vous verrez que l'initialisation se fait par instance ou à l'exécution. Un SqlConnection, par exemple, est généralement ouvert par la demande, de sorte que chaque demande doit ouvrir sa propre connexion. Quand nous voulons ouvrir la connexion "juste à temps", cela entraîne inévitablement des interfaces qui sont asynchrones. Mais attention, ici:

Si nous créons une mise en œuvre qui est synchrone, il faut seulement faire son abstraction synchrone dans le cas où nous sommes sûrs qu'il n'y aura jamais d' être une autre mise en œuvre (ou proxy, décorateur, interceptor, etc.) qui est asynchrone. Si nous validement faire de l'abstraction synchrone (c'est à dire des méthodes et des propriétés qui ne sont pas exposer Task<T>), on pourrait très bien avoir une Abstraction qui Fuit à la main. Cela peut nous forcer à faire de grands changements dans l'application, lorsque nous obtenons une implémentation asynchrone plus tard.

En d'autres termes, avec l'introduction de async nous avons à prendre encore plus soin de la conception de notre application abstractions. Cela vaut pour votre cas. Même si vous pourriez seulement besoin d'démarrage de l'initialisation maintenant, êtes-vous sûr que pour les abstractions que vous avez définies (et AzureConnections ainsi) n'aura jamais besoin de juste-à-temps async d'initialisation? Dans le cas où le comportement synchrone de l' AzureConnections est un détail d'implémentation, vous aurez à faire c'async tout de suite.

Un autre exemple de cela est de votre INugetRepository. Ses membres sont synchrones, mais c'est clairement une Abstraction qui Fuit, parce que la raison pour laquelle il est synchrone est parce que sa mise en œuvre est synchrone. Sa mise en œuvre, cependant, est synchrone, car il rend l'utilisation d'un héritage NuGet package NuGet qui a une API synchrones. Il est assez clair que l' INugetRepository devrait être complètement asynchrone, même si sa mise en œuvre est synchrone.

Dans une application qui s'applique asynchrone, la plupart des abstractions aura surtout async membres. Lorsque c'est le cas, ce serait un no-brainer pour faire ce genre de juste-à-temps de l'initialisation de la logique asynchrone ainsi, tout est déjà asynchrone.

Pour résumer:

  • Dans le cas où vous avez besoin démarrage de l'initialisation: le faire avant ou après la configuration du conteneur. Cela rend la composition de graphes d'objets lui-même rapide, fiable et vérifiable.
  • Faire l'initialisation avant de configurer le conteneur empêche Temporelle de Couplage, mais pourrait signifier que vous aurez à déplacer l'initialisation de l'classes qui en ont besoin (qui est en fait une bonne chose).
  • Async démarrage de l'initialisation est impossible dans la plupart des types d'application. Dans les autres types d'applications, il est généralement inutile.
  • Dans le cas où vous avez besoin par demande ou juste-à-temps de l'initialisation, il n'existe aucun moyen d'avoir interfaces asynchrones.
  • Être prudent avec les interfaces synchrones si vous êtes en train de construire une application asynchrone, vous pourriez être en fuite de détails de mise en œuvre.

5voto

Mark Seemann Points 102767

Alors que je suis assez sûr que la suite n'est pas ce que vous cherchez, pouvez-vous expliquer pourquoi il ne répond pas à votre question?

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

Afin de garder le design clair, j'ai seulement mis en œuvre l'une des propriétés des nuages, mais les deux autres pourraient être fait de la même façon.

L' AzureConnections constructeur ne va pas bloquer, même si cela prend beaucoup de temps pour initialiser les différents cloud objets.

Il sera, d'autre part, de commencer le travail, et depuis .NET tâches se comportent comme des promesses, la première fois que vous essayez d'accéder à la valeur (à l'aide d' Result) il va retourner la valeur produite par InitializeStorageAccount.

Je reçois la forte impression que ce n'est pas ce que vous voulez, mais depuis que je ne comprends pas quel est le problème que vous essayez de résoudre, j'ai pensé que je devais quitter cette réponse si au moins nous aurions quelque chose à discuter.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X