Bewise

Nous développons... votre avance

Le Pattern Provider

SLF
18/04/2007 - Jean-Pierre Riehl
Télécharger la version Word
Télécharger les sources

 

1 Introduction

La présentation de ce type de pattern de conception est un sujet maintes fois traité et qui fait souvent débat. Cet article reprend le problème à sa base et aboutit au développement d’un pattern complet, générique et réutilisable.

2 Principe

L’une des règles principales dans une bonne conception est d’avoir un faible couplage. La programmation objet permet de respecter cette règle en introduisant des abstractions. On cherche à abstraire des comportements, des services. On peut aussi bien abstraire une couche applicative ou une implémentation. Ces abstractions permettent de découpler des classes, des modules voire des applications entières.

Le pattern Provider va nous permettre de modéliser cette abstraction de façon générique.

Le principe de base est simple ; on considère un  « fournisseur » et un « consommateur ». Le consommateur doit pouvoir utiliser un fournisseur sans avoir à se préoccuper des spécificités de celui-ci (instanciation, configuration, etc.).

image

Il faut bien sûr que le fournisseur soit définit par un contrat qui fixe ce qu’il doit faire. Pour cela, nous utilisons au choix une interface ou une classe de base.

image

La problématique est comment masquer la tuyauterie qui permettra au consommateur de manipuler le fournisseur au travers de son contrat.

3 Utilisation

Quelle utilisation peut-on bien faire d’une telle abstraction ?

On peut avoir besoin de disposer de différentes implémentations d’un même fournisseur. Un des exemples les plus classiques, et tant de fois répété, est un fournisseur d’accès aux données. On peut avoir une implémentation pour SQL Server et une autre pour Oracle.

image

On peut aussi avoir besoin de plusieurs versions (instances) du même fournisseur à l’exécution. Chaque instance disposant d’un contexte différent. Par exemple, plusieurs instances d’un fournisseur de Trace écrivant dans des fichiers, chacune pointant sur un fichier différent.

image

On peut aussi avoir besoin d’une combinaison des deux, c'est-à-dire avoir plusieurs instances à l’exécution, chaque instance étant une implémentation différente avec un contexte particulier. Par exemple, on peut avoir besoin de traiter des messages « blancs » et des messages « noirs » de façon différentes mais au sein d’un même processus.

image

Vous aurez noté que je n’ai pas suffixé ces « fournisseurs » par Provider pour bien montrer que ce pattern s’étend à de nombreux usages.

On note les utilisations présentées ci-dessus avec des cardinalités :

1-n

Une seule instance – plusieurs implémentations

n-1

Plusieurs instances – une seule implémentation

n-n

Plusieurs instances – plusieurs implémentations

Le cas d’avoir une seule implémentation et une seule instance n’est pas évoqué car il est trivial et ne nécessite pas de conception particulière. En effet, il suffit que le consommateur référence tout simplement le fournisseur.

Nous allons partir du principe de qui peut le plus peut le moins et se baser sur la cardinalité n-n.

4 La proposition de Microsoft

