L'iniezione di dipendenza deve andare a scapito dell'incapsulamento?


128

Se ho capito bene, il meccanismo tipico di Dependency Injection è quello di iniettare attraverso un costruttore di classe o attraverso una proprietà pubblica (membro) della classe.

Questo espone la dipendenza da iniettare e viola il principio OOP dell'incapsulamento.

Sono corretto nell'identificare questo compromesso? Come gestisci questo problema?

Di seguito, vedi anche la mia risposta alla mia domanda.


7
questa è una domanda molto intelligente imho
dfa,

5
Per prima cosa rispondere a questa domanda richiede una discussione sul significato dell'incapsulamento . ;)
Jeff Sternal,

2
L'incapsulamento è gestito dalle interfacce. Espongono le caratteristiche essenziali dell'oggetto e nascondono dettagli come le dipendenze. Questo ci consente di "aprire" le classi in una certa misura in modo da fornire una configurazione più flessibile.
Lawrence Wagerfield,

Risposte:


62

C'è un altro modo di esaminare questo problema che potresti trovare interessante.

Quando usiamo l'iniezione IoC / dipendenza, non stiamo usando i concetti OOP. Certo, stiamo usando un linguaggio OO come "host", ma le idee alla base dell'IoC provengono dall'ingegneria del software orientata ai componenti, non dall'OO.

Il software componente riguarda la gestione delle dipendenze: un esempio di uso comune è il meccanismo di assemblaggio di .NET. Ogni assembly pubblica l'elenco degli assembly a cui fa riferimento e ciò rende molto più semplice riunire (e convalidare) i pezzi necessari per un'applicazione in esecuzione.

Applicando tecniche simili nei nostri programmi OO tramite IoC, miriamo a semplificare la configurazione e la manutenzione dei programmi. La pubblicazione delle dipendenze (come parametri del costruttore o altro) è una parte fondamentale di questo. L'incapsulamento non si applica davvero, come nel mondo orientato ai componenti / servizi, non esiste un "tipo di implementazione" per i dettagli da cui trapelare.

Sfortunatamente i nostri linguaggi attualmente non separano i concetti a grana fine e orientati agli oggetti da quelli a grana grossa orientati ai componenti, quindi questa è una distinzione che devi tenere a mente solo :)


18
L'incapsulamento non è solo una parte della terminologia elaborata. È una cosa reale con vantaggi reali, e non importa se consideri il tuo programma "orientato ai componenti" o "orientato agli oggetti". L'incapsulamento dovrebbe proteggere lo stato del tuo oggetto / componente / servizio / qualunque cosa venga modificata in modi inaspettati, e l'IoC elimina parte di questa protezione, quindi c'è sicuramente un compromesso.
Ron Inbar,

1
Gli argomenti forniti da un costruttore rientrano ancora nel regno dei modi previsti in cui l'oggetto può essere "cambiato": sono esplicitamente esposti e gli invarianti che li circondano vengono applicati. Nascondere le informazioni è il termine migliore per il tipo di privacy a cui ti riferisci, @RonInbar, e non è necessariamente sempre utile (rende più difficile districare la pasta ;-)).
Nicholas Blumhardt,

2
L'intero punto di OOP è che il groviglio di pasta è separato in classi separate e che devi solo confonderlo se è il comportamento di quella particolare classe che desideri modificare (è così che OOP mitiga la complessità). Una classe (o modulo) incapsula i suoi interni mentre espone una comoda interfaccia pubblica (ecco come OOP facilita il riutilizzo). Una classe che espone le sue dipendenze tramite interfacce crea complessità per i suoi clienti ed è di conseguenza meno riutilizzabile. È anche intrinsecamente più fragile.
Neutrino,

1
Indipendentemente dal modo in cui lo guardo, mi sembra che DI mina seriamente alcuni dei vantaggi più preziosi di OOP e non ho ancora incontrato una situazione in cui l'ho effettivamente trovato usato in un modo che ha risolto un reale problema esistente.
Neutrino,

1
Ecco un altro modo per inquadrare il problema: immagina se gli assembly .NET hanno scelto "incapsulamento" e non hanno dichiarato su quali altri assembly hanno fatto affidamento. Sarebbe una situazione folle, leggere documenti e sperare solo che qualcosa funzioni dopo averlo caricato. La dichiarazione delle dipendenze a quel livello consente agli strumenti automatizzati di gestire la composizione su larga scala dell'app. Devi vedere gli occhi per vedere l'analogia, ma forze simili si applicano a livello di componente. Ci sono compromessi e YMMV come sempre :-)
Nicholas Blumhardt,

29

È una buona domanda - ma a un certo punto, l'incapsulamento nella sua forma più pura deve essere violato se l'oggetto deve mai soddisfare la sua dipendenza. Alcuni provider della dipendenza devono sapere sia che l'oggetto in questione richiede un Foo, e il provider deve avere un modo per fornire Fool'oggetto.

Classicamente quest'ultimo caso viene gestito come dici tu, attraverso argomenti di costruzione o metodi setter. Tuttavia, questo non è necessariamente vero: so che le ultime versioni del framework Spring DI in Java, ad esempio, consentono di annotare i campi privati ​​(ad es. Con @Autowired) e la dipendenza verrà impostata tramite reflection senza che sia necessario esporre la dipendenza tramite uno qualsiasi dei metodi / costruttori pubblici delle classi. Questo potrebbe essere il tipo di soluzione che stavi cercando.

