Hinzufügen von Komplexität, um doppelten Code zu entfernen


24

Ich habe mehrere Klassen, die alle von einer generischen Basisklasse erben. Die Basisklasse enthält eine Auflistung mehrerer Objekte vom Typ T.

Jede untergeordnete Klasse muss in der Lage sein, interpolierte Werte aus der Auflistung von Objekten zu berechnen. Da die untergeordneten Klassen jedoch unterschiedliche Typen verwenden, variiert die Berechnung von Klasse zu Klasse geringfügig.

Bisher habe ich meinen Code von Klasse zu Klasse kopiert / eingefügt und kleinere Änderungen an jedem vorgenommen. Aber jetzt versuche ich, den duplizierten Code zu entfernen und durch eine generische Interpolationsmethode in meiner Basisklasse zu ersetzen. Das gestaltet sich jedoch sehr schwierig, und alle Lösungen, die ich mir vorgestellt habe, scheinen viel zu komplex zu sein.

Ich fange an zu glauben, dass das DRY-Prinzip in solchen Situationen nicht so häufig angewendet wird, aber das klingt nach Blasphemie. Wie viel Komplexität ist zu viel, wenn versucht wird, Codeduplizierungen zu entfernen?

BEARBEITEN:

Die beste Lösung, die ich finden kann, sieht ungefähr so ​​aus:

Basisklasse:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

Kinderklasse A:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

Kinderklasse B:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}

9
Eine übermäßige Mikroanwendung von Konzepten wie DRY und Code Reuse führt zu weitaus größeren Sünden.
Affe

1
Sie erhalten einige gute allgemeine Antworten. Die Bearbeitung mit einer Beispielfunktion kann uns dabei helfen, festzustellen, ob Sie in diesem bestimmten Fall zu weit gehen.
Karl Bielefeldt

Dies ist keine wirkliche Antwort, sondern eher eine Beobachtung: Wenn Sie nicht einfach erklären können, was eine ausgeklügelte Basisklasse tut , ist es möglicherweise am besten, keine zu haben. Eine andere Sichtweise ist (ich nehme an, Sie sind mit SOLID vertraut?): "Benötigt ein wahrscheinlicher Verbraucher dieser Funktionalität eine Liskov-Substitution?" Wenn es für einen verallgemeinerten Verbraucher mit Interpolationsfunktionalität keinen wahrscheinlichen Geschäftsfall gibt, hat eine Basisklasse keinen Wert.
Tom W

1
Das erste ist, das Triplett X, Y, Z in einem Positionstyp zu sammeln und diesem Typ eine Interpolation als Element oder möglicherweise eine statische Methode hinzuzufügen: Position interpolieren (Position other, ratio).
Kevin Cline

Antworten:


30

In gewisser Weise haben Sie Ihre eigene Frage mit dieser Bemerkung im letzten Absatz beantwortet:

Ich fange an zu glauben, dass das DRY-Prinzip in solchen Situationen nicht so häufig angewendet wird, aber das klingt nach Blasphemie .

Wenn Sie eine Übung finden, die für die Lösung Ihres Problems nicht wirklich praktisch ist, versuchen Sie nicht, diese Übung religiös anzuwenden ( Wortblasphemie ist eine Art Warnung dafür). Die meisten Praktiken haben ihr Wann und Warum und selbst wenn sie 99% aller möglichen Fälle abdecken, gibt es immer noch 1%, bei denen Sie möglicherweise einen anderen Ansatz benötigen.

Insbesondere in Bezug auf DRY fand ich auch heraus, dass es manchmal sogar besser ist, mehrere Teile von dupliziertem, aber einfachem Code zu haben, als eine riesige Monstrosität, die einem beim Betrachten übel wird.

Das Vorhandensein dieser Randfälle sollte jedoch nicht als Entschuldigung für ein schlampiges Kopieren und Einfügen oder den vollständigen Mangel an wiederverwendbaren Modulen herangezogen werden. Wenn Sie keine Ahnung haben, wie Sie einen generischen und lesbaren Code für ein Problem in einer bestimmten Sprache schreiben können , ist Redundanz wahrscheinlich weniger schlimm . Denken Sie daran, wer Code warten muss. Würden sie leichter mit Redundanz oder Verschleierung leben?

Eine spezifischere Beratung zu Ihrem speziellen Beispiel. Sie sagten, diese Berechnungen seien ähnlich und doch leicht unterschiedlich . Möglicherweise möchten Sie versuchen, Ihre Berechnungsformel in kleinere Unterformeln aufzuteilen, und dann alle Ihre geringfügig unterschiedlichen Berechnungen diese Hilfsfunktionen aufrufen, um die Unterberechnungen durchzuführen. Sie vermeiden die Situation, in der jede Berechnung von übermäßig verallgemeinertem Code abhängt und Sie immer noch einen gewissen Grad an Wiederverwendung haben.


