Verwalten von sich stark wiederholendem Code und Dokumentation in Java


71

Sehr sich wiederholender Code ist im Allgemeinen eine schlechte Sache, und es gibt Entwurfsmuster, die helfen können, dies zu minimieren. Manchmal ist es jedoch aufgrund der Einschränkungen der Sprache selbst einfach unvermeidlich. Nehmen Sie das folgende Beispiel aus java.util.Arrays:

/**
 * Assigns the specified long value to each element of the specified
 * range of the specified array of longs.  The range to be filled
 * extends from index <tt>fromIndex</tt>, inclusive, to index
 * <tt>toIndex</tt>, exclusive.  (If <tt>fromIndex==toIndex</tt>, the
 * range to be filled is empty.)
 *
 * @param a the array to be filled
 * @param fromIndex the index of the first element (inclusive) to be
 *        filled with the specified value
 * @param toIndex the index of the last element (exclusive) to be
 *        filled with the specified value
 * @param val the value to be stored in all elements of the array
 * @throws IllegalArgumentException if <tt>fromIndex &gt; toIndex</tt>
 * @throws ArrayIndexOutOfBoundsException if <tt>fromIndex &lt; 0</tt> or
 *         <tt>toIndex &gt; a.length</tt>
 */
public static void fill(long[] a, int fromIndex, int toIndex, long val) {
    rangeCheck(a.length, fromIndex, toIndex);
    for (int i=fromIndex; i<toIndex; i++)
        a[i] = val;
}

Die obige Schnipsel erscheint im Quellcode 8 mal, mit sehr wenig Änderung in der Dokumentation / Methodensignatur aber genau die gleiche Methode Körper , eine für jede der Wurzel - Array - Typen int[], short[], char[], byte[], boolean[], double[], float[], und Object[].

Ich glaube, dass diese Wiederholung unvermeidlich ist, wenn man nicht auf Reflexion zurückgreift (was an sich ein völlig anderes Thema ist). Ich verstehe, dass als Dienstprogrammklasse eine derart hohe Konzentration an sich wiederholendem Java-Code höchst untypisch ist, aber selbst mit der besten Vorgehensweise kommt es zu Wiederholungen ! Refactoring funktioniert nicht immer, weil es nicht immer möglich ist (der offensichtliche Fall ist, wenn die Wiederholung in der Dokumentation enthalten ist).

Offensichtlich ist die Pflege dieses Quellcodes ein Albtraum. Ein kleiner Tippfehler in der Dokumentation oder ein kleiner Fehler in der Implementierung wird mit der Anzahl der Wiederholungen multipliziert. Tatsächlich handelt es sich bei dem besten Beispiel um genau diese Klasse:

Google Research Blog - Extra, Extra - Lesen Sie alles darüber: Fast alle binären Suchvorgänge und Zusammenführungen sind fehlerhaft (von Joshua Bloch, Software Engineer).

Der Fehler ist überraschend subtil und tritt in dem auf, was viele für einen einfachen und unkomplizierten Algorithmus hielten.

    // int mid =(low + high) / 2; // the bug
    int mid = (low + high) >>> 1; // the fix

Die obige Zeile erscheint 11 mal im Quellcode !

Meine Fragen sind also:

  • Wie wird mit solchen sich wiederholenden Java-Codes / Dokumentationen in der Praxis umgegangen? Wie werden sie entwickelt, gewartet und getestet?
    • Beginnen Sie mit "dem Original" und machen Sie es so ausgereift wie möglich. Kopieren Sie es dann und fügen Sie es nach Bedarf ein und hoffen Sie, dass Sie keinen Fehler gemacht haben?
    • Und wenn Sie im Original einen Fehler gemacht haben, beheben Sie ihn einfach überall, es sei denn, Sie möchten die Kopien löschen und den gesamten Replikationsprozess wiederholen?
    • Und Sie wenden diesen Prozess auch für den Testcode an?
  • Würde Java von einer Art Quellcode-Vorverarbeitung mit eingeschränkter Verwendung für diese Art von Dingen profitieren?
    • Vielleicht hat Sun einen eigenen Präprozessor, der beim Schreiben, Verwalten, Dokumentieren und Testen dieser Art von sich wiederholendem Bibliothekscode hilft?