Detto questo, non penso che neanche l'iniezione del costruttore sia un grosso problema. Ho sempre pensato che gli oggetti dovrebbero essere pienamente validi dopo la costruzione, in modo tale che tutto ciò di cui hanno bisogno per svolgere il loro ruolo (cioè essere in uno stato valido) debba essere comunque fornito attraverso il costruttore. Se hai un oggetto che richiede a un collaboratore di lavorare, mi sembra che il costruttore pubblicizzi pubblicamente questo requisito e assicuri che sia soddisfatto quando viene creata una nuova istanza della classe.

Idealmente quando si ha a che fare con oggetti, si interagisce comunque con loro attraverso un'interfaccia e più si fa questo (e si hanno dipendenze cablate tramite DI), meno si deve realmente fare i conti con i costruttori. Nella situazione ideale, il tuo codice non si occupa né crea mai istanze concrete di classi; così viene semplicemente dato un IFooDI attraverso, senza preoccuparsi di ciò di cui il costruttore FooImplindica che ha bisogno per fare il suo lavoro, e di fatto senza nemmeno essere a conoscenza FooImpldell'esistenza. Da questo punto di vista, l'incapsulamento è perfetto.

Questa è un'opinione ovviamente, ma a mio avviso DI non necessariamente viola l'incapsulamento e in effetti può aiutarlo centralizzando tutte le conoscenze necessarie degli interni in un unico posto. Non solo è una buona cosa in sé, ma ancora meglio questo posto è al di fuori della tua base di codice, quindi nessuno del codice che scrivi deve conoscere le dipendenze delle classi.


2
Punti buoni. Sconsiglio di utilizzare @Autowired in campi privati; ciò rende la classe difficile da testare; come si iniettano poi beffe o tronconi?
Lumpynose,

4
Non sono d'accordo. DI viola l'incapsulamento e questo può essere evitato. Ad esempio, utilizzando un ServiceLocator, che ovviamente non ha bisogno di sapere nulla sulla classe client; deve solo conoscere le implementazioni della dipendenza Foo. Ma la cosa migliore, nella maggior parte dei casi, è semplicemente usare il "nuovo" operatore.
Rogério,

5
@Rogerio - Probabilmente qualsiasi framework DI si comporta esattamente come il ServiceLocator che descrivi; il client non conosce nulla di specifico sulle implementazioni Foo e lo strumento DI non conosce nulla di specifico sul client. L'uso di "nuovo" è molto peggio per violare l'incapsulamento, poiché è necessario conoscere non solo la classe di implementazione esatta, ma anche le classi e le istanze esatte di tutte le dipendenze di cui ha bisogno.
Andrzej Doyle,

4
L'uso di "nuovo" per creare un'istanza di una classe di supporto, che spesso non è nemmeno pubblica, promuove l'incapsulamento. L'alternativa DI sarebbe quella di rendere pubblica la classe helper e aggiungere un costruttore o setter pubblico nella classe client; entrambe le modifiche spezzerebbero l'incapsulamento fornito dalla classe helper originale.
Rogério,

1
"È una buona domanda - ma a un certo punto, l'incapsulamento nella sua forma più pura deve essere violato se si vuole che la sua dipendenza sia soddisfatta" La premessa alla base della tua risposta è semplicemente falsa. Come @ Rogério afferma di rinnovare internamente una dipendenza, e qualsiasi altro metodo in cui un oggetto satura internamente le proprie dipendenze non viola l'incapsulamento.
Neutrino,

17

Questo espone la dipendenza da iniettare e viola il principio OOP dell'incapsulamento.

Bene, francamente, tutto viola l'incapsulamento. :) È una specie di tenero principio che deve essere trattato bene.

Quindi, cosa viola l'incapsulamento?

L'ereditarietà lo fa .

"Poiché l'ereditarietà espone una sottoclasse ai dettagli dell'implementazione dei suoi genitori, si dice spesso che" l'ereditarietà rompe l'incapsulamento "". (Gang of Four 1995: 19)

La programmazione orientata all'aspetto lo fa . Ad esempio, ti registri su callMethodCall () e questo ti dà una grande opportunità per iniettare codice nella normale valutazione del metodo, aggiungendo strani effetti collaterali ecc.

Dichiarazione Friend in C ++ fa .

L'estensione di classe in Ruby lo fa . Basta ridefinire un metodo di stringa da qualche parte dopo che una classe di stringa è stata completamente definita.

Bene, molte cose fanno .

L'incapsulamento è un principio buono e importante. Ma non l'unico.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
"Questi sono i miei principi, e se non ti piacciono ... beh, ne ho altri." (Groucho Marx)
Ron Inbar,

2
Penso che questo sia una specie di punto. È iniezione dipendente vs incapsulamento. Quindi usa l'iniezione dipendente solo quando dà benefici significativi. È il DI ovunque che dà a DI un brutto nome
Richard Tingle,

Non sei sicuro di cosa stia cercando di dire questa risposta ... Che è giusto violare l'incapsulamento quando si fa DI, che è "sempre ok" perché sarebbe comunque violato, o semplicemente che DI potrebbe essere un motivo per violare l'incapsulamento? In un'altra nota, oggigiorno non è più necessario fare affidamento su costruttori o proprietà pubblici per iniettare dipendenze; invece, possiamo iniettare in campi annotati privati , che è più semplice (meno codice) e conserva l'incapsulamento. Quindi, possiamo trarre vantaggio da entrambi i principi contemporaneamente.
Rogério,

