Quando NON applicare il principio di inversione delle dipendenze?

43

Attualmente sto cercando di capire SOLID. Quindi il principio di inversione delle dipendenze significa che qualsiasi due classi dovrebbe comunicare tramite interfacce, non direttamente. Esempio: se class A ha un metodo, che si aspetta 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.

A condizione di averlo capito correttamente, la mia domanda sarebbe è una buona pratica applicarla a interazioni di classe all o dovrei provare a pensare in termini di livelli ?

Il motivo per cui sono scettico è perché paghiamo un prezzo per seguire questo principio. Ad esempio, devo implementare la funzione Z . Dopo l'analisi, concludo che la funzione Z è composta da funzionalità A , B e C . Creo una facciata classe Z , che, attraverso le interfacce, utilizza le classi A , B e C . Comincio a codificare l'implementazione e ad un certo punto mi rendo conto che l'attività Z è in realtà costituita da funzionalità A , B e D . Ora ho bisogno di scartare l'interfaccia C , il prototipo di classe C e scrivere l'interfaccia e la classe D 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 accoppiata direttamente con Python, avrei bisogno di cambiare solo l'implementazione.

    
posta Vorac 25.02.2015 - 19:26
fonte

7 risposte

86

In molte vignette 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 SOLIDO su una spalla, e YAGNI (Non ne avrai bisogno!) Seduto sull'altro.

I principi SOLID presi al massimo sono i più adatti per sistemi aziendali complessi, complessi e ultra configurabili. Per i sistemi più piccoli o più specifici, non è appropriato rendere tutto ridicolmente flessibile, poiché il tempo che passi ad astrarre le 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 per un flusso di rete. Tuttavia, per una grande quantità di progetti software, quel tipo di flessibilità non è solo mai che sarà necessario, e potresti anche solo passare classi di file concreti e chiamarlo un giorno e risparmiare le tue cellule cerebrali .

Una parte dell'arte dello sviluppo del software sta avendo un buon senso di ciò che è probabile che cambi col passare del tempo, e ciò che non lo è. Per le cose che è probabile che cambino, utilizzare le interfacce e altri concetti SOLID. Per cose che non lo sono, usa YAGNI e passa solo tipi concreti, dimentica le classi factory, dimentica tutto il runtime hooking e configuration, ecc, e dimentica molte delle astrazioni SOLID. Nella mia esperienza, l'approccio YAGNI ha dimostrato di essere corretto molto più spesso di quanto non sia.

    
risposta data 25.02.2015 - 20:27
fonte
11

In parole semplici:

L'applicazione del DIP è sia facile che divertente . Non ottenere il design giusto al primo tentativo non è una ragione sufficiente per rinunciare completamente al DIP.

  • Solitamente gli IDE ti aiutano a fare questo tipo di refactoring, alcuni addirittura ti permettono di estrarre l'interfaccia da una classe già implementata
  • È quasi impossibile ottenere il design per la prima volta
  • Il normale flusso di lavoro comporta il cambiamento e il ripensamento delle interfacce nei primi stadi di sviluppo
  • Man mano che lo sviluppo si evolve, maturerà e avrai meno motivi per modificare le interfacce
  • In una fase avanzata le interfacce (il design) saranno mature e difficilmente cambieranno
  • Da quel momento in poi, inizi a coglierne i benefici, dal momento che la tua app è aperta a scalare.

D'altra parte, la programmazione con interfacce e OOD può riportare la gioia alla capacità di programmazione a volte stantia.

Alcuni dicono che aggiunge complessità, ma penso che l'opossito sia vero. Anche per piccoli progetti. Rende più facile il test / beffa. Rende il tuo codice avere meno istruzioni case o% co_de nidificato. Riduce la complessità ciclomatica e ti fa pensare in modi nuovi. Rende la programmazione più simile al design e alla produzione del mondo reale.

    
risposta data 25.02.2015 - 19:31
fonte
9

Utilizza inversione di dipendenza dove ha senso.

Un estremo controesempio è la classe "stringa" inclusa in molte lingue. Rappresenta un concetto primitivo, essenzialmente una schiera 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 usati internamente in un modulo che non sono esposti ad altri moduli o riutilizzati ovunque, probabilmente non vale la pena di usare DI.

Ci sono due punti in cui DI dovrebbe essere usato automaticamente secondo me:

  1. Nei moduli progettati per l'estensione. Se l'intero scopo di un modulo è quello di estenderlo e modificare il comportamento, è perfettamente logico inserire DI in dall'inizio.

  2. Nei moduli che stai rifattorizzando allo scopo di riutilizzare il codice. Forse hai codificato una classe per fare qualcosa, poi renditi conto che con un refactoring puoi sfruttare quel codice altrove e c'è bisogno di farlo . Questo è un ottimo candidato per DI e altre modifiche all'estensibilità.

I tasti qui sono usali dove è necessario perché introdurranno una complessità extra e assicurati di misurare quel bisogno sia attraverso i requisiti tecnici (punto uno) o revisione quantitativa del codice (punto due).

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

* Eccezione alla regola precedente: una sega alternativa è lo strumento perfetto per qualsiasi lavoro. Se non risolve il tuo problema, lo rimuoverà. In modo permanente.

    
risposta data 25.02.2015 - 19:58
fonte
5

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

The reason I am skeptical is because we are paying some price for following this principle. Say, I need to implement feature Z. After analysis, I conclude that feature Z consists of functionality A, B and C. I create a fascade class Z, that, through interfaces, uses classes A, B and C. I begin coding the implementation and at some point I realize that task Z actually consists of functionality A, B and D. Now I need to scrap the C interface, the C class prototype and write separate D interface and class. Without interfaces, only the class would wave needed to be replaced.

