Iniezione delle dipendenze vs Ubicazione del servizio

Attualmente sto valutando i vantaggi e gli svantaggi tra DI e SL. Tuttavia, mi sono ritrovato nel seguente catch 22 che implica che dovrei semplicemente usare SL per tutto e iniettare solo un contenitore IoC in ogni class.

DI Catch 22:

Alcune dipendenze, come Log4Net, semplicemente non si adattano a DI. Io chiamo queste meta-dipendenze e sento che dovrebbero essere opachi al codice di chiamata. La mia giustificazione è che se una semplice class ‘D’ è stata originariamente implementata senza registrazione, e poi cresce per richiedere la registrazione, allora le classi dipendenti ‘A’, ‘B’ e ‘C’ ora devono in qualche modo ottenere questa dipendenza e passarla da ‘A’ a ‘D’ (supponendo che ‘A’ compone ‘B’, ‘B’ compone ‘C’ e così via). Ora abbiamo apportato modifiche significative al codice solo perché richiediamo l’accesso a una class.

Richiediamo quindi un meccanismo opaco per ottenere meta-dipendenze. Due vengono in mente: Singleton e SL. Il primo ha delle limitazioni note, principalmente per quanto riguarda le capacità di scoping rigide: nel migliore dei casi un Singleton userà una Fabbrica astratta che viene archiviata nell’ambito di applicazione (cioè in una variabile statica). Ciò consente una certa flessibilità, ma non è perfetto.

Una soluzione migliore sarebbe quella di iniettare un contenitore IoC in tali classi e quindi utilizzare SL all’interno di quella class per risolvere queste meta-dipendenze dal contenitore.

Quindi cattura 22: poiché la class viene ora iniettata con un contenitore IoC, perché non usarla per risolvere anche tutte le altre dipendenze?

Apprezzerei molto i tuoi pensieri 🙂

Poiché la class viene ora iniettata con un contenitore IoC, perché non utilizzarla per risolvere anche tutte le altre dipendenze?

L’utilizzo del pattern localizzatore di servizio sconfigge completamente uno dei punti principali dell’iniezione di dipendenza. Il punto di iniezione della dipendenza è rendere esplicite le dipendenze. Una volta nascoste queste dipendenze non rendendoli parametri espliciti in un costruttore, non si sta più facendo un’iniezione di dipendenza a tutti gli effetti.

Questi sono tutti i costruttori di una class di nome Foo (impostata sul tema della canzone di Johnny Cash):

Sbagliato:

 public Foo() { this.bar = new Bar(); } 

Sbagliato:

 public Foo() { this.bar = ServiceLocator.Resolve(); } 

Sbagliato:

 public Foo(ServiceLocator locator) { this.bar = locator.Resolve(); } 

Destra:

 public Foo(Bar bar) { this.bar = bar; } 

Solo quest’ultimo rende esplicita la dipendenza da Bar .

Per quanto riguarda la registrazione, c’è un modo giusto per farlo senza permeare il tuo codice di dominio (non dovrebbe, ma se lo fa, allora usi il periodo di iniezione delle dipendenze). Sorprendentemente, i contenitori IoC possono aiutare con questo problema. Inizia qui .

Service Locator è un anti-pattern, per ragioni ottimamente descritte su http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx . In termini di registrazione, è ansible trattarla come una dipendenza come qualsiasi altra e iniettare un’astrazione tramite il costruttore o l’iniezione di proprietà.

L’unica differenza con log4net è che richiede il tipo di chiamante che utilizza il servizio. Usando Ninject (o qualche altro contenitore) Come posso scoprire il tipo che richiede il servizio? descrive come puoi risolvere questo problema (usa Ninject, ma è applicabile a qualsiasi contenitore IoC).

In alternativa, potresti considerare la registrazione come una preoccupazione trasversale, che non è appropriata per il mix con il codice della tua logica aziendale, nel qual caso puoi utilizzare l’intercettazione fornita da molti contenitori IoC. http://msdn.microsoft.com/en-us/library/ff647107.aspx descrive l’utilizzo dell’intercettazione con Unity.

La mia opinione è che dipende. A volte uno è migliore ea volte un altro. Ma direi che generalmente preferisco DI. Ci sono alcuni motivi per questo.

  1. Quando la dipendenza viene iniettata in qualche modo in un componente, può essere considerata parte della sua interfaccia. Quindi è più facile per l’utente del componente fornire queste dipendenze, perché sono visibili. In caso di SL iniettato o Static SL, le dipendenze sono nascoste e l’utilizzo del componente è un po ‘più difficile.

  2. Le dipendenze iniettate sono migliori per i test unitari, in quanto puoi semplicemente prenderli in giro. In caso di SL devi impostare nuovamente Locator + dipendenze fittizie. Quindi è più lavoro.

