Pratiche relative al contenitore Injection / IoC per le dipendenze durante la scrittura di framework


18

Ho usato vari contenitori IoC (Castle.Windsor, Autofac, MEF, ecc.) Per .Net in numerosi progetti. Ho scoperto che tendono ad essere frequentemente abusati e incoraggiano una serie di cattive pratiche.

Esistono pratiche consolidate per l'utilizzo del contenitore IoC, in particolare quando si fornisce una piattaforma / framework? Il mio obiettivo come scrittore di framework è rendere il codice il più semplice e facile da usare possibile. Preferirei scrivere una riga di codice per costruire un oggetto piuttosto che dieci o anche solo due.

Ad esempio, un paio di odori di codice che ho notato e non hanno buoni suggerimenti per:

  1. Gran numero di parametri (> 5) per i costruttori. La creazione di servizi tende ad essere complessa; tutte le dipendenze vengono iniettate tramite il costruttore - nonostante il fatto che i componenti siano raramente opzionali (tranne forse durante i test).

  2. Mancanza di lezioni private e interne; questo potrebbe essere un limite specifico dell'utilizzo di C # e Silverlight, ma sono interessato a come è stato risolto. È difficile stabilire quale sia un'interfaccia di framework se tutte le classi sono pubbliche; mi consente di accedere a parti private che probabilmente non dovrei toccare.

  3. Accoppiamento del ciclo di vita dell'oggetto al contenitore IoC. Spesso è difficile costruire manualmente le dipendenze richieste per creare oggetti. Il ciclo di vita degli oggetti è troppo spesso gestito dal framework IoC. Ho visto progetti in cui la maggior parte delle classi sono registrate come Singletons. Si ottiene una mancanza di controllo esplicito e si è anche costretti a gestire gli interni (si riferisce al punto precedente, tutte le classi sono pubbliche e bisogna iniettarle).

Ad esempio, .Net framework ha molti metodi statici. come DateTime.UtcNow. Molte volte l'ho visto avvolto e iniettato come parametro di costruzione.

A seconda dell'implementazione concreta, il mio codice è difficile da testare. L'iniezione di una dipendenza rende il mio codice difficile da usare, soprattutto se la classe ha molti parametri.

Come posso fornire sia un'interfaccia testabile che una facile da usare? Quali sono le migliori pratiche?


1
Quali cattive pratiche incoraggiano? Per quanto riguarda le lezioni private, non dovresti averne molte se hai un buon incapsulamento; per prima cosa, sono molto più difficili da testare.
Aaronaught,

Err, 1 e 2 sono le cattive pratiche. Costruttori di grandi dimensioni e non usare l'incapsulamento per stabilire un'interfaccia minima? Inoltre, ho detto privato e interno (usare interni visibili a, per testare). Non ho mai usato un framework che mi costringe a usare un particolare contenitore IoC.
Dave Hillier,

2
È possibile che forse un segno della necessità di molti parametri per i costruttori della tua classe sia esso stesso l'odore del codice e il DIP lo sta rendendo più ovvio?
dreza,

3
L'uso di un contenitore IoC specifico in un framework generalmente non è una buona pratica. L'uso dell'iniezione di dipendenza è una buona pratica. Sei consapevole della differenza?
Aaronaught il

1
@Aaronaught (di ritorno dai morti). L'uso di "iniezione di dipendenza è una buona pratica" è falso. L'iniezione delle dipendenze è uno strumento che dovrebbe essere usato solo quando appropriato. L'uso dell'iniezione di dipendenza è sempre una cattiva pratica.
Dave Hillier,

Risposte:


27

L'unico anti-pattern di iniezione di dipendenza legittimo di cui sono a conoscenza è il pattern Service Locator , che è un anti-pattern quando viene utilizzato un framework DI per esso.

