martes, 12 de febrero de 2019
Como sabemos, ASP.NET Core incluye un sistema de inyección de dependencias que, aunque es algo simple comparado con otras alternativas más veteranas, cubre la mayoría de necesidades habituales. Por ejemplo, un aspecto que no es muy conocido y que puede ser útil en muchas ocasiones es su capacidad para registrar y recuperar múltiples implementaciones de una misma interfaz.
En este post vamos a ver cómo conseguirlo, y un caso práctico de uso de esta técnica en un escenario muy frecuente.
Pero mejor, veámoslo todo sobre un caso práctico…
El primer paso para mejorar esta implementación sería detectar que en nuestra aplicación existen procesadores de archivos de imagen que se aplican en función de determinados criterios. Con esta información podríamos crear la siguiente abstracción:
Aplicando la técnica que hemos visto anteriormente, podríamos simplificar la clase inicial
Espero que podáis aplicarlo a vuestros propios desarrollos ;)
Publicado en Variable not found.
En este post vamos a ver cómo conseguirlo, y un caso práctico de uso de esta técnica en un escenario muy frecuente.
Registro de múltiples implementaciones de la misma interfaz
Como se puede intuir, el registro de distintas implementaciones de la misma interfaz se realiza… pues eso, registrándolas ;) No hay ningún misterio aquí, simplemente añadimos el par<TService, TImplementation>
en la clase Startup
de ASP.NET Core, usando el lifetime apropiado (singleton, en el ejemplo de abajo):public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<IImageFileProcessor, JpegFileProcessor>();
services.AddSingleton<IImageFileProcessor, HeavyFileProcessor>();
... // Add more IImageFileProcessor implementations
}
...
}
Obtención de múltiples implementaciones registradas
Aquí viene la parte que es menos intuitiva, aunque tampoco anda muy lejos de lo que podríamos esperar del sistema de inyección de dependencias de ASP.NET Core. Para reclamar al contenedor las instancias de tipos asociados a la interfazTService
, simplemente añadimos un parámetro de tipo IEnumerable<TService>
en el constructor:public class ImageController: Controller
{
private readonly IEnumerable<IImageFileProcessor> _imageProcessors;
public ImageController(IEnumerable<IImageFileProcessor> imageProcessors)
{
_imageProcessors = imageProcessors;
}
...
}
Esta colección de instancias de TService
que ya tenemos en memoria podemos utilizarla para implementar las funcionalidades que necesitemos para nuestra aplicación.Pero mejor, veámoslo todo sobre un caso práctico…
Un caso práctico: SOLIDificar código
Imaginad un código como el siguiente, donde se muestra un métodoProcess()
que lo único que hace es llamar a otros métodos que procesan objetos de tipo ImageFile
bajo determinadas circunstancias:public class ImageFileProcessor
{
public ImageFile Process(ImageFile imageFile)
{
if (imageFile.Type == "JPG")
imageFile = ProcessJpeg(imageFile);
if (imageFile.Type == "PNG")
imageFile = ProcessPng(imageFile);
if (imageFile.Size > 100_000)
imageFile = ProcessHeavyFile(imageFile);
if (imageFile.Width > 1024 || imageFile.Height > 768)
imageFile = ProcessBigFile(imageFile);
[...] // Call to another processors when needed
return f;
}
private ImageFile ProcessJpeg(ImageFile f) { ... }
private ImageFile ProcessPng(ImageFile f) { ... }
private ImageFile ProcessHeavyFile(ImageFile f) { ... }
private ImageFile ProcessBigFile(ImageFile f) { .. }
[...] // Other processors
}
Entre otros, este código atenta directamente contra Principio de Responsabilidad Única, dando lugar a una clase que a todas luces tendrá a ser muy extensa y difícilmente mantenible al tener demasiadas responsabilidades. Nada que no podamos romper usando algo de Estrategia ;)El primer paso para mejorar esta implementación sería detectar que en nuestra aplicación existen procesadores de archivos de imagen que se aplican en función de determinados criterios. Con esta información podríamos crear la siguiente abstracción:
public interface IImageFileProcessor
{
bool CanProcess(ImageFile imageFile);
ImageFile Process(ImageFile imageFile);
}
En nuestra aplicación existirán tantas implementaciones de IImageProcessor
como formas de procesar los ficheros de imagen, y cada uno de estos componentes incluirá tanto la lógica para decidir si puede procesarlas como el proceso en sí mismo:public class JpegFileProcessor : IImageFileProcessor
{
public bool CanProcess(ImageFile imageFile) => imageFile.Type == "JPG";
public ImageFile Process(ImageFile imageFile)
{
// Process here the JPEG file
}
}
public class HeavyFileProcessor : IImageFileProcessor
{
public bool CanProcess(ImageFile imageFile) => imageFile.Size > 100_000;
public ImageFile Process(ImageFile imageFile)
{
// Process here the heavy file
}
}
...
De momento, esta estructura ya nos permitiría simplificar la clase inicial, liberándola de responsabilidades que no le corresponden. Serán los componentes procesadores, las distintas implementaciones de IImageFileProcessor
, los que llevarán ese peso. Aplicando la técnica que hemos visto anteriormente, podríamos simplificar la clase inicial
ImageFileProcessor
suministrándole mediante inyección de dependencias la colección de procesadores, y haciendo que su método Process()
simplemente los recorra y ejecute de forma secuencial:public class ImageFileProcessor
{
private readonly IEnumerable<IImageFileProcessor> _imageProcessors;
public ImageFileProcessor(IEnumerable<IImageFileProcessor> imageProcessors)
{
_imageProcessors = imageProcessors;
}
public ImageFile Process(ImageFile imageFile)
{
foreach (var processor in _imageProcessors)
{
if (processor.CanProcess(imageFile))
{
imageFile = processor.Process(imageFile);
}
}
return imageFile;
}
}
Para que el componente ImageFileProcessor
esté disponible en todos los puntos de nuestra aplicación deberíamos registrarlo en el contenedor de dependencias. Asimismo, ahí registraríamos todos los procesadores de imágenes que hayamos implementado:public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<ImageFileProcessor>();
services.AddSingleton<IImageFileProcessor, JpegFileProcessor>();
services.AddSingleton<IImageFileProcessor, HeavyFileProcessor>();
... // Add more IImageFileProcessor implementations
}
...
}
Ojo, aunque en este caso utilizamos un registro de tipo singleton, esto puede variar en cada escenario en función de las necesidades.Fijaos que si quisiéramos añadir un nuevo procesador de imágenes, por ejemplo un
GifProcessor
no sería necesario tocar la clase ImageFileProcessor
, ni ninguno de los procesadores existentes. Simplemente crearíamos la clase correspondiente implementando IImageFileProcessor
y la registraríamos en el contenedor… and that’s all, folks!...
services.AddSingleton<IImageFileProcessor, GifFileProcessor>();
En resumen, en este post hemos visto cómo utilizar el sistema de inyección de dependencias de ASP.NET Core para registrar múltiples implementaciones de la misma interfaz y cómo recuperarlas más adelante para implementar soluciones muy limpias a problemas frecuentes.Espero que podáis aplicarlo a vuestros propios desarrollos ;)
Publicado en Variable not found.
Publicado por José M. Aguilar a las 8:30 a. m.
Etiquetas: aspnetcore, aspnetcoremvc, patrones, trucos
2 Comentarios:
Solo una sugerencia, por si alguien va a implementar este código para probarlo, en la interfaz debes cambiar el Preprocess por Process si no te dará error.
Un saludo y gracias por tan buen material.
Hola, Yago!
Tienes razón, se coló un bug en el código ;D
Ya está solucionado, ¡muchas gracias por comentarlo!
Enviar un nuevo comentario