[an error occurred while processing this directive]

Einführung in die Zuordnung von Verantwortungen für die Prüfung von Paramtern im Rahmen der Lehre des Programmierens. [] (Parameterprüfung, Prüfung von Argumenten), Lektion, Seite 721248
http://www.purl.org/stefan_ram/pub/programmieren_verantwortungundschleifen_de ist die kanonische URI dieser Seite.
Stefan Ram

Verantwortung und Schleifen

Schleifen und Verzweigungen erlauben es uns, ein neues Verständnis dafür zu gewinnen, warum es die Verantwortung des Klienten  eines Dienstes ist, diesen mit Argumenten im Rahmen der Spezifikation zu versorgen.

Verzweigungen und Verantwortungen

Als einfaches Modell eines Dienstes dient hier eine Funktion, die einen Quotienten berechnen soll.

Spezifikation
Funktionsbeschreibung
Synopse 
ergebnis = quotient( dividend, divisor )
Beschreibung 
Die Funktion "quotient" berechnet den Quotienten 
aus dem ersten Argument als Dividend und dem zweiten 
Argument als Divisor, falls das zweite Argument nicht 
Null ist.

Durch die Spezifikation der Funktion "quotient" ist klar geregelt, daß es die Aufgabe des Klienten  ist, sicherzustellen, daß das zweite Argument nicht Null ist, denn die Spezifikation garantiert das erwünschte Ergebnis nur für diesen Fall. Über das Verhalten der Funktion für den Fall, daß das zweite Argument Null ist, wird keine Aussage gemacht. Das bedeutet, daß ein Klient, die Funktion nicht mit einem Wert von Null für das zweite Argument aufrufen sollte, weil dann eben alles mögliche passieren könnte.

Was wäre denn, wenn man „zur Sicherheit“ noch eine Prüfung des Divisors in die Funktion einbauen würde? Wenn sich dann zeigt, daß der Divisor Null ist, müßte die Funktion ja auch irgendetwas Sinnvolles machen. Es ist aber nicht klar, was  sie dann tun sollte. Das Programm einfach abzubrechen mag vielleicht während der Testphase richtig sein, aber im Einsatz sollte das Programm möglicherweise besser weiterlaufen und nur die laufende Rechnung abbrechen. Die Funktion zum Dividieren kann diese Entscheidung nicht treffen, da es nicht Aufgabe einer allgemeinen Dividier-Funktion sein kann, Entscheidungen über den Ablauf ihrer Klienten zu treffen. Sie hat auch nicht die nötigen Informationen für solch eine Entscheidung.

Es bleibt nur die Konsequenz, daß die Dividier-Funktionen im Falle eines unerlaubten Arguments den Klienten von der Diagnose in Kenntnis setzt. Hierzu kann sie einen Fehlerindikator  als weiteres Ergebnis zurückgeben. Der Klient kann diesen Fehlerindikator alsdann untersuchen und gegebenenfalls die richtigen Konsequenzen ziehen.

Fehlerindikator
 Klient 
.----------------------------------. 
| Aufruf der Divisionsfunktion | 
|----------------------------------| 
| Ist der Fehlerindikator gesetzt? | 
| Ja | Nein | 
|---------------------|------------| 
| Fehlerbehandlung | Ergebnis | 
| | kann | 
| | verwendet | 
| | werden | 
'----------------------------------'

Der Klient kann aber genausogut selber vor dem Aufruf testen, ob der Divisor nicht Null ist und die Divisionsfunktion nur dann aufrufen, wenn der Divisor nicht Null ist. Wenn der Klient dies tut, dann kann man den Test in der Funktion weglassen und spart so einen Test gegenüber der vorherigen Lösung ein.

Fehlerindikator
 Klient 
.----------------------------------. 
| Ist der Divisor Null? | 
| Ja | Nein | 
|---------------------|------------| 
| Sonderbehandlung | Aufruf der | 
| | Divisions- | 
| | funktion | 
'----------------------------------'

Hier könnte man einwenden, der Programmierer des Klienten könnte nicht sorgfältig genug sein und einen solchen Test versäumen. Dann ist die vorhergehende Lösung mit der Rückgabe eines Fehlerindikators aber auch keine Verbesserung, da es der Programmierer genausogut versäumen könnte, den Wert des Fehlerindikators zu untersuchen. (Das Werfen einer Ausnahme ist in den Sprachen, in denen dies möglich ist, eine Variante der Rückgabe eines Fehlerwertes, mit einer Störung der Kontrolle des Aufrufers, falls dieser nicht so programmiert wurde, daß er die Möglichkeit einer solchen Ausnahme berücksichtigt.)

Eine kleine Divisionsfunktion kann es eben nicht sicherstellen, daß ihr Klient sorgfältig programmiert wird. Das ist die Aufgabe des Programmierers des Klienten, nicht des Programmierers des Dienstes. Es kann nur zu Verwirrung führen, wenn der Dienst versuchen würde, etwas sicherzustellen, das er grundsätzlich nicht sicherstellen kann. Daher sollte er sich auf seine Aufgabe konzentrieren und nur den Quotienten unter der Annahme, daß das zweite Argument nicht Null sei, berechnen. Dabei kann er davon ausgehen, daß der Klient ihn gemäß der Spezifikation aufruft. Jedenfalls muß er sich nur für einen solchen Fall selber spezifikationsgetreu verhalten.