In einem Kommentar wurde ein anderes Beispiel angefordert, daher habe ich dieses aus Google Collections gezogen: com.google.common.base.Predicates Zeilen 276-310 ( AndPredicate) vs Zeilen 312-346 ( OrPredicate).

Die Quelle für diese beiden Klassen ist identisch, mit Ausnahme von:

  • AndPredicatevs OrPredicate(jeder erscheint 5 mal in seiner Klasse)
  • "And("vs Or("(in den jeweiligen toString()Methoden)
  • #andvs #or(in den @seeJavadoc-Kommentaren)
  • truevs false(in apply; !kann aus dem Ausdruck heraus umgeschrieben werden)
  • -1 /* all bits on */vs 0 /* all bits off */inhashCode()
  • &=vs |=inhashCode()

11
Es hört sich so an, als wären Sie aufgrund von Code für die Behandlung primitiver Arrays besonders besorgt über Wiederholungen. Persönlich vermeide ich diese Art der Wiederholung (und ermutige andere, dasselbe zu tun), indem ich generische Sammlungen und Autoboxen verwende und sowohl Arrays als auch Primitive vermeide, sofern dies nicht unbedingt erforderlich ist. Haben Sie Beispiele für diese Wiederholung, die keine primitiven Arrays beinhalten?
Joe Carnahan

2
+1 für den guten Beitrag und netlib.bell-labs.com/cm/cs/pearls
Stapler

Die Wiederholbarkeit aufgrund vollständiger Überlastungen ist nur ein Beispiel. Ich habe diese Art der Wiederholung auch in Szenarien mit nicht überladener und nicht primitiver Array-Behandlung gesehen.
Polygenelubricants

2
Ich habe mir diese Klasse java.util.Arrays angesehen und fand das folgende Javadoc-Snippet ziemlich amüsant: "Der Code für jeden der sieben primitiven Typen ist weitgehend identisch. C'est la vie."
David Bulté

Die Sun / Oracle Java-Bibliothek verwendet ein manky Templating-System für NIO-Puffer.
Tom Hawtin - Tackline

Antworten:


32

Für Leute, die unbedingt Leistung brauchen, sind Boxen und Unboxen und generierte Sammlungen und so weiter große No-No's.

Das gleiche Problem tritt beim Performance-Computing auf, bei dem Sie denselben Komplex benötigen, um sowohl für Float als auch für Double zu arbeiten (sagen Sie einige der Methoden, die in Goldberds Artikel " Was jeder Informatiker über Gleitkommazahlen wissen sollte " gezeigt werden ).

Es gibt einen Grund , warum Trove ‚s TIntIntHashMapläuft Kreise um Java ist , HashMap<Integer,Integer>wenn sie mit einer ähnlichen Menge an Daten arbeiten.

Wie wird nun der Quellcode der Trove-Sammlung geschrieben?

Natürlich mit Quellcode-Instrumentierung :)

Es gibt mehrere Java-Bibliotheken für eine höhere Leistung (viel höher als die Standard-Java-Bibliotheken), die Codegeneratoren verwenden, um den wiederholten Quellcode zu erstellen.

Wir alle wissen, dass "Quellcode-Instrumentierung" böse ist und dass Codegenerierung Mist ist, aber so machen es immer noch Leute, die wirklich wissen, was sie tun (dh die Art von Leuten, die Sachen wie Trove schreiben) :)

Für das, was es wert ist, generieren wir Quellcode, der große Warnungen enthält wie:

/*
 * This .java source file has been auto-generated from the template xxxxx
 * 
 * DO NOT MODIFY THIS FILE FOR IT SHALL GET OVERWRITTEN
 * 
 */

Können Sie weitere Details zu den von ihnen verwendeten Codegeneratoren usw. angeben? Ich kenne Trove nicht.
Polygenelubricants

