Sollten wiederverwendete Ausnahmetypen gegenüber Einwegarten bevorzugt werden?

8

Nehmen wir an, ich habe Doors, die von a verwaltet werden DoorService. Der DoorServiceist für das Öffnen, Schließen und Verriegeln der in der Datenbank gespeicherten Türen zuständig.

public interface DoorService
{
    void open(Door door) throws DoorLockedException, DoorAlreadyOpenedException;

    void close(Door door) throws DoorAlreadyClosedException;

    /**
     * Closes the door if open
     */
    void lock(Door door) throws DoorAlreadyLockedException;
}

Für die Sperrmethode gibt es eine DoorAlreadyLockedExceptionund für die offene Methode gibt es eine DoorLockedException. Dies ist eine Option, es sind jedoch auch andere Optionen möglich:

1) Verwendung DoorLockedExceptionfür alles, ist etwas umständlich, wenn die Ausnahme bei einem lock()Anruf abgefangen wird

try
{
    doorService.lock(myDoor);
}
catch(DoorLockedException ex) // door ALREADY locked
{
    //error handling...
}

2) Haben Sie die 2 Ausnahmetypen DoorLockedExceptionundDoorAlreadyLockedException

3) Haben Sie die 2 Ausnahmetypen aber lassen DoorAlreadyLockedException extends DoorLockedException

Welches ist die beste Lösung und warum?

Winter
quelle
1
Wie definieren Sie "am besten"? In C # haben wir nicht einmal Ausnahmen überprüft. Wir glauben, dass dies unsere Entwicklung einfacher und zuverlässiger macht.
Robert Harvey
2
Wie unterschiedlich sind die Auflösungen von AlreadyLocked und Locked?
Whatsisname
@ RobertHarvey Ich betrachte Ausnahmen als Teil der Logik der Methode. Die beste Lösung ist die expliziteste und logischste. Anders als in C # fördern wir die Explikation gegenüber der Bequemlichkeit, indem wir das Auffangen von Ausnahmefällen erzwingen und sicherstellen, dass niemand eine Methode aufruft, ohne ein umfassendes Verständnis dafür zu haben, was vor sich geht.
Winter
1
Diese Methoden sollten einfach einen Statuscode zurückgeben. Sie können Ihre Benutzer anhand des Codes informieren, den Sie zurückerhalten, und kostenlos eine Leistungssteigerung erhalten (Ausnahmen sind teuer, etwa 1000- bis 10000-mal teurer als die Rückgabe eines Statuscodes).
Robert Harvey
1
@RobertHarvey "Ausnahmen sind teuer, etwa 1000- bis 10000-mal teurer als die Rückgabe eines Statuscodes" Ich habe dies in Java analysiert und die Kosten für Ausnahmen sind oft viel zu hoch angegeben. Sie können teuer sein, wenn Sie sehr tiefe Stapel haben. IIRC, ich musste die maximale Stapelgröße erheblich erhöhen, um Stapel erstellen zu können, die groß genug sind, um Ausnahmen langsam genug zu machen, um sich Sorgen zu machen. Selbst Stapelschweine wie Spring machen das nicht. Solange Sie nicht in engen Schleifen werfen und fangen, ist es eine vorzeitige Optimierung, um Ausnahmen zu vermeiden.
JimmyJames

Antworten:

21

Ich weiß, dass die Leute viel damit anfangen, Ausnahmen für die Flusskontrolle zu verwenden, aber das ist hier nicht das größte Problem. Ich sehe eine große Verletzung bei der Trennung von Befehlsabfragen . Aber auch das ist nicht das Schlimmste.

Nein, das Schlimmste ist genau hier:

/**
 * Closes the door if open
 */

Warum zum Teufel explodiert alles andere, wenn Ihre Annahme über den Türzustand falsch ist, es aber lock()nur für Sie behebt? Vergessen Sie, dass dies das Öffnen der Tür unmöglich macht, was absolut möglich und gelegentlich nützlich ist. Nein, das Problem hier ist, dass Sie zwei verschiedene Philosophien gemischt haben, um mit falschen Annahmen umzugehen. Das ist verwirrend. Tu das nicht. Nicht auf derselben Abstraktionsebene mit demselben Namensstil. Au! Nehmen Sie eine dieser Ideen mit nach draußen. Die Türservice-Methoden sollten alle gleich funktionieren.

