Iniezione delle dipendenze e produttività dello sviluppo

Astratto

Negli ultimi mesi ho programmato un motore di gioco leggero basato su C # con astrazione API ed entity framework / componente / sistema di scripting. L’idea è di facilitare il processo di sviluppo del gioco in XNA, SlimDX e simili, fornendo un’architettura simile a quella del motore Unity.

Sfide di design

Come molti sviluppatori di giochi sanno, ci sono molti servizi diversi per accedere al codice. Molti sviluppatori ricorrono all’utilizzo di istanze statiche globali, ad esempio un gestore di rendering (o un compositore), una scena, un dispositivo grafico (DX), un logger, uno stato di input, una finestra di visualizzazione, una finestra e così via. Esistono alcuni approcci alternativi alle istanze / singleton globali statici. Uno è quello di assegnare a ciascuna class un’istanza delle classi a cui è necessario accedere, tramite un costruttore o una dipendenza di dipendenza costruttore / proprietà (DI), un’altra è l’utilizzo di un servizio di localizzazione globale, come ObjectFactory di StructureMap in cui il localizzatore di servizio viene solitamente configurato come un contenitore IoC.

Iniezione di dipendenza

Ho scelto di andare in DI per molte ragioni. La più ovvia è la testabilità, programmando contro le interfacce e avendo tutte le dipendenze di ogni class fornita loro tramite un costruttore, tali classi sono facilmente testate poiché il contenitore di test può istanziare i servizi richiesti, o i loro gesti e inserirli in ogni class da testare. Un’altra ragione per fare DI / IoC era, credeteci o no, aumentare la leggibilità del codice. Niente più enormi processi di inizializzazione che istanziano tutti i diversi servizi e istanzano manualmente le classi con riferimenti ai servizi richiesti. La configurazione del kernel (NInject) / Registry (StructureMap) fornisce convenientemente un singolo punto di configurazione per il motore / gioco, in cui le implementazioni dei servizi vengono selezionate e configurate.

I miei problemi

  • Mi sento spesso come se stessi creando interfacce per il bene delle interfacce
  • La mia produttività è diminuita drasticamente dal momento che tutto ciò che faccio è preoccuparsi di come fare le cose con il DI-way, invece del modo statico globale veloce e semplice.
  • In alcuni casi, ad esempio durante l’istanziazione di nuove quadro su runtime, è necessario accedere al contenitore / kernel IoC per creare l’istanza. Questo crea una dipendenza dal contenitore IoC stesso (ObjectFactory in SM, un’istanza del kernel in Ninject) , che in realtà va contro il motivo dell’utilizzo di uno in primo luogo. Come può essere risolto? Le fabbriche astratte vengono in mente, ma questo complica ulteriormente il codice.
  • A seconda dei requisiti del servizio, i costruttori di alcune classi possono diventare molto grandi, il che renderà la class completamente inutile in altri contesti in cui e se un IoC non viene utilizzato.

Fondamentalmente, il DI / IoC riduce drasticamente la mia produttività e in alcuni casi complica ulteriormente il codice e l’architettura. Quindi sono incerto se si tratti di un percorso che dovrei seguire, o semplicemente di rinunciare e fare le cose alla vecchia maniera. Non sto cercando una risposta singola che dica cosa dovrei o non dovrei fare, ma una discussione su se usare DI vale la pena a lungo andare invece di usare il modo statico / singleton globale, possibili pro e contro che ho trascurato e possibili soluzioni ai miei problemi sopra elencati, quando si tratta di DI.

Dovresti tornare alla vecchia maniera? La mia risposta in breve è no. DI ha numerosi vantaggi per tutti i motivi che hai menzionato.

Mi sento spesso come se stessi creando interfacce per il bene delle interfacce

Se lo fai, potresti violare il principio delle astrazioni riutilizzate (RAP)

A seconda dei requisiti del servizio, i costruttori di alcune classi possono diventare molto grandi, il che renderà la class completamente inutile in altri contesti in cui e se un IoC non viene utilizzato.

