Ist es möglich, DRY anzuwenden, ohne die Kopplung zu erhöhen?


14

Angenommen, wir haben ein Softwaremodul A, das eine Funktion F implementiert. Ein anderes Modul B implementiert die gleiche Funktion wie F '.

Es gibt verschiedene Möglichkeiten, um den doppelten Code zu entfernen:

  1. A benutze F 'aus B.
  2. Lassen Sie B F von A verwenden.
  3. Setzen Sie F in sein eigenes Modul C ein und lassen Sie A und B es verwenden.

Alle diese Optionen erzeugen zusätzliche Abhängigkeiten zwischen Modulen. Sie wenden das DRY-Prinzip auf Kosten der Erhöhung der Kopplung an.

Soweit ich sehen kann, wird die Kopplung beim Auftragen von DRY immer erhöht oder zumindest auf ein höheres Niveau verschoben. Es scheint einen Konflikt zwischen zwei der grundlegendsten Prinzipien des Software-Designs zu geben.

(Eigentlich finde ich es nicht verwunderlich, dass es solche Konflikte gibt. Dies ist wahrscheinlich der Grund, warum gutes Software-Design so schwierig ist. Ich finde es verwunderlich, dass diese Konflikte normalerweise nicht in einleitenden Texten behandelt werden.)

Edit (zur Verdeutlichung): Ich gehe davon aus, dass die Gleichheit von F und F 'nicht nur ein Zufall ist. Wenn F modifiziert werden muss, muss F 'wahrscheinlich auf die gleiche Weise modifiziert werden.


2
... Ich denke, dass DRY eine sehr nützliche Strategie sein kann, aber diese Frage zeigt, dass DRY ineffizient ist. Einige (z. B. OOP-Enthusiasten) argumentieren möglicherweise, dass Sie F in B kopieren / einfügen sollten, nur um die konzeptionelle Autonomie von A und B beizubehalten, aber ich bin nicht auf ein Szenario gestoßen, in dem ich das tun würde. Ich denke, das Kopieren / Einfügen von Code ist die schlechteste Option. Ich kann das Gefühl des "Verlusts des Kurzzeitgedächtnisses" nicht ertragen, bei dem ich nur so sicher war, dass ich bereits eine Methode / Funktion geschrieben habe, um etwas zu tun. Das Beheben eines Fehlers in einer Funktion und das Vergessen, eine andere zu aktualisieren, kann ein weiteres großes Problem sein.
Jrh

3
Es gibt viele dieser OO-Prinzipien, die sich widersprechen. In den meisten Fällen müssen Sie einen angemessenen Kompromiss finden. Aber meiner Meinung nach ist das DRY-Prinzip das wertvollste. Wie @jrh schrieb: Dasselbe Verhalten an mehreren Orten implementiert wird, ist ein Wartungs-Albtraum, der um jeden Preis vermieden werden sollte. Wenn Sie feststellen, dass Sie vergessen haben, eine der redundanten Kopien in der Produktion zu aktualisieren, kann dies Ihr Geschäft beeinträchtigen.
Timothy Truckle

2
@TimothyTruckle: Wir sollten sie nicht als OO-Prinzipien bezeichnen, da sie auch für andere Programmierparadigmen gelten. Und ja, TROCKEN ist wertvoll, aber auch gefährlich, wenn es übertrieben wird. Nicht nur, weil dadurch Abhängigkeiten und damit Komplexität entstehen. Es wird auch häufig auf zufällige Duplikate angewendet, die unterschiedliche Änderungsgründe haben.
Frank Puffer

1
... auch manchmal, wenn ich in dieses Szenario geraten bin, konnte ich F in Teile aufteilen, die für die Anforderungen von A und B verwendbar sind.
Jrh

1
Das Koppeln ist von Natur aus keine schlechte Sache und oft notwendig, um Fehler zu verringern und die Produktivität zu steigern. Wenn Sie eine parseInt-Funktion aus der Standardbibliothek Ihrer Sprache in Ihrer Funktion verwenden würden, würden Sie Ihre Funktion an die Standardbibliothek koppeln. Ich habe seit vielen Jahren kein Programm gesehen, das dies nicht tut. Der Schlüssel besteht darin, keine unnötigen Kopplungen zu erstellen. Am häufigsten wird eine Schnittstelle verwendet, um eine solche Kopplung zu vermeiden / zu entfernen. Zum Beispiel kann meine Funktion eine Implementierung von parseInt als Argument akzeptieren. Dies ist jedoch nicht immer notwendig und auch nicht immer sinnvoll.
Joshua Jones