1
Es wird in den Trove-FAQ erklärt, im Grunde haben sie ein Ant-Ziel, das ein Skript aufruft, das die Änderung vornimmt ( wenn ich mich richtig erinnere): trove4j.sourceforge.net/html/faq.html (Ich bin in Java High Performance Computing und ich Ich habe die Technik mehrmals gesehen ... Wir verwenden sie hier, wir haben unseren eigenen Java-proprietären Code, der mehr Java-Code generiert :)
SyntaxT3rr0r

1
@polygenelubricants: Übrigens ist Trove ein wunderbarer Ersatz für die Standard-Java-API, wenn Sie mit Grundelementen arbeiten müssen. Bei regulären Sammlungen sollten Sie sich Javolution oder die Google-Sammlungen usw. ansehen. Die Standard-Java-Sammlungen sind unter vielen Gesichtspunkten wirklich ziemlich schlecht. Es funktioniert für einfache Projekte, aber sie zeigen ihre Grenzen ziemlich schnell, sobald Sie anfangen, wichtige Datenmengen zu bearbeiten.
SyntaxT3rr0r

Ich mag die Codegenerierung ... es muss überhaupt nicht böse sein. Aber ich würde eher Bytecode als Java-Quelle generieren. Was ist, wenn Sie zur Laufzeit generieren müssen, werden Sie Endbenutzer zwingen, das JDK zu installieren?
CurtainDog

1
@CurtainDog: Es gibt einen Grund, warum Projekte wie Trove Quellcode und keinen Bytecode generieren. Es gibt Fälle, in denen die Bytecode-Instrumentierung in Ordnung ist, und Fälle, in denen die Quellcode-Instrumentierung besser ist. Für das, was es in dem aktuellen Projekt, an dem ich arbeite, wert ist, tun wir beides ... Eine weitere Option, wenn Sie zur Laufzeit wirklich Quellcode-Instrumentierung (anstelle von Bytecode) wünschen, besteht darin, einfach die .java-Serverseite zu generieren, sie zu kompilieren und zu senden es den Draht hinunter. Ich sage nicht, dass Sie dies in diesem späteren Fall tun sollten: Ich sage, dass nicht nur beide ihre Verwendung haben, sondern beide häufig verwendet werden.
SyntaxT3rr0r

16

Wenn Sie unbedingt Code duplizieren müssen, folgen Sie den Beispielen, die Sie gegeben haben, und gruppieren Sie den gesamten Code an einem Ort, an dem Sie ihn leicht finden und beheben können, wenn Sie Änderungen vornehmen müssen. Dokumentieren Sie die Vervielfältigung und vor allem den Grund für die Vervielfältigung, damit jeder, der nach Ihnen kommt, beides kennt.


1
+1 Das Erhöhen der Länge der duplizierten Dokumentation durch Dokumentieren der Duplizierung scheint zunächst eine schlechte Idee zu sein, aber es ist wirklich viel schlimmer, duplizierte Inhalte zu haben, die geändert werden müssen, und keine Dokumentation über die Duplizierung.
Tanzelax

6

Aus Wikipedia Wiederholen Sie sich nicht (DRY) oder Vervielfältigung ist böse (DIE)

In einigen Kontexten kann der Aufwand zur Durchsetzung der DRY-Philosophie größer sein als der Aufwand zur Aufbewahrung separater Kopien der Daten. In einigen anderen Kontexten sind doppelte Informationen unveränderlich oder werden so streng kontrolliert, dass DRY nicht erforderlich ist.

Es gibt wahrscheinlich keine Antwort oder Technik, um solche Probleme zu verhindern.


4

Sogar ausgefallene Hosen-Sprachen wie Haskell haben sich wiederholenden Code ( siehe meinen Beitrag über Haskell und Serialisierung ).

Es scheint drei Möglichkeiten für dieses Problem zu geben:

  1. Verwenden Sie Reflexion und verlieren Sie Leistung
  2. Verwenden Sie Vorverarbeitung wie Template Haskell oder Caml4p-Äquivalent für Ihre Sprache und leben Sie mit Gemeinheit
  3. Oder mein persönlicher Favorit verwendet Makros, wenn Ihre Sprache dies unterstützt (Schema und Lisp)

Ich betrachte die Makros als anders als die Vorverarbeitung, da die Makros normalerweise in derselben Sprache sind, in der sich das Ziel befindet, da die Vorverarbeitung eine andere Sprache ist.