Man kann einen Dienst auch mit einem Fahrkartenautomaten vergleichen: Dieser wird so konstruiert, daß er beim Einwurf von Geld (korrekte Argumente) in den Geldeinwurfschlitz (Parameter) eine Fahrkarte ausgibt. Falls jemand statt dessen Öl (fehlerhaftes Argumente) in den Geldeinwurf gießt, so ist es nicht die Aufgabe des Automaten nun zu erkennen, daß es sich hierbei um Öl handelt und zu erraten, was er nun machen soll—das kann er auch gar nicht. Man wird dem Automatenhersteller auch keinen Vorwurf machen, wenn der Automat das nicht kann, da der Vertrag vom Hersteller des Automaten auch nicht verlangt hat, einen solchen Automaten zu liefern. Man erwartet statt dessen vernünftigerweise von dem Benutzer des Automaten, diesen richtig zu bedienen. So erwartet man auch vom Nutzer eines Dienstes innerhalb eines Programms, daß er den Dienst mit richtigen Argumenten aufruft. Es ist nämlich vergleichsweise einfacher für den Nutzer eines Dienstes, diesen spezifikationsgetreu zu nutzen, als es für den Hersteller eines Dienstes ist, diesen so herzustellen, daß er sich für alle möglichen Arten der Fehlbedienung in bestimmter kontrollierter Weise verhält.

Schleifen und Verantwortungen

Anhand einer Schleife kann man zeigen, daß es unter Umständen deutliche Laufzeitvorteile bringen kann, wenn man dem Klienten die Verantwortung für eine eventuell nötige vorherige Prüfung von Argumenten für den Aufruf eines Dienstes zuweist. Allgemein läßt sich dies verstehen, wenn man sich bewußt macht, daß der Klient mehr Informationen über die Herkunft der Argumente hat, mit denen er einen Dienst aufruft. Wenn er aufgrund dieser Informationen schon sicher sein kann, daß der Divisor nicht Null ist, dann kann er eine zusätzliche Prüfung ganz unterlassen. Der Dienst hat diese Informationen nicht und würde einen Wert dann unnötigerweise auch dann prüfen, wenn sowieso schon sichergestellt ist, daß dieser nicht Null ist. Besonders gut kann man dies im Falle der folgenden Schleife verstehen.

Ausgabe einer Quotiententabelle
.-------------------------------------------. 
| Lies den Divisor ein | 
|-------------------------------------------| 
| Der Dividend laufe von 0 bis 999999 | 
| .---------------------------------------| 
| | Ist der Divisor Null? | 
| | Ja | Nein | 
| |----|----------------------------------| 
| | | Ergebnis = | 
| | | Quotient( Dividend, Divisor ) | 
| | |----------------------------------| 
| | | Ausgabe Dividend, Ergebnis | 
'-------------------------------------------'

Diese Schleife wird eine Million Mal durchlaufen, wobei der Dividend Werte von Null bis 999999 annimmt. In der Schleife wird stets getestet, ob der Divisor nicht Null ist. Nur dann wird der Quotient berechnet und das Ergebnis mit dem veränderlichen Dividenden zusammen ausgegeben; andernfalls wird nichts gemacht. Da sich der Divisor aber während der Schleife nicht ändert, ist es möglich, ihn nur einmal vor der Schleife zu testen. Dadurch werden 999999 Tests eingespart und das Programm wird entsprechend schneller.

Ausgabe einer Quotiententabelle
.-------------------------------------------. 
| Lies den Divisor ein | 
|-------------------------------------------| 
| Ist der Divisor Null? | 
| Ja | Nein | 
|----|--------------------------------------| 
| | Der Dividend laufe von 0 bis 999999 | 
| | .----------------------------------| 
| | | Ergebnis = | 
| | | Quotient( Dividend, Divisor ) | 
| | |----------------------------------| 
| | | Ausgabe Dividend, Ergebnis | 
'-------------------------------------------'

In der Schleife ist nun sowieso sichergestellt, daß der Divisor nicht Null ist. Daher sind alle weiteren diesbezüglichen Tests Zeitverschwendung. Insbesondere wären auch solche Tests des Divisors, die der Dienst "Quotient" bei jedem Aufruf erledigt, hier nur Zeitverschwendung, weil der Klient ja sichergestellt hat, daß der Divisor nicht Null ist.

Auch dieses Beispiel macht es verständlich, daß es recht ineffizient ist, wenn ein Programmteil seine Argumente auf Gültigkeit testet.

Anhand dieses Beispiels wurde auch ein bekanntes Optimierungsverfahren dargestellt: nämlich Berechnungen und Prüfungen mit Ergebnissen, die bei jedem Schleifendurchlauf gleich sind, nur einmal vor der Schleife zu erledigen. Obwohl manche automatische Übersetzer und Optimierer solche Optimierungen manchmal erledigen, kann man ihrer manchmal nur dann sicher sein, wenn sie im Quelltext festgeschrieben werden.

