it-swarm.com.de

Ist das Auslösen einer Ausnahme hier ein Anti-Muster?

Ich hatte gerade eine Diskussion über eine Designauswahl nach einer Codeüberprüfung. Ich frage mich, was Ihre Meinungen sind.

Es gibt diese Preferences Klasse, die ein Bucket für Schlüssel-Wert-Paare ist. Nullwerte sind legal (das ist wichtig). Wir erwarten, dass bestimmte Werte möglicherweise noch nicht gespeichert werden, und wir möchten diese Fälle automatisch behandeln, indem wir sie auf Anfrage mit einem vordefinierten Standardwert initialisieren.

Die besprochene Lösung wurde nach folgendem Muster verwendet (HINWEIS: Dies ist nicht der tatsächliche Code, offensichtlich - zur Veranschaulichung vereinfacht):

public class Preferences {
    // null values are legal
    private Map<String, String> valuesFromDatabase;

    private static Map<String, String> defaultValues;

    class KeyNotFoundException extends Exception {
    }

    public String getByKey(String key) {
        try {
            return getValueByKey(key);
        } catch (KeyNotFoundException e) {
            String defaultValue = defaultValues.get(key);
            valuesFromDatabase.put(key, defaultvalue);
            return defaultValue;
        }
    }

    private String getValueByKey(String key) throws KeyNotFoundException {
        if (valuesFromDatabase.containsKey(key)) {
            return valuesFromDatabase.get(key);
        } else {
            throw new KeyNotFoundException();
        }
    }
}

Es wurde als Anti-Muster kritisiert - Missbrauch von Ausnahmen zur Kontrolle des Flusses . KeyNotFoundException - nur für diesen einen Anwendungsfall zum Leben erweckt - wird im Rahmen dieser Klasse niemals außer Acht gelassen.

Es sind im Wesentlichen zwei Methoden, mit denen Fetch gespielt wird, nur um sich gegenseitig etwas mitzuteilen.

Der Schlüssel, der nicht in der Datenbank vorhanden ist, ist weder alarmierend noch außergewöhnlich. Wir erwarten, dass dies immer dann auftritt, wenn eine neue Voreinstellung hinzugefügt wird, daher der Mechanismus, der ihn bei Bedarf ordnungsgemäß mit einem Standardwert initialisiert.

Das Gegenargument war, dass getValueByKey - die private Methode - wie sie derzeit definiert ist, keine natürliche Möglichkeit hat, die öffentliche Methode über beide den Wert zu informieren und ob der Schlüssel da war. (Wenn nicht, muss es hinzugefügt werden, damit der Wert aktualisiert werden kann).

Die Rückgabe von null wäre mehrdeutig, da null ein vollkommen legaler Wert ist. Es ist also nicht abzusehen, ob der Schlüssel nicht vorhanden war oder ob es ein null gab .

