Dies ist der dritte Teil der Serie zum Thema Exception Handling:
- Grundlagen der Fehlerbehandlung
- Eigene Ausnahamen werfen
- Guidelines nach David Abrahams
- Fehlerbehandlung auf Anwendungsebene
David Abrahams hat 3 Guidelines definiert, an die sich Entwickler von Klassenbibliotheken halten sollen:
- Die no-throw guarantee
- Die basic guarantee
- Die strong guarantee
Es geht dabei um 3 Arten von Verträgen, die man als Autor einer Klasse den Konsumenten verspricht.
No-Throw Guarantee
Bei der “no-throw guarantee” muss sichergestellt werden, dass keine Exception die Kasse/Funktion verlässt. Dies bedeutet natürlich, dass beim Auftreten eines Fehlers in einer untergeordneten Komponente folgendes sichergestellt werden muss:
- Keine Exception verlässt die Funktion: Global catch
- Alle Ressourcen werden geschlossen (Keine Leaks)
- Maßnahmen zur Fehlerbehebung (Logging o.ä.)
Der Zustand des Objektes oder des Systems darf sich aber verändert haben. Ein Beispiel könnte so aussehen:
public static void AddPhoneNumbersNoThrowGuarantee(ref IEnumerable<User> userCollection) { ThirdPartyComponent component = null; try { component = new ThirdPartyComponent(); var user1 = userCollection.First(); user1.Telephone = component.GetTelephone(user1.Name); // state changed var user2 = userCollection.Last(); user2.Telephone = component.GetTelephoneErr(user2.Name); // exception occurs } catch { // No Throw Guarantee: all exceptions are handled. LogErrorAndPrayThatAdminFindsIt(); } finally { // No Throw Guarantee: all resources must be released. if (component != null) component.Dispose(); } }
Diese Garantie macht nur in sehr wenig Fällen sinn! Nur auf einer sehr hohen Ebene kann überhaupt entscheiden werden, was mit dem Fehler zu tun ist. Wenn Low-Level Module irgendwelche Fehler in ein Log schreiben ist das meisten nicht sehr hilfreich.
Dieser Vertrag wird in der Praxis in TryXXX-Methoden verwendet (z.B. int.TryParse()).
Außerdem sollte er bei Finalizer, Dispose und Delegates verwendet werden, weil hier auf keinen Fall Ausnahmen generiert werden sollen.
Basic Guarantee
Bei der basic guarantee handelt es sich wohl um die am meisten verbreitete Form.
- Alle Ressourcen werden (wenn nötig) geschlossen (Keine Leaks)
- Exception wird bei Ausnahme geworfen
- Der Zustand des Objektes oder des Systems kann sich aber geändert haben
public static void AddPhoneNumbersBasicGuarantee(ref IEnumerable<User> userCollection) { ThirdPartyComponent component = null; try { component = new ThirdPartyComponent(); var user1 = userCollection.First(); user1.Telephone = component.GetTelephone(user1.Name); // state changed var user2 = userCollection.Last(); user2.Telephone = component.GetTelephoneErr(user2.Name); // exception occurs } finally { // basic guarantee: all resources must be released! if (component != null) component.Dispose(); } }
Bei der basic guarantee macht es Sinn “Exception Translation” zu verwenden – also eine neue Ausnahme mit mehr Informationen und der ursprünglichen Ausnahme alls InnerException (Siehe Part 2).
Dieser Vertrag hat folgende Probleme:
Woher weiß der Konsument, in welchem Zustand das System beim Auftreten einer Ausnahme befindet? Welche Daten müssen neu geladen werden, damit es zu keinen Inkonsistenzen kommt? Deshalb eignet sich der Vertrag besonders innerhalb von Systemen und bei side-effect-free functions (Siehe Eric Evans: Domain Driven Design).
Strong Guarantee
Die strong guarantee geht noch einen Schritt weiter:
- Alle Ressourcen werden (wenn nötig) geschlossen (Keine Leaks)
- Exception wird bei Ausnahme geworfen
- Der Zustand des Objektes oder des Systems darf sich beim Auftreten einer Ausnahme nicht ändern!
Dies ist in der Praxis nicht immer ganz einfach. Und es ist nicht immer zu 100% möglich. Hier ein Beispiel, wie dir Garantie aussehen könnte. Prinzipiell geht es einfach um eine temporäre Zwischenspeicherung in einer lokalen Variable.
public static void AddPhoneNumbersStrongGuarantee(ref IEnumerable<User> userCollection) { ThirdPartyComponent component = null; var temp = new List<User>(); try { component = new ThirdPartyComponent(); var user1 = userCollection.First(); temp.Add(new User { Name = user1.Name, Telephone = component.GetTelephone(user1.Name) });// state did not change! var user2 = userCollection.Last(); temp.Add(new User { Name = user2.Name, Telephone = component.GetTelephoneErr(user2.Name) // exception occurs }); userCollection = temp; } finally { // strong guarantee: all resources must be released! if (component != null) component.Dispose(); } }
Das ganze passiert durch einen “Swap”. Dies funktioniert prima bei Value-Typen. Bei Referenztypen muss man hier extrem vorsichtig sein!
Die Beispiele mit den Ref-Argumenten mögen jetzt etwas gekünstelt aussehen – sie ließen sich aber gut testen. Hier nochmals ein realistischeres Beispiel:
public class SalesOrder { public int ID { get; private set; } public SalesOrderState Sate { get; private set; } public double Amount { get; private set; } public void RefreshSate() { var ammount = UnreliableMethod(); var state = UnreliableMethod2(); // if no error occured the state changes this.Amount = ammount; this.Sate = state; } private double UnreliableMethod() { // using releases all resources and leaves no leaks using (SPSite site = new SPSite("<a href="http://myserver">http://myserver</a>")) { using (SPWeb web = site.OpenWeb()) { var item = web.Lists["SalesOrder"].GetItemById(ID); return (double)item["State"]; } } } private SalesOrderState UnreliableMethod2() { using (CRMService service = new CRMService()) { return (SalesOrderState)service.GetState(ID); } } }
Bevorzuge immer die Strong Guarantee
Verwende die no throw guarantee für Finalizers, Dispose und delegates.
Vorsicht beim “Swap” von Referenztypen! Hier können leicht Bugs entstehen.