Tutti gli altri cosiddetti anti-pattern DI di cui ho sentito parlare, qui o altrove, sono solo casi leggermente più specifici di anti-pattern generali di progettazione OO / software. Per esempio:

  • L'iniezione eccessiva da parte del costruttore costituisce una violazione del principio della responsabilità singola . Troppi argomenti del costruttore indicano troppe dipendenze; troppe dipendenze indicano che la classe sta provando a fare troppo. Di solito questo errore è correlato ad altri odori di codice, come nomi di classi insolitamente lunghi o ambigui ("manager"). Gli strumenti di analisi statica sono in grado di rilevare facilmente un eccessivo accoppiamento afferente / efferente.

  • L'iniezione di dati, al contrario del comportamento, è un sottotipo dell'anti-pattern poltergeist , con il 'geist in questo caso il contenitore. Se una classe deve essere a conoscenza della data e dell'ora correnti, non iniettare a DateTime, che sono dati; invece, si inietta un'astrazione sull'orologio di sistema (di solito chiamo il mio ISystemClock, anche se penso che ce ne sia uno più generale nel progetto SystemWrappers ). Questo non è corretto solo per DI; è assolutamente essenziale per la testabilità, in modo da poter testare le funzioni che variano nel tempo senza doverle effettivamente attendere.

  • Dichiarare ogni ciclo di vita come Singleton è, per me, un perfetto esempio di programmazione di culto del carico e, in misura minore, il " pozzo nero " colloquialmente chiamato . Ho visto più abusi singleton di quanto non mi ricordi, e molto poco riguarda DI.

  • Un altro errore comune sono i tipi di interfaccia specifici dell'implementazione (con nomi strani come IOracleRepository) fatti proprio per essere in grado di registrarlo nel contenitore. Questo è di per sé una violazione del principio di inversione di dipendenza (solo perché è un'interfaccia, non significa che sia veramente astratto) e spesso include anche un gonfiamento di interfaccia che viola il principio di segregazione dell'interfaccia .

  • L'ultimo errore che di solito vedo è la "dipendenza opzionale", che hanno fatto in NerdDinner . In altre parole, esiste un costruttore che accetta l'iniezione di dipendenza, ma anche un altro costruttore che utilizza un'implementazione "predefinita". Ciò viola anche il DIP e tende a portare anche a violazioni di LSP , poiché gli sviluppatori, nel tempo, iniziano a fare ipotesi sull'implementazione predefinita e / o avviano nuove istanze utilizzando il costruttore predefinito.

Come dice il vecchio proverbio, puoi scrivere FORTRAN in qualsiasi lingua . Dependency Injection non è una pallottola d'argento che impedirà agli sviluppatori da avvitare la loro gestione delle dipendenze, ma non prevenire una serie di errori comuni / anti-pattern:

...e così via.

Ovviamente non si vuole progettare un quadro di dipendere da una specifica contenitore CIO realizzazione , come l'Unità o autofac. Ciò sta, ancora una volta, violando il DIP. Ma se ti ritrovi a pensare anche a fare qualcosa del genere, allora devi aver già commesso diversi errori di progettazione, perché l'iniezione di dipendenza è una tecnica di gestione delle dipendenze per scopi generali e non è legata al concetto di contenitore IoC.

Tutto può costruire un albero delle dipendenze; forse è un contenitore IoC, forse è un test unitario con un sacco di beffe, forse è un test driver che fornisce dati fittizi. Al tuo framework non dovrebbe interessare e alla maggior parte dei framework che ho visto non importa, ma fanno ancora un uso pesante dell'iniezione di dipendenza in modo che possa essere facilmente integrato nel contenitore IoC preferito dell'utente finale.

DI non è scienza missilistica. Cerca solo di evitare newe statictranne quando c'è un motivo convincente per usarli, come un metodo di utilità che non ha dipendenze esterne o una classe di utilità che non potrebbe avere alcuno scopo al di fuori del framework (wrapper di interoperabilità e chiavi del dizionario sono esempi comuni di Questo).

Molti dei problemi con i framework IoC emergono quando gli sviluppatori stanno imparando come usarli e invece di cambiare effettivamente il modo in cui gestiscono le dipendenze e le astrazioni per adattarsi al modello IoC, cercano invece di manipolare il contenitore IoC per soddisfare le aspettative dei loro vecchio stile di codifica, che comporta spesso un elevato accoppiamento e una bassa coesione. Il codice errato è un codice errato, che utilizzi o meno tecniche DI.


Ho alcune domande. Per una libreria che è distribuita su NuGet, non posso dipendere da un sistema IoC come viene fatto dall'applicazione che utilizza la libreria, non dalla libreria stessa. In molti casi, l'utente della libreria non si preoccupa affatto delle sue dipendenze interne. C'è un problema con l'avere un costruttore predefinito che avvia le dipendenze interne e un costruttore più lungo per unit testing e / o implementazioni personalizzate? Inoltre dici di evitare "nuovo". Questo vale per cose come StringBuilder, DateTime e TimeSpan?
Etienne Charland,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.