Windsor: tira oggetti transienti dal contenitore

Come posso tirare oggetti dal contenitore che sono transitori in natura? Devo registrarli con il contenitore e iniettare nel costruttore della class che necessita? Iniettare tutto nel costruttore non si sente bene. Anche solo per una class, non voglio creare una TypedFactory e iniettare la fabbrica nella class che necessita.

Un altro pensiero che venne da me fu “nuovo” su base di necessità. Ma sto anche iniettando un componente Logger (attraverso la proprietà) in tutte le mie classi. Quindi, se li ho nuovi, dovrò istanziare manualmente il Logger in quelle classi. Come posso continuare a utilizzare il contenitore per TUTTE le mie classi?

Iniezione del logger: la maggior parte delle mie classi ha la proprietà Logger definita, tranne dove c’è una catena ereditaria (in questo caso solo la class base ha questa proprietà e tutte le classi derivate la usano). Quando questi sono istanziati attraverso il contenitore di Windsor, otterrebbero la mia implementazione di ILogger immessa in essi.

 //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 } } 

Domande:

  1. Come posso creare un nuovo object Record senza utilizzare “nuovo”?

  2. QueueMonitor è Singleton , mentre Data è “Scoped”. Come posso inserire Data nel metodo OnDataReceived() ?

Dagli esempi ILogger è difficile essere molto specifici, ma in generale, quando si iniettano ILogger istanze di ILogger nella maggior parte dei servizi, è necessario porsi due domande:

  1. Registro troppo?
  2. Violato i principi SOLID?

1. Registro troppo

Stai registrando troppo, quando hai un sacco di codice come questo:

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

Scrivere codice come questo deriva dalla preoccupazione di perdere informazioni sugli errori. Duplicare questo tipo di blocchi try-catch ovunque, tuttavia, non aiuta. Ancora peggio, vedo spesso gli sviluppatori loggarsi e continuare (rimuovono l’ultima istruzione throw ). Questo è davvero pessimo (e puzza come il vecchio VB ON ERROR RESUME NEXT ), perché nella maggior parte dei casi non hai abbastanza informazioni per determinare se è sicuro continuare. Spesso c’è un errore nel codice che ha causato il fallimento dell’operazione. Continuare significa che l’utente spesso ha l’idea che l’operazione abbia avuto successo, mentre non lo ha fatto. Chiediti: cosa è peggio, mostrando all’utente un messaggio di errore generico che dice che c’è qualcosa che non va, o che salta in silenzio l’errore e lascia che l’utente pensi che la sua richiesta sia stata elaborata con successo? Pensa a come si sentirà l’utente se scoprisse che due settimane dopo il suo ordine non è mai stato spedito. Probabilmente perdi un cliente. O peggio, la registrazione MRSA di un paziente fallisce silenziosamente, facendo sì che il paziente non venga messo in quarantena da infermieri e causando la contaminazione di altri pazienti, causando costi elevati o forse addirittura la morte.

La maggior parte di questi tipi di linee try-catch-log dovrebbero essere rimosse e dovresti semplicemente lasciare che l’eccezione diventi una bolla sullo stack delle chiamate.

Non dovresti loggarti? Dovresti assolutamente! Ma se è ansible, definire un blocco try-catch nella parte superiore dell’applicazione. Con ASP.NET, è ansible implementare l’evento Application_Error , registrare un HttpModule o definire una pagina di errore personalizzata che esegue la registrazione. Con Win Forms la soluzione è diversa, ma il concetto rimane lo stesso: definire un singolo top più catch-all.

A volte tuttavia, vuoi comunque catturare e registrare un certo tipo di eccezione. Un sistema su cui ho lavorato in passato, ha lasciato che il livello aziendale generasse ValidationException s, che verrebbe catturato dal livello di presentazione. Tali eccezioni contenevano informazioni di convalida per la visualizzazione all’utente. Dal momento che tali eccezioni venivano catturate ed elaborate nel livello di presentazione, esse non si sovrapponevano alla parte più alta dell’applicazione e non finivano nel codice catch-all dell’applicazione. Volevo comunque registrare queste informazioni, solo per scoprire quante volte l’utente ha inserito informazioni non valide e per scoprire se le convalide sono state avviate per il giusto motivo. Quindi non si trattava di registrazione degli errori; solo logging. Ho scritto il seguente codice per fare questo:

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

Sembra familiare? Sì, sembra esattamente lo stesso del frammento di codice precedente, con la differenza che ho preso solo ValidationException s. Tuttavia, c’era un’altra differenza, che non può essere vista semplicemente guardando il frammento. C’era solo un posto nell’applicazione che conteneva quel codice! Era un decoratore, che mi porta alla prossima domanda che dovresti porci:

2. Violato i principi SOLID?

Cose come la registrazione, l’auditing e la sicurezza sono chiamate preoccupazioni trasversali (o aspetti). Si chiamano cross-cutting, perché possono tagliare molte parti della tua applicazione e spesso devono essere applicate a molte classi nel sistema. Tuttavia, quando trovi che stai scrivendo il codice per il loro uso in molte classi del sistema, molto probabilmente stai violando i principi SOLID. Prendiamo ad esempio il seguente esempio:

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