Was die Verletzung der Befehlsabfragetrennung betrifft, sollte ich nicht versuchen müssen, eine Tür zu schließen, um herauszufinden, ob sie geschlossen oder offen ist. Ich sollte nur fragen können. Der Türservice bietet keine Möglichkeit, dies zu tun, ohne möglicherweise den Zustand der Tür zu ändern. Das macht dies so viel schlimmer als Befehle, die auch Werte zurückgeben (ein häufiges Missverständnis dessen, worum es bei CQS geht). Hier sind Zustandsänderungsbefehle die einzige Möglichkeit, Abfragen durchzuführen! Au!

Ausnahmen, die teurer sind als Statuscodes, sind Optimierungsgespräche. Schnell genug ist schnell genug. Nein, das eigentliche Problem ist, dass Menschen in typischen Fällen keine Ausnahmen erwarten. Sie können darüber streiten, was typisch ist, was Sie wollen. Für mich ist die große Frage, wie lesbar Sie den Verwendungscode machen.

ensureClosed(DoorService service, Door door){
  // Need door closed and unlocked. No idea of its state. What to do?
  try {
    service.open(door)
    service.close(door)
  } 
  catch( DoorLockedException e ){
    //Have no way to unlock the door so give up and die
    log(e);
    throw new NoOneGaveMeAKeyException(e);      
  }
  catch( DoorAlreadyOpenedException e ){
    try { 
      service.close(door);
    }
    catch( DoorAlreadyClosedException e ){
      //Some multithreaded goof has been messing with our door.
      //Oh well, this is what we wanted anyway.
      //Hope they didn't lock it.
    }
  }
}

Bitte lassen Sie mich keinen solchen Code schreiben. Bitte geben Sie uns isLocked()und isClosed()Methoden. Mit denen kann ich meine eigenen ensureClosed()und ensureUnlocked()leicht lesbaren Methoden schreiben . Diejenigen, die nur werfen, wenn ihre Postbedingungen verletzt werden. Ich würde lieber nur feststellen, dass Sie sie natürlich bereits geschrieben und getestet haben. Mischen Sie sie nur nicht mit denen, die werfen, wenn sie den Zustand nicht ändern können. Geben Sie ihnen zumindest unterscheidende Namen.

Was auch immer Sie tun, rufen Sie bitte nichts an tryClose(). Das ist ein schrecklicher Name.

Was das DoorLockedExceptionAlleinsein betrifft, muss DoorAlreadyLockedExceptionich auch sagen: Es geht nur um die Verwendung von Code. Entwerfen Sie solche Dienste nicht, ohne den verwendeten Code zu schreiben und sich das Chaos anzusehen, das Sie erstellen. Refactor und Redesign, bis der verwendete Code mindestens lesbar ist. In der Tat sollten Sie zuerst den Verwendungscode schreiben.

ensureClosed(DoorService service, Door door){
  if( !service.isClosed(door) ){
    try{
      service.close(door);
    }
    catch( DoorAlreadyClosedException e ){
      //Some multithreaded goof has been messing with our door.
      //Oh well, this is what we wanted anyway.
      //Hope they didn't lock it.
    }
  } else {
    //This is what you wanted, so quietly do nothing. 
    //Why are you even here? Who bothers to write empty else conditions?
  }
}

