Domanda

Se capisco correttamente, 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 che viene iniettata e viola il principio di incapsulamento OOP.

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

Vedi anche la mia risposta alla mia domanda di seguito.

È stato utile?

Soluzione

Esiste 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.

Purtroppo al momento le nostre lingue non separano i concetti a grana fine e orientati agli oggetti da quelli a grana grossa e orientati ai componenti, quindi questa è una distinzione che devi tenere a mente solo :) :)

Altri suggerimenti

È una buona domanda - ma a un certo punto, l'incapsulamento nella sua forma più pura ha bisogno di essere violato se l'oggetto dovesse mai soddisfare la sua dipendenza. Alcuni provider della dipendenza devono sapere sia che l'oggetto in questione richiede un Foo e che il provider deve avere un modo per fornire il Foo all'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 riflessione senza che sia necessario esporre la dipendenza attraverso nessuno dei metodi / costruttori pubblici delle classi. Questo potrebbe essere il tipo di soluzione che stavi cercando.

Detto questo, non penso nemmeno che 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 tratta di oggetti, si interagisce comunque con loro attraverso un'interfaccia e più si fa questo (e si hanno dipendenze cablate tramite DI), meno si deve effettivamente gestire da soli i costruttori. Nella situazione ideale, il tuo codice non si occupa né crea mai istanze concrete di classi; quindi viene semplicemente dato un IFoo tramite DI, senza preoccuparsi di ciò che il costruttore di FooImpl indica che deve fare il suo lavoro, e di fatto senza nemmeno essere a conoscenza di Esistenza di FooImpl . Da questo punto di vista, l'incapsulamento è perfetto.

Questa è ovviamente un'opinione, ma secondo me DI non viola necessariamente l'incapsulamento e in effetti può aiutarla centralizzando tutta la conoscenza necessaria degli interni in un unico posto. Non solo è una buona cosa in sé, ma ancora meglio questo posto è fuori dalla tua base di codice, quindi nessuno del codice che scrivi deve conoscere le dipendenze delle classi.

  

Questo espone la dipendenza che viene iniettata e viola il principio di incapsulamento OOP.

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

Quindi, cosa viola l'incapsulamento?

Ereditarietà 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)

Programmazione orientata all'aspetto / strong>. 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 di amicizia in C ++ fa .

Estensione di classe in Ruby . Basta ridefinire un metodo stringa da qualche parte dopo che una classe stringa è stata completamente definita.

Beh, un sacco di roba fa .

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

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

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 istanziare dipendenze con stato quando e dove sono necessarie. È semplice, ben incapsulato, facile da capire e facile da testare. Quindi, perché no?

Un buon contenitore / sistema di iniezione depenancy consentirà l'iniezione del costruttore. Gli oggetti dipendenti saranno incapsulati e non dovranno essere esposti pubblicamente. Inoltre, utilizzando un sistema DP, nessuno dei tuoi codici nemmeno " conosce " i dettagli di come viene costruito l'oggetto, possibilmente includendo anche l'oggetto da costruire. 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, il "qualcun altro", che in questo caso è il contenitore DP, ha una conoscenza intima che "viola". 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" allora mai.

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

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

  • La OO classica utilizza i costruttori per definire l'inizializzazione pubblica "quot" contratto per i consumatori della classe (nascondendo TUTTI i dettagli di implementazione; aka 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 sanguinando 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:

Classe Foo ha 4 metodi e necessita 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.

Dire che questa particolare implementazione di Foo potrebbe 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 mescola i dati di inizializzazione con le dipendenze: un input è di interesse per l'utente della classe ( dimensione ), l'altra è una dipendenza interna che serve solo a confondere l'utente ed è un dettaglio di implementazione ( 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, 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 comprometterlo / riconciliarlo?

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.

Questo si occupa della dipendenza Widget, ma come inizializziamo " size " 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 semplicità dell'iniezione del costruttore.

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

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

Sembrano esserci due campi principali sul significato dell'incapsulamento:

  1. Tutto ciò che riguarda l'oggetto è un metodo su un oggetto. Pertanto, un oggetto File può avere metodi per Salva , Stampa , Display , ModifyText , ecc.
  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 oggetto File può stampare da solo, dipenderà fortemente dal comportamento della stampante. D'altra parte, se semplicemente sa su qualcosa che può stampare per esso (un IFilePrinter o qualche interfaccia simile), allora l'oggetto File non deve sapere nulla della stampa, quindi lavorare con essa porterà meno dipendenze nell'oggetto.

