Einführung in die Code-Qualitätsregeln mit FindBugs und PMD

1. Überblick

In diesem Artikel werden einige wichtige Regeln hervorgehoben, die in Codeanalyse-Tools wie FindBugs, PMD und CheckStyle enthalten sind.

2. Zyklomatische Komplexität

2.1. Was ist cyclomatische Komplexität?

Die Codekomplexität ist wichtig, aber schwierig zu messen. PMD bietet unter Code-Größenregeln ein solides Regelwerk an. Diese Regeln dienen dazu, Verstöße gegen die Methodengröße und Strukturkomplexität zu erkennen .

CheckStyle ist bekannt für seine Fähigkeit, Code anhand von Codierungsstandards und Formatierungsregeln zu analysieren. Sie können jedoch auch Probleme beim Entwurf von Klassen/Methoden erkennen, indem Sie die Komplexität berechnen ( http://checkstyle.sourceforge.net/config__metrics.html …​).

Eine der relevantesten Komplexitätsmessungen in beiden Tools ist die CC (Cyclomatic Complexity).

Der CC-Wert kann durch Messen der Anzahl unabhängiger Ausführungspfade eines Programms berechnet werden.

Beispielsweise ergibt die folgende Methode eine zyklomatische Komplexität von 3:

public void callInsurance(Vehicle vehicle) {
    if (vehicle.isValid()) {
        if (vehicle instanceof Car) {
            callCarInsurance();
        } else {
            delegateInsurance();
        }
    }
}

CC berücksichtigt die Verschachtelung von bedingten Anweisungen und mehrteiligen booleschen Ausdrücken.

Im Allgemeinen wird ein Code mit einem Wert von mehr als 11 in Bezug auf CC als sehr komplex angesehen und ist schwierig zu testen und zu warten.

Nachfolgend sind einige gängige Werte aufgeführt, die von statischen Analysewerkzeugen verwendet werden:

  • 1-4: geringe Komplexität - einfach zu testen

  • 5-7: mäßige Komplexität - tolerierbar

  • 8-10: hohe Komplexität - das Refactoring sollte leichter fallen

testen ** 11 sehr hohe Komplexität - sehr schwer zu testen

Das Komplexitätsniveau wirkt sich auch auf die Testbarkeit des Codes aus. Je höher der CC, desto schwieriger die Implementierung relevanter Tests.

Tatsächlich zeigt der zyklomatische Komplexitätswert genau die Anzahl der Testfälle, die erforderlich sind, um einen 100% igen Branchendeckungsgrad zu erreichen.

Das mit der callInsurance () -Methode verknüpfte Ablaufdiagramm ist:

Die möglichen Ausführungspfade sind:

  • 0 ⇒ 3

  • 0 ⇒ 1 ⇒ 3

  • 0 ⇒ 2 ⇒ 3

Mathematisch kann CC mit der folgenden einfachen Formel berechnet werden:

CC = E - N + 2P
  • E: Gesamtanzahl der Kanten

  • N: Gesamtanzahl der Knoten

  • P: Gesamtanzahl der Ausstiegspunkte

2.2. So reduzieren Sie die cyclomatische Komplexität?

Um wesentlich weniger komplexen Code zu schreiben, neigen Entwickler je nach Situation dazu, unterschiedliche Ansätze zu verwenden:

  • Vermeiden Sie, lange switch -Anweisungen zu schreiben, indem Sie Entwurfsmuster verwenden.

z.B. Die Builder- und Strategiemuster sind möglicherweise gute Kandidaten mit Code Größe und Komplexitätsproblemen ** Schreiben Sie wiederverwendbare und erweiterbare Methoden, indem Sie den Code modularisieren

Aufbau und Umsetzung der https://en.wikipedia.org/wiki/Single Responsibility Principle Verantwortungsgrundsatz ] ** Nach anderen PMD

code size ]Regeln können direkte Auswirkungen auf CC ** haben, z. Regel für übermäßige Methodenlänge, zu viele Felder in einer einzelnen Klasse, Liste mit übermäßigen Parametern in einer einzigen Methode …​ usw

Sie können auch die folgenden Prinzipien und Muster in Bezug auf die Codegröße und -komplexität berücksichtigen, z. das https://en.wikipedia.org/wiki/KISS principle[ KISS Prinzip ( ) und https://en.wikipedia.org/wiki/Don’t repeat__yourself[DRY (Don ' t Wiederholen Sie sich selbst]].

3. Ausnahmebehandlungsregeln

Fehler im Zusammenhang mit Ausnahmen sind möglicherweise üblich, aber einige werden stark unterschätzt und sollten korrigiert werden, um kritische Funktionsstörungen im Produktionscode zu vermeiden.

PMD und FindBugs bieten beide Regeln für Ausnahmen.

Hier können Sie auswählen, was in einem Java-Programm als kritisch gelten kann, wenn Ausnahmen behandelt werden

