Session D-BUG

VFP-Applikationen ohne Bugs

Christof Lange
The Foxpert!


This is a known issue...

Was ist ein Bug eigentlich? Die Antwort auf diese Frage kann beliebige komplex sein, aber für die Praxis reicht die wenig wissenschaftliche Antwort, daß ein Bug (oder Defekt) dann vorliegt, wenn etwas nicht so funktioniert, wie es funktionieren sollte. Können solche Defekte vermieden werden? Sicherlich nicht, denn kein noch so begabter Programmierer schreibt beim ersten Versuch fehlerfreien Code, kein Designer wird das optimale Design von Anfang an finden.

Es fehlt noch eine kleine Ergänzung dieser Definition. Für jedes Software-Projekt ist eine Quellcodekontrolle unerläßlich. Im Regelfall wird diese über ein spezielles System wie Visual Sourcesafe oder PVCS implementiert. Steht ein solches System aber nicht zur Verfügung, dann reicht es auch aus, dies manuell durch Kopieren der Dateien zu implementieren. Die entscheidenden Punkte sind: Es gibt ein Set von Mastersourcen, die den aktuellen Stand der Programmentwicklung darstellen. Für freigegebene Version besteht entweder eine Kopie der Quellen, oder aber in einem Versionskontrollsystem eine Markierung des Standes. Ein Protokoll sichert, daß nur ein Entwickler an einer Datei arbeitet, bzw. Änderungen zusammengeführt werden. In einem System wie Visual SourceSafe wird dies im Regelfall dadurch erreicht, daß Dateien ausgecheckt werden müssen, bevor sie geändert werden können und nicht ausgecheckte Dateien schreibgeschützt sind.

Bei der Verwendung einer Quellcodekontrolle besteht die Möglichkeit eine andere Art von Bugfreiheit zu erreichen: Bevor ein Entwickler die Quelltexte einchecken, muß er sicherstellen, daß diese mit der aktuellen Programmversion laufen, verschiedene Tests durchgeführt haben, durch ein Review gegangen sein, etc. Mit anderen Worten, wenn ein Entwickler eine Datei in die Mastersourcen eincheckt, dann macht er es aus der innersten Überzeugung, daß diese Quellen fehlerfrei sind.

Fehler, die der Entwickler beim Codieren macht und findet, bevor sie eingecheckt werden, sind für das Projekt nicht wirklich entscheidend. Die Auswirkungen solcher Fehler schlagen sich allenfalls in der Produktivität des Entwicklers nieder. Ein Programmierer, der beim Entwickeln viele Fehler macht, oder lange benötigt, um Fehler zu analysieren und zu beseitigen, wird weniger schaffen und eventuell seine Meilensteine häufiger verfehlen, als ein Entwickler, der solche Fehler effizient handhabt. Für den Programmierer können diese Fehler in sofern interessant sein, als das sie ihm Auskunft über Schwachpunkte geben können, aber darüber hinaus haben diese Fehler keine Auswirkung auf das Projekt.

Ganz anderes dagegen Fehler, die in die Mastersourcen gelangt sind. In dem wir fordern, daß die Mastersourcen immer fehlerfrei sind und indem wir zusätzlich darauf bestehen, daß die darin enthaltene Version immer lauffähig ist, haben wir einen großen Vorteil: Eine zu jeder Zeit lauffähige Programmversion, die den aktuellen Entwicklungsstand belegt. Damit haben wir eine Version, die wir zum Testen an den Kunden geben können, oder mit der die Anleitung geschrieben werden kann, oder aber, die für automatisierte Tests verwendet werden kann. Bei geschickter Anordnung der Reihenfolge, in der Funktionen implementiert werden, können wir so zur Not auch noch nicht so häufig gebrauchte Features fallen lassen und das Programm in der aktuellen Version freigeben, wenn wir unter Zeitdruck geraten. Die meisten Kunden haben lieber 90% der Features, die funktionieren, als 100%, die nicht funktionieren.

