Construire des applications «Extensibles» Extensibilité / Patterns : Builder Factory Provider Module Pipeline Etc. IoC Containers MEF / System.ComponentModel.Composition + interfaces, abstraction Chaque composant doit avoir un contrat des fonctionnalités à remplir, un couplage faible et doit pouvoir être remplacé facilement sans devoir toucher au reste du code. Exemple : un cache a pour contrats : enregistrer des valeurs et les restituer, après peu importe la manière dont cela est fait à l intérieur de la classe, il faut pouvoir changer par un autre cache qui remplira ces fonctionnalités. 1. Pattern Provider Ajouter une référence à System.Configuration L intérêt du Pattern Provider est de permettre de changer les types à charger simplement depuis le fichier de configuration.il est possible ainsi de créer ses propres implémentations et les charger sans toucher au code source de base.
App.config <?xml version="1.0" encoding="utf-8"?> <configuration> <configsections> <section name="onesection" type="configurationdemo.onesectionconfigurationsection,configurationdemo"/> </configsections> <onesection> <!--type pointe sur le type à créer--> <mailer name="defaultmailer" type="configurationdemo.mailer,configurationdemo" email="xx@xx.fr" smtp="xxx.gmail.com"/> </onesection> </configuration> Section contenant une propriété pour chaque élément. // core configuration public class OneSectionConfigurationSection : ConfigurationSection [ConfigurationProperty("mailer", IsRequired = true)] public MailerElement Mailer get return (MailerElement)base["mailer"]; set base["mailer"] = value;
Eléments avec une propriété pour chaque attribut. // core configuration public class MailerElement : ConfigurationElement [ConfigurationProperty("name", IsRequired = true, IsKey = true)] public string Name get return (string)base["name"]; set base["name"] = value; [ConfigurationProperty("type", IsRequired = true)] public string Type get return (string)base["type"]; set base["type"] = value; [ConfigurationProperty("email", IsRequired = true)] public string Email get return (string)base["email"]; set base["email"] = value; [ConfigurationProperty("smtp", IsRequired = true)] public string Smtp get return (string)base["smtp"]; set base["smtp"] = value; (Charger la section) var config = ConfigurationManager.GetSection("oneSection") as OneSectionConfigurationSection; ConfigurationFactory // core contracts public interface IConfigurationFactory IMailer GetMailer(); // get other elements... // core public class ConfigurationFactory : IConfigurationFactory private IMailer _Mailer; public ConfigurationFactory() var config = ConfigurationManager.GetSection("oneSection") as OneSectionConfigurationSection; if (config!= null) // Création de l instance du type donné dans le fichier de config
IMailer mailer = Activator.CreateInstance(Type.GetType(config.Mailer.Type)) as IMailer; // chargement des propriétés du type créé mailer.email = config.mailer.email; mailer.smtp = config.mailer.smtp; _Mailer = mailer; public IMailer GetMailer() // retourne l instance créée return _Mailer; Manager // core contracts public interface IManager void DoSomething(); // core public class OneManager : IManager private IMailer _Mailer; public OneManager(IConfigurationFactory configurationfactory) // load providers _Mailer = configurationfactory.getmailer(); public void DoSomething() // do something with mailer _Mailer.Send(); // common public interface IMailer string Email get; set; string Smtp get; set; void Send(); // Providers public class Mailer : IMailer public string Email get; set; public string Smtp get; set; public void Send()
// MessageBox.Show(string.Format("0, 1", Email,Smtp)); Organiser la solution <?xml version="1.0" encoding="utf-8"?> <configuration> <configsections> <section name="onesection" type="configurationdemo.core.configuration.onesectionconfigurationsection,configuratio ndemo.core"/> </configsections> <onesection> <!--type pointe sur Mailer à créer--> <mailer name="defaultmailer" type="configurationdemo.providers.mailer,configurationdemo.providers" email="xx@xx.fr" smtp="xxx.gmail.com"/> </onesection> </configuration> 2. ConfigurationCollection <?xml version="1.0" encoding="utf-8"?> <configuration> <configsections> <section name="sectiontwo" type="configurationdemo.sectiontwoconfigurationsection,configurationdemo"/> </configsections> <sectiontwo> <Elements>
<add name="elementone" email="xx@xx.fr" /> <add name="elementtwo" email="xx@xx.com" /> </Elements> </sectiontwo> </configuration> public class SectionTwoConfigurationSection : ConfigurationSection [ConfigurationProperty("Elements", IsDefaultCollection = false)] [ConfigurationCollection(typeof(SectionTwoElementCollection))] public SectionTwoElementCollection Elements get return (SectionTwoElementCollection)base["Elements"]; [ConfigurationCollection(typeof(SectionTwoElement))] public sealed class SectionTwoElementCollection : ConfigurationElementCollection protected override ConfigurationElement CreateNewElement() return new SectionTwoElement(); protected override object GetElementKey(ConfigurationElement element) return ((SectionTwoElement)element).Name; public class SectionTwoElement : ConfigurationElement [ConfigurationProperty("name", IsRequired = true, IsKey = true)] public string Name get return (string)base["name"]; set base["name"] = value; [ConfigurationProperty("email", IsRequired = true)] public string Email get return (string)base["email"]; set base["email"] = value; var config = ConfigurationManager.GetSection("sectionTwo") as SectionTwoConfigurationSection; 3. Module + Pipeline Le module peut être chargé à partir du fichier de config.
On a d un côté les events De l autre des modules avec une méthode initialize dans laquelle celui-ci s abonne à un event. Events public delegate void ModuleDelegate<T>(T e); public class ProcessedEvents public ModuleDelegate<ModuleOneEventArgs> ModuleOneProcessed get; set; public ModuleDelegate<ModuleTwoEventArgs> ModuleTwoProcessed get; set; public class ModuleOneEventArgs : EventArgs public ModuleOneEventArgs() public class ModuleTwoEventArgs : CancelEventArgs public ModuleTwoEventArgs() Modules public interface IModule void Initialize(ProcessedEvents events);
public class ModuleOne : IModule public string Value = ""; public void Initialize(ProcessedEvents events) events.moduleoneprocessed += OnModuleOneProcessed; private void OnModuleOneProcessed(ModuleOneEventArgs e) // do something Value = "Ok"; public class ModuleTwo : IModule public void Initialize(ProcessedEvents events) events.moduletwoprocessed += OnModuleTwoProcessed; private void OnModuleTwoProcessed(ModuleTwoEventArgs e) if (MessageBox.Show("Cancel?", "Cancel", MessageBoxButton.OKCancel) == MessageBoxResult.OK) e.cancel = true; En cas d interaction avec une base de données on peut encadrer le code avec une transaction : string result = ""; bool cancel = false; // events partagés var events = new ProcessedEvents(); // modules abonnés à un event var moduleone = new ModuleOne(); moduleone.initialize(events); var moduletwo = new ModuleTwo(); moduletwo.initialize(events); using (var scope = new TransactionScope()) // event 1 if (events.moduleoneprocessed!= null) var args = new ModuleOneEventArgs(); events.moduleoneprocessed(args); result = moduleone.value; // event 2 if (events.moduletwoprocessed!= null)
var args = new ModuleTwoEventArgs(); events.moduletwoprocessed(args); if (args.cancel) //MessageBox.Show("Canceled"); // throw new ApplicationException(); cancel = true; if (!cancel) scope.complete(); 4. Override Consiste à créer des classes avec des méthodes virtuelles. La classe pourra être héritée facilement afin de redéfinir les méthodes. Il suffit en plus d ajouter une propriété accessible avec un test à l initialisation : public class DefaultCache protected readonly Dictionary<Type, Dictionary<string, object>> Cache = new Dictionary<Type, Dictionary<string, object>>(); public virtual void Register(Type type, string name, object instance) if (!Cache.ContainsKey(type)) Cache[type] = new Dictionary<string, object>(); if (!Cache[type].ContainsKey(name)) Cache[type][name] = instance; public class ReplacementCache : DefaultCache public override void Register(System.Type type, string name, object instance) // private DefaultCache _cache; public DefaultCache Cache get if (_cache == null) _cache = new DefaultCache(); return _cache;
set _cache = value; Ainsi on remplace facilement la classe de base Cache = new ReplacementCache(); 5. Mef / Composition On utilise la reflection pour récupérer les éléments exportés et les utiliser à la volée. public partial class MainWindow : Window public MainWindow() InitializeComponent(); AssemblyCatalog catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); CompositionContainer container = new CompositionContainer(catalog); speaker = container.getexportedvalue<speaker>(); Speaker speaker; private void okbutton_click(object sender, RoutedEventArgs e) var message = speaker.dosayhello(tb.text); MessageBox.Show(message); [Export] public class Speaker [Import("SayHello")] public Func<string, string> DoSayHello; public class HelperMethods [Export("SayHello")] public string SayHello(string name) var message = string.format("hello 0!", name); return message; 0 ou 1 [Import(AllowDefault=true)] 1 Utiliser [Import] 0..* (enumerable) utiliser [ImportMany]
InheritedExport (héritage) [ExportMetadata] + Lazy IEnumerable<Lazy<Func<int,int>> lazymethods; ImportingConstructor (injection dans le constructor) Obtenir les exports GetExport <>(),GetExports (),GetExports<>() Obtenir une instance GetExportedValue<>(),GetExportedValueOrDefault<>(),GetExportedValues<>(), returns Lazy<T>()