Die Präsentation wird geladen. Bitte warten

Die Präsentation wird geladen. Bitte warten

Seminar aus Softwareentwicklung: Programmierstile

Ähnliche Präsentationen


Präsentation zum Thema: "Seminar aus Softwareentwicklung: Programmierstile"—  Präsentation transkript:

1 Seminar aus Softwareentwicklung: Programmierstile
Robustheit Christian Zeilinger

2 Übersicht Einführung und Beispiele von Softwarefehler
Was bedeutet „Defensives Programmieren“ Design by Contract Erfolgreiches Verwalten von Resourcen Self-Describing Data Die Kunst des Testens Robustheit, Christian Zeilinger Folie 2/23

3 Sich damit abfinden, oder gar verzweifeln?
Einführung Leider Tatsache: Es gibt keine perfekte Software! ! Also, was tun? Sich damit abfinden, oder gar verzweifeln? Was wie? – Natürlich! Es ist aber leider Tatsache … es gibt immer irgendwelche Ausnahmesituationen, die man nicht berücksichtigt hat – volle Festplatten, schier „unmögliches“ Benutzerverhalten, Putzfrauensyndrom … … oder einfach gewiffte Programmierfehler, die erst nach langer Zeit auftreten -> Software so robust wie nur möglich zu gestalten Robustheit, Christian Zeilinger Folie 3/23

4 Die schlimmsten Softwarefehler
SO NICHT!!! 1985: Software-Fehler in einem Röntgenapparat 1996: Explosion der Ariane 5 am 4. Juni 1999: Verglühen eines Mars Climate Orbiters Rechtzeitig gefundener Fehler: F-16 T+36: Fehler im Inertial Reference System Hauptcomputer weist Triebwerke an, eine große Korrektur durchzuführen Rakete bricht aufgrund aerodynamischer Kräfte auseinander (Selbstzerstörung) Sowie die dümmsten Verbrecher…. Röntgenapparat: Aufgrund zu hoher Strahlung wurden viele Leute verletzt und eine Person getötet Ariane: Inertial Reference System sendet Daten zum Hauptcomputer, dieser interpretiert sie als Flugdaten -> Korrektur -> Zerstörung Mars Orbiter: Übergabe von Daten nach dem englischen Maßsystem (feet) an ein metrisches System. F-16: Urspüngliche Software hätte beim Überfliegen des Äquators das Flugzeug auf den Kopf gestellt – Fehler durch Simulation gefunden. Robustheit, Christian Zeilinger Folie 4/23

5 Defensives Programmieren
Definition Robustheit: Fähigkeit von Softwaresystemen, auch unter außergewöhnlichen Bedingungen zu funktionieren Defensiv zu programmieren bedeutet, die Programme in Hinblick auf Robustheit zu gestalten Sehe ich als Oberbegriff um robuste Programme zu Erzeugung Definition Robustheit …. Dies können zum Beispiel fehlerhafte Eingabedaten oder eigentlich ungültige Systemzustände sein. Das heißt mit anderen Worten, jedesmal, wenn man irgendwelche Annahme über Eingabedaten, etc. trifft Annahmen PRÜFEN!!! !WICHTIG! Defensives Programmieren verhindert nicht Fehler, vermeidet aber daraus resultierende ungültige Operationen und hilft weiters beim Auffinden der Fehler. Deshalb: Alle Annahme über Eingaben, Systemzustände usw. prüfen und bei Fehlern handeln (Assertions, Exceptions,…) Zum Beispiel: „Die Variable x muss hier positiv sein“ „Dieser Zeiger darf hier (eigentlich) nicht NULL sein“ Robustheit, Christian Zeilinger Folie 5/23

6 Design by Contract Bedingungen, die gelten müssen, damit ein System funktionieren kann Beispiel im realen Leben: Paketzustelldienst Bedingungen der Zustellfirma: maximale Größe und Gewicht des Paketes Bezahlung der Dienstleistung im Voraus Bedingungen von Seiten des Klienten: Sorgfältiger Umgang mit der Fracht (keine Beschädigungen,…) Ankunft des Gutes am gewünschten Zielort innerhalb einer gewissen Zeitspanne Es geht dabei um die Vereinbarung von Bedingungen – könnte man auch als das Erstellen eines Vertrages auffassen (daher der Name) Robustheit, Christian Zeilinger Folie 6/23

