Monday, December 10, 2007

[NET2.0] Crear objetos que implementen una Interface seleccionando la implementación desde un Archivo de Configuración usando Reflection

Crear objetos dinámicamente dependiendo de un archivo de configuración es una práctica fundamental para crear aplicaciones altamente cohesivas y débilmente acopladas. Usando esta técnica podemos cambiar módulos de una aplicación sin tener que recompilar la misma, sólo cambiamos el valor de una variable en un archivo de configuración y la aplicación comenzará a usar el nuevo módulo definido.
Un caso muy común donde sucede esto es en los frameworks de acceso a datos. Generalmente con estos frameworks nos conectamos a diferentes fuentes de datos y sería bueno que cambiar nuestra fuente de datos sea transparente para el resto de nuestra aplicación. Esto se puede lograr mediante el uso de interfaces y de técnicas como cargar assemblies dinámicamente seleccionándolas desde archivos de configuración. A fines de este artículo no vamos a demostrar con un framework de acceso a datos sino con un conjunto simple de clases e interfaces. La idea es crear un interface, un contrato que deben respetar las clases que lo implementen. Luego crear dos clases que implementen la interface de manera diferente, en assemblies diferentes, y por último creamos una aplicación de prueba que tendrá un archivo de configuración de donde le indicaremos cual es la interface que se debe cargar. Comencemos con nuestro ejemplo. Definamos una interface. Nuestra interface tiene un método T GetMessage(T) que recibe un parámetro de un tipo T y devuelve un tipo T. No es necesario definir una interface genérica como esta, en este ejemplo se realizó así a fin de probar solamente, pero podría haberse hecho de igual manera con un tipo string.



Ahora definamos las dos clases en dos assemblies diferentes que implementen la interface de dos maneras distintas. A los fines de este ejemplo haremos algo simple, cada clase retornará el parámetro de entrada todo en mayúsculas o todo en minúsculas junto con un mensaje.

Implementación 1:
Implementación 2:
Tengamos en cuenta que para poder implementar estar interfaces tenemos que haber hecho previamente una referencia a la assembly (IMessageX) que tiene la interface.

Ahora debemos crear un programa que use esta estas interfaces dependiendo del archivo de configuración.
El programa tiene que tener un archivo de configuración, por ejemplo:

Como vemos en el archivo de configuración, para crear una instancia de una clase por Reflection necesitamos 2 cosas: el path a el assembly, que lo obtenemos del valor de configuración AssemblyPath1 o AssemblyPath2 y el nombre completo de la clase que queremos instanciar incluyendo el namespace de la misma, que lo obtenemos del valor de configuración ClassName1 o ClassName2.

Para leer un valor desde de un archivo de configuración usamos la clase ConfigurationManager del namespace System.Configuration y usamos el método estático Get("key") de la clase AppSettings que es expuesta por el ConfigurationManager mediante la propiedad AppSettings.

Una vez que obtuvimos el path a la assembly, podemos levantar la assembly mediante Reflection.
Para realizar esto creamos una referencia del tipo Assembly y usamos el método estático LoadFile("path") de la clase Assembly que pertenece al namespace System.Reflection.

Ahora debemos crear una instancia de la clase que implementa la interface.
Para lo cual necesitamos el nombre completo de la clase que obtenemos del archivo de configuración, luego
creamos una referencia del tipo de la interface, en nuestro caso, IMessageX y por últimos usando la assembly cargada previamente creamos la instancia del objeto con el método CreateInstance("classname") del objeto Assembly que ya levantamos.
Recordemos que este este método nos devuelve un object y debemos castearlo al tipo de nuestra interface.


Por último, para probar llamamos al método GetMessageX con el parámetro de entrada y vemos si se realizó correctamente lo que esperábamos.


Como vemos el resultado es el esperado, tenemos dos funcionalidades diferente para la misma interface cargadas desde 2 assemblies distintas dinámicamente seleccionadas desde un archivo de configuración.

Si vemos las referencias de nuestro programa veremos que solo hay referencias a la assembly que tiene la interface (IMessageX) y no hay ninguna referencia a las implementaciones (MessageXX y MessageXY).


El código de ejemplo, acá.