Per trarre il massimo vantaggio dal DIP, devi prima creare la classe Z e chiamarla con 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 e crea le classi A, B e C e compila i dettagli. Dovresti effettivamente creare le astrazioni di cui hai bisogno mentre crei la classe Z, basandoti interamente su ciò di cui la classe Z ha bisogno. Puoi persino scrivere dei test attorno alla classe Z prima ancora che le classi A, B o C siano scritte.

Ricorda che il DIP dice che "I moduli di alto livello non dovrebbero dipendere da moduli di basso livello, entrambi dovrebbero dipendere dalle astrazioni."

Una volta che hai capito di cosa ha bisogno la classe Z e il modo in cui vuole ottenere ciò di cui ha bisogno, puoi quindi inserire i dettagli. Certo, a volte le modifiche dovranno essere apportate 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 fossero scritti. Un cambiamento nei requisiti è una storia completamente diversa.

    
risposta data 26.02.2015 - 02:02
fonte
5

La risposta breve è "quasi mai", ma ci sono, in effetti, alcuni punti 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 completamente 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 devi farlo alla vecchia maniera.

  2. Implementazioni di strutture dati e algoritmi. Generalmente, in questi casi le caratteristiche salienti che stai ottimizzando (come il tempo di esecuzione e la complessità asintotica) dipendono da specifici tipi di dati utilizzati. Se stai implementando una tabella hash, devi davvero sapere che stai lavorando con un array per lo storage, non con un elenco collegato, e solo il tavolo stesso sa come allocare correttamente gli array. Inoltre, non vuoi passare in un array mutabile e fare in modo che il chiamante interrompa il tuo hash table tranciandone il contenuto.

  3. Classi del modello di dominio . Questi implementano la tua logica di business e (il più delle volte) ha senso solo avere un'implementazione, perché (la maggior parte delle volte) stai solo sviluppando il software per un'azienda. Sebbene le alcune classi di modelli di dominio possano essere costruite usando altre classi di modelli di dominio, questo sarà generalmente su base caso-per-caso. Poiché gli oggetti del modello di dominio non includono alcuna funzionalità che possa essere utilmente derisa, non c'è alcun vantaggio sulla testabilità o manutenibilità del DIP.

  4. Qualsiasi oggetto fornito come API esterna e necessario creare altri oggetti, i cui dettagli di implementazione non desideri esporre pubblicamente. Questo rientra nella categoria generale di "design della biblioteca diverso dal design dell'applicazione". Una biblioteca o un framework può fare un uso liberale di DI internamente, ma alla fine dovrà svolgere un lavoro effettivo, altrimenti non è una libreria molto utile. Diciamo che stai sviluppando una libreria di rete; davvero non vuoi che il consumatore sia in grado di fornire la propria implementazione di un socket. Potresti utilizzare internamente un'astrazione di un socket, ma l'API che esponi ai chiamanti sta per creare i propri socket.

  5. Test unitari e test raddoppia. 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, probabilmente è troppo complesso (forse perché sta implementando un'interfaccia che è anche troppo complessa).

Potrebbero esserci di più; questi sono quelli che vedo su una base frequente un po '.

    
risposta data 26.02.2015 - 06:20
fonte
2

Alcuni segni potrebbero essere l'applicazione DIP a livello di micro livello, in cui non fornisce valore:

  • hai una coppia C / CImpl o IC / C, con solo una singola implementazione di tale interfaccia
  • le firme nell'interfaccia e l'implementazione corrispondono a una a una (violando il principio di ESSERE)
  • cambi frequentemente C e CImpl allo stesso tempo.
  • C è interno al tuo progetto e non è condiviso al di fuori del tuo progetto come libreria.
  • sei frustrato da F3 in Eclipse / F12 in Visual Studio che ti porta all'interfaccia invece della classe attuale

Se questo è ciò che stai vedendo, potresti stare meglio avendo solo la chiamata Z diretta e saltare l'interfaccia.

Inoltre, non penso alla decorazione del metodo da un framework di dipendenza / proxy dinamico (Spring, Java EE) allo stesso modo del vero DIP SOLID - questo è più simile a un dettaglio di implementazione di come la decorazione del metodo funziona in quella tecnologia pila. La comunità Java EE considera un miglioramento che non ti servono coppie Foo / FooImpl come hai usato per te ( di riferimento ). Al contrario, Python supporta la decorazione delle funzioni come funzionalità di linguaggio di prima classe.

Vedi anche questo post del blog .

    
risposta data 04.11.2015 - 22:48
fonte
0

Se inverti sempre le dipendenze, tutte le dipendenze sono sottosopra. Il che significa che se hai iniziato con un codice disordinato con un nodo di dipendenze, questo è ciò che hai (davvero) ancora, solo invertito. È qui che ottieni il problema che ogni modifica a un'implementazione deve cambiare anche la sua interfaccia.

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

Il risultato dovrebbe essere un grafico di dipendenza privo di cicli: un DAG. Ci sono vari strumenti che controlleranno questa proprietà e disegneranno il grafico.

Per una spiegazione più completa, vedi questo articolo :

The essence of applying the Dependency Inversion Principle correctly is this:

Split the code/service/… you depend on into an interface and implementation. The interface restructures the dependency in the jargon of the code using it, the implementation implements it in terms of its underlying techniques.

The implementation remains where it is. But the interface has a different function now (and uses a different jargon/language), describing something the using code can do. Move it to that package. By not placing the interface and implementation in the same package, the (direction of the) dependency is inverted from user→implementation to implementation→user.

    
risposta data 25.02.2015 - 23:32
fonte

Leggi altre domande sui tag