Main Contents

Inversion de contrôle et injection de dépendances avec Castle Windsor – Partie 1

mars 30, 2008

Cet article n’est qu’une traduction de l’excellent article écrit par Simone Busoli que vous trouverez ici.

L’Inversion de Contrôle (IoC) et l’injection de dépendances (DI) sont deux pratiques connexes du développement de logiciels qui sont connues pour conduire à une meilleure testabilité et maintenabilité des produits. Bien que certaines personnes les emploi déjà quotidiennement dans leur travail, beaucoup d’autres ne les connaissent pas, principalement car elles impliquent un changement assez radical dans le processus d’analyse habituel.

Cet article vise à introduire ces notions d’une manière simple et abordable, avec un exemple concret qui évoluera étape par étape afin de tirer pleinement avantage de l’IoC et de la DI, en tirant profit des fonctionnalités offertes par un Framework open source nommé Castle Project.

Introduction

L’Ioc et la DI sont encore des sujets assez obscurs dans le développement logiciel .NET, principalement parce qu’elles requièrent un peu d’efforts au début pour comprendre comment les utiliser; et peut-être également parce qu’elles n’ont jamais été vraiment poussées ou implémentées par Microsoft jusqu’à récemment avec son Framework open source ObjectBuilder. Malgré tout, la compréhension de ces notions n’est pas une perte de temps car elles conduisent sans aucun doutes à des applications mieux structurées et plus modulaires, testables et maintenables plus facilement. Une fois qu’ils les ont découvert, les développeurs ne reviennent plus à leurs anciennes habitudes de développement.

De plus, il existe de nombreux projets et Frameworks open source fournissant ces fonctionnalités directement. Dans cet article, nous examinerons comment tirer profit de l’IoC et de la DI en utilisant Castle Project. D’autres implémentations existent, comme celles fournies par Spring.NET et StructureMap. Le choix est une question de préférences individuelle et des besoins spécifiques de l’application développée qui peut bénéficier des autres fonctionnalités fournies par ces Frameworks.

StructureMap est le plus léger et fournit uniquement un conteneur d’IoC. Castle Project fournit d’autres fonctionnalités comme la persistance de données, un Framework MCV pour le développement d’applications web; et est développé très activement. Enfin, Spring.NET est le plus large et le plus mature car il s’agit d’un portage du SpringFramework de Java; mais la plupart du temps, vous n’aurez pas besoin de toutes ses fonctionnalités.

Dans cet article, nous utiliserons l’implémentation d’IoC proposée par Castle Project car je trouve sa syntaxe plus intuitive et proposant quelques fonctionnalités additionnelles intéressantes non fournies par les autres Frameworks. Rappelez-vous que le choix est principalement une question de goûts personnels.

Pré-requis de l’application d’exemple

Afin d’approcher les concepts de l’IoC et de la DI, nous utiliserons un exemple simple d’interactions entre des objets, tout d’abord en le résolvant de manière traditionnelle et simple puis en utilisant l’IoC et DI pour montrer comment la structure de l’application gagne en flexibilité ainsi que dans toutes les caractéristiques mentionnées dans les paragraphes précédents. Les pré-requis sont d’écrire une application capable de récupérer le titre d’un document HTML téléchargé depuis internet. Les étapes sont assez simples : nous devons télécharger le fichier depuis le réseau en utilisant le protocole HTTP puis l’analyser pour en extraire le texte contenu dans le tag title.

A propos de l’IoC et de la DI

Nous ne creuserons pas trop les aspects formels de ces deux modèles car je souhaite que cet article soit une introduction et un guide rapide pour bien comprendre les sujets abordés. Pour une description plus détaillée, veuillez vous référer aux références incluses en bas de page. Je vais plutôt décrire comment l’IoC et la DI devraient être approchés par les développeurs qui n’ont jamais travaillé avec ces concepts.

Premièrement, ces modèles sont dits basés sur le principe Hollywood : « ne nous appelez pas, nous vous appellerons ». Avec une approche traditionnelle simple, vous écririez en dur dans votre code les classes des objets que vous souhaitez instancier dans les sources de votre application, en fournissant les paramètres à leurs constructeurs et en gérant leurs interactions. Chaque objets saura à la compilation quelles sont les véritables classes des objets avec lesquels ils doivent interagir et ils les appelleront directement. Ainsi, de ce point de vue, vous et vos objets êtes ceux appelés Hollywood. Pour inverser cette approche, vous avez besoin d’un Framework qui rendra votre application assez intelligente pour déterminer les objets à instancier, comment les instancier et en général, comment contrôler leur comportement. Au lieu de travailler avec des classes concrètes, vous travaillerez avec des abstractions telles que des interfaces ou des classes abstraites, conduisant votre application à décider des classes concrètes à utiliser et à déterminer comment satisfaire à leurs dépendances à d’autres composants. Ce concept peut paraître étrange au début mais vous verrez rapidement qu’il a beaucoup de sens.