Se i tuoi costruttori di classi sono troppo grandi e complessi, questo è il modo migliore per dimostrare che stai violando un altro principio molto importante: Principio della singola responsabilità . In questo caso è il momento di estrarre e rifattorizzare il codice in classi diverse, il numero di dipendenze suggerito è di circa 4.

Per fare DI non devi avere un’interfaccia, DI è solo il modo in cui ottieni le tue dipendenze nel tuo object. La creazione di interfacce potrebbe essere un modo necessario per poter sostituire una dipendenza a scopo di test. A meno che l’object della dipendenza sia:

  1. Facile da isolare
  2. Non parla con sottosistemi esterni (file system, ecc.)

Puoi creare la tua dipendenza come una class astratta o qualsiasi class in cui i metodi che desideri sostituire sono virtuali. Tuttavia le interfacce creano il miglior modo disaccoppiato di una dipendenza.

In alcuni casi, ad esempio durante l’istanziazione di nuove entity framework su runtime, è necessario accedere al contenitore / kernel IoC per creare l’istanza. Questo crea una dipendenza dal contenitore IoC stesso (ObjectFactory in SM, un’istanza del kernel in Ninject), che in realtà va contro il motivo dell’utilizzo di uno in primo luogo. Come può essere risolto? Le fabbriche astratte vengono in mente, ma questo complica ulteriormente il codice.

Per quanto riguarda una dipendenza dal contenitore IOC, non si dovrebbe mai avere una dipendenza ad esso nelle classi client. E loro non devono.

Per utilizzare correttamente l’iniezione di dipendenza, è necessario comprendere il concetto di Radice di composizione . Questo è l’unico posto in cui il tuo contenitore dovrebbe essere referenziato. A questo punto viene costruito l’intero grafico dell’object. Una volta compreso questo, ti renderai conto che non hai mai bisogno del contenitore nei tuoi clienti. Come ogni cliente riceve la sua dipendenza iniettata.

Ci sono anche MOLTI altri pattern creativi che puoi seguire per semplificare la costruzione: di ‘che vuoi build un object con molte dipendenze come questa:

new SomeBusinessObject( new SomethingChangedNotificationService(new EmailErrorHandler()), new EmailErrorHandler(), new MyDao(new EmailErrorHandler())); 

È ansible creare una fabbrica concreta che sappia come costruirlo:

 public static class SomeBusinessObjectFactory { public static SomeBusinessObject Create() { return new SomeBusinessObject( new SomethingChangedNotificationService(new EmailErrorHandler()), new EmailErrorHandler(), new MyDao(new EmailErrorHandler())); } } 

E quindi usarlo in questo modo:

  SomeBusinessObject bo = SomeBusinessObjectFactory.Create(); 

Puoi anche usare poveri mans di e creare un costruttore che non accetta argomenti affatto:

 public SomeBusinessObject() { var errorHandler = new EmailErrorHandler(); var dao = new MyDao(errorHandler); var notificationService = new SomethingChangedNotificationService(errorHandler); Initialize(notificationService, errorHandler, dao); } protected void Initialize( INotificationService notifcationService, IErrorHandler errorHandler, MyDao dao) { this._NotificationService = notifcationService; this._ErrorHandler = errorHandler; this._Dao = dao; } 

Quindi sembra che funzionasse:

 SomeBusinessObject bo = new SomeBusinessObject(); 

L’uso di DI di Poor Man è considerato negativo quando le implementazioni predefinite sono in librerie esterne di terze parti, ma meno male quando si dispone di una buona implementazione predefinita.

Quindi ovviamente ci sono tutti i contenitori DI, Object builder e altri pattern.

Quindi tutto ciò che serve è pensare a un buon modello creativo per il tuo object. Il tuo stesso object non dovrebbe preoccuparsi di come creare le dipendenze, in effetti le rende PIÙ complicate e le induce a mescolare 2 tipi di logica. Quindi non credo che l’uso di DI dovrebbe avere una perdita di produttività.

