Ce billet fait office de suite et fin des deux billets précédents.

Résultat

L’objectif était de fournir un moyen simple de développer de façon modulaire en C#. J’ai choisi de créer un système de modules basé sur un gestionnaire d’événements pour la communication intra-modulaire et pouvant s’appuyer sur un interpréteur de script.

Le mini-framework, tel qu’il est architecturé peut être utilisé de trois façons différentes :

  1. En tant que gestionnaire d’événements
  2. En tant que framework modulaire
  3. En tant que framework modulaire pouvant charger des modules écrit dans un langage de script

Je n’ai encore pas essayé de tester le système sur un projet de grande envergure, mais je prévois de développer en début d’été un logiciel de traitement de questionnaires à choix multiples pour une entreprise RH et je pense utiliser cet outil. Je publierai donc probablement quelques correctifs après avoir pris conscience de nouvelles problématiques.

Fonctionnement du gestionnaire de modules

Le fonctionnement du gestionnaire d’événement est expliqué dans un précédent billet, et je ne reviendrai pas dessus.

Commençons par la partie concernant la modularité :

Un module est caractérisé par un nom et une version. Il peut demander en prérequis une liste de modules qui devront être préalablement installés pour fonctionner. Le programmeur peut créer autant de modules qu’il le souhaite, en faisant simplement hériter une classe de la classe abstraite ModuleAdapter. Ces modules doivent implémenter la méthode Initialize() servant à initialiser les différents observateurs du module. Cette méthode est automatiquement appelée par le gestionnaire de module.

Au lancement du programme, le développeur doit « installer » les modules à l’aide de la méthode SetupModule de la classe ModuleManager. Ce setup va vérifier vérifier la correcte constitution du module (nom, version, et prérequis), puis vérifier que les prérequis sont installés et enfin appeler la méthode initialize du module.

EventSystem eventSystem = new EventSystem();
ModuleManager moduleManager = new ModuleManager(eventSystem);
moduleManager.SetupModule(new NomDuModule());

Vous constaterez ici que le gestionnaire de module prend en paramètre un système de gestion d’événement. Les modules sont en effet entièrement basés sur la levée d’événements et il est impossible d’utiliser le gestionnaire de module sans les événements.

A l’intérieur d’un module, il est impossible d’utiliser directement le gestionnaire d’événement. Le gestionnaire de modules fait l’intermédiaire en rajoutant une contrainte dans la levée d’un événement : l’émetteur. En effet, désormais un événement possède un émetteur permettant ainsi aux observateurs de filtrer les événements selon les modules les ayant déclenchés.

A l’intérieur d’un module, on doit lever les événements de la façon suivante :

/*
 * RaiseEvent prend en paramètre un module (qui sera l'émetteur de l'événement),
 * un identifiant pour l'événement et des données
 */
