Error Handling Part 3: Guidelines nach David Abrahams

Dies ist der dritte Teil der Serie zum Thema Exception Handling:

  1. Grundlagen der Fehlerbehandlung
  2. Eigene Ausnahamen werfen
  3. Guidelines nach David Abrahams
  4. Fehlerbehandlung auf Anwendungsebene

David Abrahams hat 3 Guidelines definiert, an die sich Entwickler von Klassenbibliotheken halten sollen:

  1. Die no-throw guarantee
  2. Die basic guarantee
  3. 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:

  1. Keine Exception verlässt die Funktion: Global catch
  2. Alle Ressourcen werden geschlossen (Keine Leaks)
  3. 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.

  1. Alle Ressourcen werden (wenn nötig) geschlossen (Keine Leaks)
  2. Exception wird bei Ausnahme geworfen
  3. 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:

  1. Alle Ressourcen werden (wenn nötig) geschlossen (Keine Leaks)
  2. Exception wird bei Ausnahme geworfen
  3. 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s