Quando NON applicare il principio di inversione di dipendenza?


43

Attualmente sto cercando di capire SOLID. Quindi il principio di inversione di dipendenza significa che due classi dovrebbero comunicare tramite interfacce, non direttamente. Esempio: se class Aha un metodo, che prevede un puntatore a un oggetto di tipo class B, allora questo metodo dovrebbe effettivamente aspettarsi un oggetto di tipo abstract base class of B. Questo è utile anche per Apri / Chiudi.

Se ho capito bene, la mia domanda sarebbe: è una buona pratica applicarla a tutte le interazioni di classe o dovrei provare a pensare in termini di livelli ?

Il motivo per cui sono scettico è perché stiamo pagando un prezzo per seguire questo principio. Di ', devo implementare funzionalità Z. Dopo l'analisi, concludo che caratteristica Zconsiste di funzionalità A, Be C. Creo una facciata di classe Z, che, attraverso le interfacce, utilizza le classi A, Be C. Comincio codifica l'attuazione e ad un certo punto mi rendo conto che il compito Zin realtà consiste di funzionalità A, Be D. Ora ho bisogno di cancellare l' Cinterfaccia, il Cprototipo di classe e scrivere Dun'interfaccia e una classe separate . Senza interfacce, solo la classe avrebbe dovuto essere sostituita.

In altre parole, per cambiare qualcosa, devo cambiare 1. il chiamante 2. l'interfaccia 3. la dichiarazione 4. l'implementazione. In un'implementazione direttamente accoppiata a Python, avrei bisogno di cambiare solo l'implementazione.


13
L'inversione delle dipendenze è semplicemente una tecnica, quindi dovrebbe essere applicata solo quando necessario ... non c'è limite al grado in cui può essere applicata, quindi se la applichi ovunque, finisci con la spazzatura: come in ogni altra situazione tecnica specifica.
Frank Hileman,

Per dirla semplicemente, l'applicazione di alcuni principi di progettazione del software dipende dalla capacità di refactoring senza pietà quando i requisiti cambiano. Di questi, si ritiene che la parte dell'interfaccia catturi al meglio gli invarianti contrattuali del progetto, mentre il codice sorgente (implementazione) dovrebbe tollerare cambiamenti più frequenti.
rwong

@rwong Un'interfaccia cattura gli invarianti contrattuali solo se usi una lingua che supporta gli invarianti contrattuali. Nei linguaggi comuni (Java, C #), un'interfaccia è semplicemente un insieme di firme API. L'aggiunta di interfacce superflue degrada solo un progetto.
Frank Hileman,

Direi che hai capito male. DIP consiste nell'evitare dipendenze in fase di compilazione da un componente "di alto livello" a un componente di "basso livello", al fine di consentire il riutilizzo del componente di alto livello in altri contesti, dove si utilizzerà un'implementazione diversa per i componente di livello; questo viene fatto creando un tipo astratto di alto livello, che viene implementato dai componenti di basso livello; quindi entrambi i componenti di alto e basso livello dipendono da questa astrazione. Alla fine, i componenti di alto e basso livello comunicano attraverso un'interfaccia, ma questa non è l'essenza di DIP.
Rogério,

Risposte:


87

In molti cartoni animati o altri media, le forze del bene e del male sono spesso illustrate da un angelo e un demone seduti sulle spalle del personaggio. Nella nostra storia qui, invece del bene e del male, abbiamo SOLID su una spalla e YAGNI (non ne avrai bisogno!) Seduto sull'altra.

I principi SOLID presi al massimo sono i più adatti per sistemi aziendali enormi, complessi e ultra-configurabili. Per i sistemi più piccoli o più specifici, non è appropriato rendere tutto incredibilmente flessibile, poiché il tempo che dedichi all'astrazione delle cose non si rivelerà un vantaggio.

Passare interfacce anziché classi concrete a volte significa, ad esempio, che è possibile scambiare facilmente la lettura da un file con un flusso di rete. Tuttavia, per una grande quantità di progetti software, quel tipo di flessibilità non sarà mai necessario e potresti anche passare classi di file concreti e chiamarlo un giorno e risparmiare le tue cellule cerebrali.

Parte dell'arte dello sviluppo del software è avere un buon senso di ciò che è probabile che cambi col passare del tempo, e cosa non lo è. Per le cose che potrebbero cambiare, utilizzare le interfacce e altri concetti SOLID. Per le cose che non lo faranno, usa YAGNI e passa i tipi concreti, dimentica le classi di fabbrica, dimentica tutte le operazioni di runtime e configurazione, ecc. E dimentica molte delle astrazioni SOLIDI. Nella mia esperienza, l'approccio YAGNI si è dimostrato corretto molto più spesso di quanto non lo sia.