Créer un simple analyseur de page web

En suivant les pré-requis de l’application d’exemple, écrivons une classe capable de les satisfaire. Nous l’appellerons HtmlTitleRetriever et elle exposera une seule méthode nommée GetTitle, qui accepte en paramètre l’Uri d’un fichier et retourne une chaîne de caractères contenant le titre du document HTML, si celui-ci en a un, ou une chaîne vide.

public class HtmlTitleRetriever
{
	public string GetTitle( Uri file )
	{
		string fileContents;
		string title = string.Empty; 

		WebClient client = new WebClient();
		fileContents = client.DownloadString( file ); 

		int openingTagIndex = fileContents.IndexOf( "" );

		if( openingTagIndex != -1 && closingTagIndex != -1 )
		{
			title = fileContents.Substring( openingTagIndex, closingTagIndex - openingTagIndex ).SubString( 7 );
		}

		return title;
	}
}

Ce que fait cette classe est très simple. Tout d’abord, elle instancie un objet de type WebClient, une façade facilitant l’utilisation des classes HttpWebRequest et HttpWebResponse. Puis, elle utilise cet objet pour récupérer le contenu d’une ressource distante via le protocole HTTP. Enfin, en utilisant les méthodes de la classe String, elle cherche les tags title d’ouverture et de fermeture et extrait le texte entre les deux.

Vous devez maintenant vous demandez ce qu’il y a de si mauvais dans cette classe pour nécessiter une approche différente. En fait, pas grand-chose, tant que les pré-requis restent aussi simples. Mais d’un point de vue plus général, il y a au moins deux aspects qui doivent être revus dans cette implémentation :

· La classe fait plus qu’elle ne devrait. Un des principes d’un bon design est ce que l’on appelle la SoC (Separation of Concerns ou séparation des responsabilités). Suivant ce principe, un composant devrait toujours être capable d’effectuer une seule tache simple, et le faire bien. Ici, cette classe télécharge le fichier et l’analyse afin de récupérer les données auxquelles elle s’intéresse. Ce sont deux taches très différentes et elles devraient être séparées dans deux composants différents.

· Que ce passera t’il si cette classe doit récupérer des documents non accessibles via le protocole HTTP ? Vous devrez changer l’implémentation de la classe pour remplacer ou ajouter cette fonctionnalité. Les mêmes considérations s’appliquent à l’analyse du document. Dans cet exemple, cela n’a pas beaucoup de sens mais dans certaines circonstances, utiliser un mécanisme d’analyse différent conduira à de meilleures performances. En d’autres termes, cette classe à une connaissance trop large (comprenez dépendances) des implémentations concrètes des autres composants. Il est bon d’éviter ceci car cela conduit à une mauvaise architecture d’application.

Composants et Services

Comme nous allons implémenter la solution en utilisant Castle Project, nous allons adopter la même terminologie que dans sa documentation. Je me reporte à la définition exacte donnée par Hamilton Verissimo (l’administrateur de Castle) dans son article d’introduction à Castle (référencé en bas de page) :

« Un composant est une petite unité de code réutilisable. Il ne devrait implémenter et exposer qu’un seul et unique service et le faire bien. En pratique, un composant est une classe qui implémente un service (interface). Cette interface est le contrat de ce service qui crée une couche d’abstraction afin que vous puissiez remplacer l’implémentation dudit service sans efforts. »

Composants et Services sont les concepts de base dont vous aurez besoin en travaillant avec Castle. Avec ceci en tête, nous allons maintenant faire un petit pas vers une meilleure architecture d’application.

Appliquer la SoC avec des Composants et des Services

Jusqu’ici, vous avez vu que les responsabilités de la classe HtmlTitleRetriever peuvent, et doivent, être séparés en deux classes : une pour récupérer les fichiers et une autre pour analyser leurs contenus.