Ich denke, Lisp / Scheme-Makros würden viele dieser Probleme lösen.


2

Ich verstehe, dass Sun für den Java SE-Bibliothekscode so dokumentieren muss, und vielleicht tun es auch andere Bibliotheksautoren von Drittanbietern.

Ich halte es jedoch für eine völlige Verschwendung, Dokumentation in eine Datei wie diese in Code zu kopieren und einzufügen, der nur im eigenen Haus verwendet wird. Ich weiß, dass viele Leute anderer Meinung sind, weil dadurch ihre internen JavaDocs weniger sauber aussehen. Der Nachteil ist jedoch, dass der Code sauberer wird, was meiner Meinung nach wichtiger ist.


+1 Sie können die Dokumentation für jede duplizierte Methode einmal in der Klasse Javadoc schreiben und eine kurze Methode Javadoc verwenden, die "siehe Klasse Javadoc" sagt
Henno Vermeulen

2

Java-Primitivtypen schrauben Sie, besonders wenn es um Arrays geht. Wenn Sie speziell nach Code mit primitiven Typen fragen, würde ich sagen, versuchen Sie einfach, diese zu vermeiden. Die Object [] -Methode ist ausreichend, wenn Sie die Box-Typen verwenden.

Im Allgemeinen benötigen Sie viele Komponententests, und es gibt wirklich nichts anderes zu tun, als auf Reflexion zurückzugreifen. Wie Sie sagten, es ist ein ganz anderes Thema, aber haben Sie keine Angst vor Reflexion. Schreiben Sie zuerst den DRYest-Code, den Sie können, und profilieren Sie ihn dann. Stellen Sie fest, ob der Treffer bei der Reflexionsleistung wirklich schlecht genug ist, um das Ausschreiben und Verwalten des zusätzlichen Codes zu rechtfertigen.


2

Sie können einen Codegenerator verwenden, um Variationen des Codes mithilfe einer Vorlage zu erstellen. In diesem Fall ist die Java-Quelle ein Produkt des Generators und der eigentliche Code ist die Vorlage.


Ja, darauf habe ich angespielt, als ich sagte, dass Sun vielleicht einen eigenen Präprozessor usw. hat
Polygenelubricants

1
Der offiziell genehmigte Weg, dies zu tun, wäre die Verwendung einer Annotation und eines Annotationsprozessors, so dass javac beim Kompilieren des Codes Ihren Annotationsprozessor aufruft, der wiederum im laufenden Betrieb Quellcode generiert, der vom Compiler kompiliert wird. Die inoffizielle Möglichkeit besteht darin, dass Ihr Anmerkungsprozessor die internen Compiler-Datenstrukturen ändert, wenn er aufgerufen wird. Die einzige kostenlose Java-Quellgenerierungsbibliothek, die ich gefunden habe, ist CodeModel.
Msalib

Dies scheint für größere Snippets vernünftig zu sein, aber eine kleine Duplizierung kann das geringere von zwei Übeln sein, die dazu führen, dass dem Erstellungsprozess noch eine weitere Komplexitätsebene hinzugefügt wird.
Dsimcha

2