getValueByKey müsste eine Art Tuple<Boolean, String> zurückgeben, wobei der Bool auf true gesetzt wird, wenn der Schlüssel bereits vorhanden ist, damit wir zwischen (true, null) und (false, null). (Ein out -Parameter könnte in C # verwendet werden, aber das ist Java).

Ist das eine schönere Alternative? Ja, Sie müssten eine Einwegklasse für den Effekt von Tuple<Boolean, String> Definieren, aber dann werden wir KeyNotFoundException los, damit diese Art von Ausgleich entsteht. Wir vermeiden auch den Aufwand für die Behandlung einer Ausnahme, obwohl dies praktisch nicht von Bedeutung ist. Es gibt keine nennenswerten Leistungsaspekte. Es handelt sich um eine Client-App, und es ist nicht so, dass Benutzereinstellungen millionenfach pro Sekunde abgerufen werden.

Eine Variation dieses Ansatzes könnte Guavas Optional<String> (Guave wird bereits im gesamten Projekt verwendet) anstelle eines benutzerdefinierten Tuple<Boolean, String> Verwenden, und dann können wir zwischen Optional.<String>absent() und "Proper" unterscheiden "null. Es fühlt sich jedoch aus leicht zu erkennenden Gründen immer noch hackisch an - die Einführung von zwei Ebenen der "Nullheit" scheint das Konzept zu missbrauchen, das hinter der Schaffung von Optionals stand.

Eine andere Möglichkeit wäre, explizit zu überprüfen, ob der Schlüssel vorhanden ist (fügen Sie eine boolean containsKey(String key) -Methode hinzu und rufen Sie getValueByKey nur auf, wenn wir bereits bestätigt haben, dass er vorhanden ist).

Schließlich könnte man auch die private Methode inline setzen, aber das tatsächliche getByKey ist etwas komplexer als mein Codebeispiel, so dass Inlining es ziemlich böse aussehen lassen würde.

Ich mag hier Haare spalten, aber ich bin gespannt, worauf Sie wetten würden, um in diesem Fall der besten Praxis am nächsten zu kommen. Ich habe in den Styleguides von Oracle oder Google keine Antwort gefunden.

Ist die Verwendung von Ausnahmen wie im Codebeispiel ein Anti-Pattern oder ist es akzeptabel, da Alternativen auch nicht sehr sauber sind? Wenn ja, unter welchen Umständen wäre es in Ordnung? Und umgekehrt?

33
Konrad Morawski

Ja, Ihr Kollege hat Recht: Das ist schlechter Code. Wenn ein Fehler lokal behandelt werden kann, sollte er sofort behandelt werden. Eine Ausnahme sollte nicht ausgelöst und dann sofort behandelt werden.

Dies ist viel sauberer als Ihre Version (die Methode getValueByKey() wurde entfernt):

public String getByKey(String key) {
    if (valuesFromDatabase.containsKey(key)) {
        return valuesFromDatabase.get(key);
    } else {
        String defaultValue = defaultValues.get(key);
        valuesFromDatabase.put(key, defaultvalue);
        return defaultValue;
    }
}

Eine Ausnahme sollte nur ausgelöst werden, wenn Sie nicht wissen, wie der Fehler lokal behoben werden kann.

75
BЈовић

Ich würde diese Verwendung von Ausnahmen nicht als Anti-Muster bezeichnen, sondern nicht als die beste Lösung für das Problem der Kommunikation eines komplexen Ergebnisses.

Die beste Lösung (vorausgesetzt, Sie sind noch auf Java 7)) wäre die Verwendung von Guavas Optional; ich bin nicht der Meinung, dass die Verwendung in diesem Fall hackisch wäre. Es scheint mir, basierend auf - Guavas erweiterte Erklärung von Optional , dass dies ein perfektes Beispiel für die Verwendung ist. Sie unterscheiden zwischen "kein Wert gefunden" und "Wert von null gefunden".

11
Mike Partridge

Da es keine Leistungsüberlegungen gibt und es sich um ein Implementierungsdetail handelt, spielt es letztendlich keine Rolle, für welche Lösung Sie sich entscheiden. Aber ich muss zustimmen, dass es ein schlechter Stil ist. Wenn der Schlüssel fehlt, wissen Sie, dass passieren wird, und Sie behandeln ihn nicht einmal mehr als einen Aufruf des Stapels, wo Ausnahmen auftreten sind am nützlichsten.

Der Tupel-Ansatz ist etwas hackig, da das zweite Feld bedeutungslos ist, wenn der Boolesche Wert falsch ist. Es ist albern, vorher zu prüfen, ob der Schlüssel vorhanden ist, da die Karte den Schlüssel zweimal nachschlägt. (Nun, das machen Sie bereits, also in diesem Fall dreimal.) Die Optional -Lösung passt perfekt zum Problem. Es mag etwas ironisch erscheinen, ein null in einem Optional zu speichern, aber wenn der Benutzer dies tun möchte, kann er nicht nein sagen.

Wie Mike in den Kommentaren bemerkt hat, gibt es ein Problem damit; Weder Guava noch Java 8's Optional erlauben das Speichern von nulls. Sie müssten also Ihre eigenen rollen, was - obwohl unkompliziert - eine angemessene Menge von Boilerplate, könnte also für etwas, das nur einmal intern verwendet wird, übertrieben sein. Sie können auch den Kartentyp in Map<String, Optional<String>> ändern, aber die Handhabung von Optional<Optional<String>> wird umständlich.

