Windsor – tirando objetos transitorios del contenedor

¿Cómo puedo extraer objetos del contenedor que son de naturaleza transitoria? ¿Debo registrarlos con el contenedor e inyectar en el constructor de la clase que los necesita? Inyectar todo en el constructor no se siente bien. También solo para una clase no quiero crear una TypedFactory e inyectar la fábrica en la clase que necesita.

Otro pensamiento que vino a mí fue “nuevo” sobre la base de las necesidades. Pero también estoy inyectando un componente Logger (a través de propiedades) en todas mis clases. Entonces, si los actualizo, tendré que instanciar manualmente el Logger en esas clases. ¿Cómo puedo continuar usando el contenedor para TODAS mis clases?

Inyección de registrador: la mayoría de mis clases tienen la propiedad Logger definida, excepto cuando hay una cadena de herencia (en ese caso, solo la clase base tiene esta propiedad, y todas las clases derivadas usan eso). Cuando estos se instancian a través del contenedor Windsor, obtendrían mi implementación de ILogger inyectada en ellos.

 //Install QueueMonitor as Singleton Container.Register(Component.For().LifestyleSingleton()); //Install DataProcessor as Trnsient Container.Register(Component.For().LifestyleTransient()); Container.Register(Component.For().LifestyleScoped()); public class QueueMonitor { private dataProcessor; public ILogger Logger { get; set; } public void OnDataReceived(Data data) { //pull the dataProcessor from factory dataProcessor.ProcessData(data); } } public class DataProcessor { public ILogger Logger { get; set; } public Record[] ProcessData(Data data) { //Data can have multiple Records //Loop through the data and create new set of Records //Is this the correct way to create new records? //How do I use container here and avoid "new" Record record = new Record(/*using the data */); ... //return a list of Records } } public class Record { public ILogger Logger { get; set; } private _recordNumber; private _recordOwner; public string GetDescription() { Logger.LogDebug("log something"); // return the custom description } } 

Preguntas:

  1. ¿Cómo creo un nuevo objeto de Record sin usar “nuevo”?

  2. QueueMonitor es Singleton , mientras que los Data tienen “scope”. ¿Cómo puedo inyectar Data en el método OnDataReceived() ?

A partir de las muestras que usted da, es difícil ser muy específico, pero en general, cuando inyecta instancias de ILogger en la mayoría de los servicios, debe preguntarse dos cosas:

  1. ¿Me registro demasiado?
  2. ¿Violo los principios SÓLIDOS?

1. ¿Logo demasiado

