È possibile applicare DRY senza aumentare l'accoppiamento?


14

Supponiamo di avere un modulo software A che implementa una funzione F. Un altro modulo B implementa la stessa funzione di F '.

Esistono diversi modi per eliminare il codice duplicato:

  1. Lascia che A usi F 'da B.
  2. Lascia che B usi F da A.
  3. Inserisci F nel suo modulo C e lascia che sia A che B lo utilizzino.

Tutte queste opzioni generano dipendenze aggiuntive tra i moduli. Applicano il principio DRY al costo di aumentare l'accoppiamento.

Per quanto posso vedere, l'accoppiamento è sempre aumentato o al leasing spostato a un livello superiore quando si applica DRY. Sembra esserci un conflitto tra due dei principi di base della progettazione del software.

(In realtà non trovo sorprendente che ci siano conflitti del genere. Questo è probabilmente ciò che rende così difficile una buona progettazione del software. Trovo sorprendente che questi conflitti normalmente non vengano affrontati nei testi introduttivi.)

Modifica (per chiarimenti): presumo che l'uguaglianza di F e F 'non sia solo una coincidenza. Se F dovrà essere modificato, F 'dovrà probabilmente essere modificato allo stesso modo.


2
... Penso che DRY possa essere una strategia molto utile, ma questa domanda illustra un'inefficienza con DRY. Alcuni (ad esempio, gli appassionati di OOP) potrebbero obiettare che dovresti copiare / incollare F in B solo per motivi di mantenimento dell'autonomia concettuale di A e B, ma non ho incontrato uno scenario in cui lo farei. Penso che copiare / incollare il codice sia l'opzione peggiore, non sopporto quella sensazione di "perdita di memoria a breve termine" in cui ero così sicuro di aver già scritto un metodo / funzione per fare qualcosa; correggere un bug in una funzione e dimenticare di aggiornarne un'altra può essere un altro grosso problema.
jrh

3
Ci sono molti principi di questo OO che si contraddicono a vicenda. Nella maggior parte dei casi devi trovare un compromesso ragionevole. Ma IMHO il principio DRY è il più prezioso. Come ha scritto @jrh: avere lo stesso comportamento implementato in più punti è un incubo di manutenzione che dovrebbe essere evitato a tutti i costi. Scoprire che hai dimenticato di aggiornare una delle copie ridondanti in produzione può rovinare la tua attività.
Timothy Truckle,

2
@TimothyTruckle: non dovremmo chiamarli principi OO perché si applicano anche ad altri paradigmi di programmazione. E sì, DRY è prezioso ma anche pericoloso se esagerato. Non solo perché tende a creare dipendenze e quindi complessità. Viene spesso applicato anche ai duplicati causati da coincidenza che hanno diversi motivi per cambiare.
Frank Puffer,

1
... a volte anche quando ho incontrato questo scenario sono stato in grado di suddividere F in parti che possono essere utilizzabili per ciò di cui A ha bisogno e di cosa B.
jrh

1
L'accoppiamento non è intrinsecamente negativo ed è spesso necessario per ridurre gli errori e aumentare la produttività. Se dovessi usare una funzione parseInt dalla libreria standard della tua lingua all'interno della tua funzione, assoceresti la tua funzione alla libreria standard. Non vedo un programma che non lo fa da molti anni. La chiave è non creare accoppiamenti non necessari. Molto spesso, viene utilizzata un'interfaccia per evitare / rimuovere un tale accoppiamento. Ad esempio, la mia funzione può accettare un'implementazione di parseInt come argomento. Tuttavia, questo non è sempre necessario, né è sempre saggio.
Joshua Jones,

Risposte:


14

Tutte queste opzioni generano dipendenze aggiuntive tra i moduli. Applicano il principio DRY al costo di aumentare l'accoppiamento.

Perché sì lo fanno. Ma diminuiscono l'accoppiamento tra le linee. Quello che ottieni è il potere di cambiare l'accoppiamento. L'accoppiamento è disponibile in molte forme. L'estrazione del codice aumenta l'indirizzamento indiretto e l'astrazione. Aumentare ciò può essere buono o cattivo. La cosa numero uno che decide quale ottieni è il nome che usi per questo. Se guardare il nome mi lascia sorpreso quando guardo dentro, allora non hai fatto alcun favore a nessuno.

Inoltre, non seguire DRY nel vuoto. Se uccidi la duplicazione, ti assumi la responsabilità di prevedere che questi due usi di quel codice cambieranno insieme. Se è probabile che cambino in modo indipendente, hai causato confusione e lavoro extra per pochi benefici. Ma un nome davvero buono può renderlo più appetibile. Se tutto ciò che riesci a pensare è un brutto nome, per favore, fermati ora.