ensureUnlocked(DoorService service, Door door){
  if( service.islocked(door) ){
    throw new NoOneGaveMeAKeyException(); 
  }
}
candied_orange
quelle
Die lock()Methode, die aufgerufen wird, close()war nur, dass ich beim Schreiben der Frage faul war, aber danke, dass Sie mich darauf aufmerksam gemacht haben! Ich werde nicht darauf hereinfallen, wenn ich echten Code schreibe. In meinem Fall werden die 3 Methoden nie wirklich nacheinander aufgerufen. Es ist nur: Rufen Sie eine an, wenn es keine gute Ausnahme gibt, andernfalls zeigen Sie einen übersetzten Dialog und melden Sie sich möglicherweise ab (wenn InvalidTokenException). Ich habe isLocked()und isOpen()Methoden und sie werden im Kontrollfluss des Servers verwendet, aber sie sollten nicht verwendet werden, um zu überprüfen, ob ein Fehlerdialog angezeigt werden soll, das ist ein Job für eine Ausnahme.
Winter
Für Ihren letzten Punkt sieht der verwendete Code zwar etwas besser aus DoorAlreadyLockedException, macht aber Zwillingsklassen.
Winter
2
Wenn Sie mit Zwillingsklassen die Ausnahmen selbst meinen, lassen Sie mich Ihnen meine bevorzugte Verwendung der Vererbung zeigen : public class DoorAlreadyLockedException inherits DoorLockedException {}. Definieren Sie eine Klasse in einer Zeile, um ihr einen guten Namen zu geben. Wählen Sie mit Bedacht aus, was Sie erben. Ich ziehe es vor, von so hoch wie möglich zu erben, obwohl ich nicht sinnlos überflüssig bin, um eine lange Vererbungskette zu vermeiden.
candied_orange
Ich möchte das umbenennen NoOneGaveMeAKey, NoOneGaveMeAKeyExceptionnur um pendantisch zu sein. Ansonsten stimme ich dem Hauptpunkt zu, dass wir vor der Verarbeitung in der Lage sein müssen, den Zustand des Objekts zu kennen.
Walfrat
2
Devil's Advocate - nur gut zu haben isOpen/ isClosedist nicht gut genug. Denken Sie an das Dateisystem. Sie können überprüfen, ob die Datei vorhanden ist oder IOExceptionnicht. Dies kann sich jedoch ändern, wenn Sie versuchen, sie zu lesen oder zu schreiben . Vielleicht besteht in diesem Zusammenhang wirklich die Möglichkeit, dass der Status ungültig ist, wenn Sie die Tür öffnen oder schließen.
Tom G
8

Eine Unterscheidung zwischen DoorLockedExceptionund DoorAlreadyLockedExceptionlegt nahe, dass verschiedene Ausnahmetypen für die Flusskontrolle verwendet werden, eine Praxis, die bereits allgemein als Antimuster angesehen wird.

Es ist unentgeltlich, mehrere Ausnahmetypen für etwas zu haben, das so harmlos ist. Die zusätzliche Komplexität wird durch die zusätzlichen Vorteile nicht aufgewogen.

Einfach DoorLockedExceptionfür alles verwenden.

Besser noch, einfach nichts tun. Wenn die Tür bereits verriegelt ist, befindet sie sich nach Abschluss der lock()Methode immer noch im richtigen Zustand . Wirf keine Ausnahmen für Bedingungen, die unauffällig sind.

Wenn Ihnen das immer noch unangenehm ist, benennen Sie Ihre Methode um ensureDoorLocked().

Robert Harvey
quelle
4
Bitte verwenden Sie das Mem "Ausnahmen für die Flusskontrolle" nicht zu häufig. Wenn die beiden Situationen aus irgendeinem Grund dramatisch unterschiedliche Vorgehensweisen erfordern, ist es sinnvoll, unterschiedliche Ausnahmen zu haben. Aus dem Beispiel geht jedoch nicht hervor, ob dies der Fall ist.
Whatsisname
1
@whatsisname: Wenn es sich um ein Mem handelt, das tatsächlich keine Grundlage hat, posten Sie Ihre eigene Antwort in diesem Sinne.
Robert Harvey
1
@RobertHarvey Ausnahmen werden für den "Kontrollfluss" der Fehlerdialoge und der Fehlerprotokollierung verwendet. Die Hauptlogikanwendung wird offensichtlich nicht über die catch-Klauseln ausgeführt. Ich denke nicht daran zu unterscheiden DoorLockedExceptionund DoorAlreadyLockedExceptionschlage vor, dass es nur darum geht, einen viel aussagekräftigeren Fehlerbehandler für die lock()Methode zu haben, auf Kosten einer fast doppelten Klasse.
Winter
1
Wenn Sie lock () aufrufen und die Tür verriegelt ist, wissen Sie bereits, dass die Tür bereits verriegelt war. Ihnen muss nicht gesagt werden, was Sie bereits wissen.
Robert Harvey
2
@ewan: Oh, ffs. Nennen Sie es wie Sie wollen; eine Aufzählung, ein Ergebnis, was auch immer. Es ist der richtige Ansatz.
Robert Harvey
3

Das Thema dieser Frage "Sollten recycelte Ausnahmetypen den Einwegarten vorgezogen werden?" scheint ignoriert worden zu sein, nicht nur in den Antworten, sondern auch in den Details für die Fragen (die Antworten waren auf die Details der Frage).

Ich stimme der meisten Kritik an dem von den Befragten angebotenen Code zu.

