Inyección de dependencias: Resolviendo implementaciones específicas de una interface genérica

Hoy quería hablar de algo que vengo usando hace un tiempo y cada vez que lo uso me gusta más, es el uso del contenedor de dependencias para resolver la inyección de implementaciones especificas de una interface genérica siempre y cuando esta exista, si no existe coger la genérica. En mi caso uso StructureMap, pero supongo que se puede hacer con cualquier container que se precie, o que al menos puedas ampliarlo con convenciones.

El mejor ejemplo es el del repositorio, imagina esto:

public interface IRepository<TEntity>
    where TEntity : IEntity	
{
    TEntity Create();
    TEntity Save(TEntity entity);
    TEntity Remove(TEntity entity);
    IQueryable<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate);
    TEntity Get(TPKey key);
    TEntity FirstOrDefault(TPKey key);
    TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate);
}
 
public class Repository<TEntity> : IRepository<TEntity>
    where TEntity : IEntity	
{
    public ProductRepository(IUnitOfWork uow): base(uow) {
    }
    // Implementación de los metodos IRepository ...
}

Bien, para acceder a los productos de nuestra aplicación podemos crearnos la clase especifica:

public interface IProductRepository : IRepository<Product> {
}
 
public class ProductRepository : Repository<Product>, IProductRepository
{
    public ProductRepository(IUnitOfWork uow): base(uow) {
    }
}

Y por último inyectamos el repositorio de productos en un controlador o en una clase de aplicación:

public class ProductController : Controller
{
    private readonly IProductRepository _repository;
    public ProductController(IProductRepository repository)
    {
        _repository = repository;
    }
 
    // ...
    // ...
    // ...
}

Las convenciones por defecto de la mayoría de los contenedores de dependencias ya traen que la interface ILoqueSea se resuelva con la clase LoqueSea, así pues no hay que hacer nada más, el container resolverá IProductRepository con ProductRepository sin tener que especificarselo explicitamente.

Hasta aquí todo es correcto, pero realmente es incomodo tener que crearnos una interface y una clase para cada una de nuestras entidades, así que lo que podemos hacer es resolver la dependencia usando la clase genérica:

public class ProductController : Controller
{
    private readonly IRepository<Product> _repository;
    public ProductController(IRepository<Product> repository)
    {
        _repository = repository;
    }
 
    // ...
    // ...
    // ...
}

Ahora si, podemos crear controladores para nuestras entidades sin tener que escribir una interface y una clase especifica para cada una de ellas. Lo único es que debemos especificarle al contenedor de dependencias como resolver IRepository<>, en StructureMap basta con esto:

Container.Configure(conf =>
{
    conf.For(typeof(IRepository<>)).Use(typeof(Repository<>));
});

En otros container se puede hacer también, recuerdo haberlo hecho también con Castle Windsor sin muchos problemas.

Pero claro, pronto vendrán las especializaciones, imaginemos que quiero controlar que cuando se elimina un producto no haya stock existente, entonces debemos crearnos la clase ProductRepository y hacer override del método remove:

public interface IProductRepository : IRepository<Product> {
}
 
public class ProductRepository : Repository<Product>, IProductRepository
{
    public ProductRepository(IUnitOfWork uow): base(uow) {
    }
 
    public override Product Remove(Product)
    {
        if (Product.Stock.Amount > 0)
            throw new Exception("No puede eliminar un producto con stock.");
        return base.Remove();
    }
}

Ahora si estamos obligados a inyectar la implementación concreta del repositorio de productos a través de su interface, como lo teníamos antes:

public class ProductController : Controller
{
    private readonly IProductRepository _repository;
    public ProductController(IProductRepository repository)
    {
        _repository = repository;
    }
 
    // ...
    // ...
    // ...
}

De modo que el container inyecte la clase concreta ProductRepository que lleva la reimplementación del método Remove, si no lo indicamos así el container inyectará una instancia de Repository<Product> y nos dejará eliminar cualquier producto sin mirar antes el stock.

La cosa es que las aplicaciones “crecen, crecen y vuelven a crecer (pronto es navidad :D)” entonces tienes que ir buscando donde usas Repository<Product> para cambiarlo por IProductRepository y es más, si tienes clases intermedias como clases de apliación, clases de dominio y controladores, para cambiar el comportamiento del borrado de productos tendrás que redefinir las clases intermedias para que usen tu implementación especifica, por ejemplo si tienes algo así:

public class Repository<TEntity>
    where TEntity : IEntity
{
	// ...
}
 
public class DomainService<TEntity>
    where TEntity : IEntity
{
    public DomainService(IRepository<TEntity> repository) {
    }
    // ...
}
 