L'ereditarietà non viola in linea di principio l'incapsulamento, sebbene ciò possa accadere se la classe genitore è scritta male. Gli altri punti che sollevi sono un paradigma di programmazione abbastanza marginale e diverse funzionalità linguistiche che hanno poco a che fare con l'architettura o il design.
Neutrino,

13

Sì, DI viola l'incapsulamento (noto anche come "nascondere le informazioni").

Ma il vero problema si presenta quando gli sviluppatori lo usano come scusa per violare i principi KISS (Keep It Short and Simple) e YAGNI (You Ain't Gonna Need It).

Personalmente, preferisco soluzioni semplici ed efficaci. Uso principalmente il "nuovo" operatore per creare istanze di dipendenze con stato quando e dove sono necessarie. È semplice, ben incapsulato, facile da capire e facile da testare. Quindi perche no?


Pensare in anticipo non è terribile, ma sono d'accordo Keep It Simple, Stupid, soprattutto se non ne avrai bisogno! Ho visto gli sviluppatori sprecare cicli perché stanno progettando qualcosa per essere troppo a prova di futuro, e basato su intuizioni a questo, nemmeno requisiti di business che sono noti / sospettati.
Jacob McKay,

5

Un buon contenitore / sistema di iniezione depenancy consentirà l'iniezione del costruttore. Gli oggetti dipendenti saranno incapsulati e non dovranno essere esposti pubblicamente. Inoltre, usando un sistema DP, nessuno dei tuoi codici "conosce" nemmeno i dettagli di come viene costruito l'oggetto, possibilmente includendo anche l'oggetto che viene costruito. In questo caso c'è più incapsulamento poiché quasi tutto il codice non solo è protetto dalla conoscenza degli oggetti incapsulati, ma non partecipa nemmeno alla costruzione degli oggetti.

Ora, presumo che tu stia confrontando il caso in cui l'oggetto creato crea i suoi oggetti incapsulati, molto probabilmente nel suo costruttore. La mia comprensione di DP è che vogliamo togliere questa responsabilità dall'oggetto e darlo a qualcun altro. A tal fine, "qualcun altro", che in questo caso è il contenitore DP, ha una conoscenza intima che "viola" l'incapsulamento; il vantaggio è che estrae quella conoscenza dall'oggetto stesso. Qualcuno deve averlo. Il resto della tua applicazione no.

Vorrei pensarlo in questo modo: il contenitore / sistema di iniezione della dipendenza viola l'incapsulamento, ma il tuo codice no. In effetti, il tuo codice è più "incapsulato" che mai.


3
Se si verifica una situazione in cui l'oggetto client PU instant istanziare direttamente le sue dipendenze, perché non farlo? È sicuramente la cosa più semplice da fare e non riduce necessariamente la testabilità. Oltre alla semplicità e al migliore incapsulamento, ciò rende anche più semplice avere oggetti con stato anziché singoli senza stato.
Rogério,

1
Aggiungendo a ciò che @ Rogério ha detto è anche potenzialmente significativamente più efficiente. Non tutte le classi mai create nella storia del mondo dovevano avere un'istanza di ciascuna delle sue dipendenze per l'intera durata della vita dell'oggetto proprietario. Un oggetto che utilizza DI perde il controllo più basilare delle proprie dipendenze, vale a dire la loro durata.
Neutrino,

5

Come ha sottolineato Jeff Sternal in un commento alla domanda, la risposta dipende interamente da come si definisce l' incapsulamento .

Sembrano esserci due campi principali sul significato dell'incapsulamento:

  1. Tutto ciò che riguarda l'oggetto è un metodo su un oggetto. Quindi, un Fileoggetto può avere metodi a Save, Print, Display, ModifyText, etc.
  2. Un oggetto è il suo piccolo mondo e non dipende da comportamenti esterni.

Queste due definizioni sono in diretta contraddizione l'una con l'altra. Se un Fileoggetto può stampare se stesso, dipenderà fortemente dal comportamento della stampante. D'altra parte, se sa semplicemente qualcosa che può stampare per esso (una IFilePrintero una tale interfaccia), allora l' Fileoggetto non deve sapere nulla sulla stampa, e quindi lavorare con esso porterà meno dipendenze nell'oggetto.

Pertanto, l'iniezione di dipendenza interromperà l'incapsulamento se si utilizza la prima definizione. Ma francamente non so se mi piace la prima definizione - chiaramente non si ridimensiona (se lo facesse, MS Word sarebbe una grande classe).

D'altra parte, l'iniezione di dipendenza è quasi obbligatoria se si utilizza la seconda definizione di incapsulamento.


Sono assolutamente d'accordo con te sulla prima definizione. Inoltre, viola il SoC, che è probabilmente uno dei peccati cardinali della programmazione e probabilmente uno dei motivi per cui non si adatta.
Marcus Stade,

4

Non viola l'incapsulamento. Stai fornendo un collaboratore, ma la classe decide come utilizzarlo. Finché segui Tell non chiedere che le cose vadano bene. Trovo preferibile l'iniezione del costruttore, ma i setter possono andare bene purché siano intelligenti. Cioè contengono una logica per mantenere gli invarianti che la classe rappresenta.


1
Perché ... no? Se si dispone di un logger e si dispone di una classe che necessita del logger, passare il logger a quella classe non viola l'incapsulamento. E questo è tutto l'iniezione di dipendenza.
jrockway,

3
Penso che tu fraintenda l'incapsulamento. Ad esempio, prendi una classe di date ingenua. Internamente potrebbe avere variabili di istanza di giorno, mese e anno. Se questi fossero esposti come semplici setter senza logica, ciò spezzerebbe l'incapsulamento, dal momento che potrei fare qualcosa come impostare il mese a 2 e il giorno a 31. D'altra parte, se i setter sono intelligenti e controllano gli invarianti, allora le cose vanno bene . Si noti inoltre che in quest'ultima versione, è possibile modificare l'archiviazione in giorni dall'1 / 1/1970, e nulla di ciò che ha utilizzato l'interfaccia deve essere a conoscenza di ciò, a condizione che riscrivo in modo appropriato i metodi giorno / mese / anno.
Jason Watkins,

2
DI sicuramente viola l'incapsulamento / nascondere le informazioni. Se trasformi una dipendenza interna privata in qualcosa esposto nell'interfaccia pubblica della classe, per definizione hai rotto l'incapsulamento di quella dipendenza.
Rogério,

2
Ho un esempio concreto in cui penso che l'incapsulamento sia compromesso da DI. Ho un FooProvider che ottiene "dati foo" dal DB e un FooManager che li memorizza nella cache e calcola le cose sul provider. Ho avuto i consumatori del mio codice che si erano erroneamente indirizzati a FooProvider per i dati, dove avrei preferito che fosse incapsulato in modo che fossero consapevoli solo di FooManager. Questo è fondamentalmente il grilletto per la mia domanda originale.
urig

1
@Rogerio: direi che il costruttore non fa parte dell'interfaccia pubblica, poiché viene utilizzato solo nella radice della composizione. Pertanto, la dipendenza viene "vista" solo dalla radice della composizione. L'unica responsabilità della radice della composizione è collegare queste dipendenze insieme. Quindi l'uso dell'iniezione del costruttore non interrompe alcun incapsulamento.
Jay Sullivan,

4

Questo è simile alla risposta votata, ma voglio pensare ad alta voce - forse anche altri vedono le cose in questo modo.

  • La OO classica utilizza costruttori per definire il contratto di "inizializzazione" pubblico per i consumatori della classe (nascondendo TUTTI i dettagli di implementazione; ovvero l'incapsulamento). Questo contratto può garantire che dopo l'istanza si disponga di un oggetto pronto per l'uso (ovvero nessuna ulteriore procedura di inizializzazione che deve essere ricordata (e dimenticata) dall'utente).

  • (costruttore) DI interrompe innegabilmente l'incapsulamento dei dettagli di implementazione attraverso questa interfaccia pubblica del costruttore. Finché consideriamo ancora il costruttore pubblico responsabile della definizione del contratto di inizializzazione per gli utenti, abbiamo creato un'orribile violazione dell'incapsulamento.

Esempio teorico:

La classe Foo ha 4 metodi e ha bisogno di un numero intero per l'inizializzazione, quindi il suo costruttore assomiglia a Foo (dimensione int) ed è immediatamente chiaro agli utenti della classe Foo che devono fornire una dimensione all'istanza per far funzionare Foo.

Supponiamo che questa particolare implementazione di Foo possa anche aver bisogno di un IWidget per fare il suo lavoro. L'iniezione da parte del costruttore di questa dipendenza ci farebbe creare un costruttore come Foo (dimensione int, widget IWidget)

Ciò che mi infastidisce è che ora abbiamo un costruttore che fonde i dati di inizializzazione con le dipendenze: un input è di interesse per l'utente della classe ( dimensione ), l'altro è una dipendenza interna che serve solo a confondere l'utente ed è un'implementazione dettaglio ( widget ).

Il parametro size NON è una dipendenza, è semplice un valore di inizializzazione per istanza. IoC è ideale per le dipendenze esterne (come i widget) ma non per l'inizializzazione dello stato interno.

Ancora peggio, e se il Widget fosse necessario solo per 2 dei 4 metodi su questa classe; Potrei incorrere in un'istanza ambientale per Widget anche se non può essere utilizzata!

Come compromettere / riconciliare questo?

Un approccio è passare esclusivamente alle interfacce per definire il contratto operativo; e abolire l'uso dei costruttori da parte degli utenti. Per essere coerenti, tutti gli oggetti dovrebbero essere accessibili solo attraverso le interfacce e istanziati solo attraverso una qualche forma di resolver (come un contenitore IOC / DI). Solo il contenitore può istanziare le cose.

Ciò si occupa della dipendenza Widget, ma come inizializzare la "dimensione" senza ricorrere a un metodo di inizializzazione separato sull'interfaccia Foo? Usando questa soluzione, abbiamo perso la capacità di garantire che un'istanza di Foo sia completamente inizializzata al momento dell'istanza. Peccato, perché mi piace molto l' idea e la semplicità dell'iniezione del costruttore.

Come posso ottenere l'inizializzazione garantita in questo mondo DI, quando l'inizializzazione è PIÙ di SOLO dipendenze esterne?


Aggiornamento: ho appena notato che Unity 2.0 supporta la fornitura di valori per i parametri del costruttore (come gli inizializzatori di stato), pur usando il normale meccanismo per l'IoC delle dipendenze durante la risoluzione (). Forse anche altri contenitori supportano questo? Ciò risolve la difficoltà tecnica di mescolare state init e DI in un costruttore, ma viola ancora l'incapsulamento!
ShawnT

Ti sento. Ho posto questa domanda perché anche io sento che due cose buone (DI e incapsulamento) arrivano una a spese dell'altra. A proposito, nel tuo esempio in cui solo 2 metodi su 4 richiedono l'IWidget, ciò indicherebbe che gli altri 2 appartengono a un IMHO componente diverso.
urig

3

L'incapsulamento puro è un ideale che non può mai essere raggiunto. Se tutte le dipendenze fossero nascoste, non avresti affatto bisogno di DI. Pensaci in questo modo, se hai davvero valori privati ​​che possono essere interiorizzati all'interno dell'oggetto, ad esempio il valore intero della velocità di un oggetto auto, allora non hai dipendenza esterna e non hai bisogno di invertire o iniettare quella dipendenza. Questi tipi di valori di stato interni gestiti esclusivamente da funzioni private sono ciò che si desidera incapsulare sempre.

Ma se stai costruendo un'auto che vuole un certo tipo di oggetto motore, allora hai una dipendenza esterna. Puoi istanziare quel motore - ad esempio il nuovo GMOverHeadCamEngine () - internamente al costruttore dell'oggetto auto, preservando l'incapsulamento ma creando un accoppiamento molto più insidioso con una classe concreta GMOverHeadCamEngine, oppure puoi iniettarlo, consentendo all'oggetto Car di funzionare agnosticamente (e molto più saldamente) ad esempio su un'interfaccia IEngine senza la dipendenza concreta. Non importa se usi un container IOC o un semplice DI per raggiungere questo obiettivo: il punto è che hai un'auto che può usare molti tipi di motori senza essere accoppiati a nessuno di essi, rendendo così la tua base di codice più flessibile e meno incline agli effetti collaterali.

DI non è una violazione dell'incapsulamento, è un modo per ridurre al minimo l'accoppiamento quando l'incapsulamento è necessariamente interrotto in pratica praticamente in ogni progetto OOP. L'iniezione di una dipendenza in un'interfaccia minimizza esternamente gli effetti collaterali dell'accoppiamento e consente alle classi di rimanere agnostiche sull'implementazione.


3

Dipende se la dipendenza è davvero un dettaglio dell'implementazione o qualcosa che il cliente vorrebbe / avrebbe bisogno di conoscere in un modo o nell'altro. Una cosa rilevante è il livello di astrazione che la classe sta prendendo di mira. Ecco alcuni esempi:

Se hai un metodo che utilizza la cache sotto il cofano per velocizzare le chiamate, l'oggetto cache dovrebbe essere un Singleton o qualcosa del genere e non deve essere iniettato. Il fatto che la cache sia utilizzata per niente è un dettaglio di implementazione di cui i clienti della tua classe non dovrebbero preoccuparsi.

Se la tua classe ha bisogno di produrre flussi di dati, probabilmente ha senso iniettare il flusso di output in modo che la classe possa facilmente produrre i risultati su un array, un file o ovunque qualcun altro potrebbe voler inviare i dati.

Per un'area grigia, supponiamo che tu abbia una classe che esegue una simulazione Monte Carlo. Ha bisogno di una fonte di casualità. Da un lato, il fatto che ne abbia bisogno è un dettaglio di implementazione in quanto al cliente non importa davvero da dove provenga la casualità. D'altra parte, poiché i generatori di numeri casuali del mondo reale fanno un compromesso tra grado di casualità, velocità, ecc. Che il cliente potrebbe voler controllare e il cliente potrebbe voler controllare il seeding per ottenere un comportamento ripetibile, l'iniezione potrebbe avere un senso. In questo caso, suggerirei di offrire un modo per creare la classe senza specificare un generatore di numeri casuali e di utilizzare un Singleton thread-local come predefinito. Se / quando sorge la necessità di un controllo più preciso, fornire un altro costruttore che consenta di iniettare una fonte di casualità.


2

Credo nella semplicità. L'applicazione di IOC / Dependecy Injection nelle classi Domain non comporta alcun miglioramento se non quello di rendere il codice molto più difficile da gestire avendo un file XML esterno che descriva la relazione. Molte tecnologie come EJB 1.0 / 2.0 e struts 1.1 stanno facendo retromarcia riducendo le cose messe in XML e provando a metterle in codice come fastidio ecc. Quindi applicare il CIO per tutte le classi che sviluppate renderà il codice insensato.

Il CIO ha dei vantaggi quando l'oggetto dipendente non è pronto per la creazione al momento della compilazione. Questo può accadere nella maggior parte dei componenti dell'architettura di livello astratto dell'infrastura, cercando di stabilire un framework di base comune che potrebbe essere necessario lavorare per diversi scenari. In quei luoghi l'uso del CIO ha più senso. Tuttavia, ciò non rende il codice più semplice / gestibile.

Come tutte le altre tecnologie, anche questo ha PRO e CON. La mia preoccupazione è che implementiamo le ultime tecnologie in tutti i luoghi, indipendentemente dal loro migliore utilizzo del contesto.


2

L'incapsulamento viene interrotto solo se una classe ha sia la responsabilità di creare l'oggetto (che richiede la conoscenza dei dettagli di implementazione) e quindi usa la classe (che non richiede la conoscenza di questi dettagli). Spiegherò perché, ma prima una rapida anaologia dell'auto:

Quando guidavo la mia vecchia Kombi del 1971, potevo premere l'acceleratore ed è andato (leggermente) più veloce. Non avevo bisogno di sapere perché, ma i ragazzi che costruirono il Kombi in fabbrica sapevano esattamente perché.

Ma torniamo alla codifica. incapsulamento sta "nascondendo un dettaglio dell'implementazione da qualcosa che utilizza tale implementazione". L'incapsulamento è una buona cosa perché i dettagli di implementazione possono cambiare senza che l'utente della classe lo sappia.

Quando si utilizza l'iniezione di dipendenza, l'iniezione del costruttore viene utilizzata per costruire il servizio oggetti del tipo di (anziché oggetti entità / valore quale stato modello). Qualsiasi variabile membro nell'oggetto del tipo di servizio rappresenta i dettagli dell'implementazione che non devono fuoriuscire. ad es. numero di porta socket, credenziali del database, un'altra classe da chiamare per eseguire la crittografia, una cache, ecc.

Il costruttore è rilevante quando la classe viene inizialmente creata. Ciò accade durante la fase di costruzione mentre il contenitore DI (o la fabbrica) collega tutti gli oggetti di servizio. Il contenitore DI conosce solo i dettagli di implementazione. Sa tutto sui dettagli di implementazione come i ragazzi della fabbrica Kombi conoscono le candele.

In fase di runtime, l'oggetto di servizio che è stato creato si chiama apon per fare un vero lavoro. Al momento, il chiamante dell'oggetto non è a conoscenza dei dettagli di implementazione.

Sono io a guidare il mio Kombi in spiaggia.

Ora, torniamo all'incapsulamento. Se i dettagli dell'implementazione cambiano, non è necessario modificare la classe che utilizza tale implementazione in fase di runtime. L'incapsulamento non è rotto.

Posso guidare la mia auto nuova anche in spiaggia. L'incapsulamento non è rotto.

Se i dettagli di implementazione cambiano, è necessario modificare il contenitore DI (o la fabbrica). Non hai mai cercato di nascondere i dettagli di implementazione dalla fabbrica in primo luogo.


Come testereste la vostra fabbrica? Ciò significa che il cliente deve conoscere la fabbrica per ottenere un'auto funzionante, il che significa che hai bisogno di una fabbrica per ogni altro oggetto nel tuo sistema.
Rodrigo Ruiz,

2

Dopo aver lottato un po 'di più sulla questione, sono ora dell'opinione che l'iniezione di dipendenza (in questo momento) violi l'incapsulamento in una certa misura. Non fraintendetemi, però, penso che l'uso dell'iniezione di dipendenza valga la pena il compromesso nella maggior parte dei casi.

Il motivo per cui DI viola l'incapsulamento diventa chiaro quando il componente su cui stai lavorando deve essere consegnato a una parte "esterna" (pensa a scrivere una libreria per un cliente).

Quando il mio componente richiede che i sottocomponenti vengano iniettati tramite il costruttore (o le proprietà pubbliche) non è garantito

"impedire agli utenti di impostare i dati interni del componente in uno stato non valido o incoerente".

Allo stesso tempo, non si può dire questo

"gli utenti del componente (altri software) devono solo sapere cosa fa il componente e non possono dipendere dai dettagli di come lo fa" .

Entrambe le citazioni provengono da Wikipedia .

Per fare un esempio specifico: devo fornire una DLL sul lato client che semplifichi e nasconda la comunicazione a un servizio WCF (essenzialmente una facciata remota). Poiché dipende da 3 diverse classi proxy WCF, se prendo l'approccio DI sono costretto a esporle tramite il costruttore. Con ciò espongo gli interni del mio livello di comunicazione che sto cercando di nascondere.

Generalmente sono tutto per DI. In questo esempio (estremo) particolare, mi sembra pericoloso.


2

DI viola l'incapsulamento per oggetti NON condivisi - punto. Gli oggetti condivisi hanno una durata esterna all'oggetto in fase di creazione e pertanto devono essere AGGREGATI nell'oggetto in fase di creazione. Gli oggetti privati ​​per l'oggetto da creare devono essere COMPOSTI nell'oggetto creato: quando l'oggetto creato viene distrutto, porta con sé l'oggetto composto. Prendiamo il corpo umano come esempio. Cosa è composto e cosa è aggregato. Se dovessimo usare DI, il costruttore del corpo umano avrebbe centinaia di oggetti. Molti organi, ad esempio, sono (potenzialmente) sostituibili. Ma sono ancora composti nel corpo. Le cellule del sangue vengono create nel corpo (e distrutte) ogni giorno, senza la necessità di influenze esterne (diverse dalle proteine). Pertanto, le cellule del sangue vengono create internamente dal corpo: il nuovo BloodCell ().

I sostenitori di DI sostengono che un oggetto non dovrebbe MAI utilizzare il nuovo operatore. Questo approccio "purista" non solo viola l'incapsulamento, ma anche il principio di sostituzione di Liskov per chiunque stia creando l'oggetto.


1

Ho lottato anche con questa nozione. Inizialmente, il "requisito" di usare il contenitore DI (come Spring) per creare un'istanza di un oggetto sembrava saltare attraverso i cerchi. Ma in realtà, non è davvero un cerchio - è solo un altro modo "pubblicato" per creare oggetti di cui ho bisogno. Certo, l'incapsulamento è 'rotto' perché qualcuno 'fuori dalla classe' sa di cosa ha bisogno, ma in realtà non è il resto del sistema che lo sa - è il contenitore DI. Nulla di magico accade diversamente perché DI "conosce" un oggetto ha bisogno di un altro.

In effetti migliora ancora: concentrandomi su Fabbriche e Archivi non devo nemmeno sapere che DI è coinvolto! Questo per me rimette il coperchio sull'incapsulamento. Meno male!


1
Finché DI è responsabile dell'intera catena di istanziazione, allora concordo sul fatto che si verifichi l'incapsulamento. In un certo senso, perché le dipendenze sono ancora pubbliche e possono essere abusate. Ma quando da qualche parte "in alto" nella catena qualcuno ha bisogno di creare un'istanza di un oggetto senza di loro usando DI (forse sono una "terza parte") allora diventa disordinato. Sono esposti alle tue dipendenze e potrebbero essere tentati di abusarne. O potrebbero non voler sapere affatto di loro.
urig,

1

PS. Fornendo Dependency Injection si non necessariamente rompere incapsulamento . Esempio:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

Il codice client non conosce ancora i dettagli di implementazione.


Come nel tuo esempio viene iniettato qualcosa? (Tranne la denominazione della funzione setter)
foo

1

Forse questo è un modo ingenuo di pensarci, ma qual è la differenza tra un costruttore che accetta un parametro intero e un costruttore che accetta un servizio come parametro? Questo significa che la definizione di un numero intero al di fuori del nuovo oggetto e il suo inserimento nell'oggetto interrompono l'incapsulamento? Se il servizio viene utilizzato solo all'interno del nuovo oggetto, non vedo come ciò spezzerebbe l'incapsulamento.

Inoltre, utilizzando una sorta di funzione di autowiring (Autofac per C #, ad esempio), rende il codice estremamente pulito. Costruendo metodi di estensione per il generatore Autofac, sono stato in grado di eliminare MOLTO codice di configurazione DI che avrei dovuto mantenere nel tempo man mano che l'elenco delle dipendenze cresceva.


Non si tratta di valore vs. servizio. Riguarda l'inversione del controllo - se il costruttore della classe imposta la classe o se quel servizio prende il controllo della configurazione della classe. Per il quale è necessario conoscere i dettagli di implementazione di quella classe, quindi hai un altro posto da mantenere e sincronizzare.
pippo

1

Penso che sia evidente che almeno DI indebolisce significativamente l'incapsulamento. In aggiunta a ciò ecco alcuni altri aspetti negativi di DI da considerare.

  1. Rende il codice più difficile da riutilizzare. Un modulo che un client può usare senza dover fornire esplicitamente dipendenze, è ovviamente più facile da usare rispetto a uno in cui il client deve in qualche modo scoprire quali sono le dipendenze di quel componente e quindi renderle disponibili. Ad esempio, un componente creato originariamente per essere utilizzato in un'applicazione ASP può prevedere che le sue dipendenze siano fornite da un contenitore DI che fornisce alle istanze di oggetti una durata correlata alle richieste HTTP del client. Potrebbe non essere semplice riprodurlo in un altro client che non viene fornito con lo stesso contenitore DI incorporato dell'applicazione ASP originale.

  2. Può rendere il codice più fragile. Le dipendenze fornite dalle specifiche dell'interfaccia possono essere implementate in modi inaspettati che danno origine a un'intera classe di bug di runtime che non sono possibili con una dipendenza concreta risolta staticamente.

  3. Può rendere il codice meno flessibile, nel senso che potresti finire con meno scelte su come vuoi che funzioni. Non tutte le classi devono avere tutte le dipendenze esistenti per l'intera vita dell'istanza proprietaria, ma con molte implementazioni DI non hai altra opzione.

Con questo in mente, penso che la domanda più importante diventi allora: " una specifica dipendenza deve essere specificata esternamente? ". In pratica raramente ho trovato necessario creare una dipendenza fornita esternamente solo per supportare i test.

Laddove una dipendenza debba realmente essere fornita esternamente, ciò suggerisce normalmente che la relazione tra gli oggetti è una collaborazione piuttosto che una dipendenza interna, nel qual caso l'obiettivo appropriato è quindi l'incapsulamento di ogni classe, piuttosto che l'incapsulamento di una classe all'interno dell'altra .

Nella mia esperienza, il problema principale riguardante l'uso di DI è che se inizi con un framework applicativo con DI integrato o aggiungi il supporto DI alla tua base di codice, per qualche ragione la gente presume che dato che hai il supporto DI che deve essere il corretto modo di istanziare tutto . Non si preoccupano nemmeno di porre la domanda "questa dipendenza deve essere specificata esternamente?". E peggio ancora, iniziano anche a cercare di forzare tutti gli altri a utilizzare il supporto DI per tutto .

Il risultato di ciò è che inesorabilmente la tua base di codice inizia a deviare in uno stato in cui la creazione di qualsiasi istanza di qualsiasi cosa nella tua base di codice richiede risme di configurazione del contenitore DI ottusa e il debug di qualsiasi cosa è due volte più difficile perché hai il carico di lavoro extra di provare a identificare come e dove nulla è stato istanziato.

Quindi la mia risposta alla domanda è questa. Usa DI dove puoi identificare un problema reale che risolve per te, che non puoi risolvere più semplicemente in nessun altro modo.


0

Sono d'accordo che, portato all'estremo, DI può violare l'incapsulamento. Di solito DI espone dipendenze che non sono mai state realmente incapsulate. Ecco un esempio semplificato preso in prestito dai Singletons di Miško Hevery sono bugiardi patologici :

Inizi con un test di CreditCard e scrivi un semplice test unitario.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

Il mese prossimo riceverai un conto per $ 100. Perché ti è stato addebitato? Il test unitario ha interessato un database di produzione. Internamente, CreditCard chiama Database.getInstance(). Rifattorizzare la carta di credito in modo che prenda un DatabaseInterfacenel suo costruttore espone il fatto che c'è dipendenza. Ma direi che la dipendenza non è mai stata incapsulata all'inizio poiché la classe CreditCard causa effetti collaterali visibili esternamente. Se vuoi testare CreditCard senza refactoring, puoi certamente osservare la dipendenza.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

Non penso che valga la pena preoccuparsi se esporre il Database come dipendenza riduce l'incapsulamento, perché è una buona progettazione. Non tutte le decisioni DI saranno così dirette. Tuttavia, nessuna delle altre risposte mostra un contro esempio.


1
I test unitari di solito sono scritti dall'autore della classe, quindi va bene precisare le dipendenze nel caso di test, da un punto di vista tecnico. Quando successivamente la classe della carta di credito cambia per utilizzare un'API Web come PayPal, l'utente dovrebbe cambiare tutto se fosse stato eliminato. I test unitari di solito vengono eseguiti con una conoscenza intima della materia testata (non è questo il punto?), Quindi penso che i test rappresentino più un'eccezione che un tipico esempio.
kizzx2,

1
Il punto di DI è evitare le modifiche che hai descritto. Se passassi da un'API Web a PayPal, non cambieresti la maggior parte dei test perché utilizzerebbero un MockPaymentService e CreditCard verrebbero costruiti con un PaymentService. Avresti solo una manciata di test che esaminano la vera interazione tra CreditCard e un vero PaymentService, quindi i cambiamenti futuri sono molto isolati. I vantaggi sono ancora maggiori per i grafici di dipendenza più profondi (come una classe che dipende dalla CreditCard).
Craig P. Motlin,

@Craig p. Motlin Come può CreditCardcambiare l' oggetto da una WebAPI a PayPal, senza che nulla di esterno alla classe debba cambiare?
Ian Boyd,

@Ian ho menzionato CreditCard dovrebbe essere refactored per prendere un DatabaseInterface nel suo costruttore che lo protegge dalle modifiche nelle implementazioni di DatabaseInterfaces. Forse questo deve essere ancora più generico e prendere una StorageInterface di cui WebAPI potrebbe essere un'altra implementazione. PayPal è al livello sbagliato, perché è un'alternativa alla CreditCard. Sia PayPal che CreditCard potrebbero implementare PaymentInterface per proteggere altre parti dell'applicazione dall'esterno di questo esempio.
Craig P. Motlin,

@ kizzx2 In altre parole, non ha senso dire che una carta di credito dovrebbe usare PayPal. Sono alternative nella vita reale.
Craig P. Motlin,

0

Penso che sia una questione di portata. Quando si definisce l'incapsulamento (senza far sapere come) è necessario definire qual è la funzionalità incapsulata.

  1. Classe com'è : ciò che stai incapsulando è l'unica responsabilità della classe. Cosa sa fare. Ad esempio, l'ordinamento. Se inietti un comparatore per l'ordinazione, diciamo, clienti, non fa parte dell'incapsulato: quicksort.

  2. Funzionalità configurata : se si desidera fornire una funzionalità pronta per l'uso, non si fornisce la classe QuickSort, ma un'istanza della classe QuickSort configurata con un comparatore. In tal caso, il codice responsabile della creazione e della configurazione deve essere nascosto al codice utente. E questo è l'incapsulamento.

Quando stai programmando le classi, è, implementando singole responsabilità in classi, stai usando l'opzione 1.

Quando stai programmando le applicazioni, lo è, facendo qualcosa che intraprende qualche utile lavoro concreto , allora stai ripetutamente usando l'opzione 2.

Questa è l'implementazione dell'istanza configurata:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

Ecco come lo usano alcuni altri codici client:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

È incapsulato perché se si modifica l'implementazione (si modifica la clientSorterdefinizione del bean) non si interrompe l'utilizzo del client. Forse, mentre usi i file xml con tutti scritti insieme stai vedendo tutti i dettagli. Ma credimi, il codice client ( ClientService) non sa nulla del suo selezionatore.


0

Vale probabilmente la pena ricordare che Encapsulationdipende in qualche modo dalla prospettiva.

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

Dal punto di vista di qualcuno che lavora in Aclasse, nel secondo esempio Aconosce molto meno la natura dithis.b

Considerando che senza DI

new A()

vs

new A(new B())

La persona che guarda questo codice conosce meglio la natura del Asecondo esempio.

Con DI, almeno tutta quella conoscenza trapelata è in un posto.

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.