Nur Fehler, die in die Mastersourcen gelangt sind, definiere ich im folgenden als Defekt oder Bug, da nur diese entscheidend sind. Priorität muß daher sein, diese Mastersourcen fehlerfrei zu halten. Nur so können wir die Vorteile nutzen, haben einen klaren Überblick über den Projektfortschritt und können sicher sein, wenigstens irgend etwas jederzeit ausliefern zu können. Die Frage ist also, wie verhindern wir, daß Fehler in die Mastersourcen gelangen?

Je länger ein Defekt in den Mastersourcen verbleibt, desto teurer wird er. Wenn Sie in einem Team arbeiten, ist es wahrscheinlich, daß einer Ihrer Kollegen den Bug ebenfalls findet. Nur wird er nicht unbedingt den Fehler in Ihren Quellen vermuten, sondern in seinen zuerst suchen. Dadurch kann es dazu kommen, daß mehrere Entwickler an den selben Fehlern arbeiten. Auch die Personen, die im weiteren Verlauf mit dem Projekt zu tun haben, wie Tester, Anwender, etc. Können über solche Bugs stolpern. Diese Leute haben nicht viel davon, wenn Sie einen Fehler in den Mastersourcen behoben haben, denn für eine längere Zeit werden sie jede neue Version daraufhin überprüfen, ob dieser Fehler immer noch in der Anwendung ist.

Testen, testen, testen...

Im Gegensatz zu anderen Entwicklungstools findet der Visual FoxPro Compiler nicht viele Fehler während der Kompilierung. Aber wenn er welche findet, dürfen diese keinesfalls ignoriert werden. Der Grundsatz für fehlerfreie Programme muß daher sein, daß jede Zeile zumindest einmal im Debugger ausgeführt wurde. Dies ist um so wichtiger als das viele Fehler erst dann sichtbar werden, wenn wir die Ablaufreihenfolge sehen und wir feststellen können, wie sich Variablen, Datensätze, Eigenschaften, etc. während des Programmablaufes ändern.

Der Visual FoxPro Debugger ist leistungsfähiger, als viele glauben möchten. So können Sie, wenn Sie den Debugger in einem separatem Fenster laufen lassen, zum Beispiel die Programmausführung an jeder beliebigen Stelle fortfahren. Wenn also eine Methode andere Werte zurückgibt, als Sie erwartet haben, setzen Sie den Zeiger doch einfach eine Zeile zurück, und gehen in die Methode hinein, um festzustellen, was genau schief gegangen ist. Im gleichen Menü finden Sie auch die Option, das Programm abzubrechen und den Designer an der Stelle zu öffnen, an welcher der Fehler aufgetreten ist.

Noch effektiver setzen Sie den Debugger ein, wenn Sie die APP in einer zweiten FoxPro Instanz testen. Da Sie hier auf die APP zugreifen, die alle Dateien bereits enthält, können Sie sämtliche VCX und SCX Dateien bearbeiten, während die Applikation läuft. Mit anderen Worten, wenn Sie einen Fehler finden, können Sie diesen sofort beseitigen und dann die Applikation weiter testen, ohne daß Sie das Debuggen abbrechen möchten. Das ist zwar nicht ganz so wie in Visual Basic, wo Sie den Code während des Testens direkt umschreiben können, aber immerhin kommt es nahe.s

Dabei hilft vor allem das Befehlsfenster. Wenn Sie im Debugger sind und das Programm gerade unterbrochen haben, dann verhält sich das Befehlsfenster so, als ob es zu dieser Methode dazugehören würde. Sie können also auf alle lokalen Variablen zugreifen, aber auch Referenzen wie THIS und THISFORM, auf die Sie sonst nicht zugreifen könnten. Sogar ein WITH...ENDWITH, daß Sie in einer Methode ausführen, wirk im Befehlsfenster fort. Solange Sie nur eine Zeile zur Zeit ausführen, können Sie jede beliebige Zeile aus dem Quellcode in das Befehlsfenster kopieren, dort korrigieren und anschließend ausführen und bei Gelegenheit auch in den Originalquellen korrigieren.

Das alleine reicht natürlich nicht, auch wenn auf diese Weise viele Fehler gefunden werden können. Um langfristig und dauerhaft gute Ergebnisse zu erhalten, müssen weitere Techniken eingesetzt werden. Bevor ich diese Techniken im Detail erläutere, möchte ich aber noch das Thema "defensive Programmierung" behandeln.