Qui misuriamo il tempo necessario per eseguire l’operazione MoveCustomer e registriamo tali informazioni. È molto probabile che altre operazioni nel sistema richiedano questa stessa preoccupazione trasversale. ShipOrder aggiungere codice come questo per i tuoi ShipOrder , CancelOrder , CancelShipping , ecc. Questo porta a un sacco di duplicazione del codice e alla fine un incubo di manutenzione.

Il problema qui è una violazione dei principi SOLID . I principi SOLID sono una serie di principi di progettazione orientata agli oggetti che aiutano nella definizione di software flessibile e gestibile. L’esempio MoveCustomer violato almeno due di queste regole:

  1. Il principio della singola responsabilità . La class che detiene il metodo MoveCustomer non sposta solo il cliente, ma misura anche il tempo necessario per eseguire l’operazione. In altre parole ha più responsabilità. Dovresti estrarre la misurazione nella sua class.
  2. Il principio Open-Closed (OCP). Il comportamento del sistema dovrebbe poter essere modificato senza modificare alcuna riga di codice esistente. Quando hai bisogno anche di una gestione delle eccezioni (una terza responsabilità) devi (di nuovo) modificare il metodo MoveCustomer , che è una violazione dell’OCP.

Oltre a violare i principi SOLID abbiamo decisamente violato il principio DRY qui, che sostanzialmente dice che la duplicazione del codice è ctriggers, mkay.

La soluzione a questo problema è di estrarre la registrazione nella propria class e consentire a quella class di avvolgere la class originale:

 // 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."); } } 

Con il wrapping del decoratore sull’istanza reale, è ora ansible aggiungere questo comportamento di misurazione alla class, senza che alcuna altra parte del sistema cambi:

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

L’esempio precedente ha tuttavia risolto solo parte del problema (solo la parte SOLIDA). Quando scrivi il codice come mostrato sopra, dovrai definire i decoratori per tutte le operazioni nel sistema e ti ritroverai con decoratori come MeasuringShipOrderCommandDecorator , MeasuringCancelOrderCommandDecorator e MeasuringCancelShippingCommandDecorator . Ciò ha portato di nuovo a un sacco di codice duplicato (una violazione del principio DRY), e ha ancora bisogno di scrivere codice per ogni operazione nel sistema. Quello che manca qui è un’astrazione comune sui casi d’uso nel sistema. Ciò che manca è un’interfaccia ICommandHandler .

Definiamo questa interfaccia:

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

E archiviamo gli argomenti del metodo MoveCustomer nella sua class ( Parameter Object ) chiamata MoveCustomerCommand :

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

E mettiamo il comportamento del metodo MoveCustomer in una class che implementa ICommandHandler :

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

Questo potrebbe sembrare strano, ma poiché ora abbiamo un’astrazione generale per casi d’uso, possiamo riscrivere il nostro decoratore come segue:

 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."); } } 

Questo nuovo MeasuringCommandHandlerDecorator assomiglia molto al MeasuringMoveCustomerCommandDecorator , ma questa class può essere riutilizzata per tutti i gestori di comandi nel sistema:

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

In questo modo sarà molto più facile aggiungere preoccupazioni trasversali al sistema. È abbastanza semplice creare un metodo conveniente nella vostra Root di composizione che possa avvolgere qualsiasi gestore di comandi creato con i gestori di comandi applicabili nel sistema. Per esempio:

 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)))); } 

Tuttavia, se la tua applicazione inizia a crescere, può diventare doloroso eseguire il bootstrap senza contenitore. Soprattutto quando i tuoi decoratori hanno vincoli di tipo generico.

La maggior parte dei moderni contenitori DI per .NET ha un supporto abbastanza decente per i decoratori al giorno d’oggi, e specialmente Autofac ( esempio ) e Simple Injector ( esempio ) rendono facile la registrazione di decoratori generici aperti. Simple Injector consente persino di applicare condizionatori in base a un determinato predicato o a complessi vincoli di tipo generico, consente di iniettare la class decorata come fabbrica e consente di inserire il contesto contestuale nei decoratori, che possono essere davvero utili dal tempo a tempo.

Unity e Castle d’altra parte dispongono di servizi di intercettazione (come Autofac fa a btw). L’intercettazione ha molto in comune con la decorazione, ma utilizza la generazione proxy dynamic sotto le copertine. Questo può essere più flessibile rispetto al lavoro con decoratori generici, ma quando si tratta di manutenibilità pagherai il prezzo, perché perdi spesso la sicurezza del tipo e gli intercettori ti costringono sempre ad assumere una dipendenza dalla libreria di intercettazione, mentre i decoratori sono sicuri e può essere scritto senza dipendere da una libreria esterna.

Leggi questo articolo se vuoi saperne di più su questo modo di progettare la tua applicazione: Nel frattempo … sul lato comando della mia architettura .

Spero che aiuti.