Scoperto come scoprire quali sono i moduli installati non ci resta che caricarli… fosse semplice ;-)
La prima cosa che dobbiamo fare è trovare un sistema per collegare un IModuleDescriptor ad un modulo, dato che le informazioni presenti in un ModuleDescriptor arrivano da un file di configurazione non abbiamo gratis nessun “ponte” tra i 2 mondi: la descrizione di un modulo e il modulo stesso.
Quello che possiamo banalmente fare, e che faremo, è aggiungere alcune informazioni al sistema di configurazione, in realtà scopriamo che ci basta aggiungere al file di configurazione un valore che ci dica quale sia la classe che fa da entry point per il nostro modulo; l’unico requisito è che questa classe sia un modulo:
public interface IModule
{
void Boot();
}
se procediamo in questa direzione però ci scontriamo subito con un problema: il modulo ha bisogno di poter configurare a sua volta il service container per iniettare la sua configurazione, potremmo essere allora tentati di scrivere qualcosa del tipo:
public interface IModule
{
void Boot( IServiceProvider provider );
}
Questo apparentemente risolve, ma finisce con l’assegnare troppe responsabilità al metodo Boot() e non ci permette di disaccoppiare la fase di inizializzazione dalla fase di avvio, imponendoci di controllare le dipendenze tra i moduli per garantirci che quando il modulo X parte, il modulo Y, da cui X dipende, abbia già fatto quello che deve fare pena il fallimento dell’avvio di X; e questo è evidente che puzza un sacco e ci complica troppo la vita.
Potremmo tentare a questo punto di spostarci verso un qualcosa del tipo:
public interface IModule
{
void Initialize( IServiceProvider provider );
void Boot();
}
Funziona è vero, ma:
- Se pensiamo al modulo, isolandolo dal resto del mondo, ci rendiamo conto che il modulo è un’applicazione isolata a tutti gli effetti e diventa evidente che l’IModule è l’entry point di questa applicazione. In quest’ottica è lecito pensare che il modulo abbia delle dipendenze e che durante la fase di avvio abbia qualcosa da fare… sarebbe quindi molto bello se potessimo ottenere un’istanza del modulo da Castle in modo da avere le dipendenze iniettate “a gratis”;
- L’altro problema, che è più un dettaglio implementativo che un vero e proprio problema, è che la fase di inizializzazione del modulo, che coincide principalmente con la configurazione del container, non dovrebbe legarci mani e piedi; Se configurassimo il container attraverso un file xml saremmo liberi di cambiare la configurazione come e quando vogliamo senza creare problemi di sorta, qui vogliamo ottenere lo stesso risultato del resto la fase di inizializzazione quello è: configurazione;
Quindi perchè non pensare a qualcosa del tipo:
public interface IModule
{
/// <summary>
/// Boots the module.
/// </summary>
void Boot();
}
e poi del tipo:
public interface IModuleBootstrapper
{
void Initialize( IServiceProvider container );
IModule GetModule();
}
In questo modo, mettendo le 2 implementazioni in assembly diversi (esattamente come abbiamo con MyApplication.Runtime e MyApplication.Boot), otteniamo la netta separazione dei 2 mondi.
Quello che ci resta da fare è rendere conoscio il sistema di configurazione dei moduli di questa novità:
<modulesConfiguration>
<installedModules>
<modules>
<add name="module.MyFirstModule" Bootstarpper="-- Fully Qualified Type Name --" />
</modules>
</installedModules>
</modulesConfiguration>
Semplicemente ci basta aggiungere alla configurazione quale sia il tipo da cui dobbiamo fare la fase di boot del singolo modulo, nella solution allegata trovate “propagata” questa variazione a tutto il sistema di configurazione.
Rendiamo infine capace il nostro sistema (in questo caso il ModuleDescriptor) di caricare un IModuleBootstrapper:
public IModuleBootstrapper Load()
{
var bootstrapperType = Type.GetType( this.bootstrapper );
var obj = ( IModuleBootstrapper )Activator.CreateInstance( bootstrapperType );
return obj;
}
Il prossimo step ci consente di entrare nel vivo del problema: il mio primo modulo.
Un modulo è composto come minimo da 3 assembly, non è obbligatorio, ma, almeno all’inizio per capire, sarebbe una buona regola da seguire:
- MyFirstModule.System: contiene la definizione di tutti i contratti (entità e servizi) utilizzati ed esposti dal modulo;
- MyFirstModule.Runtime: contiene l’implementazione concreta di quello che abbiamo definito in *.System;
- MyFirstModule.Boot: si occupa della fase di boot del modulo ed in particolare della configurazione del container;
Procediamo quindi con il creare i 3 progetti e configurare Visual Studio per:
- Eseguire le azioni di “post build” al fine di copiare gli assembly del modulo nella bin dell’applicazione;
- Compilare i progetti nell’ordine corretto;
Creati i progetti non facciamo altro che aggiugere il minimo indispensabile:
- La classe MyFirstModule che implementa l’interfaccia IModule e non fa nulla per ora;
- La classe Bootstrapper responsabile della fase di inizializzazione del modulo, per ora questa classe si occupa di:
- Configurare il container registrando il modulo stesso;
- Restituire l’istanza del modulo;
La fase di avvio dell’applicazione a questo punto diventa:
var moduleManager = container.Resolve<IModuleManager>();
var installedModules = moduleManager.GetInstalledModules();
var bootstrappers = new List<IModuleBootstrapper>();
foreach( var module in installedModules )
{
var bootstrapper = module.Load();
bootstrappers.Add( bootstrapper );
}
foreach( var bootstrapper in bootstrappers )
{
bootstrapper.Initialize( container );
}
var modules = new List<IModule>();
foreach( var bootstrapper in bootstrappers )
{
var module = bootstrapper.GetModule();
}
foreach( var module in modules )
{
module.Boot();
}
E’ un filino troppo complessa e carica la fase di Boot dell’applicazione di troppe responsabilità quindi facciamo un po’ di refactoring:
public interface IModuleManager
{
IEnumerable<IModuleDescriptor> InstalledModules { get; }
IEnumerable<IModule> LoadedModules { get; }
IModuleManager LoadInstalledModules();
IModuleManager InitializeModules();
void BootModules();
}
modifichiamo il module manager per supportare le nuove funzionalità e ci aggiugiamo un pizzico di fluent interfaces, la fase di boot dell’applicazione diventa molto più snella a questo punto:
var moduleManager = container.Resolve<IModuleManager>();
moduleManager.LoadInstalledModules()
.InitializeModules()
.BootModules();
Quello che succede è:
- Carichiamo i descriptor dei moduli installati;
- Inizializziamo tutti i moduli;
- Avviamo i moduli inizializzati;
La fase di inizializzazione di un modulo necessita di una reference al container corrente, per far si che il module manager possa fornire una reference facciamo dipendere il module manager stesso dal service container e registriamo il service container in se stesso (un po’ contorto… ma solo in apparenza):
container.Register( Component
.For<IWindsorContainer, IServiceProvider>()
.Instance( container ) );
Estendiamo, infine, anche il nostro IEnvironmentService al fine di fargli esporre le nuove informazioni relative ai moduli in esecuzione.
public interface IEnvironmentService
{
IEnumerable<IModuleDescriptor> InstalledModules { get; }
IEnumerable<IModule> LoadedModules { get; }
}
Concludendo…
Adesso siamo in grado di caricare i moduli installati, lo stesso identico sistema può essere utilizzato per un sistema a plugin, ora viene la fase più complessa che è rendere pluggabile anche la UI, quindi permettere ad un singolo modulo di iniettare contenuti in zone (Region) messe a disposizione da altri; per fare ciò nella prossima puntata parleremo di:
- Comunicazione: Applicazione <–> Moduli e Modulo <-> Modulo;
- RegionService, RegionManager e Region;
Il progetto allo stato attuale: CompositeUI_v3.zip
.m