19
La mia prima introduzione a SOLID è stata circa 15 anni fa su un nuovo sistema che stavamo costruendo. Abbiamo bevuto tutti il ​​kool aid man. Se qualcuno menzionava qualcosa che suonava come YAGNI, eravamo come "Pfffft ... plebeian". Ho avuto l'onore (orrore?) Di vedere il sistema evolversi nel corso del prossimo decennio. È diventato un casino ingombrante che nessuno poteva capire, nemmeno noi fondatori. Gli architetti adorano il SOLID. Le persone che si guadagnano da vivere amano YAGNI. Nessuno dei due è perfetto, ma YAGNI è più vicino alla perfezione e dovrebbe essere il tuo valore predefinito se non sai cosa stai facendo. :-)
Calphool,

11
@Nard Sì, l'abbiamo fatto su un progetto. Sono andato fuori di testa con esso. Ora i nostri test sono impossibili da leggere o mantenere, in parte a causa dell'eccessiva derisione. Inoltre, a causa dell'iniezione di dipendenza, è un dolore nella parte posteriore navigare attraverso il codice quando stai cercando di capire qualcosa. SOLID non è un proiettile d'argento. YAGNI non è un proiettile d'argento. I test automatizzati non sono un proiettile d'argento. Nulla può salvarti dal fare il duro lavoro di pensare a quello che stai facendo e prendere decisioni sul fatto che possa aiutare o ostacolare il tuo lavoro o quello di qualcun altro.
jpmc26,

22
Un sacco di sentimenti anti-SOLIDI qui. SOLID e YAGNI non sono le due estremità di uno spettro. Sono come le coordinate X e Y su un grafico. Un buon sistema ha pochissimo codice superfluo (YAGNI) E segue i principi SOLIDI.
Stephen,

31
Meh, (a) non sono d'accordo sul fatto che SOLID = impresa e (b) l'intero punto di SOLID sia che tendiamo a essere previsioni estremamente povere di ciò che sarà necessario. Devo essere d'accordo con @Stephen qui. YAGNI afferma che non dovremmo cercare di anticipare i requisiti futuri che non sono chiaramente definiti oggi. SOLID afferma che dovremmo aspettarci che il design si evolva nel tempo e applicare alcune semplici tecniche per facilitarlo. Non si escludono a vicenda; entrambi sono tecniche per adattarsi alle mutevoli esigenze. I veri problemi si verificano quando si tenta di progettare per requisiti poco chiari o molto distanti.
Aaronaught il

8
"puoi facilmente scambiare la lettura da un file con un flusso di rete" - questo è un buon esempio in cui una descrizione troppo semplificata di DI fa smarrire le persone. Le persone a volte pensano (in effetti), "questo metodo utilizzerà un File, quindi invece ci vorrà un IFilelavoro fatto". Quindi non possono facilmente sostituire un flusso di rete, perché hanno richiesto troppo l'interfaccia e ci sono operazioni che IFileil metodo non usa nemmeno, che non si applicano ai socket, quindi un socket non può implementare IFile. Una delle cose per cui DI non è un proiettile d'argento, sta inventando le giuste astrazioni (interfacce) :-)
Steve Jessop,

11

Nelle parole di laici:

Applicare il DIP è sia facile che divertente . Non ottenere il progetto giusto al primo tentativo non è una ragione sufficiente per rinunciare del tutto al DIP.

  • Di solito gli IDE ti aiutano a fare quel tipo di refactoring, alcuni addirittura ti permettono di estrarre l'interfaccia da una classe già implementata
  • È quasi impossibile ottenere il design giusto la prima volta
  • Il normale flusso di lavoro prevede la modifica e il ripensamento delle interfacce nelle prime fasi di sviluppo
  • Man mano che lo sviluppo evolve, matura e avrai meno motivi per modificare le interfacce
  • In una fase avanzata le interfacce (il design) saranno mature e difficilmente cambieranno
  • Da quel momento, inizi a raccogliere i vantaggi, poiché la tua app è aperta per ridimensionare.

D'altra parte, la programmazione con interfacce e OOD può riportare la gioia al mestiere a volte stantio della programmazione.