10
Ein weiterer Punkt, der ähnlich und doch ein wenig anders ist, ist, dass sie zwar im Code ähnlich aussehen, aber nicht bedeuten müssen, dass sie im "Geschäft" ähnlich sind. Kommt natürlich darauf an, was es ist, aber manchmal ist es eine gute Idee, die Dinge getrennt zu halten, denn obwohl sie gleich aussehen, können sie auf unterschiedlichen Geschäftsentscheidungen / -anforderungen beruhen. Vielleicht möchten Sie sie als sehr unterschiedliche Berechnungen betrachten, auch wenn sie hinsichtlich des Codes ähnlich aussehen. (Keine Regel oder irgendetwas, sondern nur etwas, das man beachten muss, wenn man entscheidet, ob Dinge kombiniert oder umgestaltet werden sollen :)
Svish

@Svish Interessanter Punkt. Ich habe nie so darüber nachgedacht.
Phil

17

untergeordnete Klassen verwenden unterschiedliche Typen, die Berechnung variiert geringfügig von Klasse zu Klasse.

In erster Linie gilt: Zuerst muss klar festgestellt werden, welcher Teil sich zwischen den Klassen ändert und welcher NICHT. Sobald Sie das erkannt haben, ist Ihr Problem gelöst.

Tun Sie dies als erste Übung, bevor Sie mit dem Re-Factoring beginnen. Alles andere wird danach automatisch in Position gebracht.


2
Gut gesagt. Dies kann nur ein Problem der wiederholten Funktion sein, die zu groß ist.
Karl Bielefeldt

8

Ich glaube, dass fast jede Wiederholung von mehr als ein paar Codezeilen auf die eine oder andere Weise ausgeschlossen werden kann und fast immer sollte.

Dieses Refactoring ist jedoch in einigen Sprachen einfacher als in anderen. In Sprachen wie LISP, Ruby, Python, Groovy, Javascript, Lua usw. ist dies recht einfach. In C ++ ist die Verwendung von Vorlagen normalerweise nicht allzu schwierig. Schmerzhafter in C, wo das einzige Werkzeug Präprozessor-Makros sein können. In Java oft schmerzhaft und manchmal einfach unmöglich, z. B. beim Versuch, generischen Code zu schreiben, um mehrere integrierte numerische Typen zu verarbeiten.

In ausdrucksstärkeren Sprachen steht außer Frage: Refaktorieren Sie nur ein paar Zeilen Code. Bei weniger ausdrucksstarken Sprachen müssen Sie den Schmerz des Refactorings gegen die Länge und Stabilität des wiederholten Codes abwägen. Wenn der wiederholte Code lang ist oder sich häufig ändert, neige ich zur Umgestaltung, auch wenn der resultierende Code etwas schwierig zu lesen ist.

Ich akzeptiere wiederholten Code nur, wenn er kurz und stabil ist und das Refactoring einfach zu hässlich ist. Grundsätzlich ziehe ich fast alle Duplikate heraus, es sei denn, ich schreibe Java.

Es ist unmöglich, eine spezifische Empfehlung für Ihren Fall abzugeben, da Sie den Code nicht veröffentlicht oder sogar angegeben haben, welche Sprache Sie verwenden.


Lua ist kein Akronym.
DeadMG

@DeadMG: notiert; Fühlen Sie sich frei zu bearbeiten. Deshalb haben wir Ihnen diesen Ruf verliehen.
Kevin Cline

5

Wenn Sie sagen, dass die Basisklasse einen Algorithmus ausführen muss, der Algorithmus jedoch für jede Unterklasse unterschiedlich ist, klingt dies wie ein perfekter Kandidat für das Vorlagenmuster .

Damit führt die Basisklasse den Algorithmus aus, und wenn es um die Variation für jede Unterklasse geht, verlagert sie sich auf eine abstrakte Methode, für deren Implementierung die Unterklasse verantwortlich ist. Denken Sie an die Art und Weise, wie sich ASP.NET-Seiten auf Ihren Code verlagern, um beispielsweise Page Load zu implementieren.


4

Klingt so, als würde Ihre "eine generische Interpolationsmethode" in jeder Klasse zu viel bewirken und sollte in kleinere Methoden umgewandelt werden.

Je nachdem, wie komplex Ihre Berechnung ist, kann nicht jedes "Stück" der Berechnung eine solche virtuelle Methode sein

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

Und überschreiben Sie die einzelnen Teile, wenn Sie diese "leichte Abweichung" in der Berechnungslogik vornehmen müssen.

(Dies ist ein sehr kleines und nutzloses Beispiel dafür, wie Sie viele Funktionen hinzufügen und gleichzeitig kleine Methoden überschreiben können.)


