Sortir du moule avec le pattern provider
Par amethyste le Jun 7, 2009 | Dans focus | Réagir »
Actuellement je travaille sur un site Web capable de s’adapter à plusieurs marques et pour différents pays.
L’idée générale est de construire un moule auquel tout le monde devra s’adapter.
Ce n’est pas toujours possible, en particulier la difficile question de certaines règles métiers qui peuvent différer fortement.
Par exemple les prix, promotion et autres cadeaux clients répondent à des logiques différentes selon la marque.
On a donc besoin par moment de sortir localement du moule et exécuter un code ou un autre selon le contexte. Comment s’y prendre concrètement ?
Si on ne parlait que de quelques lignes on pourrait envisager de placer une ou deux clauses if et tester par exemple le nom de la marque. Evidemment si une nouvelle marque vient s’ajouter, on devra prendre le risque de modifier un code qui marche.
Ce blog est publié sur un site qui fait depuis toujours la promotion de l’architecture de code alors on va essayer de faire mieux avec un patron de conception que j’aime bien : le provider.
L’idée générale n’est pas très révolutionnaire. On a (au moins) trois étapes à implémenter :
1. On écrit une interface qui définit la liste des méthodes et propriétés communes. Lorsque je parle d’interface, il s’agit aussi de classe abstraite. A vous de voir ce qui est utile dans votre cas
2. On implémente cette interface autant de fois que nécessaire (une implémentation par marque a priori)
3. On isole la logique d’instanciation de la classe réelle selon le contexte dans une classe factory qui retourne une instance de notre interface.
4. Puisque l’on est ambitieux, on voudrait que la factory soit paramétrable de façon à ne pas avoir besoin de la modifier si une nouvelle marque vient s’ajouter
Le provider est omniprésent dans .Net, il n’est donc pas étonnant que Microsoft nous fournissent une plomberie pour l’implémenter facilement. Découvrons la ensemble.
On commence par l’interface qui doit avoir une dépendance technique avec la classe ProviderBase [1].
public abstract class PricesProvider : ProviderBase
La suite du code définit les méthodes, propriétés… dont nous aurons besoin. Par exemple :
/// <summary> /// Obtient le meilleur prix d'un article donné compte tenu des réductions auxquelles le client est eligible /// </summary> /// <param name="userContext">Instance de <see cref="IPrincipal"/> détenant le profil du client</param> /// <param name="idProduit">Id du produit</param> /// <returns>Meilleurs prix proposé</returns> public abstract double GetBestPrice(IPrincipal userContext, string idProduit);
Je vous conseille vivement d’adopter une cohérence dans la signature des méthodes. Par exemple dans notre cas elles dépendent toutes du profil client que nous chargeons dans l’IPrincipal en cours (on a écrit un principal personnalisé). Celui-ci est par convention le premier paramètre de la ligne et porte systématiquement le même nom et les autres paramètres sont toujours positionnés dans le même ordre.
Croyez moi, c’est très pratique à l’usage et peut à l’occasion éviter quelques bogues de copier/coller.
Il ne vous reste plus qu’à implémenter votre logique métier pour chaque marque :
public sealed class ContosoPricesProvider : PricesProvider { Random rnd = new Random(DateTime.Now.Millisecond); public double GetBestPrice(IPrincipal userContext, string idProduit) { return 500 * rnd.NextDouble(); } }
Note : ce code n’est pas contractuel !
Implémentons la factory. Le corps de la classe est le suivant :
public static class PricesFactory { // le code ici }
Avant de procéder à son implémentation des étapes intermédiaires nous attendent.
Tout d’abord une collection de fournisseurs nous rendra quelques services :
public sealed class PricesProviderCollection : ProviderCollection { new public PricesProvider this[string name] { [DebuggerStepThrough] get { return (PricesProvider)base[name]; } } public override void Add(ProviderBase provider) { if (provider == null) { throw new ArgumentNullException(Ressources.PricesProviderCollection_ProviderNull); } if (!(provider is PricesProvider)) { throw new ArgumentException("Le type du provider doit être PricesProvider"); } base.Add(provider); } }
Il s’agit simplement de typer fortement ProviderCollection qui ne semble pas exister en version générique. Rien d’obligatoire, mais conseillé tout de même.
Nous souhaiterions déclarer dans le fichier web.config le provider qui sera utilisé. Pour cela on doit écrire une classe de configuration. Rien de plus simple :
public sealed class PricesProviderConfiguration : ConfigurationSection { [ConfigurationProperty("providers")] public ProviderSettingsCollection Providers { get { return (ProviderSettingsCollection)base["providers"]; } } [ConfigurationProperty("default")] public string DefaultProviderName { [DebuggerStepThrough] get { return base["default"] as string; } [DebuggerStepThrough] set { base["default"] = value; } } }
On peut ajouter diverses fioritures selon ce que vous souhaitez faire, mais vous voyez l’idée générale qui est de pouvoir ajouter dans le fichier de configuration quelque chose comme ceci :
<PricesConfigSection default="ContosoPricesProvider"> <providers> <add name="ContosoPricesProvider" type=" PriceModule.ContosoPricesProvider, Business" /> </providers> </PricesConfigSection>
Dans la section <providers> vous ajoutez autant de déclaration de classe réelle que nécessaire, et l’attribut default permet de sélectionner celle que le provider devra charger.
Une petite formalité toutefois, il faut aussi ajouter une déclaration similaire à celle-ci :
<section name="PricesConfigSection" type=" PriceModule.PricesProviderConfiguration, Business" />
Pour que l’analyseur du fichier web.config sache comment traiter la section ajoutée.
Revenons donc à notre factory.
Le constructeur (forcément static) est très simple :
static PricesFactory()
{
Initialize();
}
La méthode Initialize va simplement charger la collection de fournisseur trouvée dans le fichier de configuration. On implémente un patron singleton.
private static void Initialize() { if (IsInitialized) { return; } // début de l'initialisation lock (InitializationLock) { // pattern du double-checkin if (IsInitialized) { return; } PricesProviderConfiguration configSection = (PricesProviderConfiguration)ConfigurationManager.GetSection("PricesConfigSection"); if (configSection == null) { throw new … } _Providers = new PricesProviderCollection(); ProvidersHelper.InstantiateProviders(configSection.Providers, _Providers, typeof(PricesProvider)); _ProviderSettings = configSection.Providers; if (_Providers[configSection.DefaultProviderName] == null) { throw new … } _Default = _Providers[configSection.DefaultProviderName]; IsInitialized = true; } } /// <summary> /// <b>false</b> indique que la classe n'a pas été initialisée /// </summary> private static bool IsInitialized; /// <summary> /// Variable utilisée pour poser un verrou /// </summary> private static object InitializationLock = new object();
Notez l’utilisation des classes ConfigurationManager et ProvidersHelper qui font l’essentiel du travail. Donc au total un code relativement simple.
Ce n’est pas tout, on doit encore déclarer la propriété suivante qui sera alimentée par la méthode que nous venons de voir :
[DebuggerBrowsable(DebuggerBrowsableState.Never)] private static PricesProvider _Default; public static PricesProvider Default { [DebuggerStepThrough] get { return _Default; } }
Elle remonte le provider par défaut déclaré dans le fichier de configuration. Notez bien que la propriété est statique.
On peut agrémenter la classe en rendant public les collections ProvidersCollection ou ProviderSettingsCollection.
Bibliographie
[1] Aide en ligne sur ProviderBase
http://msdn.microsoft.com/fr-fr/library/system.configuration.provider.providerbase.aspx
Aucun commentaire pour le moment
Laisser un commentaire
| « Accélérer MSDN | Des sons mystérieux » |