Präventive Programmierung am Beispiel von Ruby

Winfried Mueller, www.reintechnisch.de, Start: 24.05.2005

Präventive Programmierung ist eine Geisteshaltung. Vielleicht kennen Sie Menschen, die bei allem, was ihnen begegnet, schon Katastrophen voraus ahnen. Steigen diese Menschen in ein Auto ein, denken sie schon über einen möglichen Unfall nach.

Manchmal kann so ein Charakterzug nervig sein, besonders wenn er zu extrem ist. Oft schützt er aber auch vor möglichen Gefahren. Und diese Angewohnheit kommt ja auch nicht von ungefähr - meist haben solche Menschen schon leidvolle Erfahrungen gemacht, als sie mal nicht genügend aufpassten.

Solch eine Fähigkeit in der Programmierung zu entwickeln, darum geht es bei der präventiven Programmierung. Alles, was schief gehen könnte, sollte man im Auge behalten. Und sich darin schulen, Strukturen zu erkennen, die potentiell gefährlich sind.

Meine erste Lektion darin lernte ich, als ich ein Zeiterfassungsterminal entwickelte. Ein Gerät, wo auf einem Eprom das Programm gespeichert wird. Dann wird es zugeschraubt und ausgeliefert. Wenn Software so verschnürt im Gerät sitzt und dieses bei vielen Kunden montiert wurde, wird es echt teuer und unangenehm, wenn in der Software Bugs sind.

Zu dieser Zeit begann für mich eine ganz andere Art, Software zu entwickeln. Ich fing an, die komplette Terminal-Software nochmal neu zu schreiben, damals in C aber schon ojektorientiert (mit einigen Handständen klappt das tatsächlich). Selbst ein Exception-Handling konnte ich da implementieren.

Meine Aufmerksamkeit bei der Programmierung war nun auf alles gerichtet, was schief gehen könnte. Um dann Maßnahmen zu treffen, wie ich solche Fehlerzustände erkenne und ggf. auch treffend behandle. Keine Frage - Programmierung wurde so ein ganzes Stück aufwändiger. Es entstand aber Code, der sehr robust war.

Oft ist der Fehlerbehandlungscode aufwändiger als das eigentliche Programm. Vernachlässigt man dies aber, funktioniert das Programm nur unter bestimmten Umständen.

Ein weiteres großes Problem ist die Fehlersuche. Das schlimmste dabei sind Fehler, die sich im Programm fortpflanzen. Die eigentliche Ursache führt zu einer Kettenreaktion, die irgendwo dann mal zu Tage tritt. Dabei kann sie eine Menge Daten zerstört oder Fehlfunktionen ausgelöst haben. Sowas kann eine Katastrophe sein, wenn man sich vorstellt, dass dadurch ein Industrieroboter völlig falsche Bewegungen macht oder eine CNC-Fräse quer durchs Werkstück fährt.

Inspiriert hat mich damals auch das Buch von Steve Maguire: "Nie wieder Bugs!". Er hat aufmerksam beobachtet, wie Bugs entstehen. Und bei jedem Bug, den er entdeckt hat, hat er sich gefragt: Was hätte ich tun können, damit ich den Fehler schneller gefunden hätte. Der Ansatz war also hier, effiziente Methoden zu finden, um Bugs zu vermeiden oder schnell aufzuspüren. Wer einmal eine Nacht lang einen komplexen Fehler gesucht hat, weiß, wie viel Zeit durch Bugs verloren gehen kann.

Im folgenden möchte ich ein Weblog führen, welches Ideen sammelt, wie man in Ruby präventiv programmiert.

Weblog

26.05.2005 :: Fehlerfälle absichtlich aufspüren

Mitunter kann man Code schreiben, der Fehlerfälle nicht meldet.

 
a = 2
case a
  when 1
    puts "Tue dies"
  when 2
    puts "Tue jenes"
end

Hier werte ich nur 1 oder 2 aus, weil ich erwarte, dass nur diese Werte kommen sollen. In so einem Fall sollte man sofort ein Assert in einen else Zweig einbauen. Damit man informiert wird, wenn der Code nicht das tut, was er sollte. Ein anderes typisches Beispiel ist:

 
a = 1
if a == 1 
  puts "Tue dies"
else
  x_assert unless a == 2
  puts "Tue jenes"
end