Antworten:


14

Alle diese Optionen erzeugen zusätzliche Abhängigkeiten zwischen Modulen. Sie wenden das DRY-Prinzip auf Kosten der Erhöhung der Kopplung an.

Warum tun sie das? Sie verringern jedoch die Kopplung zwischen den Leitungen. Was Sie bekommen, ist die Kraft, die Kupplung zu ändern. Kopplung gibt es in vielen Formen. Das Extrahieren von Code erhöht die Indirektion und Abstraktion. Das kann gut oder schlecht sein. Die Nummer eins, die entscheidet, welche Sie erhalten, ist der Name, den Sie dafür verwenden. Wenn ein Blick auf den Namen mich überrascht, wenn ich hineinschaue, dann haben Sie niemandem einen Gefallen getan.

Folgen Sie DRY auch nicht im luftleeren Raum. Wenn Sie die Vervielfältigung beenden, müssen Sie vorhersagen, dass sich diese beiden Verwendungszwecke des Codes gemeinsam ändern werden. Wenn sie sich wahrscheinlich unabhängig voneinander ändern, haben Sie Verwirrung und zusätzliche Arbeit mit geringem Nutzen verursacht. Aber ein wirklich guter Name kann das schmackhafter machen. Wenn Sie nur an einen schlechten Ruf denken können, hören Sie jetzt einfach auf.

Eine Kopplung besteht immer, es sei denn, Ihr System ist so isoliert, dass niemand jemals weiß, ob es funktioniert. Das Refactoring der Kopplung ist also ein Spiel, bei dem Sie Ihr Gift auswählen müssen. Die Einhaltung von DRY kann sich auszahlen, indem die entstehende Kopplung minimiert wird, indem an vielen Stellen dieselbe Konstruktionsentscheidung wiederholt zum Ausdruck gebracht wird, bis eine Änderung sehr schwierig ist. Aber DRY kann es unmöglich machen, Ihren Code zu verstehen. Der beste Weg, diese Situation zu retten, besteht darin, einen wirklich guten Namen zu finden. Wenn Ihnen kein guter Name einfällt, können Sie hoffentlich bedeutungslose Namen vermeiden


Um sicherzugehen, dass ich Ihren Standpunkt richtig verstehe, lassen Sie es mich etwas anders formulieren: Wenn Sie dem extrahierten Code einen guten Namen zuweisen, ist der extrahierte Code für das Verständnis der Software nicht mehr relevant, da der Name (idealerweise) alles sagt. Die Kopplung existiert immer noch auf technischer Ebene, aber nicht auf kognitiver Ebene. Daher ist es relativ harmlos, oder?
Frank Puffer

1
Nevermind, meine schlechte: meta.stackexchange.com/a/263672/143358
Basilevs

@FrankPuffer besser?
candied_orange

3

Es gibt Möglichkeiten, explizite Abhängigkeiten aufzuheben. Eine beliebte Methode ist das Einfügen von Abhängigkeiten in die Laufzeit. Auf diese Weise erhalten Sie DRY, entfernen die Kupplung auf Kosten der statischen Sicherheit. Es ist heutzutage so beliebt, dass die Leute es nicht einmal verstehen, dass das ein Kompromiss ist. Beispielsweise stellen Anwendungscontainer routinemäßig ein Abhängigkeitsmanagement bereit, das die Software erheblich kompliziert, indem es die Komplexität verbirgt. Sogar die einfache Injektion alter Konstrukteure kann einige Verträge aufgrund fehlender Typsysteme nicht garantieren.

Um den Titel zu beantworten - ja, es ist möglich, aber auf die Folgen des Laufzeitversands vorbereitet zu sein.

  • Definieren Sie die Schnittstelle F A in A und stellen Sie die Funktionalität von F bereit
  • Definieren Sie die Schnittstelle F B in B
  • Setzen Sie F in C
  • Erstellen Sie Modul D, um alle Abhängigkeiten zu verwalten (es hängt von A, B und C ab)
  • Passen Sie F an F A und F B an in D an
  • Injizieren (übergeben) Sie Wrapper an A und B

Auf diese Weise ist die einzige Art von Abhängigkeiten, die Sie haben würden, D, abhängig von den anderen Modulen.