7 Design by Contract „Lazy Code“ Preconditions
Bedingungen, die gelten müssen, damit eine Methode (Komponente) ausgeführt werden kann Postconditions Bedingungen, die nach dem Ausführen einer Methode gelten müssen (impliziert auch Resultate) Invarianten Bedingungen, die aus Sicht des Rufers immer erfüllt sein müssen Beispiele: Class-Invarianten, Loop-Invarianten „Lazy Code“ Preconditions: Es darf niemals eine Methode aufgerufen werden, sofern die Preconditions nicht erfüllt sind – D.h. der Rufer muss dies bewerkstellen – dennoch sollte man diese Bedingungen am Beginn der Methode prüfen (defensives Programmieren!) Aussagen über vom Benutzer eingegeben Daten (z.B. über Dialogfeld) sollte man aber nicht als Preconditions formulieren Postconditions: Die Methode/Komponente garantiert im Gegenzug eine Reihe von Bedingungen nach dem Beenden – Sortiermethode garantiert z.B. dass ein Array anschließend sortiert ist Weiters bedeutet auch da alleinige Vorhandensein einer Postcondition, dass die Mehtode terminiert (keine Endlosschleifen) Invarianten: Können während der Ausführung der Methode gebrochen werden, müssen jedoch am Ende wieder gelten. Grundsätzlich muss gelten: Sofern alle Preconditions einer Methode (Komponente) erfüllt sind, muss diese sicherstellen, dass alle Postconditions sowie Invarianten nach der Ausführungen gelten müssen. Für Vererbungsbeziehungen gilt: Eine Subklasse (bzw. eine Methode in dieser Klasse, die eine Methode der Basisklasse überschreibt) kann eine größere Menge von Inputs akzeptieren und kann mehr als Resultat garantieren. Das heißt, die Precondition kann gelockert werden und die Postcondition kann verschärft werden. Lazy-Code: Dies bedeutet, man soll sehr streng mit der Akzeptanz der Annahme von Daten umgehen und sowenig wie möglich im Nachhinein garantieren. Würde man alles akzeptieren und alles mögliche versprechen, so würde man immens viel Code zu programmieren haben! ;) …. Und weiters ist es sehr schwierig sicherzustellen. Robustheit, Christian Zeilinger Folie 7/23