Man baut einen else Zweig, weil man weiß, dass ein Wert nur 1 oder 2 annehmen kann. Wenn nicht 1, dann muss es 2 sein. Dem ist aber mitunter nicht so und da ist es dann gut, wenn ein assert einen darüber informiert.

Mit dieser Methode macht man alle nicht ausgesprochenen Annahmen zu dokumentierten Annahmen, die auch getestet werden. Damit ist auch die wichtige Strategie genannt: Füge für jede Annahme, die du machst, die aber im Code nicht klar hervorgeht, ein assert ein.

Ich wette nämlich, dass es jede Menge Code gibt, der schlußendlich zwar zu dem Ergebnis führt, was man wollte (das hat man ja getestet). Innen verhält er sich aber oft gar nicht so, wie man sich das vorgestellt hat. Und so wirken diese eigenen falschen Vorstellungen bei der Programmentwicklung weiter und führen zu fehlerhaften Code. Oder der Code ist nicht robust und verhält sich fehlerhaft, wenn andere Eingaben gemacht werden.

Asserts sind ein Feedbacksystem, die einem immer wieder mitteilen, ob das, was man annimmt sich auch wirklich so verhält.

26.05.2005 :: Entwickler und Tester zugleich

Die Entwickler eines Programmes sind meist keine guten Tester. Die Geisteshaltung ist oft: "Hoffentlich geht alles gut.". Wenn ich dagegen ein fremdes Programm teste, ist meine Geisteshaltung oft: "Schaun wir mal, was wir da alles Fehlerhaftes entdecken." Jeder Bug ist eine kleine Freude wie beim Ostereier suchen. Aus diesem Grund ist es in der Softwarebranche häufig üblich, dass die Entwickler nicht gleichzeitig auch die Tester ihres Codes sind.

Wenn man präventiv programmiert, muss man oft eine gespaltene Persönlichkeit haben - zum einen der Entwickler sein, zum anderen Spaß dafür entwickeln, eigene Fehler aufzudecken. Oft, wenn ich ein Assert in den Code einbaue, fühle ich mich dann in der Rolle des Testers, der sich freut, wenn der dann auch wirklich auslöst. Sozusagen ein Teil in mir ist Entwickler und ein Teil rennt hinterher und sucht sich nette Fallen heraus, um sie mit Asserts zu präparieren. Dieser Teil zieht seine Befriedigung daraus, dass diese "Feuerwerke" auch ausgelöst werden. Natürlich nur dann, wenn der Enwickler in mir wirklich einen Fehler gemacht hat.

So entsteht ein interessanter Effekt. Der Entwickler in mir lernt, immer besser zu programmieren, weil es wie eine Art Wettbewerb ist. Und der Tester in mir lernt immer besser, Fallen zu erkennen. Dieser Wettbewerb führt zu besserem Code. Und es macht auch ein Stück Freude. Fehler erlebe ich so wie "Treffer versenkt!" beim Schiffe versenken spielen.

24.05.2005 :: assert-Wächter

In C gibt es das assert-Makro. Überall, wo man im Programm meint, ein bestimmter Zustand müsste dort existieren, kann man das mit assert prüfen. Fällt die Prüfung positiv aus, läuft das Programm weiter. Im anderen Fall beendet das Programm im Debug-Modus. Ist der Debugmodus ausgeschaltet, wird das assert-Makro i.d.R. deaktiviert. Asserts sind also vor allem etwas für die aktive Programmentwicklung und werden im Produktivcode entfernt.

Es gibt jedoch auch Fälle, wo man sie auch im Produktivcode drin lässt, dann aber z.B. lediglich eine Logzeile ins Logfile schreibt.

Weil asserts normal im Produktivcode nicht mehr wirksam sind, dürfen sie nicht zur echten Fehlerbehandlung missbraucht werden. Jede Fehler-behandlung und -Erkennung, die im Produktivsystem funktionieren muss, darf nicht auf asserts aufbauen. Auch dürfen asserts keine Variablen verändern, weil sich sonst das Programm anders verhält, wenn sie ausgeschaltet werden.

Eine einfache Möglichkeit für Asserts in Ruby wäre dies:

 
DEBUG = true
def x_assert( dsc="" )
  if DEBUG
    raise "Assert: #{dsc}"
  end
end

def Foo( a, b )
  x = a + b
  # Hier möchte ich in meinem Anwendungsfall nur Fixnums
  x_assert( "Fixnum erwartet." ) unless x.is_a?(Fixnum) 
end

Foo( 10.0, 20.5 )