Alcune persone dicono che aggiunge complessità ma penso che l'opossite sia vera. Anche per piccoli progetti. Rende più facile il test / derisione. Fa in modo che il tuo codice abbia un numero inferiore di caseistruzioni o nidificato ifs. Riduce la complessità ciclomatica e ti fa pensare in modi nuovi. Rende la programmazione più simile alla progettazione e alla produzione nel mondo reale.


5
Non so quali lingue o IDE vengano utilizzati dall'OP, ma in VS 2013 è ridicolmente semplice lavorare contro interfacce, estrarre interfacce e implementarle e fondamentale se si utilizza TDD. Non vi è alcun costo aggiuntivo di sviluppo per lo sviluppo utilizzando questi principi.
stephenbayer,

Perché questa risposta parla di DI se la domanda riguardava DIP? DIP è un concetto degli anni '90, mentre DI è del 2004. Sono molto diversi.
Rogério,

1
(Il mio commento precedente era destinato a un'altra risposta; ignoralo.) "Programmare su interfacce" è molto più generale di DIP, ma non si tratta di rendere ogni classe implementare un'interfaccia separata. E semplifica il "test / derisione" solo se gli strumenti di test / derisione soffrono di gravi limitazioni.
Rogério,

@ Rogério Di solito quando si utilizza DI, non tutte le classi implementano un'interfaccia separata. Un'interfaccia implementata da più classi è comune.
Tulains Córdova,

@ Rogério Ho corretto la mia risposta, ogni volta che ho citato DI intendevo DIP.
Tulains Córdova,

9

Usa l' inversione di dipendenza dove ha senso.

Un controesempio estremo è la classe "stringa" inclusa in molte lingue. Rappresenta un concetto primitivo, essenzialmente una serie di personaggi. Supponendo che potresti cambiare questa classe principale, non ha senso usare DI qui perché non avrai mai bisogno di scambiare lo stato interno con qualcos'altro.

Se hai un gruppo di oggetti utilizzati internamente in un modulo che non sono esposti ad altri moduli o riutilizzati ovunque, probabilmente non vale la pena usare DI.

A mio avviso, ci sono due posti in cui DI dovrebbe essere usato automaticamente:

  1. In moduli progettati per l'estensione. Se l'intero scopo di un modulo è quello di estenderlo e cambiare comportamento, ha perfettamente senso inserire DI dall'inizio.

  2. Nei moduli che stai eseguendo il refactoring ai fini del riutilizzo del codice. Forse hai codificato una classe per fare qualcosa, poi ti rendi conto in seguito che con un refactor puoi sfruttare quel codice altrove e c'è bisogno di farlo . Questo è un ottimo candidato per DI e altri cambiamenti di estensibilità.

Le chiavi qui vengono utilizzate laddove è necessario perché introdurranno ulteriore complessità e si assicureranno di misurare tale necessità attraverso requisiti tecnici (punto uno) o revisione quantitativa del codice (punto due).

DI è un ottimo strumento, ma proprio come qualsiasi altro strumento *, può essere abusato o abusato.

* Eccezione alla regola di cui sopra: una sega alternativa è lo strumento perfetto per qualsiasi lavoro. Se non risolve il problema, lo rimuoverà. Permanentemente.


3
E se "il tuo problema" fosse un buco nel muro? Una sega non lo rimuoverebbe; peggiorerebbe le cose. ;)
Mason Wheeler,

3
@MasonWheeler con una sega potente e divertente da usare, "il buco nel muro" potrebbe trasformarsi in "porta" che è una risorsa utile :-)

1
Non puoi usare una sega per creare una toppa per il foro?
JeffO,

Mentre ci sono alcuni vantaggi nell'avere un tipo non estensibile dall'utente String, ci sono molti casi in cui rappresentazioni alternative sarebbero utili se il tipo avesse una buona serie di operazioni virtuali (ad esempio copiare una sottostringa in una porzione specificata di a short[], segnalare se un la sottostringa contiene o potrebbe contenere solo ASCII, provare a copiare una sottostringa che si ritiene contenga solo ASCII in una porzione specificata di a byte[], ecc.) È un peccato che i framework non abbiano i loro tipi di stringa implementare utili interfacce relative alle stringhe.
supercat,

1
Perché questa risposta parla di DI se la domanda riguardava DIP? DIP è un concetto degli anni '90, mentre DI è del 2004. Sono molto diversi.
Rogério,

5

Mi sembra che alla domanda originale manchi parte del punto del DIP.

