Esaminerò numericamente i tuoi punti, ma prima di tutto c'è qualcosa di cui dovresti stare molto attento: non confondere il modo in cui un consumatore utilizza una biblioteca con il modo in cui la biblioteca è implementata . Buoni esempi di questo sono Entity Framework (che tu stesso citi come una buona libreria) e MVC di ASP.NET. Entrambi fanno molto sotto il cofano, ad esempio con la riflessione, che non sarebbe assolutamente considerata un buon design se lo diffondessi attraverso il codice quotidiano.
Queste librerie sono assolutamente non "trasparente" come funzionano, o quello che stanno facendo dietro le quinte. Ma questo non è un danno, perché supportano buoni principi di programmazione nei loro consumatori . Quindi ogni volta che parli di una biblioteca come questa, ricorda che come consumatore di una biblioteca non è il tuo lavoro preoccuparti della sua implementazione o manutenzione. Dovresti solo preoccuparti di come aiuta o ostacola il codice che scrivi che utilizza la libreria. Non confondere questi concetti!
Quindi, per passare per punto:
Immediatamente raggiungiamo ciò che posso solo supporre sia un esempio di quanto sopra. Dici che i contenitori IOC impostano la maggior parte delle dipendenze come statiche. Bene, forse alcuni dettagli di implementazione di come funzionano includono l'archiviazione statica (anche se dato che tendono ad avere un'istanza di un oggetto come Ninject IKernel
come memoria principale, ne dubito persino). Ma questa non è una tua preoccupazione!
In realtà, un contenitore IoC è altrettanto esplicito con portata quanto l'iniezione di dipendenza dei poveri. (Continuerò a confrontare i contenitori IoC con i DI del povero perché confrontarli con nessun DI sarebbe ingiusto e confuso. E, per essere chiari, non sto usando "DI del povero" come peggiorativo.)
Nel DI del povero, avresti un'istanza manuale di una dipendenza, quindi la inietteresti nella classe che ne ha bisogno. Nel punto in cui costruisci la dipendenza, sceglieresti cosa fare con essa: archiviarla in una variabile con ambito locale, una variabile di classe, una variabile statica, non archiviarla affatto, qualunque cosa. È possibile passare la stessa istanza in molte classi oppure crearne una nuova per ogni classe. Qualunque cosa. Il punto è, per vedere quale sta accadendo, si guarda al punto - probabilmente vicino alla radice dell'applicazione - dove viene creata la dipendenza.
E che dire di un contenitore IoC? Bene, fai esattamente lo stesso! Ancora una volta, passando per Ninject terminologia, si guarda dove è configurato il legame, e scoprire se si dice qualcosa del tipo InTransientScope
, InSingletonScope
o qualsiasi altra cosa. Semmai questo è potenzialmente più chiaro, perché hai il codice proprio lì che dichiara il suo ambito, piuttosto che dover guardare attraverso un metodo per tenere traccia di ciò che accade a un oggetto (un oggetto può essere impostato su un blocco, ma viene usato più volte in quello blocco o solo una volta, per esempio). Quindi forse ti viene respinto dall'idea di dover utilizzare una funzionalità sul contenitore IoC piuttosto che una funzionalità in linguaggio grezzo per dettare l'ambito, ma finché ti fidi della tua libreria IoC - cosa che dovresti! - non ci sono svantaggi reali .
Ancora non so davvero di cosa stai parlando qui. I contenitori IoC considerano le proprietà private come parte della loro implementazione interna? Non so perché lo farebbero, ma ancora una volta, se lo fanno, non è una tua preoccupazione come viene implementata una libreria che stai usando.
O, forse, espongono una capacità come l'iniezione in setter privati? Onestamente non l'ho mai visto, e sono dubbioso sul fatto che questa sia davvero una caratteristica comune. Ma anche se è lì, è un semplice caso di uno strumento che può essere utilizzato in modo improprio. Ricorda, anche senza un contenitore IoC, è solo poche righe di codice Reflection per accedere e modificare una proprietà privata. È qualcosa che non dovresti quasi mai fare, ma ciò non significa che .NET sia dannoso per esporre la funzionalità. Se qualcuno utilizza in modo così evidente e sfrenato uno strumento, è colpa della persona, non dello strumento.
Il punto finale qui è simile a 2. La stragrande maggioranza delle volte, i contenitori IoC usano il costruttore! L'iniezione settata viene offerta per circostanze molto specifiche in cui, per motivi particolari, non è possibile utilizzare l'iniezione del costruttore. Chiunque usi costantemente l'iniezione di setter per nascondere quante dipendenze vengono passate, sta massacrando enormemente lo strumento. NON è colpa dello strumento, è loro.
Ora, se questo fosse un errore davvero facile da fare innocentemente, e uno che i contenitori IoC incoraggiano, allora va bene, forse avresti ragione. Sarebbe come rendere pubblico ogni membro della mia classe, quindi dare la colpa ad altre persone quando modificano cose che non dovrebbero, giusto? Chiunque utilizzi l'iniezione di setter per nascondere le violazioni di SRP o ignora volontariamente o ignora completamente i principi di progettazione di base. È irragionevole dare la colpa a questo nel contenitore IoC.
Questo è particolarmente vero perché è anche qualcosa che puoi fare altrettanto facilmente con il DI del povero:
var myObject
= new MyTerriblyLargeObject { DependencyA = new Thing(), DependencyB = new Widget(), Dependency C = new Repository(), ... };
Quindi, davvero, questa preoccupazione sembra completamente ortogonale all'utilizzo o meno di un contenitore IoC.
Cambiare il modo in cui le lezioni lavorano insieme non è una violazione di OCP. Se lo fosse, allora tutte le inversioni di dipendenza incoraggerebbero una violazione dell'OCP. Se così fosse, non sarebbero entrambi nello stesso acronimo SOLID!
Inoltre, né i punti a) né b) si avvicinano ad avere a che fare con OCP. Non so nemmeno come rispondere a queste domande rispetto all'OCP.
L'unica cosa che posso immaginare è che pensi che OCP abbia qualcosa a che fare con il comportamento che non viene modificato in fase di esecuzione o su dove nel codice è controllato il ciclo di vita delle dipendenze. Non è. OCP consiste nel non dover modificare il codice esistente quando i requisiti vengono aggiunti o cambiati. Si tratta solo di scrivere codice, non di come incollare insieme il codice che hai già scritto (anche se, naturalmente, l'accoppiamento lento è una parte importante del raggiungimento dell'OCP).
E un'ultima cosa che dici:
Ma non posso riporre la stessa fiducia in uno strumento di terze parti che sta modificando il mio codice compilato per eseguire soluzioni alternative a cose che non sarei in grado di fare manualmente.
Sì, puoi . Non c'è assolutamente alcun motivo per cui tu pensi che questi strumenti, fatti valere da un gran numero di progetti, siano più corretti o inclini a comportamenti imprevisti rispetto a qualsiasi altra libreria di terze parti.
appendice
Ho appena notato che anche i tuoi paragrafi introduttivi potrebbero usare alcuni indirizzi. Dici sardonicamente che i contenitori IoC "non stanno usando una tecnica segreta di cui non abbiamo mai sentito parlare" per evitare codice disordinato e soggetto a duplicazione per creare un grafico delle dipendenze. E hai ragione, quello che stanno facendo è in realtà affrontare queste cose con le stesse tecniche di base come facciamo sempre i programmatori.
Lascia che ti parli attraverso uno scenario ipotetico. Come programmatore, hai messo insieme una grande applicazione, e nel punto di ingresso, dove stai costruendo il tuo grafico a oggetti, noti che hai un codice piuttosto disordinato. Ci sono alcune classi che vengono usate più e più volte e ogni volta che ne costruisci una devi ricostruire l'intera catena di dipendenze sotto di esse. Inoltre, scopri di non avere alcun modo espressivo per dichiarare o controllare il ciclo di vita delle dipendenze, ad eccezione del codice personalizzato per ognuna. Il tuo codice non è strutturato e pieno di ripetizioni. Questa è la confusione di cui parli nel tuo paragrafo introduttivo.
Quindi, per prima cosa, inizi a riformattare un po '- dove un po' di codice ripetuto è abbastanza strutturato da estrarlo in metodi di supporto, e così via. Ma poi inizi a pensare: è un problema che potrei forse affrontare in senso generale , uno che non è specifico di questo particolare progetto ma potrebbe aiutarti in tutti i tuoi progetti futuri?
Quindi ti siedi, ci pensi e decidi che dovrebbe esserci una classe in grado di risolvere le dipendenze. E fai uno schizzo dei metodi pubblici di cui avrebbe bisogno:
void Bind(Type interfaceType, Type concreteType, bool singleton);
T Resolve<T>();
Bind
dice "dove vedi un argomento di tipo costruttivo interfaceType
, passa un'istanza di concreteType
". Il singleton
parametro aggiuntivo indica se utilizzare la stessa istanza di concreteType
ogni volta o crearne sempre una nuova.
Resolve
proverà semplicemente a costruire T
con qualsiasi costruttore che possa trovare i cui argomenti sono tutti i tipi che sono stati precedentemente associati. Può anche chiamarsi in modo ricorsivo per risolvere le dipendenze fino in fondo. Se non è in grado di risolvere un'istanza perché non tutto è stato associato, genera un'eccezione.
Puoi provare a implementarlo tu stesso e scoprirai che hai bisogno di un po 'di riflessione e di un po' di cache per gli attacchi dove singleton
è vero, ma certamente niente di drastico o terrificante. E una volta terminato , voilà , hai il nucleo del tuo contenitore IoC! È davvero così spaventoso? L'unica vera differenza tra questo e Ninject o StructureMap o Castle Windsor o qualunque cosa tu preferisca è che quelli hanno molte più funzionalità per coprire i (molti!) Casi d'uso in cui questa versione di base non sarebbe sufficiente. Ma al centro, quello che hai lì è l'essenza di un contenitore IoC.
IOC
edExplicitness of code
è esattamente la cosa con cui ho problemi. Il DI manuale può facilmente diventare un lavoro ingrato, ma almeno mette il flusso esplicito del programma autonomo in un unico posto: "Ottieni ciò che vedi, vedi ciò che ottieni". Mentre i binding e le dichiarazioni dei contenitori IOC possono facilmente diventare un programma parallelo nascosto da solo.