Archivo de la etiqueta: inyección de dependencias

Otros tipos de inyección de dependencias

No es la primera vez que hablo en este blog sobre inyección de dependencias, el patrón que nos permite pasar todas las dependencias que una clase necesita en el constructor en vez de como argumentos en métodos o utilizar clases estáticas.

En el caso de Java, donde últimamente paso la mayoría de mi tiempo, contamos con Spring y Guice como las maneras más conocidas de inyectar dependencias, veamos un ejemplo de Spring.

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ClassWithDependencies {
    private final DependentClass dependentInstance;

    public void doSomethingWithDependency(){
        dependentInstance.doSomething();
    }
}

@Component
public class DependentClass {
    public void doSomething(){
        System.out.println("Doing something")
    }
}

En este ejemplo, con las anotaciones @Component de Spring y @RequiredArgsConstructor de Lombok (un pre-procesador que nos permite agregar setters, getters y constructores a Java) podemos definir ClassWithDependencies como una clase que recibe DependentClass como parámetro del constructor, y luego operar sobre ella.

En artículos anteriores hemos hablado de otros sistemas como el de ASP.net vNext o incluso crear el nuestro propio. Creando un motor de inyección de dependencias con C#

Es importante destacar que en este ejemplo concreto, la inyección construye una nueva instancia de la clase cada vez que se inyecta, siendo la más sencilla de las opciones de inyección.

Una instancia por ejecución (Singleton)

Sin embargo, podemos personalizar aún más el tipo de inyección que hacemos. Volviendo a nuestro ejemplo anterior, supongamos que DependentClass contiene información de la máquina (memoria, procesador, etc) en la que se ejecuta, y por tanto no cambiará a lo largo de la vida de nuestra aplicación.

En este caso, una nueva instancia de cada clase sería innecesario o incluso costoso, dependiendo del tipo de inyección, para lo cual, en la definición del componente, especificamos qué tipo de inyección queremos

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ClassWithDependencies {
    private final MachineContext machineContext;

    public void doSomethingWithDependency(){
        machineContext.getProcessor();
    }
}

@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class MachineContext {
    public String getProcessor() {
        return "Intel";
    }
}

Comparemos esto con definir un patrón singleton de manera manual:

public class ClassWithDependencies {
    private final MachineContext dependentInstance;

    public void doSomethingWithDependency(){
        MachineContext.getInstance().getProcessor();
    }
}

public class MachineContext {
    private static MachineContext instance;

    public String getProcessor() {
        return "Intel";
    }

    private MachineContext() {
    }

    public static MachineContext getInstance(){
        if(instance == null){
            instance = new MachineContext();
        }
        return instance;
    }
}

La principal diferencia de usar Singleton en inyección respecto al singleton estándar que podemos ver en el ejemplo anterior es con inyección de dependencias tenemos las ventajas de singleton y mantenemos la capacidad de probar nuestro sistema, ya que en una prueba de ClassWithDependencies podríamos simular o utilizar un mock de MachineContext (por ejemplo si queremos probar cómo se comporta nuestra aplicación en distintos contextos).

Una instancia por petición (Request)

Una variante de Singleton especialmente para web, ocurre cuando el contexto depende de la petición o la sesión. Para este ejemplo, tenemos una función que se dedica a llevar un registro de las excepciones, y queremos, para cada excepción, registrar además el Id del usuario al que le ocurrió la excepción.

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GlobalExceptionHandler {
    private final Request request;

    public void handleException(Exception ex){
        logger.error(request.getUserId(), ex.getStacktrace())
        machineContext.getProcessor();
    }
}

@Component
@Getter
@Setter
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class Request {
    private String userId;
}

Este formato nos permite rellenar la clase Request en la capa del controlador de de nuestra aplicación, y en la otra directamente preguntar por el estado, sin necesidad de tener que pasar el objeto request a lo largo de toda la cadena de llamadas hasta la captura de esta excepción.

Con ello, evitamos que nuestras clases tengan una referencia al objeto únicamente por tener que pasarlo a la siguiente capa, se simplifica el número de parámetros que manejamos y podemos referirnos al contexto siempre que lo necesitemos.