8 Design by Contract /* class invariant:
* count enspricht der Anzahl der gesetzten Integer-Werte array ist ein Feld von Werten * die gesetzt werden können, wobei jeder noch nicht gesetzter Wert -1 entspricht. */ public class IntegerArray { ………………… public int Max { get { int max = -1; /* Loop-Invariante: * Vor jedem Durchlauf gilt: max = max(array[0:i-1]) (auch nach dem Ende der Schleife)*/ for(int i=0; i < array.Length; i++) if(array[i] > max) max = array[i]; return max; } } public int this[int index] { get { return array[index]; } // Precondition: 0 <= index < size set { array[index] = value; /* Nach der Ausführung dieser Anweisung ist die Klasseninvariante * verletzt, da count ungleich der der Anzahl der gesetzten Werte*/ count++; //Erst jetzt gilt die Klasseninvariante wieder } // Postcondition: Wert gesetzt …………………… Robustheit, Christian Zeilinger Folie 8/23

9 Überprüfung der Kontrakte
Design by Contract Überprüfung der Kontrakte Mit Hilfe von Tools Tool für Java: iContract @invariant) Assertionen Methodenaufruf mit Übergabe der Bedingung - Beispiel: assert(x > 0); Bedingung erfüllt: okay Bedingung verletzt: Fehler -> Programmabbruch Exceptions Sprachliche Unterstützung, um mit einfachen Mitteln: dem Rufer einen Ausnahmefall mitzuteilen (throw) ein (kollektives) Errorhandling zu bewerkstelligen (try …… catch) iContract: angeben von Pre, Post und Invariant ….. Zum Ausformulieren der Bedingungen gibt es weiters Operationen wie forall, exists oder implies Assertionen: Werden diese nicht von Seiten der Programmiersprache unterstützt kann man sie leicht selbst implementieren. Programmabbruch: Zwar keine zwingende Aktion, dennoch die günstigste Keine Abbruch -> Programm läuft in ungültigem Zustand weiter und kann schwerwiegende Schäden verursachen Weiters erleichtert ein frühes Beenden (Early Crash) bei Auffinden der eigentlichen Fehlerursache. Exceptions: Und zwar gilt, dass mit throw eine Exception direkt zum Rufer weitergeleitet wird, dieser muss sie aber nicht abfangen, sondern kann sie weitergeben. Das heißt mit anderen Worten: Irgendwo im Programm tritt ein Fehler – oder besser gesagt eine Ausnahme – auf, die erst in einem völlig anderem Programmstück behandelt werden kann. Robustheit, Christian Zeilinger Folie 9/23

10 Design by Contract Assertions using System.Diagnostics.Debug;
public class IntegerArray { ………………… public int this[int index] { get { Debug.Assert(index >0 && index < array.Length); //Prüfung der Precondition return array[index]; } set { array[index] = value; count++; public IntegerArray(int size) { Debug.Assert(size > 0); //Prüfung der Precondition array = new int[size]; for(int i=0; i<size; i++) array[i] = -1; Weiters können ebenfalls Postconditions und Invariante überprüft werden. Wie die Verwendung der Klasse Debug vermuten lässt, können Assertionen oft zentral (mittels Preprozessoranweisung oder Compileroption) deaktiviert werden. Die grundlegende Idee dahinter ist, dass man zur Testzeit, die Assertionen verwendet und in der endgültigen Releaseversionen diese „ausschaltet“, wodurch die Performance nicht unnötig gebremst wird. Dies ist jedoch kein gutes Prinzip. In der realen Welt treten immer irgendwelche Ausnahmen auf, die bei allen noch so sorgfältigen Tests nicht berücksichtigt wurden - beispielsweise eine Festplatte erreicht ihre Kapazitätsgrenzen oder eine Maus knabbert das Netzwerkkabel an. Somit sollte man auch Assertionen in Releaseversionen eingeschaltet lassen. Zwar kann es dann sein, dass das Programm öfters angehalten wird, was oft als Absturz wahrgenommen wird – dennoch werden dadurch unabsehbare, möglicherweise verheerende Folgen verhindert (zB Strahlendosis für einem Patient darf nur in einem gewissen Bereich liegen…) Hat man dennoch bedenken was die Perfromance angelangt, so sollte man nur jene Assertionen deaktivieren, die wirklich die Laufzeit in die Knie zwingen. Robustheit, Christian Zeilinger Folie 10/23

11 Design by Contract Exceptions
#include <stdio.h> //without Exceptions int main() { int xMin, xMax, yMin, yMax, error = 0; FILE *file = fopen("file.cfg", "rb"); if (f != NULL) { error = 1; } else if(xMin = fgetc(file) == EOF) { } else if(xMax = fgetc(file) == EOF) { } else if(yMin = fgetc(file) == EOF) { } else if(yMax = fgetc(file) == EOF) { return -1; } if (error == 0) { printf("%d,%d,%d,%d", xMin,xMax,yMin,yMax); } else printf("Error reading file.cfg"); fclose(file); using System; //using Exceptions using System.IO; class Test { static void Main() { int xMin, xMax, yMin, yMax; try { FileStream str = new FileStream("file.cfg", FileMode.Open); xMin = str.ReadByte(); xMax = str.ReadByte(); yMin = str.ReadByte(); yMax = str.ReadByte(); Console.WriteLine("{0},{1},{2},{3}", xMin,xMax,yMin,yMax); } catch(IOException) { Console.WriteLine("Error reading file.cfg"); } finally { //Code der in jedem Fall ausgeführt wird str.Close(); } Wie bereits erwähnt, werden Exceptions von Funktionen geworfen, sobald irgendwelche Ausnahmefälle auftreten – zB Nichtvorhandensein einer Datei. Würde man kein Exception-Handling zur Verfügung haben, so müsste man permanent überprüfen, ob z.B. die letzte Dateioperation auch erfolgreich war. Dies führt zu einer Vielzahl von if-Anweisung Mit Exception hingegen geht dies um einiges einfacher Try – catch Der finally-Block enthält Anweisung, die in jedem Fall ausgeführt werden – dass heißt, egal ob eine Exception aufgetreten ist, oder nicht. Eine generelle Frage ist, wann man Exceptions verwenden soll? Grundsätzlich lautet die Antwort: Nur verwenden, wenn es sich dabei um unerwartete Ereignisse handelt – ein File, dass eigentlich noch offen sein sollte, ist es nicht mehr – eine Überschreitung von Arraygrenzen,… Grundsätzlich sollte man sich die Frage stellen: „Würde mein Code unter normalen Umständen laufen, sofern ich die Exception weglassen würde?“ Wäre das nicht der Fall, so dürfte man auch keine Exceptions verwenden. Wenn man sich beispielsweise die Frage stellt, ob man bei Nichtvorhandensein einer Datei eine Exception werfen sollte, ist die Antwort: „Es hängt davon ab!“ Sofern die Datei vorhanden sein muss – z.B. system.ini oder ähnliches – so ist eine Exception angebracht. Ist man sich nicht sicher, ob die Datei vorhanden ist - zum Bilddatei – so sollte man eine if-Anweisung verwenden, und selbst die Prüfung durchführen. Exceptions werden gerne in General-Purpose-Routinen verwendet. In diesem Fall hat nämlich die Routine selbst oft keine Möglichkeit einen aufgetretenen Fehler in geeigneter Art und Weise zu behandeln. Das heißt, es ist günstiger diese Ausnahme an den Rufer weiterzuleiten. Eine weitere Anmerkung zu Exceptions: Durch das Werfen einer Exceptions wird der normale Programmablauf durch einen abrupten Sprung unterbrochen - die entspricht einem ähnlichen Verhalten wie bei einer goto-Anweisung. Dadurch leidet das Konzept etwas an den Schwächen des „altertümlichen“ Spaghetti-Codes. Beispielsweise an der erschwerten Lesbarkeit. Robustheit, Christian Zeilinger Folie 11/23

12 Verhaltensmuster: allocate – use – deallocate
Resource-Balancing Hauptspeicher, Threads, Dateien, Timer,… sind limitiert Verhaltensmuster: allocate – use – deallocate using System.IO; class Budget { FileStream fileStr = null; void ReadBudget(string fileName, out int budget) { fileStr = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite); budget = (new BinaryReader(fileStr)).ReadInt32(); } void WriteBudget(int budget) { fileStr.Seek(0, SeekOrigin.Begin); (new BinaryWriter(fileStr)).Write(budget); fileStr.Close(); public virtual int Update(string fileName, int newBudget) { int oldBudget; ReadBudget(fileName, out oldBudget); //Altes auslesen WriteBudget(newBudget); //Neues schreiben return newBudget; } } class NewBudget : Budget { public override int Update(string fileName, int newBudget) { int oldBudget; ReadBudget(fileName, out oldBudget); if (newBudget > 0) { //nur mit positiven neuem Budget überschreiben! WriteBudget(newBudget); return newBudget; } return oldBudget; Es gibt die unterschiedlichsten Arten vom Resourcen: Hauptspeicher, Threads, Dateien, Timer, usw. Von all diesen gibt es jeweils nur eine begrenzte Anzahl. Deshalb muss man sich stets um die sorgfältige Verwaltung dieser kümmern. Teilweise wird das durch die Programmiersprache unterstützt, teils muss man sich selbst darum kümmern. Hier ist ein kleines Beispiel, das zeigt, wie ein Verwaltung einer Datei-Resource aussehen könnte. Die Datei wird in der Methode ReadBudget geöffnet und das aktuelle Budget ausgelesen – es könnten sich noch andere Information in der Datei befinden, die später durch andere Methoden der Klasse ausgelesen werden können. Es sei sichergestellt, dass nach dem Schreiben eines neuen Budget die Datei nicht mehr benötigt wird. Deshalb befindet sich in dieser Methode auch das Schließen der Datei. Wohlgemerkt, ReadBudget und WriteBudget sind private! Die öffentliche Methode Update ruft nun beide Methode auf, um das Budget zu erneuern. Soweit so gut – die Resource „Datei“ wird belegt (durch ReadBudget) und wieder freigegeben (durch WriteBudget). Funktioniert also ganz gut – vielleicht etwas umständlich programmiert – aber dennoch fehlerfrei. Ein paar Monate später verordnet die Firma, dass bei neuen Finanzplanungen nur ein positiven Budget resultieren darf (ein negatives Budget hätte beispielsweise einen Bankkredit bedeutet). Als Konsequenz daraus wird eine neue Budget-Klasse erstellt, die die Funktionalität der alten etwas abändert. Zwar liest sie das alte Budget aus, setzt das neue jedoch nur dann, wenn es positiv ist. Das wiederum bedeutet, die Methode WriteBudget wird jetzt nicht jedesmal aufgerufen. Somit ist auch nicht sichergestellt, dass die Datei-Resource immer freigegeben wird. Man Stelle sich ein Finanzunternehmen vor, die Tausende solcher Budgets verwaltet. Nach einigen Stunden stürzt das Programm plötzlich ab und meldet „Too many open files“. – Viel Spaß bei der Fehlersuche! Das Problem an dieser ganzen Sache ist, dass die beiden Methoden ReadBudget und WriteBudget sich die globale Variable fileStr teilen und diese eben eine Resource hält. Robustheit, Christian Zeilinger Folie 12/23

13 Resource-Balancing Finish What You Start! using System.IO;
class Budget { protected void ReadBudget(FileStream fileStr, out int budget) { budget = (new BinaryReader(fileStr)).ReadInt32(); } protected void WriteBudget(FileStream fileStr, int budget) { fileStr.Seek(0, SeekOrigin.Begin); (new BinaryWriter(fileStr)).Write(budget); public virtual int Update(string fileName, int newBudget) { int budget; FileStream fileStr = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite); ReadBudget(fileStr, out budget); //Altes Budget auslesen if (newBudget > 0) { WriteBudget(fileStr, newBudget); //eventuell mit neuem Budget überschreiben budget = newBudget; fileStr.Close(); return budget; } } „Finish What You Start“ ist die Lösung für solche Probleme. Dies bedeutet idealerweise, dass eine Methode, die eine Resource allokiert, diese auch wieder freigibt. Somit würde unser voriges Beispiel folgendermaßen aussehen. Hier kümmert sich nun nur die Methode Update um das Öffnen und Schließen der Datei. Das Prinzip „Finish What You Start“ lässt sich auf das Belegen und Freigeben aller denkbaren Resourcen anwenden. !!! Mit anderen Worten heißt dies, jeder der eine Resource belegt, muss sich ebenfalls um die Freigabe kümmern. Leider ist dieses Prinzip nicht immer anwendbar. Beispielsweise bei dynamischen Datenstrukturen. Hierbei kann nicht jede Methode, die ein Objekt in diese Struktur einhängt, sich auch um die Freigabe kümmern. Es muss genau geregelt werden, wer dafür zuständig ist. Ein guter Kanditat wäre eine Klasse die diese Datenstruktur managet. Heutzutage erleichtern Garbage Collectoren, wie sie in C# oder Java vorhanden sind, den Umgang mit Resourcen wesentlich. In anderen Sprachen muss man sich selbst um die sorgfältige Handhabung von Resourcen kümmern. Um ein Programm auf korrekte Resource-Handhabung zu prüfen, gibt es Tools wie Purify oder Insure++. Diese suchen während der Laufzeit des Programms nach Speicherlöchern und geben so Auskunft über nicht freigegebene Resourcen. Robustheit, Christian Zeilinger Folie 13/23

14 Problem: Mysteriöse Daten!
Self-Describing Data Problem: Mysteriöse Daten! Linux-Verzeichnisausgabe mittels „dir“: total 44 -rw-r--r--   1 pr17     pr          4810 Dec  4 14:37 ETRC -rw-r--r--   1 pr17     pr          4810 Dec  4 14:36 ETRC.BAK drwxr-xr-x   2 root     root         512 Oct 24  2001 TT_DB -rw-r--r--   1 pr17     pr             0 Dec  4 17:02 out.txt drwxr-xr-x   3 pr17     pr           512 Dec  4 14:19 round drwxr-xr-x   2 pr17     pr           512 Nov  7 15:12 test drwxr-xr-x   2 pr17     pr           512 Dec  4 14:25 traces1 drwxr-xr-x   2 pr17     pr           512 Dec  4 14:29 traces2 drwxr-xr-x   2 pr17     pr           512 Dec  4 14:33 traces3 drwxr-xr-x   2 pr17     pr           512 Nov 12 17:35 ueb13 drwxr-xr-x   2 pr17     pr           512 Nov 20 16:51 ueb24 drwxr-xr-x   2 pr17     pr           512 Dec  3 15:35 ueb32 drwxr-xr-x   2 pr17     pr           512 Dec  3 17:43 ueb33 drwxr-xr-x   2 pr17     pr           512 Dec  4 12:30 ueb42 drwxr-xr-x   3 pr17     pr           512 Dec  4 12:17 ueb43 ??? Kundendaten: Hans Maier, 03/04/1976, 02/07/2001, 01/12/2002, 20 Christoph Huber, 01/02/1979, 01/01/2000, 11/02/2002, 10 Hannes Dorfer, 09/09/1958, 01/01/2000, 01/01/2002, 5 Linux-Prozessübersicht mittels: ps –ef | grep pr17 pr     1  0 17:00:58 ?        0:00 /system/apps/gup/bin/lamd -H P n 0 -o 0 pr  0 17:04:16 pts/2    0:00 -tcsh pr  0 17:03:19 ?        0:00 /usr/local/bin/tcsh -c sftp-server pr  0 17:00:48 pts/2    0:00 -tcsh pr  0 17:03:19 ?        0:00 sftp-server Jeder Programmierer kennt die Situation, vor einer Fülle von Daten zu stehen und eigentlich nicht genau zu wissen, was diese genau beschreiben. Vor allem beim Testen oder der Fehlersuche verzweifelt man oft aufgrund solcher Situationen. Zum Beispiel: Durchforsten von Datenfiles die Kundendaten repräsentieren. Ein Datensatz könnte beispielsweise folgendermaßen Aussehen: Hans Maier, 03/04/1976, 02/07/2001, 01/12/2002, 20 Was bedeuten diese Werte? Hans Maier ist der Name – das ist klar! Weiters könnte man vermuten, dass das nachfolgende Datum (03/04/1976) sein Geburtsdatum ist – wobei je nach Art der Auffassung Herr Maier entweder am 3. April oder am 4. März geboren sein muss (-> Was ist der Tag, was das Monat?) Die anderen Daten sind schon schwerer zu analysieren. Beispielsweise könnte dieser Datensatz bedeuten: Herr Hans Maier, geboren , ist sein bei uns Kunde und bekommt seit einen Treuerabatt von 20%. – oder es heißt ganz etwas anderes – wer weiß das schon. Ein anderes Beispiel ist die Ausgabe eines Linux-Verzeichnis mittels „dir“. Der Dateiname, die Größe und die Zeit der letzten Veränderung sind noch einigermaßen verständlich. Was jedoch bedeuten diese Attribute zu Beginn jeder Zeile? Was folgt danach? Eine Zahl und vermutlich der Benutzer der die Datei erstellt hat - oder? Selbige Schwierigkeiten treten beim Betrachten der zur Zeit laufenden Prozesse auf. Welches ist die Prozessnummer? - damit ich zum Beispiel den Prozess mittels „kill“ abschießen kann, weil er nicht mehr terminiert? – die erste oder zweite Zahl? Fragen über Fragen … die in diesem Fall vielleicht nur ein eingefleischter Linux-Freak beantworten kann. Wie bekommt man solche Probleme in den Griff? Das Stichwort heißt: Selbstbeschreibende Daten! Robustheit, Christian Zeilinger Folie 14/23

15 Self-Describing Data Speichereffizienz?
Name-Value Pairs: Daten + Schema Beispiel: Kundendaten %name „Hans Maier“ %birthday „03/04/1976“ %firstTransaction „02/07/2001“ %discountStartDate „01/12/2002“ %discountPercent 20 %name „Christoph Huber“ ……… Name-Value Pairs ist ein Konzept, Daten zusammen mit Interpretationsinformation anzugeben. Somit ist nun klar, welche Information welche Daten repräsentiert. Ein sehr bekanntes Beispiel für solche Name-Value-Pairs stellt die Windows-Registry dar. Klarerweise ist die Abspeicherung in dieser Art und Weise nicht gerade effizient – vor allem, wenn man bedenkt, dass es sich dabei vielleicht um 1000ende von Kunden handelt. Speichereffizienz? Robustheit, Christian Zeilinger Folie 15/23

16 Self-Describing Data Komprimierte Speicherung von Name-Value Pairs
Beispiel: Kundendaten naHans Maier|bi03/04/1976|ft02/07/2001|ds01/12/2002|di20 naChristoph Huber| Zusatzinformationen in einem Data Dictionary: ABBREVIATION NAME UNIT na name text bi birthday date ft firstTransaction date ds discountStartDate date di discount percent Herkunft der Daten und Geschichte von Veränderungen Wer? Wann? Wieso? Gerade im Bereich des Internets ist die Beschreibung von Daten von hoher Wichtigkeit, da diese oft alleine – das heißt, ohne Interpretationswerkzeug – versendet werden. Einen interessanten Ansatz bietete ein SDR-Model von Hewlett-Packard, welches im Jahre 1997 entwickelt wurde. Dieses beruht im Wesentlichen auf Name-Value-Pairs und entkoppelt die Daten vollständig von der eigentlichen Applikation. Dadurch können Datensätze strukturell erweitert werden, ohne die Funktionsfähigkeit von alten Anwendungen zu gefährden. Heutzutage verwendet man XML für diesen Zweck Weiters gehören zum das Kapitel der „Self-Describing Data“ Informationen über die Herkunft der Daten und der Geschichte von Veränderungen. Diese können ebenfalls in den Datenfiles mitgespeichert werden und sagen beispielsweise aus, welcher Prozess diese Daten erstellt hat, welcher sie verändert hat usw. usf. Robustheit, Christian Zeilinger Folie 16/23

17 Was bedeuten die einzelnen Werte?
Self-Describing Data Name-Value Pairs in der Programmierung Bsp.: AddProduct(1001, „C# and .NET Reference“, 5, 29.90, 20, 10); Was bedeuten die einzelnen Werte? Programmierdisziplin (Kommentare): AddProduct( 1001, //Produktnummer „C# and .NET Reference“, //Bezeichnung 5, //Stückzahl 29.90, //Verkaufspreis netto 20, //Mehrwertsteuer (in %) 10); //maximaler Rabatt Sprachliche Unterstützung AddProduct(pNr = 1001, name = „C# and .NET Reference“, count = 5, price = 29.90, MWSt = 20, discount = 10); Heutzutage sollte man auch die Möglichkeiten von Klassen und Strukturen beachten. Ich hätte beispielsweise anstelle dieser doch etwas komplizierte Methode AddProduct(…) eine einfacherer implementiert, der man nur ein einziges Objekt übergeben muss. In C# wäre dies dann beispielsweise eine Instanz einer Klasse, bei der man zuvor die Daten mittels Attribute setzt. Dadurch entsteht auch wieder eine Name-Value-Beziehung. Dieses Konzept hilft bei der Vermeidung von Fehlern. Beim ersten Aufruf von AddProduct hätte man mit Leichtigkeit die letzten beiden Werte verwechseln können – was beim schnellen Überarbeiten des Codes sicher nicht aufgefallen wäre. Bei der letzten Variante hingegen, wird einem der Fehler sofort bewusst, bzw. ist bei einer späteren Fehlersuche leichter zu finden. Zusammenfassend ist zu sagen, dass man einen Wert nie einfach so angeben sollte, sondern diesen auch zumindest mit einer kleinen Beschreibung versehen sollte. Dies gilt sowohl für Eingabedaten als auch für Outputs. Robustheit, Christian Zeilinger Folie 17/23

18 Die Kunst des Testens Testen bedeutet:
Ein Programm mit der Absicht auszuführen, Fehler zu finden! Fehler gefunden -> Test war erfolgreich Ging alles gut -> Erfolgloser Test Zum letzten Punkt: Das bedeutet, dass es immer irgendwelche sehr raffinierten Restfehler gibt, die noch so gute Teststrategien nicht finden können. Um alle Fehler finden zu können müsste man mit allen Eingaben und Systemzuständen testen – auch mit allen ungültigen! (meist unendlich viele) Testen kann NICHT zeigen, dass ein Programm fehlerfrei ist. Robustheit, Christian Zeilinger Folie 18/23

19 Teste während der Codeerstellung
Die Kunst des Testens Teste während der Codeerstellung Testen im Software-Lebenszyklus Test der Spezifikation Modultest Integrationstest Systemtest Abnahmetest Systematisch Testen inkrementelles Testen beginnend mit den einfachen und grundlegenden Teilen Welchen Output erwartet man sich? Vergleich verschiedener Implementierungen Coverage? Test sobald du den Code schreibst: Desto früher man einen Fehler findet, desto einfacher kann er behoben werden! Test der Spezifikation: Als Grundlage jeder Softwareentwicklung dient die Spezifikation. Deshalb ist auch ein Test dieser in Hinblick auf Klarheit, Vollständigkeit und Widerspruchsfreiheit von extremer Wichtigkeit da somit bereits in frühen Phasen Fehler entdeckt werden können. Modultest: Darunter versteht man das Testen von einzelnen Modulen wie Klassen, Methoden usw. Dazu muss eine Testumgebung geschaffen werden, die das Modul aufruft. Integrationstest: Werden Module zu einzelnen Subsystemen zusammengefasst, so muss erneut getestet werden. Systemtest Dieser dient zum Testen des gesamten Systems, wobei hier natürlich eine Menge neuer Fehlerquellen hinzukommen. Beispielsweise können sich zwei Module bzw. Subsysteme beeinflussen und dadurch neue Fehler hervorrufen. Abnahmetest Schließlich und endlich muss beim Verkauf der Software der Kunde von ihrer Funktionsfähigkeit überzeugt werden. Inkrementelles Testen: Sobald ein Codestück abgeschlossen ist, sollte es auch getestet werden. Zu diesem Zeitpunkt kennt man noch die korrekte Funktionsweise der Methode bzw. des Codestückes am Besten. Desto länger man mit den Testen wartet, desto schwieriger wird die Fehlersuche. Weiters können dann schlagartig eine Vielzahl von Fehlern auftreten, deren Korrektur mehr Zeit beansprucht, als wenn man sie bereits einzeln behoben hätte. Einfache und grundlegende Teile zuerst: Zuerst sollte man diese Teile testen, die als Basis für andere Programmstücke dienen und am häufigsten verwendet werden. Bei einer Fehlersuche zu einem späteren Zeitpunkt kann dann nämlich die Fehlerursache mit hoher Wahrscheinlichkeit auf die darauf aufsetzenden Module beschränkt werden und man kann dort dann auf höherer Abstraktionsebene testen. Output? Sehr wichtig ist, dass man, bevor man einen Test ausführt, sich das erwartete Ergebnis vor Augen hält. In manche Fälle ist dies sehr offensichtlich, zB bei einer Addition über Listenelemente sollte die korrekte Summe ausgegeben werden. Schwieriger ist dies jedoch beispielsweise beim Test eines Compilers. Was genau wäre hier der Output? (ein Binärfile – aber, wie sollte dies genau aussehen?) Hierfür könnte man sich händisch das Resultat überlegen oder jedoch Test-Soruce-Files verwenden und das Ergebnis mit dem eines bereits existierenden Compilers vergleichen. Vergleich verschiedener Implementierungen Dies ist sowieso ein gutes Vorgehen und eignet sich vor allem beim Entwickeln effizienter Algorithmen. Dabei schreibt man zuerst eine einfach Implementierung des Algorithmus, die das Ergebnis liefert, jedoch dafür längere Zeit benötigt. Danach erst versucht man das Problem auf möglichst effektive Art und Weise zu lösen. Ständiges Vergleichen der Ergebnisse mit deren der einfachen Variante deckt relativ schnell Fehler auf – Voraussetzung ist natürlich die Verwendung geeigneter Testdaten. Coverage Weiters sollte man sich fragen, inwieweit meine Testfälle den Code abdecken. Dabei unterscheidet man zwischen Anweisungsabdeckung und Verzweigungsabdeckung und unterscheidet drei Fälle. Abdeckung aller Anweisung (d.h. jede Codezeile wird zumindest einmal ausgeführt) Abdeckung aller Verzweigungen (d.h. jede Sprungalternative wird einmal gewählt) Abdeckung aller Bedingungen (d.h. bei komplexeren Bedingungen sollte jede Teilbedingung einmal wahr bzw. falsch sein) Abdeckung aller möglichen Pfade Robustheit, Christian Zeilinger Folie 19/23

20 Die Kunst des Testens ? White-Box-Testing vs. Black-Box-Testing
Grundsätzlich unterscheidet man beim Testen zwischen Black-Box- und White-Box-Testen. Beim White-Box-Testen inspiziert man den Code und versucht möglichst hohe Coverage zu erzielen zu erzielen. Das Black-Box-Modell kennt jedoch den genauen Inhalt der Methode/des Moduls nicht. Dadurch kann man die Abdeckung von Anweisungen, Pfaden usw. nicht feststellen. Hier hingegen konzentriert man sich auf das I/O-Verhalten welches durch die Spezifikation definiert ist. Black-Box-Testen hat zwar den Nachteil, dass es nie vollständig sein kann, was auch bedeutet, dass man weniger Fehler findet, dennoch ist es eine sehr gängige Testmethode, da man den Code des zu testenden Softwaremoduls nicht kennen muss. Darum werde ich an dieser Stelle nur auf die Vorgehensweise beim Black-Box-Testen eingehen. ? Robustheit, Christian Zeilinger Folie 20/23

21 Die Kunst des Testens Abklären, was getestet werden soll
z.B.: Methode: String ToUpperCase(String str, int startIndex, boolean unicode); Wahl geeigneter Inputs Äquivalenzklassen str: null, im ASCII-Code, im Unicode; unicode: true, false startIndex < 0, 0 <= startIndex < N, startIndex >=N Randbereiche startIndex: -1, 0, 1, N-2, N-1, N; str: null, 1-Zeichen, 2-Z., N-Zeichen Reduktion der Testeingaben „unmögliche“ Kombinationen: str.length == 0 und unicode == true nur für einen Parameter eine ungültige Äquivalenzklasse wählen Festlegen der erwarteten Ausgabe Durchführen des Test Vergleich der Ausgabe mit der erwarteten Vorgehensweise beim Black-Box-Testen Abklären, was getestet werden soll: Methode ToUpperCase, die in einem String Kleinbuchstaben durch Großbuchstabe ersetzt ab einem bestimmten StartIndex. Ist unicode auf true, so werden alle mögliche klein-groß Konvertierungen im Unicode durchgeführt – bei false nur die der Zeichen vom ASCII-Code. Als nächstes muss man geeignet Inputs für diese Parameter wählen. Grundsätzlich sind hier unendlich viele Wertkombinationen denkbar, deshalb sollte man jene suchen, die mit hoher Wahrscheinlichkeit zu einem Fehler der Methode führen. Dazu werden die Inputwerte zuerst in Klassen, den so genannten Äquivalenzklassen, gegliedert. Was genau eine solche Klasse bildet hängt sowohl vom Typ des Inputs ab als auch von der Art und Weise wie der Input laut Spezifikation behandelt wird. „str“ könnte man in die Klassen: null, reiner ASCII-Code, Unicode unterteilen. „startIndex“ in <0, von 0 bis N-1, >0. Würde die Methode beispielsweise anders funktionieren, falls der startIndex > N/2 ist, so sollte man die mittlerer Klasse erneut unterteilen. Wenn man hier sehen kann, gibt es gültige und ungültige Äquivalenzklassen. Ungültige wären startIndex < 0, str == null, usw. In weiterer Folge würde es reichen, einen Repräsentanten aus jeder Klasse auszuwählen und mit diesem zu testen. Die meisten Fehler treten jedoch im Randbereich der Äquivalenzklassen auf, somit findet man mit höherer Wahrscheinlichkeit Fehler, wenn man Randwerte verwendet. (z.B. startIndex -1, 0 und 1, N-2, N-1, N, usw.) Nun müsste man alle Kombinationen von Äquivalenzklassen testen (z.B. str im ASCII-Code, startIndex 0, unicode=false – oder selbiges nur mit unicode=true). Hierdurch entstehen natürliche eine Vielzahl von unterschiedlichen Eingaben. Eine Reduktion ist möglich, indem man alle „unmögliche Kombinationen“ wegstreicht wie z.B.: einen String mit der Länge 0, bei denen alle Unicode-Zeichen verändert werden sollen. Weiters sollte man immer nur eine ungültige Äquivalenzklasse auswählen und für die anderen Parameter Werte aus deren gültigen Klassen verwenden. Im nächsten Schritt muss die erwartete Ausgabe gewählt werden, im Beispiel ein korrekt veränderter String. Auch hier sollte man beachten, dass die Randbereiche der Ausgabe erreicht werden. Eine Inputbelegung zusammen mit der erwarteten Eingabe bildet einen Testfall. Ein Testfall ist gut, wenn er mit hoher Wahrscheinlichkeit Fehler findet. Anschließend wird der Test durchgeführt und die Ausgabe mit der erwarteten verglichen. Grundsätzliche soll man Testfälle nie wegwerfen und auch die Abspeicherung der Testergebnisse kann oft von Nutzen sein. Robustheit, Christian Zeilinger Folie 21/23

22 Die Kunst des Testens Testautomation Testabbruch Code-Review
Erzeugung von Eingabewerten Generische Daten Intelligente Daten Stress-Tests Regressionstesten Testabbruch wenn bestimmte Anzahl von Fehlern entdeckt wurde 1 Fehler / Anweisungen Es bleiben immer Restfehler!!! wenn bei gleichmäßiger Testanstrengung die Fehlerentdeckungsrate deutlich abnimmt Automatische Code-Review testen den Code auf die Einhaltung von Programmierrichtlinien. Dies können rein syntaktische Vorgaben sein, wie beispielsweise die Klammerung jedes if-Blockes, aber auch Richtlinien was die Deklaration von Methoden, Variablen etc. betrifft (Namenskonventionen, bestimmtes Aussehen der Parameterliste, etc.) Als weiterer Unterstützung können Eingabedaten automatisch erzeugt werden. Wie man bereits vorher gesehen hat, ist oft eine große Menge verschiedener Inputs notwendig um ausreichend zu testen. Bei der Erzeugung unterscheidet man grundsätzlich zwei Verfahren: Generische Vorgehensweise: Dabei erzeugt man Daten unabhängig von der Softwarekomponente die getestet werden soll. Beispielsweise für eine String würde man immer einen Null-String, einen String mit nur einem Zeichen und einen langen String erzeugen. Intelligente Daten: Im Gegensatz zu der generischen Erzeugung verwendet man das Wissen über die zu testende Komponente. Würde der String beispielsweise einen Dateinamen repräsentieren, so erzeugt man einen Null-String, einen Namen einer tatsächlich vorhanden Datei und einen Namen einer nicht vorhandenen Datei. Andere Werte erzeugt man, wenn der String den Namen eines Druckers repräsentiert usw. Daraus resultiert natürlich, dass für verschiedene Anwendungen verschieden Datengeneratoren geschrieben werden müssen, wohingegen bei der generischen Erzeugung bloß eine einziger notwendig ist. Stress-Tests: Dabei versucht man ein Software-Modul mit immens vielen Daten zu füttern, damit auch Fehler, die durch Überläufe von Buffern, Arrays, Zählern verursacht werden, entdeckt werden können. Hierfür ist natürlich Automation angesagt, da man selbst diesen Aufwand schon allein aus zeitlichen Gründen nicht bewältigen kann. Regressionstests: Nach Programmänderungen werden immer wieder die alten Tests von neuem ausgeführt, um zu verifizieren, ob nach wie vor alles funktioniert. Dabei vergleicht man das Resultat des neuen Testdurchlaufes mit den Ergebnissen vorangegangener (alter) Tests. Da immer wieder viele Tests ausgeführt werden müssen, ist an dieser Stelle Automatisierung angebracht. Diese übernimmt die Ausführung der Tests, die Abspeicherung der Ergebnisse und den Vergleich mit den alten Werten. Testabbruch: Basierend auf der Tatsache, dass alle 10 bis 25 Anweisung ein Fehler eingebaut wurde, kann man die ungefähre Fehleranzahl abschätzen und in Folge solange testen, bis größenordnungsmäßig so viele Fehler gefunden wurden. Zu beachten ist jedoch, dass immer Restfehler bleiben werden! Ein anderes Kriterium wäre, dass bei gleichmäßiger Testanstrengung die Fehlerentdeckungsrate deutlich abnimmt. Das heißt, trotz hinzufügen neuer Testfälle treten keine Fehler mehr auf. Robustheit, Christian Zeilinger Folie 22/23

23 Zusammenfassung Defensives Programmieren steigert die Robustheit von Softwareprodukten Design by Contract trifft Aussagen über notwendige Bedingungen zur Ausführung von Software Korrektes Resource-Balancing ist von hoher Notwendigkeit Selbstbeschreibende Daten helfen bei der Analyse und tragen zum allgemeinen Verständnis bei Ausreichende Tests sind der Schlüssel zu robusten Programmen Robustheit, Christian Zeilinger Folie 23/23

24 Danke für die Aufmerksamkeit!


Herunterladen ppt "Seminar aus Softwareentwicklung: Programmierstile"

Ähnliche Präsentationen


Google-Anzeigen