Freitag, 25. Februar 2011

Best practice: Fail-Fast, Exceptions und Assertions

Heute habe ich einiges an Fail-Fast-Code geschrieben, also eine günstige Gelegenheit mal darüber zu sprechen.

Eine einfache und preiswerte Möglichkeit die Qualität zu steigern und das Debuggen zu erleichtern ist der Fail-Fast-Ansatz.

Aber was bedeutet das? Es geht darum, einen Fehler auszulösen bevor er wirklich entsteht. Wir alle kennen die schöne NullPointerException. 

Meistens ärgere ich mich, wenn sie geworfen wird, denn leider hat man es oft mit Code zu tun, der nicht Fail-Fast implementiert wurde. Besonders unerfreulich ist das in fehlerhaften Code der schon beim Kunden ist. Ein einfaches Beispiel für Fail-Late:

class Example {
private int width = 5;
private int height = 8;
private int state = 0;

public void doCalculation() {
state = width * height + getAnotherIntegerValue();
delegate.getInsets().addOffsetX( state );
state += 2;
}
}

Und Peng! Eine NullPointerException in der 2. Zeile von doCalculation(). Aber was war jetzt null? "delegate" oder lieferte "Delegate.getInsets()" null? Das ist ein typisches Problem bei diesem Vorgehen, man bekommt nur wenige Infos darüber, was schiefgegangen ist.

Fail-Fast bedeutet, schneller zu sein und nicht zu warten bis vielleicht eine NPE fliegt, sondern an der frühst möglichen Stelle die Exception zu werfen:

public void doCalculation() {
if (delegate == null) {
throw new NullPointerException("delegate is null");
}
if (delegate.getInsets() == null) {
throw new IllegalStateException("delegate.getInsets() returned null");
}
state = width * height + getAnotherIntegerValue();
delegate.getInsets().addOffsetX( state );
state += 2;
}

Dieser Code hat verschiedene Vorteile. Man weiss sofort, was genau schief gegangen ist. Darüber hinaus können wir auch sicher sein, dass "state" nicht in einem seltsamen Zustand ist.

Klar, so hat man mehr if-Abfragen (wir möchten jetzt nicht über die Kosten dafür diskutieren). Es gäbe es auch noch folgenden Kompromiss (der auch lesbarer ist):

public void doCalculation() {
assert delegate != null : "delegate is null";
assert delegate.getInsets() != null : "delegate.getInsets() returned null";
state = width * height + getAnotherIntegerValue();
delegate.getInsets().addOffsetX( state );
state += 2;
}

Je nach Kompiler-Einstellung werden die assert-Befehle restlos entfernt. Oder halt nicht, was uns bei fehlerhaften Produktions-Code dann allerdings auch nicht weiterhilft. Aber das Test-Team hat ja gründlich getestet :-) Achtung: Assertations sind per Default abgeschaltet!

Das war natürlich ein sehr einfaches Beispiel. Wer sich mal anschauen möchte wie so etwas im "echten" Leben auch aussehen kann sollte sich mal die Klassen (z.B. ArrayList und den Iterator) im Collections-Package von JDK anschauen. Die erkennen Multitasking-Probleme mit Hilfe eines Modifikations-Zählers und werfen lieber früher eine ConcurrentModificationException statt irgendwann später merkwürdige Ergebnisse zu liefern.

Keine Kommentare:

Kommentar veröffentlichen