Als Antwort auf die Überschriftenfrage würde ich jedoch sagen, dass Sie die Wiederverwendung von "recycelten Ausnahmen" gegenüber "Einwegausnahmen" nachdrücklich vorziehen sollten.

Bei einer typischeren Verwendung der Ausnahmebehandlung haben Sie einen großen Abstand im Code zwischen dem Ort, an dem die Ausnahme erkannt und ausgelöst wird, und dem Ort, an dem die Ausnahme abgefangen und behandelt wird. Das bedeutet, dass die Verwendung privater (modularer) Typen bei der Ausnahmebehandlung im Allgemeinen nicht gut funktioniert. Ein typisches Programm mit Ausnahmebehandlung - enthält eine Handvoll Speicherorte der obersten Ebene im Code, an denen Ausnahmen behandelt werden. Und da es sich um Top-Level-Standorte handelt, ist es für sie schwierig, detaillierte Kenntnisse über enge Aspekte bestimmter Module zu haben.

Stattdessen werden sie wahrscheinlich einen High-Level-Handler für ein paar High-Level-Probleme haben.

Der einzige Grund, warum die Verwendung detaillierter benutzerdefinierter Ausnahmeklassen in Ihrem Beispiel sinnvoll erscheint, liegt darin, dass Sie (und hier die anderen Befragten nachzeichnen) nicht die Ausnahmebehandlung für den normalen Kontrollfluss verwenden sollten.

Lewis Pringle
quelle
1
"Bei einer typischeren Verwendung der Ausnahmebehandlung haben Sie eine große ENTFERNUNG." Wir analysieren nicht, was typisch ist. Wir entwerfen einen Weg, damit es funktioniert. Viele gute Designs fangen ihre Ausnahmen auf der nächsten Ebene im Aufrufstapel und in einem äußerst begrenzten Umfang. Das sollte nicht als Flusskontrolle abgetan werden, wenn sie sich nur von dem Problem erholen. Die besten Ausnahmedesigns beginnen mit der Planung der Wiederherstellung, bevor die Ausnahmen definiert werden. Dann müssen die Ausnahmenamen nicht unbedingt das Problem melden. Es geht darum, die Lösung zu finden. Sie können die Nachrichtenzeichenfolge verwenden, um das Problem zu melden.
candied_orange
"Wir analysieren nicht, was typisch ist. Wir entwerfen einen Weg, wie es funktioniert.", Einverstanden, das haben Sie getan. Es ist nicht das, was ich getan habe. Wenn Sie sich die Überschrift ansehen, wurde eine Frage gestellt. Wenn Sie sich den Text der Nachricht ansehen, wurden drei verschiedene (verwandte) Fragen gestellt. Sie haben die Körperfragen beantwortet (bis zu einem gewissen Grad - wirklich mehr haben Sie den Beispielcode kritisiert). Ich beantwortete die Überschriftenfrage.
Lewis Pringle
Übrigens behaupten Sie: "Viele gute Designs fangen ihre Ausnahmen auf der nächsten Ebene im Aufrufstapel und in einem äußerst begrenzten Umfang." Ich bin mir nicht sicher, ob ich dem überhaupt nicht zustimmen würde. Aber ich muss sagen, nach 40 Jahren Programmierung fällt es mir schwer, ein Beispiel zu finden, um diese Behauptung zu unterstützen. Mir ist klar, dass es sich um ein Spin-off handelt (und ich handele dies möglicherweise nicht richtig - neu im Stackoverflow) - aber würde es Ihnen etwas ausmachen, ein oder zwei Beispiele anzubieten, bei denen eine solche lokalisierte Try / Catch-Handhabung die beste Lösung ist? Es ist sicherlich kein typisches / allgemeines Muster.
Lewis Pringle
Ein Beispiel, oder? Lassen Sie mich Ihnen Jon Skeet
vorstellen
OK, das ist überhaupt kein Beispiel für das, was Sie behauptet haben. Sie sagten: "Viele gute Designs fangen ihre Ausnahmen auf der nächsten Ebene im Aufrufstapel und in einem äußerst begrenzten Umfang." In Ihrem Beispiel wurde lediglich eine API übersetzt, die nur fehlerhafte Ausnahmen in eine API übersetzte, die stattdessen einen Sentinel-Wert erzeugte. Vielleicht ist dies nur ein Wortlautunterschied zwischen uns beiden. Ich würde das nicht als "gutes Design" beschreiben, sondern als "lokalisierten Hack, um eine unbequeme Designauswahl in einem von Ihnen verwendeten Tool zu umgehen".
Lewis Pringle
0