Viele Standardfunktionen testen daher ihre Argumente nicht und auch ein Programmierer kann es sich angewöhnen, solche Tests zu unterlassen.

Zusammenhang zur Kohäsion

Ein Dienst, der seine Argumente immer prüft, erledigt zwei Aufgaben: Das Prüfen der Argumente und den eigentlichen Dienst. Oben wurde gezeigt, daß dies für den Klienten nachteilig ist, wenn dieser weiß, daß die Prüfung überflüssig ist. Der Klient kann den Dienst nicht ohne die unerwünschte Prüfung erhalten. Solche Kombinationsdienste haben also eine zu geringe Kohäsion.

Wenn ein Dienst aber keine  Prüfung erledigt und ein Klient diese Prüfung wünscht, kann er sich immer einen eigenen Dienst auf der Grundlage des vorgegebenen Dienstes bauen, der zuerst die gewünschte Prüfung erledigt und dann den prüfungslosen Dienst aufruft.

PQuotient
 Definition der Funktion PQuotient mit dem
Eingangsparameter Dividend (Zahl) und dem
Eingangsparameter Divisor (Zahl):
.-------------------------------------------.
| Ist der Divisor Null? |
| Ja | Nein |
|----------------------|--------------------|
| Fehlerbehandlung, je | Ergebnis = |
| nachdem, was hier | Quotient |
| sinnvoll ist. | ( Dividend, |
| | Divisor ) |
'-------------------------------------------'

Dann kann ein Klient immer seinen Dienst "PQuotient" anstelle des Dienstes "Quotient" aufrufen und dabei gleichzeitig weiterhin indirekt den Dienst "Quotient" nutzen, zu dem jetzt aber eine klientenspezifische Fehlerprüfung hinzugefügt wurde.

Eine Einheit (z.B. ein Unterprogramm) sollte also zunächst immer nur eine einzige  Aufgabe erledigen. Wenn zwei Aufgaben gemeinsam erledigt werden sollen, dann kann man diese darauf aufbauend aus solchen Einheiten kombinieren. Wenn ein Dienst in einer Einheit hingegen nur  in Kombination mit einem anderen Dienst verfügbar ist, dann kann man ihn niemals alleine verwenden, wenn man des anderen Dienstes gar nicht bedarf. So wäre es beispielsweise ärgerlich, wenn es nur „Salzpfeffer“ zu kaufen gibt, weil man manchmal auch Salz oder Pfeffer einzeln braucht. Hat man Salz oder Pfeffer aber einzeln, so kann man sie jederzeit leicht selber zu „Salzpfeffer“ kombinieren.

Testindikationen

Allerdings gibt es auch Fälle, in denen Tests innerhalb eines Unterprogramms angezeigt sind. Alle Daten, die von außerhalb des Programmes  kommen, sollten an den Stellen, wo sie eingelesen werden sorgfältig untersucht werden und alle Fehlersituationen, für die der Klient gar keine Möglichkeit hat, sie vorher abzufangen, sollten erkannt und behandelt werden. Hierbei ist es oft richtig, die Information über die Fehlersituation an den Klienten weiterzugeben, wenn ein Dienst solche von außen kommenden Daten einlesen und an seinen Klienten weitergeben soll.

Schließlich gibt es auch noch einen Umstand, der alle Tests rechtfertigt, einschließlich von Tests auf die Gültigkeit von Eingangsparametern, nämlich die Entwicklung  und das Testen  von Programmen. Hier kann man ruhig alles Mögliche doppelt und dreifach testen, da dies immer hilfreich zur Aufdeckung von Programmierfehlern ist. Wenn das Programm dann ausgetestet ist und ausgeliefert wird, dann sollten die überflüssigen Tests schließlich aus dem Programm entfernt werden. Wurden sie entsprechend gekennzeichnet, so kann dies automatisch geschehen, einige Programmiersprachen bieten hierfür bereits spezielle Hilfsmittel an.

Zusammenfassung

Klienten und Dienste müssen sich an die Spezifikationen halten. Es ist die Aufgabe des Klienten  sicherzustellen, daß Eingangsparameter eines Dienstes im spezifizierten Bereich liegen, weil der Klient  die Eingangsparameter festlegt. Der Dienst ist nicht dafür verantwortlich, sie zu prüfen. Würde der Dienst seine Eingangsparamter auch noch prüfen, würden sie unnötigerweise doppelt geprüft werden.

Seiteninformation und Impressum  |   Mitteilungsformular  |   "ram@zedat.fu-berlin.de" (ohne die Anführungszeichen) ist die Netzpostadresse von Stefan Ram.   |   Von der Stefan-Ram-Startseite ausgehend finden sich oft noch mehr Informationen zu Themen, die auf einer Seite angesprochen wurden. (Eine Verbindung zur Stefan-Ram-Startseite befindet sich ganz oben auf dieser Seite.)  |   Der Urheber dieses Textes ist Stefan Ram. Alle Rechte sind vorbehalten. Diese Seite ist eine Veröffentlichung von Stefan Ram. slrprd, PbclevtugFgrsnaEnz