Ein vernünftiger Kompromiss könnte darin bestehen, die Ausnahme beizubehalten, aber ihre Rolle als "alternative Rendite" anzuerkennen. Erstellen Sie eine neue aktivierte Ausnahme mit deaktiviertem Stack-Trace und werfen Sie eine vorab erstellte Instanz davon, die Sie in einer statischen Variablen behalten können. Dies ist billig - möglicherweise so billig wie eine lokale Niederlassung - und Sie können nicht vergessen, damit umzugehen, sodass das Fehlen einer Stapelverfolgung kein Problem darstellt.

8
Doval

Es gibt diese Preferences-Klasse, die ein Bucket für Schlüssel-Wert-Paare ist. Nullwerte sind legal (das ist wichtig). Wir erwarten, dass bestimmte Werte möglicherweise noch nicht gespeichert werden, und wir möchten diese Fälle automatisch behandeln, indem wir sie auf Anfrage mit einem vordefinierten Standardwert initialisieren.

Das Problem ist genau das. Aber Sie haben die Lösung bereits selbst gepostet:

Eine Variation dieses Ansatzes könnte Guavas Optional (Guava wird bereits im gesamten Projekt verwendet) anstelle eines benutzerdefinierten Tupels verwenden, , und dann können wir zwischen Optional.absent () und "unterscheiden. richtig "null . Es fühlt sich jedoch aus leicht zu erkennenden Gründen immer noch hackisch an - die Einführung von zwei Ebenen der "Nullheit" scheint das Konzept zu missbrauchen, das hinter der Schaffung von Optionals stand.

Verwenden Sie jedoch nicht null oder Optional. Sie können und sollten nur Optional verwenden. Verwenden Sie es für Ihr "hackisches" Gefühl einfach verschachtelt, sodass Sie am Ende Optional<Optional<String>> Erhalten, was deutlich macht, dass möglicherweise ein Schlüssel in der Datenbank vorhanden ist (erste Optionsebene) und dass er möglicherweise einen vordefinierten Wert enthält (zweiter Optionsschicht).

Dieser Ansatz ist besser als die Verwendung von Ausnahmen und leicht zu verstehen, solange Optional für Sie nicht neu ist.

Bitte beachten Sie auch, dass Optional einige Komfortfunktionen hat, so dass Sie nicht die ganze Arbeit selbst erledigen müssen. Diese beinhalten:

  • static static <T> Optional<T> fromNullable(T nullableReference), um Ihre Datenbankeingabe in die Typen Optional zu konvertieren
  • abstract T or(T defaultValue), um den Schlüsselwert der inneren Optionsebene abzurufen oder (falls nicht vorhanden) Ihren Standardschlüsselwert abzurufen
5
valenterry

Ich weiß, dass ich zu spät zur Party komme, aber Ihr Anwendungsfall ähnelt auf jeden Fall der Frage, wie Javas Properties auch eine Reihe von Standardeigenschaften definieren kann, die überprüft werden, wenn kein entsprechender Schlüssel vorhanden ist von der Instanz geladen.

Betrachten Sie, wie die Implementierung für Properties.getProperty(String) durchgeführt wird (von Java 7):

Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;

Es ist wirklich nicht notwendig, "Ausnahmen zu missbrauchen, um den Fluss zu kontrollieren", wie Sie zitiert haben.

Ein ähnlicher, aber etwas knapperer Ansatz für die Antwort von @ BЈовић kann auch sein:

public String getByKey(String key) {
    if (!valuesFromDatabase.containsKey(key)) {
        valuesFromDatabase.put(key, defaultValues.get(key));
    }
    return valuesFromDatabase.get(key);
}
5
h.j.k.