Notez que ce sont des taches assez génériques qui peuvent être implémentées de bien des façons. L’implémentation ci-dessus ne représente qu’un des choix possibles, mais vous pouvez certainement penser à d’autres méthodes pour récupérer des fichiers via d’autres médiums, ainsi qu’à d’autres mécanismes pour extraire le contenu du tag title. En d’autres termes, ces taches sont fournies par un Service et peuvent être effectuées de différentes manières. Les classes concrètes qui effectuent les taches sont les Composants. Les contrats pour le téléchargement et l’extraction du titre peuvent être définis via des interfaces que nous nommerons IFileDownloader et ITitleScraper.

public interface IFileDownloader
{
	string Download( Uri file );
}

public interface ITitleScraper
{
	string Scrape( string fileContents );
}

Implémentons maintenant ces services avec des classes concrètes, des composants, qui fourniront les mêmes fonctionnalités que la classe HtmlTitleRetriever originale.

public class HttpFileDownloader : IFileDownloader
{
	public string Download( Uri file )
	{
		return new WebClient().DownloadString( file );
	}
}

public class StringParsingTitleScraper : ITitleScraper
{
	public string Scrape( string fileContents )
	{
		string title = string.Empty;

		int openingTagIndex = fileContents.IndexOf( "" );

		if( openingTagIndex != -1 && closingTagIndex != -1 )
		{
			title = fileContents.SubString( openingTagIndex, closingTagIndex - openingTagIndex ).SubString( 7 );
		}

		return title;
	}
}

Ces composants satisfont complément les pré-requis de l’application. Maintenant, ils doivent être assemblés afin de fournir les services de téléchargement et d’analyse ensembles. Modifions donc la classe originale afin de bénéficier de leurs fonctionnalités. Cette fois, la classe ne doit plus être au courant des implémentations concrètes des services. Elle doit juste savoir que quelqu’un lui fournira ces services et elle pourra simplement les utiliser. La nouvelle classe HtmlTitleRetriever ressemble maintenant à ceci :

public class HtmlTitleRetriever
{
	private readonly IFileDownloader downloader;
	private readonly ITitleScraper scraper;

	public HtmlTitleRetriever( IFileDownloader downloader, ITitleScraper scraper )
	{
		this.downloader = downloader;
		this.scraper = scraper;
	}

	public string GetTitle( Uri file )
	{
		string fileContents = downloader.Download( file );

		return scraper.Scrape( fileContents );
	}
}

Vous pouvez voir qu’elle fournit maintenant un constructeur acceptant en paramètres deux références aux composants qui fournissent les services définis plus haut. Encore une fois, notez qu’elle n’a aucune connaissance de la façon dont ces services effectuent leur travail. Elle sait seulement qu’ils le font et, heureusement, ils le font bien.

Nous voyons que le problème précédent est résolu mais instancier la classe HtmlTitleRetriever est devenu plus compliqué qu’avant car elle à désormais des dépendances à satisfaire. Voici ce que vous devez faire pour en créer une instance :

IFileDownloader downloader = new HttpFileDownloader();

ITitleScraper scraper = new StringParsingTitleScraper();

HtmlTitleRetriever retriever = new HtmlTitleRetriever( downloader, scraper );

C’est plus de travail que vous ne souhaitez en faire, évidement, et imaginez un moment une classe exposant un service et ayant beaucoup plus de dépendances à satisfaire, qui a leur tour, ont également des dépendances à d’autres services. Cela deviendrait un cauchemar très rapidement. C’est là que l’IoC et la DI arrivent à notre secours. Croyez-le ou non, vous serez capables d’instancier la classe HtmlTitleRetriever sans fournir à son constructeur les dépendances dont elle a besoin. Quelqu’un le fera pour vous.

Approcher l’IoC et la DI

Gérer la création d’objets et leur destruction en utilisant l’IOC et la DI requièrent en fait moins de magie que vous le croyez. Alors, qui est va s’occuper de gérer les objets si ce n’est plus vous ?

Le point principal d’un Framework offrant IoC et DI est un composant appelé Container. Comme son nom l’indique, le conteneur aura connaissance des composants nécessaires à votre application et essaiera d’être assez intelligent pour comprendre quels composants vous souhaitez. Cela arrivera quand vous lui demanderez de vous fournir une instance d’un des composants qu’il contient. C’est ce que signifie IoC en pratique : vous n’instanciez plus les classes en utilisant leurs constructeurs, vous les enregistrez dans le conteneur puis vous lui demandez de vous fournir une instance d’un composant. Cela peut être fait de plusieurs manières et avec beaucoup d’options de configuration que vous verrez plus tard dans le prochain article.

