Bild von Björn Geisemeyer
Björn Geisemeyer

Exceptions: Strategien und Best Practices

Wie der Titel schon vermuten lässt, werde ich in diesem Beitrag verschiedene Strategien für den Umgang mit Exceptions beleuchten. Eine Exception zu fangen oder zu werfen ist einfach. Aber wie gehe ich sinnvoll vor? Wichtig ist die Berücksichtigung der Fehlerkategorien. Jede Ausnahme kann einer Fehlerkategorie zugeordnet und entsprechend behandelt werden. Und dann ist noch zu klären, wer die Verantwortung hat.

Eine Exception behandeln

Ich beziehe mich auf das schon in Teil 1 vorgestellte Beispiel:

				
					public class FileProvider
{
	public IEnumerable<string> ReadFileContent(string filename) {
		return File.ReadAllLines(filename);
	}
}
				
			

Die Signatur der Methode ReadFileContent gibt den Aufrufenden der Methode bisher keine Information über mögliche Ausnahmen. Ihre Implementierung kennt keinen Umgang mit Exceptions. Das führt zu unerwarteten Ergebnissen für die Aufrufenden. Die Methode ReadAllLines dagegen liefert keine unerwarteten Ergebnisse. Die Dokumentation erläutert exakt, welche Resultate zurückgegeben werden. Somit sind die von dieser Methode definierten Ausnahmen für die Aufrufenden transparent. Eine Lösung für diese möglichen Ergebnisse kann und sollte erstellt werden. Eine einfache Überarbeitung der Methode ReadFileContent mag wie folgt aussehen:

				
					public class FileProvider
{
    public Result<IEnumerable<string>> ReadFileContent(string filename)
    {
        IEnumerable<string> lines = Array.Empty<string>();
        
        try
        {
            lines = File.ReadAllLines(filename);
        }
        catch (Exception exception)
        {
            return new Result<IEnumerable<string>>(false, lines, exception.Message);
        }
            
        return new Result<IEnumerable<string>>(true, lines, string.Empty);
    }
}

public record Result<T>(bool HasValue, T Value, string Message);
				
			

In dieser überarbeiteten Methode wird eine generische Klasse Result<T> verwendet, die den gewünschten Rückgabewert enthält. Der bool-Wert gibt an, ob der Wert vorhanden ist, und die Message informiert im Falle einer Exception über die entsprechende Nachricht. Diese Klasse ist als record implementiert, was sie immutable macht. Der try/catchBlock stellt sicher, dass alle zehn möglichen Resultate in dieser Methode behandelt werden können. Jedes Ergebnis wird als entsprechendes Result zurückgegeben. Die Implementierung ist einfach, die Methode ReadFileContent erhält einen wichtigen Mehrwert. Sie übernimmt Verantwortung für den Umgang mit allen Ergebnissen die auftreten können und transformiert sie. Damit setzt sie einen Use Case um. Sie macht aus den Ausnahmen einer aufgerufenen Methode eine Regel für die eigene Anwendung. Durch diese Anpassung ändert sich auch die Signatur der Methode. Nutzende wissen nun, dass sie mehr als nur ein Ergebnis erhalten können. Das Result liefert das gewünschte Ergebnis ebenso wie mögliche Alternativergebnisse. Die Aufrufenden Methoden können aus dem Result über eine Fallunterscheidung den weiteren Arbeitsablauf ableiten, siehe [Abb. 3].

Abb. 3 ReadFileContent mit Result FD exceptions best practices
Abbildung 3

Die Komplexität der Prüfung der möglichen Ergebnisse und des Rückgabewerts hängt vom Use Case ab. Gleichermaßen ist abhängig vom Use Case, ob es sich bei einer unerwünschten Rückgabe um einen Fehler handelt und um welche Art von Fehler.

Warum nicht einfach eine Exception weiterwerfen?