this.manager.RaiseEvent(this, "identifiant du type d'événement", [data...]");

De la même façon, vous pouvez demander au gestionnaire de module de créer des observateurs spécifiques à un module :

/*
 * RequestWatcher prend en paramètre un module (qui sera le propriétaire de
 * l'observateur), un filtre pour l'émetteur (seuls les événements émis
 * par l'émetteur spécifiés ne seront traités par l'observateur, un filtre
 * pour l'identifiant du type d'événement et un Effet
 */
this.manager.RequestWatcher(this,
                            "nom du module émetteur",
                            "identifiant du type d'événement",
                            Effect.Create(this.MethodeAAppeler));
/*
 * Il faut noter qu'il est aussi possible de demander la création d'un
 * observateur sans contrainte sur l'émetteur de la façon suivante :
 */
this.manager.RequestWatcher(this,
                            "identifiant du type d'événement",
                            Effect.Create(this.MethodeAAppeler));

Ensuite, tout fonctionne automatiquement, vos modules interceptent des événements qu’ils traitent pour relayer à nouveau de nouveaux événements aux autres modules, et ainsi de suite !

Exemple :

Il s’agit de l’implémentation sous forme modulaire du programme bidon présenté en exemple sur le billet sur le gestionnaire d’événements.

Le premier point à remarquer c’est que la classe Program est-elle même un module. Il doit donc être installé, c’est ce qu’on effectue dans le constructeur de Program. Dans la méthode Initialize on ajoute un watcher pour les événements déclenchés lorsqu’un module est installé et on installe un nouveau module (say_hello). Dans la méthode Run() on lève enfin un événement « EntryPoint » qui sera intercepté par d’autres modules.

    class Program : ModuleAdapter
    {
        static void ModuleInstalled(string name)
        {
            Console.WriteLine("Module \"{0}\" successfully installed", name);
        }

        static void Main(string[] args)
        {
            Program prgrm = new Program();
            prgrm.Run();
        }

        public Program()
            : base("program", 1.0f, new ModuleManager(new EventSystem()))
        {
            this.manager.SetupModule(this);
        }

        public override void Initialize()
        {
            this.manager.RequestWatcher(this, "module_manager", "installed", Effect.Create(ModuleInstalled));
            this.manager.SetupModule(new SayHello(this.manager));
        }

        public void Run()
        {
            this.manager.RaiseEvent(this, "EntryPoint");
            Console.Read();
        }
    }

Le contenu du module SayHello :

    class SayHello : ModuleAdapter
    {
        public SayHello()
        {
            this.name = "say_hello";
            this.version = 1.0f;
        }

        public override void Initialize()
        {
            /*
             * Note : Un événement déclenché en dehors d'un module porte pour
             * identifiant d’émetteur : "program"
             */
            // L'événement EntryPoint entraine l'appel de la fonction AskName
            this.manager.RequestWatcher(this,
                  "program", //On n'observe que les événements levés par "program"
                  "EntryPoint",
                  Effect.Create(this.AskName));
        }

        public void AskName()
        {
            Console.WriteLine("What is your name ?");
            string name = Console.ReadLine();
            if (name == "")
                this.manager.RaiseEvent(this, "warning", "The name is empty");
            Console.WriteLine("Hello " + name);
        }
    }

On remarquera que le module déclenche des événements de type « warning » qui sont interceptés par un module de gestionnaire d’erreur et d’avertissements. Ce module n’est pas installé par le développeur mais est automatiquement installé.

    class ErrorHandler : ModuleAdapter
    {
        public ErrorHandler()
        {
            this.name = "error_handler";
            this.version = 1.0f;
        }

        public override void Initialize()
        {
            this.manager.RequestWatcher(this, "error", Effect.Create(this.HandleError));
            this.manager.RequestWatcher(this, "warning", Effect.Create(this.HandleWarning));
        }

        private void HandleError(string error)
        {
            Console.WriteLine(" " + error);
            Console.WriteLine("Press Enter to exit");
            Console.Read();
            Environment.Exit(0);

        }

        private void HandleWarning(string warning)
        {
            Console.WriteLine(" " + warning);
        }
    }

La communication intra-modulaire fonctionne donc très simplement.

Fonctionnement du gestionnaire de scripts

L’idée est simple : vous gérez vos scripts comme vous l’entendez (qu’importe le langage, le nombre de fichiers, leur nom, les fonctions, …) mais vous devez être capable de créer un ou plusieurs objet de la classe ModuleAdapter avec vos scripts.

Fondamentalement, mon travail sur cette partie s’est résumé à l’écriture de 3 lignes de code :

interface ScriptEngine
{
    ModuleAdapter CreateModuleFromScript(string folder);
}

A l’aide de cette interface, vous devrez implémenter vos gestionnaires de scripts qui répondront donc à la contrainte de posséder une méthode générant un module à partir d’un dossier contenant des scripts.
Ceci garanti une parfaite équivalence entre les modules qui sont construits à partir d’un script et les modules qui sont écrits en C#.

Une fois implémenté, c’est simple, il suffit d’installer un module crée à partir du moteur de script :

EventSystem eventSystem = new EventSystem();
ModuleManager moduleManager = new ModuleManager(eventSystem);
ScriptEngine scriptEngine = new LUAScriptEngine();

moduleManager.SetupModule(scriptEngine.CreateModuleFromScript(@"ModulePath\"));

Bon, je vais quand même prendre le temps de réaliser une ou voire deux implémentations (probablement le LUA parce que le réflexif c’est booo !).

Conclusion

J’ai fini ce projet, je ne sais pas si un équivalent existe déjà, ou si j’ai réinventé la roue carré, à vrai dire peu importe j’ai appris pas mal de choses !

Je met à disposition une archive des mes sources en l’état actuel des choses (il reste surement des erreurs bien cachées, c’est non documenté et mon anglais – pour le nommage – est loin d’être parfait) : Modularité – Mini-framework

Je ferai une release propre des sources quand j’aurais tout bien commenté, surchargé et documenté l’utilisation, c’est à dire d’ici une à deux semaines au maximum…