Inyección de dependencias.
Vamos a introducirnos en un algo importante y además muy interesante, la inyección de dependencias y la inversión de control.
Ambos patrones se encuentran muy asociados, tanto que es normal confundirlos, para hacerlo claro, inversión de control es el modo en que funcionan generalmente los frameworks, en lugar de nosotros instanciar un objeto Database en el data access application block de Enterprise Library le decimos a la clase DatabaseFactory que lo haga por nosotros, es es inversión de control, en lugar de tener nosotros el control sobre la creación de objetos le pedimos alguien más que lo haga. Inyección de dependencias es cuando un objeto A depende de uno B, esta dependencia es inyectada al objeto A por el constructor por ejemplo, es decir, el objeto A tiene un constructor que acepta un parámetro del tipo objeto B, esto es inyectar una dependencia.
Para poder entender el por qué de todo esto vamos a remitirnos a un ejemplo:
El problema:
public interface IProcessor { void Process(); } public class ProcessorImp : IProcessor { IDbConnection _connection; public ProcessorImp() { _connection = new System.Data.OleDb.OleDbConnection(); } public void Process() { //llama a _connection y hace algo... _connection.Open(); } }
Si miramos con detenimiento el método Process vemos algo interesante, imaginemos que queremos probar nuestro ProcessorImp y nos encontramos que en el constructor se instancia _connection, por lo tanto, para correr una prueba vamos a necesitar acceder a la base de datos, eso definitivamente no es deseable y mucho menos práctico para una prueba unitaria, pero ¿cuál es la solución?
La solución, inyectar dependencias:
La forma de solucionar esto es de alguna manera evitar que se instancie _connection dentro de ProcessorImp y utilizarlo en forma transparente de modo que podamos, en caso de probarlo, utilizar una implementación de _connection que no acceda a la base de datos, o sea, un Stub o por qué no un Mock.
Primera aproximación, desacoplar con interfaces
La inyección de dependencias es algo tan simple como evitar que nuestros objetos de negocios creen objetos que necesitan utilizar, proveyéndoselos ya listos para operar. ¿Y cómo hacemos esto? Inyectándolos.
Hay un par de formas de inyectar dependencias, pero nosotros vamos a elegir la inyección de dependencias por constructor (ya que no tiene sentido que exista la clase Processor sin un IDbConnection), que no es más que pasarle a nuestro objeto de negocios ese objeto que necesita ya listo a través de su constructor, más aún, vamos a pasarle una interfaz lo cual nos va a traer otras ventajas adicionales, vamos a ver cómo queda el código con esta modificación:
public class ProcessorImp : IProcessor { IDbConnection _connection; public ProcessorImp(IDbConnection connection) { _connection = connection; } public void Process() { //llama a _connection y hace algo... } }
Un poco mejor, vemos que estamos pasando el IDbConnection en el constructor, lo cual es bueno porque nos va permitir pasarle cualquier cosa que implemente esta interfaz y simule el comportamiento de la capa de datos en caso de querer probarlo en una prueba unitaria. Ahora bien, para construir un objeto IProcessor antes hacíamos:
IProcessor processor = new ProcessorImp();
Pero ahora no podemos porque el constructor de ProcessorImp (la única clase que tenemos que implementa IProcessor) en su lugar tenemos que hacer esto:
IDbConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["default"] .ConnectionString); IProcessor processor = new ProcessorImp(connection);
Para simplemente después hacer
processor.Process();
O sea, 1 línea útil y 2 inútiles (para la finalidad de la lógica de negocios), esto puede agravarse en caso de tener más de una dependencia de inyectar o que la dependencia que inyectamos a su vez tenga otras dependencias.
Entonces, ¿cómo se resuelve?, fácil, echando mano de otro patrón de diseño Factory para realizar la inversión de control.
El patrón Factory
Nos va a permitir recuperar objetos listos para ser utilizados, sin necesidad de saber nada de ellos, ni de qué dependen, si necesitan información de configuración, etc.
Mágico, si pero ¿cómo hacemos?, veamos un poco de código:
public class ProcessorFactory { public static IProcessor BuildProcessor() { return new ProcessorImp( new System.Data.SqlClient.SqlConnection( ConfigurationManager.ConnectionStrings["default"] .ConnectionString); } }
A esto le llamamos inversión de control (IoC) y es justamente eso, en lugar de decidir nosotros cómo crear un objeto hacemos que alguien más decida por nosotros, o sea, invertimos el control, se usa así:
IProcessor processor = ProcessorFactory.BuildProcessor(); processor.Process();
Lindo, ahora la creación del objeto no interfiere con la lógica de negocios y no necesitamos saber nada del objeto que vamos a utilizar, no limitamos a saber de la interfaz. Mejorando las pruebas unitarias Esta forma de hacer las cosas nos permite mejorar las pruebas unitarias ya que para probar el ProcessorImp, simplemente creamos un Factory que nos dé un ProcessorImp y le inyecte por constructor una IDbConnection que no haga nada o que haga lo que necesitamos para la prueba más o menos así:
public class ProcessorFakeFactory { public static IProcessor BuildProcessor() { return new ProcessorImp(new FakeDbConnection()); } }
Algo mejor sería hacerlo con un framework de Mock como RhinoMocks o el increíble Moq del amigo Kzu.
Otras ventajas de la inversión de control
Otra de las ventajas que tenemos utilizando este diseño es que podemos obtener diferentes implementaciones de IProcessor sin hacer mucho y lo más importante, sin impactar en el código de lógica de negocios. Si recuerdan algo que dije antes, en la interfaz no se encuentra el constructor y esto nos permite con la misma interfaz utilizar indistintamente diferentes implementaciones de IProcessor que pueden tener otras dependencias, por ejemplo:
public class ProcessorFakeFactory { public static IProcessor BuildProcessor() { return BuildProcessor(ProcessorType.Sql); } public static IProcessor BuildProcessor(ProcessorType type) { switch (type) { case ProcessorType.Sql: return new ProcessorImp( new System.Data.SqlClient.SqlConnection( ConfigurationManager.ConnectionStrings["default"].ConnectionString)); break; case ProcessorType.OleDb: return new ProcessorImp( new System.Data.OleDb.OleDbConnection( ConfigurationManager.ConnectionStrings["oledb"].ConnectionString)); break; default: throw new ArgumentException("type"); break; } } public enum ProcessorType { Sql, OleDb } }
Esta implementación es más o menos lo que hace DataAccess Application Block cuando hacemos DataBaseFactory.CreateDataBase() con una agregado que veremos más adelante.
Se ve claramente las ventajas de éste modelo, disminuimos el acoplamiento, mejoramos las pruebas unitarias, obtenemos mayor flexibilidad y queda lindo.
Ahora si, framework de inversión de control
El siguiente paso en todo esto es utilizar un framework de inyección de dependencias que haga la inversión de control, y en el mundo de .net hay para todos los gustos, vamos a enumerarlos:
- Spring.net: es un port del famoso framework de Java
- Windsor container: parte del proyecto Castle, muy extendido
- Unity: de patterns & practices de Microsoft, basado en ObjectBuilder que es lo que utiliza internamente DataAccess Application Block
- Ninject: Este es nuevito, la principal característica es que se basa en atributos para saber qué y cómo inyectar.
¿Cómo funcionan los frameworks de IoC?
Los framework de IoC nos permite tener un repositorio (en realidad es transparente el repositorio lo maneja el framework, de echo vamos a utilizar el término repositorio para referirnos a él) donde tenemos nuestros objetos e ir pidiéndoselos, algunas ventajas adicionales, como controlar el ciclo de vida de los objetos, crearlos tipo singleton, reutilizar instancias, y resolver dependencias anidadas, como tener un objeto A configurado y pedirle un B y que sólo se dé cuenta que necesita A y crearlos según lo configurado.
Basados en archivos de configuración
Hay variantes entre estos que enumeramos, la más utilizada es basarse en un archivo de configuración para saber cómo crear los objetos, a mí, personalmente no me gusta porque cuando tenemos una aplicación con varios (no digo mucho, 30 nomás) objetos se pone bastante molesto el archivo de configuración, a esto hay que sumarle que en caso de necesitar comprender algunas cosas sobre estos objetos tenemos que “bucear” el archivo de configuración, y no anda el intellisense ;-).
Basados en declaración programática
Casi todos, los frameworks soportan definir los objetos por medio de código, esto puede parece poco ventajoso, en lugar de escribir una sintaxis especial para instanciar los objetos escribo todo de forma normal y listo, pero nos perdemos el manejo del ciclo de vida que hace el framework.
Basados en atributos
El chico nuevo de la cuadra es Ninject que se basa en atributos para la definición de las dependencias, cero configuración, simplemente agregamos atributos a nuestras clases y listo, mágico, la idea es muy interesante aunque no me gusta mucho la implementación por motivos que vienen al caso.
Ahora sí, un ejemplo con un framework de IoC
Dentro de los frameworks que he nombrado los que más me agradan son Unity y Windsor (ya sé que me contradigo), vamos a ver unos ejemplos introductorios con Windsor ya que es simple y es bueno conocerlo.
Windsor container
También conocido como microkernel porque en realidad se basa en él y lo extiende, es parte del proyecto castle que si no lo conocen les recomiendo una vuelta. En él se basan muchos otros proyectos, como NHibernate, Moq, Rhino, etc. Diría sin temor a equivocarme que es el más extendido en la comunidad .net.
Antes que nada vamos a necesitar descargarlos desde acá .Y referenciar estos assemblies
- Castle.Core.dll
- Castle.MicroKernel.dll
- Castle.Windsor.dll
- Castle.DynamicProxy.dll
El código para configurar el contenedor es éste
IWindsorContainer container = new WindsorContainer(); container.AddComponent("Sql", typeof(IDbConnection), typeof(System.Data.SqlClient.SqlConnection)); container.AddComponent("Processor", typeof(IProcessor), typeof(ProcessorImp));
Y para utilizarlo
IProcessor processor = container.Resolve<IProcessor>();
La idea es poner todo esto dentro del Factory o en alguna inicialización de nuestro objeto aplicación o no, es una discusión de arquitectura que no viene al caso.
Más flexibilidad con Windsor
Seguramente se están preguntando dos cosas:
- Cómo le paso el parámetro del connectionString a SqlConnection
- Cómo hago en caso de tener más de una implementación de IDbConnection para indicarle al contenedor cuál utilizar para crear el IProcessor
Excelentes preguntas, la respuesta para todas es: no se puede por código, esta flexibilidad sólo está disponible si configuramos el contenedor por archivo de configuración. De la siguiente forma:
Utilizando Windsor desde archivo de configuración
IWindsorContainer container = new WindsorContainer( new XmlInterpreter(new ConfigResource("castle"))); IProcessor processor = container.Resolve();
Esto con relación al código, le indicamos al contenedor que obtenga la configuración desde la sección “castle” del archivo de configuración. El archivo de configuración queda así:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> <!--de esta manera le indicamos al motor de configuración que vamos a utilizar una sección llamanda castle que se parsea a partir del tipo CastleSectionHandler que se encuentra en el assemblie Castle.Windsor--> <section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" /> </configSections> <!--y esta es la sección de configuración de castle--> <castle> <components> <!--cada elemento que queremos registrar en el contenedor se define dentro de un tag component--> <!--en el tag id se coloca el nombre con el que queremos identificar este componente particular--> <!--en el tag service la interface--> <!--en el tag type el tipo concreto que querems que el contenedor devuelva--> <component id="Sql" service="System.Data.IDbConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="System.Data.SqlClient.SqlConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <parameters> <!--Windsor busca el constructor que tenga este parámetro y se lo inyecta--> <connectionString>Database=Database;Server=(local)\SQLEXPRESS;Integrated Security=SSPI</connectionString> </parameters> </component> <component id="OleDb" service="System.Data.IDbConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="System.Data.OleDb.OleDbConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> </component> <component id="Processor" service="Tipos.IProcessor, Tipos" type="Tipos.ProcessorImp, Tipos" > <parameters> <connection>${Sql}</connection> </parameters> </component> </components> </castle> <connectionStrings> <add name="Sql" connectionString="Database=Database;Server=(local)\SQLEXPRESS;Integrated Security=SSPI" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>
Como vemos en la sección “components” definimos nuestros componentes, dentro de cada elemento “component” tres atributos:
- Id: El nombre con el que identificamos unívocamente ese componente.
- service: El servicio, o sea la interfaz o el tipo con el que vamos a invocar nuestro componente, es decir, cuando le digamos dame un IAlgo ese es el servicio.
- type: El tipo concreto que queremos que devuelva cuando se solicite un servicio
Con esto es suficiente, ahora para poder inyectarle parámetros en los constructores tenemos el elemento “parameters” dentro de él colocamos un elemento por cada parámetro del constructor que queremos que el contener invoque, él sólito se va a dar cuenta qué constructor llamar.
¿Cómo se decide qué implementación de un servicio quiero?
Utilizando el tag “parameters” ponemos el nombre del parámetro y luego con un poco de sintaxis “castle” le indicamos qué implementación queremos que le inyecte por el “id” como en el ejemplo, diciendo algo como: cuando te pida IProcessor dame el tipo IProcessorImp y a su constructor que pide un IDbConnection pasale la implementación que tiene el "id" Sql.
Conclusión:
En una palabra, mágico, la inversión de control + inyección de dependencias + factory + frameworks de inversión de control nos permiten crear aplicaciones menos acopladas, más fáciles de probar, de cambiar, de mantener, definitivamente muy interesante. Hasta la próxima.
10 comentarios:
muy bueno me sirvio de mucho dada la poca bibliografia en español y no tan bien desglozada pero si pudieras poner ese mismo ejemplo con el Unity en vez de con Windsor Container te lo agradeceria
Salu2,
Dunia.
Por supuesto, gracias por el comentario.
Gracias por incluir
Introducción a Unity, o inyección de dependencias parte dos
es decir el ejemplo con la Unity es que estoy comenzando a adentrarme ahora en el tema y estoy en pañales..
Salu2,
Dunia
Por nada, cualquier cosa avisame, saludos.
Hola, muy buena su explicación, lo felicito.
saludos!.
Te agradezco, espero que te sea útil, saludos.
Muy buen post!!!
Dejo un aporte:
http://emanuelpeg.blogspot.com/2009/07/inyeccion-de-dependencias.html
Saludos!!
Gracias Emanuel por dejar tu comentario, excelento tu post. Saludos.
Este artículo tiene 5 años y a pesar de eso acaba de salvarme la vida para un proyecto del laburo. Gracias Leonardo!
Gracias Silvio! qué bueno que te haya sido útil.
saludos, Leonardo.
Publicar un comentario