Conclusiones

En este artículo hemos repasado inyección de dependencias de manera tradicional y además hemos visto las ventajas de utilizar construcciones como Singleton y Request.

Y tú, usas estas construcciones cuando desarrollas aplicaciones?

Anuncios

Creando un motor de inyección de dependencias con C#

Read this article in English: Creating a Dependency Injection in C#

De acuerdo con la Wikipedia, la inyección de dependencias es un patrón de diseño de software que nos permite seguir el principio de inversión de dependencias mediante inversión de control, es decir, definir las dependencias de una clase desde fuera de la misma. En el caso de C# tenemos a nuestra disposición varios proyectos que generan todas las dependencias necesarias para nuestros objetos. Uno de estos es Ninject, un motor open-source con una sintaxis simple y plugins para ASP.net MVC y SignalR.

En este artículo veremos cómo funciona un motor de inyección de dependencias creando uno propio, basado en la sintaxis y la filosofía de Ninject.

Introducción

La inyección de dependencias nos ayuda a separar las responsabilidades entre las diferentes capas de nuestro código, así como mejorar la encapsulación y la facilidad de crear pruebas en nuestro código. Comencemos por un ejemplo simple, una lista de notas que nos permiten agregar una nota y guardarla llamando a _dataStorage, un objeto inicializado en el constructor:

public class NoteList
{
    private DataStorage _dataStorage;
    public NoteList()
    {
        _dataStorage = new DataStorage();
    }
    public void Add(string note)
    {
        if (string.IsNullOrEmpty(note))
        {
            throw new ArgumentException("Note cannot be empty");
        }

        _dataStorage.Save(note);
    }
}

DataStorage es una clase que escribe el contenido de una nota en disco, y que tiene el siguiente código para el método Save:

internal void Save(string note)
{
    using (StreamWriter writer = new StreamWriter("db.txt"))
    {
        writer.WriteLine(note);   
    }
}

El código de test se muestra a continuación:

//Arrange
var noteList = new NoteList();
var noteText = "myCustomNote";

//Act
noteList.Add(noteText);

//Assert
using (StreamReader reader = new StreamReader("db.txt"))
{
    var all = reader.ReadToEnd();
    Assert.IsTrue(all.Contains(noteText));
}

Como se puede ver, generamos un acoplamiento con DataStorage, esto significa que no podemos probar NoteList a menos que también probemos el comportamiento de DataStorage.

Eliminando el acoplamiento

El primer cambio que podemos hacer es abstraer DataStorage en una interfaz llamada IDataStorage. Seguidamente pasamos dicha interfaz como argumento en el constructor, en vez de inicializarla dentro de nuestra clase:

...
private IDataStorage _dataStorage;
public NoteList(IDataStorage dataStorage)
{
    _dataStorage = dataStorage;
}
...

El código de test cambia también, ya que pasamos a definir las dependencias fuera de la clase:

...
var dataStorage = new DataStorage();
var noteList = new NoteList(dataStorage);
var noteText = "myCustomNote";
...

Construyendo nuestro inyector

Una vez que tenemos una clase que recibe los parámetros como argumentos. Estos parámetros deben ser definidos e inicializados manualmente antes de llamar a nuestro constructor, con lo cual, el primer paso es generar dichos parámetros solicitando un tipo específico a nuestro Iniector. La primera versión es una clase estática con dos métodos y un diccionario interno para almacenar los mapeos, que tiene este aspecto:

private static Dictionary<Type, object> mappings
    = new Dictionary<Type, object>(); 

public static T Get<T>()
{
    return (T)mappings[typeof(T)];
}

public static void Map<T>(object o)
{
    mappings.Add(typeof(T), o);
}

Ahora podemos solicitar un objeto para la interfaz de nuestro test:

Injector.Map<IDataStorage>(new DataStorage());
var dataStorage = Injector.Get<IDataStorage>();