Obwohl ich denke, dass die Antwort von @ BЈовић in Ordnung ist, falls getValueByKey nirgendwo anders benötigt wird, denke ich nicht, dass Ihre Lösung schlecht ist, falls Ihr Programm beide Anwendungsfälle enthält:

  • abrufen per Schlüssel mit automatischer Erstellung, falls der Schlüssel vorher nicht vorhanden ist, und

  • abrufen ohne diesen Automatismus, ohne etwas in der Datenbank, im Repository oder in der Schlüsselzuordnung zu ändern (denken Sie daran, dass getValueByKey öffentlich und nicht privat ist)

Wenn dies Ihre Situation ist und solange der Leistungseinbruch akzeptabel ist, denke ich, dass Ihre vorgeschlagene Lösung völlig in Ordnung ist. Es hat den Vorteil, dass der Abrufcode nicht doppelt verwendet wird, es basiert nicht auf Frameworks von Drittanbietern und es ist ziemlich einfach (zumindest in meinen Augen).

In einer solchen Situation hängt in der Tat vom Kontext ab, ob ein fehlender Schlüssel eine "außergewöhnliche" Situation ist oder nicht. Für einen Kontext, in dem dies der Fall ist, wird so etwas wie getValueByKey benötigt. In einem Kontext, in dem eine automatische Schlüsselerstellung erwartet wird, ist die Bereitstellung einer Methode, die die bereits verfügbare Funktion wiederverwendet, die Ausnahme verschluckt und ein anderes Fehlerverhalten bereitstellt, durchaus sinnvoll. Dies kann als Erweiterung oder "Dekorator" von getValueByKey interpretiert werden, weniger als eine Funktion, bei der die "Ausnahme für den Kontrollfluss missbraucht wird".

Natürlich gibt es eine dritte Alternative: Erstellen Sie eine dritte private Methode, die Tuple<Boolean, String> Zurückgibt, wie Sie vorgeschlagen haben, und verwenden Sie diese Methode sowohl in getValueByKey als auch in getByKey wieder. Für einen komplizierteren Fall, der zwar die bessere Alternative sein könnte, aber für einen so einfachen Fall wie hier gezeigt, riecht dies meiner Meinung nach nach Überentwicklung, und ich bezweifle, dass der Code auf diese Weise wirklich wartbarer wird. Ich bin hier mit die oberste Antwort hier von Karl Bielefeldt :

"Sie sollten sich nicht schlecht fühlen, wenn Sie Ausnahmen verwenden, wenn dies Ihren Code vereinfacht.".

4
Doc Brown

Optional ist die richtige Lösung. Wenn Sie es jedoch vorziehen, gibt es eine Alternative, die sich weniger wie "zwei Nullen" anfühlt. Betrachten Sie einen Sentinel.

Definieren Sie eine private statische Zeichenfolge keyNotFoundSentinel mit dem Wert new String(""). *

Jetzt kann die private Methode return keyNotFoundSentinel Statt throw new KeyNotFoundException(). Die öffentliche Methode kann nach diesem Wert suchen

String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
    ... // generate default value
} else {
    return rval;
}

In Wirklichkeit ist null ein Sonderfall eines Sentinels. Dies ist ein von der Sprache definierter Sentinel-Wert mit einigen speziellen Verhaltensweisen (d. H. Gut definiertes Verhalten, wenn Sie eine Methode für null aufrufen). Es ist einfach so nützlich, einen solchen Sentinel zu haben, dass fast jede Sprache ihn benutzt.

* Wir verwenden absichtlich new String("") und nicht nur "", Um zu verhindern, dass Java) die Zeichenfolge "interniert", wodurch sie dieselbe Referenz wie jede andere leere Zeichenfolge erhält string. Da dieser zusätzliche Schritt ausgeführt wurde, ist garantiert, dass der von keyNotFoundSentinel referenzierte String eine eindeutige Instanz ist, die wir benötigen, um sicherzustellen, dass er niemals in der Map selbst angezeigt werden kann.

3
Cort Ammon

Lernen Sie aus dem Framework, das aus allen Schwachstellen von Java gelernt hat:

.NET bietet zwei weitaus elegantere Lösungen für dieses Problem, beispielsweise:

Letzteres ist sehr einfach in Java zu schreiben, Ersteres erfordert lediglich eine "starke Referenz" -Helferklasse.

2
Ben Voigt