Oder registrieren Sie C im Anwendungscontainer mit integrierter Abhängigkeitsinjektion und genießen Sie die Möglichkeit, automatisch langsam wachsende Laufzeitklassen-Ladeschleifen und Deadlocks zu verdrahten.


1
+1, aber mit dem ausdrücklichen Vorbehalt, dass dies normalerweise ein schlechter Kompromiss ist. Diese statische Sicherheit dient Ihrem Schutz und die Umgehung mit ein paar gruseligen Aktionen aus der Ferne verlangt nur nach schwer auffindbaren Fehlern, wenn Ihre Projektkomplexität ein wenig zunimmt ...
Mason Wheeler

1
Kann man wirklich sagen, dass DI die Abhängigkeit bricht? Auch formal benötigen Sie eine Schnittstelle mit der Signatur von F, um sie zu implementieren. Aber noch, wenn das Modul A rwally wird mit F von C auf sie depwnds, unabhängig von C zur Laufzeit eingespritzt wird , oder direkt linked.DI nicht bricht die Abhängigkeit, Fehler nur defer Versagen , wenn die dependenc nicht vorgesehen
max630

@ max630 Ersetzt die Implementierungsabhängigkeit durch eine vertragliche, die schwächer ist.
Basilevs

@ max630 Du bist richtig. Von DI kann nicht gesagt werden, dass es eine Abhängigkeit unterbricht. DI ist in der Tat eine Methode, um Abhängigkeiten einzuführen , und ist wirklich orthogonal zu der Frage, die in Bezug auf die Kopplung gestellt wird. Eine Änderung von F (oder der Schnittstelle, die es einschließt) würde immer noch eine Änderung von A und B erfordern.
King-Side-Slide

1

Ich bin mir nicht sicher, ob eine Antwort ohne weiteren Kontext sinnvoll ist.

Kommt Aschon drauf an Boder umgekehrt? - In diesem Fall könnten wir eine offensichtliche Wahl für unser Zuhause haben F.

Haben Aund Bteilen Sie bereits gemeinsame Abhängigkeiten, für die ein gutes Zuhause sein könnte F?

Wie groß / komplex ist F? Was hängt noch Fdavon ab?

Werden Module Aund Bim selben Projekt verwendet?

Wird Aund wird es Btrotzdem eine gemeinsame Abhängigkeit geben?

Welches Sprach- / Modulsystem wird verwendet: Wie schmerzhaft ist ein neues Modul bei Programmierschmerzen und Leistungskosten? Wenn Sie beispielsweise in C / C ++ schreiben, während das Modulsystem COM ist, was den Quellcode schmerzt, alternative Tools erfordert, Auswirkungen auf das Debugging hat und Auswirkungen auf die Leistung hat (für Aufrufe zwischen Modulen), könnte ich dies tun mach eine ernsthafte Pause.

Wenn es sich hingegen um Java- oder C # -DLLs handelt, die sich nahtlos in einer einzigen Ausführungsumgebung kombinieren lassen, ist dies eine andere Sache.


Eine Funktion ist eine Abstraktion und unterstützt DRY.

Gute Abstraktionen müssen jedoch vollständig sein - unvollständige Abstraktionen können sehr wohl dazu führen, dass der konsumierende Client (Programmierer) das Defizit anhand der Kenntnis der zugrunde liegenden Implementierung ausgleicht. Dies führt zu einer engeren Kopplung, als wenn die Abstraktion stattdessen als vollständiger angeboten würde.

Ich würde also versuchen, eine bessere Abstraktion zu schaffen Aund sich darauf Bzu verlassen, als nur eine einzelne Funktion in ein neues Modul zu verschiebenC

Ich würde nach einer Reihe von Funktionen suchen, die sich in eine neue Abstraktion einfügen lassen. Das heißt, ich könnte warten, bis die Codebasis weiter fortgeschritten ist, um eine umfassendere / vollständigere Abstraktion zu identifizieren, die umgestaltet werden soll, anstatt auf einer Basis auf einem einzigen Funktionscode erzählen.


2
Ist es nicht gefährlich, Abstraktionen und Abhängigkeitsgraphen zu koppeln?
Basilevs

Does A already depend on B or vice versa? — in which case we might have an obvious choice of home for F.Dies setzt voraus, dass A immer auf B angewiesen ist (oder umgekehrt), was eine sehr gefährliche Annahme ist. Die Tatsache, dass OP F als nicht inhärent Teil von A (oder B) ansieht, legt nahe, dass F existiert, ohne einer der Bibliotheken inhärent zu sein. Wenn F zu einer Bibliothek gehört (z. B. eine DbContext-Erweiterungsmethode (F) und eine Entity Framework-Wrapper-Bibliothek (A oder B)), ist die Frage von OP unbeantwortet.
Flater