A volte la registrazione può essere implementata utilizzando AOP , in modo che non si mischi con la logica aziendale.

Altrimenti, le opzioni sono:

  • utilizzare una dipendenza opzionale (come la proprietà setter) e per il test dell’unità non si inserisce alcun logger. Il contenitore di IOC si prenderà cura di impostarlo automaticamente per te se usi la produzione.
  • Quando si ha una dipendenza che utilizza quasi tutti gli oggetti della tua app (l’object “logger” è l’esempio più comune), è uno dei pochi casi in cui l’anti-pattern singleton diventa una buona pratica. Alcuni chiamano questi “buoni singleton” un ambiente ambient : http://aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/

Ovviamente questo contesto deve essere configurabile, in modo da poter usare stub / mock per il test dell’unità. Un altro uso suggerito di AmbientContext consiste nel mettere il fornitore di data / ora corrente, in modo da poterlo stub durante il test unitario e accelerare il tempo se lo si desidera.

Se l’esempio prende log4net solo come dipendenza, allora è sufficiente farlo:

 ILog log = LogManager.GetLogger(typeof(Foo)); 

Non ha senso iniettare la dipendenza poiché log4net fornisce una registrazione granulare prendendo il tipo (o una stringa) come parametro.

Inoltre, DI non è correlato con SL. IMHO lo scopo di ServiceLocator è risolvere dipendenze opzionali.

Ad esempio: se la SL fornisce un’interfaccia ILog, scriverò il registro daa.

Ho usato il framework di Google Guice DI in Java e ho scoperto che fa molto di più che rendere i test più facili. Ad esempio, avevo bisogno di un log separato per applicazione (non di class), con l’ulteriore requisito che tutti i miei codici di libreria comuni usassero il logger nel contesto della chiamata corrente. L’iniezione del registratore lo ha reso ansible. Certo, tutto il codice della libreria doveva essere cambiato: il logger è stato iniettato nei costruttori. All’inizio, ho resistito a questo approccio a causa di tutte le modifiche di codifica richieste; alla fine ho capito che i cambiamenti avevano molti vantaggi:

  • Il codice è diventato più semplice
  • Il codice divenne molto più robusto
  • Le dipendenze di una class diventarono ovvie
  • Se c’erano molte dipendenze, era una chiara indicazione che una class aveva bisogno di refactoring
  • Singletons statici sono stati eliminati
  • La necessità di oggetti di sessione o di contesto è scomparsa
  • Il multi-threading è diventato molto più semplice, perché il contenitore DI può essere costruito per contenere solo un thread, eliminando così la contaminazione incrociata involontaria

Inutile dire che ora sono un grande fan di DI, e lo uso per tutte le applicazioni tranne quelle più banali.

Siamo giunti a un compromesso: utilizzare DI ma raggruppare le dipendenze di primo livello in un object evitando il refactoring dell’inferno nel caso in cui tali dipendenze cambino.

Nell’esempio seguente, è ansible aggiungere a “ServiceDependencies” senza dover refactoring tutte le dipendenze derivate.