L'accoppiamento esisterà sempre a meno che il tuo sistema non sia così isolato che nessuno saprà mai se funziona. Quindi l'accoppiamento refactoring è un gioco di scelta del tuo veleno. Seguire DRY può ripagare minimizzando l'accoppiamento creato esprimendo ripetutamente la stessa decisione progettuale in molti punti fino a quando è molto difficile cambiare. Ma DRY può rendere impossibile la comprensione del codice. Il modo migliore per salvare quella situazione è trovare un nome davvero buono. Se non riesci a pensare a un buon nome, spero che tu sia abile nell'evitare nomi insignificanti


Giusto per essere sicuro di aver compreso correttamente il tuo punto, lasciami in modo leggermente diverso: se assegni un buon nome al codice estratto, il codice estratto non è più pertinente per comprendere il software perché il nome dice tutto (idealmente). L'accoppiamento esiste ancora a livello tecnico ma non a livello cognitivo. Quindi è relativamente innocuo, giusto?
Frank Puffer,

1
Non importa, mio ​​male: meta.stackexchange.com/a/263672/143358
Basilevs

@FrankPuffer meglio?
candied_orange

3

Esistono modi per interrompere dipendenze esplicite. Uno popolare è quello di iniettare dipendenze in fase di esecuzione. In questo modo, si ottiene ASCIUTTO, si rimuove l'accoppiamento a costo della sicurezza statica. È così popolare al giorno d'oggi, che le persone non capiscono nemmeno, che è un compromesso. Ad esempio, i contenitori di applicazioni forniscono regolarmente la gestione delle dipendenze che complica immensamente il software nascondendo la complessità. Anche la semplice iniezione di un vecchio costruttore non garantisce alcuni contratti a causa della mancanza di un sistema di tipo.

Per rispondere al titolo: sì, è possibile, ma sii preparato alle conseguenze dell'invio di runtime.

  • Definire l'interfaccia F A in A, fornendo la funzionalità di F
  • Definire l'interfaccia F B in B
  • Metti F in C
  • Crea il modulo D per gestire tutte le dipendenze (dipende da A, B e C)
  • Adatta F a F A e F B in D
  • Iniettare (passare) i wrapper in A e B

In questo modo, l'unico tipo di dipendenze che avresti è D a seconda dell'altro modulo.

Oppure registra C nel contenitore dell'applicazione con l'iniezione di dipendenza integrata e goditi le fortune di autowiring in lento aumento della classe di runtime caricando loop e deadlock.


1
+1, ma con l'esplicito avvertimento che di solito si tratta di un cattivo compromesso. Quella sicurezza statica è lì per la tua protezione, e aggirarla con un mucchio di azioni inquietanti a distanza sta solo chiedendo bug difficili da rintracciare più in basso quando la complessità del tuo progetto cresce un po '...
Mason Wheeler

1
Si può davvero dire che DI spezza la dipendenza? Anche formalmente, hai bisogno di un'interfaccia con la firma di F per implementarla. Tuttavia, se il modulo A utilizza r da F utilizzando C da esso generato, indipendentemente dal fatto che C sia iniettato in fase di esecuzione o direttamente collegato. Non interrompe la dipendenza, il bug rimanda l'errore solo se la dipendenza non viene fornita
max630

@ max630 sostituisce la dipendenza dell'implementazione con quella contrattuale, che è più debole.
Basilevs,

@ max630 Hai ragione. Non si può dire che DI rompa una dipendenza. DI è, infatti, un metodo per introdurre dipendenze ed è veramente ortogonale alla domanda posta in merito all'accoppiamento. Un cambiamento in F (o l'interfaccia che lo incapsula) richiederebbe comunque un cambiamento sia in A che in B.
king-side-slide

1

Non sono sicuro che una risposta senza ulteriore contesto abbia senso.

Dipende Agià Bo viceversa? - nel qual caso potremmo avere una scelta ovvia di casa per F.

Non Ae Bgià condividere eventuali dipendenze comuni che potrebbero essere una buona casa per F?

Quanto è grande / complesso F? Da cos'altro Fdipende?

Sono moduli Ae Butilizzati nello stesso progetto?

Sarà Ae Bfinirà per condividere qualche dipendenza comune comunque?

Quale linguaggio / sistema viene utilizzato: quanto è doloroso un nuovo modulo, nel dolore del programmatore, nel sovraccarico delle prestazioni? Ad esempio, se stai scrivendo in C / C ++ con il sistema del modulo in COM, che causa dolore al codice sorgente, richiede strumenti alternativi, ha implicazioni sul debug e ha implicazioni sulle prestazioni (per invocazioni tra moduli), potrei fai una pausa seria.

D'altra parte, se stai parlando di DLL Java o C # che si combinano piuttosto facilmente in un singolo ambiente di esecuzione, è un'altra questione.


Una funzione è un'astrazione e supporta DRY.