Il motivo per cui sono scettico è perché stiamo pagando un prezzo per seguire questo principio. Supponiamo di dover implementare la funzione Z. Dopo l'analisi, concludo che la funzione Z è costituita dalle funzionalità A, B e C. Creo una classe fascade Z, che, attraverso le interfacce, utilizza le classi A, B e C. Comincio a codificare il implementazione e ad un certo punto mi rendo conto che l'attività Z in realtà consiste nelle funzionalità A, B e D. Ora devo scartare l'interfaccia C, il prototipo di classe C e scrivere un'interfaccia D e classe separate. Senza interfacce, solo la classe dovrebbe essere sostituita.

Per sfruttare davvero il DIP, devi prima creare la classe Z e farla chiamare la funzionalità delle classi A, B e C (che non sono ancora state sviluppate). Questo ti dà l'API per le classi A, B e C. Quindi vai a creare le classi A, B e C e inserisci i dettagli. Dovresti effettivamente creare le astrazioni di cui hai bisogno mentre crei la classe Z, basata interamente sulla classe Z di cui hai bisogno. Puoi persino scrivere test intorno alla classe Z prima ancora che vengano scritte le classi A, B o C.

Ricorda che il DIP afferma che "I moduli di alto livello non dovrebbero dipendere da moduli di basso livello. Entrambi dovrebbero dipendere dalle astrazioni".

Una volta che hai capito quale classe Z ha bisogno e il modo in cui vuole ottenere ciò di cui ha bisogno, puoi quindi inserire i dettagli. Certo, a volte sarà necessario apportare modifiche alla classe Z, ma il 99% delle volte non sarà così.

Non ci sarà mai una classe D perché hai capito che Z ha bisogno di A, B e C prima che vengano scritte. Un cambiamento nei requisiti è una storia completamente diversa.


5

La risposta breve è "quasi mai", ma in realtà ci sono alcuni posti in cui il DIP non ha senso:

  1. Fabbriche o costruttori, il cui compito è creare oggetti. Questi sono essenzialmente i "nodi foglia" in un sistema che abbraccia pienamente l'IoC. Ad un certo punto, qualcosa deve effettivamente creare i tuoi oggetti e non può dipendere da nient'altro per farlo. In molte lingue, un contenitore IoC può farlo per te, ma a volte è necessario farlo alla vecchia maniera.

  2. Implementazioni di strutture dati e algoritmi. In genere, in questi casi le caratteristiche salienti per cui si sta ottimizzando (come il tempo di esecuzione e la complessità asintotica) dipendono dai tipi di dati specifici utilizzati. Se stai implementando una tabella hash, devi davvero sapere che stai lavorando con un array per l'archiviazione, non un elenco collegato, e solo la tabella stessa sa come allocare correttamente gli array. Inoltre, non vuoi passare in un array mutabile e fare in modo che il chiamante rompa la tua tabella hash armeggiando con il suo contenuto.

  3. Classi del modello di dominio . Questi implementano la tua logica di business e (la maggior parte delle volte) ha senso avere un'unica implementazione, perché (la maggior parte delle volte) stai sviluppando il software solo per un'azienda. Sebbene alcune classi di modelli di dominio possano essere costruite utilizzando altre classi di modelli di dominio, questo sarà generalmente caso per caso. Poiché gli oggetti del modello di dominio non includono alcuna funzionalità che può essere utilmente derisa, il DIP non ha alcun vantaggio in termini di testabilità o manutenibilità.

  4. Qualsiasi oggetto che viene fornito come API esterna e deve creare altri oggetti, i cui dettagli di implementazione non si desidera esporre pubblicamente. Questo rientra nella categoria generale di "progettazione di librerie diversa dalla progettazione di applicazioni". Una libreria o un framework può fare un uso liberale di DI internamente, ma alla fine dovrà fare qualche lavoro reale, altrimenti non è una libreria molto utile. Supponiamo che tu stia sviluppando una libreria di rete; non vuoi davvero che il consumatore sia in grado di fornire la propria implementazione di un socket. È possibile utilizzare internamente un'astrazione di un socket, ma l'API esposta ai chiamanti sta per creare i propri socket.

  5. Test unitari e test doppi. I falsi e gli stub dovrebbero fare una cosa e farlo semplicemente. Se hai un falso abbastanza complesso da preoccuparti se fare o meno l'iniezione di dipendenza, allora è probabilmente troppo complesso (forse perché sta implementando un'interfaccia che è anche troppo complessa).

Potrebbe essercene di più; questi sono quelli che vedo su una base piuttosto frequente.