Eine häufige Frage in Trainings ist, ob das Weiterwerfen gefangener Exceptions eine akzeptable Alternative zum beschriebenen Umgang mit Exceptions ist. Das folgende Listing zeigt ein angepasstes Beispiel.

				
					public IEnumerable<string> ReadFileContent(string filename)
{
    IEnumerable<string> lines = Array.Empty<string>();
    
    try
    {
        lines = File.ReadAllLines(filename);
    }
    catch (Exception exception)
    {
		// Seiteneffekt: Vielleicht etwas loggen...
        throw;
    }
        
    return lines;
}
				
			

Es repräsentiert einen klassischen rethrow-Mechanismus. Die Exception wird in der Methode gefangen, ein Seiteneffekt wird ausgeführt, beispielsweise ein Logging und abschließend wird dieselbe Exception erneut geworfen. Im weiteren Verlauf erläutere ich Gründe, warum dieses Vorgehen suboptimal ist.

Lesbarkeitseinbußen: Aus der Methodensignatur geht nicht eindeutig hervor, dass neben den lines auch Ausnahmen als mögliche Ergebnisse auftreten können. Es wird daher notwendig, eine ähnlich komplexe Dokumentation wie in [Abb. 1] im ersten Teil des Artikels zu erstellen, um Aufrufende über potenzielle Ausnahmen zu informieren.

Eingeschränkte Testbarkeit: Bei der Testabdeckung der Fail Cases müssen neben den erwarteten Exceptions auch die ausgelösten Seiteneffekte mitgetestet werden. Dies führt zu komplexeren Testfällen und erfordert den Einsatz von Attrappen.

Vermischung von Verantwortlichkeiten: Gemäß dem Single Responsibility Principle (SRP) hat die Klasse FileProvider die Verantwortung, mit den Ausnahmen der ReadAllLines-Methode umzugehen. Einen Seiteneffekt auszulösen gehört nicht dazu. Die Klasse nimmt dadurch eine zusätzliche Verantwortung wahr, die eigentlich in den Bereich der aufrufenden Methode fällt. Wird die Information aus der Exception für den weiteren Programmablauf benötigt, kann der Result-Datentyp aus [Listing 2] entsprechend erweitert werden.

Zerstückelung des Workflows: Das Werfen einer Exception als Ergebnis innerhalb einer Anwendung führt grundsätzlich dazu, dass diese Exception auf einem anderen Abstraktionsniveau behandelt werden kann. Der Workflow steigt an einem Punkt beim Auslösen einer Exception aus und wird potentiell an einer ganz anderen Stelle im Programmcode fortgesetzt. Das erschwert das Verständnis der abgebildeten Lösung. Man muss an verschiedenen Stellen suchen und das Gesamtbild wieder zusammensetzen.

Klärung der Fehlerkategorien: Wer ist für das Fangen der weitergeworfenen Exception zuständig? Ein globaler Exception-Handler, wie im Abschnitt „Entwicklerfehler“ beschrieben? Dieser würde versehentlich entstandene Exceptions verwalten. Die hier auftretenden Ausnahmen sind jedoch bekannt und mittels Seiteneffekt teilweise behandelt. Es müsste ein eigener Handler für diese Ausnahmen erstellt werden, der einen weiterführenden Workflow abbildet. Daraus folgt die Notwendigkeit bekannte von unbekannten Exceptions zu unterscheiden. Dies wiederum erfordert die Erstellung eigener Exception-Typen. Erst dann können die Verantwortungen getrennt werden. Schließlich bleibt die Frage, ob der Seiteneffekt nicht besser im Handler für bekannte Exceptions aufgehoben ist. Das würde die eingeschränkte Testbarkeit und die Vermischung von Verantwortlichkeiten entlasten. Gleichzeitig bestätigt es die Zerstückelung des Workflows.

Wann nutzt man den Throw-Mechanismus?

