Assertion Vermeidung
 
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern

Veränderung (letzte Änderung) (keine anderen Diffs, Normalansicht)

Verändert: 95c95
Ich glaube vielmehr, dass man gar nicht erst versuchen sollte, komplizierte und undurchsichtige Implementierungen durch komplizierte und doch nicht perfekte Assertions nachträglich gegen Fehlfunktionen abzusichern, sondern man sollte versuchen, Implementierungen von vornherein in möglichst kleine, für sich verstehbare und überprüfbare Bausteine zu zerlegen, deren Korrektheit man überschauen kann, statt dem eigenen Spaghetti-Code im nachhinein selbst zu misstrauen und zu versuchen, durch Assertions das Schlimmste zu verhindern.
Ich glaube vielmehr, dass man gar nicht erst versuchen sollte, komplizierte und undurchsichtige Implementierungen durch komplizierte und doch nicht perfekte Assertions nachträglich gegen Fehlfunktionen abzusichern, sondern man sollte versuchen, Implementierungen von vornherein in möglichst kleine, für sich verstehbare und überprüfbare Bausteine zu zerlegen, deren Korrektheit man überschauen kann, statt dem eigenen Spaghetti-Code im nachhinein selbst zu misstrauen und zu versuchen, durch Assertions das Schlimmste zu verhindern. (Siehe dazu auch http://lavape.sourceforge.net/doc/html/OOPS-MIMM.htm.)

Verändert: 99c99,101
-- KlausGünther
-- KlausGünther

Zusatz 21.1.2005: Ungeachtet unserer grundsätzlichen Skepsis gegen Assertions haben wir uns inzwischen überzeugen lassen, dass Pre-/Postconditions und Invarianten a la Eiffel aus ganz pragmatischen Gründen nützlich sein können, um grobe Fehler wenigstens frühzeitig zu erkennen, wenn man schon sein Programm nicht im Sinne der obigen Ratschläge durch sorgfältig zugeschnittene Klassen mit durch Assertions abgesicherten Konstruktoren aufgebaut hat. SpracheLava unterstützt jetzt also auch Pre-/Postconditions und Invarianten nach Eiffel-Muster (aber mit einem Vererbungs-Konzept, das neueren Erkenntnissen Rechnung trägt). Siehe http://lavape.sourceforge.net/doc/html/DBC.htm.

Bezüglich der Frage ExceptionsConsideredHarmful (s. a. ExceptionsDiskussion) habe ich in ExceptionsInSpracheLava eine ausgesprochen positive Ansicht vertreten:

Exceptions sind geradezu unentbehrlich, wenn man das Scheitern eines Methoden-Aufrufs dem Aufrufer auf sichere Weise mitteilen will, so dass er es nicht übersehen, sondern nur bewusst ignorieren kann.

Umgekehrt wird ein Schuh draus. In Sprachen, die das Sprachmittel "Exception" nicht kennen, _kann_ eine Methode/Funktion nicht scheitern, und es ist deshalb müssig, über ein Scheitern zu philosophieren. Die Sprache C belegt, dass man recht gut so programmieren kann, daher sind Exceptions sehr wohl entbehrlich. Wenn nicht sogar überflüssig. -- vgl

Wenn Du aus der Eigenschaft von C (und BASIC und Assembler) nicht auf die Allgmeinheit schliessen würdest, könnte ich Dir zustimmen. -- DavidSchmitt

(Antwort in ExceptionsInSpracheLava.) --kg

Bezüglich der Frage IstAssertSinnvoll habe ich dagegen eine eher skeptische Einstellung. Meine Vermutung ist:

Assertions werden in den meisten Fällen benutzt, um Schwächen der Programmiersprache oder Schwächen der Programmstruktur nachträglich auszubügeln.

Ich möchte Assertions nicht pauschal und grundsätzlich verdammen. Sie sind in manchen Fällen als Mittel zur einfachen Auslösung spezieller anwendungsbezogener Exceptions ähnlich nützlich und notwendig wie letztere.

In SpracheLava unterstützen wir Assertions

Aber ich denke dennoch, dass sich Assertions in den meisten heute als typisch angesehenen Einsatzfällen vermeiden lassen. Um dies etwas näher zu begründen, möchte ich drei Haupt-Einsatzbereiche von Assertions unterscheiden:

  1. Assertions, die unerlaubte Werte von Input-Parametern ausschließen,
  2. darunter speziell die sehr verbreiteten (Objekt-ungleich-Null)-Tests (s.a. NullAlsInputParameter.), und
  3. Assertions, die Zwischen- oder Endergebnisse einer Methodenanwendung auf Korrektheit/Plausibilität überprüfen.
Beginnen wir mit dem 2. Typ:

"Objekt ungleich Null" oder bei Verletzung: "missing initialization"

Dieser Assertion-Typ wird völlig entbehrlich, sobald die Programmiersprache die Möglichkeit bietet, alle Arten von Variablen (lokale Variablen, Member Variablen, Methoden-Parameter und -Return-Values) als optional zu deklarieren. (In SpracheLava geht das.) Damit drückt man aus, dass dieser Variablen niemals der Wert Null zugewiesen werden darf. Das kann dann zur Laufzeit automatisch geprüft werden und führt bei Verletzung zu einer spezifischen Exception.

In SpracheLava haben wir durch zusätzliche sprachliche Maßnahmen (Single-Assignment, Ersetzen traditioneller Schleifenkonstrukte durch rekursive Funktionen und beschränkte Quantoren) erreicht, dass man die meisten Fälle von "missing initialization" schon durch statische Checks, also zur Programmierzeit, ausschließen kann. Insbesondere wird in Lava eine strenge Initialisierungs-Disziplin erzwungen, die verhindert, dass man neu kreierte Objekte irgendwie benutzen kann, solange nicht alle nicht-optionalen Member-Variablen initialisiert sind (siehe http://www.sit.fhg.de/Lava/LavaDoc/html/IniChecks.htm).

Nun zum 1. Typ:

Unerlaubte Werte von Input-Parametern

Auch dieser Assertion-Typ wird in sehr vielen Fällen entbehrlich, indem man Objektklassen so gestaltet (und nötigenfalls spezialisierte Klassen definiert), dass Objekte der fraglichen Klasse die durch die Assertion ausgedrückte Bedingung von vornherien immer, nämlich per Konstruktion, erfüllen und diese Eigenschaft auch nicht nachträglich ruiniert werden kann. Letzteres wird man durch geeignete Access-Direktiven (private oder protected) und Attribut-Zugriff über Set/Get-Methoden (statt direkt) sicherstellen.

Assertions braucht man dann höchstens noch in den Konstruktoren der betreffenden Klassen, um unerlaubte Objekt-Parameter schon bei der Objekt-Konstruktion auszuschließen. Man muss dann aber nicht mehr vor jeder einzelnen Verwendung eines solchen Objektes erneut prüfen, ob es auch wirklich die verlangten Bedingungen erfüllt.

Beispiel aus DesignByContract:

Das Einfügen eines Elementes in ein Verzeichnis, und zwar unter einem bestimmten Schlüssel (in SpracheEiffel formuliert):

 put (x: ELEMENT; key: STRING) is
            -- Insert x so that it will be retrievable through key.
            require
                    count < capacity
                    not key.empty
            do
                    ... Some insertion algorithm ...
            ensure
                    has (x)
                    item (key) = x
                    count = old count + 1
            end

Hier ist in der require-Klausel nur "count < capacity" als eine natürliche und notwendige explizite Assertion anzusehen. Die Bedingung "not key.empty" könnte entfallen, wenn man, wie oben empfohlen, eine Klasse NonEmptySTRING einführen und als Parameter-Typ für "key" verwenden würde.

Beispiel Wertebereichs-Typ:

Man stelle sich eine Klasse mit vielen Methoden vor, deren Input-Parameter Wahrscheinlichkeiten sind, die also durch Gleitkommazahlen zwischen 0.0 und 1.0 dargestellt werden. In den heutigen Procedure- oder Klassen-Libraries werden diese Parameter typischerweise vom Typ Double sein, und man wird in jeder dieser Prozeduren immer wieder durch Assertions prüfen müssen, ob die Wahrscheinlichkeits-Inputs tatsächlich zwischen 0.0 und 1.0 liegen (wohingegen man in Sprachen mit strenger Typ-Bildung nicht zur Laufzeit prüfen muss, ob es sich tatsächlich um ein Double handelt).

Diese Assertions könnte man entbehren, wenn man stattdessen eine Klasse Probability aus Double ableiten und als Parametertyp statt Double verwenden würde, deren Konstruktoren zwar Double-Input-Parameter hätten, aber per Assertion Werte außerhalb des [0.0,1.0]-Intervalls ausschließen würden.

Nebenbei bemerkt: Eine ähnliche Ableitung spezialisierter Klassen aus numerischen Klassen könnte man auch benutzen, um die "Dimension" physikalischer Größen nachzubilden und typsicher verwendbar zu machen. So würde man die Verwechslung von Yard und Meter vermeiden, die schon zum Scheitern einer amerikanischen Mars-Mission geführt haben soll.

SpracheLava bietet für solche Fälle vereinfachte Ausdrucksmittel, um Objekte des weniger abgeleiteten Typs quasi "hochzuheben" auf das Niveau des mehr abgeleiteten Typs. Zum Beispiel möchte man etwa das Ergebnis der mathematischen Sinus-Funktion, das als Double abgeliefert wird, in ein Objekt der Klasse Meter, die aus Double abgeleitet ist, umwandeln. Dazu kann man in SpracheLava einen Ausdruck "sin(x) Meter" konstruieren, der das gewünschte leistet.

Nun zum 3. Typ von Assertion-Einsatz:

Korrektheit/Plausibilität von Zwischen- oder Endergebnissen

Betrachten wir noch einmal das obige Beispiel aus DesignByContract. Die dortige ensure-Klausel ist IMHO

Denn die ensure-Klausel lässt zum Beispiel eine Implementierung mit einer verzeigerten Liste zu, bei der (vielleicht versehentlich) das einzufügende neue Element nicht mit der vorhandenen Liste verzeigert wird, so dass immer nur das zuletzt eingefügte Element vom Anfang der Liste her erreichbar ist (wohl aber der count erhöht wird).

Die ensure-Klausel, und damit der Contract, ist dann zwar erfüllt, aber weder der Methoden-Benutzer, noch der Methoden-Programmierer hatten sich vorgestellt, dass das Einfügen in das Verzeichnis so funktionieren sollte, sondern die Meinung war eigentlich, dass die früher eingefügten Elemente auch nach dem Einfügen des neuen Elements noch unverändert und in der richtigen Anordnung in dem Verzeichnis stehen sollten. Aber so komplizierte Bedingungen lassen sich eventuell nicht oder nicht mit vertretbarem Programmier- oder Laufzeit-Aufwand realisieren; sie wären vielleicht ähnlich aufwändig wie die Implementierung selber.

Ich glaube vielmehr, dass man gar nicht erst versuchen sollte, komplizierte und undurchsichtige Implementierungen durch komplizierte und doch nicht perfekte Assertions nachträglich gegen Fehlfunktionen abzusichern, sondern man sollte versuchen, Implementierungen von vornherein in möglichst kleine, für sich verstehbare und überprüfbare Bausteine zu zerlegen, deren Korrektheit man überschauen kann, statt dem eigenen Spaghetti-Code im nachhinein selbst zu misstrauen und zu versuchen, durch Assertions das Schlimmste zu verhindern. (Siehe dazu auch http://lavape.sourceforge.net/doc/html/OOPS-MIMM.htm.)

Mit Kommentaren sollte man hingegen nicht geizen, aber auch nicht übertreiben, sondern diejenigen Stellen in Interfaces wie in Implementationen mit einem verständlichen Kommentar versehen, die "anders funktionieren, als der allgemeinen Erwartung entspricht", die sonst zu Missverständnissen führen würden oder an deren Sinn sich der Implementierer sonst nach einiger Zeit nur mühsam erinnern würde. Die "Gesamt-Philosophie" einer Klasse und die typische Benutzungsweise ihrer Methoden im Gesamt-Kontext sollte bei der Klassen-Deklaration nötigenfalls ausführlicher dokumentiert werden.

-- KlausGünther

Zusatz 21.1.2005: Ungeachtet unserer grundsätzlichen Skepsis gegen Assertions haben wir uns inzwischen überzeugen lassen, dass Pre-/Postconditions und Invarianten a la Eiffel aus ganz pragmatischen Gründen nützlich sein können, um grobe Fehler wenigstens frühzeitig zu erkennen, wenn man schon sein Programm nicht im Sinne der obigen Ratschläge durch sorgfältig zugeschnittene Klassen mit durch Assertions abgesicherten Konstruktoren aufgebaut hat. SpracheLava unterstützt jetzt also auch Pre-/Postconditions und Invarianten nach Eiffel-Muster (aber mit einem Vererbungs-Konzept, das neueren Erkenntnissen Rechnung trägt). Siehe http://lavape.sourceforge.net/doc/html/DBC.htm.


StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Text dieser Seite ändern (zuletzt geändert: 21. Januar 2005 11:58 (diff))
Suchbegriff: gesucht wird
im Titel
im Text