0

Die Antworten hier, die sich auf alle Möglichkeiten konzentrieren, wie Sie dieses Problem „minimieren“ können, sind für Sie ein schlechter Dienst. Und „Lösungen“, die lediglich verschiedene Möglichkeiten zum Erstellen von Kopplungen bieten, sind überhaupt keine wirklichen Lösungen.

Die Wahrheit ist, dass Sie das von Ihnen geschaffene Problem nicht verstehen. Das Problem mit Ihrem Beispiel hat nichts mit DRY zu tun, sondern (allgemeiner) mit dem Anwendungsdesign.

Fragen Sie sich, warum die Module A und B getrennt sind, wenn beide von derselben Funktion F abhängen? Natürlich werden Sie Probleme mit Abhängigkeitsmanagement / Abstraktion / Kopplung / Sie-Name-es haben, wenn Sie sich zu einem schlechten Design bekennen.

Die ordnungsgemäße Anwendungsmodellierung erfolgt je nach Verhalten. Daher müssen die von F abhängigen Teile von A und B in ein eigenes, unabhängiges Modul extrahiert werden. Ist dies nicht möglich, müssen A und B kombiniert werden. In beiden Fällen sind A und B für das System nicht mehr nützlich und sollten nicht mehr existieren.

DRY ist ein Prinzip, das verwendet werden kann, um schlechtes Design aufzudecken, und nicht, um es zu verursachen. Wenn Sie nicht erreichen können, TROCKEN ( wenn aufgrund der Struktur Ihrer Anwendung dies wirklich zutrifft - und Ihre Bearbeitung notieren), ist dies ein klares Zeichen dafür, dass die Struktur zur Verbindlichkeit geworden ist. Deshalb ist auch das „Continuous Refactoring“ ein Grundsatz.

Das ABC der anderen Gestaltungsprinzipien (fest, trocken, usw.) da draußen sind alle verwendet, um zu ändern (einschließlich Refactoring) einer Anwendung mehr schmerzlos. Konzentrieren Sie sich darauf , und alle anderen Probleme verschwinden allmählich.


Schlagen Sie vor, in jeder Anwendung genau ein Modul zu haben?
Basilevs

@Basilevs Absolut nicht (es sei denn, es ist gerechtfertigt). Ich schlage vor, so viele vollständig entkoppelte Module wie möglich zu haben. Immerhin ist das der gesamte Zweck eines Moduls.
King-Side-Slide

Nun, die Frage impliziert, dass die Module A und B nicht verwandte Funktionen enthalten und bereits entsprechend extrahiert werden, warum und wie sollten sie aufhören zu existieren? Was ist Ihre Lösung für das angegebene Problem?
Basilevs

@Basilevs Die Frage impliziert, dass A und B nicht richtig modelliert wurden. Es ist dieser inhärente Mangel, der das Problem in erster Linie verursacht. Gewiß ist die einfache Tatsache , dass sie tun gibt , ist kein Beweis , dass sie sollten vorhanden sein. Das ist der Punkt, den ich oben anspreche. Natürlich ist ein alternatives Design erforderlich, um ein Brechen von TROCKEN zu vermeiden. Es ist wichtig zu verstehen, dass der eigentliche Zweck all dieser „Konstruktionsprinzipien“ darin besteht, die Änderung einer Anwendung zu vereinfachen.
King-Side-Slide

Wurden unzählige andere Methoden mit völlig anderen Abhängigkeiten als schlecht modelliert und mussten nur wegen dieser einzigen nicht zufälligen Methode überarbeitet werden? Oder nehmen Sie an, dass die Module A und B jeweils eine Methode enthalten?
Basilevs

0

Alle diese Optionen erzeugen zusätzliche Abhängigkeiten zwischen Modulen. Sie wenden das DRY-Prinzip auf Kosten der Erhöhung der Kopplung an.

Zumindest für die dritte Option bin ich anderer Meinung:

Aus Ihrer Beschreibung:

  • A braucht F
  • B braucht F
  • Weder A noch B brauchen sich.

Das Einfügen von F in ein C-Modul erhöht die Kopplung nicht, da sowohl A als auch B bereits die C-Funktion benötigen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.