Esempio:

 public ServiceDependencies{ public ILogger Logger{get; private set;} public ServiceDependencies(ILogger logger){ this.Logger = logger; } } public abstract class BaseService{ public ILogger Logger{get; private set;} public BaseService(ServiceDependencies dependencies){ this.Logger = dependencies.Logger; //don't expose 'dependencies' } } public class DerivedService(ServiceDependencies dependencies, ISomeOtherDependencyOnlyUsedByThisService additionalDependency) : base(dependencies){ //set local dependencies here. } 

So che le persone stanno davvero dicendo che DI è l’unico buon modello IOC ma non capisco questo. Proverò a vendere SL un po ‘. Userò il nuovo framework MVC Core per mostrarti cosa intendo. I primi motori DI sono davvero complessi. Ciò che le persone intendono veramente quando dicono DI, è utilizzare alcune strutture come Unity, Ninject, Autofac … che fanno tutto il lavoro pesante per te, dove SL può essere semplice come fare una class di fabbrica. Per un piccolo progetto veloce questo è un modo semplice per fare IOC senza apprendere un intero framework per il corretto DI, potrebbero non essere così difficili da imparare ma comunque. Ora al problema che DI può diventare. Userò una citazione da documenti MVC Core. “ASP.NET Core è progettato da zero per supportare e sfruttare l’iniezione di dipendenza”. La maggior parte della gente dice che riguardo al DI “il 99% della propria base di codice non dovrebbe avere alcuna conoscenza del proprio contenitore IoC.” Quindi, perché dovrebbero progettare da zero se solo l’1% del codice dovrebbe esserne a conoscenza, il vecchio MVC non supportava il DI? Bene, questo è il grosso problema di DI dipende da DI. Far funzionare tutto “COME DOVREI FARLO” richiede molto lavoro. Se si osserva la nuova azione di iniezione, ciò non dipende da DI se si utilizza l’attributo [FromServices] . Ora la gente di DI dirà NO che supponiamo di andare con le Fabbriche, non questa roba, ma come puoi vedere nemmeno le persone che fanno MVC ha fatto bene. Il problema di DI è visibile anche in Filtri e guarda cosa devi fare per ottenere DI in un filtro

 public class SampleActionFilterAttribute : TypeFilterAttribute { public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl)) { } private class SampleActionFilterImpl : IActionFilter { private readonly ILogger _logger; public SampleActionFilterImpl(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); } public void OnActionExecuting(ActionExecutingContext context) { _logger.LogInformation("Business action starting..."); // perform some business logic work } public void OnActionExecuted(ActionExecutedContext context) { // perform some business logic work _logger.LogInformation("Business action completed."); } } } 

Dove se hai usato SL avresti potuto farlo con var _logger = Locator.Get () ;. E poi arriviamo ai Views. Con tutto ciò che c’è di buono riguardo a DI hanno dovuto usare SL per i punti di vista. la nuova syntax @inject StatisticsService StatsService è la stessa di var StatsService = Locator.Get(); . La parte più pubblicizzata di DI è il test unitario. Ma ciò che le persone stanno facendo è solo testare i servizi di simulazione senza scopo o dover colbind un motore DI per fare veri test. E so che puoi fare qualcosa di male ma le persone finiscono per creare un localizzatore SL anche se non sanno cosa sia. Dove non molte persone fanno DI senza mai leggerle prima. Il mio più grande problema con DI è che l’utente della class deve essere consapevole del funzionamento interno della class in altri per usarlo.
SL può essere utilizzato in modo ottimale e presenta alcuni vantaggi, soprattutto la sua semplicità.

Questo riguarda il “Service Locator è un anti-pattern” di Mark Seeman. Potrei sbagliarmi qui. Ma ho pensato di dover condividere anche i miei pensieri.

 public class OrderProcessor : IOrderProcessor { public void Process(Order order) { var validator = Locator.Resolve(); if (validator.Validate(order)) { var shipper = Locator.Resolve(); shipper.Ship(order); } } } 

Il metodo Process () per OrderProcessor in realtà non segue il principio “Inversion of Control”. Rompe anche il principio della responsabilità unica a livello di metodo. Perché un metodo dovrebbe riguardare l’istanziazione del

oggetti (tramite nuova o qualsiasi class SL) ha bisogno di realizzare qualsiasi cosa.

Invece di avere il metodo Process () per creare gli oggetti, il costruttore può effettivamente avere i parametri per i rispettivi oggetti (dipendenze di lettura) come mostrato di seguito. Quindi, come un Localizzatore di servizi può essere diverso da un COI

contenitore. E sarà di aiuto anche nel test delle unità.

 public class OrderProcessor : IOrderProcessor { public OrderProcessor(IOrderValidator validator, IOrderShipper shipper) { this.validator = validator; this.shipper = shipper; } public void Process(Order order) { if (this.validator.Validate(order)) { shipper.Ship(order); } } } //Caller public static void main() //this can be a unit test code too. { var validator = Locator.Resolve(); // similar to a IOC container var shipper = Locator.Resolve(); var orderProcessor = new OrderProcessor(validator, shipper); orderProcessor.Process(order); } 

So che questa domanda è un po ‘vecchia, ho solo pensato di dare il mio contributo.

In realtà, 9 volte su 10 non hai davvero bisogno di SL e dovresti fare affidamento su DI. Tuttavia, ci sono alcuni casi in cui si dovrebbe usare SL. Un’area in cui mi trovo a usare SL (o una sua variante) è nello sviluppo del gioco.

Un altro vantaggio di SL (a mio avviso) è la possibilità di passare internal classi internal .

Di seguito è riportato un esempio:

 internal sealed class SomeClass : ISomeClass { internal SomeClass() { // Add the service to the locator ServiceLocator.Instance.AddService(this); } // Maybe remove of service within finalizer or dispose method if needed. internal void SomeMethod() { Console.WriteLine("The user of my library doesn't know I'm doing this, let's keep it a secret"); } } public sealed class SomeOtherClass { private ISomeClass someClass; public SomeOtherClass() { // Get the service and call a method someClass = ServiceLocator.Instance.GetService(); someClass.SomeMethod(); } } 

Come puoi vedere, l’utente della biblioteca non ha idea di come questo metodo sia stato chiamato, perché non abbiamo DI, non che saremmo comunque in grado di farlo.