Eine unerforschte Alternative. Möglicherweise modellieren Sie mit Ihren Klassen das Falsche. Was wäre, wenn Sie diese stattdessen hätten?

public interface OpenDoor
{
    public ClosedDoor close();
    public LockedDoor lock();
}

public interface ClosedDoor
{
    public OpenDoor open();
    public LockedDoor lock();
}

public interface LockedDoor
{
    // ? unlock? open?
}

Jetzt ist es unmöglich, etwas zu versuchen, das ein Fehler ist. Sie brauchen keine Ausnahmen.

Caleth
quelle
In den meisten Anwendungen können Sie den Verweis auf diese Objekte jedoch nicht beibehalten, da die Datenbank jederzeit geändert werden kann. Gute kreative Leistung
Winter
0

Ich werde die ursprüngliche Frage beantworten, anstatt das Beispiel zu kritisieren.

Einfach ausgedrückt, wenn Sie erwarten, dass der Client für jeden Fall anders reagieren muss, ist ein neuer Ausnahmetyp erforderlich.

Wenn Sie sich für die Verwendung mehrerer Ausnahmetypen entscheiden und dennoch erwarten, dass der Client in einigen Fällen eine gemeinsame catch-Klausel für diese erstellen möchte, sollten Sie wahrscheinlich eine abstrakte Oberklasse erstellen, die beide erweitern.

Gudmundur Orn
quelle
0

Dies ist ein Pseudocode, aber ich denke, Sie werden verstehen, was ich meine:

public interface DoorService
{
    // ensures the door is open, uses key if locked, returns false if lock without a key or key does not fit
    bool Open(Door door, optional Key key) throws DoorStuckException, HouseOnFireException;

    // closes the door if open. already closed doors stay closed. Locked status does not change.
    void Close(Door door) throws throws DoorStuckException, HouseOnFireException;

    // closes the door if open and locks it
    void CloseAndLock(Door door, Key key) throws DoorStuckException, HouseOnFireException;
}

Ja, die Wiederverwendung von Ausnahmetypen ist sinnvoll, wenn Sie Ausnahmen für wirklich außergewöhnliche Dinge verwenden . Denn wirklich außergewöhnliche Dinge sind meistens Querschnittsthemen. Sie haben Ausnahmen hauptsächlich für Kontrollflüsse verwendet, und das führt zu diesen Problemen.

nvoigt
quelle
Aber diese Ausnahmetypen sind wirklich unbeschreiblich. Was bedeutet es überhaupt, wenn eine Tür "stecken bleibt", wenn Sie eine Tür nur öffnen, schließen oder verriegeln können?
Winter
Dies verschluckt nur die Ausnahmen und macht nichts mit ihnen. Dies ist in einem Kontext nicht angemessen, in dem Sie nicht wissen möchten, was los ist. Dies fühlt sich wie die C # -Methode meines Java-Beispiels an. Es gibt einen Grund, warum ich mit Java arbeite.
Winter
Nun, dass es feststeckt. Es kann sich nicht bewegen. Sie können es also nicht öffnen und nicht schließen. Es ist kein logischer Zustand, den Sie durch Aufrufen einer anderen Methode beheben müssen, sondern ein außergewöhnlicher Zustand, den Sie nicht über den Türservice lösen können.
nvoigt
Und es verschluckt nichts , es erzeugt keine Ausnahme für einen nicht außergewöhnlichen Programmzustand. Wenn meine Frau mich bittet, die Balkontür zu schließen, und ich aufstehe und herausfinde, dass sie bereits geschlossen ist, renne ich nicht herum und schreie und gerate in Panik. Ich gehe einfach zurück und sage "es ist jetzt geschlossen".
nvoigt
Genau, mit aktivierten Ausnahmen kann die Ausnahme nicht zum Start des Programms zurückkehren und muss vom Aufrufer abgefangen werden. Beim Anruf ignorieren Sie einfach die Ausnahme oder protokollieren eine Warnung und Sie sind gut. Was Sie auch als "außergewöhnliche Ausnahmen" bezeichnen, sind nur RuntimeExceptions. DoorStuckExceptionund HouseOnFireExceptionsollten nicht überprüft werden, sie sollen nicht bei allen Anrufen abgefangen werden.
Winter