L’autre fonctionnalité fondamentale du conteneur est d’être capable de résoudre et d’injecter les dépendances entre vos objets, d’où le terme Injection de dépendances. Dans l’application d’exemple, le conteneur sera assez intelligent pour déterminer qu’afin d’instancier un objet de type HtmlTitleRetriever, il doit d’abord instancier des composants fournissant les services IFileDownloader et ItitleScraper.

Castle Microkernel

Le conteneur IoC de Castle est offert en deux versions. Le MicroKernel, est un conteneur léger qui offre les fonctionnalités principales de l’IoC et de la DI. Windsor fournit un conteneur construit par-dessus le MicroKernel et qui étend les ses fonctionnalités en ajoutant le support d’une configuration externe ainsi que les intercepteurs. La plupart du temps, vous utiliserez Windsor, mais pour garder les choses simples, commençons par illustrer l’utilisation de l’IoC et de la DI avec le MicroKernel.

Ci-dessous, le code nécessaire pour configurer une application afin d’utiliser la classe HtmlTitleRetriever via le MicroKernel.

IKernel kernel = new DefaultKernel();

kernel.AddComponent( "HttpFileDownloader", typeof( IFileDownloader ), typeof( HttpFileDownloader ) );
kernel.AddComponent( "StringParsingTitleScraper", typeof( ITitleScraper ), typeof( StringParsingTitleScraper ) );
kernel.AddComponent( "HtmlTitleRetriever", typeof( HtmlTitleRetriever ) );

HtmlTitleRetriever retriever = ( HtmlTitleRetriever ) kernel[ typeof( HtmlTitleRetriever ) ];

string title = retriever.GetTitle( new Uri( "some uri..." ) );

kernel.ReleaseComponent( retriever );

Les étapes impliquées dans la configuration et l’utilisation de l’API du MicroKernel sont décrites ci-dessous :

· Une nouvelle instance du conteneur est crée.

· Un par un, les composants sont enregistrés dans le conteneur en utilisant la méthode AddComponent, qui exposent plusieurs variantes. Le premier paramètre est la clef utilisée pour identifier le composant. Nous spécifions ensuite le type de l’interface du service et le type de l’implémentation que nous souhaitons utiliser. Dans le cas du HtmlTitleRetriever, nous spécifions uniquement son type car il n’implémente pas d’interface.

· Une instance de la classe HtmlTitleRetriever est obtenue depuis le conteneur en spécifiant son type. Vous pourriez également utiliser sa clef. A ce moment, le conteneur voit que pour instancier la classe, il doit fournir les deux paramètres de son constructeur. Notant que les paramètres sont de type IFileDownloader et ITitleScraper respectivement, il réalise que deux composants implémentant ces interfaces sont déjà enregistrés. Il les instancie et les fournit au constructeur de la classe HtmlTitleRetriever.

· Après avoir utilisé l’instance du HtmlTitleRetriever, vous devriez la détruire en appelant la méthode ReleaseComponent du conteneur. Cela n’est pas obligatoire car le conteneur le fera automatiquement dans les situations les plus communes, mais il y a des cas ou vous souhaiterez avoir un contrôle plus fin sur le cycle de vie des composants et voudrez décider quand les détruire.

Castle Windsor Container

Même si le MicroKernel fournit assez de fonctionnalités pour cet exemple très simple, le conteneur de Windsor est généralement plus adapté aux applications nécessitant une approche plus flexible de la configuration du conteneur et une API plus facile à utiliser. Le code suivant montre comment configurer notre application avec le conteneur de Windsor :

IWindsorContainer container = new WindsorContainer();

container.AddComponent< IFileDownloader, HttpFileDownloader >();
container.AddComponent< ITitleScraper, StringParsingTitleScraper >();
container.AddComponent< HtmlTitleRetriever >();

HtmlTitleRetriever retriever = container.Resolve< HtmlTitleRetriever >();

string title = retriever.GetTitle( new Uri( "some uri..." ) );

container.Release( retriever );

Comme vous pouvez le voir, l’API est très similaire. C’est parce que Windsor n’est pas un autre conteneur mais est construit par-dessus le MicroKernel et l’enrichit en ajoutant quelques fonctionnalités. L’une de ces fonctionnalités est de pouvoir enregistrer et récupérer un composant en utilisant les génériques, ce qui évite les cast.

