Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Les délégués fournissent un mécanisme qui autorise des conceptions logicielles impliquant un couplage minimal entre les composants.
Un excellent exemple pour ce type de conception est LINQ. Le modèle d’expression de requête LINQ s’appuie sur des délégués pour toutes ses fonctionnalités. Prenons cet exemple simple :
var smallNumbers = numbers.Where(n => n < 10);
Cela filtre la séquence de nombres uniquement pour ceux inférieurs à la valeur 10.
La Where méthode utilise un délégué qui détermine quels éléments d’une séquence passent le filtre. Quand vous créez une requête LINQ, vous fournissez l’implémentation du délégué à cette fin spécifique.
Le prototype de la méthode Where est :
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Cet exemple est répété avec toutes les méthodes qui font partie de LINQ. Ils s’appuient tous sur des délégués pour le code qui gère la requête spécifique. Ce modèle de conception d’API est un puissant modèle pour apprendre et comprendre.
Cet exemple simple montre comment les délégués nécessitent très peu de couplage entre les composants. Vous n’avez pas besoin de créer une classe qui dérive d’une classe de base particulière. Vous n’avez pas besoin d’implémenter une interface spécifique. La seule exigence consiste à fournir l’implémentation d’une méthode fondamentale à la tâche.
Créer vos propres composants avec des délégués
Nous allons tirer profit de cet exemple en créant un composant à l’aide d’une conception qui s’appuie sur des délégués.
Définissons un composant qui peut être utilisé pour les messages de journal dans un système volumineux. Les composants de bibliothèque peuvent être utilisés dans de nombreux environnements différents, sur plusieurs plateformes différentes. Il existe de nombreuses fonctionnalités courantes dans le composant qui gère les journaux d’activité. Il devra accepter les messages de n’importe quel composant du système. Ces messages auront des priorités différentes, que le composant principal peut gérer. Les messages doivent avoir des horodatages dans leur formulaire archivé final. Pour les scénarios plus avancés, vous pouvez filtrer les messages par le composant source.
Il existe un aspect de la fonctionnalité qui changera souvent : où les messages sont écrits. Dans certains environnements, ils peuvent être écrits dans la console d’erreur. Dans d’autres, un fichier. D’autres possibilités incluent le stockage de base de données, les journaux des événements du système d’exploitation ou d’autres stockages de documents.
Il existe également des combinaisons de sortie qui peuvent être utilisées dans différents scénarios. Vous pouvez écrire des messages dans la console et dans un fichier.
Une conception basée sur les délégués fournira une grande flexibilité et facilite la prise en charge des mécanismes de stockage qui peuvent être ajoutés à l’avenir.
Dans cette conception, le composant de journal principal peut être une classe non virtuelle, voire sealed. Vous pouvez utiliser n’importe quel groupe de délégués pour inscrire les messages sur différents supports de stockage. La prise en charge intégrée des délégués multidiffusion facilite la prise en charge des scénarios où les messages doivent être écrits à plusieurs emplacements (un fichier et une console).
Une première implémentation
Commençons petit : l’implémentation initiale accepte les nouveaux messages et les écrit à l’aide d’un délégué attaché. Vous pouvez commencer par un délégué qui écrit des messages dans la console.
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(string msg)
{
if (WriteMessage is not null)
WriteMessage(msg);
}
}
La classe statique ci-dessus est la chose la plus simple qui peut fonctionner. Nous devons écrire l’implémentation unique de la méthode qui écrit des messages dans la console :
public static class LoggingMethods
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
Pour finir, nous devons raccorder le délégué en l’attachant au délégué WriteMessage déclaré dans l’enregistreur d’événements :
Logger.WriteMessage += LoggingMethods.LogToConsole;
Pratiques
Notre exemple jusqu’à présent est assez simple, mais il illustre toujours certaines des lignes directrices importantes pour les conceptions impliquant des délégués.
L’utilisation des types de délégués définis dans le cadre de base facilite aux utilisateurs le travail avec les délégués. Vous n’avez pas besoin de définir de nouveaux types et les développeurs qui utilisent votre bibliothèque n’ont pas besoin d’apprendre de nouveaux types délégués spécialisés.
Les interfaces utilisées sont aussi minimales et aussi flexibles que possible : pour créer un enregistreur d’événements de sortie, vous devez créer une méthode. Cette méthode peut être une méthode statique ou une méthode d’instance. Elle peut avoir n’importe quel accès.
Mettre en forme la sortie
Nous allons rendre cette première version un peu plus robuste, puis commencer à créer d’autres mécanismes de journalisation.
Ensuite, ajoutons quelques arguments à la LogMessage() méthode afin que votre classe de journal crée des messages plus structurés :
public enum Severity
{
Verbose,
Trace,
Information,
Warning,
Error,
Critical
}
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(Severity s, string component, string msg)
{
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Ensuite, utilisons cet Severity argument pour filtrer les messages envoyés à la sortie du journal.
public static class Logger
{
public static Action<string>? WriteMessage;
public static Severity LogLevel { get; set; } = Severity.Warning;
public static void LogMessage(Severity s, string component, string msg)
{
if (s < LogLevel)
return;
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Pratiques
Vous avez ajouté de nouvelles fonctionnalités à l’infrastructure de journalisation. Étant donné que le composant enregistreur d’événements est très faiblement couplé à n’importe quel mécanisme de sortie, ces nouvelles fonctionnalités peuvent être ajoutées sans impact sur l’un des codes implémentant le délégué de l’enregistreur d’événements.
Au fur et à mesure que vous créez cela, vous verrez d’autres exemples de la façon dont ce couplage libre permet une plus grande flexibilité dans la mise à jour des parties du site sans aucune modification apportée à d’autres emplacements. En fait, dans une application plus grande, les classes de sortie de l’enregistreur d’événements peuvent se trouver dans un autre assembly et n’ont même pas besoin d’être reconstruites.
Générer un deuxième moteur de sortie
Le composant journal commence à prendre forme. Ajoutons un autre moteur de sortie qui enregistre les messages dans un fichier. Ce moteur de sortie est légèrement plus complexe. Il s’agit d’une classe qui encapsule les opérations de fichier et garantit que le fichier est toujours fermé après chaque écriture. Cela garantit que toutes les données sont vidées sur le disque une fois chaque message généré.
Voici cet enregistreur d’événements basé sur des fichiers :
public class FileLogger
{
private readonly string logPath;
public FileLogger(string path)
{
logPath = path;
Logger.WriteMessage += LogMessage;
}
public void DetachLog() => Logger.WriteMessage -= LogMessage;
// make sure this can't throw.
private void LogMessage(string msg)
{
try
{
using (var log = File.AppendText(logPath))
{
log.WriteLine(msg);
log.Flush();
}
}
catch (Exception)
{
// Hmm. We caught an exception while
// logging. We can't really log the
// problem (since it's the log that's failing).
// So, while normally, catching an exception
// and doing nothing isn't wise, it's really the
// only reasonable option here.
}
}
}
Une fois que vous avez créé cette classe, vous pouvez l’instancier et attacher sa méthode LogMessage au composant Logger :
var file = new FileLogger("log.txt");
Ces deux ne sont pas mutuellement exclusifs. Vous pouvez attacher les deux méthodes de journalisation et générer des messages à la console et dans un fichier.
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier
Plus tard, même dans la même application, vous pouvez supprimer l’un des délégués sans aucun autre problème sur le système :
Logger.WriteMessage -= LoggingMethods.LogToConsole;
Pratiques
À présent, vous avez ajouté un deuxième gestionnaire de sortie pour le sous-système de journalisation. Celui-ci a besoin d’un peu plus d’infrastructure pour prendre correctement en charge le système de fichiers. Le délégué est une méthode d’instance. Il s’agit également d’une méthode privée. Il n'est pas nécessaire d'améliorer l'accessibilité, car l'infrastructure des délégués peut connecter les délégués.
Deuxièmement, la conception basée sur les délégués permet plusieurs méthodes de sortie sans code supplémentaire. Vous n’avez pas besoin de créer une infrastructure supplémentaire pour prendre en charge plusieurs méthodes de sortie. Elles viennent simplement s’ajouter comme autre méthode dans la liste d’invocation.
Faites particulièrement attention au code de la méthode de sortie de journalisation de fichier. Le code est conçu afin de garantir qu'il ne génère aucune exception. Bien que cela ne soit pas toujours strictement nécessaire, il s’agit souvent d’une bonne pratique. Si l’une des méthodes délégué lève une exception, les délégués restants qui sont sur la liste d’invocation ne sont pas appelés.
Enfin, l'enregistreur de fichiers doit gérer ses ressources en ouvrant et en fermant le fichier pour chaque message de log. Vous pouvez choisir de conserver le fichier ouvert et d’implémenter IDisposable pour fermer le fichier une fois que vous avez terminé.
L’une ou l’autre méthode présente ses avantages et ses inconvénients. Les deux créent un peu plus de couplage entre les classes.
Aucun du code de la classe n’aurait besoin d’être mis à jour pour prendre en charge l’un ou l’autre Logger scénario.
Gérer les délégués null
Enfin, nous allons mettre à jour la méthode LogMessage afin qu’elle soit robuste pour ces cas lorsqu’aucun mécanisme de sortie n’est sélectionné. L'implémentation actuelle génère un NullReferenceException lorsqu'il n'y a pas de liste d'invocation attachée au délégué WriteMessage.
Vous préférerez peut-être une conception qui se poursuit silencieusement lorsqu’aucune méthode n’a été attachée. Il est facile d’utiliser l’opérateur conditionnel Null, combiné à la Delegate.Invoke() méthode :
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
L'opérateur conditionnel null (?.) s'arrête lorsque l'opérande gauche (WriteMessage dans ce cas) est null, ce qui signifie qu'aucune tentative n'est effectuée pour enregistrer un message.
Vous ne trouverez pas la Invoke() méthode répertoriée dans la documentation pour System.Delegate ou System.MulticastDelegate. Le compilateur génère une méthode de type sécurisé Invoke pour tout type de délégué déclaré. Dans cet exemple, cela signifie que Invoke prend un seul argument string et a un type de retour vide.
Résumé des pratiques
Nous venons de voir le début d’un composant de journal qui peut être étendu avec d’autres enregistreurs, ainsi que d’autres fonctionnalités. En utilisant des délégués dans la conception, ces différents composants sont faiblement couplés. Cela offre plusieurs avantages. Il est facile de créer de nouveaux mécanismes de sortie et de les attacher au système. Ces autres mécanismes n’ont besoin que d’une seule méthode : la méthode qui écrit le message de journal. Il s’agit d’une conception résiliente lorsque de nouvelles fonctionnalités sont ajoutées. Le contrat requis pour tout enregistreur consiste à implémenter une méthode. Cette méthode peut être une méthode statique ou instance. Il peut s’agir d’un accès public, privé ou juridique.
La classe Logger peut apporter des améliorations ou des modifications sans introduire de modifications avec rupture. Comme n’importe quelle classe, vous ne pouvez pas modifier l’API publique sans risque de rupture des modifications. Toutefois, étant donné que le couplage entre le logger et tous les moteurs de sortie se fait uniquement par l'intermédiaire du délégué, aucun autre type (comme des interfaces ou des classes de base) n'est impliqué. Le couplage est aussi réduit que possible.