Ci sono alcuni casi speciali in cui il tuo object non può semplicemente farsi iniettare una singola istanza. Dove la durata è generalmente più breve e occorrono istanze immediate. In questo caso devi iniettare la Factory nell’object come dipendenza:

 public interface IDataAccessFactory { TDao Create(); } 

Come puoi notare questa versione è generica perché può utilizzare un contenitore IoC per creare vari tipi (Prendi nota anche se il contenitore IoC non è ancora visibile al mio cliente).

 public class ConcreteDataAccessFactory : IDataAccessFactory { private readonly IocContainer _Container; public ConcreteDataAccessFactory(IocContainer container) { this._Container = container; } public TDao Create() { return (TDao)Activator.CreateInstance(typeof(TDao), this._Container.Resolve(), this._Container.Resolve()) } } 

Notate che ho usato l’triggerstore anche se avevo un contenitore Ioc, questo è importante notare che la fabbrica ha bisogno di build una nuova istanza di object e non solo assumere che il contenitore fornirà una nuova istanza in quanto l’object può essere registrato con diverse durate (Singleton , ThreadLocal, ecc.). Tuttavia, a seconda del contenitore in uso, è ansible generare queste fabbriche. Tuttavia, se sei certo che l’object è registrato con la durata del Transitorio, puoi semplicemente risolverlo.

EDIT: aggiunta di classi con dipendenza Abstract Factory:

 public class SomeOtherBusinessObject { private IDataAccessFactory _DataAccessFactory; public SomeOtherBusinessObject( IDataAccessFactory dataAccessFactory, INotificationService notifcationService, IErrorHandler errorHandler) { this._DataAccessFactory = dataAccessFactory; } public void DoSomething() { for (int i = 0; i < 10; i++) { using (var dao = this._DataAccessFactory.Create()) { // work with dao // Console.WriteLine( // "Working with dao: " + dao.GetHashCode().ToString()); } } } } 

Fondamentalmente, il DI / IoC riduce drasticamente la mia produttività e in alcuni casi complica ulteriormente il codice e l’architettura

Mark Seeman ha scritto un blog fantastico sull’argomento, e ha risposto alla domanda: La mia prima reazione a questo tipo di domande è: tu dici che il codice liberamente accoppiato è più difficile da capire. Più difficile di cosa?

Accoppiamento sciolto e l’immagine grande

EDIT: Infine vorrei sottolineare che non tutti gli oggetti e le dipendenze necessitano o dovrebbero essere iniettati in dipendenza, in primo luogo considerare se ciò che si sta utilizzando è in realtà considerato una dipendenza:

Quali sono le dipendenze?

  • Configurazione dell’applicazione
  • Risorse di sistema (orologio)
  • Librerie di terze parti
  • Banca dati
  • WCF / Servizi di rete
  • Sistemi esterni (file / email)

Qualsiasi object o collaboratore sopraindicato può essere fuori dal tuo controllo e causare effetti collaterali e differenze di comportamento e rendere difficile il test. Questi sono i tempi per considerare un’Astrazione (Classe / Interfaccia) e usare DI.

Cosa non sono le dipendenze, non ha davvero bisogno DI?

  • List
  • MemoryStream
  • Strings / Primitives
  • Oggetti foglia / Dto’s

Oggetti come sopra possono essere semplicemente istanziati dove necessario usando la new parola chiave. Non suggerirei di usare DI per oggetti così semplici a meno che non ci siano ragioni specifiche. Considerare la domanda se l’object è sotto il pieno controllo e non causa alcun object grafico o effetti collaterali aggiuntivi nel comportamento (almeno qualsiasi cosa che si desidera modificare / controllare il comportamento o il test). In questo caso, semplicemente nuovi.

Ho postato molti link ai post di Mark Seeman, ma vi consiglio davvero di leggere il suo libro e i post del blog.