Che ne dici di "ogni volta che lavori in un linguaggio dinamico"?
Kevin,

No? Faccio molto lavoro in JavaScript e si applica ugualmente bene lì. La "O" e l '"I" in SOLID possono però risultare un po' sfocate.
Aaronaught,

Huh ... Trovo che i tipi di prima classe di Python combinati con la tipizzazione di anatre lo rendano piuttosto meno necessario.
Kevin,

Il DIP non ha assolutamente nulla a che fare con il sistema di tipi. E in che modo i "tipi di prima classe" sono unici per Python? Quando vuoi testare qualcosa in isolamento, devi sostituire i doppi di test con le sue dipendenze. Questi doppi di test possono essere implementazioni alternative di un'interfaccia (in linguaggi di tipo statico) oppure possono essere oggetti anonimi o tipi alternativi che hanno gli stessi metodi / funzioni (digitando duck). In entrambi i casi, è comunque necessario un modo per sostituire effettivamente un'istanza.
Aaronaught,

1
@Kevin Python non è stato il primo linguaggio a possedere una digitazione dinamica o derisioni. È anche completamente irrilevante. La domanda non è quale sia il tipo di un oggetto ma come / dove viene creato quell'oggetto. Quando un oggetto crea le sue dipendenze, sei costretto a testare l'unità di quelli che dovrebbero essere i dettagli di implementazione, facendo cose orribili come mettere a fuoco costruttori di classi di cui l'API pubblica non fa menzione. E dimenticare di testare, mescolare il comportamento e la costruzione di oggetti porta semplicemente a un accoppiamento stretto. La digitazione anatra non risolve nessuno di questi problemi.
Aaronaught

2

Alcuni segni che potresti applicare DIP a un livello troppo micro di livello, in cui non fornisce valore:

  • hai una coppia C / CImpl o IC / C, con una sola implementazione di quella interfaccia
  • le firme nella tua interfaccia e implementazione corrispondono una a una (violando il principio DRY)
  • cambi spesso C e CImpl contemporaneamente.
  • C è interna al tuo progetto e non condivisa al di fuori del tuo progetto come libreria.
  • sei frustrato da F3 in Eclipse / F12 in Visual Studio che ti porta all'interfaccia anziché alla classe effettiva

Se questo è quello che stai vedendo, potresti stare meglio semplicemente avendo Z che chiama direttamente C e salta l'interfaccia.

Inoltre, non penso alla decorazione del metodo tramite un'iniezione di dipendenza / un proxy dinamico (Spring, Java EE) allo stesso modo del vero DIP DI SOLIDO - questo è più simile a un dettaglio di implementazione di come funziona la decorazione di metodo in quello stack tecnologico. La comunità Java EE considera un miglioramento che non hai bisogno di coppie Foo / FooImpl come una volta ( riferimento ). Al contrario, Python supporta la decorazione di funzioni come funzionalità di linguaggio di prima classe.

Vedi anche questo post sul blog .


0

Se invertite sempre le dipendenze, tutte le dipendenze sono sottosopra. Ciò significa che se hai iniziato con un codice disordinato con un nodo di dipendenze, è quello che hai (davvero) ancora, invertito. È qui che si ottiene il problema che ogni modifica a un'implementazione deve cambiare anche la sua interfaccia.

Il punto di inversione delle dipendenze è invertire selettivamente le dipendenze che stanno facendo aggrovigliare le cose. Quelli che dovrebbero andare da A a B a C lo fanno ancora, sono quelli che vanno da C a A che ora vanno da A a C.

Il risultato dovrebbe essere un grafico delle dipendenze privo di cicli: un DAG. Esistono vari strumenti che controlleranno questa proprietà e disegneranno il grafico.

Per una spiegazione più completa, vedi questo articolo :

L'essenza dell'applicazione corretta del principio di inversione di dipendenza è questa:

Dividi il codice / servizio / ... dipende da un'interfaccia e un'implementazione. L'interfaccia ristruttura la dipendenza nel gergo del codice utilizzandolo, l'implementazione lo implementa in termini di tecniche sottostanti.

L'implementazione rimane dove si trova. Ma l'interfaccia ora ha una funzione diversa (e usa un linguaggio / linguaggio diversi), descrivendo qualcosa che il codice usando può fare. Spostalo in quel pacchetto. Non posizionando l'interfaccia e l'implementazione nello stesso pacchetto, la dipendenza (direzione della) viene invertita da utente → implementazione a implementazione → utente.

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.