Seien Sie nicht so defensiv!

Defensive Programmierung kommt in zwei Kategorieren: Zum einen bezeichnet der Terminus "defensive Programmierung" die Überprüfung sämtlicher Eingaben und Aktionen des Anwenders, um Fehleingaben und Fehlaktionen zu vermeiden. Letztendlich ist die gesamte Schicht mit den Geschäftsregeln in einer Mehrschichtenanwendung nichts anderes als diese Art von defensiver Programmierung, um die Datenbank vor dem Anwender zu schützen.

Der zweite Aspekt der defensiven Programmierung besteht in der Gestaltung des Codes. Paradebeispiel in Visual FoxPro Applikationen zur defensiven Programmierung ist:

Damit soll verhindert werden, daß die Prozedur einen Fehler erzeugt, wenn der Parameter nivht übergeben wurde. Damit wird die Applikation stabiler und fehlerfreier... STOP! Genau das Gegenteil ist der Fall. Wenn der Parameter nicht übergeben wurde, also ein Fehler vorliegt, dann beseitigt der obige Code den Fehler still und heimlich. Anstatt hemmungslos abzustürzen, hält sich die Anwendung noch so einigermaßen über Wasser und zeigt keine Anzeichen dafür, daß sie gerade einen Fehler überlebt hat: Defensive Programmierung versteckt Fehler!

Ist defensive Programmierung deswegen schlecht? Natürlich nicht, aber sie verfolgt ein anderes Ziel als Sie als Entwickler verfolgen. Bei jeder Anwendung gibt es grundsätzlich zwei Versionen: Die Debug- und die Release-Version. Letztere ist es, die der Anwender erhält, erste verwenden Sie zum Testen. In vielen Entwicklungsumgebungen können Sie zwischen den beiden Versionen problemlos hin- und herschalten und unterschiedliche Konfigurationen definieren. In Visual FoxPro beschränkt sich der Unterschied auf das Entfernen der Debug-Informationen. Dennoch sollten Sie sich die Unterschiede verdeutlichen, denn beide Versionen verfolgen äußerst unterschiedliche Ziele:

Die Release-Version einer Software sollte möglichst stabil laufen, mit allen Kräften verhindern, daß der Anwender Daten verliert oder falsche Ergebnisse erhält, sie sollte möglichst klein und schnell sein. Bei der Debug-Version dagegen ist weder Größe, noch Geschwindigkeit entscheidend. Und auch soll diese Version nicht stabil sein, sondern beim ersten Anzeichen eines Fehlers möglichst spektakulär ihre Arbeit einstellen. Sei es durch eine Fehlermeldung, durch einen Crash, durch einen ASSERT-Dialog, oder durch irgendeine andere Methode. Dabei soll sie so viele Informationen wie möglich liefern, etwa durch Ausführungs- und Fehlerprotokolle, Speicherauszüge, etc.

Die defensive Programmierung verfolgt nur ein einziges Ziel: Den Anwender vor Datenverlust und falschen Ergebnissen zu bewahren. Sie ist notwendig in der Release-Version, denn die Daten des Anwenders ist der einzige Sinn und Zweck für die meisten Applikationen. Defensive Programmierung kann aber kein Schutz gegen schlampige Programmierer, Schreibfaulheit oder mangelnde Tests sein.

Bugfreiheit oder Freiheit für die Bugs...

Fehler können wir während der gesamten Entwicklung machen: Das fängt bei Mißverständnissen während der ersten Unterhaltung mit dem Kunden an und endet bei falschen Installationsmedien. Für alle diese Bereiche gibt es zahlreiche gute Bücher, daher werde ich mich hier auf den Bereich der Codierung beschränken. Die ersten Fehler geschehen üblicherweise bereits bei der detaillierten Designphase, in der festgelegt wird, welche Parameter eine Methode oder Funktion entgegennimmt, und was sie zurückgibt.