Jusqu’ici, vous avez vu comment configurer le conteneur par code. Dans une véritable application, vous devriez écrire beaucoup de code pour configurer le conteneur. Changer quoi que ce soit nécessiterais de reconstruire la solution. Windsor offre une nouvelle fonctionnalité vous permettant de configurer le conteneur en utilisant un fichier de configuration XML, tels que ceux que vous utilisez avec une application .NET standard. Réécrivons donc le code pour utiliser une configuration externe.

Tout d’abord, vous devez créer un fichier de configuration, nommé App.config ou Web.config suivant le type d’applications que vous développer. Notez que Windsor peut également lire sa configuration depuis d’autres sources. Le fichier de configuration de l’application n‘est qu’une des options disponibles.

< ?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>

Tout d’abord, le gestionnaire de section pour la configuration de Windsor doit être enregistré dans la section configSections. Puis, la configuration prend place dans la section castle. Elle est similaire à ce que nous avons fait par code précédemment. La syntaxe est la suivante :

· id (requis) : un nom utilisé pour identifier le composant

· service : le contrat implémenté par le composant

· type : le type du composant concrètement utilisé

Les attributs service et type requièrent un type complètement qualifié (namespace.typename) ainsi que le nom de l’assembly après la virgule.

Vous avez probablement noté que la classe HtmlTitleRetriever est enregistrée sans fournir de service. En fait, elle n’implémente aucune interface et n’a pas de classe de base car il est peu probable que vous en fournissiez une implémentation différente. Les deux autres composants, au contraire, sont des implémentations concrètes de services pouvant être implémentés de différentes manières. Cette syntaxe vous permet d’enregistrer plusieurs composants pour le même service. Par défaut, quand le conteneur trouve plusieurs implémentations du même service, il fournira le premier composant enregistré mais ce comportement peut être modifié en appelant le composant via son identifiant plutôt que son interface. Voici maintenant le code de l’application utilisant cette configuration :

IWindsorContainer container = new WindsorContainer( new XmlInterpreter() );

HtmlTitleRetriever retriever = container.Resolve< HtmlTitleRetriever >();

string title = retriever.GetTitle( new Uri( "some address..." ) );

container.Release( retriever );

La principale différence est que le constructeur du conteneur est maintenant appelé avec une instance de la classe XmlInterpreter, qui, par défaut, lit la configuration depuis le fichier de configuration de l’application.

Tirer avantage de l’IoC et de la DI

Jusqu’à présent, vous avez vu comment passer d’une programmation traditionnelle simple à l’inversion de contrôle. Maintenant, passons à l’étape suivante dans la compréhension des bénéfices de l’IoC pour une application.

Supposez que les pré-requis de votre application changent et que vous deviez récupérer les fichiers non plus via le protocole HTTP mais via le protocole FTP. Avec notre première approche, vous deviez réécrire la section de code de la classe HtmlTitleRetriever. Ce ne serait pas beaucoup de travail pour notre exemple, mais dans une application d’entreprise, cela pourrait signifier énormément de travail. Comment ferions-nous avec Windsor ?

Premièrement, nous créerions une nouvelle classe implémentant l’interface IFileDownloader qui récupérerait les fichiers via FTP. Puis, nous l’enregistrerions dans le fichier de configuration en remplaçant la version HTTP. Vous ne changeriez donc pas une seule ligne de code dans votre application et ne devriez donc pas la recompiler puisque vous pourriez fournir la nouvelle implémentation dans un autre assembly. En fait, les fonctionnalités de Windsor sont beaucoup plus intelligentes que cela mais ce sera le sujet d’un autre article.

Conclusion

Dans cet article, vous avez vu ce que sont l’IoC et le DI et comment ils permettent d’obtenir une meilleure architecture d’application. Vous avez vu comment en tirer avantage en utilisant le conteneur Windsor fourni par le projet Castle. Dans le prochain article, nous examinerons plus en détails les fonctionnalités du conteneur et comment il rendra nos applications plaisantes à écrire, à tester et à maintenir.

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 |

Flux RSS de l'article | Trackback URI

0 Commentaire »

Pas de commentaires.

Nom
E-mail
Site Web
Taille du champ texte (Diminuer | Augmenter)


Vous pouvez utiliser <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong> dans vos commentaires.


Fermer
E-mail It