Defensive Programmierung



  • Was haltet ihr von defensiver Programmierung? Der Begriff scheint unterschiedlich definiert zu werden, deshalb mal kurz zur Illustration ein Beispiel:

    // foo wird nur von bar benutzt
    void foo(FILE* f) {
      if (f)
        dosomething(f);
      else
        barf();
    }
    
    void bar() {
      FILE *f = fopen("baz", "r");
      if (f)
        foo(f);
      else
        barf();
    }
    

    Es wird also jede Funktion für sich so behandelt, als müßte sie sich gegen alle Eventualitäten absichern.

    Ich nehme an, es gibt Leute, die sowas vertreten, anstatt einfach die Funktion foo mit der Precondition f != 0 zu dokumentieren und die Einhaltung mittels assert zu überprüfen. Möglicherweise muß ich mich bald mit so jemandem auseinandersetzen.
    Wie verbreitet ist diese Sicht überhaupt? Gibt es Menschen, die von Design-by-contract nichts halten? Mit was für Argumenten muß ich rechnen? Oder bin ich selbst auf dem Holzweg?
    Natürlich sind hier gerade die Profis gefragt ...



  • Hi,

    also ich hatte diese Mentalität auch mal eine Zeit lang.

    Heute bin ich eher ein Verfechter des 'exception handling by request'.
    Entstanden ist diese Mentalität durch den immer stärkeren Einsatz von Regressionstests und Refactorings. Indem ich die "defensive Programmierung" der Unit-Strategie unterordne und erst mal keinen Gedanken an 'ungelegte Eier' verschwende.

    Ich habe jedenfalls damit meine beste Entwicklungseffizients erzielt.

    cu

    P84



  • _Das_ Argument wird wahrscheinlich sein, dass man foo nun leichter in anderen Projekten wiederverwenden kann und das man bei Änderungen an dem Programm abgesichert ist gegen Fehler.

    (Ich finde diese Art der Programmierung nicht sehr gut. Wenn ich ein Programm schreibe, dann benutze ich für interne Prüfungen assert (wenn überhaupt), aber ich denke, dass so was in der Release Version nichts zu suchen hat, da es unnötig langsam ist)



  • Bei kleinen Projekten hat man ja den Ueberblick.

    Ich sicher mich wenn's geht auch meistens (leider nicht immer 😞 ) stark ab!

    Man weis nie, ob 'ne Fuktion spaeter sauber gerufen wird?

    Wichtig find ich absichern vor allem im Team (was weis ich wie die anderen sichern).

    ob direkt mit Tests oder Exceptions ist eigenlich Formsache / Stil, wichtig find ich DASS man sich (das Prog) absichert.



  • Besonders wichtig wird defensive Programmierung in nur halb organisierten
    Teams. Kannst du dir sicher sein, das deine Kollegen alles richtig machen?
    Nein? Dann fang Fehler selbst ab.
    Ja? Du bist also in einem professionellen Team, das alle Schnittstellen gut
    Dokumentiert und sich nach den eigenen Vorgaben richtet? Dann kann man
    hierrauf (wenigstens zum Teil) verzichten.
    Wenn du den Menschen also "Überzeugen" willst, dann mach ihm höflich klar,
    dass defensive Programmierung vor allem dort nötig wird, wo mehr Fehler als
    normal gemacht werden... 😃

    (Ja, ich weiß, diese Argumentation ist nur halb stichhaltig und ein wenig
    polemisch)



  • Absicherungen sind ja auch nicht so schlimm, dafür hat man ja assert in der Develop Version ist das meistens auch ganz nützlich, aber nachher beim Final Release muss das nicht wirklich sein, finde ich.



  • Ich sehe auf jeden Fall einen sehr großen Unterschied zwischen vermeidbaren Logikfehlern und unvermeidbaren, durch die Umgebung verursachten, Laufzeitfehlern (z.b. ungültige Benutzereingaben, unerwartete EOF, Datei nicht gefunden, etc). Indem ich eine Assertion setze, verwandele ich einen Laufzeitfehler in einen Logikfehler, da bereits der aufrufende Programmteil dafür hätte sorgen müssen, dass die Fehlersituation nicht bis zur Assertion weitergetragen wird, und zwinge damit den Aufrufer dazu, meine jetzt enger gefaßte Schnittstelle einzuhalten.

    <schön wäre es ...>
    Ich weiß nicht, ob es Sprachen gibt, die solche Assertions statisch checken ... Eiffel könnte ein Kandidat sein, tut es aber meines Wissens nicht. D.h. wenn ich eine Funktion habe wie:

    /* pseudo-eiffel-c */
    void foo(FILE *f) require (f != 0) {
      dosomething(f);
    }
    

    dann würde der Compiler an jeder Aufrufstelle der Funktion prüfen, ob das Argument 0 sein kann oder nicht. Ich denke die Datenflußanalysetechniken sind theoretisch vorhanden, jetzt muß es nur noch jemand umsetzen -- oder das ganze ist aus irgendeinem Grund nicht praktikabel, kann ja auch sein 🙂
    </schön wäre es ...>

    EDIT: hat keiner gemerkt *g*

    [ Dieser Beitrag wurde am 01.03.2003 um 12:05 Uhr von Bashar editiert. ]



  • <es wäre schön>
    Aspektorientierte Programmierung http://www.heise.de/ix/artikel/2001/08/143/
    </es wäre schön>



  • Das hat jetzt aber nix mit meinem <es wäre schön ...></> zu tun, oder? Von statischem Checking seh ich da nix, nur eine Technik, mit der man u.a. DBC-Konstrukte bauen kann.



  • @Bashar:
    Doch ... nur am Begriff 'static' stoß ich mich ein bischen.

    Trennung von Komponenten und Aspekten
    Ein wichtiger Begriff im AOP-Konzept ist das so genannte Code Tangling. Er bezeichnet die ‘Verschmutzung’ des funktionalen Codes durch den der verschiedenen Aspekte, die der funktionale Code zusätzlich aufnehmen muss, damit bestimmte Randbedingungen erfüllt sind. Für SimpleMessageQueue ist jetzt also das Code-Tangling geringer geworden, der Aspekt Thread-Sicherheit macht circa 40 Prozent der endgültigen Implementierung aus.

    AOP soll den Programmierer darin unterstützen, Komponenten und Aspekte sauber voneinander zu trennen. Dazu stellt es Mechanismen zur Verfügung, die einerseits das Auffinden von Aspekten und andererseits das Zusammenfügen aller Teile zum Gesamtsystem ermöglichen (siehe [5]).

    Ein Aspekt kann somit aus zwei Blickwinkeln beschrieben werden. Einerseits verkörpert er eine Anforderung an ein System. Andererseits stellt er ein Programmkonstrukt dar, das die Anforderung kapselt - etwa eine Klasse in Java.

    Dadurch dass Aspekte bestimmte Anforderungen in eigene Module auslagern, lässt sich im Gesamtsystem eine stärkere Trennung bezüglich der Anforderungen von Design und Implementierung durchhalten. Dabei werden Modul-, Objekt- und Komponentengrenzen in gewisser Hinsicht aufgehoben, da Aspekte auf Objekte zugreifen können, ohne sich dabei auf deren öffentliche Schnittstelle beschränken zu müssen. Aber eigentlich werden diese Grenzen nicht wirklich verletzt, da der gleiche Aspekt, der auf mehrere Objekte wirkt, diese unabhängig voneinander erweitert und er integraler Bestandteil jedes der betroffenen Objekte wird. Hier gibt es gewisse Ähnlichkeiten von Aspekten und Bibliotheken.

    Beispiele für Aspekte sind:

    Tracing/Logging und Monitoring/Diagnose/Profiling
    Fehlerbehandlung und Fehlertoleranz
    Synchronisation/Thread-Sicherheit
    Caching-Strategien
    Resource-Sharing
    Echtzeit
    History
    Optimierungen
    Objekt-Interaktion
    Transaktionen
    Sicherheit
    Konfiguration
    Test
    Verteilbarkeit und Fernaufrufe
    einheitliches Look & Feel von Benutzerschnittstellen

    http://www.heise.de/ix/artikel/2001/08/143/03.shtml



  • Anscheinend drück ich mich unklar aus: Mit statischem Checking mein ich, dass der Compiler *beweist* dass eine Assertion gültig ist (ich glaube SPARK kann sowas). Das Wort statisch ist die Essenz des ganzen, wenn du dich daran störst, bist du hinter einem ganz anderen Problem her als ich.



  • @Bashar:
    Das ist für mich wieder ein Regressionstest ... wir drehen uns im Kreis...
    Compiler mit Regressionstest?! 😕
    Wäre aber mal eine Erfindung. 🙂



  • Mir scheint ich sollte mal in Erfahrung bringen was ein Regressionstest genau ist bevor ich hier weiterposte 😉 schönes Wochenende dann ...



  • Auch so...
    Hellau!! 😃



  • Original erstellt von Prof84:
    Auch so...
    Hellau!! 😃

    *Arghh* 😡



  • ich bin der meinung dass foo keine chanze hat irgendwas zu ändern. Wenn jetzt ein NULL Zeiger übergeben wird, was kann foo machen? Exception werfen, OK.

    Aber bar() kann viel mehr machen. bar() weiss dass die Datei "baz" sich nicht öffnen hat lassen. Also sollte bar() vielleicht schaun ob genug rechte da sind oder so - zumindest kann bar() einen sinnvollen Fehler ausgeben.

    Das Problem bei foo() ist nämlich: es hilft nichts wenn foo() meldet: "man hat mir einen ungültigen Pointer übergeben". Dann muss ich nämlich suchen WO foo() aufgerufen wurde. OK, das geht ja mit debuggern recht gut - allerdings was machen wir wenn der Fehler nicht reproduzierbar ist? Dann wissen wir nur, dass foo() uU mit einem 0-Zeiger aufgerufen werden kann - Notlösung (was leider bei uns dann gemacht wird):
    if(!f) return;
    aber ist das das wahre?



  • Original erstellt von Shade Of Mine:
    **Exception werfen, OK.
    **

    der sinnvollste ort wo sie gecatcht werden sollte wäre in bar, alles ander kommt ein assert ähnlich, was sonnst sollen die oben da machen wenn sie von irgend wo unten so eine exception bekommen?

    Original erstellt von Shade Of Mine:
    Notlösung (was leider bei uns dann gemacht wird):
    if(!f) return;
    aber ist das das wahre?

    ist das nicht auch ein fehler im weitern programm verlauf?



  • ich bin der meinung dass foo keine chanze hat irgendwas zu ändern. Wenn jetzt ein NULL Zeiger übergeben wird, was kann foo machen? Exception werfen, OK.

    Naja, bei der defensiven Programmierung, wird foo wohl nie in die Situation kommen einen NULL Zeiger zu erhalten, deswegen ist ja defensive Programmierung quatsch.

    🙂

    Aber ich muss dir recht geben, Fehler fängt man am besten an der Stelle ab, an de man dem User eine Vernünftige Fehlermeldung geben kann und ihm danach ein stabiles System überlassen kann.



  • das Beispiel war wohl zu abstrakt, geb ich zu. foo müßte ein boolsches Ergebnis haben und bei Fehlern nach oben propagieren, wenn es keine Exception wirft. Das Problem ist, dass ich momentan mit C arbeite (Legacy-Code), also keine Exceptions zur Verfügung habe.

    Unter Umständen kann sich das ändern, aber dazu muß ich mich erst durchringen.



  • man kann auch unter C Exceptions simulieren, ist zwar nicht so toll wie in C++, aber hier ist mal ein kleiner denkanstoß

    #include <setjmp.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    struct exception_t
    {
      int no;
      jmp_buf buf;
    };
    
    struct exception_t NOMEMORY;
    
    void init_exceptions(void)
    {
      NOMEMORY.no=1;
    }
    
    inline void throw(struct exception_t exception)
    {
      longjmp(exception.buf,exception.no);
    }
    
    #define catch(exception,dofunc,doexception) \
    { \
      int ret=setjmp(exception.buf); \
      if(ret==0) \
        dofunc \
      else if(ret==exception.no) \
        doexception \
    }
    
    void *mymalloc(size_t n)
    {
      void *ptr=malloc(n);
      if(!ptr)
        throw(NOMEMORY);
      return ptr;
    }
    
    void foo(void)
    {
      free(mymalloc(1000000000000000u));
    }
    
    int main(void)
    {
      init_exceptions();
      catch(NOMEMORY,
      {
        foo();
      },
      {
        fprintf(stderr,"nicht genug speicher!\n");
        exit(1);
      });
      return 0;
    }
    

Anmelden zum Antworten