3

Meiner Meinung nach haben Sie in gewissem Sinne Recht, dass DRY zu weit gehen kann. Wenn sich zwei ähnliche Codeteile wahrscheinlich in sehr unterschiedliche Richtungen entwickeln, können Sie Probleme verursachen, indem Sie versuchen, sich zunächst nicht zu wiederholen.

Sie haben jedoch auch Recht, sich vor solchen blasphemischen Gedanken in Acht zu nehmen. Überlegen Sie sich sehr genau, bevor Sie sich dazu entschließen, es in Ruhe zu lassen.

Wäre es zum Beispiel besser, diesen wiederholten Code in eine Utility-Klasse / -Methode zu schreiben, als in die Basisklasse? Siehe Komposition gegenüber Vererbung bevorzugen .


2

DRY ist eine Richtlinie, die befolgt werden muss und keine unzerbrechliche Regel. Irgendwann müssen Sie entscheiden, dass es sich nicht lohnt, X-Vererbungsstufen und Y-Vorlagen in jeder Klasse zu haben, die Sie verwenden, nur um zu sagen, dass dieser Code keine Wiederholung enthält. Ein paar gute Fragen wären, ob ich länger brauche, um diese ähnlichen Methoden zu extrahieren und als eine zu implementieren. Dann müsste ich sie alle durchsuchen, falls sich die Notwendigkeit einer Änderung ergibt oder ihr Potenzial besteht, dass eine Änderung eintreten könnte Meine Arbeit rückgängig machen, indem ich diese Methoden extrahiere? Bin ich an dem Punkt angelangt, an dem zusätzliche Abstraktion zu verstehen beginnt, wo oder was dieser Code tut, ist eine Herausforderung?

Wenn Sie eine dieser Fragen mit "Ja" beantworten können, haben Sie ein gutes Argument dafür, potenziell duplizierten Code zu hinterlassen


0

Sie müssen sich die Frage stellen: "Warum sollte ich es umgestalten?" In Ihrem Fall, wenn Sie "ähnlichen, aber unterschiedlichen" Code haben, wenn Sie einen Algorithmus ändern, müssen Sie sicherstellen, dass Sie diese Änderung auch an den anderen Stellen widerspiegeln. Dies ist normalerweise ein Rezept für eine Katastrophe. Ausnahmslos jemand anderes wird eine Stelle verpassen und einen anderen Fehler einbringen.

In diesem Fall wird die Umgestaltung der Algorithmen zu einem einzigen Riesen-Algorithmus zu kompliziert, was die zukünftige Wartung zu schwierig macht. Also, wenn Sie das Gemeinsame nicht sinnvoll herausrechnen können, ein einfaches:

// this code is similar to class x function b

Kommentar wird ausreichen. Problem gelöst.


0

Bei der Entscheidung, ob es besser ist, eine größere Methode oder zwei kleinere Methoden mit überlappender Funktionalität zu verwenden, lautet die erste 50.000-Dollar-Frage, ob sich der überlappende Teil des Verhaltens möglicherweise ändert und ob Änderungen gleichermaßen auf die kleineren Methoden angewendet werden sollten. Wenn die Antwort auf die erste Frage Ja lautet, die Antwort auf die zweite Frage jedoch Nein lautet, sollten die Methoden getrennt bleiben . Lautet die Antwort auf beide Fragen "Ja", muss sichergestellt werden, dass jede Version des Codes synchron bleibt. In vielen Fällen ist es am einfachsten, nur eine Version zu haben.

Es gibt einige Stellen, an denen Microsoft gegen die DRY-Prinzipien verstoßen zu haben scheint. Beispielsweise hat Microsoft ausdrücklich davon abgeraten, dass Methoden einen Parameter akzeptieren, der angibt, ob ein Fehler eine Ausnahme auslösen sollte. Während es stimmt, dass ein Parameter "failures throw exceptions" in der API "general usage" einer Methode hässlich ist, können solche Parameter in Fällen, in denen eine Try / Do-Methode aus anderen Try / Do-Methoden bestehen muss, sehr hilfreich sein. Wenn eine äußere Methode beim Auftreten eines Fehlers eine Ausnahme auslösen soll, sollte jeder innere Methodenaufruf, der fehlschlägt, eine Ausnahme auslösen, die von der äußeren Methode weitergegeben werden kann. Wenn die äußere Methode keine Ausnahme auslösen soll, ist dies auch nicht die innere Methode. Wenn ein Parameter verwendet wird, um zwischen try / do zu unterscheiden, dann kann die äußere Methode sie an die innere Methode übergeben. Andernfalls muss die äußere Methode "try" -Methoden aufrufen, wenn sie sich als "try" verhalten soll, und "do" -Methoden, wenn sie sich als "do" verhalten soll.

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.