Pendant la conception d’ASP.NET 2.0, sont apparus de nombreux starter kits. Rob Howard, un des pères d’ASP.NET y a dévoilé une implémentation du pattern Provider (http://weblogs.asp.net/rhoward/archive/2004/03/02/83026.aspx). Depuis, ce pattern fait partie intégrante des fonctionnalités de ASP.NET 2.0 à savoir les Memberships, les Roles et les Profiles.

Il est trivial pour un développeur web d’avoir à configurer un SqlMembershipProvider, un WindowsTokenRoleProvider ou d’implémenter le sien.

Mais ce n’est pas tout, le Framework 2.0 offre une base de travail pour implémenter son propre provider.

public abstract class DotNetProviderBase : System.Configuration.Provider.ProviderBase { public abstract string Talk(); }

public class DotNetProviderImplementation : DotNetProviderBase { public override string Talk() { return "Ceci est une implémentation du provider avec la classe de base du Framework"; } }

Mais l’implémentation du Framework apporte quelques lacunes :

  • Utilisation d’une classe de base : ce qui interdit l’utilisation de sa propre hiérarchie de classe. De plus, la classe de base du Framework se trouve dans System.Configuration.dll (System.Configuration.Provider.ProviderBase) ce qui implique un couplage avec l’Assembly de configuration.
  • Il n’y a aucune Factory (routines d’instanciation) qui permette de récupérer une instance du provider. Il existe bien une routine qui se trouve dans l’Assembly System.Web.dll : System.Web.Configuration.ProvidersHelper.InstantiateProviders(). Cela implique un couplage avec l’Assembly System.Web.dll.
Configuration.ProviderConfigurationSection section = (Configuration.ProviderConfigurationSection)System.Configuration.ConfigurationManager.GetSection("DotNetImplementation"); ProviderSettings settings = section.Providers[section.DefaultProvider]; System.Web.Configuration.ProvidersHelper.InstantiateProviders(section.Providers, allProviders, typeof(DotNetProviderBase)); DotNetProviderBase p = allProviders[section.DefaultProvider] as DotNetProviderBase; return p;

Bien que le Framework permette de développer ses propres providers, cela nécessite quelques lignes de code et force à un couplage à des assemblages dont on n’a pas nécessairement besoin.

5 Une solution alternative

Nous allons maintenant développer une solution alternative qui a pour dessein de combler les lacunes évoquées ci-dessus.

Le propos de l’exemple est un « fournisseur de discussion », à savoir un TalkProvider qui doit fournir une méthode Talk.

image

Nous allons partir de la configuration que l’on souhaiterait avoir pour notre provider, puis nous allons détailler le mécanisme d’instanciation. Enfin nous enrichirons le modèle.

Bien sûr, nous allons utiliser les Generics pour permettre une réutilisation maximale.

5.1 Configuration

Définissons à quoi doit ressembler la configuration de notre provider :

<TalkProvider> <providers defaultProvider="blabla"> <add name="blabla" type="Bewise.Patterns.Consumer.InterfaceImplementation.MyTalkInterfaceProvider, Bewise.Patterns.Consumer" param="1234"/> </providers> </TalkProvider>

Le format de la configuration est le même que celui d’un provider du Framework à la différence que la définition du provider par défaut est portée ici par le nœud <providers/>.

Les classes qui implémentent la configuration sont :

  • la section de configuration : ProviderSection
  • la collection de nœuds provider : ProviderCollectionElement
  • et l’élément provider lui-même : ProviderElement

Le but n’étant pas de présenter l’API de configuration du Framework 2.0, je vous livre le code tel quel.

class ProviderSection : ConfigurationSection { public static ProviderSection GetCurrent(string providerName) { return (ProviderSection)System.Configuration.ConfigurationManager.GetSection(providerName); } [ConfigurationProperty("providers", IsRequired = true)] public ProviderCollectionElement Providers { get { return (ProviderCollectionElement)base["providers"]; } } } [ConfigurationCollection(typeof(ProviderElement))] class ProviderCollectionElement : ConfigurationElementCollection { [ConfigurationProperty("defaultProvider", IsRequired = false)] public string DefaultProvider { get { return (string)base["defaultProvider"]; } set { base["defaultProvider"] = value; } } protected override ConfigurationElement CreateNewElement() { return new ProviderElement(); } protected override object GetElementKey(ConfigurationElement element) { return element.ToString(); } } class ProviderElement : ConfigurationElement { [ConfigurationProperty("name", IsRequired = true, IsKey = true)] public string Name { get { return (string)this["name"]; } set { this["name"] = value; } } [System.ComponentModel.TypeConverter(typeof(TypeNameConverter))] [ConfigurationProperty("type", IsRequired = true)] public Type Type { get { return (Type)this["type"]; } set { this["type"] = value; } } }

L’un des points forts du pattern Provider du Framework est la notion de paramétrage sous la forme de paires clés/valeurs dans la configuration. Cela permet de « contextualiser » l’instance du provider. On peut citer l’exemple trivial de la chaîne de connexion à une source de données mais il n’y a qu’à regarder la configuration du SqlMembershipProvider pour comprendre la portée du paramétrage de son instance.

<add name="MonMemberShip" type="System.Web.Security.SqlMembershipProvider" connectionStringName="SqlLocal" enablePasswordRetrieval="true" enablePasswordReset="false" requiresQuestionAndAnswer="false" applicationName="Whisky" requiresUniqueEmail="true" passwordFormat="Encrypted" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="5" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"/>

On peut gérer cela avec en implémentant la méthode OnDeserializeUnrecognizedAttribute() d’un élément de configuration (classe ProviderElement). Attention, les paramètres ne sont pas typés, ce ne sont que des String.

private NameValueCollection settings = new NameValueCollection(); protected override bool OnDeserializeUnrecognizedAttribute(string name, string value) { settings.Add(name, value); return true; }

Ces paramètres seront passés au provider lors de son instanciation.

5.2 Instanciation

Nous savons comment récupérer les informations nécessaires à la construction de notre provider. Il faut maintenant l’instancier. Cette instanciation doit se faire dynamiquement à l’exécution, c’est la condition pour éviter le couplage à la compilation. Pour cela nous utilisons l’instruction Activator.CreateInstance() présentée ci-dessous.

Configuration.ProviderElement providerDeclaration; TalkProvider providerInstance = (TalkProvider)Activator.CreateInstance(providerDeclaration.Type);

Le type concret à instancier étant uniquement défini dans la configuration, TalkProvider doit être soit une interface soit une classe de base. Mais allons plus loin. Nous n’avons même pas besoin de connaître l’interface du Provider. Nous allons l’abstraire elle aussi en utilisant un type Generic.

J’ai encapsulé les routines d’instanciation dans une classe Factory (encore un pattern J). Cette factory peut être plus ou moins complexe. Elle peut par exemple intégrer les fonctionnalités suivantes :

  • Charger les providers à la volée (Lazy Loading)
  • Mettre en cache les providers
  • Intégrer un délégué permettant de sélectionner le bon provider

L’exemple présenté ci-après ne montre qu’une méthode permettant de charger tous les providers en une opération.

public class ProviderFactory<T> { Configuration.ProviderSection section; public static void LoadProviders() { string defaultProviderName = section.Providers.DefaultProvider; foreach (Configuration.ProviderElement providerDeclaration in section.Providers) { try { T providerInstance = (T)Activator.CreateInstance(providerDeclaration.Type); allProviders.Add(providerDeclaration.Name, providerInstance); } catch (System.Reflection.TargetInvocationException tEx) { throw new ApplicationException(string.Format("Impossible de charger le provider {0} de type {1}. Voir InnerException pour plus de détails.", providerDeclaration.Name, providerDeclaration.Type), tEx.InnerException); } } } }

La récupération de l’exception TargetInvocationException permet d’intercepter les erreurs lors de l’instanciation (erreur dans le constructeur) du provider.

L’exemple ci-dessus n’est pas complet puisqu’on ne récupère pas la section de configuration qui porte les informations sur les providers. Comment récupérer la bonne section de configuration sans mettre en dur son nom dans le code ? Pour cela, il suffit simplement d’utiliser le type générique du provider.

section = Configuration.ProviderSection.GetCurrent(typeof(T).Name);

5.3 Passage des paramètres du provider

Le provider étant générique, il faut tout de même contractualiser l’initialisation des paramètres. Pour cela, nous définissons une méthode qui sera portée par une interface ou une classe de base.

public interface IProvider { void Initialize(string providerName, NameValueCollection settings); }

L’utilisation d’une classe de base permet de coder un comportement par défaut pour l’initialisation.

abstract class ProviderBase { protected NameValueCollection _settings; private string _nameProvider; public virtual void Initialize(string providerName, NameValueCollection settings) { this._nameProvider = providerName; this._settings = settings; } }

Il faut donc ajouter à la factory le contrat sur le type générique. Dans ma factory générique, j’ai préféré l’interface.

public class ProviderFactory<T> where T : IProvider

Puis, il faut initialiser l’instance créée par Activator.CreateInstance() avec les paramètres du fichier de configuration.

providerInstance.Initialize(providerDeclaration.Name, providerDeclaration.Settings);

5.4 Vérification de type

Si le provider concret indiqué dans la configuration n’implémente pas votre provider ou tout simplement IProvider, vous aurez une InvalidCastException puisque la factory fait un transtypage direct. Vous pouvez bien évidemment tester le type concret instancié et renvoyer une exception ad hoc.

Je vous propose toutefois de vérifier au moins que le type implémente le contrat de base (IProvider ou ProviderBase) au chargement de la configuration.

Dans ProviderElement, vous pouvez surcharger la méthode DeserializeElement() et vérifier que le type correspond bien à vos attentes.

#region Vérification du type private bool IsProvider(Type type, object o) { return (type == typeof(IProvider)); } protected override void DeserializeElement(System.Xml.XmlReader reader, bool serializeCollectionKey) { base.DeserializeElement(reader, serializeCollectionKey); //On vérifie pour chaque élément que le type fourni implémente bien l'interface IProvider if (Type.FindInterfaces(new System.Reflection.TypeFilter(IsProvider), null).Length != 1) throw new System.Configuration.ConfigurationErrorsException("Type does not implement IProvider"); } #endregion

6 Utilisation du pattern

Voila, notre pattern Provider est prêt, reste à l’utiliser dans un cas concret. Reprenons l’exemple du TalkProvider que nous allons terminer.

Je commence par définir l’interface contractualisant un TalkProvider. Elle-même implémente IProvider qui provient de ma factory générique (ProviderFactory<T>).

interface ITalkProvider : Bewise.Patterns.GenericProvider.IProvider { string Talk(); }

Puis j’implémente ma classe concrète. Elle attend en paramètre un texte qui sera stocké dans l’instance et utilisé dans la méthode Talk().

class MyTalkInterfaceProvider : ITalkProvider { private string _speech; public string Talk() { return string.Format("Ceci est l'implémentation de ITalkProvider; le paramètre texte est {0}", _speech); } public void Initialize(string providerName, System.Collections.Specialized.NameValueCollection settings) { //ici on charge les paramètres du provider _speech = settings["text"]; } }

Enfin, il reste la configuration à mettre en place. Le nom de ma section est le nom du type de base de mon provider, ici ITalkProvider. On passe aussi le paramètre text qui sera affiché par la suite.

<ITalkProvider> <providers defaultProvider="blabla"> <add name="blabla" type="Bewise.Patterns.Consumer.InterfaceImplementation.MyTalkInterfaceProvider, Bewise.Patterns.Consumer" text="Interface"/> </providers> </ITalkProvider>

Mon provider est prêt à être utilisé dans mon application. La factory me permet de récupérer une instance que je peux utiliser directement.

InterfaceImplementation.ITalkProvider iprovider = ProviderFactory<InterfaceImplementation.ITalkProvider>.GetDefaultProvider(); iprovider.Talk()));

Et voila le résultat :

image

Vous trouverez une utilisation via une classe de base dans le code associé à cet article.

7 Analogie avec des patterns connus

Le pattern provider et le code mis en œuvre ici rappellent d’autres patterns de conception connus.

  • Builder : ce pattern se focalise sur la construction complexe (instanciation + paramétrage) d’objets du domaine
  • Abstract Factory (décrit par Frédéric Colin) : ce pattern permet d’instancier une famille d’objets sans en connaître le type concret, il se rapproche fortement de la factory mise en œuvre ici
  • Strategy : ce pattern se concentre sur l’abstraction de comportements (d’algorithmes)

8 Conclusion

Le pattern Provider est un outil puissant pour ajouter une abstraction entre deux couches ou deux modules d’une application.

Microsoft y a apporté une solution toute prête mais avec des inconvénients (mineurs). On voit qu’avec quelques lignes de code, on peut faire le sien…et surtout le rendre générique et réutilisable.

> Tous les articles

Commentaires

aucun commentaire
Page 1/1
   
Connexion
  • Accueil
  • Plan du site
  • Contact
Bewise TV, Blog technique, Webcasts...

Votre carrière et nous

  • Nos offres
  • Votre candidature
Ignorer les liens de navigation > Accueil > Nos Métiers > Solutions Langages et Framework > Détail Article
Ignorer les liens de navigation
Nous
Nos Métiers
Vous Former
Nos Evènements
Nos Références
Nos Activités
Nos Certifications
Nos Chiffres
Le Groupe
Nos Partenaires
On Parle de Nous
Nous contacter
Votre Carrière et Nous
Défiler vers le haut
Défiler vers le bas
Administration, Système et Communication
Architecture, Méthodes, Industrialisation
Décisionnel et Gestion des Données
Nouvelles Interfaces Utilisateurs
Portail et Travail Collaboratif
Solutions Langages et Framework
Solutions Web Avancées
Défiler vers le haut
Défiler vers le bas
Nos cours
Le Planning
Offres promotionnelles
Défiler vers le haut
Défiler vers le bas
Bewise Day Conference 2011
Nos Expresso
Défiler vers le haut
Défiler vers le bas
  • Infos légales
  • Lettre du Regional Director
  • Revue de presse