public class ApplicationService<TEntity>
    where TEntity : IEntity
{
    public ApplicationService(IDomainService<TEntity> domainService) {
    }
    // ...
}
 
public class ProductController : Controller
{
    private readonly IApplicationService<Product> _appService;
    public ProductController(IApplicationService<Product> appService)
    {
        _appService = appService;
    }
}

Al cambiar el comportamiento del repositorio, necesitarás crearte interfaces y clases especificas para Product para que el contenedor de dependencias inyecte el repositorio correcto … Esto se hace pesado y siempre tienes el miedo de haber olvidado algo.

Bien, la idea para solucionar esto es montárselo para que cada vez que en algún lado se pida un IRepository<Product> la inversión del control resuelva un ProductRepository, siempre y cuando este exista, si no existe entonces debe resolverlo con un Repository<Product>.

Con StructureMap es muy fácil, tan solo hay que decirle:

Scan(scan => {
    scan.TheCallingAssembly();
    scan.WithDefaultConventions();
    scan.ConnectImplementationsToTypesClosing(typeof(IRepository<>));
});

Con eso, cada vez que se pida un IRepository<LoQueSea> intentará resolverlo con la clase concreta LoQueSeaRepository pero si no existe te cogerá la clase genérica Repository<LoQueSea>.

Si no usas StructureMap no hay problema, Castle Windsor muestra en su documentación el equivalente:

kernel.Register(AllTypes.FromAssembly(Assembly.GetExecutingAssembly())
                        .BasedOn(typeof(ICommand<>))
                        .WithService.Base());

Si el tuyo no tiene una equivalencia se puede hacer a mano, casi todos los contenedores de IoC decentes te permiten escribir tus propias convenciones, yo antes de tener este método en StructureMap lo hacía a mano, lo pongo aquí como ejemplo:

    public class SrvConvention : IRegistrationConvention
    {
        public void Process(Type type, Registry registry)
        {
            // only interested in non abstract concrete types
            if (type.IsAbstract || !type.IsClass)
                return;
 
            var genericParams = type.GetInterfaces()
                .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IRepository<>))
                .SelectMany(i => i.GetGenericArguments())
                .ToArray();
 
            if (genericParams.Length > 0)
            {
                var genericType = typeof (IRepository<>).MakeGenericType(genericParams);
 
                registry.For(genericType).Use(type).Named("Final");
                registry.For(genericType).Use("Final");
            }
        }
    }

No voy a entrar al detalle de la implementación, pero, así por encima, la el método Process es llamado por el container por cada uno de los tipos que escanea, así pues miramos si el tipo implementa IRepository y cogemos los parametros genéricos, si cumple entonces registramos IRepository del tipo implementado para resolverse con el tipo concreto.

Bueno, esto es todo. Es una buena forma de olvidarse de lo enrevesado que puede ser el tema de las dependencias de nuestras clases tengamos las capas que tengamos. Cuando tenemos que hacer algo especializado para una entidad creamos la clase especifica, implementamos el comportamiento requerido y nos olvidamos, compilamos y sin cambiar nada el contenedor resuelve con la nueva clase y el nuevo comportamiento se ejecuta.

3 comments

  • Muy buen post Javier! StructureMap es un contendor muy potente! Nosotros usamos AutoFac que también mola.

    Una sola apreciación:

    public override Product Remove(Product)
    {
    if (Product.Stock.Amount > 0)
    throw new Exception(“No puede eliminar un producto con stock.”);
    return base.Remove();
    }

    En mi opinión el repo no debería contener lógica de negocio, solo debería saber borrar el producto. Si he llegado a borrar es porque puedo borralo. A mí me gusta delegar esa responsabilidad en un servicio, commandHandler o similar. Algo como:

    public class ProductsService
    {
    public void RemoveProduct(int productId)
    {

    if (product.IsInStock())
    {
    productsRepository.Remove(product);
    }


    }
    }

    Como digo es mi opinión, pero para gustos los colores no?

    Un saludo tío!!!

  • 100% de acuerdo Luis, de hecho yo no suelo usar repositorios, mis clases de negocio usan el DbSet.

    El ejemplo de este post se basaba inicialmente en mis clases de negocio llamadas DomainService<TEntity> donde si que meto lógica de negocio.

    Para hacerlo más cercano a cualquier lector lo cambié por IRepository.
    En fin falta de experiencia escribiendo posts, pero me lo apunto para el próximo.

    Muchas gracias Luis por comentar.
    Un abrazo.

  • Pingback: Programando mantenimientos | 0 errors, 0 warnings

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

Demuestra que no eres un bot *