Angesichts von zwei Codefragmenten, von denen behauptet wird, dass sie ähnlich sind, verfügen die meisten Sprachen nur über begrenzte Möglichkeiten, Abstraktionen zu erstellen, die die Codefragmente zu einem Monolithen vereinen. Um zu abstrahieren, wenn Ihre Sprache dies nicht kann, müssen Sie die Sprache verlassen: - {

Der allgemeinste "Abstraktions" -Mechanismus ist ein vollständiger Makroprozessor, der beim Instanziieren beliebige Berechnungen auf den "Makrokörper" anwenden kann (denken Sie an ein Post- oder String-Rewriting- System, das Turing-fähig ist). M4 und GPM sind typische Beispiele. Der C-Präprozessor gehört nicht dazu.

Wenn Sie einen solchen Makroprozessor haben, können Sie eine "Abstraktion" als Makro erstellen und den Makroprozessor auf Ihrem "abstrahierten" Quelltext ausführen, um den tatsächlichen Quellcode zu erzeugen, den Sie kompilieren und ausführen.

Sie können auch eingeschränktere Versionen der Ideen verwenden, die häufig als "Codegeneratoren" bezeichnet werden. Diese sind normalerweise nicht Turing-fähig, aber in vielen Fällen funktionieren sie gut genug. Dies hängt davon ab, wie komplex Ihre "Makro-Instanziierung" sein muss. (Der Grund , warum Menschen mit dem Template - Mechanismus ++ C verliebten sind , ist ths trotz seiner Hässlichkeit, es ist fähig Turing und so können die Menschen tun wirklich hässlich , aber erstaunlich Code - Generierung Aufgaben mit ihm). Eine andere Antwort hier erwähnt Trove, der anscheinend in der begrenzten, aber immer noch sehr nützlichen Kategorie liegt.

Wirklich allgemeine Makroprozessoren (wie M4) bearbeiten nur Text. Das macht sie leistungsfähig, aber sie beherrschen die Struktur der Programmiersprache nicht gut, und es ist wirklich umständlich, einen Generator in einen solchen Mcaro-Prozessor zu schreiben, der nicht nur Code erzeugen, sondern auch das generierte Ergebnis optimieren kann. Die meisten Codegeneratoren, auf die ich stoße, sind "diese Zeichenfolge in diese Zeichenfolgenvorlage einfügen" und können daher kein generiertes Ergebnis optimieren. Wenn Sie willkürlichen Code generieren und eine hohe Leistung erzielen möchten, benötigen Sie etwas, das Turing-fähig ist, aber die Struktur des generierten Codes versteht, damit er leicht manipuliert (z. B. optimiert) werden kann.

Ein solches Tool wird als Programmtransformationssystem bezeichnet . Ein solches Tool analysiert den Quelltext genau wie ein Compiler und führt dann Analysen / Transformationen durch, um einen gewünschten Effekt zu erzielen. Wenn Sie Markierungen in den Quelltext Ihres Programms einfügen können (z. B. strukturierte Kommentare oder Anmerkungen in Sprachen, in denen sie enthalten sind), die das Programmtransformationstool anweisen, was zu tun ist, können Sie es verwenden, um eine solche Abstraktionsinstanziierung, Codegenerierung und / oder Codeoptimierung. (Der Vorschlag eines Posters, sich in den Java-Compiler einzubinden, ist eine Variation dieser Idee.) Wenn Sie ein allgemeines Puprose-Transformationssystem verwenden (z. B. DMS Software Reengineering Tookit) , können Sie dies für praktisch jede Sprache tun.


1

Viele dieser Wiederholungen können jetzt dank Generika vermieden werden. Sie sind ein Glücksfall, wenn Sie denselben Code schreiben, bei dem sich nur die Typen ändern.

Leider denke ich, dass generische Arrays immer noch nicht sehr gut unterstützt werden. Verwenden Sie zumindest vorerst Container, mit denen Sie Generika nutzen können. Polymorphismus ist auch ein nützliches Werkzeug, um diese Art der Codeduplizierung zu reduzieren.

Um Ihre Frage zum Umgang mit Code zu beantworten, der unbedingt dupliziert werden muss ... Kennzeichnen Sie jede Instanz mit leicht durchsuchbaren Kommentaren. Es gibt einige Java-Präprozessoren, die Makros im C-Stil hinzufügen. Ich glaube, ich erinnere mich, dass Netbeans eine hatten.


3
"Leider denke ich, dass generische Arrays immer noch nicht sehr gut unterstützt werden." - Ich bin nicht sicher, wie Sie generische Arrays in Java mit Typlöschung unterstützen können. Ich denke es ist unmöglich.
Polygenelubricants

1
Ich habe eine Problemumgehung gesehen. Wirken eines Objektarrays oder Verwenden von Reflexion. Keiner ist hübsch, aber sie funktionieren anscheinend.
Patros

Im Allgemeinen ist es am besten, die Verwendung von Arrays in Java zu vermeiden. ArrayLists bieten viel mehr Funktionen und haben im Vergleich zu einem Array normalerweise vernachlässigbare Leistungskosten.
Jared Levy
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.