Una aplicación modular suele ser una aplicación donde sus funcionalidades son opcionales, de forma que podemos quitar ó añadirlas según nos convenga. La aplicación solo sabe que va a tratar con instancias que cumplen un determinado contrato, ya sea cumpliendo con una interfaz ó determinado tipo base (usando clases abstractas). Estos contratos suelen estar en ensamblados que conocen las dos partes, de forma que la aplicación espera una instancia de clase que cumple el contrato definido en el ensamblado común, y el ensamblado "opcional" provee una instancia de clase que cumple dicho contrato, dicho contrato ó acuerdo mutuo indica a la aplicación como usar dicha instancia. En el ejemplo de solución de Visual Studio que se ve a la izquierda, LoadTypeTest sería la aplicación, Common el ensamblado compartido donde se definen los contratos y los tipos comunes, y LibraryTest donde se encuentra definida la clase, LoadTypeTest y LibraryTest tienen referenciado a Common, pero se desconocen entre ellos.
En en ensamblado común he definido una interfaz y un delegado, que definen la forma en que la instancia "desconocida" se va a relacionar con la aplicación:
public delegate void DataArrival_(String Data);
public interface ITestInterface
{
String Name { get; }
event DataArrival_ DataArrival;
void SendData(String Data);
}
Si la clase que que implementamos cumpliendo esta interfaz, tiene un constructor por defecto, tenemos dos formas de cargar el tipo dinámicamente:
Reflection:
Assembly myAssembly = Assembly.LoadFrom("LibraryTest.dll");
ITestInterface test2 = myAssembly.CreateInstance("LibraryTest.LibraryTest") as ITestInterface;
test2.DataArrival += new DataArrival_(delegate(String Data)
{
Console.WriteLine("Received: " + Data);
});
test2.SendData("Hello " + test2.Name + "!!");
Remoting:
ObjectHandle obj = Activator.CreateInstance("LibraryTest", "LibraryTest.LibraryTest");
ITestInterface test = obj.Unwrap() as ITestInterface;
test.DataArrival += new DataArrival_(delegate(String Data)
{
Console.WriteLine("Received: " + Data);
});
test.SendData("Hello "+ test.Name +"!!");
En este contexto, nos valen ambos planteamientos por igual, ambos cargan la instancia en el mismo dominio de aplicación que la aplicación principal y su rendimiento es bastante similar, a excepción de la primera instancia que realiza Remoting, que parece que le cuesta un poco más:
Pero, cuando la instancia tiene un constructor parametrizado tendrémos que usar una mezcla de las dos, ya que para poder invocar un constructor específico debemos de proveer el tipo a Activator.CreateInstance, que por supuesto lo podemos extraer con Reflection; vamos a suponer que la calse "desconocida" espera dos parámetros, un Int32 y un DataTable (ojo con el orden de los parámetros):
Assembly myAssembly = Assembly.LoadFrom("LibraryTest.dll");
Type myType = myAssembly.GetType("LibraryTest.LibraryTest");
ITestInterface test = Activator.CreateInstance(myType,myIndex,myTable) as ITestInterface;
test.DataArrival += new DataArrival_(delegate(String Data)
{
Console.WriteLine("Received: " + Data);
});
test.SendData("Hello " + test.Name + "!!");
Pero... y si ni siquiera supiesemos que número de parámetros tiene el constructor... o que tipo de parámetros son... pues con Reflection podemos interrogar al tipo para que nos de información sobre los constructores que hay en la clase y sus respectivos parámetros. Un ejemplo un poco tosco :
Assembly myAssembly = Assembly.LoadFrom("LibraryTest.dll");
Type myType = myAssembly.GetType("LibraryTest.LibraryTest");
// Interrogo al tipo para comprobar si existe el constructor
// que necesito.
Object[] constructorParameters = null;
foreach (ConstructorInfo constructor in myType.GetConstructors())
{
ParameterInfo[] parameters = constructor.GetParameters();
if ((parameters[0].ParameterType == typeof(Int32)) &&
parameters[1].ParameterType == typeof(DataTable))
{
constructorParameters = new Object[] {myIndex, myTable };
}
parameters.ToString();
}
ITestInterface test = Activator.CreateInstance(myType,constructorParameters) as ITestInterface;
test.DataArrival += new DataArrival_(delegate(String Data)
{
Console.WriteLine("Received: " + Data);
});
test.SendData("Hello " + test.Name + "!!");
Como decía este es un ejemplo un poco tosco, podemos mejorar la lógica para detectar el constructor, ordenar los parámetros... etc.. etc..
Lo máximo que sabe la aplicación sobre la clase, es su nombre y en que ensamblado esta, cosas que le podemos pasar como parámetros ó tenerlo alojado en un archivo de configuración, de forma que alterando dicho archivo la aplicación usa una u otras funcionalidades. Y con esto e imaginación puedes hacer tus arquitecturas tan inteligentes y escalables como tu quieras, de forma que sean capaces de cargar tipos sin conocerlos previamente y usarlos por medio de una interfaz ó clase abstracta. Si a esto le añades el uso de atributos como metadatos para describir las clases... aún puedes conseguir cosas más inteligentes y escalables, a ver si tengo tiempo otro día para escribir sobre este tema, que es bastante apasionante :)