"Diese Funktion liefert den Kreditbetrag des Kunden oder .F., wenn ein Fehler auftrat." Kommen Ihnen solche Beschreibungen bekannt vor? Viele Entwickler verwenden den Rückgabewert, um sowohl ein Ergebnis als auch einen Status zurückzumelden. Oftmals wird dabei ein Wert als Fehlercode verwendet, der zu dem damaligen Zeitpunkt kein gültiger Rückgabewert war, bzw. einen komplett anderen Datentyp hat. Was ist daran aber falsch? Wenn ein Parameter, eine Variable oder ein Rückgabewert mehr als ein Bedeutung hat, liegt in alle Regel ein Design-Problem vor. Zum Beispiel kann es sein, daß eine Methode in Wirklichkeit zwei verschiede Aufgaben erfüllt, die besser auf zwei Methoden aufgesplittet werden sollten. Dadurch wird die Methode klarer und einfacher verständlich. In der Regel bedeutet dies neben der einfacheren Wartbarkeit auch, daß diese Methode weniger Fehler enthält.

Die Verwendung eines Fehlercodes sollte immer die Alarmglocken schrillen lassen. Ein Fehlercode ist schnell definiert: "Die Methode gibt .T. zurück, wenn ... erfolgreich ausgeführt werden konnte, und .F., wenn bei der Ausführung ein Fehler auftrat". Aber die Folgen dieses Satzes sind weitreichend! Jede Funktion, die diese Methode aufruft, muß den Rückgabewert überprüfen und einen Fehler entsprechend behandeln. Im Regelfall bedeutet dies, daß der Fehler an die nächst höhere Ebene weitergereicht werden muß, denn erst auf der äußersten Ebene können Sie Fehlermeldungen an den Anwender ausgeben, andernfalls haben Sie schnell ein Programm entwickelt, daß eine Serie von Dialogboxen mit Fehlermeldungen hintereinander anzeigt. Aber ein solch einfacher Fehlercode bedeutet auch, daß Sie unter Umständen eine Menge Code schreiben müssen, um Aktionen rückgängig zu machen. Nehmen wir an, diese Methode sollte das Kreditlimit eines Kunden neu berechnen, nachdem eine Rechnung gebucht wurde. Die Rechnung wurde gebucht, aber das Kreditlimit konnte nicht berechnet werden. Folglich muß die Buchung ebenfalls rückgängig gemacht werden, oder aber Sie müssen das Programm so entwickeln, daß Sie auch ohne eine Ermittlung des Kreditlimits zu diesem Zeitpunkt weiterarbeiten können. Es bedeutet aber auch, daß Sie weitere Informationen sammeln müssen, wenn eine Methode schief geht, denn in der äußersten Ebene können Sie ohne diese Angaben nur feststellen: "Es ist ein Fehler aufgetreten - Weiter". Und sollten Sie gar für den Fehlercode einen anderen Datentyp verwenden, müssen Sie zuvor noch diesen Überprüfen.

In der Praxis werden wir aber feststellen, daß solche Fehlercode oftmals gar nicht überprüft werden, und wenn sie überprüft werden, dies nicht konsequent geschieht, und wenn es konsequent geschieht, dieser Code nicht richtig getestet wird. Dadurch öffnen wir Fehlern in der Applikation natürlich alle Türen. Es ist nur allzu verständlich, daß Methoden und Funktionen daher wenn möglich keinen Fehlercode zurückgeben sollten. Eine Methode sollte immer funktionieren, wenn sie gültige Werte erhält. Ungültige Werte dagegen sind nicht das Problem der aufgerufenen Funktion, sondern das Problem des Aufrufers. Wenn eine ungültige Kunden-ID übergeben wird, ist das Problem die aufrufende Funktion. Dort müssen wir nach der Ursache für die fehlerhafte ID suchen, nicht das Problem in der aufgerufenen Methode umgehen, indem wir dort einen Status zurückgeben.