3.1. Keine Ausnahme in Schluß werfen

Wie Sie vielleicht bereits wissen, wird der finally \ {} -Block in Java im Allgemeinen zum Schließen von Dateien und zum Freigeben von Ressourcen verwendet. Die Verwendung für andere Zwecke kann als betrachtet werden. Codegeruch .

Eine typische fehleranfällige Routine löst eine Ausnahme innerhalb des finally \ {} -Blocks aus:

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    throw new IOException();
}

Diese Methode soll eine NullPointerException auslösen, überraschenderweise jedoch eine IOException , die die aufrufende Methode dazu verleiten kann, die falsche Ausnahme zu behandeln.

3.2. Rückgabe im finally -Block

Die Verwendung der return -Anweisung innerhalb eines finally \ {} -Blocks kann nur verwirrend sein. Der Grund, warum diese Regel so wichtig ist, liegt daran, dass ein Code, der eine Ausnahme auslöst, von der Anweisung return verworfen wird.

Der folgende Code wird beispielsweise ohne Fehler ausgeführt:

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    return;
}

Eine NullPointerException wurde noch nicht abgefangen und von der return -Anweisung im finally -Block noch verworfen.

3.3. Stream kann ausnahmsweise nicht geschlossen werden

Das Schließen von Streams ist einer der Hauptgründe, warum wir einen finally -Block verwenden, aber es ist keine triviale Aufgabe, wie es scheint.

Der folgende Code versucht, zwei Streams in einem finally -Block zu schließen:

OutputStream outStream = null;
OutputStream outStream2 = null;
try {
    outStream = new FileOutputStream("test1.txt");
    outStream2  = new FileOutputStream("test2.txt");
    outStream.write(bytes);
    outStream2.write(bytes);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        outStream.close();
        outStream2.close();
    } catch (IOException e) {
       //Handling IOException
    }
}

Wenn die Anweisung outStream.close () eine IOException auslöst, wird outStream2.close () übersprungen.

Eine schnelle Lösung wäre die Verwendung eines separaten try/catch-Blocks zum Schließen des zweiten Streams:

finally {
    try {
        outStream.close();
    } catch (IOException e) {
       //Handling IOException
    }
    try {
        outStream2.close();
    } catch (IOException e) {
       //Handling IOException
    }
}

Wenn Sie eine schöne Methode zum Vermeiden aufeinander folgender try/catch -Blöcke suchen, überprüfen Sie die folgenden Anweisungen: https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/IOUtils.html IOUtils.closeQuiety]-Methode von Apache Commons, macht es einfach, Streams zu schließen, ohne eine IOException auszulösen.

5. Schlechte Praktiken

5.1. Klasse definiert compareTo () und verwendet Object.equals ()

Wenn Sie die Methode compareTo () implementieren, vergessen Sie nicht, dies auch mit der Methode equals () zu tun. Andernfalls können die von diesem Code zurückgegebenen Ergebnisse verwirrend sein:

Car car = new Car();
Car car2 = new Car();
if(car.equals(car2)) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}
if(car.compareTo(car2) == 0) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}

Ergebnis:

They're not equal
They're equal

Um Verwirrungen zu beseitigen, wird empfohlen, sicherzustellen, dass Object.equals () niemals aufgerufen wird, wenn __Comparable implementiert wird.

boolean equals(Object o) {
    return compareTo(o) == 0;
}

5.2. Mögliche Nullzeigerdereferenz

NullPointerException (NPE) gilt als die am häufigsten angetroffene Exception in der Java-Programmierung, und FindBugs beschwert sich über die Null-PointeD-Dereferenzierung, um das Auslösen zu vermeiden.

Hier ist das einfachste Beispiel für das Werfen einer NPE:

Car car = null;
car.doSomething();

NPEs vermeiden Sie am einfachsten, indem Sie eine Nullprüfung durchführen:

Car car = null;
if (car != null) {
    car.doSomething();
}

Bei NULL-Prüfungen werden NPEs möglicherweise vermieden, doch wenn sie umfangreich verwendet werden, beeinträchtigen sie sicherlich die Lesbarkeit des Codes.

Hier sind einige Techniken, mit denen NPEs ohne Nullprüfungen vermieden werden:

  • Vermeiden Sie das Schlüsselwort null beim Codieren von _: _ Diese Regel ist einfach:

Vermeiden Sie die Verwendung des Schlüsselworts null , wenn Sie Variablen initialisieren oder zurückgeben Werte Benutzen

@NotNull und @Nullable Anmerkungen Verwenden Sie java.util.Optional

Objektmuster ** ]

6. Fazit

In diesem Artikel haben wir einen Überblick über einige der von statischen Analysetools festgestellten kritischen Defekte gegeben und grundlegende Richtlinien zur angemessenen Lösung der erkannten Probleme gegeben.

Sie können die vollständigen Regeln für jede davon durchsuchen, indem Sie die folgenden Links aufrufen: