Dans mon article précédent, j'ai décrit comment obtenir le comportement de monade "Peut-être" en utilisant les opérateurs asynchrones / attendent . Cette fois, je vais montrer comment implémenter un autre modèle de conception populaire "Reader Monad" en utilisant les mêmes techniques.
Ce modèle permet de passer implicitement du contexte dans une fonction sans utiliser de paramètres de fonction ou d'objets globaux partagés et il peut être considéré comme une autre façon de mettre en œuvre l'injection de dépendance. Par exemple:
class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"}));
"Reader" classique
Tout d'abord, regardons comment la monade peut être implémentée sans opérateurs asynchrones / attendent :
public class Config { public string Template; } public static class ClassicReader { public static void Main() { var greeter = GreetGuys(); Console.WriteLine(greeter.Apply(new Config{Template = "Hello, {0}"}));
(Lecteur)
public class Reader<T, TCtx> { private readonly Func<TCtx, T> _exec; public Reader(Func<TCtx, T> exec) => this._exec = exec; public T Apply(TCtx ctx) => this._exec(ctx); } public static class Reader { public static Reader<TJoin, TCtx> SelectMany<TIn, TOut, TCtx, TJoin>( this Reader<TIn, TCtx> source, Func<TIn, Reader<TOut, TCtx>> bind, Func<TIn, TOut, TJoin> join) => new Reader<TJoin, TCtx>(ctx => { var inValue = source.Apply(ctx); var outValue = bind(inValue).Apply(ctx); return join(inValue, outValue); }); }
Le code fonctionne mais il ne semble pas naturel pour les développeurs C #. Pas étonnant, car les monades provenaient de langages fonctionnels où un code similaire peut être écrit de manière plus concise. Cependant, l'implémentation classique permet de sous-estimer l'essence du modèle - au lieu d'exécuter immédiatement du code, il est placé dans une fonction qui sera appelée lorsque son contexte sera prêt.
public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name));
SelectMany peut combiner plusieurs de ces fonctions en une seule, vous pouvez donc créer une sous-routine dont l'exécution sera différée jusqu'à ce que son contexte soit appliqué. D'un autre côté, cette approche ressemble à l'écriture de code asynchrone où l'exécution du programme est arrêtée si une opération asynchrone est en cours d'exécution. Lorsqu'un résultat de l'opération est prêt, l'exécution du programme se poursuit. Une hypothèse se pose que l'infrastructure C # conçue pour fonctionner avec des opérations asynchrones ( async / attente ) pourrait être utilisée d'une manière ou d'une autre dans l'implémentation de la monade "Reader" et ... l'hypothèse est correcte!
"Lecteur" asynchrone
Dans mon article précédent, j'ai montré comment obtenir le contrôle des opérateurs asynchrones / attendent en utilisant des types de retour asynchrones généralisés . La même technique sera utilisée cette fois. Commençons par la classe "Reader" qui sera utilisée comme type de résultat des opérations asynchrones:
[AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ...
La classe a deux responsabilités différentes (théoriquement on pourrait créer 2 classes):
- Extraire une certaine valeur d'un contexte lorsque le contexte est appliqué
- Création d'une liste chaînée d'instances de Reader qui sera utilisée pour distribuer un contexte sur une hiérarchie d'appels.
Pour chaque responsabilité, nous utiliserons un constructeur distinct:
private readonly Func<object, T> _extractor;
Lorsqu'une instance de la classe Reader est utilisée comme argument de l'opérateur wait , l'instance recevra un lien vers un délégué de continuation qui ne devrait être appelé que lorsqu'un contexte d'exécution est résolu et nous pouvons extraire (du contexte) certaines données qui seront utilisé dans la suite.

Pour créer des connexions entre les "lecteurs" parents et enfants, créons la méthode:
private IReader _child; internal void SetChild(IReader reader) { this._child = reader; if (this._ctx != null) { this._child.SetCtx(this._ctx); } }
qui sera appelé dans ReaderTaskMethodBuilder :
public class ReaderTaskMethodBuilder<T> { ... public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IReader reader) { this.Task.SetChild(reader); } awaiter.OnCompleted(stateMachine.MoveNext); } public Reader<T> Task { get; } }
Dans la méthode SetChild , nous appelons SetCtx qui propage un contexte vers le bas dans une hiérarchie et appelle un extracteur s'il est défini:
public void SetCtx(object ctx) { this._ctx = ctx; if (this._ctx != null) { this._child?.SetCtx(this._ctx); if (this._extractor != null) { this.SetResult(this._extractor(this._ctx)); } } }
SetResult stocke une valeur extraite d'un contexte et appelle une continuation:
internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); }
Dans le cas où une instance de Reader ne dispose pas d'un «extracteur» initialisé, SetResult est censé être appelé par ReaderTaskMethodBuilder lorsqu'une machine d'état générée passe à son état final.
La méthode Apply n'appelle que SetCtx
public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; }
Vous pouvez trouver tout le code sur github ( s'il n'est toujours pas bloqué )
Maintenant, je veux montrer un exemple plus réaliste de la façon dont le lecteur asynchrone peut être utilisé:
Cliquez pour développer l'exemple public static class ReaderTest { public class Configuration { public readonly int DataBaseId; public readonly string GreetingTemplate; public readonly string NameFormat; public Configuration(int dataBaseId, string greetingTemplate, string nameFormat) { this.DataBaseId = dataBaseId; this.GreetingTemplate = greetingTemplate; this.NameFormat = nameFormat; } } public static async Task Main() { int[] ids = { 1, 2, 3 }; Configuration[] configurations = { new Configuration(100, "Congratulations, {0}! You won {1}$!", "{0} {1}"), new Configuration(100, "¡Felicidades, {0}! Ganaste {1} $", "{0}"), }; foreach (var configuration in configurations) { foreach (var userId in ids) {
Le programme affiche des salutations pour certains utilisateurs mais nous ne connaissons pas leurs noms à l'avance car nous n'avons que leurs identifiants, nous devons donc lire ces informations dans une "base de données". Pour se connecter à la base de données, nous devons connaître un identifiant de connexion et pour créer un message d'accueil, nous avons besoin de son modèle et ... toutes les informations sont transmises implicitement via le lecteur asynchrone.
Injection de dépendances avec Async "Reader"
Par rapport à l'implémentation classique, le lecteur asynchrone a un défaut - nous ne pouvons pas spécifier un type de contexte passé. Cette limitation vient du fait que le compilateur C # n'attend qu'un seul paramètre de type générique dans une classe de générateur de méthode asynchrone (il sera peut-être corrigé à l'avenir).
D'un autre côté, je ne pense pas que ce soit critique car dans la vie réelle, très probablement, un conteneur d'injection de dépendance sera passé en contexte:
public static class Reader { public static Reader<TService> GetService<TService>() => Reader<TService>.Read<IServiceProvider>(serviceProvider => (TService)serviceProvider .GetService(typeof(TService))); }
... private static async Reader<string> Greet(string userName) { var service = await Reader.GetService<IGreater>(); return service.GreetUser(userName); } ...
( ici vous pouvez trouver un exemple complet ... )
Contrairement à l' async "Peut-être" , que je ne recommande pas d'utiliser dans n'importe quel code de production (en raison du problème avec finalement les blocs), j'envisagerais d'utiliser l'async Reader dans des projets réels en remplacement (ou "en plus") du approche d'injection de dépendance traditionnelle (lorsque toutes les dépendances sont passées dans un constructeur de classe) car le Reader présente certains avantages:
- Il n'y a pas besoin de propriétés de classe qui stockent des liens vers des ressources injectées. En fait, il n'y a aucun besoin de classes - toute la logique peut être implémentée dans des méthodes statiques.
- L'utilisation du Reader encouragera la création de code non bloquant car toutes les méthodes seront asynchrones et rien n'empêchera les développeurs d'utiliser des versions non bloquantes des opérations d'entrée / sortie.
- Le code sera un peu plus "lisible" - chaque fois que nous verrons le Reader comme un type de résultat d'une méthode, nous saurons qu'il nécessite l'accès à un contexte implicite.
- Le lecteur asynchrone n'utilise pas de réflexion.
Bien sûr, il peut y avoir des arguments contre l'utilisation du Reader, mais de toute façon, le principal objectif de ces articles est de montrer comment les modèles, qui ont été initialement conçus pour les langages fonctionnels, peuvent être adoptés pour un style de codage impératif qui est censé être plus simple à comprendre par la plupart des gens.