Tuttavia, devono essere complete buone astrazioni - le astrazioni incomplete possono benissimo causare al cliente consumatore (programmatore) di colmare il deficit usando la conoscenza dell'implementazione sottostante: questo si traduce in un accoppiamento più stretto rispetto a se l'astrazione fosse offerta invece come più completa.

Quindi, direi di cercare di creare un'astrazione migliore Ae su cui Bfare affidamento piuttosto che spostare una singola funzione in un nuovo modulo C

Sarei alla ricerca di una serie di funzioni da prendere in giro in una nuova astrazione, vale a dire, potrei aspettare fino a quando la base di codice non sarà più avanti al fine di identificare un refactoring di astrazione più completo / più completo da fare piuttosto che uno basato su un unico codice funzione dire.


2
Non è pericoloso associare astrazioni e grafico delle dipendenze?
Basilevs,

Does A already depend on B or vice versa? — in which case we might have an obvious choice of home for F.Questo presuppone che A farà sempre affidamento su B (o viceversa), il che è un presupposto molto pericoloso. Il fatto che OP veda F come non intrinsecamente parte di A (o B) suggerisce che F esiste senza essere inerente a nessuna delle due librerie. Se F appartiene a una libreria (ad esempio un metodo di estensione DbContext (F) e una libreria wrapper Entity Framework (A o B)), allora la domanda di OP sarebbe controversa.
Flater

0

Le risposte qui che si concentrano su tutti i modi in cui è possibile "minimizzare" questo problema ti stanno facendo un disservizio. E le "soluzioni" che offrono semplicemente modi diversi di creare accoppiamenti non sono affatto soluzioni.

La verità è che non riescono a capire il problema che hai creato. Il problema con il tuo esempio non ha nulla a che fare con DRY, piuttosto (più in generale) con il design dell'applicazione.

Chiediti perché i moduli A e B sono separati se entrambi dipendono dalla stessa funzione F? Naturalmente avrai problemi con la gestione delle dipendenze / l'astrazione / l'accoppiamento / il tuo nome se ti impegni a progettare male.

La modellazione dell'applicazione corretta viene eseguita in base al comportamento. Pertanto, i pezzi di A e B che dipendono da F devono essere estratti nel loro modulo indipendente. Se ciò non è possibile, è necessario combinare A e B. In entrambi i casi, A e B non sono più utili al sistema e dovrebbero cessare di esistere.

DRY è un principio che può essere utilizzato per esporre un design scadente, non per causarlo. Se non riesci a raggiungere il DRY ( quando si applica veramente - notando la tua modifica) a causa della struttura della tua applicazione, è un chiaro segno che la struttura è diventata una responsabilità. Ecco perché anche il "refactoring continuo" è un principio da seguire.

Gli ABC degli altri principi di progettazione (SOLID, DRY, ecc.) Là fuori sono tutti usati per rendere il cambiamento (incluso il refactoring) un'applicazione più indolore. Concentrati su questo , e tutti gli altri problemi iniziano a scomparire.


Suggerisci di avere esattamente un modulo in ogni applicazione?
Basilevs,

@Basilevs Assolutamente no (a meno che non sia garantito). Suggerisco di avere il maggior numero possibile di moduli completamente disaccoppiati. Dopotutto, questo è l'intero scopo di un modulo.
King-side-slide

Bene, la domanda implica che i moduli A e B contengano funzioni non correlate e che siano già estratti di conseguenza, perché e come dovrebbero cessare di esistere? Qual è la tua soluzione al problema come indicato?
Basilevs,

@Basilevs La domanda implica che A e B non sono stati modellati correttamente. È questa deficienza intrinseca che sta causando il problema in primo luogo. Certamente il semplice fatto che essi fanno esistere non è una prova che essi dovrebbero esistere. Questo è il punto che sto sottolineando. Chiaramente, è necessario un design alternativo per evitare di rompere il DRY. È importante capire che lo scopo stesso di tutti questi "principi di progettazione" è quello di rendere più semplice la modifica di un'applicazione.
King-side-slide

Molti altri metodi, con dipendenze completamente diverse, sono stati modellati male e devono essere rifatti, proprio a causa di questo singolo non accidentale? O pensi che i moduli A e B contengano un singolo metodo ciascuno?
Basilevs,

0

Tutte queste opzioni generano dipendenze aggiuntive tra i moduli. Applicano il principio DRY al costo di aumentare l'accoppiamento.

Ho un'opinione diversa, almeno per la terza opzione:

Dalla tua descrizione:

  • A ha bisogno di F
  • B ha bisogno di F
  • Né A né B hanno bisogno l'uno dell'altro.

Inserire F in un modulo C non aumenta l'accoppiamento poiché sia ​​A che B hanno già bisogno della funzione C.

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.