Ebenso problematisch sind optionale Parameter. Die meisten optionalen Parameter haben nur zwei Ursachen: Entweder verwenden wir in den meisten Fällen einen bestimmten Wert und mit dem optionalen Parameter wollen wir uns oder dem Anwender dieser Funktion das Leben erleichtern. Oder aber die Funktion ist historisch gewachsen und neue Funktionalitäten steuern wir über optionale Parameter. Der erste Grund ist definitv kein gültiges Argument, wenn Ihnen fehlerfreie Applikationen wichtig sind. Und im zweiten Fall ist ein Refactoring oftmals angebracht. Unter Umständen macht es mehr Sinn, diese Methode in mehrere zu splitten. Eine strengere Handhabung optionaler Parameter hat viele Vorteile: Wenn eine Methode einen optionalen Parameter entgegennimmt und dieser nicht übergeben wird, woran liegt es: Wurde er schlichtweg vergessen, oder wurde bewußt kein Wert übergeben wohl wissend, daß dann der Default-Wert verwendet wird? Wir können das schlichtweg nicht automatisiert überprüfen, während es ein klarer Fehler wäre, wenn wir optionale Parameter nicht zuließen.

Je strenger wir bei der Gestaltung der Richtlinien für die Übergabe von Parametern sind, desto mehr Voraussetzungen können wir nennen, die erfüllt sein müssen, damit eine Methode einwandfrei funktioniert. Um diese Voraussetzung schriftlich festzuhalten, gibt es in Visual FoxPro einen Befehl: ASSERT. Dieser Befehl ist nur in der Entwicklungsversion von Visual FoxPro aktiv. Zur Laufzeit wird er wie ein Kommentar übersprungen und belegt nur Speicherplatz. Technisch gesehen ist dieser Befehl relativ simpel. Er wertet einen Ausdruck aus und wenn dieser .F. ist, dann wird entweder eine Standarddialogbox oder ein angegebener Text ausgegeben und die Option geboten, den Debugger zu aktivieren.

Wie können wir das alles nun so kombinieren, daß wir zur Entwicklungszeit die gewünschten Meldungen bekommen und zur Laufzeit die Anwendung stabil läuft? Der folgende Codeauschnitt verdeutlicht die Technik der kombinierten Programmierung:

Auf den ersten Blick, scheinen hier die Bedingungen unnützerweise doppelt überprüft zu werden. Das ist zwar prinzipiell richtig, aber die ASSERT-Befehle erfüllen eine ganz andere Aufgabe als der Codeblock darunter. Mit ASSERT werden die Bedingungen überprüft, die immer erfüllt werden müssen. Diese Befehle sind nur für die Testversion im VFP Interpreter von Bedeutung. DerCode darunter ist für den Fall, der eigentlich nicht eintreten kann, aber nach Murphy’s Gesetz gerade deswegen eintreten wird. Er wird in der Release-Version beim Kunden dafür verwendet, um das Programm stabil zu halten.

Wenn Sie nun eine solche Funktion aufrufen und keinen Parameter übergeben, bekommen Sie mehrere ASSERT-Dialoge. Die falsche Reaktion wäre es, nun die ASSERT-Befehle zu entfernen, weil die ASSERT-Dialoge Ihnen auf die Nerven gehen. Ein solcher Dialog deuetet immer auf einen Fehler hin. Entweder ist der ASSERT-Befehl fehlerhaft und sollte überarbeitet werden, oder aber, was meist wahrscheinlicher ist, der Programmaufruf ist falsch und sollte korrigiert werden.

Anfangs mag es unsinnig sein, so strikt darauf zu achten, daß alle Parameter korrekt übergeben werden, wo es doch so einfach ist, Default-Parameter zu verwenden und das im obigen Code sogar gemacht wird. Aber bedenken Sie dies: Wenn Sie die Stellen im Programm durch #IF...#ENDIF entsprechend klammern, können Sie nach umfangreichen Tests VFP anweisen, diesen Codeblock zur defensiven Programmierung nicht mehr in die Applikation zu kompilieren. Dadurch wird diese kleiner und schneller, weil ja nicht mehr ständig vollkommen unnötigerweise der Typ überprüft werden muß. Außerdem können Sie so die Validierung verbessern. Nehmen wir an, den Parameter tnIndex berechnen Sie durch eine etwas komplexere Formel, die eigentlich nur Werte zwischen eins und drei liefern sollte. Aber wenn ein beteiligtes Feld NULL ist, dann kommt plötzlich vier heraus. Was glauben Sie, wie schnell Sie diesen Fehler gefunden hätten, wenn die Prozedur diesen Wert klammheimlich in 1 geändert hätte?

