Inversion de contrôle et injection de dépendances avec Castle Windsor - Partie 3
avril 30, 2008
Cet article n’est qu’une traduction de l’excellent article écrit par Simone Busoli que vous trouverez ici.
Dans l’article précédent, nous avons vu comment tirer profit de certaines fonctionnalités offertes par Windsor pour configurer les composants et leur fournir leurs dépendances. Vous avez vu comment gérer les dépendances optionnelles et obligatoires ainsi que comment injecter des valeurs simples ou des références de composants, individuellement ou collectés dans des tableaux, des listes et des dictionnaires.
Dans cet article, nous conclurons la discussion à propos des fonctionnalités principales du conteneur Windsor en faisant évoluer l’exemple qui nous a accompagné tout au long de cette série d’articles. Le prochain article examinera des sujets plus avancés.
Introduction
Jusqu’ici, nous avons abordé quelques fonctionnalités simples disponibles dans Windsor. Il expose beaucoup plus d’options de configuration, non seulement en ce qui concerne la façon d’injecter des dépendances mais également sur comment contrôler leur comportement. Comme d’habitude, nous examinerons ces fonctionnalités en introduisant quelques changement de manière incrémentielle dans les requis de notre application.
Pour le moment, l’application d’exemple est capable de télécharger des fichiers via quelques protocoles - HTTP et FTP - et peut être étendue facilement pour supporter n’importe quel autre, simplement en suivant les étapes suivantes:
- Créer un composant implémentant l’interface IFileDownloader.
- Enregistrer le composant dans le fichier de configuration de Windsor.
- Ajouter le composant au tableau (ou à la liste, dictionnaire) des composants acceptés par le constructeur de la classe HtmlTitleRetriever.
Afin de rafraîchir notre mémoire, créons une nouvelle implémentation du service qui soit capable de récupérer un fichier depuis le système de fichiers. Notez qu’une Uri référençant un fichier sur le système de fichiers local doit avoir sa propriété Scheme à la valeur file.
L’extrait de code suivant montre l’implémentation de cette classe :
public class FileSystemFileDownloader : IFileDownloader
{
public string Download( Uri file )
{
using( StreamReader reader = new StreamReader( file.LocalPath ) )
return reader.ReadToEnd();
}
public bool SupportsUriScheme( Uri file )
{
return file.Scheme == "file";
}
}
Évidemment, le composant doit être enregistré dans le fichier de configuration et ajouté au tableau des IFileDownloaders acceptés par le constructeur de la classe HtmlTitleRetriever.
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>This is the title of the document:</additionalmessage>
<downloaders>
<array>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
<item>${FileSystemFileDownloader}</item>
</array>
</downloaders>
</parameters>
</component>
<component id="FileSystemFileDownloader" type="WindsorSample.FileSystemFileDownloader, WindsorSample" service="WindsorSample.IFileDownloader, WindsorSample">
</component>
Maintenant, modifions les requis de l’application et introduisons de nouveaux concepts.
Propriétés de configuration et inclusions
Les nouveaux requis de l’application spécifie que trois fichiers doivent être téléchargés: un via FTP, un autre via HTTP et le dernier via le système de fichiers. Ces fichiers sont statiques mais ne devraient pas être codés en dur car ils peuvent éventuellement changer. Nous choisissons de les spécifier via le fichier de configuration et de modifier le code de la classe HtmlTitleRetriever pour qu’elle accepte les Uris des fichiers en tant que dépendance obligatoire ou optionnelle.
Réfléchissons un peu à cette nouvelle fonctionnalité. Devrions nous rendre cette nouvelle dépendance optionnelle ou obligatoire ? La fournirons nous via un tableau, une liste ou un dictionnaire ? Dans notre cas, c’est principalement une question de goûts. Si nous optons pour une dépendance obligatoire, il nous faudra la fournir via un paramètre de constructeur. Sinon, il nous faudra ajouter une propriété en lecture/écriture dont le champ correspondant aura comme valeur initiale un tableau vide (ou une liste, dictionnaire) afin d’éviter les références nulles dans le cas ou aucun paramètre ne serait spécifié. Implémentons cette fonctionnalité avec une dépendance optionnelle. Le code de la classe HtmlTitleRetriever deviendra le suivant :
public class HtmlTitleRetriever
{
private readonly IFileDownloader[] downloaders;
private readonly ITitleScraper scraper;
private Uri[] files;
private string additionalMessage = "Title: ";
public Uri[] Files
{
get { return files; }
set { files = value; }
}
public string AdditionalMessage
{
get { return additionalMessage; }
set { additionalMessage = value; }
}
public HtmlTitleRetriever( IFileDownloader[] downloaders, ITitleScraper scraper )
{
this.downloaders = downloaders;
this.scraper = scraper;
}
public string GetTitle( Uri file )
{
foreach( IFileDownloader downloader in downloaders )
if( downloader.SupportsUriScheme( file ) )
return string.Concat( additionalMessage, scraper.Scrape( downloader.Download( file ) ) );
return string.Empty;
}
public IEnumerable<string> GetTitles()
{
foreach( Uri file in files )
yield return GetTitle( file );
}
}
Mis à part la nouvelle propriété Files, une nouvelle méthode nommée GetTitles a été ajoutée. Elles ne prend aucun paramètres et itère simplement sur la liste des fichiers, appelant la méthode GetTitle pour chacun d’entre eux. Le reste du code n’a pas changé. Ces modifications ont un impact sur le fichier de configuration également:
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>This is the title of the document:</additionalmessage>
<files>
<array>
<item>http://mi.mirror.garr.it/mirrors/postfix/index.html</item>
<item>ftp://mi.mirror.garr.it/mirrors/postfix/index.html</item>
<item>file://c:\index.html</item>
</array>
</files>
<downloaders>
<array>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
<item>${FileSystemFileDownloader}</item>
</array>
</downloaders>
</parameters>
</component>
Maintenant, un problème survient. Comme vous pouvez le voir, la verbosité du fichier de configuration est proportionnelle à la complexité et les requis de l’application grandissants. En particulier, les informations statiques comme l’enregistrement des composants est mélangée avec d’autres variables comme les dépendances additionalMessage et Files. Il pourrait être intéressant de les séparer du reste, afin qu’elles puissent être modifiées sans devoir les rechercher dans le fichier de configuration. Windsor offre une fonctionnalité nommé properties qui peut nous aider sur ce point précis.
En utilisant les propriétés de configuration, vous pouvez créer un mécanisme d’indirection qui vous permet de spécifier des références à des valeurs dans les autres sections de configuration. La syntaxe pour référencer une propriété de configuration est #{ Nom de la propriété }. Elle ne requière aucune modification de votre code. Les changements dans le fichier de configuration sont les suivants :
<castle>
<properties>
<files>
<array>
<item>http://mi.mirror.garr.it/mirrors/postfix/index.html</item>
<item>ftp://mi.mirror.garr.it/mirrors/postfix/index.html</item>
<item>file://c:\index.html</item>
</array>
</files>
<message>This is the title of the document:</message>
</properties>
<components>
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>#{message}</additionalmessage>
<files>#{files}</files>
<downloaders>
<array>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
<item>${FileSystemFileDownloader}</item>
</array>
</downloaders>
</parameters>
</component>
</components>
</castle>
Un nouveau sous-élément de la section castle, nommé properties, à été ajouté. Il contient les valeurs qui apparaissaient directement dans la déclaration des composants. Ces valeurs sont référencées par le composant HtmlTitleRetriever en utilisant la syntaxe présentée plus haut.
Mais ceci ne serait pas un si grand avantage si vous ne pouviez pas séparer réellement les sections properties et components. Si vous deviez déployer l’application de nombreuses fois, il est probable que les seules différences soit au niveau de la section propriétés et que la section components ne change pas. Pour ce type de cas, Windsor vous permet d’utiliser plusieurs fichiers de configuration qui seront inspectés et rassemblés au démarrage de l’application. Ceci est accompli grâce aux inclusions. Les inclusions permettent de charger des fichiers de configuration depuis différentes sources telles que le système de fichiers, un partage de réseaux ou encore une ressource d’assembly.
Le code suivant à quoi ressemble le fichier de configuration avec des inclusions. Notez que le fichier properties.config (le nom du fichier est un choix personnel) est chargé depuis le système de fichiers en utilisant un chemin relatif à l’exécutable de l’application, alors que le fichier components.config est chargé depuis l’assembly.
< ?xml version="1.0" encoding="utf-8" ?> <configuration> <configsections> <section type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" name="castle" /> </configsections> <castle> <include uri="file://Properties.config" /> <include uri="assembly://WindsorSample/Components.config" /> </castle> </configuration>
Ci-dessous, le fichier properties.config:
< ?xml version="1.0" encoding="utf-8" ?> <configuration> <properties> <files> <array> <item>http://mi.mirror.garr.it/mirrors/postfix/index.html</item> <item>ftp://mi.mirror.garr.it/mirrors/postfix/index.html</item> <item>file://c:\index.html</item> </array> </files> <message>This is the title of the document:</message> </properties> </configuration>
Enfin, le fichier components.config. Celui étant récupéré en tant que resource de l’assembly de l’application, sa Build Action dans Visual Studio doit être Embedded Resource.
< ?xml version="1.0" encoding="utf-8" ?>
<configuration>
<components>
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>#{message}</additionalmessage>
<files>#{files}</files>
<downloaders>
<array>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
<item>${FileSystemFileDownloader}</item>
</array>
</downloaders>
</parameters>
</component>
<component id="StringParsingTitleScraper" type="WindsorSample.StringParsingTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
</component>
<component id="FileSystemFileDownloader" type="WindsorSample.FileSystemFileDownloader, WindsorSample" service="WindsorSample.IFileDownloader, WindsorSample">
</component>
<component id="HttpFileDownloader" type="WindsorSample.HttpFileDownloader, WindsorSample" service="WindsorSample.IFileDownloader, WindsorSample">
</component>
<component id="FtpFileDownloader" type="WindsorSample.FtpFileDownloader, WindsorSample" service="WindsorSample.IFileDownloader, WindsorSample">
</component>
</components>
</configuration>
En utilisant les inclusions, vous pouvez séparer la configuration statique de la volatile, facilitant ainsi son édition.
Les convertisseurs de type personnalisés
Si vous lancer l’application d’exemple dès maintenant, vous obtiendez une erreur de type ConverterException disant "Aucun convertisseur n’a été enregistré pour prendre en charge le type System.Uri". Le conteneur Windsor, en fait, ne peut convertir les paramètres depuis une chaîne de caractères vers leur type de destination que si ils sont d’un type dit "simple". Les conversions prises en charge de base par le conteneur sont listées dans cette page de documentation.
En fait, notre dépendance Files est de type Uri[]. Le type Array est pris en charge par Windsor mais pas le type Uri. Néanmoins, Castle fournit une API permettant d’enregistrer des convertisseurs de type, mais cela requiert quelques efforts.
Premièrement, il faut créer une classe héritant de la classe abstraire nommée AbstractConverter, qui effectuera la conversion de la chaîne de caractères en Uri. Ceci est assez simple, comme le montre le code suivant:
public class UriTypeConverter : AbstractTypeConverter
{
public override bool CanHandleType( Type type )
{
return type.IsAssignableFrom( typeof( Uri ) );
}
public override object PerformConversion( string value, Type targetType )
{
return new Uri( value );
}
public override object PerformConversion( IConfiguration configuration, Type targetType )
{
return PerformConversion( configuration.Value, targetType );
}
}
Puis, et c’est maintenant que cela devient un peu plus complexe, vous devez informer le conteneur a propos de ce convertisseur. L’approche suggérée est de créer une classe héritant de WindsorContainer et d’effectuer l’enregistrement du convertisseur dans le constructeur. Voici le code de notre classe CustomContainer :
public class CustomContainer : WindsorContainer
{
public CustomContainer( IConfigurationInterpreter interpreter )
{
// Register the type converter
IConversionManager manager = ( IConversionManager ) Kernel.GetSubSystem(
Castle.MicroKernel.SubSystemConstants.ConversionManagerKey );
manager.Add( new UriTypeConverter() );
// Process the configuration
interpreter.ProcessResource( interpreter.Source, Kernel.ConfigurationStore );
// Install the components
Installer.SetUp( this, Kernel.ConfigurationStore );
}
}
Votre application instancie désormais la classe CustomContainer au lieu de WindsorContainer et tout fonctionne comme nous nous y attendions.
Les décorateurs
Decorator est un modèle de conception (design pattern) formalisée pour la première fois dans le livre "Design Patterns: Elements of reusable object oriented software". Si vous ne connaissez pas déjà ce modèle, il vous suffit de savoir qu’il "décore" un objet en ajoutant des fonctionnalités à celui ci, sans changer la classe d’origine. L’IoC vous permet de bénéficier de cette méthode d’une manière très simple. En premier lieu, introduisons un nouveau requis à notre application. Intéressons-nous au mécanisme de récupération du titre du document HTML.
Jusqu’ici, nous avons extrait le titre depuis le contenu du fichier HTML en utilisant des fonctions de la classe string. Vous vous êtes peut-être demandé si cela était avisé, si l’on considère les performances. Je me le suis demandé et j’ai pensé à un autre mécanisme permettant d’extraire ce titre. Cela implique évidemment de créer une nouvelle classe implémentant l’interface ITitleScraper.
Il existe de nombreux analyseurs de code HTML en .NET mais nous n’en aurons pas besoin pour cette application très simple. Nous ferons ici le choix des expressions régulières. Voici le code de la classe RegexTitleScraper, qui devrait normalement être plus performante que la classe StringParsingTitleScraper.
public class RegexTitleScraper : ITitleScraper
{
public string Scrape( string fileContents )
{
return Regex.Match( fileContents, "", RegexOptions.Singleline ).Groups[ "title" ].Value;
}
}
Nous pouvons désormais passer de l’ancienne implémentation à la nouvelle en l’enregistrant dans le fichier de configuration et en la spécifiant dans la configuration du composant HtmlTitleRetriever, comme le montre le code suivant:
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>#{message}</additionalmessage>
<files>#{files}</files>
<downloaders>
<array>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
<item>${FileSystemFileDownloader}</item>
</array>
</downloaders>
<scraper>${RegexTitleScraper}</scraper>
</parameters>
</component>
<component id="StringParsingTitleScraper" type="WindsorSample.StringParsingTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
</component>
<component id="RegexTitleScraper" type="WindsorSample.RegexTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
</component>
Comment savoir qu’utiliser les expressions régulières est plus rapide que d’utiliser les chaînes de caractères ? Il nous faut un mécanisme de mesure de performances. En utilisant la classe StopWatch, nous pouvons mesurer avec précision le temps pris par une certaine opération. Pour cela, il nous faudrait modifier le code de chaque implémentation du service ITitleScraper. Une fois que nous aurions déterminer la classe la plus performante, il nous faudrait supprimer le code de mesure et spécifier à Windsor d’utiliser celle-ci. Mais vous ne souhaitez pas faire cela car vous vous doutez maintenant qu’il existe une meilleure méthode. En effet, celle-ci consiste à appliquer la méthode du decorator. Pour cela, nous créons une classe nommée BenchmarkingTitleScraperDecorater qui fera exactement cela.
public class BenchmarkingTitleScraperDecorator : ITitleScraper
{
private readonly ITitleScraper inner;
private readonly Stopwatch watch = new Stopwatch();
public BenchmarkingTitleScraperDecorator( ITitleScraper inner )
{
this.inner = inner;
}
public string Scrape( string fileContents )
{
watch.Start();
string result = inner.Scrape( fileContents );
watch.Stop();
Console.WriteLine(
"Scraping via {0} took {1} ticks to complete.",
inner.GetType(),
watch.ElapsedTicks );
watch.Reset();
return result;
}
}
Cette classe implémente l’interface ITitleScraper. Il est donc possible de l’utiliser comme les autres implémentations de cette interface. La différence est qu’elle n’effectue aucune extraction du titre. Elle se contente de déléguer l’opération à son ITitleScraper interne, fourni via un paramètre de constructeur. Ceci est exactement la logique derrière le modèle de conception decorator. Voyons maintenant comment l’utiliser via Windsor.
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>#{message}</additionalmessage>
<files>#{files}</files>
<downloaders>
<array>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
<item>${FileSystemFileDownloader}</item>
</array>
</downloaders>
<scraper>${BenchmarkingTitleScraperDecorator}</scraper>
</parameters>
</component>
<component id="BenchmarkingTitleScraperDecorator" type="WindsorSample.BenchmarkingTitleScraperDecorator, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
<parameters>
<inner>${StringParsingTitleScraper}</inner>
</parameters>
</component>
<component id="StringParsingTitleScraper" type="WindsorSample.StringParsingTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
</component>
<component id="RegexTitleScraper" type="WindsorSample.RegexTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
</component>
Vous pouvez voir que nous avons créer une chaîne. Une instance de BenchmarkingTitleScraperDecorator est fournie à notre classe HtmlTitleRetriever. Une instance de StringParsingTitleScraperest fournie en paramètre à la classe BenchmarkingTitleScraperDecorator. Ce simple mécanisme nous permet de décorer les ITitleScraper avec les fonctionnalités de mesure de performance. Il suffit de passer une instance de la classe RegexTitleScraper pour mesurer ses performances et les comparer avec celles de l’autre analyseur. Facile, n’est ce pas ? Il est intéressant de noter que sur ma machine, la classe StringParsingTitleScraperest est plus performante que la classe RegexTitleScraper. Il faut toujours mesurer.
Defines et conditions
Une autre fonctionnalité intéressante dans beaucoup de cas est la possibilité de définir des flag similaires aux directives de pré processeur que vous utilisez dans votre code .NET. Windsor peut comprendre des defines et des ifs, qui suivent la syntaxe montrée dans le fichier de configuration ci-dessous. Ils peuvent être employés afin d’enregistrer des composants conditionnellement ou de choisir un composant selon un flag. Dans notre exemple d’application, nous les utiliserons afin de passer du BenchmarkingTitleScraperDecorator à une vraie implémentation afin qu’il soit facile d’effectuer les tests de performance en mode debug et de retourner au mode de production sans mesure de performance.
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>{message}</additionalmessage>
<files>{files}</files>
<downloaders>
<array>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
<item>${FileSystemFileDownloader}</item>
</array>
</downloaders>
< ?if DEBUG?>
<scraper>${BenchmarkingTitleScraperDecorator}</scraper>
< ?else?>
<scraper>${StringParsingTitleScraper}</scraper>
< ?end?>
</parameters>
</component>
<component id="BenchmarkingTitleScraperDecorator" type="WindsorSample.BenchmarkingTitleScraperDecorator, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
<parameters>
<inner>${StringParsingTitleScraper}</inner>
</parameters>
</component>
<component id="StringParsingTitleScraper" type="WindsorSample.StringParsingTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
</component>
<component id="RegexTitleScraper" type="WindsorSample.RegexTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample">
</component>
Comme vous pouvez le deviner en lisant ce code, le composant HtmlTitleRetriever utilisera le BenchmarkingTitleScraperDecorator si la constante debug existe; sinon il utilisera le StringParsingTitleScraperest. Nous ajouterons ce flag dans le fichier de configuration principal de l’application, afin qu’il doit facile de le retrouver et de le supprimer en mode de production :
<castle> < ?define DEBUG ?> <include uri="file://Properties.config"></include> <include uri="assembly://WindsorSample/Components.config"></include> </castle>
Il suffit donc de commenter le define pour passer en mode de production. Les define et les if peuvent être utilisés sans aucune restriction sur leur localization dans le fichier de configuration. La seule contrainte est que les if ne peuvent être imbriqués.
Conclusion
Dans cet article, nous avons vu comment Windsor permet d’isoler les paramètres de configuration volatiles dans des sections séparées. Ensuite, nous avons vu comment séparer les fichiers de configuration afin d’obtenir une meilleure modularité pendant la configuration et le déploiement. Puis, nous avons vu comment gérer les paramètres dont la conversion n’était pas prise en compte directement par Windsor, en implémentant des convertisseurs personnalisés et en créant notre propre conteneur. Enfin, nous avons présenté une fonctionnalité rendue très simple à utiliser grâce à Windsor, le modèle de conception decorator et comment il était possible de passer d’une implémentation à une autre d’un composant automatiquement en utilisant les define et les if. Dans le prochain article, nous aborderons les factories, les cycles de vie (lifecycle) et les styles de vie (lifestyle).
Références
· Martin Fowler - Inversion of Control Containers and the Dependency Injection pattern - 2004
· Hamilton Verissimo - Introducing Castle, Part I - 2004
· Oren Eini - Inversion of Control and Dependency Injection: Working with Windsor Container - 2006
· Alex Henderson - Container Tutorials - 2007
· Castle Windsor Container documentation
Classé dans : Castle |
salut
Merci pour la traduction.
Merci
C’est juste chiant que le code soit faux a cause de LiveWriter… J’espère avoir du tps pour corriger ça bientôt.