2012-03-27 13 views
49

¿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 un TypedFactory e inyectar la fábrica en la clase que necesita.Windsor: extracción de objetos transitorios del contenedor

Otro pensamiento que me vino fue "nuevo" por necesidad. Pero también estoy inyectando un componente Logger (a través de la propiedad) 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 Logger: La mayoría de mis clases tienen la propiedad Logger definido, salvo en caso de cadena de herencia (en ese caso sólo la clase base tiene esta propiedad, y todas las clases derivadas que usan). 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<QueueMonitor>().LifestyleSingleton()); 
//Install DataProcessor as Trnsient 
Container.Register(Component.For<DataProcessor>().LifestyleTransient()); 

Container.Register(Component.For<Data>().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 se crea nueva Record objeto sin utilizar "nuevo"?

  2. QueueMonitor es Singleton, mientras que Data es "Ampliado". ¿Cómo puedo inyectar Data en el método OnDataReceived()?

+0

@Steven Agregué un ejemplo de código que muestra el uso del registrador. ¿Crees que este es un mal diseño? – user1178376

+1

¿Puede ilustrar con código concreto, incluso mejor una prueba, lo que está tratando de lograr? –

+0

Lo siento por construir mal mi pregunta. Agregué más código para explicar mi caso. – user1178376

Respuesta

211

De las muestras que das es difícil ser muy específico, pero en general, cuando se inyecta ILogger casos en la mayoría de los servicios, usted debe preguntarse dos cosas:

  1. accedo demasiado ¿mucho?
  2. ¿Violo los principios SOLIDOS?

1. Cómo entro demasiado

Usted está ingresando demasiado, cuando se tiene una gran cantidad de código como este:

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

código de escritura como esto proviene 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. Peor aún, a menudo veo a los desarrolladores iniciar sesión y continuar (eliminan la última declaración throw). Esto es realmente malo (y huele a viejo 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 un paciente MRSA 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 la pila de llamadas.

¿No debería iniciar sesión? ¡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 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 s, que quedaría atrapada 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; 
} 

¿Te resulta familiar? Sí, se ve exactamente igual que el fragmento de código anterior, con la diferencia de que solo capté 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! Era un decorador, lo que me lleva a la siguiente pregunta que debe hacerse:

2. ¿Violo los principios SOLIDOS?

Las cosas como el registro, la auditoría y la seguridad se llaman cross-cutting concerns (o aspectos). 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í se mide el tiempo que tarda en 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 métodos 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 SOLID. 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 MoveCustomer ejemplo violado al menos dos de esas reglas:

  1. El Single Responsibility Principle. 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 Open-Closed principle (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 violó el principio DRY aquí, que básicamente dice que la duplicación de código es mala, mkay.

La solución a este problema consiste en extraer el registro en su propia clase y permitir que esa clase para envolver 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 el decorador torno a la instancia real, ahora se puede añadir este de medición el comportamiento de la clase, sin ninguna otra parte del sistema para cambiar:

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

el ejemplo anterior se hizo sin embargo apenas se solucionan parte del problema (sólo la parte sólida). Al escribir el código como se muestra arriba, tendrá que 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<TCommand>.

Vamos a definir esta interfaz:

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

Y vamos a almacenar los argumentos del método del método MoveCustomer en su propia (Parameter Object) clase llamada MoveCustomerCommand:

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

Y vamos a poner el comportamiento de la Método MoveCustomer en una clase que implementa ICommandHandler<MoveCustomerCommand>:

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

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

public class MeasuringCommandHandlerDecorator<TCommand> 
    : ICommandHandler<TCommand> 
{ 
    private ICommandHandler<TCommand> decorated; 
    private ILogger logger; 

    public MeasuringCommandHandlerDecorator(
     ICommandHandler<TCommand> 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."); 
    } 
} 

Esta nueva MeasuringCommandHandlerDecorator<T> se parece mucho al MeasuringMoveCustomerCommandDecorator, pero esta clase puede ser reutilizado para todos controladores de comandos en el sistema:

ICommandHandler<MoveCustomerCommand> handler1 = 
    new MeasuringCommandHandlerDecorator<MoveCustomerCommand>(
     new MoveCustomerCommandHandler(), 
     new DatabaseLogger()); 

ICommandHandler<ShipOrderCommand> handler2 = 
    new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
     new ShipOrderCommandHandler(), 
     new DatabaseLogger()); 

esta manera será mucho, mucho más fácil de añadir temas transversales al sistema. Es bastante fácil crear un método conveniente en su Composition Root que puede envolver cualquier controlador de comandos creado con los controladores de comando aplicables en el sistema. Por ejemplo:

ICommandHandler<MoveCustomerCommand> handler1 = 
    Decorate(new MoveCustomerCommandHandler()); 

ICommandHandler<ShipOrderCommand> handler2 = 
    Decorate(new ShipOrderCommandHandler()); 

private static ICommandHandler<T> Decorate<T>(ICommandHandler<T> decoratee) 
{ 
    return 
     new MeasuringCommandHandlerDecorator<T>(
      new DatabaseLogger(), 
       new ValidationCommandHandlerDecorator<T>(
        new ValidationProvider(), 
        new AuthorizationCommandHandlerDecorator<T>(
         new AuthorizationChecker(
          new AspNetUserProvider()), 
         new TransactionCommandHandlerDecorator<T>(
          decoratee)))); 
} 

Si su aplicación comienza a crecer, sin embargo, 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 (example) y Simple Injector (example) facilitan el registro de decoradores genéricos 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 sea injected as a factory y permite que contextual context se inyecte en decoradores, todos los cuales pueden ser realmente útiles de vez en cuando.

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 genéricos, 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: Meanwhile... on the command side of my architecture.

Espero que esto ayude.

+7

Gran respuesta Steven. Estoy de acuerdo con todo lo que dijo aquí, y no estoy de acuerdo con captar excepciones solo para iniciar sesión y volver a lanzar, ya que creo que debería haber un punto central en el dominio de la aplicación para hacerlo, sin embargo, creo que hay momentos en los que desea capte una excepción específica como SQLException para volver a intentar la acción. – OutOFTouch

+1

SimpleInjector se ve genial y está en la parte superior de mi lista para usar, sigan con el buen trabajo. Aquí hay un ejemplo de un decorador con Windsor http://mikehadlow.blogspot.com/2010/01/10-advanced-windsor-tricks-4-how-to.html para cualquiera que esté interesado. – OutOFTouch

+3

@OutOFTouch: Hacer un reintento de una operación es una muy buena opción para AOP, pero no desea envolver tal cosa con cada operación de DB, sino en los límites de la transacción. De hecho, [esta entrada de blog mía] (http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91) muestra esto (eche un vistazo al 'DeadlockRetryCommandHandlerDecorator') . – Steven

Cuestiones relacionadas