Este código presenta un problema, y es que siempre devolveremos el mismo objeto, y seguimos especificando el inyector en el constructor. Sería mucho más sencillo si el inyector devolviera una nueva copia de nuestro objeto, y si adicionalmente pudiéramos pedir nuestro objeto directamente desde el inyector:

Injector.Map<IDataStorage>(new DataStorage());
var noteList = Injector.Get<NoteList>();

Para poder llegar a estos resultados necesitamos una serie de mejoras importantes en nuestro inyector. El primer paso consiste en dividir el método Get en un método no genérico, de tal manera que podamos realizar llamadas recursivas como veremos más adelante:

public static T Get<T>()
{
    var type = typeof(T);
    return (T)Get(type);
}

El segundo paso consiste en cambiar la manera de almacenar los objetos, ya que en vez de una instancia, almacenaremos un tipo. De esta manera cambiamos el método Map para asegurarnos que no estamos almacenando un objeto específico, y que el tipo a almacenar hereda o implementa el tipo que se usará de clave:

public static void Map<T, V>() where V : T
{
    mappings.Add(typeof(T), typeof(V));
}

Finalmente, cambiamos nuestro método Get para invocar al constructor del tipo requerido, tras algunas comprobaciones adicionales:

private static object Get(Type type)
{
    var target = ResolveType(type);
    var constructor = target.GetConstructors()[0];
    var parameters = constructor.GetParameters();

    List<object> resolvedParameters = new List<object>();

    foreach (var item in parameters)
    {
        resolvedParameters.Add(Get(item.ParameterType));
    }

    return constructor.Invoke(resolvedParameters.ToArray());
}

Veamos este código paso a paso: En primer lugar se resuelve el tipo, es decir, se comprueba si hay algún mapping disponible para el tipo especificado, en caso contrario se devuelve el mismo tipo:

private static Type ResolveType(Type type)
{
    if (mappings.Keys.Contains(type))
    {
        return mappings[type];
    }

    return type;
}

Una vez nos hemos asegurado de estar usando el tipo de dato correcto, el siguiente paso es recuperar el constructor, y su lista de parámetros (si existe).

...
    var target = ResolveType(type);
    var constructor = target.GetConstructors()[0];
    var parameters = constructor.GetParameters();
...

Si el constructor tiene parámetros, para cada uno de ellos se intentará resolver (es decir, se repetirá el proceso de manera recursiva), y si la resolución tiene éxito, se agregará a una lista de parámetros resueltos. En este caso el orden es muy importante ya que debe coincidir con el orden en el que se especifican los parámetros en el constructor**.

    foreach (var item in parameters)
    {
        resolvedParameters.Add(Get(item.ParameterType));
    }

Finalmente se invoca el constructor con la lista de parámetros y se devuelve el objeto que se acaba de crear.

    return constructor.Invoke(resolvedParameters.ToArray());

La sintaxis final para obtener un nuevo objeto de nuestro inyector de dependencias es la siguiente:

Injector.Map<IDataStorage, DataStorage>();
var noteList = Injector.Get<NoteList>();

El resultado es una manera muy conveniente de generar un objeto sin preocuparnos de sus dependencias, que pueden estar definidas a un nivel mucho mayor.

Resumen

La inyección de dependencias no es un término oscuro ni mucho menos, nos permite crear arquitecturas desacopladas, y motores como el que acabamos de crear nos permiten crear la infraestructura necesaria a un nivel más alto, con lo cual el resto de las clases pueden tener solamente la lógica relacionada con su propio comportamiento (principio de responsabilidad simple) con un conjunto mínimo de dependencias. Por otro lado no es una pieza de software muy complicada, aunque en este caso es un ejemplo y el conjunto de casos cubiertos es mínimo.

Finalmente, las características de C# nos permiten hacer algo de metaprogramación al obtener tipos y parámetros de una función en tiempo de ejecución, así como crear objetos de manera dinámica, lo cual no está mal para un lenguaje fuertemente tipado. Sería interesante comprobar cómo podríamos crear una versión de este inyector en un entorno más dinámico y débilmente tipado.

Descarga el código!