Ich habe hier bewusst die Prüfung nicht in eine Methode gesetzt, weil das Laufzeiteinbußen hätte. In die Methode x_assert wird nur gesprungen, wenn ein Fehler auftritt. Die Geschwindigkeit der Ausführung wird also nur durch die Auswertung hinter dem unless gebremst.

Oft ist die Laufzeiteinbuße überhaupt kein Problem. Manchmal kann es aber zu extremen Veränderungen kommen, wenn eine Methode z.B. sehr häufig aufgerufen oder eine Schleife oft durchlaufen wird. Oder wenn man reguläre Ausdrücke für die assert-Prüfung hat, die ineffizient sind.

Meine Angewohnheit ist oft, eine Menge asserts mit dem Salzstreuer ins Programm zu verteilen. Immer dort, wo ich ahne, dass da was schief gehen könnte, packe ich es rein. Mit der Zeit bekommt man ein gutes Gefühl dafür, wo Dinge schief gehen könnten oder wo bestimmtes Verhalten erwartet wird. Und dann hauen diese Anker auch öfters mal rein und ersparen mir stundenlanges suchen. Ein kurzer Kommentar über dem assert kann zusätzlich helfen, das Problem später zu verstehen.

Asserts sind auch eine geniale Dokumentation. Wie oft sitzt man vor Programmen und versteht nicht recht, wie der Algorithmus funktioniert. Man nimmt an, dass an diesem Punkt nur jenes Verhalten auftreten dürfte, man weiß es aber nicht genau. Ein Assert sagt dann dort aus: "Der Entwickler war damals der Meinung, hier sollte nur dies oder jenes auftreten." Welch ein tolles Feedback, dass man die Sache verstanden hat oder jetzt klar sieht, wo das Programm von der eigentlichen Idee des Programmierers abweicht.

24.05.2005 :: Fehler so früh wie möglich aufspüren

Wer programmiert, wird auch Fehler machen. Oft ist es so, dass Fehler sich nicht dort bemerkbar machen, wo sie entstehen. Sie bleiben unentdeckt und setzen sich fort. Viel später, an einem ganz anderen Ort tauchen sie auf einmal auf, dringen sie an die Oberfläche. Das kann manchmal auch sehr spät sein - ich erinnere mich an ein Datensicherungs-System, bei dem ich erst Monate später merkte, dass meine kompletten Datensicherungen wg. eines abstrusen Netzwerkfehlers defekt waren. Da greift der alte Witz: "Das Backup funktionierte gut, nur mit dem Restore hatten wir Probleme."

Windows 95 hatte damals oft die Eigenschaft, Fehler zu verbergen. Anstatt also eine Fehlermeldung zu produzieren, wenn irgendwas mit der Registry oder sonstigen Systemressourcen nicht in Ordnung war, wurde der Fehler unter den Teppich gekehrt. Der Nutzer spürte dann ein paar Seiteneffekte - ein Icon was man anklickt, wo sich aber nichts tut. Oder andere Funktionalitäten, die sich auf einmal ganz merkwürdig verhielten.

Wenn Fehler auftreten, müssen sie so früh wie möglich erkannt und bearbeitet werden. Das ist ein wichtiger Grundsatz. So vermeidet man Folgefehler und ist auch beim Debugging schnell.

Ein schönes Beispiel, dieser Forderung zu folgen, ist die frühe Prüfung von Konsistenz. Sind Übergabeparameter vom korrekten Typ und Inhalt? Man lässt also potenziell Gefährliches oder Fehlerhaftes gar nicht erst tief ins System eindringen sondern baut möglichst früh Wächter auf.

Exceptionhandling schafft übrigens schonmal eine hervorragende Grundlage für frühe Fehlererkennung. Früher war es so, dass Funktionen einen Fehlerwert zurückgegeben haben, der dann überall im Programm ausgewertet werden musste. Weil das oft nervte und die Programmstruktur zerstörte, verzichteten Programmierer gerne darauf. So wurden fehlerhaft ausgeführte Programmteile nicht erkannt und das Programm fortgesetzt. Fehler wurden nicht oder erst irgendwann später erkannt.

In der Entwicklungsphase bietet es sich oft an, bei jedem fehlerhaften Zustand ein Programm sofort zu beenden. In den meisten Fällen kann so am wenigsten kaputt gehen und man kann den Fehler analysieren. Aufräumarbeiten vor Beendigung können in Ruby in einem ensure-Pfad gemacht werden.