Inversion de contrôle et injection de dépendances avec Castle Windsor – Partie 2
avril 29, 2008
Cet article n’est qu’une traduction de l’excellent article écrit par Simone Busoli que vous trouverez ici.
Étant donné que le premier article ne montrait qu’une petite partie des fonctionnalités de Castle Windsor, nous allons reprendre la précédente discussion en étendant les pré requis de l’exemple afin de montrer comment l’IoC gère les changements et quel est son véritable potentiel.
Dans l’article précédent nous avons introduit les concepts d’Inversion de contrôle et d’injection de dépendances, et avons montré comment bénéficier de leur usage en développant une application simple. Bien que cet exemple naïf ne profitait pas de tous les avantages offerts par l’IoC et la DI comme le ferait une véritable application, il permettait aux développeurs qui découvraient ces concepts de les aborder simplement et de la bonne manière.
Introduction
Dans l’article précédent, nous n’avons abordé qu’une petite partie des fonctionnalités offertes par le Conteneur Windsor. Cette fois, nous allons introduire de façon incrémentielle de petites modifications aux requis de l’application afin de démontrer comment Windsor permettra de les aborder.
Jusqu’ici, vous avez vu comment vous pouviez indiquer au conteneur comment récupérer et instancier des composants, ainsi que comment résoudre les dépendances de leurs constructeurs. Le code ci-dessous est la signature du constructeur de la classe HtmlTitleRetriever telle que nous l’avons défini à la fin de l’article précédent:
public HtmlTitleRetriever( IFileDownloader downloader, ITitleScraper scraper )
Ainsi, la classe HtmlTitleRetriever a une dépendance sur deux références à des objets implémentant les interfaces IFileDownloader et ITitleScraper (nous les appellerons des services) devant être satisfaite pour pouvoir l’instancier. Avec une approche canonique, vous devriez créer ces deux références et les fournir au constructeur. En utilisant Windsor, vous pouvez résoudre les références en les déclarant dans le fichier de configuration comme le montre le code ci-dessous:
< ?xml version="1.0" encoding="utf-8" ?> <configuration> <configsections> <section type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" name="castle" /> </configsections> <castle> <components> <component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample"></component> <component id="StringParsingTitleScraper" type="WindsorSample.StringParsingTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, WindsorSample"></component> <component id="HttpFileDownloader" type="WindsorSample.HttpFileDownloader, WindsorSample" service="WindsorSample.IFileDownloader, WindsorSample"></component> </components> </castle> </configuration>
Injection de constructeur et dépendances obligatoires
IWindsorContainer container = new WindsorContainer( new XmlInterpreter() ); HtmlTitleRetriever retriever = container.Resolve< HtmlTitleRetriever >();
Quand une instance de la classe HtmlTitleRetriever est requise comme dans le code ci-dessous, le conteneur est capable de déduire qu’afin d’instancier cette classe, il doit fournir ses dépendances au constructeur. L’instanciation est réussie uniquement parce que ces dépendances sont satisfaites par les deux autres composants déclarés dans le fichier de configuration. Si un seul d’entre eux n’était pas enregistré, le conteneur n’aurait pas été capable de les résoudre et aurait lancer une exception de type Castle.MicroKernel.Handlers.HandlerException.
Comme vous pouvez le deviner, les dépendances déclarées en paramètres de constructeur sont obligatoires car vous ne pouvez instancier un composant si vous ne les fournissez pas toutes. L’opération consistant à résoudre et à fournir des dépendances via le constructeur est appelée Injection de constructeur dans les termse d’IoC et n’est qu’une des deux manières de fournir des dépendances à des composants.
Vous devriez vous appuyer sur l’injection de constructeur quand un composant nécessite vraiment un autre composant pour pouvoir fonctionner. Dans d’autres cas, vous préférerez peut-être fournir une implémentation par défaut pour le composant en question, rendant ainsi cette dépendance optionnelle. Nous allons maintenant examiner ce concept.
Injection des setter et dépendances optionnelles
Afin d’illustrer comment gérer les dépendances optionnelles, nous allons effectuer un petit changement dans les pré requis de l’application. La méthode GetTitle de la classe HtmlTitleRetriever doit retourner une chaîne de caractères contenant le titre du document HTML fourni en paramètre (comme avant); mais désormais, ce titre doit être préfixé par un message descriptif, qui peut être personnalisé ou fourni par défaut. Vous pouvez penser que pour satisfaire cette condition, il suffit d’ajouter une propriété nommée AddtionalMessage dont la variable prendrait la valeur par défaut "Titre"; et qui pourrait être modifiée en utilisant le setter de la propriété. Ces modifications sont montrées dans l’extrait de code suivant:
public class HtmlTitleRetriever
{
private readonly IFileDownloader downloader;
private readonly ITitleScraper scraper;
private string additionalMessage = "Title: ";
public string AdditionalMessage
{
get { return additionalMessage; }
set { additionalMessage = value; }
}
public HtmlTitleRetriever( IFileDownloader downloader, ITitleScraper scraper )
{
this.downloader = downloader;
this.scraper = scraper;
}
public string GetTitle( Uri file )
{
string fileContents = downloader.Download( file );
return string.Concat( additionalMessage, scraper.Scrape( fileContents ) );
}
}
Bien sur, ces modifications dans le code ne requierent aucune modification du fichier de configuration du conteneur. L’application fonctionnera correctement si aucune valeur n’est fournie pour la propriété AddtionalMessage, car son getter renverra au moins la valeur fixée sur sa variable lors de l’instanciation.
Maintenant, que se passe t-il si vous souhaitez modifier le message par défaut retourné par la méthode GetTitle en plus du titre du document ? Rappelez-vous, la responsabilité d’instancier la classe HtmlTitleRetriever n’est plus votre mais celle du conteneur.
Pour ce cas et pour d’autres, Windsor offre de nombreuses options de configuration, que nous allons aborder dans la suite de cet article et dans les articles suivants.
Revenons à nos conditions: vous voulez pouvoir fournir une valeur différente de celle par défaut (codée en dur dans la classe) pour la propriété AddtionalMessage. Ceci peut être accompli via les paramètres de configuration des composants, qui peuvent être spécifiés dans le fichier de configuration.
L’extrait de code ci-dessous montre les modifications apportées au fichier de configuration pour satisfaire cette fonctionnalité en spécifiant la valeur de la propriété AddtionalMessage :
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample"> <parameters> <additionalmessage>This is the title of the document:</additionalmessage> </parameters> </component>
Notez la syntaxe utilisée pour spécifier la valeur de la propriété. Dans l’élément component, vous insérez un élément enfant nommé parameters, contenant lui même un autre élément enfant dont le nom est le celui de la propriété que vous souhaitez spécifier.
Le mécanisme consistant à spécifier les dépendances en utilisant les propriétés est appelé Injection de setter.
A la différence de l’injection de constructeur, l’injection de setter concerne les dépendances optionnelles, car la seule contrainte devant être satisfaite lors de l’instanciation d’un objet est que tous ses paramètres de constructeur lui soient fournis. Les dépendances optionnelles, n’empêche pas le conteneur d’instancier un composant. Elles ont habituellement des valeurs par défaut qui peuvent être modifiées en spécifiant une nouvelle valeur dans la configuration.
Une autre chose à noter est que la syntaxe utilisée ci-dessus dans le fichier de configuration peut-être utilisée pour les paramètres optionnels ainsi que pour les paramètres de constructeur. Dans notre exemple, le constructeur de la classe HtmlTitleRetriever peut être résolu automatiquement par le conteneur car les dépendances aux autres composants ont été enregistrées dans la configuration. Si nous souhaitions que la dépendance à la propriété AddtionalMessage soit requise, il nous suffirait de modifier le constructeur en lui ajoutant un troisième paramètre de type string et nommé addtionalMessage. La syntaxe du fichier de configuration ne changerait pas. Nous pourrions modifier le nom de l’élément XML en addtionalMessage par souci de cohérence mais le conteneur n’est pas sensible à la casse. Le seul changement de comportement serait que si ce paramètre n’était pas fourni dans le fichier de configuration, une exception serait lancée par le conteneur.
Revenons à l’injection de setter. Lorsque nous demandons une instance du composant HtmlTitleRetriever, Windsor réalise que nous avons spécifié un paramètre de configuration qui peut être associé à une propriété du composant car celle-ci a un setter. Il va donc instancier la classe HtmlTitleRetriever puis assigner la valeur du fichier de configuration à la propriété avant de nous retourner l’instance demandée. Souvenez-vous que si ce paramètre n’était pas spécifié dans le fichier de configuration, il aurait toujours sa valeur par défaut. Ainsi, prenez bien garde à toujours fournir une valeur par défaut afin d’éviter les références nulles et les valeurs incohérentes.
Injection de composants
Passons maintenant à quelque chose d’encore plus intéressant. En lisant le paragraphe précédent, vous avez certainement remarqué la différence entre la déclaration de dépendances par propriété de type string et celles concernant d’autres composants, comme celles que la classe HtmlTitleRetriever à sur les services IFileDownloader et ITitleScraper. Supposez qu’il vous faille télécharger les fichiers via le protocole FTP plutôt qu’HTTP. Vous devriez créer une nouvelle classe implémentant l’interface IFileDownloader et l’enregistrer dans le fichier de configuration en tant que nouveau composant. Voyant comment le faire :
public class FtpFileDownloader : IFileDownloader
{
public string Download( Uri file )
{
FtpWebRequest request = WebRequest.Create( file ) as FtpWebRequest;
if(request != null)
using( StreamReader reader = new StreamReader( request.GetResponse().GetResponseStream() ) )
return reader.ReadToEnd();
return string.Empty;
}
}
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>This is the title of the document:</additionalmessage>
</parameters>
</component>
<component id="StringParsingTitleScraper" type="WindsorSample.StringParsingTitleScraper, WindsorSample" service="WindsorSample.ITitleScraper, 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>
Vous vous posez certainement la question suivante : comment puis-je passer d’une implémentation à une autre ? Il y a forcément un moyen de spécifier explicitement un composant en particulier. Dans le cas ou aucun composant n’est spécifié explicitement, Windsor choisira le premier composant enregistré dans le fichier de configuration.
La syntaxe pour spécifier explicitement un composant est différente de celle utilisée pour spécifier des valeurs telles que les chaînes de caractères, les nombres, les dates et autres types "simples". Sinon, le conteneur serait incapable de deviner ce que vous souhaitez accomplir. Cette syntaxe utilise le format ${ identifiant du composant }. Voyons maintenant un exemple. Ayant enregistré le composant FtpFileDownloader, vous pouvez choisir de l’utiliser quand vous obtiendrez une instance de la classe HtmlTitleRetriever en ajoutant simplement un nouveau paramètre de configuration au composant HtmlTitleRetriever, comme le montre le code suivant:
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>This is the title of the document:</additionalmessage>
<downloader>${FtpFileDownloader}</downloader>
</parameters>
</component>
Notez ici la nouvelle syntaxe utilisée dans la section parameters. Vous pouvez maintenant passer d’une implémentation à l’autre du service IFileDownloader simplement en référençant l’identifiant de l’implémentation que vous souhaitez utiliser. L’élément XML est nommé downloader car c’est le nom du paramètre pour ce composant dans le constructeur de la classe HtmlTitleRetriever.
Travailler avec des tableaux, des listes et des dictionnaires
Pour le moment, la classe HtmlTitleRetriever n’est pas très utile si nous devons télécharger des fichiers via différents protocoles ou stockés à différents endroits. Pour chaque fichier, vous devriez changer la référence dans le fichier de configuration afin utiliser l’implémentation spécifique au protocole souhaité. Essayons de résoudre ce problème.
Notez que la classe WebClient est tout à fait capable de télécharger un fichier via FTP et HTTP. La classe FtpFileDownloader n’est là que pour illustrer cet exercice.
La classe HtmlTitleRetriever devrait être capable de déduire comment l’adresse de fichier fournie à la méthode GetTitle devrait être traitée. Elle devrait être capable de récupérer le fichier si un composant implémentant IFileDownloader en est capable. Pour qu’elle puisse savoir si un composant supporte l’adresse de fichier fournie, modifions l’interface IFileDownloader :
public interface IFileDownloader
{
string Download( Uri file );
bool SupportsUriScheme( Uri file );
}
La nouvelle méthode retourne un booléen indiquant si le composant est capable de télécharger le fichier correspondant à l’adresse fournie en paramètre. Voyons maintenant les implémentations de cette méthode dans les classes HtmlFileDownloader et FtpFileDownloader.
public class HttpFileDownloader : IFileDownloader
{
public string Download( Uri file )
{
return new WebClient().DownloadString( file );
}
public bool SupportsUriScheme( Uri file )
{
return file.Scheme == "http";
}
}
public class FtpFileDownloader : IFileDownloader
{
public string Download( Uri file )
{
FtpWebRequest request = WebRequest.Create( file ) as FtpWebRequest;
if( request != null )
using( StreamReader reader = new StreamReader( request.GetResponse().GetResponseStream() ) )
return reader.ReadToEnd();
return string.Empty;
}
public bool SupportsUriScheme( Uri file )
{
return file.Scheme == "ftp";
}
}
De cette façon, chaque composant fournissant le service IFileDownloader possède la connaissance de sa capacité à télécharger un fichier spécifique. Vous allez voir très rapidement que cela à du sens.
Revenons maintenant à notre "pas très utile" classe HtmlTitleRetriever. Afin de lui permettre de télécharger des fichiers en utilisant différents protocoles, elle a besoin de références à chaque implémentation du service IFileDownloader. Cela peut être accompli de différentes manières. Voyons comment supporter plusieurs composants grâce à un tableau. Dans le code suivant, le constructeur de la classe accepte désormais un tableau de IFileDownloaders:
public class HtmlTitleRetriever
{
private readonly IFileDownloader[] downloaders;
private readonly ITitleScraper scraper;
private string additionalMessage = "Title: ";
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;
}
}
La méthode GetTitle à changé également. Désormais, elle demande à chaque implémentation du service IFileDownloader si elle est capable de télécharger le fichier souhaité. Une fois le composant requis trouvé, elle lui demande de télécharger le fichier et récupère le titre comme avant. Si aucune implémentation n’a été trouvée pour le fichier, elle retourne une chaîne de caractères vide. Rien de bien nouveau pour le moment mais nous devons modifier le fichier de configuration pour faire savoir à Windsor que la classe doit maintenant être instanciée avec un tableau des implémentations du service IFileDownloader. Voici comment:
<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>
</array>
</downloaders>
</parameters>
</component>
Le code est assez simple à comprendre. Comme pour un paramètre standard, l’élément array accepte n’importe quel type simple en tant que sous-élément item. Comme pour un paramètre standard, si vous souhaitez référencer un autre composant, vous devez utiliser la syntaxe ${ identifiant du composant }.
Vous pouvez passer de la même façon des listes et des dictionnaires. La syntaxe est similaire, comme le montre les exemples suivants. Le premier exemple démontre comment utiliser une liste:
public class HtmlTitleRetriever
{
private readonly List< IFileDownloader > downloaders;
private readonly ITitleScraper scraper;
private string additionalMessage = "Title: ";
public string AdditionalMessage
{
get { return additionalMessage; }
set { additionalMessage = value; }
}
public HtmlTitleRetriever( List< 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;
}
}
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>This is the title of the document:</additionalmessage>
<downloaders>
<list>
<item>${HttpFileDownloader}</item>
<item>${FtpFileDownloader}</item>
</list>
</downloaders>
</parameters>
</component>
L’exemple suivant démontre comment utiliser un dictionnaire:
public class HtmlTitleRetriever
{
private readonly IDictionary< string, IFileDownloader > downloaders;
private readonly ITitleScraper scraper;
private string additionalMessage = "Title: ";
public string AdditionalMessage
{
get { return additionalMessage; }
set { additionalMessage = value; }
}
public HtmlTitleRetriever( IDictionary< string, IFileDownloader > downloaders, ITitleScraper scraper )
{
this.downloaders = downloaders;
this.scraper = scraper;
}
public string GetTitle( Uri file )
{
foreach( IFileDownloader downloader in downloaders.Values )
if( downloader.SupportsUriScheme( file ) )
return string.Concat( additionalMessage, scraper.Scrape( downloader.Download( file ) ) );
return string.Empty;
}
}
<component id="HtmlTitleRetriever" type="WindsorSample.HtmlTitleRetriever, WindsorSample">
<parameters>
<additionalmessage>This is the title of the document:</additionalmessage>
<downloaders>
<dictionary>
<item key="http">${HttpFileDownloader}</item>
<item key="ftp">${FtpFileDownloader}</item>
</dictionary>
</downloaders>
</parameters>
</component>
Notez qu’en utilisant un dictionnaire, nous pourrions omettre la méthode SupportsUriScheme car nous pourrions utiliser la clé du dictionnaire pour récupérer l’implementation d’un protocole spécifique.
Conclusion
Dans cet article, nous avons vu comment gérer des pré requis plus évolués grâce à la configuration du conteneur Windsor. En particulier, nous avons vu les dépendances optionnelles et requises et comment les associer aux propriétés et constructeurs ainsi que comment passer d’une implémentation à une autre d’un composant en utilisant une syntaxe spécifique offerte par Windsor. Enfin, nous avons rapidement abordé l’utilisation de tableaux, de listes et de dictionnaires. Dans le prochain article, nous examinerons plus en détails les options de configuration et introduirons quelques concepts avancés tels que les décorateurs (decorators) et les style de vie (lifestyles).
Classé dans : Castle |
Pas de commentaires.