Risiken

Würden Sie in ein Flugzeug steigen, wenn dieses von Windows 95 gesteuert würde? Vermutlich nicht, denn das Risiko ist schlichtweg zu groß. Genauso wie es riskante und weniger riskante Lösungen in allen Bereichen des Lebens gibt, gibt es auch bei der Entwicklung riskante Wege und weniger riskante Wege. Einige Implementierungen sind riskanter, als andere. Wenn Sie sich zu sehr auf undokumentiertes Verhalten von FoxPro, auf zu viele verschiedene Komponenten und Techniken, auf Grenzverhalten, etc. stützen, wird die Wahrscheinlichkeit größer, daß Sie einen Fehler einbauen. Wenn Sie versuchen, eine Prozedur zu optimieren und noch die letzte Nanosekunde herauszuholen, könnten Sie merken, daß es manchmal besser ist langsam die richtigen als schnell die falschen Ergebnisse zu bekommen.

Risiken bei der Wahl des richtigen Algorithmus, der richtigen Implementation lassen sich nicht vermeiden. Man sollte sich aber ihrer bewußt sein und sie richtig abwägen. Das gleiche gilt für die oft unterschätzten Sprachrisiken. Alle Befehle und Funktionen in Visual FoxPro haben ein Normalverhalten, daß sich oftmals in einem Satz zusammenfassen läßt. Aber gleichzeitig haben alle Befehle gewisse Risiken, die dazu führen, daß der Befehl eben nicht so ausgeführt wird, wie üblich. Zum Beispiel kann die Zeile

SKIP lnAnzahl

zu einem anderen als den gewünschten Ergebnis führen, wenn

Nahezu jeder Befehl in Visual FoxPro und jede Methode bzw. Ereignis haben solche Risiken. In Visual FoxPro stechen dabei die folgenden Risiken besonders hervor: Das String-Handling; hier ist auf SET EXACT, SET ANSI und SET COLLATE zu achten. Außerdem muß berücksichtigt werden, welche Operatoren wie arbeiten, wie zum Beispiel „=“ und „==“. Ebenso muß bei fast allen Datenbezogenen Befehlen darauf geachtet werden, daß implizite Updates bei Zeilenspeicherung und ohne Bufferung ausgeführt werden, die aus verschiedenen Gründen zu einem Fehler führen können. Gleiches gilt für Validierungsregeln und Eindeutigkeitsverletzungen eines Indizes. Bei Variablen und Feldern muß vor allem auf die Priorität der Feldnamen, sowie bei Variablen auf die Gültigkeitsbereiche geachtet werden. Schwierigkeiten können auch Wert- und Referenzübergaben (SET UDFARAM) und regionale Einstellungen (SET DATE, SET HOURS, etc.) machen.

Zusammenfassung

Um die Fehler in Ihren Anwendung zu reduzieren, sollten Sie die folgenden Punkte berücksichtigen:

FVerwenden Sie ein Versionskontrollsystem wie Visual SourceSafe

FBei den Schnittstellen zu Methoden definieren Sie möglichst exakt, welche Werte gültig sind, denn je weniger gültig ist, desto mehr können Sie ungültige Eingaben erkennen.

FVermeiden Sie Ausnahmebehandlungen, insbesondere Fehlercodes in Prozeduren. Jeder Fehlerstatus erfordert ein vielfaches an Quellcode zur Behandlung dieses Status.

FVerwenden Sie ASSERT, um alle Bedingungen zu verifizieren. ASSERT ist eine mächtige Möglichkeit in Visual FoxPro, mit denen VFP die Fehler für Sie findet.

FMachen Sie sich den Unterschied zwischen defensiver Programmierung und der Debugversion klar. Bestimmen Sie die Risiken aller VFP Befehle, Methoden und Funktionen sowie Risiken in den verwendeten Tools, um nicht plötzlich bei Grenzsituationen Fehler zu erhalten.