Der vorherige Abschnitt thematisiert, in welchen Situationen das Werfen von Exceptions wenig sinnvoll ist. Nun werde ich die Stellen beschreiben, an denen das Werfen von Exceptions Sinn ergibt. Auch dazu beginne ich mit einer Frage, die oft in Schulungen aufkommt: Was ist besser – die Verwendung eigener Datentypen oder die Arbeit mit Exceptions? Die Antwort darauf ist erstmal einfach. Es kommt darauf an. Wie zur Klärung von Fehlern muss auch hier der Kontext betrachtet werden. Eine genauere Aussage kann und muss getroffen werden, wenn die folgenden Fragen beantwortet werden können.

  1. Was entwickle ich?
  2. Für wen sind Exceptions Ausnahmen?
  3. Wer hat die Verantwortung für den Umgang mit den Ausnahmen?

Um diese Fragen zu klären, betrachten wir zwei Beispiele:

Im ersten Szenario entwickle ich eine Bibliothek zur PDF-Generierung. Aus Input-Daten und Formatvorlagen soll ein PDF erzeugt und im Dateisystem gespeichert werden. Diese Bibliothek soll in verschiedenen Projekten als Paket verwendet werden. Die Schnittstelle dieses Pakets sind seine öffentlichen Methoden. Sie greift auch auf eine externe Ressource, das Dateisystem, zu.

Bedienerfehler und Fehler aus der Umgebung müssen geprüft werden. Die Eingangsdaten können für die Erzeugung eines PDFs ungültig sein. Innerhalb der Bibliothek kenne ich den Kontext nicht, aus dem die Daten stammen. Ungültige Daten werden als Fehler betrachtet, da die Bibliothek mit ihnen ihre Aufgabe nicht erfüllen kann.

Die Verantwortung für den Umgang mit den durch diese Bibliothek erzeugten Ausnahmen trägt der Verwendende. Dort sind die Eingabedaten bekannt. Ebenso der Kontext, in dem das PDF erstellt werden soll und der dazugehörige Workflow. Daten, die zu ungültigen Bedingungen für die Aufgabe der Bibliothek führen, dürfen als Exception nach außen gegeben werden. Wichtig ist, dass die Exceptions in der Dokumentation der öffentlichen Methoden als mögliche Ergebnisse aufgenommen werden.

Im zweiten Szenario entwickle ich eine Desktop-Anwendung, die Rechnungen erstellen soll. Dabei wird unter anderem das Paket aus dem vorherigen Beispiel verwendet, um eine Rechnung als PDF zu generieren und anschließend per E-Mail versenden zu können.

Das Paket zur PDF Generierung wird als eine externe Ressource betrachtet. Die Ausnahmen, die aus dem Paket kommen, können mit den Daten aus dem Kontext der Anwendung verarbeitet werden. Letztendlich führt dies zu einem alternativen Workflow. In diesem Szenario sind Exceptions nicht unerwartet, sondern erwartbare Ergebnisse.

Die Verantwortung für den Umgang mit den Ausnahmen liegt bei der Desktop-Anwendung selbst. Hier sind die Daten bekannt. Der gesamte Workflow der Rechnungslegung wird von der Anwendung gesteuert. Hier werden also auch mögliche Exceptions in reguläre Ergebnisse umgewandelt.

Fazit

Exceptions sind Ergebnisse im Code, die sich von Standard-Rückgabewerten unterscheiden. Ihr Wert liegt darin, unerwartetes Verhalten im Code zu protokollieren. Als Entwickelnde liegt unsere Aufgabe darin zu entscheiden, ob dieses Verhalten im Sinne unserer Anwendung tatsächlich unerwartet ist. Dabei helfen die Fehlerkategorien. Beim Umgang mit Exceptions geht es um das Thema Verantwortung. Entwickele ich ein Endprodukt, trage ich die Verantwortung, für bekannte Exceptions Lösungen zu entwerfen. Bei der Entwicklung einer Bibliothek, die eine Teilaufgabe erfüllt, liegt die Verantwortung für den Umgang mit möglichen Exceptions beim Anwendenden. Die hier vorgestellten Strategien sollen dabei helfen, passende Lösungen zu finden.

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_DEGerman