Estás registrando demasiado, cuando tienes muchos códigos como este:

 try { // some operations here. } catch (Exception ex) { this.logger.Log(ex); throw; } 

Escribir código como este viene de la preocupación de perder información de error. Sin embargo, duplicar estos tipos de bloques de prueba en todo el lugar no ayuda. Lo que es peor, a menudo veo que los desarrolladores inician sesión y continúan (eliminan la última instrucción throw ). Esto es realmente malo (y huele como el anterior VB ON ERROR RESUME NEXT ), porque en la mayoría de las situaciones simplemente no tiene suficiente información para determinar si es seguro continuar. A menudo hay un error en el código que hizo que la operación fallara. Continuar significa que el usuario a menudo tiene la idea de que la operación tuvo éxito, mientras que no. Pregúntese: ¿qué es peor, mostrarle al usuario un mensaje de error genérico que dice que algo salió mal, o saltarse el error en silencio y dejar que el usuario piense que su solicitud fue procesada con éxito? Piense en cómo se sentiría el usuario si descubriera dos semanas después que su pedido nunca se envió. Probablemente perderías un cliente. O peor, el registro de MRSA de un paciente falla silenciosamente, haciendo que el paciente no sea puesto en cuarentena por la lactancia y que resulte en la contaminación de otros pacientes, causando altos costos o incluso la muerte.

La mayoría de estos tipos de líneas try-catch-log deben eliminarse y simplemente debe dejar que la excepción salte a la stack de llamadas.

¿No deberías registrarte? ¡Debes hacerlo! Pero si puede, defina un bloque try-catch en la parte superior de la aplicación. Con ASP.NET, puede implementar el evento Application_Error , registrar un HttpModule o definir una página de error personalizada que realice el registro. Con Win Forms, la solución es diferente, pero el concepto sigue siendo el mismo: defina un solo top para todos.

A veces, sin embargo, aún desea capturar y registrar un cierto tipo de excepción. Un sistema en el que trabajé en el pasado, deja que la capa empresarial arroje ValidationException , que sería capturado por la capa de presentación. Esas excepciones contenían información de validación para mostrar al usuario. Dado que esas excepciones quedarían atrapadas y procesadas en la capa de presentación, no llegarían a la parte superior de la aplicación y no terminarían en el código catch-all de la aplicación. Aún así, quería registrar esta información, solo para averiguar con qué frecuencia el usuario ingresaba información no válida y para averiguar si las validaciones se activaron por el motivo correcto. Entonces esto no fue un error de registro; solo registrando. Escribí el siguiente código para hacer esto:

 try { // some operations here. } catch (ValidationException ex) { this.logger.Log(ex); throw; } 

¿Luce familiar? Sí, se ve exactamente igual que el fragmento de código anterior, con la diferencia de que solo capturé ValidationException s. Sin embargo, había otra diferencia, que no se puede ver simplemente mirando el fragmento. ¡Había solo un lugar en la aplicación que contenía ese código! Fue un decorador, lo que me lleva a la siguiente pregunta que debe hacerse:

2. ¿Violo los principios SÓLIDOS?

Cosas como el registro, la auditoría y la seguridad se llaman preocupaciones (o aspectos) transversales . Se llaman transversales, ya que pueden atravesar muchas partes de su aplicación y, a menudo, se deben aplicar a muchas clases en el sistema. Sin embargo, cuando descubra que está escribiendo código para su uso en muchas clases del sistema, lo más probable es que esté violando los principios SÓLIDOS. Tomemos por ejemplo el siguiente ejemplo:

 public void MoveCustomer(int customerId, Address newAddress) { var watch = Stopwatch.StartNew(); // Real operation this.logger.Log("MoveCustomer executed in " + watch.ElapsedMiliseconds + " ms."); } 

Aquí medimos el tiempo que lleva ejecutar la operación MoveCustomer y registramos esa información. Es muy probable que otras operaciones en el sistema necesiten la misma preocupación transversal. Comenzará a agregar código como este para sus ShipOrder , CancelOrder , CancelShipping , etc. Esto termina con una gran cantidad de duplicación de código y, finalmente, una pesadilla de mantenimiento.

El problema aquí es una violación de los principios SÓLIDOS . Los principios SOLID son un conjunto de principios de diseño orientados a objetos que lo ayudan a definir un software flexible y mantenible. El ejemplo MoveCustomer violó al menos dos de esas reglas:

  1. El Principio de Responsabilidad Individual . La clase que contiene el método MoveCustomer no solo mueve al cliente, sino que también mide el tiempo que lleva realizar la operación. En otras palabras, tiene múltiples responsabilidades. Debe extraer la medición en su propia clase.
  2. El principio de Open-Closed (OCP). El comportamiento del sistema debería poder modificarse sin cambiar ninguna línea de código existente. Cuando también necesita manejo de excepciones (una tercera responsabilidad) usted (nuevamente) debe alterar el método MoveCustomer , que es una violación del OCP.

Además de violar los principios SÓLIDOS definitivamente violamos el principio DRY aquí, que básicamente dice que la duplicación de código es mala, mkay.

La solución a este problema es extraer el registro en su propia clase y permitir que esa clase envuelva la clase original:

 // The real thing public class MoveCustomerCommand { public virtual void MoveCustomer(int customerId, Address newAddress) { // Real operation } } // The decorator public class MeasuringMoveCustomerCommandDecorator : MoveCustomerCommand { private readonly MoveCustomerCommand decorated; private readonly ILogger logger; public MeasuringMoveCustomerCommandDecorator( MoveCustomerCommand decorated, ILogger logger) { this.decorated = decorated; this.logger = logger; } public override void MoveCustomer(int customerId, Address newAddress) { var watch = Stopwatch.StartNew(); this.decorated.MoveCustomer(customerId, newAddress); this.logger.Log("MoveCustomer executed in " + watch.ElapsedMiliseconds + " ms."); } } 

Al envolver al decorador en torno a la instancia real, ahora puede agregar este comportamiento de medición a la clase, sin cambiar ninguna otra parte del sistema:

 MoveCustomerCommand command = new MeasuringMoveCustomerCommandDecorator( new MoveCustomerCommand(), new DatabaseLogger()); 

Sin embargo, el ejemplo anterior solo solucionó parte del problema (solo la parte SÓLIDA). Al escribir el código como se muestra arriba, deberá definir decoradores para todas las operaciones en el sistema, y ​​terminará con decoradores como MeasuringShipOrderCommandDecorator , MeasuringCancelOrderCommandDecorator y MeasuringCancelShippingCommandDecorator . Esto conduce nuevamente a una gran cantidad de código duplicado (una violación del principio DRY), y aún necesita escribir código para cada operación en el sistema. Lo que falta aquí es una abstracción común sobre casos de uso en el sistema. Lo que falta es una interfaz ICommandHandler .

Vamos a definir esta interfaz:

 public interface ICommandHandler { void Execute(TCommand command); } 

Y almacenemos los argumentos de método del método MoveCustomer en su propia clase ( objeto de parámetro ) llamada MoveCustomerCommand :

 public class MoveCustomerCommand { public int CustomerId { get; set; } public Address NewAddress { get; set; } } 

Y pongamos el comportamiento del método MoveCustomer en una clase que implemente ICommandHandler :

 public class MoveCustomerCommandHandler : ICommandHandler { public void Execute(MoveCustomerCommand command) { int customerId = command.CustomerId; var newAddress = command.NewAddress; // Real operation } } 

Esto puede parecer extraño, pero debido a que ahora tenemos una abstracción general para casos de uso, podemos reescribir nuestro decorador de la siguiente manera:

 public class MeasuringCommandHandlerDecorator : ICommandHandler { private ICommandHandler decorated; private ILogger logger; public MeasuringCommandHandlerDecorator( ICommandHandler decorated, ILogger logger) { this.decorated = decorated; this.logger = logger; } public void Execute(TCommand command) { var watch = Stopwatch.StartNew(); this.decorated.Execute(command); this.logger.Log(typeof(TCommand).Name + " executed in " + watch.ElapsedMiliseconds + " ms."); } } 

Este nuevo MeasuringCommandHandlerDecorator parece mucho al MeasuringMoveCustomerCommandDecorator , pero esta clase se puede reutilizar para todos los controladores de comando en el sistema:

 ICommandHandler handler1 = new MeasuringCommandHandlerDecorator( new MoveCustomerCommandHandler(), new DatabaseLogger()); ICommandHandler handler2 = new MeasuringCommandHandlerDecorator( new ShipOrderCommandHandler(), new DatabaseLogger()); 

De esta manera, será mucho, mucho más fácil agregar preocupaciones transversales al sistema. Es bastante fácil crear un método conveniente en su raíz de composición que pueda envolver cualquier controlador de comandos creado con los controladores de comando aplicables en el sistema. Por ejemplo:

 ICommandHandler handler1 = Decorate(new MoveCustomerCommandHandler()); ICommandHandler handler2 = Decorate(new ShipOrderCommandHandler()); private static ICommandHandler Decorate(ICommandHandler decoratee) { return new MeasuringCommandHandlerDecorator( new DatabaseLogger(), new ValidationCommandHandlerDecorator( new ValidationProvider(), new AuthorizationCommandHandlerDecorator( new AuthorizationChecker( new AspNetUserProvider()), new TransactionCommandHandlerDecorator( decoratee)))); } 

Sin embargo, si su aplicación comienza a crecer, puede ser doloroso arrancar todo esto sin un contenedor. Especialmente cuando tus decoradores tienen restricciones de tipo genérico.

La mayoría de Contenedores DI modernos para .NET tienen un soporte bastante decente para los decoradores hoy en día, y especialmente Autofac ( ejemplo ) y Simple Injector ( ejemplo ) facilitan el registro de decoradores generics abiertos. Simple Injector incluso permite que los decoradores se apliquen condicionalmente en base a un predicado dado o restricciones complejas de tipo genérico, permite que la clase decorada se inyecte como una fábrica y permite que el contexto contextual se inyecte en decoradores, todo lo cual puede ser realmente útil de tiempo para hora.

Unity y Castle por otro lado tienen instalaciones de interceptación (como Autofac lo hace por cierto). La interceptación tiene mucho en común con la decoración, pero utiliza la generación dinámica de proxy bajo las cubiertas. Esto puede ser más flexible que trabajar con decoradores generics, pero pagará el precio en términos de mantenibilidad, ya que a menudo perderá seguridad y los interceptores siempre lo obligarán a tomar una dependencia de la biblioteca de interceptación, mientras que los decoradores son seguros y se puede escribir sin tomar una dependencia de una biblioteca externa.

Lea este artículo si desea obtener más información sobre esta forma de diseñar su aplicación: mientras tanto … en el lado del comando de mi architecture .

Espero que esto ayude.