Quindi, 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 stai usando la seconda definizione di incapsulamento.

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 nuovo GMOverHeadCamEngine () - internamente all'interno del 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ù efficacemente) 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 all'interno di 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.

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 amp; struts 1.1 sta facendo retromarcia riducendo le cose inserite in XML e provando a inserirle nel codice come fastidio, ecc. Quindi applicare il CIO per tutte le classi sviluppate renderà il codice insensato.

IOC 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 & amp; Cons. La mia preoccupazione è che implementiamo le ultime tecnologie in tutti i luoghi, indipendentemente dal loro migliore utilizzo del contesto.

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 dovrebbe essere non iniettato. Il fatto che la cache sia utilizzata per niente è un dettaglio dell'implementazione di cui i clienti della tua classe non dovrebbero preoccuparsi.

Se la tua classe deve emettere 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 l'iniezione di una fonte di casualità.

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 delle auto:

  

Quando guidavo la mia vecchia Kombi del 1971,   Potrei premere l'acceleratore e esso   è andato (leggermente) più veloce. io no   bisogno di sapere perché, ma i ragazzi che   costruito il Kombi in fabbrica lo sapeva   esattamente perché.

Ma torniamo alla codifica. Incapsulamento sta "nascondendo un dettaglio dell'implementazione da qualcosa che utilizza tale implementazione." L'incapsulamento è positivo 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 oggetti di tipo servizio (al contrario di oggetti entità / valore che indicano lo stato del modello). Qualsiasi variabile membro nell'oggetto del tipo di servizio rappresenta i dettagli dell'implementazione che non devono fuoriuscire. per esempio. 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 esecuzione, l'oggetto di servizio che è stato creato si chiama apon per fare del lavoro reale. 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 anche la mia nuova auto in spiaggia. L'incapsulamento non è rotto.

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

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

Il caso per cui DI viola l'incapsulamento diventa chiaro quando il componente su cui stai lavorando deve essere consegnato a un "esterno". festa (pensa a scrivere una biblioteca per un cliente).

Quando il mio componente richiede che i sottocomponenti vengano iniettati tramite il costruttore (o le proprietà pubbliche) non esiste alcuna garanzia per

  

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

Allo stesso tempo, non si può dire che

  

" 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.

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. Niente di magico accade diversamente perché DI "sa" che 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!

PS. Fornendo Iniezione di dipendenza non necessariamente interrompi incapsulamento . Esempio:

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

Il codice client non conosce ancora i dettagli di implementazione.

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, usando una sorta di funzione di autowiring (Autofac per C #, per esempio), rende il codice estremamente pulito. Costruendo metodi di estensione per il builder 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.

Penso 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ò utilizzare senza dover fornire esplicitamente dipendenze, è ovviamente più facile da usare rispetto a quello 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, successivamente, " 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 abbia davvero bisogno di 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 ciascuna 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 modo corretto 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 usare il supporto DI anche 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 ottuso e il debug di qualsiasi cosa è due volte più difficile perché hai il carico di lavoro extra di provare per identificare come e dove è stata istanziata qualsiasi cosa.

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 altro modo.

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 dall'organismo: la nuova BloodCell ().

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

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 da Mi & # 353; ko Hevery's Singletons sono bugiardi patologici :

Inizi con un test con carta di credito 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 una fattura di $ 100. Perché ti è stato addebitato? Il test unitario ha interessato un database di produzione. Internamente, CreditCard chiama Database.getInstance () . Rifattorizzare la CreditCard in modo che impieghi un DatabaseInterface nel suo costruttore espone il fatto che esiste 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 credo 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.

Penso che sia una questione di portata. Quando definisci l'incapsulamento (senza far sapere come) devi 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, questo 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 applicazioni, è, facendo qualcosa che intraprende qualche utile 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 usa qualche altro codice client:

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

È incapsulato perché se si modifica l'implementazione (si modifica la definizione del bean clientSorter ) non si interrompe l'uso del client. Forse, mentre usi i file xml con tutti scritti insieme stai vedendo tutti i dettagli. Ma credimi, il codice client ( ClientService ) non so nulla della sua selezionatrice.

Probabilmente vale la pena ricordare che Encapsulation dipende 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 sulla classe A , nel secondo esempio A conosce molto meno la natura di this.b

Considerando che senza DI

new A()

vs

new A(new B())

La persona che guarda questo codice conosce meglio la natura di A nel secondo esempio.

Con DI, almeno tutta quella conoscenza trapelata si trova in un unico posto.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top