Software: Programmentwicklung und Projektorganisation [2. durchges. Aufl. Reprint 2019] 9783110862232, 9783110078657


185 91 18MB

German Pages 260 [264] Year 1979

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Vorwort
Inhalt
Einführung
1. Programmentwicklung
2. Systementwicklung
A. Anhänge
Literatur
Sachregister
Recommend Papers

Software: Programmentwicklung und Projektorganisation [2. durchges. Aufl. Reprint 2019]
 9783110862232, 9783110078657

  • 0 0 0
  • Like this paper and download? You can publish your own PDF file online for free in a few minutes! Sign Up
File loading please wait...
Citation preview

de Gruyter Lehrbuch Schnupp/Floyd • Software

Peter Schnupp • Christiane Floyd

Software Programmentwicklung und Projektorganisation 2., durchgesehene Auflage

W DE

G Walter de Gruyter • Berlin • New York 1979

Dr. rer. nat. Peter Schnupp, Mitinhaber der Firma SOFTLAB (Softwarelabor für Systementwicklung und EDV-Anwendung) in München, Lehrbeauftragter für Systemprogrammierung an der Hochschule für Sozial- und Wirtschaftswissenschften in Linz Dr. phil. Christiane Floyd, Prof. für Softwaretechnik an der Technischen Universität Berlin Das Buch enthält 74 Abbildungen und 6 Tabellen.

CIP-Kurztitelauf nähme der Deutschen Bibliothek Schnupp, Peter Software: Programmentwicklung u. Projektorganisation/ Peter Schnupp; Christiane Floyd. - 2., durchges. Aufl. - Berlin, New York: de Gruyter, 1978. (de-Gruyter-Lehrbuch) ISBN 3-11-007865-1 NE: Floyd, Christiane.

© Copyright 1978 by Walter de Gruyter & Co., vormals G. J. Göschen'sche Verlagshandlung, J. Guttentag, Verlagsbuchhandlung Georg Reimer, Karl J. Trübner, Veit & Comp., Berlin 30. Alle Rechte, insbesondere das Recht der Vervielfältigung und Verbreitung sowie der Übersetzung, vorbehalten. Kein Teil des Werkes darf in irgendeiner Form (durch Photokopie, Mikrofilm oder ein anderes Verfahren) ohne schriftliche Genehmigung des Verlages reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden. Printed in Germany. Satz: IBM-Composer, Walter de Gruyter, Berlin. Druck: Karl Gerike, Berlin. Bindearbeiten: Lüderitz & Bauer Buchgewerbe GmbH, Berlin.

Vorwort

Dieses Buch entstand aus einer Vorlesung über den Entwurf von EDV-Systemen, sowie aus einer Seminarreihe über Moderne Softwaretechnologie. Dies war auch der Arbeitstitel während der Manuskripterstellung: es soll in die Methoden einfuhren, welche heute von Entwicklungszentren, wie EDV-Herstellern und Softwarehäusern, zur rationellen Herstellung großer Softwaresysteme eingesetzt werden. Ebenso wie die Computer müssen auch die auf ihnen ablaufenden Programme hochwertige Industrieprodukte sein. Diese aber können nur von Technikern und Ingenieuren entwickelt werden, geschulten Fachleuten, welche die Fortschritte der wissenschaftlichen Forschung in die tägliche Praxis zu übertragen verstehen. Die Grundlage hierfür bietet die Informatik. Die aus ihren Ergebnissen abgeleiteten Regeln zur systematischen Programmentwicklung werden oft als Strukturierte Programmierung propagiert. Auch dies wäre ein möglicher Titel für dieses Buch gewesen. Das Ziel der vorliegenden Darstellung ist eine Einführung in die „höhere Programmierung". Dies einerseits als Grundlage einer entsprechenden Vorlesung für Hochschul- und Fachschul-Studenten der Informatik und verwandter Fächer, andererseits aber auch für den in der praktischen Systementwicklung erfahrenen Programmierer, Systemplaner oder Projektleiter, welcher sich im Selbststudium über die neueren Entwicklungen in seinem Fachgebiet unterrichten will. Das jedem EDV-Autor geläufige Problem, sein Buch an eine Programmiersprache wie COBOL, PL/1, FORTRAN, ALGOL oder gar einen existierenden oder frei erfundenen Assembler binden zu müssen, wurde dadurch umgangen, daß neue Konzepte jeweils zugleich mit ihrer Repräsentation in „einer" Programmiersprache eingeführt werden. Es wird eine Sprache verwendet, die für jedes logische Strukturelement eines Programms genau eine, zumindest für ALGOL- oder PL/1-Programmierer leicht verständliche Darstellung bietet. Daß diese Sprache PASCAL heißt, wird dem Leser zwar verraten, braucht ihn aber nur dann zu interessieren, wenn er sich mit ihr näher beschäftigen will. Wie die einzelnen Strukturelemente in anderen Sprachen wiederzugeben sind, wird einem nicht ganz unerfahrenen Praktiker meist unmittelbar einsichtig sein. Für die häufigsten Programmiersprachen COBOL, FORTRAN und PL/1 werden im Text und in den Anhängen Hinweise gegeben.

6

Vorwort

Wenn dieses Buch wirklich sein Ziel erreicht, Methoden und Techniken der modernen Softwareplanung und Programmierung darzustellen, so ist dies vor allem auch ein Verdienst unserer Kollegen in SOFTLAB, welche die aus ihrer täglichen Entwicklungspraxis gewonnenen Erfahrungen und Kritiken beisteuerten, sowie von Frl. Helga Wittmann, die wegen der dadurch bedingten laufenden Änderungen und Verbesserungen das Manuskript mit unendlicher Geduld und Sorgfalt sicher zweimal geschrieben hat. Christiane Floyd Peter Schnupp

Inhalt

0. Einführung

11

1. Programmentwicklung 1.1 Die Aufgabe eines Programms 1.1.1 Maschinen und ihre Zustände 1.1.2 Benutzer-und Basismaschine

15 15 15 16

1.2 Die Aufgabenlösung durch "schrittweise Verfeinerung" 1.2.1 Beispiel: Häufigkeitszählung von Worten 1.2.2 Vorgehen bei der Problemlösung 1.2.3 Die Top Down-Entwicklung des Programms 1.2.4 Verifikation der Lösung

20 20 22 24 33

1.3 Ablaufstrukturen 1.3.1 Dynamischer Steuerfluß und statische Niederschrift 1.3.2 Die Strukturierung des Steuerflusses 1.3.3 Strukturblöcke 1.3.3.1 Logische Grundstrukturen 1.3.3.2 Elementarblöcke 1.3.3.3 Die Reihung 1.3.3.4 Die Auswahl 1.3.3.5 Die Wiederholung (Iteration) 1.3.4 Struktogramme 1.3.5 Rekursion

38 38 42 47 47 48 49 51 57 63 66

1.4 Datenstrukturen 1.4.1 Die Daten als Ausgangspunkt der Programmplanung 1.4.2 Attribute von Daten 1.4.3 Basistypen und Wertebereiche 1.4.4 Die Strukturierung von Daten 1.4.4.1 Der Unterschied zwischen Daten- und Ablaufstrukturen. . 1.4.4.2 Das record 1.4.4.3 Alternative Strukturen 1.4.4.4 Das array 1.4.5 Referenzen (Zeiger, Adreßvariablen)

72 72 78 80 88 88 89 93 96 96

1.5 Datenverwaltung 1.5.1 Daten im Arbeitsspeicher 1.5.1.1 Scope, Lebensdauer und Speicherklasse 1.5.1.2 Der Keller (Stack)

102 102 102 107

8

Inhalt

1.5.1.3 Dynamische Speicherverwaltung (heap storage) 1.5.2 Daten auf Externspeichern 1.5.2.1 Residenz, Organisation und Zugriffsmethode 1.5.2.2 Sequentielle Organisation 1.5.2.3 Direkte Organisation 1.5.3 Beispiel: ein einfaches Auskunftssystem 1.6 Virtuelle Maschinen 1.6.1 Die Visualisierung der Planung eines hierarchischen Systems . . 1.6.2 Primitivoperationen 1.6.3 Betriebsmittelverwaltung und-transformation 1.6.4 Beispiel zur hierarchischen Gliederung 2. Systementwicklung 2.1 Der Projektablauf 2.1.1 Die organisatorische Umgebung 2.1.2 Die Entwicklungsphasen

113 120 120 124 126 130 135 135 139 142 143 148 148 148 149

2.2 Die Spezifikation 2.2.1 Die Aufgabe einer Spezifikation 2.2.1.1 Das Benutzermodell und die Schnittstellenbeschreibung. . 2.2.1.2 Der Spezifikationsrahmen und die endgültige Spezifikation 2.2.2 Der Aufbau einer Spezifikation 2.2.2.1 Der Leserkreis und die Strukturforderungen 2.2.2.2 Die Hauptabschnitte einer Spezifikation 2.2.3 Formale Schreibregeln 2.2.3.1 Sinn der formalen Regeln 2.2.3.2 Die Abschnittsstrukturierung und die äußere Form des Inhaltsverzeichnisses 2.2.3.3 Anordnungslogik im Inhaltsverzeichnis und im laufenden Text 2.2.3.4 Präsens oder Futur? 2.2.3.5 Seiten-, Formel-und Abbildungsnumerierung 2.2.3.6 Übernahme fremder Texte 2.2.3.7 Einsatz von Kopiergeräten 2.2.4 Zusammenfassung der Grundregeln

152 152 152 155 156 156 158 162 162

2.3 Die 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6

167 167 168 172 184 188 190

Planung Abgrenzung und Definition der Planungsphase Modularisierung Konventionelle und hierarchische Planung Schnittstellen und Beziehungen zwischen Modulen Nicht-hierarchische Abhängigkeiten Fehlerbehandlung in hierarchischen Systemen

163 163 164 165 165 166 166

Inhalt

2.4 Die 2.4.1 2.4.2 2.4.3 2.4.4

9

Realisierung Die Top Down-Programmierung Kodierregeln Testmethodik Überprüfung und Verbesserung von Programmen

195 195 199 200 204

2.5 Die Dokumentation 2.5.1 Sinn und Entstehen der Dokumentation 2.5.2 Programmtext und Kommentare

205 205 206

2.6 Die Projektleitung 2.6.1 Das "klassische" Projektmanagement 2.6.1.1 Führungsprobleme bei Software-Projekten 2.6.1.2 Der Berichtsweg und die Kontrolle des Entwicklungsfortschritts 2.6.2 Das Chef-Programmierer-Team 2.6.2.1 Die Top Down-Programmierung und das Zustandsdiagramm einer Software-Komponente 2.6.2.2 Der Projektsekretär und die Team-Organisation 2.6.2.3 Freigabe und Abnahme einer Komponente 2.6.3 Die Projektbibliotheks-Verwaltung 2.6.3.1 "Projektsekretär" vs. "Projektverwalter" 2.6.3.2 Das Projektbibliotheks-Verwaltungssystem

208 208 208 213 216 216 220 224 225 225 228

A. Anhänge A.l Realisierung der Strukturblöcke in COBOL A.2 Realisierung der Strukturblöcke in FORTRAN A.3 Realisierung der Strukturblöcke in PL/1 A.4 PL/1-Version des Beispielprogramms "Häufigkeitszählung von Worten"

235 235 238 242

Literatur

251

Sachregister

257

248

„Aufregung ist kein Programm." Masaryk „. . . als Projektleiter bereite ich die Probleme auf, bis ich sie den Programmierern überlassen kann . . . " Aus einer Bewerbung

Einführung

Gute Programme sind Mangelware. Es gibt sie, aber sie fallen nicht auf. Den Alltag des EDV-Benutzers, sei er nun Rechenzentrumsleiter, Teilnehmer an einem Timesharingdienst, oder auch nur ein in die zivilisierte Gesellschaft eingebetteter Bürger, der täglich computergeschriebene Rechnungen, Mitteilungen, Mahnungen, Bescheide in seinem Briefkasten findet — diesen Alltag bestimmen viel stärker schlechte Programme und ihre Folgen. Zuweilen sind diese Folgen erheiternd, wie die mehrmalige Anmahnung eines offenstehenden Betrages von 0.00 DM. Meist jedoch sind sie lästig und teuer, wie die bis zu 25% der verbrauchten Rechenzeit ausmachenden Wiederholungsläufe falscher oder falsch bedienter Programme in kommerziellen Rechenzentren. Vielleicht sind sie gar katastrophal, wie der finanzielle Zusammenbruch einer amerikanischen Eisenbahngesellschaft auf Grund eines Fehlers in dem zur Überwachung des Waggonbestandes eingesetzten Datenbanksystem. Die Ausbildung eines Programmierers darf sich deshalb nicht auf das Erlernen einer oder mehrerer Programmiersprachen als Rohmaterial zur Montage von Programmen beschränken. Ihr Ziel muß die Mitteilung und Einübung von Techniken zur Produktion guter Programme sein. Diese Verfahren werden oft unter dem Schlagwort moderne Softwaretechnologie zusammengefaßt. Die moderne Softwaretechnologie bereitete übrigens einem ganzen Berufsbild ein jähes Ende: dem Programmierer als „Kodierer", der unverstandene Flußdiagramme weitgehend mechanisch in unverständliche Programme umsetzte. Deshalb und im Sinne von Dijkstra [DIJK72] soll in diesem Buch unter Programmierer jeder verstanden werden, der an der Konzeption, Planung und Realisierung eines Programms aktiven Anteil nimmt — auch der Systemplaner und Projektleiter, der Chefprogrammierer in der modernen Projektorganisation (vgl. Abschn. 2.6.2). Was ist überhaupt ein „gutes" Programm? Wie erkennt man seine Qualität beim Studium des Programmtextes? Vor wenigen Jahren noch galten als einzige Kriterien für die Güte eines Programmes seine Laufzeit- und Speichereffektivität.

12

Einführung

Deshalb dokumentierte ein Durchschnittsprogrammierer — von wenigen, schon damals skeptischen Ästheten abgesehen — sein Können durch ungewöhnliche Sprachkonstruktionen und Tricks, welche er in sein Programm hineinpackte. Wer etwa dynamisch zur Laufzeit Sprungbefehle generierte und wieder löschte, zeigte damit, daß er kein Anfänger mehr war, und als Meister suchte man sich dadurch auszuweisen, daß man die Bitmuster des Programmcodes gleichzeitig als Konstanten verwendete oder durch nicht allgemein bekannte Nebeneffekte eines Befehls einen anderen sparte. Für den Benutzer jedoch, der ein Programm an seinen Folgen und nicht an seinem Code mißt, machen Tricks es nicht besser, sondern allenfalls schlechter: Tricks haben nun einmal die Eigenheit, zuweilen nicht zu funktionieren — im Gegensatz zum Zirkus leidet unter einem derartigen Versagen in der EDV nicht der Artist, sondern der unschuldige Konsument. Deshalb konnte es nicht ausbleiben, daß sich mit dem wachsenden Unmut der EDV-Benutzer über die „Softwarekrise", die offenbar immer teurere Produktion und Wartung immer schlechterer Programme, ein benutzergerechterer Qualitätsmaßstab für Softwareprodukte durchsetzte. Heute gilt ein Programm als „gut", wenn es — benutzerfreundlich ist, d. h. nach einfachen Bedienungsmaßnahmen das tut, was der Benutzer möchte und erwartet, — fehlerfrei ist, d. h. seine.Aufgabe nicht nur im Normalfall, sondern immer, auch bei ungewöhnlichen Datenkombinationen oder Bedienungsmaßnahmen, erfüllt, — wartbar ist, d. h. auch von einem anderen Programmierer als seinem ursprünglichen Autor in endlicher Zeit verstanden und abgeändert werden kann (und dabei natürlich seine Fehlerfreiheit nicht verliert). Der Anteil der Wartung an den gesamten Softwarekosten wird meist unterschätzt. Eine britische Untersuchung in über 900 EDV-Installationen ergab einen durchschnittlichen Wartungsaufwand von 40% der gesamten Softwarekosten [ANON73a], Mit zunehmender Komplexität der Programme verschiebt sich dieses Verhältnis noch drastisch: die US-Air Force ermittelte Entwicklungskosten von 75 $ pro Befehl, während bei der Wartung zuweilen 4000 $ pro Befehl erreicht wurden [TRAI73]. Die Planungs- und Programmierverfahren der Softwaretechnologie lassen die oben genannten benutzerorientierten Qualitätsforderungen bereits in den Programmentwurf eingehen. Sie erlauben es, die Güte des entstandenen Softwareprodukts schon an Hand des Programmtextes zu beurteilen und damit seine Benutzerfreundlichkeit, Fehlerfreiheit und Wartbarkeit nicht erst aus der Einsatz-

Einführung

13

erfahrung zu verifizieren. Sie erzwingen zugleich den Verzicht auf alle liebgewordenen Kodiertricks. Vielleicht fiel dem Leser auf, daß wir die gern zitierte Softwarekrise als eine zunehmende Verteuerung und Qualitätsverschlechterung der Programmproduktion charakterisierten. Woher kommt die darin implizierte negative zeitliche Entwicklungstendenz? Sie geht parallel mit der wachsenden Größe der durchschnittlichen Programme. Das Betriebssystem einer Anlage der zweiten Generation hatte einige Tausend Anweisungen, eines der dritten Generation das zehn- bis hundertfache. Ähnlich ist das Verhältnis zwischen einem einfachen Assembler und einem Compiler für eine höhere, problemorientierte Sprache, oder das zwischen einem Personalstammband-Update und einem integrierten Personal-Informationssystem. Dieses Wachstum im Umfang der zu entwickelnden Programme führt nicht nur zu einer in der Regel überproportionalen Erhöhung des Entwicklungsaufwands und der Fehlerwahrscheinlichkeit. Es bedeutet auch, daß nicht mehr ein einziger Programmierer, sondern ein Team eingesetzt werden muß, um das gewünschte Produkt in vertretbarer Zeit zu erstellen. Ein Team aber muß untereinander kommunizieren, es muß sich auf einheitliche Regeln, Verfahren und Schnittstellen festlegen, und es muß geleitet werden. Dies bringt neue Fehlerquellen und neuen, zusätzlichen Personalaufwand. Damit werden Projektorganisations- und -managementverfahren einschließlich der Spezifikations- und Dokumentationsmethodik zu wesentlichen Teilen der Softwareentwicklungstechnik. Diese Verfahren sind aber noch schwerer lehrbar als Planungs- und Programmiertechniken. Saubere, fachlich einwandfreie Planung und Programmierung lassen sich an kleinen Übungsprogrammen vorführen und nachvollziehen - dies versuchen wir im ersten Teil des Buches. Die Schwierigkeiten des Projektmanagements und die zu ihrer Überwindung notwendigen Überlegungen und Verfahren können hingegen nur bei der tatsächlichen Realisierung eines größeren Projektes im Rahmen eines nicht nur aus einem Bearbeiter bestehenden Teams erlebt und erprobt werden. Das aber überschreitet meist die Möglichkeiten eines Programmierkurses und wohl immer die des Selbststudiums. Andererseits — ein erfolgreiches Arbeiten im Team setzt ein Verständnis seiner Organisation und seines Managements voraus. Und das nicht nur bei der Projektleitung, sondern auch bei den übrigen Mitarbeitern, soll es nicht dauernd zu unbeabsichtigten Mißverständnissen oder gar zur bewußten Sabotage der „Bürokratisierung" und des „Berichtswesens" kommen. Deshalb gehen die modernen Projektleitungs- und -dokumentationsverfahren von den allgemeinen Projektplanungs- und Programmiermethoden aus. Als Erweiterung und Ergänzung dieser Techniken können sie im Rahmen eines Kurses über

14

Einführung

Softwareentwicklungsmethoden zwar nicht erlebt, aber doch wenigstens plausibel gemacht und verstanden werden. Um zumindest die Grundideen und die Logik der Softwareentwicklung im Team und des Chefprogrammierer-Projektmanagements zu vermitteln, schien es den Verfassern am besten, den zu behandelnden Stoff in zwei Teile aufzugliedern. Sie werden hier, vielleicht etwas willkürlich, als — Programmentwicklung und — Systementwicklung bezeichnet. Diese Unterscheidung entspricht der von Parnas [PARNOO] eingeführten Unterscheidung zwischen — Solo-Programmierung,

der Programmierung eines bestimmten

Problems ausschließlich durch den Benutzer selbst, und — Kooperativer Programmierung, der Programmierung eines fremden Problems durch ein Programmiererteam. Für Parnas ist der Übergang von der Solo-Programmierung zur kooperativen Programmierung zugleich der Übergang von der „naiven" Vorgehensweise zum Software Engineering, der ingenieurmäßigen Entwicklung von Programmen in einer industriellen Umgebung. Unter Programmentwicklung sollen hier diejenigen Verfahren und Techniken zusammengefaßt werden, welche für jede, auch die kleinste, Programmieraufgabe angewandt werden sollten. Im Mittelpunkt stehen hier die strukturierte Programmierung, die schrittweise Verfeinerung und der Begriff der virtuellen Maschine. Die Systementwicklung stellt den Programmierer dann in die Umgebung eines Teams, von Auftraggebern und Benutzern, und in den stärker formalisierten Ablauf eines Produktionsprozesses, wie er in der industriellen Softwareentwicklung bei Hardware-Herstellern und Softwarehäusern üblich ist. War der Programmierer eines wissenschaftlich-technischen Anwendungsprogramms häufig noch sein eigener Auftraggeber und gleichzeitig auch der einzige Benutzer seines Programms, so ist dies jetzt nicht mehr der Fall. Durch die Erfordernisse der Projektleitung und -Überwachung erhalten die Methoden zur Spezifikation, (Grob-) Planung, (Programm-) Abnahme und Dokumentation eine Bedeutung, die sie bei der ad hoc-Entwicklung eines einfachen Anwendungsprogrammes noch nicht hatten. Sie ändern aber — und das ist das wesentliche Anliegen dieses Buchs — nichts an den im ersten Teil dargestellten Programmentwicklungsverfahren. Die Maßstäbe für „gute" und „schlechte" Programmierung bleiben die gleichen, ob es sich nun um ein kleines Anwendungsprogramm oder aber um einen Baustein eines großen Systems handelt.

1. Programmentwicklung 1.1 Die Aufgabe eines Programms 1.1.1 Maschinen und ihre Zustände Eine Rechenanlage ist eine Maschine. Phantasievolle Bezeichnungen wie „Elektronengehirn" versuchen zuweilen darüber hinwegzutäuschen. Beschäftigt man sich jedoch nicht nur literarisch mit der Datenverarbeitung, so sollte man dies nie vergessen. Als Maschine wird eine Rechenanlage durch einige Eigenschaften qualifiziert, welche sie mit allen anderen Maschinen des täglichen Lebens gemeinsam hat: — sie nimmt vom Benutzer über irgendwelche Einrichtungen, wie Hebel, Knöpfe, Tastaturen, Eingaben entgegen, — sie erfüllt entsprechend der jeweiligen Eingabe (solange sie funktioniert) eine Aufgabe und liefert ein Ergebnis, — die Bearbeitung der Aufgabe erfolgt (zumindest makroskopisch betrachtet) deterministisch, — auf Grund dieser deterministischen Bearbeitung der Aufgabe hat die Maschine zu jedem Zeitpunkt einen bestimmten Zustand. Mikroskopisch, bei genügend detaillierter Betrachtungsweise und einem hinreichend fein unterteilten Zeitmaßstab, braucht die Maschine nicht deterministisch zu arbeiten. In welcher Reihenfolge eine Kaffeemühle die einzelnen Kaffeebohnen zerkleinert, ist sicher zufallsbedingt. Auf der den Benutzer ausschließlich interessierenden Betrachtungsebene mit den drei wesentlichen Zuständen — abgeschaltet, — mahlend mit Kaffeebohnen im Vorratsbehälter, — laufend mit leerem Vorratsbehälter funktioniert sie jedoch deterministisch. Daß und unter welchen Umständen ein intern nicht deterministisch ablaufender Mechanismus trotzdem von außen gesehen deterministische Ergebnisse liefern kann, ist nicht Gegenstand dieses Buches. Modelle hierfür liefert die Theorie der Petri-Netze [DENN72, MISU73]. Die Maschine interpretiert die Eingabe. Ihr Folgezustand nach der Eingabe ist im allgemeinen abhängig von ihrem Zustand im Moment der Eingabe.

16

1. Programmentwicklung

Ob und in welcher Richtung der Motor eines Aufzugs anläuft, hängt davon ab, wo sich der Fahrkorb beim Drücken eines Rufknopfes befindet. Je mehr mögliche Zustände eine Maschine hat, umso komplexer ist ihre Funktionsweise und umso schwieriger ist ihre Konstruktion. Deshalb versucht man, den Gesamtoperationsumfang der Maschine aus einfacheren Elementaroperationen zusammenzusetzen. Bei einer Waschmaschine wären derartige Elementaroperationen etwa — Wassereinlauf, — Wasser abpumpen, — Wasser aufheizen, — Waschmittel einspülen, — Trommel bewegen, — Schleudern. Ais Kombinationen dieser Elementaroperationen können dann viele verschiedene, komplexe Waschabläufe auf einfache Weise durch die Wahl unterschiedlicher Programme aktiviert werden. Programme zur Steuerung der Aufgabenbearbeitung durch Maschinen sind also keine Besonderheit der elektronischen Datenverarbeitung. Der Vergleich mit der Programmsteuerung einer konventionellen Maschine wurde hier nicht grundlos herangezogen. Er demonstriert nämlich den Unterschied zwischen drei Grundbegriffen, die auch in der EDV oft verwechselt werden: — die Aufgabe ist definiert durch Eingaben (den Input) und soll als Ergebnis die Ausgabe (den Output) erzeugen, — der Ablauf des Lösungsvorgangs der Aufgabe ist ein Prozeß, dessen Fortschritt durch den zu jedem Zeitpunkt erreichten Zustand (State) definiert wird, — die Vorschrift für den Ablauf dieses Prozesses in Abhängigkeit von den jeweiligen Zuständen ist das Programm. 1.1.2 Benutzer- und Basismaschine Die korrekte Herstellung von Programmen für Rechenmaschinen, durch den einzelnen Benutzer oder durch ein Team als „industrielle" Tätigkeit bei Hardware-Herstellern oder Softwarehäusern, ist der Gegenstand dieses Buchs. Daß hierbei die exakte Definition der Aufgabe des Programms (Eingabe und Ausgabe) sowie die Darstellung und Abfrage der Zustandsvariablen während seines Ablaufs eine wichtige Rolle spielen werden, versteht sich nach dem vorher Gesagten von selbst.

1.1 Die Aufgabe eines Programms

17

Die Programmierung ist in der elektronischen Datenverarbeitung von zentraler Bedeutung, weil Rechenanlagen im Gegensatz zu allen üblichen Maschinen frei programmierbar sind. Bei anderen programmgesteuerten Maschinen werden die Programme dem Kunden vom Hersteller mitgeliefert. Ihr Aufbau und ihre physikalische Realisierung (als Schaltwalze, Lochstreifen, Verdrahtung . . . ) sind ihm meist völlig gleichgültig und unbekannt. Dagegen ist eine EDV-Anlage, so wie sie vom Hardware-Hersteller zur Verfügung gestellt wird, zuerst einmal völlig „nutzlos". Sie ist eine aus Speichern, Verarbeitungs- und Ein-/AusgabeEinheiten bestehende Basismaschine. Erst das meist vom Benutzer selbst zu schreibende oder von ihm irgendwie sonst zu beschaffende Programm transformiert sie in die Benutzermaschine, die seine Aufgabe lösen kann (Abb. 1.1.2-1).

Abb. 1.1.2-1. Die Transformationsaufgabe eines Programms

Es ist verständlich, daß diese „ n u r " durch ein Programm realisierte Maschine oft als „weniger wirklich" als die Basismaschine empfunden wird. Es hat sich deshalb eingebürgert, die Hardware als reale Maschine zu bezeichnen — im Gegensatz zu einer virtuellen („scheinbaren") Maschine, die erst durch Software, durch ein oder mehrere Programme, auf der realen simuliert wird. Für den außenstehenden Benutzer ist die virtuelle Benutzermaschine jedoch ebenso „real" wie die Hardware — er kann nicht unterscheiden, welche Funktionen unmittelbar von der Hardware oder erst

18

1. Programmentwicklung

mittelbar über ein Programm ausgeführt werden. Streng genommen ist sogar der Grundbefehlsvorrat einer modernen Rechenmaschine, etwa einer IBM /370, der einer „virtuellen" Maschine — er wird nämlich durch Mikroprogramme [HUSS70, SCHN72] aus einfacheren Operationen zusammengesetzt. Ein Programm kann somit als Realisierung einer (virtuellen) Maschine mit Hilfe der Funktionen, des Anweisungsvorrats, einer anderen angesehen werden. Der Vorteil der freien Programmierbarkeit ist, daß durch Auswechseln des Programms die gleiche Basismaschine in viele unterschiedliche Benutzermaschinen transformiert werden kann. Die Hardware soll möglichst leistungsfähig und universell verwendbar sein. Deshalb ist ihr Anweisungsvorrat in der Regel komplex und nicht gut auf die jeweils zu lösende konkrete Anwendungsaufgabe abgestimmt. Um dem Programmierer die Überbrückung dieses großen Abstands zwischen der realen Hardware und der gewünschten Benutzermaschine zu erleichtern, werden von Hardware-Herstellern und Softwarehäusern Programmsysteme zur Vergüfung gestellt, die als Basissoftware dem Anwendungsprogrammierer bereits eine problemnähere und bequemer zu handhabende (virtuelle) Maschine simulieren. Hierzu gibt es grundsätzlich zwei Methoden: Betriebssysteme [BRIN70, DIJK68b, HOAR72] und Interpreter für höhere Programmiersprachen realisieren diese „höheren" virtuellen Maschinen unmittelbar durch entsprechende Programme auf „tieferliegenden". Compiler [COCK70, GRIE71 ] hingegen übersetzen ein in einer höheren Programmiersprache (den Anweisungen einer virtuellen COBOL-, PL/1- oder FORTRAN-Maschine) geschriebenes Programm in eines für eine „tiefere" Basismaschine. Da es für einen Programmierer zur Lösung seiner Programmieraufgabe grundsätzlich belanglos sein sollte, wie seine Basismaschine realisiert ist, kann man sich für die Diskussion einer Programmieraufgabe auf den Standpunkt stellen, daß der Funktionsvorrat der jeweiligen Basismaschine genau durch die verwendete Programmiersprache gegeben ist. Für die Beispiele in diesem Buch wurde als Basismaschine, soweit nichts anderes gesagt wird, eine PASCAL Maschine gewählt. Diese interpretiere die Programmiersprache PASCAL [JENS74], PASCALProgramme werden im allgemeinen auch von Programmierern, die ALGOL oder PL/1 beherrschen, ohne große Mühe verstanden 1 . 1

Die nicht unmittelbar verständlichen Sprachkonstruktionen von PASCAL werden wir jeweils in Anmerkungen informell erklären. Für die formale Definition dieser Sprache sei auf die Literatur verwiesen 1WIRT70).

1.1 Die Aufgabe eines Programms

19

Die Aufgabe jedes Programms ist es also, eine Basismaschine in eine Benutzermaschine zu transformieren. Damit ist zwangsläufig die genaue Definition dieser beiden Maschinen zugleich die Definition der zu lösenden Programmieraufgabe. In der industriellen Softwareproduktion ist dies der wichtigste Teil der Spezifikation eines Programmprodukts. Dieses Dokument ist die verbindliche Grundlage für Auftragsvergabe, Planung und Implementierung und wird in Abschnitt 2.2 näher besprochen. Aber auch ohne daß von einem Auftraggeber oder einer Finanzierungsstelle eine formale Spezifikation gefordert wird, sollte keine Programmen t wicklung begonnen werden, solange nicht schriftlich zumindest die folgenden Angaben über die Aufgabenstellung niedergelegt sind:

Was ist die

Benutzermaschine?

— Welche Eingabedaten verarbeitet sie, und woher stammen diese (Lochkarten, Tastatureingaben, zu verarbeitende Magnetbänder. ..)? — Welche Ausgaben erzeugt sie (Druckerprotokolle, Sichtgeräteausgaben, Zeichnungen, neue oder geänderte Dateien . ..)? — Welche Funktionen bietet die Benutzermaschine, und wie werden sie angefordert (Steuerkarten, Kommandos von Terminals oder vom Bedienungsblattschreiber . ..)? — Wie soll sich die Benutzermaschine bei Fehlbedienung, Ausnahmebedingungen (z. B. Fehlen bestimmter Eingabedaten) oder falschen Eingaben verhalten?

Was ist die Basismaschine'1. - Welche Programmiersprache wird benutzt? - Welche Externspeicher stehen zur Verfügung? - Welche Ein-/Ausgabe-Peripherie wird verwendet? Je nachdem, ob die Basismaschine die „nackte" Hardware ist oder ob sie selbst bereits durch ein Betriebssystem und eine höhere Programmiersprache in eine virtuelle Maschine transformiert wurde, werden die Externspeicher und die Ein-/ Ausgabe-Peripherie entweder physikalische Geräte oder symbolisch benannte Dateien und Ein-/Ausgabe-Datenströme sein. In beiden Fällen ist jedoch eine genaue Festlegung ihrer Eigenschaften notwendig. Zugriffsmöglichkeiten (Lesen, Schreiben, Ändern), zugelassene Verarbeitungsreihenfolgen (direkt, sequentiell), Steueroperationen (Zeilenvorschub, Rücksetzen, Such Vorgänge), vorgesehene Satzformate, Zeichenvorräte sowie die zu erwartenden und vom Programm zu bearbeitenden Ausnahme- und Fehlerbedingungen sind Vorgaben, die bereits die Programmplanung wesentlich bestimmen.

20

1. Programmentwicklung

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung" 1.2.1 Beispiel: Häufigkeitszählung von Worten Eine übliche Formulierung einer Programmieraufgabe wäre etwa die folgende: 1.2.1/1 „Es soll ein PASCAL-Programm geschrieben werden, welches die Häufigkeit der einzelnen Worte in einem eingegebenen Text zählt und ausgibt." Das reicht als Grundlage für die tatsächliche Programmierung noch nicht aus. Immerhin — es ist eine bessere Basis für eine Programmentwicklung als das, was häufig in umfangreichen Dokumenten festgelegt wird. Viele Vorgaben enthalten nämlich zu viele Angaben darüber, wie etwas getan werden soll (Algorithmen, Flußdiagramme, Datenformate, interne Tabellen, Bitmuster. . . ) und zu wenig Information, was eigentlich die Aufgabe ist. Da ein Programm aber immer die Vorschrift für die Lösung einer Aufgabe durch die Rechenmaschine ist, sollte sich die Problemstellung auf das „was" beschränken: die Formulierung der jeweiligen Aufgabe mit ihrer Ein- und Ausgabe. Das „wie", die programmtechnische Realisierung, ist Sache des Programmierers. Man sollte ihm die Arbeit nicht dadurch erschweren, daß man ihn zwingt, aus einer vielleicht falschen oder unzweckmäßigen Beschreibung des „wie" erst auf das „was" zurückzuschließen. Die obige Problemstellung definiert zwar eindeutig und klar die zu lösende Aufgabe — sie ist das, was man in der industriellen Softwareentwicklung als Spezifikationsrahmen bezeichnet. Sie ist aber n.och zu allgemein: zu einer programmierungsreifen Vorgabe (einer Spezifikation) wird sie entsprechend Abschnitt 1.1.2 erst durch die genaue Festlegung der Benutzer- und Basismaschine mit ihren Einund Ausgaben, den Bedienungsmöglichkeiten und den Fehlerreaktionen. Wie weit würden Sie die folgende Spezifikation als vollständig und genau empfinden? Was ist die

Benutzermaschine?

— Ihre Eingabe besteht aus einem fortlaufenden Text. Dieser besteht aus Worten Worte werden durch jeweils einen oder mehrere Zwischenräume und/oder einen Punkt (Satzende) getrennt. Der Text ist auf 80 Zeichen langen Zeilen (Lochkarten) gespeichert. Das Zeilenende ist immer auch Wortende.

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

21

- Als Ausgabe soll eine Aufstellung aller im Text vorkommender Worte mit der Zahl der Vorkommen jedes Wortes im eingelesenen Text ausgedruckt werden. - Nach Laden und Starten des Programmes (der Benutzermaschine) soll ein Stapel von Textzeilen (Lochkarten) vom Lochkartenleser bis zum Ende eingelesen werden. Nach Ausdrucken der Ausgabeliste ist das Programm zu Ende (die Benutzermaschine schaltet ab). - Sonderfälle: Enthält der Eingabekartenstapel keine oder ausschließlich leere Karten, so ist auch die Ausgabeliste leer. Die Zahl der möglichen verschiedenen Worte im Text und die maximale Wortlänge sind durch zwei einstellbare Parameter listlength und wordlength beschränkt. Sobald listlength verschiedene Worte erkannt wurden, werden neue Worte nicht mehr bearbeitet. Worte mit mehr als wordlength Zeichen werden auf diese Länge gekürzt. Was ist die Basismaschinel — Die Basismaschine sei eine PASCAL-Maschine. — Die Karteneingabe sei eine sequentielle Datei mit dem Namen input. — Die Druckausgabe sei eine sequentielle Datei mit dem Name output. Für eine „gute" Spezifikation fehlen auch in dieser Aufgabenstellung noch eine Reihe notwendiger Angaben: Die Fehlerbehandlung durch die Benutzermaschine ist außerordentlich primitiv. So ist etwa jedes auf einer Lochkarte codierbare Zeichen (selbst wenn es vom Drucker nicht abdruckbar ist!) zulässiger Bestandteil eines Wortes, bis auf die (einzigen) Trennzeichen Zwischenraum und Punkt. „Alkoholgehalt=85%; Vorsicht:Gift!" wäre ein einziges Wort. Ferner ist keine Sortierung der Ausgabeliste verlangt. Für die praktische Anwendung wäre sicher eine Sortierung nach dem Alphabet oder nach der ermittelten Worthäufigkeit zweckmäßig. Suchen Sie andere Angaben in der Definition von Benutzer- und Basismaschine, die in der Grobformulierung der Aufgabe noch nicht vorhanden sind. Fällt Ihnen ein noch nicht definierter Fehler- oder Ausnahmefall auf? (Hinweis: Was geschieht mit den Punkten am Ende des Satzes „Langsam verschwand der Kahn im Mondlicht. . . " ? )

1. Programmentwicklung

22

1.2.2 Vorgehen bei der Problemlösung Die Programmieraufgabe besteht darin, die in Abschnitt 1.2.1 definierte Benutzerauf die Basismaschine abzubilden, d. h. ein PASCAL-Programm zu schreiben, das den auf Lochkarten eingegebenen Text zu einer Liste der in ihm enthaltenen Worte mit ihren Häufigkeiten aufbereitet. Dazu müssen aus den Basisanweisungen der PASCAL-Sprache höhere Programmeinheiten zusammengesetzt werden: komplexe Datenstrukturen, wie die aufzubereitende Liste mit Worten und Wortzählern, oder Anweisungsfolgen, wie etwa eine Wiederholungsschleife zur Ausgabe der (intern) erstellten Wortliste auf den Drucker nach Einlesen der letzten Eingabezeile. Die Planung, der Entwurf des Programms, und damit die eigentliche intellektuelle Aufgabe des Programmierers besteht in der Zerlegung der Gesamtaufgabe in derartige kleinere Unteraufgaben. Diese sollen als eigenständige logische Einheiten den Abstand zwischen dem Anweisungsvorrat der Benutzermaschine (hier nur die einzige Funktion „lese Eingabekartenstapel und drucke Liste") und der Basismaschine (hier der PASCAL-Maschine) überbrücken, Dieser Abstand ist umso größer, je unterschiedlicher die beiden Maschinen sind: eine Programmierung in Assembler erfordert deshalb mehr Anweisungen, mehr Zwischenstufen und damit auch mehr Planung als die Benutzung einer „bequemeren" höheren Sprache. Bei der Zerlegung in Unterkomponenten sind grundsätzlich zwei Vorgehensweisen möglich. Man kann entweder aus den Elementaranweisungen der Basismaschine höhere Programmeinheiten wie Prozeduren (Subroutinen) und Datenstrukturen entwickeln, aus diesen wieder höhere, und diesen Prozess solange fortsetzen, bis man (hoffentlich) bei der zu realisierenden Benutzermaschine „ankommt", bis sich also aus den höchsten so entwickelten Komponenten das gewünschte Programm einfach zusammensetzen läßt. Man kann aber auch versuchen, das zu lösende Problem in „tiefere" logische Einheiten aufzuteilen, diese wieder in tiefere, bis derartige Einheiten sich leicht in wenigen Anweisungen der Basismaschine formulieren lassen. Das sicher unpräzise Wort „ t i e f soll hier „näher an der Basismaschine" bedeuten — ein Unterproblem, welches mit weniger Anweisungen dieser Basismaschine formulierbar ist als das Ausgangsproblem. Die dabei implizierte Vorstellung der „oben" liegenden Benutzermaschine und der „darunter" liegenden Struktur von Programmkomponenten bis zur ganz „tiefen" Basismaschine ließ für die erste Methode den Ausdruck Bottom Up, für die zweite die Bezeichnung Top Down entstehen.

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

23

Die erste Vorgehensweise, der schrittweise Aufbau immer höherer Programmeinheiten von „unten" nach „oben", ist sicher die näherliegende, naturgemäßere, damit aber auch die naivere. Es ist die Methode, nach der ein Kind seinen Baukasten benutzt, der Primitive seine Hütte baut und der Radioamateur sein erstes Funkgerät zusammensetzt. Es ist deshalb auch das Verfahren, nach dem wohl jeder Anfangsprogrammierer, noch unsicher in der von ihm benutzten Programmiersprache sein erstes Programm „zusammenbastelt". Kein Ingenieur jedoch plant so eine Maschine, kein Architekt ein Haus und kein Komponist eine Symphonie. Der Fachmann unterscheidet sich vom unerfahrenen und ungeschulten Amateur dadurch, daß er mit einem Grobentwurf des Gesamtvorhabens anfängt und diesen schrittweise verfeinert. Dementsprechend wollen auch wir hier den Weg von der Benutzer- zur Basismaschine von „oben" nach „unten" zurücklegen. Diese Methode der schrittweisen Verfeinerung, der Top Down-Programmentwicklung, wurde von erfahrenen Programmierern schon seit langem intuitiv, aber oft inkonsequent durchgeführt. Als zweckmäßige heuristische Vorgehensweise zum Entwurf „besserer" Programme wurde sie erst Ende der 60er Jahre vor allem durch Dijkstra [DIJK69, DAHL72] und Wirth [WIRT71] propagiert. Ihre Einführung als verbindliche Standardmethode bei der industriellen Softwareentwicklung begann sich schließlich Anfang der 70er Jahre durch die Erfolge in einigen Großprojekten [MILL71] durchzusetzen. Diese Erfolge beruhen vor allem darauf, daß sich auf dem Top Down-Verfahren eine wesentlich bessere Methodik der Projektleitung und -Überwachung aufbauen läßt, die wir in Abschnitt 2 dieses Buches besprechen werden. Grundsätzlich läßt sich die Methode der schrittweisen Verfeinerung in jeder Programmiersprache anwenden. „Moderne" Programmiersprachen legen dieses Verfahren jedoch näher als ältere, wie etwa COBOL oder FORTRAN. Welche Eigenschaften sollte eine Programmiersprache haben, um eine bequeme Top Down-Entwicklung eines Programmes zu ermöglichen? Da schrittweise Verfeinerung bedeutet, daß — in der Regel wiederholt — ein „höheres" logisches Konzept in eine Reihe von „niedrigeren" aufgelöst werden muß, sollte die dadurch definierte Hierarchie von Anweisungen auch im Programmtext sichtbar sein. Dafür ist die Prozedurerklärung in PASCAL, ebenso wie in ALGOL oder PL/1, ein geeignetes sprachliches Mittel. Sie ermöglicht es, mehrere Anweisungen unter einem Namen zu einer Einheit zusammenzufassen. Eine logisch übergeordnete Prozedur kann sie nur über diesen Namen ansprechen. Innerhalb einer Prozedur können jeweils neue lokale Variable und Prozeduren deklariert werden. Sie gelten nur innerhalb dieser und der von ihr eingeschlosse-

24

1. Programmentwicklung

nen Prozeduren, nicht aber außerhalb. Eine derartige Prozedur kann als weitgehend unabhängig von ihrer Umgebung betrachtet werden. Sie ist dann nicht darauf angewiesen, daß höhere Prozeduren ihr die benötigten ArbeitsdatenBereiche als globale Datenbereiche zur Verfügung stellen. Wie weit erfüllen die folgenden Sprachelemente älterer Sprachen die obigen Forderungen: - die Kapitel- (SECTION-) Einteilung in COBOL [IBMOOd], - das Unterprogramm (SUBROUTINE) in FORTRAN [USAS66], - der Kontrollabschnitt (CSECT) in BAL (/360 oder 4004-Assembler) [STRU71]? Nicht nur (ausführbare) Anweisungen der Programmiersprache sollen hierarchisch strukturierbar sein, sondern auch Datenbereiche. Aus den Basis-Datentypen, wie sie die meisten Programmiersprachen bereitstellen (z. B. integer, real, Boolean), sollen dem jeweiligen Problem oder Unterproblem angepaßte höhere Datenstrukturen (records) zusammengestellt werden können. Und ähnlich wie eine Prozedur erklärt, mit einem Namen versehen und an der für ihre Ausführung vorgesehenen Stelle im Code durch Aufruf ihres Namens vertreten werden kann, so soll auch einer Datenstruktur ein Name gegeben werden können. Eine derartige Typendeklaration macht sie dann zu einem höheren Datentyp, auf den sich der Programmierer bei der Deklaration von Variablen wie auf einen Basisdatentyp über den vereinbarten Namen beziehen kann. Die aus COBOL [IBMOOd] und PL/1 [IBMOOe, WEIN70a] bekannte Datenstrukturierung über ¿ei>e/-Nummern ist eine derartige Strukturierungsmöglichkeit. Diese Sprachen erlauben jedoch nicht das Benennen derartiger Strukturen durch Typen-Namen. Wie weit simulieren das DEFINED-Attribut in PL/1 und die DummySection (DSECT) in BAL [STRU71 ] derartige Typendeklarationen? PASCAL ist eine der wenigen Sprachen, welche auch zur Definition von höheren Datentypen sprachliche Mittel bereitstellen. Die Programmierung der Aufgabe 1.2.1/1 in PASCAL soll dafür als einfaches Beispiel dienen. Erst dann soll darauf eingegangen werden, welche Strukturierungsmittel für Programme bevorzugt oder ausschließlich verwendet werden sollten und wie sie in den üblichen Programmiersprachen realisierbar sind. 1.2.3 Die Top Down-Entwicklung des Programms Wir beginnen die Programmplanung am besten mit der Festlegung der Datenstrukturen der Benutzermaschine. Laut Abschnitt 1.2.1 sind dies die (Loch-

25

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

karten-)Zeile {Card) von 80 Zeichen, das Wort (Word) von maximal wordlength Zeilen und der Listeneintrag (Listentry), der eine zusammengesetzte Datenstruktur (record) aus je einem Wort und einem ganzzahligen Zähler (counter) für die zu ermittelnde Worthäufigkeit ist. Die Typenerklärungen für diese Datenstrukturen in PASCAL sind 1.2.3/1 type

Card = array [1 . . cardlength] of char; Word = array [1 . . wordlength] of char; Listentry = record word : Word; counter : integer end;

char, ein Zeichen, und integer, eine ganze Zahl, sind hierbei Basis-Datentypen von PASCAL. Der obere Index der beiden arrays wurde jeweils symbolisch durch cardlength bzw. wordlength benannt. Durch vorangestellte Konstantenerklärungen, z. B. const cardlength = 80; wordlength = 20; kann diesen symbolischen Bezeichnungen ein konkreter Wert zugewiesen werden. Was ist der Vorteil einer derartigen symbolischen Benennung von Konstanten? Kennen Sie andere Programmiersprachen, die diese Möglichkeit bieten (Hinweis: EQU-Anweisung in BAL [STRU71])? Wie weit sind DATA in FORTRAN [USAS66], VALUE in COBOL [IBMOOd] und INIT in PL/1 [WEIN70a] ein Ersatz hierfür? In der record-Erklärung für die Datenstruktur Listentry wurde der vorher erklärte Typ Word verwendet. Eine Änderung des Datentyps Word, z. B. durch eine Änderung der maximalen Zeichenzahl wordlength, würde damit automatisch auch für Listentry wirksam. Mit den so definierten Datentypen können nun diejenigen Variablen erklärt werden, die globalen Charakter für das Programm haben. Dies sind sicher die Ein- oder Ausgaben der Benutzermaschine, laut Abschnitt 1.2.1 also die sequentielle Eingabe-Datei input aus Zeilen {Card) und eine sequentielle Ausgabe-Datei output von Listentry-Daten für die Druckausgabe {print).

26

1. Programmentwicklung

Ferner benötigen wir eine (intern a u f z u b a u e n d e ) Wortliste list, die maximal listlength Worte mit ihren Häufigkeitszählern fassen kann u n d am Ende ausgedruckt werden soll, sowie einen Zähler entrycounter, der zu j e d e m Z e i t p u n k t angibt, wie viele Listentry-Einträge diese Liste bereits enthält. Die Erklärung dieser globalen Variablen lautet in PASCAL 1.2.3/2 2 var

input [in] o u t p u t [print] list : array [1 entrycounter

: file of Card; : file of Listentry; . . listlength] of Listentry; : integer;

Die mit d e m A t t r i b u t print erklärte Datei Output ist eine Druckausgabe-Datei von Listentry-Einträgen3. Deklaration 1.2.3/2 definiert list als eine Datei von Listentry-Daten. G e m ä ß 1.2.3/2 sind dies selbst keine Basis-Datentypen, sondern aus Feldern mit den N a m e n word u n d counter zusammengesetzte records 4 . Nachdem die D a t e n auf oberster Ebene definiert sind, ist die grobe Formulierung des Algorithmus nahezu trivial. Es m u ß — zuerst sicher eine Initialisierung stattfinden, — dann folgt eine Inputverarbeitung — und schließlich ein Listedrucken 1.2.2/3

begin Initialisierung; Inputverarbeitung; Listedrucken

end. 2

3

4

file erklärt eine sequentielle Datei von Daten des angegebenen Typs, [in] bedeutet, daß die Datei input eine Eingabedatei ist. Mit der PASCAL-Anweisung get (input) kann diese auf das nächste Eingabedatum vom Typ Card vorgesetzt werden: die jeweils aktuelle, gerade gelesene Kartenzeüe wird in den auf sie bezugnehmenden Anweisungen mit input t bezeichnet. Zu jeder Eingabe-Datei sieht PASCAL eine Boole'sche Variable eof vor, welche angibt, ob das Ende der betreffenden Eingabedatei erreicht wurde. Eine Boole'sche Variable kann die Werte false und true haben. Mit put (Output) kann jeweils die aktuelle Zeile ausgeschrieben und die nächste initialisiert werden. Diese noch nicht gedruckte Zeile wird - entsprechend der Notation bei Eingabedateien - durch Output t bezeichnet. In PASCAL wird ein Feld in einem record dadurch angegeben, daß der Feldname, durch einen Punkt abgetrennt, hinter den record-Namen geschrieben wird. Ist also etwa z als record x : integer,y : Boolean end deklariert, so bedeutet z.x das (integer-) Feld x im record z.

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

27

Initialisierung, Inputverarbeitung und Listedrucken sind hier Aufrufe von Prozeduren 5 . Die schrittweise Verfeinerung besteht darin, daß zuerst symbolische Namen für die (u. U. komplexen) Aktionen eingesetzt werden und diese dann in den folgenden Entwurfsschritten selbst als Prozeduren formuliert werden. Dies wird solange wiederholt, bis die „tiefsten" Prozeduren keine Prozeduraufrufe mehr, sondern nur noch Anweisungen der Basissprache enthalten. Wenn alle Prozeduren formuliert sind, kann entschieden werden, ob sie im endgültigen Programm auch als Prozeduren belassen werden sollen. Sie müssen dann jeweils zwischen den Variablendeklarationen (var . . . ) und den ausführbaren Anweisungen (begin . . . end) des Programms als procedura . . . erklärt werden. Kommen ihre Aufrufe (wie Initialisierung, Inputverarbeitung und Listedrucken) im Programm hingegen nur einmal vor, so wird man in der Regel aus Effektivitätsgründen ihren Text bei der endgültigen Fertigstellung des Programms anstelle des Aufrufes als inline-Code einfügen. 1.2.3/4 faßt die bis jetzt in 1.2.3/1 bis 1.2.3/3 geleistete Programmentwicklung zusammen: 1.2.3/4 program Häufigkeitszählung; {Konstanten-Erklärungen 6 } type Card = array [1 . . cardlength] of char; Word = array [1 . . wordlength] of char; Listentry = record word : Word; counter : integer end; var input [in] : file of Card; output [print] : file of Listentry; list : array [1 . . listlength] of Listentry; entrycounter : integer; {Prozedur-Erklärungen} begin Initialisierung; Inputverarbeitung; Listedrucken end. Fußnoten 5 und 6 folgen auf Seite 28.

1. Programmentwicklung

28

Als erste Verfeinerung dieser Lösung sind nun die Prozeduren Initialisierung, Inputverarbeitung und Listedrucken zu definieren. Die Initialisierung stellen wir zurück — erst am Ende der Programmentwicklung wissen wir genau, was alles initialisiert werden muß. Deshalb beginnen wir mit der Inputverarbeitung. 1.2.3/5 7 procedura Inputverarbeitung; var actualword : Word; begin Wortaufbereiten; while actualword eofword do begin Suche Listentry; Wortzählen; Wortaufbereiten end end; Die zu verarbeitende Eingabe besteht aus einer Folge von Worten. Es ist deshalb sicher zweckmäßig, einen Puffer actualword zu definieren. In ihm soll eine Prozedur Wortaufbereiten jeweils das nächste zu verarbeitende Wort abliefern, SucheListentry soll in der Wortliste List das Listentry für das betreffende Wort suchen und Wortzählen schließlich sein Auftreten dort vermerken. Wortaußereiten muß auf irgendeine Weise mitteilen, wann das letzte Wort der Eingabedaten übergeben ist. Die einfachste Methode hierfür ist die Übergabe eines besonderen, im Text sicher nicht vorkommenden Wortes eofword (z. B. ,@@@@l) im Puffer actualword. Sofern der Text auf der Datei input nicht ohnehin durch ein derartiges Ende-Wort abgeschlossen ist (Software-End ofFile') kann das Leseprogramm es bei Erkennen des eof(input) generieren. Die Prozedur Inputverarbeitung muß die Folge SucheListentry, Wortzählen und Wortaufbereiten bis zum Endekennzeichen eofword wiederholen. Um ein Wort im Zwischenspeicher actualword aufbauen zu können, muß die Prozedur Wortaufbereiten den Eingabetext zeichenweise interpretieren. Sie s

6

7

Ein Prozeduraufruf wird in PASCAL nicht durch „CALL . . . " oder ein ähnliches Schlüsselwort eingeleitet. Geschweifte Klammern „ { " und „ } ' fassen in PASCAL Kommentare ein. {.KonstantenErklärungen} und {Prozedur-Erklärungen} sollen daran erinnern, daß an den betreffenden Stellen die Erklärungen der Konstanten (cardlength,.. .) bzw. der Prozeduren (Initialisierung,. . .) eingesetzt werden müssen, sofern diese out of line belassen werden sollen. Die PASCAL-Anweisung while Bedingung do Anweisung ist eine Wiederholungsschleife. Sie verlangt, daß die Anweisung ausgeführt wird, solange die Bedingung, ein beliebiger logischer Ausdruck, den Wahrheitswert true hat. Die Bedingung wird vor Eintritt in die Schleife abgefragt.

29

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung

sollte deshalb mit einer Prozedur Getchar das jeweils nächste Zeichen in einem Puffer nextchar anfordern können. Ebenso wie das Dateiende durch ein vereinbartes Ende-Wort im Puffer actualword gemeldet wird, gibt Getchar das Auftreten des Zeilenendes (Kartenendes) durch ein spezielles Zeichen cardend in nextchar bekannt. Wortaufbereiten muß das Zeilenende kennen, da dieses laut Aufgabenstellung gleichzeitig Wortende ist. Sehen Sie einen Vorteil darin, ein spezielles Zeichen cardend einzuführen und nicht einfach das Kartenende durch einen Zwischenraum zu verkoden (was vom Standpunkt der zeitlichen Optimierung wegen des Einsparens einer Vergleichsoperation günstiger wäre)? Wortaufbereiten überliest zuerst ggf. vorhandene Trennzeichen (Zwischenraum, Punkt, cardend) und sammelt dann bis zum nächsten Trennzeichen maximal wordlength Zeichen des nächsten Wortes in actualword auf. Dazu verwendet es einen Zeichenzähler wordpos. F

1.2.3/6

procedure Wortaufbereiten; var wordpos, i : 1 . . wordlength; nextchar : char; begin {actualword ablöschen:} for i: = 1 to wordlength do actualword [i] := ' '; {Trennzeichen überlesen:} while (nextchar = ' ') v (nextchar = '.') v (nextchar=cardend) do Getchar; {Erstes Zeichen gefunden :} actualword [1] := nextchar; wordpos :=1; {Folgezeichen aufsammeln:} Getchar; while 1 ((nextchar = ' ') v (nextchar = '.') v (nextchar=cardend)) do begin wordpos :=wordpos + 1; if wordpos < wordlength then actualword [wordpos] := nextchar; Getchar; end end; Fußnote 8 folgt auf Seite 30.

30

1. Programmentwicklung

Die Prozedur Getchar soll die einzelnen Zeichen aus der Eingabezeile Card in nextchar übergeben. Das Zeilenende soll sie durch Übergabe des Zeichens cardend melden. Um dies tun zu können, muß sie die Position des jeweils letzten gelesenen Zeichens der Zeile in einer Variablen cardpos führen. Die einfachste Initialisierung des Gesamtprogramms ist es, cardpos auf cardlength +1 zu setzen. Dann hat beim ersten Aufruf Getchar „den Eindruck", daß eine (nicht vorhandene) vorausgegangene Karte vollständig verarbeitet ist und liest die nächste (in Wirklichkeit die erste!) ein. 1.2.3/7

procedure Getchar; begin if cardpos > cardlength then begin nextchar :=cardend; Getinput; cardpos :=1 end else begin nextchar:=inputt [cardpos]; cardpos :=cardpos + 1 end end;

Die Aufgabe von Getinput ist im wesentlichen das Einlesen einer neuen Zeile durchget(input). Allerdings hat diese Prozedur noch eine Zusatzaufgabe: sie muß den höheren Prozeduren das Ende der Datei melden. Dafür muß sie die End of File-Bedingung eof(input) in eine „Pseudo-Eingabezeile" umwandeln, welche lediglich das Endekennzeichen eofword enthält.

8

In logischen Ausdrücken bedeutet v das (inklusive) „oder", A „und" und 1 „nicht". Die bedingte Verzweigung if Bedingung then A nweisung i eise A nweisung2 fuhrt Anweisungi aus, wenn die Bedingung den Wahrheitswert true hat, andernfalls die Anweisung2• Ist, wie hier im Beispiel, keine alternative Anweisung2 für den Fall Bedingung = false vorhanden, so kann das eise entfallen.

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

31

1.2.3/8 9 procedure Getinput; begin get (input); if eof (input) then inputt := eofword end; SucheListentry durchsucht die interne Liste list nach dem Inhalt von actualword. Es kommuniziert mit der Prozedur Wortzählen über eine (globale) Variable listpos. In ihr wird der Index des dem Wort zugeteilten Eintrags Listentry in list übermittelt. Ist listpos < entrycounter, so ist das aktuelle Wort actualword bereits einmal vorgekommen und in der Liste list enthalten, andernfalls ist es ein neues Wort. 1.2.3/9 procedure SucheListentry; begin listpos :=1 ; while listpos < entrycounterA actualword # list [listpos].word do listpos := listpos + 1 end; Wortzählen muß das aktuelle Wort in der Liste vermerken. Ist listpos < entrycounter, so ist es dort bereits eingetragen, und es muß der Zähler list.counter um 1 erhöht werden. Andernfalls muß es neu in die Liste eingetragen werden. 1.2.3/10 procedure Wortzàhlen, begin if listpos < entrycounter then list [listpos].counter := list [listpos]. counter + 1 else if listpos < listlength then begin entrycounter : =listpos ; list [listpos].word:=actualword; list [listpos].counter:=l end end; Fußnote 9 folgt auf Seite 32.

32

1. Programmentwicklung

Damit ist die Inputverarbeitung beendet. Die Wortliste list muß nun lediglich noch durch die einfache Prozedur Listedrucken auf die Druckausgabe-Datei •Output übertragen werden. 1.2.3/11 1 0 procedure Listedrucken; var listpos:integer; begin forlistpos:=l to entrycounter do begin o u t p u t ! :=list [listpos]; put (output) end end; Schließlich fehlt nur noch die Initialisierung. Beim Durchmustern der Prozeduren Inputverarbeitung und Listedrucken sehen wir, daß die einzigen notwendigen Initialisierungsmaßnahmen das Nullsetzen des Listeneintrags-Zählers entrycounter sowie das Lesen der ersten Zeile von der Datei input sind. In den Eingabeprozeduren Wortaufbereiten (1.2.3/6) und Getchar (1.2.3/7) werden neue Leseoperationen dadurch angestoßen, daß das Zeilenende erreicht ist {nextchar=cardend, cardpos > cardlength). Dieser Zustand muß also durch die Initialisierung simuliert werden. 1.2.3/12 procedure Initialisierung; begin entrycounter := 0; nextchar := cardend; cardpos := cardlength + 1 end;

9

Die Zuweisung zur Eingabezeile inputt ist in PASCAL strenggenommen nicht erlaubt. Die konkrete Realisierung der Eingabe müßte deshalb über einen Zwischenpuffer erfolgen - dies ist ein Beispiel dafür, daß praktisch jede Programmiersprache in gewissen Fällen den Programmierer dazu zwingt, die von ihm gewünschte logische Funktion aufgrund von „physikalischen" Beschränkungen mit Hilfe von Hilfskonstruktionen zu erzwingen.

10

Die Zählschleife for Index =Anfangswerl to Endwert do Anweisung zählt einen Index vom Anfangswert bis zum Endwert in Einerschritten hoch und führt für jeden dieser Indexwerte einmal die Anweisung aus.

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

33

Damit ist die Top Down-Entwicklung des Programmes „Häufigkeitszählung von Worten" abgeschlossen. Als wesentliches Qualitätsmerkmal eines Programms wurde in der Einführung seine Wartbarkeit genannt. Überlegen Sie sich folgende Änderungen des Programms: — Es soll auch das Komma als Trennzeichen akzeptiert werden. — Das Zeilenende soll nicht gleichzeitig auch Wortende sein (kann in diesem Fall die Endemarkierung durch das eofword unverändert belassen werden?). — Aus Gründen der Laufzeitverkürzung soll eine effektivere Verwaltung der internen Liste (z. B. ein Hash-Verfahren [HOPG69, KNUT73, MAUR75]) benutzt werden. (Hinweis: diese Änderung ist verhältnismäßig aufwendig. Das liegt daran, daß mehrere Prozeduren unmittelbar auf die Liste zugreifen. In dem Abschnitt 1.4.1 und 2.3.3 werden dieser Planungsfehler und seine Vermeidung näher diskutiert). — Die Ausgabeliste soll nach dem Alphabet oder nach der Worthäufigkeit sortiert werden. — Es sollen Fehlermeldungen bei Sonderfällen (unzulässiges Eingabezeichen, Überlauf der internen Liste, leere Eingabe) ausgegeben werden. In Anhang A.4 ist eine PL/1-Version des vollständigen Programms ausgelistet. Sie wurde nahezu wörtlich übertragen, wobei lediglich einige Prozeduren als in lineCode unmittelbar eingesetzt wurden. Das Beispiel soll zeigen, daß — die Übertragung eines PASCAL-Programms in eine andere Programmiersprache nahezu mechanisch erfolgen kann, — der schrittweise verfeinerte Top Down-Entwurf jedoch weiterhin eine sprachund maschinenunabhängige Dokumentationsgrundlage der Programmlogik bleibt, — weil sich manches in dem PASCAL-Entwurf wesentlich knapper und trotzdem lesbarer ausdrücken ließ als selbst in einer an Anweisungsformen so reichen Programmiersprache wie PL/1.

1.2.4 Verifikation der Lösung Wieso ist ein durch schrittweise Verfeinerung systematisch entwickeltes Programm nun besser als ein konventionell „in einem Zug heruntergeschriebenes"? Neben der Benutzerfreundlichkeit und der Wartbarkeit war die Fehlerfreiheit bereits in der Einleitung als Kriterium für die Programmqualität genannt worden.

34

1. Programmentwicklung

Wie prüft man ein Programm auf Fehlerfreiheit? Der Programmtest, bei dem es mit einer Reihe von zufällig oder (besser) systematisch ausgewählten Daten auf der Rechenmaschine ausprobiert wird, ist das allgemein übliche, allerdings auch unbefriedigende Verfahren. Unbefriedigend ist es nicht nur wegen des Aufwands an teurer Rechenzeit, die dabei verbraucht wird - es hat vor allem auch den Nachteil, daß es den Nachweis der Fehlerfreiheit grundsätzlich nicht erbringen kann. Von Dijkstra stammt die inzwischen oft kolportierte Feststellung, daß Testen immer nur die Anwesenheit, nie aber die Abwesenheit von Fehlern beweisen kann. In vielen Programmiervorschriften wird deshalb die — sicher richtige — Empfehlung gegeben, sich, bevor man ein Programm auf der Maschine testet, so gut wie möglich durch einen „Schreibtischtest" von seiner Richtigkeit zu überzeugen. Wesentlich seltener wird aber gesagt, wie man eine derartige Verifikation des Programmes durchführt. Zuweilen wird vorgeschlagen, ein paar Testfälle mit Bleistift und Papier nachzuvollziehen. Ob dieses Vorgehen viel Sinn hat, kann bezweifelt werden — das Programm einfach ausprobieren kann die Rechenmaschine sicher schneller und ökonomischer. Außerdem glaubte der Programmierer ja, die von ihm geschriebenen Anweisungen würden genau das tun, was er wollte. Also wird er auch beim Ausprobieren mit hoher Wahrscheinlichkeit genau die gewünschten Aktionen vornehmen — ob die Anweisungsfolge nun in Wirklichkeit richtig ist oder falsch. Er wird also meist nur triviale Fehler finden, wie vergessene Anweisungen und Schreibfehler, nicht aber die viel gefährlicheren logischen Fehler. Deshalb wird seit einiger Zeit an der Entwicklung von Methoden zur formalen Beweisbarkeit der Richtigkeit eines Programmes gearbeitet [ELSP72, LINS72]. Die Grundidee dieser Verfahren ist, in Form von logischen Ausdrücken alle diejenigen Aussagen (assertions) bezüglich der Ausgabedaten eines Programms oder eines abgeschlossenen Programmabschnittes (z. B. einer Prozedur) aufzuschreiben, die bei richtigem Ablauf des Programmes gelten. Hinter jede einzelne Anweisung des Programmes werden entsprechend dem dynamischen Ablauf der Anweisungsfolge alle Aussagen notiert, die nach Ausführung der betreffenden Anweisung wahr sind. Jede Anweisung ändert in der Regel eine oder mehrere dieser Aussagen. Führt diese Folge logischer Aussagen genau auf diejenigen, welche am Ausgang des Programms gelten sollen, so ist die Richtigkeit des Programms bewiesen.

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

35

Für die praktische Programmierung ist diese Methode allerdings noch (und vielleicht für immer) zu schwierig, zeitaufwendig und schwerfällig. Sie soll deshalb nicht weiter besprochen werden. Trotzdem haben diese theoretischen Arbeiten aus zwei Gründen eine nicht zu übersehende Bedeutung auch im Rahmen dieses Buches. Erstens zeigte es sich, daß die ohnehin schon großen Schwierigkeiten bei dem Beweis der Richtigkeit von Programmen fast unüberwindlich werden, wenn die Programme nicht aus kleinen, überschaubaren Elementen zusammengesetzt sind, viele Querbeziehungen aufweisen und „unkontrollierte" Sprünge (GOTO-Anweisungen) enthalten.

Vor allem das letztere ist unmittelbar evident. An der Eingangsstelle einer Sprunganweisung gelten nicht mehr nur die logischen Aussagen hinter der statisch vorhergehenden Anweisung, sondern auch die in der Regel anderslautenden, die vor dem Sprung galten. Die Formulierung der richtigen Aussagen über die Daten an der Einsprungstelle ist deshalb oft praktisch unmöglich.

Die Arbeiten waren deshalb auch ein starker Antrieb zur Erforschung der Struktur von Programmen im allgemeinen sowie der empfehlenswerten und abzulehnenden Strukturelemente. Zum zweiten ist die bei derartigen Beweisversuchen eingesetzte Technik der Formulierung logischer Aussagen über Werte der Variablen am Ende eines Programms und nach den einzelnen Anweisungen, Blöcken und Prozeduraufrufen auch in informeller Form als Verifikationsverfahren einsetzbar. An Hand des im vorigen Abschnitt enthaltenen Beispielprogramms soll dies gezeigt werden. Die Prozeduren Initialisierung (1.2.3/12) und Listedrucken (1.2.3/11) sind trivial genug, daß ihre Richtigkeit unmittelbar evident ist. Zu verifizieren ist im Programm 1.2.3/4 also im wesentlichen die Prozedur Inputv erarbeitung (1.2.3/5). Vor dieser Prozedur herrscht durch die Initialisierung folgender Anfangszustand: 1.2.4/1 (1) Listenzähler entrycounter = 0. (2) Der Programmzustand entspricht dem nach Abarbeitung einer Eingabezeile : nextchar = cardend, cardpos = cardlength + 1.

36

1. Programmentwicklung

Am Ausgang der Prozedur soll gelten: 1.2.4/2 (1) Jede Zeile wurde vollständig ausgewertet. (2) Jedes Wort wurde erkannt und in die Liste list eingetragen. (3) Der Listenzähler entrycounter enthält die Zahl der gefüllten Listeneinträge und damit (solange die Liste nicht gefüllt ist) gleichzeitig die Zahl der verschiedenen Worte im Text. (4) Jeder gefüllte Listeneintrag list [ 1 ] bis list [entrycounter] enthält ein Wort und die Zahl seines Auftretens im Text. Die Richtigkeit der Aussage 1.2.4/2 (1) ist unmittelbar evident: die Prozedur Getchar (1.2.3/7) beginnt nach Einlesen jeder neuen Zeile durch Getinput die Übergabe der einzelnen Zeichen mit der Position cardpos=l und zählt diese in Einerschritten bis cardpos > cardlength hoch. Daß jede Zeile ausgewertet wurde, ergibt sich daraus, daß Getinput (1.2.3/8) bei Erkennen des eof(input), also nach Einlesen der letzten Zeile von der Datei input, einen „Pseudo-Endesatz" mit dem Endekennzeichen eofword simuliert. Erst das Auftauchen dieses Wortes in actualword veranlaßt die Beendigung der Wiederholungsschleife in der Inputverarbeitung (1.2.3/5), und zwar ohne das eofword noch in die Liste aufzunehmen. Überzeugen Sie sich, daß auch bei einer leeren Eingabedatei input das Programm korrekt abläuft. Die Aussage 1.2.4/2(2) erfordert zu ihrer Bestätigung die Überprüfung von drei Teilaussagen: 1.2.4/3 (2.1) Wortaußereiten übergibt jedes Wort aus jeder Eingabezeile korrekt im Puffer actualword. (2.2) SucheListentry findet entweder das Wort in der Liste oder aber einen leeren Platz darin. (2.3) Sofern der Platz leer war, trägt Wortzählen das neue Wort darin ein. Aussage 1.2.4/3 (2.1), die korrekte Übergabe jedes Wortes durch Wortaufbereiten, läßt sich an Hand von 1.2.3/6 überprüfen. Der Übergabepuffer actualword wird durch die Prozedur selbst auf Zwischenräume abgelöscht. Zu Beginn der Prozedur enthält nextchar immer ein Trennzeichen (einschließlich cardend). Alle Trennzeichen werden überlesen, und am Ausgang der ersten

1.2 Die Aufgabenlösung durch „schrittweise Verfeinerung"

37

while-Schleife enthält nextchar das erste Zeichen des nächsten Wortes (unabhängig von der Zahl der überlesenen Trennzeichen und auch im Fall leerer Zeilen). Das erste Wortzeichen wird in actualword [ 1 ] eingesetzt und dies im Zähler wordpos vermerkt. Anschließend wird das nächste Zeichen mit Getchar gelesen. Ist es bereits wieder ein Trennzeichen (einbuchstabiges Wort), so wird die nächste while-Schleife gar nicht betreten und die Prozedur mit dem richtigen Inhalt von actualword verlassen. Andernfalls setzt die while-Schleife die nächsten Zeichen in die jeweils nächsten Positionen von actualword ein, so lange das Wortende noch nicht erreicht ist. Die Schleife wird erst dann beendet, wenn in nextchar wieder ein Trennzeichen auftritt. Aufgrund des einfachen linearen Suchalgorithmus der Prozedur SucheListentry (1.2.3/9) ist Aussage 1.2.4/3(2.2) unmittelbar evident. Am Prozedurausgang gilt entweder — listpos < entrycounter und list [listpos].word enthält das in actualword stehende Wort, oder - listpos = entrycounter +1, und damit ist list [listpos] der erste leere Platz der Liste. In diesem zweiten Fall muß, um die Aussage 1.2.4/3(2.3) zu erfüllen, Wortzählen das neue Wort aus actualword in die Liste übertragen und durch entrycounter:= listpos den Platz als besetzt vermerken. Ein Blick auf die Prozedur 1.2.3/10 zeigt, daß dies der Fall ist. Überprüfen Sie auch die Grenzfälle der leeren Liste (entrycounter = 0) und der vollen Liste (entrycounter = listlength) Damit sind die drei Teilaussagen 1.2.4/3 und somit auch die Gesamtaussage 1.2.4/2(2) bestätigt. Aussage 1.2.4/2(3) ist wieder sehr einfach zu verifizieren. Der Listenzähler entrycounter wird von der Initialisierung (1.2.3/12) auf Null gesetzt. Bei jedem neuen, noch nicht in der Liste verzeichneten Wort steht listpos nach Abschluß der Prozedur SucheListentry (1.2.3/9) auf entrycounter + 1. Dann und nur dann wird — sofern das Listenende noch nicht erreicht ist — entrycounter durch Wortzählen (1.2.3/10) vor Füllen des Listeintrags um 1 erhöht. Damit zählt entrycounter die Zahl der gefüllten Listeneinträge. Die Bestätigung der Aussage 1.2.4/2 (4) läßt sich schließlich unmittelbar aus der Prozedur Wortzählen ablesen. Bei Füllen des Listeneintrags wird der Zähler list [listpos],counter auf 1 initialisiert. Jedesmal, wenn das gleiche Wort gefunden

38

1. Programmentwicklung

wurde, wird Wortzählen mit listpos < entrycounter angesprungen und so der zuständige Zähler list [listpos].counter um 1 erhöht. Damit ist der „Schreibtischtest" des Programms abgeschlossen. Der Vorteil einer derartigen Verifikation eines Programms gegenüber der herkömmlichen „Hand-Simulation der Rechenanlage" ist, daß sie — zu einer vollständigen Formulierung des gewünschten Endzustands nach Ablauf des Programms und — zu einer exakten Rechenschaft über die logische Korrektheit und Plazierung der zu seiner Realisierung beitragenden Programmschritte zwingt. Statt „Anweisung nach Anweisung" wird „Teil^iel nach Teilziel" überprüft. Der Programmierer beurteilt das zu verifizierende Programm aus einem neuen Aspekt. Dieser ist der Problemstellung und ihrer schrittweisen logischen Lösung sogar meist besser angepaßt als der sequentielle Programmtext, da in diesem oft die einzelnen Maßnahmen zum Erreichen eines logischen Ziels an völlig verschiedenen Stellen des Programms ergriffen werden. Eine Verifikation durch Beweisen von Einzelaussagen zwingt den Programmierer, für jede Aussage die wesentlichen Anweisungen im Programmtext aufzusammeln, und diese sowohl im Zusammenhang mit den übrigen teilziel-orientierten verarbeitenden Anweisungen als auch im Rahmen des Steuerflusses durch das Programm zu überprüfen.

1.3 Ablaufstrukturen 1.3.1 Dynamischer Steuerfluß und statische Niederschrift Wie dem Leser bei der Verifikation des Beispielprogramms (Abschnitt 1.2.4) vielleicht aufgefallen ist, hing die Überprüfung der Korrektheit des Programms davon ab, daß an bestimmten Stellen der (statischen) Niederschrift des Programms leicht Aussagen über Werte oder Wertebereiche der Variablen gemacht werden konnten, ohne viel Mühe auf das Nachvollziehen des (dynamischen) Steuerflusses, der Ablaufreihenfolge der Anweisungen, zu verwenden. Das wurde dadurch erreicht, daß das Programm keinen einzigen Sprungbefehl, kein goto enthält. Andernfalls hätten Aussagen über die Werte von Variablen hinter einem Sprungziel ein Verfolgen aller zu ihm führenden Wege durch das Programm verlangt. Anstelle der Sprungbefehle wurden in dem Beispielprogramm Sprachkonstruktionen wie i f . . . then . . . eise . . . und while . . . do . . . verwendet, welche den dynamischen Ablauf des Programms sofort aus der Struktur der statischen Niederschrift erkennen lassen.

1.3 Ablaufstrukturen

39

Dies ist der Grundgedanke der Strukturierten Programmierung. Dieser Begriff wurde erstmals von Dijkstra als Titel eines Aufsatzes [DIJK69] und später (zusammen mit Dahl und Hoare) als Buchtitel [DAHL72] verwendet. Inzwischen erschien eine Reihe von Schriften, welche diese Bezeichnung als Synonym für die jeweils behandelten Verfahren zur systematischen Programmentwicklung benutzen [BAKE72, BRIN72, DONA73, FLOY74, SCHU75]. Was allerdings genau unter dem zugkräftigen Titel „Strukturierte Programmierung" zu verstehen ist, wurde trotz einer Reihe von Veröffentlichungen darüber 11 nicht geklärt. Eine hübsche Definition aus einer dieser Veröffentlichungen lautet „Strukturierte Programmierung ist die Rückkehr zum gesunden Menschenverstand" (Denning). Deshalb wollen die Verfasser im folgenden den Begriff nicht mehr verwenden: er umfaßt als generelle „Philosophie" ungefähr alles, was im ersten Teil dieses Buches dargestellt wird. Eine der frühesten Publikationen zu diesem Thema war Dijkstra's „GOTO-Brief" [DIJK68], in dem er vor der Sprunganweisung, dem goto, als einer der häufigsten Fehlerquellen in der Programmierung warnte. Dementsprechend wird oft der völlige Verzicht auf ihre Verwendung (die GOTO toxe-Programmierung) als eine der Grundforderungen guter Programmierung angesehen. In vielen Sprachen, z. B. FORTRAN und üblichen Assemblern, können jedoch Verzweigungen des Steuerflusses nicht ohne Sprünge wiedergegeben werden. Ihre Vermeidung ist in diesen Sprachen also offensichtlich nicht durchführbar. Argumente gegen die völlig freie Verwendung des Sprungbefehls liefert übrigens nicht nur die Softwaretechnologie. Auch die moderne Hardware ist GOTO-feindlich. Auf früheren Rechnern gehörten die Sprungbefehle in der Regel zu den schnellsten Operationen und waren damit zu vernachlässigen — der „gekonnte" Einsatz von gotos galt sogar als Optimierungsmethode zur Verringerung des Laufzeit- und Speicherbedarfs. Die neueren Entwicklungen der Rechnertechnik, wie — Cache-Speicher, — Anweisung-Pipelines, 11

P. J. Denning, „Is it not time to define Structured programming'?", ACM Operating Systems Review 8,1 (Jan.74) p.6 M. V. Zelkowitz, „It is not time to define „Structured Programming", ACM Operating Systems Review 8,2 (April 1974) p. 7 C. R. Faulk, „Yet another attempt to define .Structured Programming'", ACM Operating Systems Review 8,3 (July 1974) p.4 P. J. Denning,, J s .Structured Programming' any longer the right term?", ACM Operating Systems Review 8,4 (Oct. 1974) p.4

40

1. Programmentwicklung

— Virtuelle Speicher, — Hardware-Stacks, — Überlappung der Befehlsdekodierung, — Assoziativregister zur schnellen Adressumrechnung haben jedoch zumindest die „weiten" Sprünge zu den zeitaufwendigsten Befehlen gemacht. Damit bringt ihre Vermeidung auf derartigen Maschinen auch einen nennenswerten Beitrag zur Laufzeiteffektivierung des Programms [HOAR75]. Ein Feldzug gegen die goto-Anweisung allein ist allerdings ebenso unsinnig wie jeder andere Glaubenskrieg [ABRA75, KNUT74]. Gute Programmierung entsteht nicht aus Dogmen, sondern aus dem Verständnis der Bedeutung, der Semantik jeder benutzten Anweisung und ihrer richtigen Verwendung im Programmganzen — dies gilt auch und besonders für Sprunganweisungen. Dieses Verständnis kann nur aus einem Nachdenken über die Arbeit des Programmierers und die bei ihrer Durchführung auftretenden Probleme erwachsen: Warum ist Programmieren schwierig? Ebenso wie bei der Fehlersuche und bei Änderungen in bereits vorliegenden Programmen müssen beim Programmieren im Grunde nicht eine, sondern zwei Aufgaben gelöst werden. Diese hängen zwar zusammen, haben aber logisch kaum etwas miteinander zu tun (Abb. 1.3.1-1). Der Programmierer muß eine — funktionelle

Lösung, ein Verfahren (Algorithmus), für das Problem erarbeiten

und diese als — technische Lösung, als Programm, in der gewählten Programmiersprache niederschreiben. Dem fertigen Programmtext sieht man aber nicht ohne weiteres an, welche Anweisungen oder Anweisungsteile — funktional, in der Logik der Aufgabenstellung und welche — technisch, in der Hard- und Software-Struktur der gegebenen Maschine und den Besonderheiten der gewählten Programmiersprache begründet sind. Bei den verarbeitenden Anweisungen, wie Wertzuweisung, Lese- und Schreiboperationen, ist dies im allgemeinen nicht sehr schwer zu erkennen. Vor allem in höheren Sprachen haben sie meist eine funktionale, im Rahmen des Algorithmus verständliche Bedeutung, und selbst wenn im Assembler ein Additionsbefehl zur Adressrechnung dient (und damit Teil der technischen Lösung ist), sieht man dies in der Regel sofort. Problematisch sind steuernden Anweisungen. Nicht alle sind der Logik der Aufgabenstellung angepaßt.

41

1.3 Ablauf strukturen Problem: (Zielvorstellung des Benutzers)

Aufgabenstellung

Verfahren: (Funktionelle Lösung)

Algorithmus

Programm: (Technische Lösung)

Maschine: (Programmiersprache, Funktionen der Hardware und des Betriebssystems)

Statische Niederschrift von dynamisch auszuführenden Anweisungen

Vorrat an verarbeitenden Anweisungen

Vorrat an steuernden Anweisungen

Abb. 1.3.1-1. Aufgabe des Programmierers

Zwar ist etwa die Bedeutung der Prozeduraufrufe (in den meisten Programmiersprachen eine CALL-Anweisung) unmittelbar verständlich: es wird — hoffentlich — ein logisch abgeschlossenes Unterproblem statisch ausgelagert. Der Verzicht auf das Schlüsselwort CALL in PASCAL macht diese Funktion des Prozeduraufrufs deutlich - der Prozedurname selbst wird als Kurzbezeichnung für eine neue verarbeitende Anweisung empfunden, die der Programmierer durch die Prozedurerklärung ad hoc entsprechend seinen Wünschen definiert. Eine ähnliche Funktion hat die Erweiterung der meisten Assembler durch vordefinierte und/oder durch den Programmierer zu schreibende Makros [KENT69].

42

1. Programmentwicklung

Einem Sprung, einem goto, sieht man hingegen nicht an, welche Funktion er erfüllt. Er kann das Ende einer Wiederholungsschleife, Einleitung oder Abschluß einer Fallunterscheidung, Ansprung der Behandlung einer Sonder- oder Fehlerbedingung und noch manches andere sein, und er kann auch eine rein technische Funktion haben: es wird irgendein Codeabschnitt angesprungen, weil er die im Moment notwendige Aufgabe erfüllt und „eben gerade an der angesprungenen Stelle steht". Während also bei den verarbeitenden Anweisungen in fast allen Fällen eine enge Korrespondenz zwischen problembezogener Funktion und technischer Realisierung besteht, ist dies bei Sprungbefehlen nicht der Fall. Deshalb ist in üblichen, konventionell geschriebenen Programmen das Verständnis und Nachvollziehen des dynamischen Steuerflusses an Hand der statischen Programmniederschrift so schwierig. Es besteht fast kein erkennbarer Zusammenhang zwischen der Problemlogik und ihrer Realisierung, weil aus den Sprungbefehlen ihre Bedeutung nicht hervorgeht. In einem nicht völlig undiszipliniert geschriebenen Programm ist dieser Zusammenhang jedoch sicher vorhanden: der Programmierer wußte vermutlich, warum er den Steuerfluß ändert. Er hatte nur kein sprachliches Mittel, die logische Bedeutung dieser Maßnahme im Rahmen seines Programms eindeutig auszudrücken. Sprachkonstruktionen wie i f . . . then . . . eise . . . und while . . . do . . . leisten genau das. Sie sind deshalb dem semantisch Undefinierten Sprungbefehlen überlegen. Deshalb sollte man, — wenn die benutzte Programmiersprache ausreichend sprachliche Mittel zur problembezogenen Steuerung des Kontrollflusses enthält, ausschließlich diese verwenden, und — wenn dies nicht der Fall ist, wenigstens beim Programmentwurf von derartigen Strukturen ausgehen, und diese dann unter Zuhilfenahme von Sprungbefehlen in einer standardisierten Weise in die benutzte Programmiersprache übersetzen (Beispiele hierfür bieten die Anhänge A.l bis 1.3). Welche verschiedenen Ablaufstrukturen müssen nun zur Verfügung stehen, um — jeden Algorithmus verständlich formulieren zu können, und andererseits nur — einen möglichst beschränkten Satz übersichtlicher Grundstrukturen und Verknüpfungsregeln erlernen zu müssen? 1.3.2 Die Strukturierung des Steuerflusses Vor der Festlegung von Standard-Ablaufstrukturen zur Planung und Programmierung des Steuerflusses lohnt sich die Überlegung, welche gemeinsamen Eigen-

1.3 Ablauf strukturen

43

schaften sie haben sollten, um die Nachteile des Sprungbefehls zu vermeiden. Seine Undefinierte logische Bedeutung war ja nicht der einzige Einwand, der gegen ihn vorgebracht werden konnte. Schwerwiegender ist, daß er die Verifikation und das Verständnis des Programms außerordentlich erschweren kann. In Abschnitt 1.2.4 wurde bereits daraufhingewiesen, daß dies immer dann der Fall ist, wenn viele verschiedene Wege durch das Programm verfolgt werden müssen, um die möglichen Werte von Variablen hinter einer Ansprungstelle zu überprüfen. Die Problemstellen sind also weniger die, an welchen der Steuerfluß durch eine Sprunganweisung umgelenkt wird, als diejenigen, wo er von mehreren Stellen aus wieder zusammenläuft. In vielen großen Programmen ist es kaum möglich festzustellen, von wo überall Sprünge auf eine bestimmte Anweisung führen können. Der Vorschlag, die GOTO-lose Programmierung einfach durch Ersatz der GOTO- durch eine COMEFROM-Anweisung zu erzwingen [CLAR73], hat deshalb wie jeder gute Scherz einen durchaus ernsten Hintergrund. Um den Überblick über die verschiedenen möglichen Wege des Steuerflusses wenigstens etwas zu erleichtern, ist deshalb bereits seit vielen Jahren das Flußdiagramm (auch Ablaufplan genannt) als graphische Darstellung der dynamischen Struktur eines Programms eingeführt und sogar durch DIN-Vorschrift 66001 genormt. Es wird deshalb leider oft als Mittel zur — Planung und — Dokumentation empfohlen oder sogar gefordert. Soweit für die Dokumentation ein Flußdiagramm-Generator zur Verfügung steht, der das Diagramm aus dem Programmtext automatisch erstellt, kostet dies nur etwas Maschinenzeit und ist weiter nicht schlimm. Als Planungsmittel ist das Flußdiagramm jedoch völlig ungeeignet. Es verführt den Programmierer aus mehreren Gründen, auf die wir zum Teil erst später eingehen können (vgl. Abschnitt 2.3.3), zu einer schlechten Programmplanung. Abb. 1.3.2-1 zeigt ein Flußdiagramm. Es ist unter Weglassung der Symbol-Beschriftung, die hier irrelevant ist, aus einer Publikation [DAV072] entnommen. Es dient dort als Beispiel für ein besonders gut strukturiertes Programm.

44

1. Programmentwicklung

Abb. 1.3.2-1. Nicht strukturiertes Flußdiagramm

Was die Strukturierung des Steuerflusses anlangt, ist es dies jedoch nur in Teilen. Der gestrichelt eingerahmte Bereich ist einwandfrei strukturiert: es wird eine Fallunterscheidung durchgeführt, und danach laufen die beiden Programmzweige wieder zusammen.

1.3 Ablaufstrukturen

45

In PASCAL wäre diese Konstruktion, wie auch in PL/1 oder COBOL, durch eine i f . . . then . . . else-Anweisung unmittelbar auszudrücken. Eine nächsthöhere, diese Fallunterscheidung umfassende Struktur wäre die dick eingezeichnete Wiederholungsschleife. Sie wäre es — sie ist es aber nicht. Der Punkt unmittelbar über der eingerahmten Fallunterscheidung ist nämlich außer über die Wiederholungsschleife noch auf mindestens sechs anderen Wegen zu erreichen. Diese bilden teilweise selbst Wiederholungsschleifen, welche die dick gezeichnete teilweise, aber nicht ganz, überlappen. Schätzen Sie ab, wie lange ein Programmierer beim Test für die Diagnose eines hinter diesem Einsprungpunkt erkannten Fehlers braucht, wenn man folgende Annahmen macht: — die Bestimmung der möglichen und sinnvollen Werte einer Variablen kostet ihn im Durchschnitt für jeden der sechs verschiedenen Steuerfl ußwege 1 Minute, — 50 Variable sind unter Umständen relevant und müssen überprüft werden. Ausgangspunkt der Kritik an der Sprunganweisung sind derartige, nur durch sie ermöglichte Mängel in der Programm-Strukturierung. Eine Struktur verlangt immer eine Ordnungsrelation, die angibt, ob ein Element in einem anderen enthalten ist oder nicht. Von zwei sich teilweise überlappenden Schleifen kann aber nicht festgestellt werden, welche die andere enthält. Damit ist auch keine einfache Aufteilung des Programms in selbständig zu verstehende Teile mit einfachen und überschaubaren Schnittstellen mehr möglich. Da der Programmierer im Flußdiagramm bei der Planung beliebig und ohne Einschränkung Steuerflußwege aufspalten und wieder zusammenfuhren kann, wird ihm das Fehlen einer derartigen Strukturierung noch nicht einmal bewußt. Besteht aber keine eindeutige Ordnungsrelation zwischen den einzelnen Elementen eines Flußdiagramms, so ist auch seine eindeutige Umsetzung in ein Programm nicht mehr möglich. Als Niederschrift in einer Programmiersprache und letztlich als Maschinencode im Arbeitsspeicher der Rechenmaschine ist ein Programm nämlich immer eindimensional, und für einen beliebig vernetzten Graph läßt sich keine umkehrbar eindeutige Abbildungsregel in ein eindimensionales Gebilde formulieren. Das ist wohl der Grund, warum Flußdiagramme auch als Dokumentationsmittel bei der praktischen Programmwartung selten wirklich be-

46

1. Programmentwicklung

nutzt werden. Es ist in der Regel sehr mühsam, zu einer Stelle im Flußdiagramm den zugehörigen Programmtext zu finden oder die Übereinstimmung von Programmtext und Flußdiagramm zu verifizieren. Als graphische Darstellungsmethode für Ablaufstrukturen sollen deshalb im folgenden nicht Flußdiagramme benutzt werden, sondern die auf einem Vorschlag von Nassi und Shneidermann [NASS73] beruhenden Struktogramme. Ihre Grundelemente werden Strukturblöcke genannt. Diese von Dijkstra [DAHL72] definierten Standard-Bausteine schließen Überlappungen aus. Ihre Zusammenfügung nach einfachen Regeln ergibt immer wieder neue, größere Strukturblöcke. Alle zum Aufbau von strukturierten Programmen benutzten Strukturblöcke haben die folgenden Eigenschaften (Abb. 1.3.2-2): (dynamischer) Steuerfluß

Eingang

V

aussen

V Ausgang

V Abb. 1.3.2-2. Strukturblock-Konventionen

1.3 Ablaufstrukturen

47

— Sie haben einen Eingang und einen Ausgang. — Der dynamische Steuerfluß läuft durch Strukturblöcke, und damit auch durch ein aus ihnen zusammengesetztes Struktogramm, immer von oben nach unten. — Jeder Strukturblock definiert eindeutig ein Innen und ein Außen, ein anderer Strukturblock ist entweder (vollständig) in ihm enthalten oder er befindet sich außerhalb von ihm. — Damit ist jeder Strukturblock, gleichgültig auf welcher logischen Ebene er im Programmentwurf steht, eine abgeschlossene funktionale Einheit. — Ein Strukturblock korrespondiert ausschließlich mit den direkt anschließenden Blöcken. Er erhält die Kontrolle nur von seinem oberen Nachbarn und gibt sie nur an seinen unteren Nachbarn weiter. Dies gewährleistet eine durchgehende, hierarchische Aufteilung der Ablaufstruktur des Programms in abgeschlossene Bausteine, von denen je zwei entweder — unabhängig voneinander sind — oder einer dem anderen eindeutig vor- oder nachgeschaltet ist — oder der eine vollständig in dem anderen (als logische Komponente) enthalten ist.

1.3.3 Strukturblöcke 1.3.3.1 Logische Grundstrukturen Welche Strukturblocktypen sind nun zweckmäßige Bausteine für Programme? Einer der Einwände gegen die Sprunganweisung als Mittel zur Änderung des Kontrollflusses war, daß ihr die Bedeutung im Rahmen der logischen Problemlösung nicht anzusehen ist. Dies sollte bei der Definition der Strukturblöcke vermieden werden: Jeder Strukturblocktyp muß den logischen, problemorientierten Grund für eine bestimmte Steuerfluß-Struktur sofort ersichtlich werden lassen. Nun gibt es grundsätzlich nur eine geringe Zahl logisch verschiedener Anlässe zur Wahl einer Steuerfluß-Verzweigung und dementsprechend auch nur wenige Grundstrukturen: — Ein Elementarblock ist eine einzelne verarbeitende Anweisung. — Die Reihung {Sequenz) verlangt die sequentielle Ausführung mehrerer, üblicherweise untereinander geschriebener Anweisungen (oder Strukturblöcke). — Die Auswahl {Selektion, Alternative) verlangt die von einer logischen Bedingung abhängige Wahl zwischen zwei oder mehr Anweisungen (oder Strukturblöcken). — Die Wiederholung (Iteration) verlangt die wiederholte Ausführung einer Anweisung (oder eines Strukturblocks), solange eine bestimmte Bedingung gilt.

1. Programmentwicklung

48

Dies ist bereits eine vollständige Liste der logischen SteuerflußGrundstrukturen. Böhm und Jacopini [BÖHM66] haben bewiesen, daß je ein Sprachmittel zur Darstellung von Reihung, Auswahl und Wiederholung ausreicht, um den Steuerfluß jedes programmierbaren Algorithmus darstellen zu können. Im folgenden werden die Strukturblöcke besprochen, mit denen diese logischen Steuerfluß-Grundstrukturen realisiert werden können. Sie werden jeweils — als Flußdiagramm-Ausschnitt, — als Strukturblock nach Nassi-Shneidermann und — in PASCAL angegeben. Die Anhänge A. 1 bis A.3 zeigen die Realisierung der wichtigsten Strukturblock-Typen in COBOL, FORTRAN und PL/1.

1.3.3.2 Elementarblöcke Ein Elementarblock ist eine einzelne, verarbeitende Anweisung. Sie entspricht dem rechteckigen Kastensymbol im konventionellen Flußdiagramm. Als derartige verarbeitende Anweisung kennt PASCAL nur die Zuweisung, z. B. y:=x

+ 1.

In anderen Programmiersprachen gibt es in der Regel ein größeres Spektrum, wie z. B. Eingabe- und Ausgabe-Anweisungen. Diese sind in PASCAL Aufrufe vordefinierter Prozeduren. Nicht nur deshalb ist es sinnvoll, auch Prozedur- und Makroaufrufe als „höhere" verarbeitende Anweisungen und somit als Elementarblöcke aufzufassen. Dies entspricht dem Gedanken der Programmentwicklung durch schrittweise Verfeinerung, wie sie in Abschnitt 1.2 dargestellt wurde. Somit ist ein Elementarblock im erweiterten Sinn jede durch eine Anweisung, einen mnemonischen Namen oder eine kurze Beschreibung ihrer Funktion dargestellte Verarbeitungseinheit, die zumindest auf der gerade betrachteten logischen Ebene nicht weiter aufgelöst werden soll. Ob sie später durch Unterteilung in weitere Strukturblöcke „verfeinert" wird und ob diese Verfeinerung in den Programmtext als Prozedur- oder Makrodefinition oder aber unmittelbar als Anweisungsfolge in line eingefügt wird, ist für die logische Ablaufstruktur belanglos. Dies ist ausschließlich ein Detail der technischen Realisierung, die erst nach Fertigstellung und Verifizierung der logischen Lösung der Aufgabe angegangen werden sollte.

1.3 Ablaufstrukturen

1.3.3.3 D i e R e i h u n g Die sequentielle Ausführung mehrerer verarbeitender Anweisungen oder auch mehrerer Strukturblöcke kann dadurch dargestellt werden, daß diese Strukturblöcke einfach untereinander gezeichnet werden (Abb. 1.3.3.3-1). Wie in den

Abb. 1.3.3.3-1. Reihung von Strukturblöcken

49

1. Programmentwicklung

50

meisten Programmiersprachen wird diese Reihung in PASCAL durch Hinter- bzw. Untereinanderschreiben der Einzelanweisungen ausgedrückt. Explizit kann eine derartige Sequenz von Anweisungen in PASCAL (ebenso wie in ALGOL) durch begin . . . end zu einer Compound-Anweisung zusammengefaßt werden. Dies ist notwendig, wenn die Anweisungsfolge als Einheit in einen Strukturblock eingefügt werden soll. PL/1 benutzt zur Bildung von Compound-AnWeisungen eine DO . . . END-Klammerung. Einige Sprachen (PL/1 und ALGOL) drücken durch eine BEGIN . . . ENDKonstruktion eine Blockbildung und nicht nur die Zusammenfassung zu einer Compound-An Weisung aus. Ein Block unterscheidet sich von einer Compound-Anweisung dadurch, daß nach dem BEGIN Variablen-Deklarationen folgen, die lokal zu diesem Block, d. h. außerhalb von ihm unbekannt sind (vgl. Scope-Attribut, Abschnitt 1.5.1.1). Soll die Zusammenfassung einer Anweisungssequenz zu einem Block betont werden, so kann dies wie in Abb. 1.3.3.3-2 geschehen.

BEGIN

Variablen-Definitionen

STRUKTUR-BLOCK

STRUKTUR-BLOCK

STRUKTUR-BLOCK END

Abb. 1.3.3.3-2. Der BEGIN END-Block (in ALGOL, PL/1, aber nicht in PASCAL, FORTRAN, COBOL ausdruckbar)

51

1.3 Ablauf strukturen

In vielen Sprachen, wie COBOL, FORTRAN, PASCAL und den meisten Assemblern, läßt sich die Blockbildung jedoch nicht ausdrücken. Ebensowenig gibt es ein Flußdiagramm-Äquivalent hierfür. Unabhängig davon, ob die benutzte Programmiersprache über Sprachmittel zur Formulierung einer Blockstruktur verfügt, ist das BEGIN-END-Block-Symbol nützlich, um Deklarationen und Wertebereiche der verwendeten Variablen in Struktogrammen notieren zu können. Abb. 1.3.4-1 bis 1.3.4-4 zeigen Beispiele hierfür.

1.3.3.4 Die Auswahl Verzweigt der Steuerfluß an einer bestimmten Stelle des Programms, so ist der logische Grund hierfür eine vom jeweiligen Programmzustand abhängige Auswahl zwischen mehreren alternativen Anweisungen oder Strukturblöcken. Der Programmzustand wird hierbei durch die aktuellen Werte einer oder mehrerer Variablen bestimmt. Der einfachste und häufigste Fall ist die Auswahl zwischen zwei Alternativen, je nach dem Wahrheitswert eines logischen Ausdrucks, einer Bedingung. Den entsprechenden Strukturblock (Abb. 1.3.3.4-1) nennt man deshalb meist bedingte Verzweigung. Wie in anderen höheren Sprachen (COBOL, PL/1) wird dieser Strukturblock in PASCAL durch if Bedingung then AnweisungY

eise Anweisung}

ausgedrückt, wobei A nweisung\ ausgeführt wird, wenn die Bedingung den Wahrheitswert true hat, andernfalls die Anweisung2. Anweisung) und Anweisung2 können beliebige Strukturblöcke sein. Der linke Strukturblock {Anweisung ^) wird oft als THEN-Block, der rechte (Anweisung 2 ) alsELSE-Block bezeichnet. Ein Sonderfall der bedingten Verzweigung ist die bedingte Anweisung (oder Anweisungsfolge), bei der eine der beiden Alternativen (meist die für den Wahrheitswert false) fehlt. Wie in anderen Sprachen kann in diesem Fall auch in PASCAL der else-Teil der if-Konstruktion weggelassen werden: if Bedingung then

Anweisung.

52

1. Programmentwicklung

V BEDINGUNG true

STRUKTUR-BLOCK (Then-Block)

^

^

^

false

STRUKTUR-BLOCK (Else-Block)

Abb. 1.3.3.4-1. Bedingte Verzweigung

Dies gilt aber nicht für das Strukturblock-Symbol. In diesem muß auch ein leerer ELSE-Block eingetragen werden: Abb. 1.3.3.4-2 zeigt die richtige und die falsche Darstellung. Warum ist das Strukturblock-Symbol 1.3.3.4-2, welches man zuweilen angewendet sieht, falsch? (Hinweis: Ist es auch noch eindeutig interpretierbar, wenn der bedingt auszuführende Strukturblock eine Reihung entsprechend Abb. 1.3.3.3-1 ist?)

53

1.3 Ablauf strukturen

^ ^ ^ true

BEDINGUNG v

false

STRUKTURBLOCK

(a)

Richtig

(b)

Falsch!

Abb. 1.3.3.4-2. Bedingte Anweisung

Eine Auswahl aus mehr als zwei Alternativen ist die Fallunterscheidung. 1.3.3.4-3 zeigt ihre Darstellung als Strukturblock.

Abb.

In den meisten Programmiersprachen kann die Fallunterscheidung nur durch Sonderformen der Sprunganweisung (goto) realisiert werden (vgl. Anhang A.l bis A.3).

54

1. Programmentwicklung

FaUÌ^

^

FALLABFRAGE

STRUKTURBLOCK 1

Falln

^

STRUKTURBLOCK 2 STRUKTURBLOCK n

Abb. 1.3.3.4-3. Fallunterscheidung

55

1.3 Ablaufstrukturen

PASCAL gehört zu den wenigen Programmiersprachen, welche für die Fallunterscheidung unmittelbar ein Sprachelement, die case-Anweisung, bereitstellen: case Ausdruck of Fall\ Anweisungx; Fall2 : Anweisung2; Falln

:

Anweisungn

end. Hierbei sind Fallx bis Falln Konstanten des gleichen Datentyps wie das Ergebnis des Ausdrucks, von dem die Fallunterscheidung abhängt. Der aktuelle Wert des Ausdrucks bestimmt dann über das entsprechende Fallj die {Compound-) Anweisung, die ausgeführt werden soll. Häufig wird der Ausdruck einfach eine ganzzahlige Variable sein, die z. B. durch v a r f a l l : integer-, deklariert wurde. Dann wäre ein Beispiel für eine Fallunterscheidung etwa case fall of 1 : y:= x +1; 2 : y:=-x + l; 3 : y:=x end. Es stellt sich nun die Frage, was geschieht, wenn ein unvorhergesehener Fall auftritt, also wenn z. B. im obigen Beispiel die Variable fall den Wert 4 hat. Die verschiedenen Programmiersprachen, und zuweilen sogar verschiedene Compiler, verhalten sich hierbei, je nach der Implementierung der Fallunterscheidung oder des zu ihrer Simulation einzusetzenden Sprachmittels, unterschiedlich. Deshalb sollte in der Regel eine Fallunterscheidung mit Fehlerausgang vorgesehen werden, wie sie Abb. 1.3.3.4-4 zeigt. Jedes nicht unter den Fällen 1 bis n vorgesehene Ergebnis der Fallabfrage führt dann auf den Strukturblock zur Fehlerbehandlung. In PASCAL kann durch Beschränkung des die Fallabfrage steuernden Ausdrucks auf eine Variable und überlegte Typendefinition die Gefahr eines derartigen Fehlers minimisiert werden. PASCAL erlaubt nämlich, den Wertebereich für eine ganzzahlige Variable durch var Variable

\n..m

auf den Unterbereich n bis m einzuschränken (n und m sind hier ganzzahlige Konstanten). Für das oben angeführte Beispiel würde also var fall :1

..3

die Gefahr eines unzulässigen Falls ausscheiden.

1. Piogrammentwicklung

56

FAI T ARFRAP.F /

STRUKTURBLOCK 1

/ STRUKTURBLOCK 2

/Unzulässig

STRUKTURBLOCK STRUKTUR(Fehler) BLOCK n

Abb. 1.3.3.4-4. Fallunterscheidung mit Fehlerausgang

57

1.3 Ablauf strukturen

Die gleiche Sicherheit, verbunden mit der Möglichkeit einer mnemonischen Benennung der Fälle im Programmtext, bietet in PASCAL die Deklaration einer Variablen als skalaren Typ (vgl. Abschnitt 1.4.3) durch eine Liste symbolischer Bezeichnungen. var fall: (rot, gelb, grün) bedeutet, daß die Variable fall die drei Farbbezeichnungen als Werte annehmen kann. Hierbei ist es irrelevant und dem Programmierer unbekannt, wie diese rechnerintern dargestellt sind. Er referiert sie in seinem Programm ausschließlich als symbolische Bezeichnungen; also etwa in einer Zuweisung fall '•=gelb oder in einer bedingten Anweisung if fall rot then . . . . Damit erhielte die obige Fallunterscheidung die Form case fall of rot: y:=x + 1; gelb : y:= -x +1; grün: y-'= x end.

1.3.3.5 D i e W i e d e r h o l u n g (Iteration) Die letzte Grundstruktur der Ablaufsteuerung ist die wiederholte Abarbeitung einer Anweisung oder eines Strukturblocks bis zum Eintritt einer bestimmten Bedingung. Je nachdem, welche Form diese Bedingung hat und an welcher Stelle der Wiederholungsschleife die Abfrage auf Fortsetzung erfolgt, gibt es mehrere Iterationsformen. Die bedingte Wiederholung (Abb. 1.3.3.5-1) fragt die Ausführungsbedingung am Eingang, d. h. vor Beginn der zu iterierenden Verarbeitung ab. Ist sie schon bei ihrer ersten Abfrage nicht erfüllt, so wird die Anweisung überhaupt nicht ausgeführt. Das entsprechende Sprachmittel in PASCAL ist die while-An Weisung: while Ausführungsbedingung

do Anweisung.

Da auch in mehreren anderen Sprachen (z. B. PL/1) diese Wiederholung durch eine WHILE-Konstruktion ausgedrückt wird, hat sich für diesen Strukturblocktyp der Name WHILE-Schleife eingebürgert.

58

1. Piogrammentwicklung

Wiederhole solange AUSFÜHRUNGSBEDINGUNG gilt

STRUKTUR-BLOCK

Abb. 1.3.3.5-1. Bedingte Wiederholung

59

1.3 Ablaufstrukturen

Eine Sonderform der bedingten Wiederholung ist die Zählschleife, bei welcher ein Index in Einerschritten von einem Anfangswert auf einen Endwert gezählt wird und für jeden Indexwert in diesem Intervall die Anweisung einmal ausgeführt wird. In der Regel ist der Anfangswert dann f o r I n d e x •=Anfangswert

kleiner als der Endwert. Die Zählschleife lautet to Endwert do Anweisung.

Ist jedoch der Anfangswert größer als der Endwert, d. h. soll abwärts gezählt werden, so wird dies durch for Index '• - A nfangswert downto Endwert do A nweisung ausgedrückt. Anfangswert und Endwert können beliebige Ausdrücke sein, die natürlich bei der Auswertung den gleichen (skalaren) Datentyp wie der Index selbst liefern müssen. Sind sie bei der Auswertung gleich, so wird die Anweisung einmal ausgeführt. Liegt die vorausgesetzte Größenbeziehung von Anfang an nicht vor, so wird die Anweisung auch nicht ausgeführt. Dies ist ein Unterschied zur FORTRAN-Zählschleife, welche nach dem Sprachstandard [USAS66] und dementsprechend von den meisten Implementierungen wegen der Abfrage am Ende immer mindestens einmal durchlaufen wird. Eine weitere Sonderform ist die bedingte Wiederholung mit Endabfrage (Abb. 1.3.3.5-2). Hierbei wird am Ende der Verarbeitung eine Endebedingung geprüft. Die zu iterierende Anweisung oder der Strukturblock werden wiederholt, wenn die Endebedingung nicht erfüllt ist. Man beachte den Unterschied zwischen der Ausfuhrungsbedingung bei der normalen bedingten Wiederholung und der Endebedingung bei der Endabfrage: im einen Fall verlangt der Wahrheitswert true die Ausführung der zu iterierenden Verarbeitung, im anderen Fall false. Die unbedingte Wiederholung {Zyklus, Cycle-Schleife), die Abb. 1.3.3.5-3 zeigt, kommt nur selten in reiner Form vor, da sie nie abbricht. In PASCAL kann sie auf einfache Weise durch while true do Anweisung ausgedrückt werden.

1. Programmentwicklung

60

Wiederhole bis ENDEBEDINGUNG

Abb. 1.3.3.5-2. Bedingte Wiederholung mit Endabfrage

In Prozeßrechner- und Teilnehmer-Systemen dient sie zuweilen als äußerste Schleife zum immer wiederholten Abfragen von Meßstellen oder Datenstationen (Polling-Cycle). In der Regel enthält sie eine oder mehrere Unterbrechungen (Breaks), wodurch sie zur Wiederholung mit Unterbrechung (Abb. 1.3.3.5-4) wird. Die Wiederholung wird an dieser Stelle bei Vorliegen einer bestimmten Bedingung abge-

61

1.3 Ablauf strukturen

V STRUKTURBLOCK

Wiederhole für immer

STRUKTUR-BLOCK

Abb. 1.3.3.5-3. Unbedingte Wiederholung (Zyklus)

brochen. Die Verarbeitung wird dann immer am Ende des die Unterbrechung unmittelbar umschließenden Wiederhokings-Strukturblocks fortgesetzt. Ein gleichzeitiger Abbruch mehrerer, ineinander geschachtelter Wiederholungs-Schleifen ist nicht möglich. Unterbrechungen können auch in bedingte Wiederholungen eingefügt werden.

62

1. Programmentwicklung

Wiederhole (oder wiederhole solange Bedingung)

STRUKTUR-BLOCK)

Bedingte Unterbrechung

^ ^

STRUKTUR-B LOCK2

Abb. 1.3.3.5-4. Wiederholung mit Unterbrechung

Beispiele für typische Anwendungen dieser Unterbrechung sind -

Auftreten einer Sonder- oder Fehlerbedingung,

-

Finden des gesuchten Tabelleneintrags in einer Suchschleife.

1.3 Ablauf strukturen

63

Nur wenige Programmiersprachen stellen ein explizites Sprachmittel für Unterbrechungen (als BREAK, EXIT o. ä.) zur Verfügung. In allen üblichen Sprachen, selbst in PASCAL, muß zu ihrer Realisierung auf ein goto zurückgegriffen werden: while true do begin Anweisungi; if Bedingung then goto 100; Anweisung2 end; 100 : Programmfortsetzung.

1.3.4 Struktogramme Mit Hilfe der verschiedenen Strukturblocktypen kann die Ablaufstruktur eines Programms wesentlich besser dargestellt werden als mit dem herkömmlichen Flußdiagramm. Struktogramme zeigen das gesamte Programm als einen einzigen, nach den in Abschnitt 1.3.3 dargestellten Regeln immer wieder in Unter-Strukturblöcke aufgeteilten Strukturblock. Sie können sowohl als Planungs- als auch als Dokumentationsmittel verwendet werden. Eine ähnliche, allerdings weniger konsequente Diagrammtechnik wurde von Chapin vorgeschlagen [CHAP74]. Abb. 1.3.4-1 bis 4 zeigen Struktogramme für die in Abschnitt 1.2 als Beispiel benutzte Aufgabe Häufigkeitszählung von Worten. Man beachte, daß in die Strukturblöcke bewußt nicht PASCALAnweisungen, sondern die Beschreibung der Verarbeitungen in normaler Prosa eingesetzt wurden. Ein Struktogramm soll eine von den technischen Details der Realisierung unabhängige Planungs- oder Dokumentationsunterlage zum Verständnis der logischen Lösung des Problems und seiner Ablaufstruktur sein. Ein gutes Struktogramm ist deshalb auch weitgehend sprachunabhängig. Jedes Struktogramm wurde in einen BEGIN-END-Block (vgl. Abb. 1.3.3.3-2) eingefaßt. Er dient dazu, die in ihm verwendeten Daten angeben zu können. Man beachte, daß hierbei nicht nur lokale, sondern auch globale, in höheren Blöcken erklärte Daten aufgeführt wurden. Je nach der benutzten Programmiersprache können die globalen Daten entweder als Parameter bei einem Prozeduraufruf oder durch Erklärung in einer umfassenderen Prozedur (wie im PASCAL-Beispiel

64

1. Programmentwicklung

Abb. 1.3.4-1. Struktogramm zur „Häufigkeitszählung von Worten" (vgl. Aufgabe 1.2.1/1 und Abschnitt 1.2.3)

von Abschnitt 1.2.3) übergeben werden. FORTRAN, welches als nicht blockstrukturierte Sprache die zweite Möglichkeit nicht kennt, bietet durch die Einführung von COMMON-Datenbereichen einen Ersatz hierfür. Ein Struktogramm bietet gegenüber dem Flußdiagramm die folgenden Vorteile: - Abläufe, die nicht den logischen Grundstrukturen entsprechen („wilde" Sprünge), lassen sich gar nicht darstellen. Dadurch ist die Richtigkeit der Lösung besser verifizier- und testbar. — Es entspricht der Entwurfstechnik der schrittweisen Verfeinerung und ist deshalb sowohl zur Planung als auch als Dokumentationsmittel zum besseren

65

1.3 Ablaufstrukturen

BEGIN

Wortaufbereiten Daten

Eingabedatei input (Lochkarten), aufzubereitendes Wort actualword

Wortzwischenspeicher actualword

(Ausgabe).

auf Zwischenräume ablöschen

Überlese Zwischenräume, Punkte und Zeilenenden Erstes Zeichen in actualword

einsetzen

Nächstes Zeichen lesen

Wiederhole bis Zwischenraum, Punkt oder Zeilenende

actualword gefüllt? ja

^



__ —

Zeichen in actualword

' nein

einsetzen

Nächstes Zeichen lesen END

(Übergebe

actualword)

Abb. 1.3.4-2. Struktogramm zu „Wortaufbereiten" (vgl. Abb. 1.3.4-1)

Verständnis des Programms durch Nachvollziehen des Planungsprozesses besser geeignet. — Es zeigt die logische Struktur der Problemlösung und ist deshalb von Realisierungs-Details weniger abhängig.

Zwei Beispiele, wie sehr übliche, aus der Literatur entnommene Flußdiagramme die technische Realisierung widerspiegeln und die Logik der Problemlösung verschleiern, sind in [SCHN74a] zu finden.

66

1. Programmentwicklung

BEGIN

Suche Listentry Daten : Aufbereitetes Wort (Eingabe).

actualword

Liste list von Listeneinträgen (Anzahl = entry counter), Zeiger listpos in Liste list (Ausgabe).

Initialisiere auf Listenanfang : listpos'-^ 1

Wiederhole bis actualword in Liste gefunden oder Listenende (listpos > entrycounter)

Vergleiche list [listpos] mit

actualword

Gefunden? ja

^ ^

—"

' " nein

Listenzeiger listpos um 1 erhöhen

END

(Ubergebe listpos)

Abb. 1.3.4-3. Struktogramm zu „Suche Listentry" (vgl. Abb. 1.3.4-1)

1.3.5 Rekursion Eine Ablaufstruktur, welche eine Wiederholung völlig anders realisiert als mit den in Abschnitt 1.3.3.5 dargestellten sprachlichen Mitteln, ist die Rekursion. Für viele Programmieraufgaben ist sie der Problemstellung am besten angepaßt [BARR68]. In Struktogrammen läßt sie sich darstellen, indem in einem Strukturblock auf einen höher liegenden, das heißt ihn selbst umfassenden, Bezug genommen wird. Abb. 1.3.5-1 zeigt dies an dem besonders einfachen Fall der Fakultät n\

67

1.3 Ablaufstrukturen

BEGIN

Wort Zählen Daten : Aufbereitetes Wort actualword, Zeiger listpos in Liste list (Eingaben). Liste list von Listeneinträgen mit Wort und Wortzähler (Anzahl =

actualword bereits in Liste? ja

^s.

Wortzähler um 1 erhöhen

entrycounter)

—-—' " —-

—•—"""" ^ ^ ja

^

nein

Liste voll?

entrycounter (: = listpos)

^ nein berichtigen

actualword in Liste einsetzen

Wortzähler auf 1 initialisieren

END Abb. 1.3.4-4. Struktogramm zu „Wort Zählen" (vgl. Abb. 1.3.4-1) Da die mathematische Definition der Fakultät n! = n*(n-l)! für n>0 ein Standardbeispiel für eine rekursive Definition ist, kann sie auch unmittelbar in ein Struktogramm für den Algorithmus übertragen werden. In PASCAL läßt sich die rekursive Funktions-Definition 1 2 Fak(n) sofort niederschreiben : 12

Eine Funktionsdeklaration ist in PASCAL der Prozedurdeklaration sehr ähnlich. Sie unterscheidet sich von ihr nur in der Kopfzeile. Diese beginnt mit function, gefolgt vom Funktionsnamen, einer Argumentenliste und dem Typ der Funktion function Name (Argx : Argtyp, . . .): ftyp.

68

1. Programmentwicklung

BEGIN

Fak (n) Parameter : n ganze Zahl > 0

n? < 0

JS

Fak (0) := 1 Fak («) := «•Fak (n-1)

Fehler: Fakultät nicht definiert!

END : Übergebe Fak (n)

Abb. 1.3.5-1. Struktogramm Fak (n) zur rekursiven Berechnung der Fakultät n!

1.3.5/1 function Fak (n:integer) : integer; begin if n < 0 then begin error ('Fakultät Undefiniert'); Fak:=0 end else if n= 0 then Fak := 1 else Fak := n*Fak (n-1) end; error {'Meldung') sei hierbei eine Fehlerprozedur zur Ausgabe einer Meldung. Das Beispiel zeigt, wie auf die Funktion Fak durch eine normale Zuweisung in der Funktionsdeklaration selbst Bezug genommen wird.

69

1.3 Ablaufstrukturen

Auch in der System- und Anwendungsprogrammierung lassen sich Prozeduren rekursiv oft leichter formulieren als mit den üblicheren Wiederholungsschleifen. Eine typische, in den verschiedenen Zweigen der Datenverarbeitung immer wieder vorkommende Problemklasse sind Prozeduren zur Abarbeitung von Baum-Strukturen. Abb. 1.3.5-2 zeigt ein Beispiel für einen binären Baum. Jeder Knoten des Baumes ist durch eine Nummer i bezeichnet und enthält — einen Verweis L auf den linken Unterknoten oder Unterbaum, — einen Verweis R auf den rechten Unterknoten oder Unterbaum, — eine Information info, z. B. eine Zeichenkette mit einem Text. L = 0 bzw. R = 0 bedeute, daß kein linker bzw. rechter Unterknoten existiert. Rechnerintern kann der Baum leicht durch ein array von Knoten dargestellt werden, in dem jeder Eintrag einen linken und einen rechten Verweis sowie eine Textinformation enthält: 1.3.5/2 type Knoten = record L : 0 . . max; info : array [1 . . 20] of char; R : 0 . . max end; var Baum : array [1 . . max] of Knoten;

Für derart dargestellte Bäume sei folgende Aufgabe zu lösen [KNUT74]. 1.3.5/3 Man schreibe eine Prozedur tprint(i), welche die Informationen info in dem mit dem Knoten i beginnenden Baum oder Unterbaum von links nach rechts ausdruckt, d. h. (1) alle info des jeweils linken Unterbaums (wenn vorhanden), (2) info (i) selbst, (3) alle info des jeweils rechten Unterbaums (wenn vorhanden). Versuchen Sie, die Aufgabe 1.3.5/3 ohne Rekursion zu programmieren.

70

1. Programmentwicklung

2

(jj^)

Beispiel für einen binären Baum

L(i)

Info (i)

L (i) = 0 : kein linker Unterbaum R (i) = 0 : kein rechter Unterbaum

©

Rechnerinterne Darstellung durch ein array

Abb. 1.3.5-2. Baumstruktur (logisch und rechnerintern)

R(i)

1.3 Ablaufstrukturen

71

Abb. 1.3.5-3 zeigt das rekursive Struktogramm für den Algorithmus tprint(i). Vollziehen Sie den Algorithmus an Hand des Struktogramms und des Beispiels Abb. 1.3.5-2 für tprint(l) nach. Die Prozedurerklärung für tprint (i) läßt sich unmittelbar hinschreiben. 1.3.5/4 13 procedure tprint (i:integer); begin if i 0 then with Baum [i] do begin tprint (L); print (info); tprint (R) end end; Nicht jede Programmiersprache bietet rekursive Prozedur- und Funktionsaufrufe. COBOL und FORTRAN verbieten sie, in ALGOL und PL/1 sind sie erlaubt. Allerdings gibt es auch ALGOL-Implementierungen, welche die Rekursion ausschließen, und umgekehrt nicht-standardisierte FORTRANCompiler, welche sie ermöglichen. Beides ist abzulehnen: ob Rekursion erlaubt ist oder nicht, ist keine Sache der Implementierung sondern der Sprachdefinition, da sie von den in der betreffenden Sprache vorgesehenen Datenattributen (Scope, Lebensdauer, vgl. Abschnitt 1.5.1.1) abhängt. Beide Abweichungen vom Standard bedeuten deshalb, daß die Datenverwaltung anders implementiert wurde, als die Sprachdefinition es vorschreibt, und daß deshalb u. U. korrekte Programme falsch ablaufen. Wenn die benutzte Programmiersprache Rekursion nicht unterstützt, so kann man einen rekursiven Algorithmus immer in einen iterativen, mit den in Abschnitt 1.3.3.5 besprochenen Wiederholungsschleifen arbeitenden, umsetzen. Allerdings ist dieser oft weniger einfach — vor allem dann, wenn wie im Beispiel 1.3.5/3 die Aufgabenstellung unmittelbar rekursiv formuliert wurde. 13

Hier wurde zur Abkürzung der Variablennamen die with-Konstruktion von PASCAL benutzt. with Baum[i] bedeutet, daß alle Variablennamen im Bereich des do mit Baum[i\ qualifiziert werden sollen: tprint (L) ist somit innerhalb der Reichweite der with-Konstruktion gleichbedeutend mit tprint (Baum |i].L).

1. Programm entwicklung

72

BEGIN

tprint (i)

Baum

Daten (global) :

= array von Knoten ( L , info, R ) .

. Parameter : /

Index des obersten Knoten eines Unterbaums.

^^ " | (Unter-) Baum leer J-

IT

tprint (Baum [i].L)

Drucke Baum [i].info

tprint (Baum [ i ] . R )

END Abb. 1.3.5-3. Rekursives Struktogramm tprint (i) zum Ausdrucken der Informationen in einer binären Baumstruktur (vgl. Abb. 1.3.5-2)

Eine nicht-rekursive Version tprintl der Prozedur tprint zum Ausdrucken von binären Bäumen wird in Abschnitt 1.5.1.2 gezeigt (1.5.1.2/2).

1.4 Datenstrukturen14 1.4.1 Die Daten als Ausgangspunkt der Programmplanung Programmierer als Berufsbezeichnung und Programmieren als wesentliche Tätigkeit der Angehörigen dieser Berufsgruppe sind nicht sehr glücklich gewählte Ausdrücke — das zeigen nicht zuletzt die Schwierigkeiten bei der Definition eines Berufsbilds [DIJK72], 14

Das Wort Datenstruktur wird in der Literatur oft auch oder sogar ausschließlich für dynamisch veränderliche Datenaggregate (wie Listen und Bäume) verwendet. Die Autoren möchten sich diesem Gebrauch nicht anschließen. Structura heißt Bau, und dynamisch veränderliche Bauwerke sind ihnen unbehaglich. Deshalb sollen hier nur statische Datenaggregate (wie records und anays) als Datenstrukturen bezeichnet werden. Die Behandlung dynamischer Datenaggregate ist ohnehin kein Gegenstand dieses Buchs. Der Leser sei hierfür auf die Literatur verwiesen |KNUT68, MAUR74a].

73

1.4 Datenstrukturen

Durch die Betonung des Programmierens, des formalisierten Niederschreibens von Algorithmen, tritt das eigentliche Ziel dieser Tätigkeit, die Datenverarbeitung, oft in den Hintergrund. Die Festlegung und Strukturierung der Daten wird so zuweilen gegenüber dem Entwurf der Programmabläufe fast vergessen. Auf diese Weise entstandene Programme sind daran zu erkennen, daß zwar Logik und gegenseitige Wechselbeziehungen der einzelnen Programmkomponenten einigermaßen verständlich sind, nicht aber die der gewählten Datenstrukturen. Um diesem Übelstand abzuhelfen, beschäftigt sich eine Reihe neuerer Arbeiten und Verfahren bevorzugt mit der systematischen Strukturierung der Daten — sowohl als Ausgangspunkt für die Problemformulierung als auch während der Detailplanung und Programmierung [DAHL72, DESJ74, HART71, HOAR68, HOAR75, JACK74, JOHN71, MAUR74a, WARN74], Ein Programmentwurfs- und Dokumentationsverfahren, welches auf der in Abschn. 1.2 dargestellten Aufgabenlösung durch „schrittweise Verfeinerung" beruht, jedoch stärker als andere von den zu bearbeitenden Daten ausgeht, wurde von der IBM unter dem Namen HIPO-Technik (Hierarchy of Input-Process-Output) eingeführt und standardisiert [IBMOO, IMBOOb, IBM74], Es verträgt sich gut mit der in Abschnitt 1.3 besprochenen Darstellung der Ablaufstrukturen als Struktogramme und kann mit diesen zusammen eingesetzt werden — als vorgeschaltete oder parallel ablaufende Planungstechnik für die zu definierenden Datenstrukturen und ihre Beziehungen mit den jeweiligen Verarbeitungseinheiten (Prozeduren, höhere Strukturblöcke). Die HIPO-Technik geht als hierarchisches Top Down-Verfahren für die Grobplanung davon aus, daß sich die Problemstellung sowohl für die gesamte Programmieraufgabe als auch für jede tiefere, abgeschlossene Teilkomponente als eine Funktion P(Processing) definieren läßt, welche, angewandt auf die Eingabedaten I(Input), die Ausgabedaten O (Output) erzeugt: 0 = P(I)

1.4.1/1

Der erste Planungsschritt beginnt deshalb mit der Festlegung der gewünschten Ausgabedaten O und der nötigen Eingabedaten / ; diese sind identisch mit den Daten, die wir in Abschn. 1.2.1 als Aus-.und Eingabedaten der Benutzermaschine bezeichneten. Es wird empfohlen, bei der systematischen Planung mit HIPOs auf jeder Stufe immer zuerst die jeweils gewünschten Ausgabedaten O,-, und dann erst — zusammen mit dem Entwurf der Verarbeitung P, — die nötigen Eingabedaten/¿festzulegen. Das Ziel der Verarbeitung besteht nämlich immer in der Herstellung der Ausgabedaten. Ein Eingabedatum hat dagegen nur dann eine Existenzberechtigung im Programm, wenn es für mindestens einen Verarbeitungsschritt benötigt wird.

74

1. Programmentwicklung

Die Grobplanung der Funktion P besteht in der Festlegung tieferer, einfacherer Funktionen^ aus denen sich die FunktionPmit Hilfe der in Abschn. 1.3 besprochenen Ablaufstrukturen zusammensetzen läßt. Jede so definierte Funktion Pf kann als der Name eines Elementarblocks (vgl. Abschn. 1.3.3.2) im Struktogramm für die Realisierung der höheren Funktion(en) aufgefaßt werden. Die von einer Funktion Pt zu leistende Verarbeitung wird in der HIPO-Technik meist informell verbal festgelegt. Eine Notation zur formalen Beschreibung von Funktionen ohne Rückgriff auf ihre algorithmische Realisierung wurde von Parnas vorgeschlagen [PARN72]. Sie ist jedoch vorwiegend für die exakte Dokumentation des Ergebnisses des Planungsprozesses gedacht (vgl. Abschn. 2.3). Für die Planung selbst ist sie wohl zu aufwendig. Jede der tieferen Funktionen Pt ist ihrerseits durch Angabe ihrer Ausgabedaten O t , ihrer Eingabedaten /, und ihrer Auflösung in einfachere Verarbeitungs-Funktionen zu planen. Diese Top Down-Planung wird wiederholt, bis die Funktionen so einfach geworden sind, daß sie sich unmittelbar in den Anweisungen der gewählten Programmiersprache niederschreiben lassen. Die Aus- und Eingabedaten Oy und Iy der hierarchisch tieferen Funktionen Py sind hierbei teils die entsprechenden Daten 0/ und Ij der höheren FunktionP t , zu deren Realisierung/^- dient, teils sind es aber auch Arbeits- oder Steuerdaten, die nur lokale Bedeutung haben und außerhalb von Pt nicht mehr in Erscheinung treten. Die Darstellung eines derartigen Tripels aus Eingangsdaten, Verarbeitung und Ausgabe als ein rechteckiger dreigeteilter Kasten wird als HIPO bezeichnet. Abb. 1.4.1-1 zeigt eine Folge derartiger HIPOs für den Grobentwurf der aus Abschn. 1.2 bereits bekannten Aufgabe Häufigkeitszählung von Worten. In der Praxis wird man die HIPO-Methode wohl nur bei komplexeren Programmieraufgaben einsetzen, wobei dann auch jede einzelne, auf tieferer Ebene definierte Funktion selbst wieder komplexer ist, mehr Eingabe- und Ausgabedaten hat und aus mehr Verarbeitungsschritten besteht. Jeder HIPO, gleichgültig auf welcher Stufe, enthält — rechts die Ausgaben (Output), — in der Mitte die Verarbeitung (Process) und — links die hierzu nötigen Eingaben (Input).

75

1.4 Datenstrukturen

Häufigkeitszählung

Eingaben (Input)

Textzeilen

Q

Verarbeitung (Process) c

gedruckte Wortliste ti^^

Inputverarbeitung

Verarbeitung (Process)

Eingaben (Input)

'

©

^ i ) Inputverarbeitung ( ? ) Listedrucken

Ausgaben (Output)

Textzeilen

C

, ( T ) Wortaufbereiten ( T ) Wortverarbeiten

Ausgaben (Output)

Wortliste

Listedrucken

Abb. 1.4.1-1. HIPOs für den Grobentwurf der Programmieraufgaben „Häufigkeitszählung von Worten" (vgl. 1.2.1/1)

76

(u)

1. Programmentwicklung

Wortaufbereiten

Eingaben (Input)

TextZeile

nextchar

Verarbeitung (Process) J ( 7 ) Zeichen lesen /y [ ( 2 ) Zeichen interpretieren ' 3 ) evt. in Wortzwischen^ Speicher einsetzen

Ausgaben (Output)

Wortzwischenspeicher

1.2) Wortverarbeiten

Eingaben (Input)

Verarbeitung (Process)

111 I I I I I I I I I C ^ H D w o r t i n Wortliste Wortzwischenrj suchen speicher ( 2 ) Wort zählen oder neu eintragen

M

Ausgaben (Output)

Wortliste

Wortliste

Abb. 1.4.1-1. (Fortsetzung) HIPOs für den Grobentwurf der Programmieraufgaben „Häufigkeitszählung von Worten" (vgl. 1.2.1-1) Die „tieferen" Funktionen sind in jeder Verarbeitung fortlaufend numeriert. Soweit sie auch ihrerseits durch HIPOs dargestellt werden, wird zu deren Bezeichnung eine durch Verketten der Funktionsnummern gewonnene Mehrfach-Indizierung benutzt. Welche Verarbeitungsschritte welche Eingabedaten verwenden und welche Ausgabedaten sie erzeugen, wird durch Pfeile

angedeutet. Man beachte, daß

77

1.4 Datenstrukturen

ein Update, die Änderung eines Eingabe- oder Ausgabedatums, auf zwei Weisen dargestellt werden kann: — durch einen rückläufigen Pfeil von der Ausgabe zur Verarbeitung oder von der Verarbeitung zur Eingabe (vgl. Abb. 1.4.1-1, (Tj)) oder — durch Angabe des betreffenden Datums (bzw. der Datenstruktur) in Eingabe und Ausgabe (vgl. Abb. 1.4.1-2 (f^)). Die hierarchische Abhängigkeit der HIPOs für die „tieferen" Funktionen von den „höheren", zu deren Detailplanung sie dienen, zeigt Abb. 1.4.1-2 a\s Programmbaum, der bei Verwendung der HIPOs als Dokumentationsmittel ihnen als graphische Darstellung des „Inhaltsverzeichnisses" vorangestellt werden sollte.

© Wortaufbereiten

Wortverarbeiten

Abb. 1.4.1-2. „Programmbaum" der Aufgabe „Häufigkeitszählung von Worten"

78

1. Programmentwicklung

Überlegen Sie sich den Zusammenhang zwischen — der so erhaltenen Darstellung des Programms als Baum, — der Forderung der vollständigen Aufteilung eines Programmes in ein „Innen" und „Außen" bezüglich jedes seiner Strukturblöcke (vgl. Abb. 1.3.2-2), — dem Verbot sich teilweise überlappender „Schleifen" im Flußdiagramm (vgl. Abb. 1.3.2-1). Beim Vergleich der Baumstruktur (Abb. 1.4.1 -2) mit dem Struktogramm (Abb. 1.3.4-1) fällt auf, daß das Suchen in der Wortliste und das Wortzählen bzw. -eintragen hier nicht als getrennte Aktionen aufgefaßt wurden, sondern in einen „Modul" Wortverarbeiten zusammengefaßt sind. Dies ist Ausdruck dafür, daß die Prozeduren SucheListentry (1.2.3/9) und Wortzählen (1.2.3/10) eine Datenstruktur, die Wortliste list, verwalten. Bereits in Abschnitt 1.2.3 hatten wir bemerkt, daß eine Aufteilung der Listenverwaltung die Wartung erschwert, wenn die Struktur der Wortliste geändert werden soll. Die von den zu verwaltenden Datenstrukturen ausgehende HIPO-Technik bewahrt den Planer vor einer ungeschickten Modularisierung. SucheListentry und Wortzählen würden erst eine Strukturierungsebene tiefer (HIPO und (LZ2) in den Abb. 1.4.1-1 und 1.4.1-2) auftreten: sie sind die „Primitivoperationen" zur Verwaltung der Wortliste, auf welche die Wortverarbeitung zurückgreift. Dies ist ein erster Hinweis auf die Bedeutung der Datenstrukturierung als Ausgangspunkt der Programmplanung und Modularisierung. Die Abschnitte 1.6 und 2.3 werden hierauf ausführlicher eingehen. 1.4.2 Attribute von Daten Die von einer Programmkomponente (Funktion) zu verarbeitenden Daten können in primäre, Arbeits- und Steuerdaten unterteilt werden. Primäre Daten sind Eingabe-, Ausgabe- und Update-Daten, deren Bearbeitung als der primäre Zweck der jeweiligen Funktion angesehen werden kann. Arbeitsdaten sind Variable, die außerhalb der betreffenden Funktion keine Bedeutung haben und nur lokal, d. h. innerhalb von ihr, zur Berechnung von Zwischenergebnissen, Aufbereitung von Texten oder ähnlichen Zwecken benötigt werden. Steuerdaten sind Variable, die bestimmte Zustände des Gesamtsystems (z. B. Betriebsbereitschaft einer bestimmten Hardware-Komponente, Füllungsgrad einer Tabelle, Durchführung und Abschluß einer Initialisierung) anzeigen oder beschreiben. Sie werden deshalb auch oft Zustandsvariable genannt. Klassifizieren Sie die in dem Programmbeispiel „Häufigkeitszählung von Worten" benutzten Daten nach ihrer Rolle im Rahmen der ein-

1.4 Datenstrukturen

79

zelnen Funktionen (Prozeduren 1.2.3/3 bis 1.2.3/11). Beachten Sie, daß ein bestimmtes Datum in verschiedenen Prozeduren unterschiedliche Rollen spielen kann — primäre Eingabe-, Ausgabe oder UpdateDaten auf einer Ebene können auf einer anderen Ebene Arbeitsoder Steuer-Daten sein. Die Rolle, welche ein Datum im Programm spielt, hat einen wesentlichen Einfluß auf àie Attribute, welche der Programmierer ihm über seine Deklaration im Programmtext geben sollte. Die weithin übliche Vernachlässigung der überlegten Strukturierung von Daten gegenüber der von Algorithmen zeigt sich unter anderem auch darin, daß manche Programmiersprachen eine implizite oder kontextabhängige Deklaration von Daten erlauben (PL/1) oder sogar als Normalfall ansehen (FORTRAN). Die triviale Tatsache, daß ein im ausführbaren Code angesprochenes Datum auch definiert sein muß, veranlaß te die Entwickler von FORTRAN und PL/1, das Auftreten eines nicht explizit deklarierten Datennamens in einer verarbeitenden Anweisung einfach als implizite Deklaration aufzufassen, wobei die übrigen Attribute dieses Datums nach irgendwelchen sprachspezifischen Regeln festgelegt werden (Anfangsbuchstaben des Namens bei FORTRAN, zusätzlich Kontext bei PL/1). Der dadurch erhoffte Komfort, daß es keine Undefinierten Variablen mehr geben kann, wird aber durch neue Fehlermöglichkeiten überkompensiert: zwar sind jetzt alle vom Programmierer gewünschten Variablen automatisch deklariert, dafür aber auch alle, die er nicht wünschte, sondern die ihm nur durch Schreib- oder Ablochfehler in sein Programm eingeschleppt wurden. Tatsächlich sind derartige unerwünschte Variablen eine der unangenehmsten Fehlerquellen in FORTRAN- und PL/1-Programmen: der Programmierer wird ja nicht durch Fehlermeldungen des Compilers auf sie aufmerksam gemacht, sondern braucht zu ihrem Auffinden oft mehrere Testläufe. Erfahrene Programmierer deklarieren deshalb auch in diesen Sprachen grundsätzlich alle Variablen explizit. Die vom Compiler beim Ausdrucken der Variablenliste (hoffentlich) mitgelieferte Angabe, daß eine Variable implizit durch Auftreten in einer Anweisung deklariert wurde, dient dann als Pseudo-Fehlermeldung! Wie auch sonst in der Datenverarbeitung sind die Attribute (die Eigenschaften) einer Variablen meist ein Kompromiß zwischen den — logischen, problembezogenen Eigenschaften eines Datums und — seiner physikalischen Repräsentation (als Bitmuster auf einem Speicher mit bestimmten Zugriffseigenschaften).

1. Programmentwicklung

80

Die wichtigsten Datenattribute sind die folgenden: — Typ: Welche Operationen können auf das Datum sinnvoll angewandt werden (arithmetische Festkomma- oder Gleitkomma-Operationen, Zeichenmanipulation, Boole'sche Algebra), und welche Werte kann das Datum annehmen? Dieses Attribut ist oft eine Mischung von logischen und physikalischen Eigenschaften des Datums (DECIMAL PACKED heißt, daß das Datum logisch eine Dezimalzahl ist und physikalisch gepackt, d. h. 4 bit/Ziffer, dargestellt wird). — Der Wertebereich (Domäne, ränge) ist leider in den meisten Programmiersprachen ausschließlich durch die physikalische Realisierung vorgegeben. — Lebensdauer: Wie lange bleibt das Datum während des dynamischen Ablaufs des Programms definiert? — Scope: Für welche Anweisungen in der statischen Niederschrift eines Programms ist dieses Datum definiert? — Residenz und Organisation: Auf welchen Datenträgern wird das Datum gehalten, und auf welche Weise kann zu ihm zugegriffen werden. Die Residenz ist ein rein physikalisches Attribut, die Organisation ist logisch begründet, darf aber mit der Residenz nicht in Widerspruch stehen (vom Drucker kann nicht gelesen werden). In vielen Programmiersprachen können noch andere Attribute festgelegt werden, sie sind aber in der Regel nur eine Ergänzung oder feinere Unterscheidung der oben aufgeführten. Kann etwa (wie in PL/1) ein Datum als variable lange Zeichenkette mit einer vorgegebenen Maximallänge definiert werden, so ist dies nichts anderes als eine (logische) Festlegung des Typs und Wertebereichs, kombiniert mit einer Hilfe zur Wahl einer günstigen physikalischen Repräsentation. Zuweilen läßt sich auch noch die gewünschte Externdarstellung des Datums (z. B. auf einem Drucker oder Terminal) als PICTURE in COBOL und PL/1 oder durch Spezifikation der Kommastellung in FORTRAN- und PL/1-Formaten vereinbaren. Diese Attribute sollen hier nicht weiter besprochen werden. 1.4.3 Basistypen und Wertebereiche Die meisten Programmiersprachen bieten dem Benutzer eine Reihe von BasisDatentypen, die er für die Deklaration seiner Variablen benutzen kann. Für fast alle Programmiersprachen gilt, daß der Typ einer, durch ihren Namen definierten Variablen durch die (explizite oder implizite) Deklaration zur Übersetzungszeit festgelegt wird und sich während der Ausßhrungszeit nicht mehr ändern kann. Bei Sprachen mit Blockstruktur wie ALGOL und PL/1 sind hier-

1.4 Datenstrukturen

81

bei natürlich in verschiedenen Blöcken deklarierte Variablen mit identischen Namen als verschieden zu betrachten — der Übersetzer ergänzt üblicherweise den vom Programmierer gewählten Namen um eine, diesem nicht bekannte und zugängliche, Identifikation des Blocks der jeweiligen Deklaration. Bei ganz wenigen Sprachen, vor allem bei Listensprachen wie LISP [BERK64], kann sich der Typ einer Variablen zur Ausführungszeit ändern. Derartige Sprachen werden zuweilen typenlos („typeless") genannt - eine sicher nicht sehr glückliche Bezeichnung. Sehen Sie Gründe, warum nur selten derartige dynamische Variablentypen vorgesehen werden (vgl. [WAIT74], S. 170)? Die folgenden Basis-Datentypen werden nicht nur von PASCAL sondern auch von den meisten anderen Programmiersprachen (teilweise mit anderer Benennung) zur Verfügung gestellt: -

integer umfaßt die ganzen Zahlen zwischen zwei, meist implementierungsabhängigen, größten darstellbaren positiven und negativen Maximalwerten. real ist eine ebenfalls implementierungsabhängige Untermenge der reellen Zahlen in Gleitkomma-Darstellung. Boolean ist eine zweiwertige logische Variable mit den möglichen Werten false und true. char ist eine Variable, die ein Zeichen (Ziffer, Buchstabe oder Sonderzeichen) darstellen kann. alfa ist eine Zeichenkette (String), deren Maximallänge implementierungsabhängig ist.

Die Basis-Datentypen der jeweils benutzten Programmiersprache sollte der Programmierer als die Arbeitsspeicher-Einheiten der durch die Sprache definierten Basismaschine betrachten können. Er sollte sich weder um ihre Realisierung als Bitmuster im Speicher der „echten" Hardware kümmern müssen, noch sollte er sein Wissen um diese Realisierung ausnutzen. Jeder Rückgriff auf die (physikalische) Darstellung eines derartigen Typs bindet nämlich das Programm an die betreffende Hardware. Es ist dann nicht mehr ein PASCAL-, ALGOL- oder PL/1-Programm, sondern eines etwa für IBM/360PASCAL, - ALGOL oder -PL/1, oder es ist sogar von einem bestimmten Compiler auf einer bestimmten Hardware abhängig. In der Praxis allerdings wird diese Hardware-Unabhängigkeit dem Programmierer von den Sprachentwicklern und -implementierern nicht leicht gemacht. Eng mit den Basis-Datentypen ist nämlich üblicherweise ihr physikalischer Wertevorrat verknüpft:

82

1. Programmentwicklung

— Was ist die größte positive und negative als integer darstellbare ganze Zahl? — Wie genau werden raz/-Zahlen dargestellt, d. h. bei welcher Differenz zweier Zahlen müssen diese noch als im Rahmen der Rechengenauigkeit gleich angenommen werden, und welche Differenz garantiert, daß sie verschieden sind ('Signifikanz)? — Was ist der Zeichenvorrat einer char-Variablen, und welche dieser Zeichen sind auf der Ein-/Ausgabe-Peripherie der Maschine (Lochkartenleser und -Stanzer, Drucker, Konsolschreibmaschine, Terminals) darstellbar? — Gibt es ein Maximum für die Länge von Zeichenketten (intern und/oder für ihre Ein-/Ausgabe auf den verschiedenen peripheren Geräten), und was sind diese Obergrenzen? Dieser Wertevorrat der Variablen ist implementierungsabhängig und wird vom Entwickler des Compilers so gewählt, daß er den Hardwareeigenschaften der jeweiligen (realen) Maschine möglichst gut entspricht. Die Struktur des Arbeitsspeichers, der Arbeitsregister und des Befehlsvorrats der meisten Maschinen legt als physikalische Einheiten für die Darstellung von Daten als Bitmuster — ein Zeichen (Characterj von n c (meist 6 oder 8) bit — ein Wort von n w = k X n c (meist zwischen 12 und 64) bit nahe. Der Betrag der maximalen darstellbaren integer-Zahl liegt dementsprechend in der Größenordnung von 2 " w _ 1 . Meist ist die größte positive integer-Zahl 2 " w _ 1 - l , die größte negative jedoch - 2 n w _ 1 . Warum? Hinweis: negative integer-Zahlen werden in der Regel als 2er-Komplement der entsprechenden positiven Zahl dargestellt (vgl. [STEI62], S. 1085). Dieses erhält man, indem man in der Dualdarstellung jede 0 durch 1 und jede 1 durch 0 ersetzt und dann noch eine 1 addiert. Die Signifikanz und der maximale Wertebereich von real-Zahlen hängen von der gewählten Aufteilung der n w bit eines Wortes (oder der bits mehrerer, im Arbeitsspeicher hintereinanderliegender Worte) in Mantissen- und Exponententeil ab. Der interne Zeichenvorrat wird meist durch n c auf 2" c , also 64 oder 256 verschiedene Zeichen festgelegt. Ob alle diese Zeichen auch abdruckbar, also auf Drucker oder Terminals wiedergebbar sind, hängt zum einen von der Hardware des betreffenden Geräts ab, zum anderen aber auch davon, ob derartigen Zeichen durch irgendwelche Konventionen interne oder Ein-/Ausgabe-Steuer-Funktionen zugeteilt wurden und ob sie wegen dieser Sonderfunktion selbst nicht im Druckbild erscheinen.

1.4 Datenstrukturen

83

So hatten ältere Maschinen wie die IBM 1401 und die Siemens 3003 ein oder mehrere Endezeichen, welche den Abschluß von Zeichenketten markierten. Für die Datenübertragung [HOFE73] sehen die üblichen Codes ASCII und EBCDIC eine Reihe von Steuerzeichen vor, die ausschließlich dem Leitungsprotokoll zur Verständigung zwischen Sender und Empfänger vorbehalten sind und die selbst nicht ohne weiteres als Daten übertragen werden können. Ob es eine Obergrenze für die Länge von Zeichenketten gibt, wird schließlich dadurch bestimmt, ob man diese Länge von vornherein auf ein oder mehrere Worte (und damit auf ein Vielfaches von k Zeichen) festlegt (PASCAL), ob der Programmierer für jede Zeichenkette bei ihrer Deklaration die Länge als unveränderliches Attribut angibt (COBOL, PL/1) oder ob diese Länge variabel ist (PL/1) und wie diese Längeninformation intern gespeichert wird. Überlegen Sie sich einige Möglichkeiten für die interne Kodierung einer Zeichenkette samt ihrer Länge sowie die Vorteile und Nachteile der verschiedenen Methoden. Der logische Wertebereich einer Variablen ist immer durch das vorliegenden Programmierproblem und seine Lösung definiert und stimmt nur selten mit ihrem physikalischen Wertevorrat überein. So könnten etwa in einem Textverarbeitungsproblem als Zeichen nur Buchstaben und bestimmte Sonderzeichen, in einer Fakturierung als Jahreszahlen nur die des laufenden Jahrzehnts und in einer Bevölkerungsstatistik als Altersangaben nur die Zahlen 0 bis 120 relevant und sinnvoll sein - jeweils Untermengen der Typen char und integer. Die üblichen Programmiersprachen wie ALGOL, COBOL, FORTRAN und PL/1 erlauben jedoch keine Angabe dieses gewünschten Wertebereichs einer Variablen bei ihrer Deklaration. DOUBLE PRECISION in FORTRAN kann nicht in diesem Sinne als Festlegung eines Wertebereichs angesehen werden: diese Angabe besagt lediglich, daß der Mantissen-Teil einer real-Zahl in der Interndarstellung (mindestens) doppelt so groß wie normal sein soll. Da nicht definiert ist, was „normal" ist, gilt das gleiche natürlich auch für das doppelte davon!

84

1. Programmentwicklung

PASCAL bietet zur Deklaration von Wertebereichen mehrere Möglichkeiten, die bereits in Abschnitt 1.3.3.4 angedeutet wurden: — Ein skalarer Typ kann einfach als eine geordnete Menge von Werten definiert werden, die jeweils durch eine Bezeichnung (identifier) repräsentiert werden. Beispiele sind:

1.4.3/1

= (rot, gelb, blau); = (Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag); Familienstand = (ledig, verheiratet, verwitwet, geschieden);

type Farbe Wochentag

- Sowohl aus den Standardtypen integer und real als auch aus den explizit als Menge ihrer Werte definierten skalaren Typen kann ein Unterbereich (subrange) durch Angabe der Unter- und Obergrenze definiert werden. Beispiele sind: var

1.4.3/2

Tag: 1 . . 31; Jahr: 1900 . . 1999; Werktag: Montag.. Freitag;

Ein als geordnete Menge definierter skalarer Typ oder ein Unterbereich wird in der Regel intern als Dualzahl dargestellt werden. Wie der Compiler die Abbildung vornimmt, d. h. ob er (rot, gelb, grün) als (-1, 0,1), (0,1,2) (1,2,3) oder noch anders darstellt, bleibt dem Programmierer unbekannt — es ist ein Detail der physikalischen Realisierung, die mit der Logik der Problemlösung nichts zu tun hat. In den meisten anderen Programmiersprachen kann der Programmierer zwar die Benennung von Werten einer Variablen durch symbolische Bezeichnungen simulieren. Er muß jedoch ihre Zuordnung zu den jeweils durch sie repräsentierten Werten selbst vornehmen. Wie würden Sie dies in PL/1, FORTRAN und COBOL tun? Kann eine derartige, durch den Programmierer vorgenommene Benennung von Werten einer Variablen die Skalar-Definition als geordnete Menge in PASCAL voll ersetzen, oder hat diese weitere Vorteile? (Hinweis: Wie weit kann der Compiler in den beiden Fällen den Programmierer gegen eine Überschreitung des erlaubten Wertebereichs schützen? Was kann zur Übersetzungszeit und was zur Laufzeit geprüft werden?)

85

1.4 Datenstrukturen

Unter den hier aufgeführten (logischen) Basis-Datentypen werden vor allem Assembler-Programmierer einen vermissen: die Bitliste. Sie wird gern verwendet, um sich zu merken, welche von n verschiedenen möglichen Ereignissen im Laufe eines Programms aufgetreten sind. Ein Beispiel wäre etwa die folgende Programmieraufgabe: 1.4.3/3 Man lese von einer Datei ganze Zahlen ein, die Werte zwischen 1 und 9999 haben können. Nach Einlesen der Daten ist eine geordnete Liste aller Zahlen auszugeben, die vorkamen. Ein „typischer" Assemblerprogrammierer wird diese Aufgabe so lösen, daß er sich eine Bitliste von 9999 Bits im Arbeitsspeicher definiert, die er auf 0 initialisiert und von 1 bis 9999 numeriert. Dann liest er die Zahlen ein und setzt jeweils das Bit mit der Nummer der gelesenen Zahl auf 1 (gleichgültig, ob es noch auf 0 steht oder bereits vorher schon einmal auf 1 gesetzt worden war). Schließlich druckt er die Nummern aller Bits aus, die nach Lesen der letzten Zahl auf 1 gesetzt sind. Programmieren Sie die Aufgabe 1.4.3/3 in PASCAL und nehmen Sie dabei an, es gäbe folgende „eingebaute" Prozeduren - clearbits (n), welche eine Bitliste von n bits auf 0 initialisiert, und setbit (m), welche das m-te bit auf 1 setzt, sowie eine Funktion — checkbit (m), deren Wert true ist, falls das m-te bit gesetzt wurde, andernfalls false. Diese Methode ist deshalb nicht sehr befriedigend, weil die Bitliste eine physikalische Realisierung, nicht aber ein logisches Konzept ist. In einführenden Lehrbüchern für Programmierer wird sie deshalb oft als Schalterleiste nahegebracht, wobei sich der Programmierer vorstellen soll, daß das Setzen eines Bits dem Umlegen eines Schalters entspricht. Was ist das logische Konzept, zu dessen Realisierung die Bitliste dient und das deshalb unmittelbar in der Programmiersprache als geeigneter Datentyp vorhanden sein sollte (und in PASCAL auch ist)? In allen derartigen Fällen handelt es sich um folgende Situation: — Es ist eine geordnete Menge (set) von endlich vielen verschiedenen Elementen definiert. In Aufgabe 1.4.3/3 waren dies die Zahlen zwischen 1 und 9999.

86

1. Programmentwicklung

— Es soll eine Variable definiert werden, deren Wertebereich die Menge der möglichen Untermengen der ersten Menge ist. In Aufgabe 1.4.3/3 waren dies neben der leeren Menge alle möglichen Kombinationen von einer oder mehreren der Zahlen zwischen 1 und 9999. PASCAL kennt einen entsprechenden Variablentyp set. Die oben als Bitliste realisierte Variable könnte in PASCAL durch var numbers : set of 1 . . 9999;

1.4.3/4

deklariert werden. Andere mögliche Deklarationen (unter Verwendung der Typendeklaration 1.4.3/1) wären 1.4.3/5 type Mischfarbe = set of Farbe; var Feiertage, Arbeitstage: set of 1 . . 366; Fehltage: set of Montag . . Freitag; Regentage: set of Wochentag; Auf eine set-Variable können mengentheoretische Operationen angewendet werden, welche unmittelbar die logische Bedeutung des „Bitsetzens" und „Bitabfragens" wiedergeben. Es sind dies: — Die Durchschnittsbildung A : sind x, y, z sets, so liefert z •'= x A y in der Variablenz die Menge der Elemente, die sowohl i n * als auch i n y enthalten waren. — Die Vereinigung V : z '= x V y liefert als z die Menge der entweder in x oder in y (oder in beiden) enthaltenen Elemente. - Die Komplementbildung "1: z •'= ~~bc bildet in z die Menge der Elemente der Grundmenge, die in x nicht enthalten sind. - Abfrage auf Gleichheit, Ungleichheit und Enthaltensein: Eine Boolesche Variable b wird auf true gesetzt durch b:=x=y, wenn die sets x und y gleich sind, b-'=x¥=y, wenn die sets x und y ungleich sind, b-'-x^iy, wenn jedes Element aus je auch in y enthalten ist, b:=x>y, wenn jedes Element aus y auch in x enthalten ist. Ist schließlich a eine Variable, deren Wertebereich die Grundmenge zu dem set x ist, so hat b:=a in x den Wert true, wenn der aktuelle Wert von a in x enthalten ist.

1.4 Datenstruktuien

87

Eine set-Konstante wird durch Angabe ihrer Elemente in eckigen Klammern ausgedrückt, z. B. [1,13,9999], [Montag,Freitag], [rot], [ ] bedeutet die leere Menge. Verfolgen Sie (mit der Deklaration 1.4.3/1 für Wochentag) die Werte von b in dem folgenden Programm: var b: Boolean; x,y,z: set of Wochentag; begin x =[ ]; y:=x;z:= i x ; b = Montag in z; y = [Montag, Dienstag]; X = y A z b = Montag in z; b = x < z; b =x> z end. Das folgende Programm löst die Aufgabe 1.4.3/3 in PASCAL unter Verwendung des Datentyps set an Stelle der Bitliste. program LookforNumbers; const maxvalue = 9999; type values : 1 . . maxvalue; var numbers: set of values; i: values; x[in]:file of values; list[print]:file of values; begin numbers := []; get (x); while - ! eof(x) do begin numbers := numbers V [xt]; get(x) end; for i := 1 to maxvalue do if i in numbers then begin listt := i; put (list) end; end.

88

1. Programmentwicklung

1.4.4 Die Strukturierung von Daten 1.4.4.1 Der Unterschied zwischen Daten- und Ablaufstrukturen Ein einzelnes Datenelement eines der im letzten Abschnitt aufgezählten Basistypen (oder der sonstigen Basistypen, welche die jeweils benutzte Programmiersprache abweichend von den hier aufgeführten bereitstellt) wird meist Feld genannt. Es liegt nahe, es mit einer einzelnen verarbeiteten Anweisung (einem Elementarblock in der Terminologie von Abschnitt 1.3.3) zu vergleichen und zu fragen, wie — entsprechend dem Aufbau von Ablaufstrukturen aus den verarbeitenden Anweisungen — komplexere Datentypen aus logisch zusammengehörenden Feldern gleichen oder verschiedenen Typs aufgebaut werden können. Jackson [JACK74] versucht, zur Datenstrukturierung unmittelbar die logischen Grundstrukturen der Ablaufstrukturierung zu übernehmen (vgl. Abschnitt 1.3.3): — die Reihung, — dievluswfl/i/und — die Wiederholung. Dieser Ansatz ist fast, aber nicht ganz befriedigend — Ablaufstrukturen unterscheiden sich von Datenstrukturen darin, daß eine (statische) Reihung von verarbeitenden Anweisungen oder Elementarblöcken eine zeitliche (dynamische) Ablauffolge und damit vorher-nachher-Beziehungen impliziert, eine Zusammenfassung von Daten aber in der Regel nicht. Dementsprechend ist die Reihung als logisches Konzept zur Datenstrukturierung nicht unmittelbar einsichtig und sollte durch etwas anderes ersetzt werden. Was beabsichtigt ein Programmierer, wenn er etwa in COBOL oder PL/1 Felder zu einem Satz (record) zusammenfaßt, was in diesen Programmiersprachen durch eine Struktur wie der folgenden ausgedrückt wird: 1.4.4.1/1 1 PERSON 2 NAME 3 VORNAME. . . 3 NACHNAME.. . 2 ADRESSE 3 STADT . . . 3 STRASSE . . . 3 NUMMER. . . 2 EIGENSCHAFTEN 3 GESCHLECHT . . . 3 GEBURTSJAHR . . . 3 FAMILIENSTAND . . .

1.4 Datenstrukturen

89

Die Punkte (. . . ) sollen hierbei jeweils Basistypen andeuten, deren genaue Spezifikation in diesem Zusammenhang uninteressant ist. Die Datendeklaration 1.4.4.1 /I soll offensichtlich bedeuten, daß es eine logisch zusammenhängende, als Einheit zu speichernde und zu bearbeitende höhere Datenstruktur PERSON gibt, die aus drei Unterstrukturen NAME, ADRESSE und EIGENSCHAFTEN zusammengesetzt ist. Diese sind ihrerseits wieder jeweils aus Datenfeldern aufgebaut: die Unterstruktur NAME beispielsweise aus VORNAME und NACHNAME, welche als Basistypen (in diesem Fall sicher Zeichenketten einer bestimmten Maximallänge) erklärt sind. Im Gegensatz zur Reihung von verarbeitenden Anweisungen bedeutet 1.4.4.1/1 jedoch nicht, daß zuerst der NAME, dann die ADRESSE und dann die EIGENSCHAFTEN einer PERSON bearbeitet werden sollen. Und da den Programmierer die physikalische Realisierung seiner auf logischer Ebene deklarierten Daten nicht interessieren sollte, ist es für ihn auch irrelevant, ob die räumliche Anordnung der Bitmuster für die einzelnen Datenfelder im Arbeitsspeicher und ggf. auf den Externspeichern mit ihrer Reihenfolge in der Deklaration der Datenstruktur übereinstimmt oder nicht. In COBOL und PL/1 ist dies zwar der Fall, es können aber implementierungsbedingte Lücken, sogenannte Slag-Bytes, zwischen den einzelnen deklarierten Feldern auftreten. Der Programmierer sollte deshalb auch grundsätzlich keine Annahmen über die physikalische Speicherung eines Satzes machen. Die Bedeutung eines derartigen records, eines logischen Satzes, ist also unabhängig von der Reihenfolge seiner Felder. Die Menge der möglichen Werte der gesamten Datenstruktur, ihr Wertebereich, ist dadurch definiert, daß jedes der in ihr enthaltenen einzelnen Felder (unabhängig von den Werten der anderen) alle die Werte annehmen kann, die entsprechend seiner Deklaration seinen Wertebereich bilden. Eine derartige „Multiplikation" von Einzelmengen wird als ihr cartesisches Produkt bezeichnet. Im Beispiel 1.4.4.1/1 ist PERSON also als cartesisches Produkt von NAME, ADRESSE und EIGENSCHAFTEN dieser PERSON definiert, und jede dieser Unterstrukturen wieder als cartesische Produkte ihrer Felder. 1.4.4.2 D a s record Die cartesische Produktbildung ersetzt also als logisches Grundkonzept die Reihung für die Strukturierung von Daten [DAHL72]. In Anlehnung an den Sprachgebrauch in der kommerziellen Datenverarbeitung wurde ein derartiges cartesisches Produkt von Datentypen durch Hoare [HOAR68] als record bezeichnet. Es bildet in PASCAL einen höheren, zusammengesetzten Datentyp

90

1. Programmentwicklung

und kann als solcher deklariert werden. Für das Beispiel 1.4.4.1/1 kann folgende Typendeklaration formuliert werden: 1.4.4.2/1 = record vorname, nachname: alfa end; = record tadresse Stadt, strasse: alfa; nummer: integer end; teigenschaft = record geschlecht: (m,w); geburtsjahr: 1850 . . 2000; familienstand: (ledig, verheiratet, geschieden, verwitwet) end; = record tperson name: tname; adresse: tadresse; eigenschaften: teigenschaften end; var person : tperson; file of tperson; mitarbeiter: type tname

Ein bestimmtes Feld aus einem record wird durch Qualifizierung ausgewählt, wobei jeweils die Namen der höheren Strukturen, durch einen Punkt getrennt, vorangesetzt werden. Beispiele wären etwa die folgenden Zuweisungen:

1 4 4 2p15

person . name . vomame := 'otto'; person . eigenschaften . geburtsjahr := 1908; mitarbeitet . adresse := person . adresse;

15

Wie wir bereits früher (Abschn. 1.3.5) sahen, vereinfacht PASCAL die vollständige Qualifizierung durch die with-Anweisung: with person, eigenschaften do begin geschlecht '•= w\ geburtsjahr '•= 1940; familienstand '•= geschieden end;

91

1.4 Datenstrukturen

Sollten die Beispiele 1.4.4.1/1 und 1.4.4.2/1 nun den Eindruck erweckt haben, als sei das record-Konzept nicht viel mehr als eine andere Schreibweise für die Abspeicherung von Datensätzen auf Externspeichern, so ist dieser falsch. Zwar kommt die Bezeichnung record aus dem Datenmanagement auf Externspeichern. So wie das record in PASCAL definiert ist, ist es jedoch primär ein Sprachmittel zur Definition neuer, zusammengesetzter Datentypen, die in der Sprache als Basistypen nicht vorgesehen sind. Nach der Deklaration als record sind sie genauso als Einheiten anzusehen, wie in Abschnitt 1.3.3 jeder Ablauf-Strukturblock als Ganzes in einen höheren eingebettet werden konnte. Beispiele für derartige „höhere", als record definierte Datentypen gibt 1.4.4.2/3. 1.4.4.2/3 = record Tag: 1 . . 3 1 ; Monat: (Januar, Februar, März, April, Mai, Juni, Juli, August, September, Oktober, November, Dezember); Jahr: 1900.. 1999 end; complex = record R, I : real end;

type Datum

Mit derartigen Datentypen kann dann durch Definition von Funktionen die Sprache leicht erweitert werden. Ein Beispiel ist die folgende Funktion zur Addition komplexer Zahlen. 1.4.4.2/4 function cADD (cl, c2:complex) : complex; begin cADD.R := cl.R + c2.R; cADD.I := cl. I + c2.I end; Schreiben Sie entsprechende Funktionen cSUB, cMULT, cDIV für die übrigen arithmetischen Operationen mit komplexen Zahlen. Schreiben Sie eine Funktion function AddTage (dat:Datum, tage:integer):Datum, welche zu einem entsprechend 1.4.4.2/3 definierten Datum dat eine Anzahl

92

1. Programmentwicklung

tage von Tagen addiert (kann auch negativ sein!) und das entsprechende neue Datum liefert. 1 6 Ein record ist ein rein logischer Begriff. Über seine physikalische Darstellung als Bitmuster ist nichts ausgesagt. Diese ist entweder Sache des PASCAL-Compilers oder - wenn PASCAL nur als „Entwurfssprache" benutzt und der record anschließend in einer anderen Sprache, etwa PL/1 oder Assembler, realisiert wird — des Programmierers. Es gibt hierzu in der Regel viele verschiedene Möglichkeiten, die zwar sämtlich logisch äquivalent sind, vom Speicher- und Codieraufwand her jedoch unterschiedlich aufwendig und teilweise auch nicht in allen Sprachen realisierbar sind. Abb. 1.4.4.2-1 zeigt einige verschiedene konkrete Realisierungen des Datentyps Datum entsprechend der Definition 1.4.4.2/3. (a) Zeichenkette: Länge 14 Zeichen ä 8 bit

Tag

1 integer-Zahl (=32 bit für IBM/360) pro Feld

(4)

(8)

(2) (b) verkodet:

Jahr

Monat

Tag Monat Jahr

(c) gepackt verkodet : 1 byte (=8 bit, Wertebereich 0 . . 255) pro Feld

Tag

Monat

Jahr

(d) minimal gepackt : Tag = 5 bit (1 . . 3 1 ) Monat = 4 bit (1 . . 12) Jahr = 7 bit (0 .. 99) (a) Zeichenkette (b) verkodet

Tag

Monat

Jahr

(5)

(4)

(7)

= 112 bit = 96 bit

(c) gepackt verkodet = 24 bit (d) minimal gepackt = 16 bit

Abb. 1.4.4.2-1. Realisierungen eines Datums (vgl. Definition 1.4.4.2/3) 16

Die Formulierung wird einfacher, wenn folgende PASCAL-Standard-Funktionen verwendet werden: succ(x) liefert den Nachfolger, predfx) den Vorgänger

einer skalaren oder subrange-Variablen x (sofern er existiert). Ist dat als Datum (entsprechend 1.4.4.2/4) erklärt und hat es den Wert „27. April 1945", so liefert with dat do begin

Tag' =succ(Tag); Monat-=pred(Monat): Jahr: =succ(succ(Jahr))

end als neuen Wert von dat den 28. März 1947.

1.4 Datenstrukturen

93

1.4.4.3 Alternative Strukturen Im Gegensatz zur Reihung ist die Auswahl verschiedener, alternativer Strukturen je nach einem bestimmten Kennzeichen {tag) eine auch bei der Datenverwaltung logisch durchaus sinnvolle und oft erwünschte Funktion. Das Kennzeichen ist hierbei selbst Teil der Struktur, des records. Es gibt Auskunft darüber, wie die restliche Struktur aufgebaut und zu interpretieren ist. In PASCAL können derartige alternative records mit Hilfe einer case-Konstruktion deklariert werden. Sie ist der bei alternativen Abläufen (Abschn. 1.3.3.4) verwandten ähnlich, aber nicht genau gleich: record case tag: Fälle of Fallx :. .. (Deklaration,); Fallk :. .. (Deklarationk) end. Hierbei sind Fälle entweder ein (explizit oder durch seinen Typnamen bezeichneter) skalarer Typ oder ein Unterbereich (vgl. Abschnitt 1.4.3, bes. 1.4.3/1 und 1.4.3/2), Fa/^ bis Fall^ seine verschiedenen möglichen Werte und Deklaration l bis Deklaration^ die jeweiligen alternativen Felddeklarationen. Sind diese für mehrere der Werte gleich, so kann dies als Fall, : Fall} : (Deklarationj) zusammengefaßt werden. Eine alternative Datenstruktur bedeutet, daß das durch sie beschriebene reale Objekt unterschiedlich deklariert werden muß, je nach dem Wert eines Charakteristikums, des Kennzeichens tag. Beispiele hierfür geben 1.4.4.3/la und b.

type Getränk

= record case art:(Wein,Bier,Schnaps) of Wein: (färbe:(rot,weiß,rose); läge, traubenart : alfa; jahrgang: 1900 . . 1980); Bier: (typ:(hell,dunkel,pils); brauerei : alfa); Schnaps: (marke:alfa; alkoholgehalt: 15 . . 98) end;

1.4.4.3/la

94

1. Programmentwicklung

1.4.4.3/1 b

type Kalenderdatum = record case notation: (TMJ , TJ ) of TMJ: (Tag: 1 . 31; Monat: 1 . . 1 2 ; Jahr: 0 . 2000); TJ: (Tag: 1 . 366; Jahr: 0 . 2000) end;

In der Praxis wird es oft vorkommen, daß lediglich ein Teil der nötigen Angaben je nach dem Wert des tag unterschiedlich strukturiert sein muß, die übrigen aber unabhängig von ihm für alle beschriebenen Objekte gemeinsam sind. PASCAL sieht deshalb vor, daß vor, aber nicht hinter die case-Konstruktion ein fester recordTeil gesetzt werden darf. Diese Felder gelten für alle Alternativen gemeinsam. 1.4.4.3/3 gibt ein Beispiel hierfür.

1.4.4.3/2 type Person = record anschrift :

record stadt,strasse:alfa; nummer : integer end; kundennummer : integer; case art:(natürlich, juristisch) of natürlich: (name,vorname : alfa; geburtsdatum : Datum); juristisch: (firma, geschäftszweig, rechtsform : alfa) end;

Zwar besteht von der Problemlogik (der gewünschten Benutzermaschine) her o f t ein Bedürfnis nach derartigen alternativen Datenstrukturen, sie werden aber von den wenigsten Programmiersprachen (Basismaschinen) als Sprachmittel bereitgestellt. Sie müssen deshalb meist vom Programmierer selbst realisiert werden. Der

1.4 Datenstrukturen

95

Grund ist, daß ihre konkrete Implementierung sowohl von der Semantik her als auch vom Standpunkt der Verwaltung des physikalischen Speichers keineswegs einfach und oft fehleranfällig ist. Die semantische Schwierigkeit entsteht aus der Interpretation einer Änderung des Kennzeichens. Jede Änderung des tag ändert auch die Struktur des records. Dies wäre zuweilen noch logisch sinnvoll: im Typ Kalenderdatum in Beispiel 1.4.4.3/lb würde ein Umsetzen der notation eine Umrechnung der Darstellung des Datums bedingen. Eine Automatisierung dieser Umformung eines records ist jedoch unmöglich, da dem Computer die Semantik des Kennzeichens (die verschiedenen Felddefinitionen) und der Umrechnungsalgorithmus immer unbekannt sind. In vielen Fällen ist eine Änderung des tag sogar von der Semantik her sinnlos: die beiden anderen Beispiele 1.4.4.3/1 a und 1.4.4.3/2 illustrieren diesen Fall. Deshalb wird man den Wert des tag als prinzipiell unveränderliches Attribut des records ansehen, das während seiner ganzen Lebensdauer nur gelesen, aber nie verändert werden darf. Die Schwierigkeit in der Verwaltung des physikalischen Speichers liegt darin, daß in der Regel die verschiedenen alternativen record-Formate in der konkreten Darstellung als Bitmuster im Arbeitsspeicher oder auf Externspeichern unterschiedlich lang sein werden; Abb. 1.4.4.3-1 skizziert dies an Hand der obigen Beispiele. Das Problem wird noch ernster, wenn ein oder mehrere Unterstrukturen selbst wieder mehrere Alternativen besitzen — wenn also z. B. im Typ Person (1.4.4.3/2) für das Geburtsdatum bei natürlichen Personen der Typ Kalenderdatum entsprechend Beispiel 1.4.4.3/1 spezifiziert worden wäre. Der Implementierer von alternativen Datenstrukturen — sei es nun der Planer eines Compilers für eine sie ermöglichende Programmiersprache oder der Anwendungsprogrammierer, der sie benötigt — steht also vor einer schwierigen Entscheidung. Entweder muß er grundsätzlich genügend Platz für die längstmögliche Alternative reservieren und auf diese Weise unter Umständen viel Arbeitsoder Externspeicherplatz unnötig verschenken, oder er muß eine aufwendige dynamische Speicherverwaltung realisieren. In der Praxis sollte man deshalb versuchen, ohne alternative Datenstrukturen auszukommen. Dies ist auch meist ohne allzugroßen Verlust an logischer Durchsichtigkeit der Datenstruktur möglich. Da ohnehin der tag während der Lebensdauer des records nicht geändert werden darf und damit ein festes Attribut ist, spricht kaum etwas dagegen, die durch ihn ausgedrückte Differenzierung bereits im Namen des Datentyps auszudrücken. Im Fall des Beispiels 1.4.4.3/2 würde man wesentlich besser zwei Typen natürliche Person und juristische Person definieren und Daten dieser beiden verschiedenen Typen auch grundsätzlich getrennt, also jeweils in eigenen Arbeitsspeicherbereichen und Dateien, verwalten.

96

1. Programmentwicklung

Getränk: art traubenart j ahrgang |

Wein

färbe

läge

Bier

typ

brauerei

Sclinaps

marke

alkoholgehalt

Kalenderdatum: notation TMJ

tag

monat

TJ

tag

jähr

jähr

Person: Stadt

strasse

nummer

kundennummer

art

anschrift natürlich

name

vorname

geburtsdatum

juristisch

firma

geschäfts- rechtsform zweig

Abb. 1.4.4.3-1. Interndarstellung der alternativen Datenstrukturen (Beispiel 1.4.4.3/1 und 1.4.4.3/2)

1.4.4.4 D a s array Die Wiederholung schließlich ist in der Datenstrukturierung ebenso natürlich wie die Ablaufstrukturierung. Sie wird in PASCAL (wie in ALGOL und PL/1) durch Deklaration eines Datenelementes als array ausgedrückt. Häufige Ausdrücke sind auch Feld11, Vektor, Matrix, dimensionierte Variable (FORTRAN). COBOL spezifiziert eine derartige Wiederholung durch eine OCCURS-Klausel in der Datendeklaration. 17

Auf Grund der Verwechslungsmöglichkeit mit dem (Einzel-)Feld eines Satzes ist diese Bezeichnung besonders unglücklich.

1.4 Datenstrukturen

97

Eine array-Deklaration hat in PASCAL die Form type Name = array [indextyp] of

Komponenten

indextyp kann hierbei ein (entweder explizit oder über seinen Typnamen) angegebener skalarer Typ oder ein Unterbereich sein (vgl. Abschnitt 1.4.3, bes. 1.4.3/1 und 1.4.3/2). Komponenten ist ein beliebiger Typ. Das array kann als eine eindimensionale Kette von Komponenten aufgefaßt werden, wobei es für jeden index, d. h. jeden möglichen Wert von indextyp, genau ein Komponenten-Element gibt. Jede der einzelnen Komponenten ist durch ihren zugeordneten index gekennzeichnet. In der Regel wird ein eindimensionales array auch physikalisch im Speicher als eine eindimensionale Kette realisiert. Beispiel für arrays gibt 1.4.4.4/1. Wie in praktisch allen anderen höheren Sprachen kann ein array auch in PASCAL mehrdimensional sein. Statt eines indextyps wird in diesem Fall eine Liste von indextypen in der Deklaration angegeben. 1.4.4.4/1 const

type

var

p = 5 ; l v = 100; lcard = 80; Flaschenzahl = 100; = array [Wochentag, 1 . . p] of Person Besuchsplan {vgl. 1.4.3/1 und 1.4.4.3/2}; = array [1 . . 365] of Datum Jahr {vgl. 1.4.4.2/4}; Schaltjahr = array [1 . . 366] of Datum; : array [1 . . lv] of real; Vektor Lochkarte : array [ 1 . . lcard] of char; Verkehrsampel : array [(rot,gelb,grün)] of Boolean; : array [1 . . Flaschenzahl] of Getränk Hausbar {vgl. 1.4.4.3/1}; Man beachte, daß PASCAL keine arrays variabler Dimensionen kennt, wie sie in einigen Programmiersprachen definiert werden können (z. B. in ALGOL und PL/1). Die Vorschrift, daß die Dimensionsangabe(n) immer indextypen sind, der Wertebereich eines Typs aber bei seiner Deklaration festgelegt wird und sich zur Laufzeit nie ändern kann, schließt dies aus.

98

1. Programmentwicklung

Variable array-Dimensionen bringen ähnliche Schwierigkeiten mit sich, wie sie im Abschnitt 1.4.4.3 für alternative records bereits diskutiert wurden. Eine Ausnahme sind variable Dimensionen in formalen Parametern von Prozedur- oder Funktionsaufrufen, wie sie z. B. F O R T R A N erlaubt. Warum ist dies problemlos, und wozu sind variable, zur Laufzeit einstellbare Dimensionen in diesem Fall nützlich? Elemente eines arrays werden durch Angabe des gewünschten Indexwertes (bzw. der Indexwerte bei mehrfacher Dimensionierung) in eckigen Klammern hinter dem Namen des arrays angesprochen. Beispiele gibt 1.4.4.4/2. 1.4.4.4/2 Vektor[20] := Matrix [0, k + 1], for i := 1 to lcard do Lochkarte [i] := '

';

begin {Ende der Gelbphase einer Verkehrsampel} Verkehrsampel [gelb] := false; Verkehrsampel [rot] := ~~' Verkehrsampel [rot]; Verkehrsampel [grün] := i Verkehrsampel [grün] end; for i := 1 to Flaschenzahi do with Hausbar [i] do if art = Wein A Jahrgang < 1960 then aussortieren; Da das array wohl die bekannteste Datenstruktur ist, dürfte sich ein weiteres Eingehen auf sie nicht lohnen. Es sollten lediglich in Tabelle 1.4.4.4-1 noch seine Unterschiede zum record (Abschn. 1.4.4.2) zusammengestellt werden, da diese zwar wesentlich und grundlegend sind, o f t aber nicht klar getrennt werden. Tab. 1.4.4.4-1. Unterschiede zwischen record und array

record

array

Typen der Elemente

ggf. unterschiedlich

identisch

Anordnung der Elemente wesentlich?

nein, für den Anwender irrelevant

ja, durch Indexordnung gegeben

Ansprache der Elemente

durch qualifizierte Namen: a.b.c

durch Angabe des Index: a[i]

1.4 Datenstrukturen

99

1.4.5 Referenzen (Zeiger, Adreßvariablen) Bis jetzt betrachteten wir ein Datum als ein mit einem Namen bezeichnetes Objekt, dem Werte aus einem Wertevorrat zugewiesen werden können. Man kann es jedoch auch als Namen für einen Speicherplatz ansehen, der mit einem Wert „gefüllt" werden kann. Überlegt man sich diese beiden Interpretationen des Datumsbegriffs an einer üblichen Zuweisungsanweisung, wie etwa y -=a +x, so ist auf der linken Seite der Speicheraspekt, auf der rechten der Wertaspekt stärker betont: Die aktuellen Werte von a und x werden addiert und das Ergebnis in y gespeichert. Von „rechts" und „links" wurden die Bezeichnungen R-Wert und L-Wert abgeleitet. Es charakterisieren der - R-Wert (R-value) das Datum als Träger eines Wertes und der - L-Wert (L-vahie) das Datum als Ort im Speicher [STRA67, SCHU75], So lange der Programmierer mit den von seiner Programmiersprache bereitgestellten Basisdatentypen und Datenstrukturen auskommt, braucht ihn dieser Unterschied nicht sehr zu kümmern. Dies wird anders, sobald er komplexere Datenaggregate realisieren muß und diese vielleicht noch dynamisch veränderlich sind. Ab dann genügt es ihm nämlich nicht mehr, nur zu den Werten seiner Daten zugreifen zu können, sondern er muß auch Referenzen auf sie manipulieren können. Referenz ist der Oberbegriff für die verschiedenen Möglichkeiten, ein bestimmtes Datenobjekt über einen Software- oder Hardwaremechanismus eindeutig ansprechen zu können. Je nach der logischen Ebene kann eine Referenz ein Name (Zeichenkette), ein Zeiger (POINTER) oder eine (Hardware-) Adresse sein. Abb. 1.4.5-1 zeigt dies an der Darstellung der Struktur einer Familie. — Die Aufgabenstellung beschreibe sie als eine Tabelle®. Die problembezogenen Referenzen sind hier die in den einzelnen Spalten eingetragenen Namen (Zeichenketten). — Beim logischen Entwurf seines Programms wird sich der Programmierer wahrscheinlich die Datenstruktur als ein Netzwerk Q) von Verweisen (einen Graphen) vorstellen. Die logischen Referenzen sind hier Zeiger (die gerichteten Kanten des Graphen). — In der Hardware-Realisierung wird jedem Familienmitglied eine Speicherzelle (record) zugeordnet Q) . Die physikalischen Referenzen sind die Adressen dieser Speicherzellen.

1. Programmentwicklung

100

O Name

Ehegatte

Kind-1

Kind-2

Kind-n

Hugo Anna Anatol Elfriede Oskar Maria

Anna Hugo Maria

Anatol Anatol Max

Elfriede Elfriede

Oskar Oskar

-

An atol





-

-

-

-

-

-

-

-

Max

© 1000

Max

1001

0

0

0

0

Anna

1005

1003

1004

1002

1002

Oskar

0

0

0

0

1003

Anatol

1006

1000

0

0

1004

Elfriede

0

0

0

0

1005

Hugo

1001

1003

1004

1002

1006

Maria

1003

1000

0

0

Adressen

Speicherzellen

Abb. 1.4.5-1. Darstellungen einer Familienstruktur (l)problembezogen, (5)logisch, ^ p h y s i k a l i s c h

1.4 Datenstrukturen

101

Je nach dem Komfort der benutzten Programmiersprache muß der Programmierer die problembezogene Darstellung seiner Datenstruktur in ein hardwarenäheres Konzept umsetzen, um seine Verarbeitungsalgorithmen formulieren zu können. — Keine der üblichen Programmiersprachen erlaubt unmittelbar die Verwendung von problemorientierten Namen, d. h. allgemeinen alphanumerischen Zeichenketten, als Referenzen auf Daten. Lediglich Datenbanksysteme realisieren einen derartigen assoziativen Zugriff. — Manche höheren Programmiersprachen wie PL/1 und PASCAL kennen für Referenzen auf logischer Ebene den Datentyp Zeiger (pointer), mit denen der Programmierer Listen, Bäume und andere Graphenstrukturen definieren, aufbauen und dynamisch verändern kann. — In Assemblern wie BAL muß der Programmierer Referenzen physikalisch als (Hardware-) Adressen realisieren. In höheren Sprachen ohne den Typ Zeiger können die Indizes eines arrays dem gleichen Zweck dienen (vgl. Abb. 1.3.5-2 undAbschn. 1.5.1.3). Die frei verwendbaren Zeiger in der Darstellung einer Datenstruktur als Graph (Abb. 1.4.5-1 Q) ) haben Ähnlichkeit mit den ebenso frei benutzbaren Konnektoren des konventionellen Flußdiagramms (Abb. 1.3.2-1). Ihre Realisierung durch Maschinenadressen oder Indizes in arrays ähnelt den Sprungbefehlen an grundsätzlich beliebige Stellen des Programmtextes. Bei der Bearbeitung vernetzter Datenaggregate entstehen damit die gleichen Probleme wie in umfangreichen, unstrukturierten Programmabläufen : Unübersichtlichkeit, Fehleranfälligkeit und Änderungsunfreundlichkeit. Auch die Abhilfe ist die gleiche : eine konsequente Top Down-Strukturierung der Datenaggregate vom problembezogenen Ansatz zur logischen und physikalischen Realisierung [HOAR75], Leider unterstützen auch moderne Programmiersprachen den Programmierer hierbei noch viel weniger durch geeignete sprachliche Mittel als bei der Ablaufstrukturierung. Einen ersten Schritt in dieser Richtung zeigt PASCAL: ein pointer wird in dieser Sprache bei der Deklaration immer einem bestimmten Typ (in der Praxis meist einem record-Typ) zugeordnet. Dadurch kann der Übersetzer bereits prüfen, ob die referierten Daten von diesem Typ und damit wenigstens korrekt interpretierbar sind. „Wildlaufende" Zeiger werden hierdurch ebenso sicher vermieden wie „wilde" Sprünge durch eine i f . . . then . . . else-Konstruktion. Erfahrene Programmierer simulieren diese feste Zuordnung von Zeiger- oder Adreßvariablen zu bestimmten record-Typen schon seit langem durch die Verwendung von Zeigerschlüsseln. Hierbei werden jedem record ein Typenschloß und jedem Zeiger ein Zeigerschlüssel zugeordnet:

102

1. Programmentwicklung

1.4.5/1 var

pzeiger :

record Zeigerschlüssel : char {Initialisiert auf 'p'}; Zeiger: pointer end; personalsatz : record Typenschloß : char {Initialisiert auf'p'}; Name, Vorname : alfa; end;

Vor jedem Zugriff über einen Zeiger wird dann überprüft, ob Zeigerschlüssel und Typenschloß übereinstimmen. Andernfalls wird eine Fehlermeldung gegeben. Die Verwendung von Zeigerschlüsseln bringt zwar eine gewisse. Einbuße an Laufzeiteffektivität. Zumindest bei der Verwaltung großer Datenbasen auf Externspeichern läßt sich diese aber wegen des hohen Zeit- und Kostenaufwands für die Wiederherstellung der Datenbestände nach Auftreten von Fehlern durchaus rechtfertigen. Die von PASCAL unterstützte Kopplung von pointer und Typ bringt keine derartige Effektivitätseinbuße. Warum?

1.5 Datenverwaltung 1.5. Daten im Arbeitsspeicher 1.5.1.1 Scope, Lebensdauer und Speicherklasse Die Zusammenfassung verschiedener Daten oder Unterstrukturen zu höheren Strukturen entsprechend Abschn. 1.4.4 bedeutet eine logische Zusammengehörigkeit dieser Daten. Eine derartige Datenstruktur wird deshalb von den sie bearbeitenden Prozeduren in Bezug auf den Zugriff zu ihr als Einheit angesehen. Attribute, welche die Zuordnung von Datenstruktur und Ablaufstruktur betreffen, sind deshalb immer Attribute der gesamten Struktur und nicht einzelner Felder oder Unterstrukturen (wie Typen und Wertebereiche). Derartige Attribute sind der Scope (Gültigkeitsbereich) und die Lebensdauer. Das Scope-Attribut sagt aus, für welche Prozeduren oder — in ALGOL oder PL/1 — Blöcke eine bestimmte, durch ihren bei der Deklaration angegebenen Namen bezeichnete Datenstruktur statisch definiert ist, d. h. in welchen verarbeitenden Anweisungen sie oder ihre Einzelfelder angesprochen werden können.

1.5 Datenverwaltung

103

Die Lebensdauer eines Datums gibt an, wie lange ein ihm zugeteilter Wert gültig bleibt, d. h. wie lange er während des dynamischen Ablaufs des Programms bei einem Zugriff unter dem Namen des Datums wieder gefunden wird. Hierbei wird natürlich eine explizite Neuzuweisung eines anderen Wertes außer Acht gelassen. Für die Bestimmung der Lebensdauer ist nur entscheidend, wann ein Datum, verursacht durch den dynamischen Lauf des aktuellen Ausführungsortes (locus ofcontrol) durch das Programm, seinen Wert ändert, also etwa wieder Undefiniert wird oder einen früher zugewiesenen Wert zurückerhält. Welchen Scope ein Datum (oder eine Datenstruktur) haben muß, ist meist ziemlich einfach festzulegen : überall dort, wo es in verarbeitenden Anweisungen angesprochen wird, muß es auch definiert sein. Bei einer Darstellung des Programms als HIPOs, wie sie Abb. 1.4.1-1 zeigt, muß jedes Datum spätestens in den HIPOs bekannt sein, in welchen es bei der Verarbeitung verwendet wird. Es kann aber auch bereits in HIPOs definiert werden, die diesen gemäß dem Programmbaum (Abb. 1.4.1-2) übergeordnet sind. Dies ist nicht nur dann notwendig, wenn das betreffende Datum in den höheren HIPOs explizit angesprochen wird. Auch wenn es, etwa wie die speicherinterne Wortliste im Beispiel 1.4.1-1, Ausgabe eines und Eingabe eines auf gleicher Ebene stehenden anderen HIPOs ist, muß es bereits in einer beiden übergeordneten Prozedur (HIPO) definiert werden. Deren Aufgabe ist es lediglich, das Datum „weiterzureichen". (Vgl. hierzu die Definition von list bereits in der „obersten" Prozedur des Programms „Häufigkeitszählung", 1.2.3/4). PASCAL verlangt — wie COBOL und ALGOL — die explizite Deklaration jeder benutzten Variablen durch den Programmierer. Wie wir bereits sahen, geschieht dies hinter dem Schlüsselwort var durch Nennung des Namens, gefolgt von einem Doppelpunkt und dem gewünschten Typ. Der Scope der Variablen ist damit festgelegt auf — die Prozedur, in welcher die Variable erklärt ist, sowie — alle ihr untergeordneten, d. h. in ihr (oder ihrerseits untergeordneten) neu definierten Prozeduren, soweit diese nicht eine erneute Deklaration des gleichen Variablennamens enthalten. Die gleichen Scope-Regeln gelten auch für — formale Parameter von Prozeduren, die bei der Prozedurerklärung hinter deren Namen in Klammern angegeben werden,

1. Programmentwicklung

104

— Typ- und Konstanten-Vereinbarungen (type, const), — Labels und Prozedurnamen. Überlegen Sie sich den Scope der einzelnen Variablen, Typen und Konstanten in dem Beispielprogramm „Häufigkeitszählung von Worten", 1.2.3/4 bis 1.2.3/12. Die verschiedenen Programmiersprachen unterscheiden sich sehr in den von ihnen vorgesehenen Scope-Regeln. Tab. 1.5.1.1-1 stellt diese für die häufigsten Programmiersprachen zusammen. Tab. 1.5.1.1-1. Scope-Regeln der häufigsten Programmiersprachen Sprache

explizite Deklaration

implizite Deklaration

ALGOL 60

nicht möglich Zu Beginn jedes Blockes (begin . . . end); Scope ist der gesamte Block und alle von ihm eingeschlossenen Blöcke und Prozeduren, soweit keine Neudeklaration erfolgt.

BAL (Assembler IBM /360 - /370, Siemens 4004, Univac 9000)

Der gesamte zusammen übersetzte Programmtext, ohne Rücksicht auf ggf. vorgenommene Aufteilung in Kontrollabschnitte (CSECTs).

COBOL

Alle in der DATA DIVISION deklarierten nicht möglich Variablen gelten für das gesamte Programm.

FORTRAN

Die Routine (Hauptprogramm oder Subroutine), in welcher das Datum deklariert wird. Durch COMMON-Deklarationen können sich mehrere Routinen (unter gleichen oder verschiedenen Namen) auf das gleiche Datum beziehen.

PASCAL

Zu Begin jeder Prozedur; Scope ist die ge- nicht möglich samte Prozedur und alle innerhalb von ihr erklärten, soweit keine Neudeklaration des Datums erfolgt.

PL/1

Uberall möglich, Scope ist jeweils der unmittelbar umschließende BEGIN-ENDBlock bzw. Prozedur. Durch EXTERNALDeklaration können Namen und die durch sie bezeichneten Daten als extern, d. h. auch von anderen Prozeduren ansprechbar, bezeichnet werden.

nicht möglich

Die Routine (Hauptprogramm oder Subroutine), in welcher das Datum auftritt.

Die äußerste umschliessende Prozedur (in der Regel Hauptprogramm: OPTIONS (MAIN)).

Die Lebensdauer eines Datums muß nicht mit dem Scope der Deklaration seines Namens zusammenhängen. Sie ist vielmehr davon abhängig, wann der Arbeitsspeicher für das betreffende Datum zugeteilt und wieder freigegeben wird. In PL/1 ist diese Speicherzuteilung deshalb auch ein explizit anzugebendes Attribut Speicherklasse (storage class) eines Datums.

1.5 Datenverwaltung

105

Bei den meisten Programmiersprachen gilt für die Speicherzuteilung bei allen Daten die gleiche Regel. Diese ist mit wenigen Ausnahmen (vor allem Listensprachen wie LISP [BERK64]) eine der Speicherklassen von PL/1. Damit ist auch die Lebensdauer eines Datums bei diesen Programmiersprachen eine spezifische Eigenschaft der Sprache und muß bei der Datendeklaration nicht explizit als Attribut angegeben werden. PL/1 kennt die folgenden Speicherklassen: — Automatisch {AUTOMATIC)', beim (dynamischen) Durchlaufen des Anfangs (BEGIN, PROCEDURE) des Scopes der Datendeklaration wird der Speicher zugeordnet, bei seinem Verlassen (zugeordnetes END) wieder freigegeben. Die Lebensdauer eines als automatisch deklarierten Datums entspricht deshalb (bis auf innere Neudeklarationen) seinem statischen Scope. — Statisch (STA TIC): Der Speicher wird bei Beginn des Programmlaufes18 zugeteilt und erst an seinem Ende wieder freigegeben. Damit ist die Lebensdauer eines als statisch deklarierten Datums die gesamte Laufzeit des Programms. Wenn der Ausführungsort (locus of control) beim dynamischen Ablauf des Programms den Scope der Datendeklaration verläßt, ist das Datum zwar nicht mehr ansprechbar, bleibt aber erhalten. Sobald sich die Ausfuhrung des Programms wieder innerhalb des Scopes bewegt, ist sein (alter) Wert wieder zugänglich. — Kontrolliert (CONTROLLED). Zuteilung und Freigabe des Speichers wird durch den Programmierer über besondere Anweisungen (ALLOCATE und FREE) verlangt. Die Lebensdauer eines Datums reicht vom ALLOCATE bis zum zugehörigen FREE, unabhängig, ob sein Scope zwischendurch einmal verlassen wurde (entsprechend statischen Variablen). Wird nach einem ALLOCATE für die gleiche Variable ein zweites gegeben, so wird ein neuer Speicherbereich für dieses Datum zugeteilt, ohne den ersten freizugeben. Eine Ansprache des Datennamens bezieht sich ab dann auf den neuen Speicherbereich; der im alten stehende Wert bleibt erhalten, ist jedoch temporär unzugänglich bis zur Freigabe des neu zugeteilten Speichers durch das (erste) FREE. Nach dieser Freigabe ist der alte Wert wieder verfügbar. — Dynamisch (BASED): derartige Daten werden (wie kontrolliert gespeicherte) vom Programmierer explizit angefordert und freigegeben (ALLOCATE und FREE). Sie werden jedoch prinzipiell über einen Zeiger (POINTER) verwaltet. Dessen Wert erhält der Programmierer bei der Zuteilung (ALLOCATE) mitgeteilt; bei Ansprache des Datums und bei seiner Freigabe muß er sich auf ihn beziehen. Die Lebensdauer eines dynamischen Datums reicht von seiner Zuteilung (ALLOCATE) bis zu seiner Freigabe (FREE). 18

bzw. der Task in einer Multitasking-Umgebung [IBMOOe]

1. Programmentwicklung

106

Welche Speicherklassen und damit welche Lebensdauer für Daten die üblichen Programmiersprachen vorsehen, geht aus Tabelle 1.5.1.1-2 hervor. Bei der Angabe „gesamte Programmlaufzeit" für die Lebensdauer statischer Daten ist hierbei vorausgesetzt, daß keine Überlagerungsstrukturen [HOAR72a] im Programm vorgesehen sind. Ist dies der Fall, so kann es — abhängig von der Realisierung im benutzten Betriebssystem — sein, daß bei der Überlagerung eines Programmsegments durch ein anderes die Daten des ersten zerstört werden, auch wenn sie gemäß den Regeln der verwendeten Programmiersprache nach erneutem Aufruf des ersten Segments eigentlich noch verfugbar sein müßten.

Tab. 1.5.1.1-2. Speicherklassen und Lebensdauer von Variablen der häufigsten Programmiersprachen Sprache

Speicherklasse

Lebensdauer

ALGOL 60

automatisch

entspricht Scope

statisch (own)

gesamte Programmlaufzeit

BAL (Assembler IBM /360 - /370, Siemens 4004, Univac 9000)

statisch

gesamte Programmlaufzeit

dynamisch (Uber DSECT)

zwischen Zuteilung und Freigabe des Speichers (Speicherverwaltung durch Programmierer selbst oder durch Ein/AusgabeMakros)

COBOL

statisch

gesamte Programmlaufzeit

FORTRAN

statisch

gesamte Programmlaufzeit

PASCAL

PL/1

automatisch*)

entspricht Scope

dynamisch**)

von Zuteilung (new) bis Freigabe (dispose)

automatisch

entspricht Scope

statisch

gesamte Programmlaufzeit (Task)

kontrolliert

zwischen Zuteilung und (zugehöriger) FreiIgabe des Arbeitsspeichers (ALLOCATE bis FREE)

dynamisch

*) In PASCAL als „static variable" bezeichnet. **) Ob der durch dispose freigegebene Speicher zur Neuzuteilung (durch new) wieder zur Verfügung gestellt wird, ist implementierungsabhängig.

Tab. 1.5.1.1-1 und 1.5.1.1-2 zeigen, daß die meisten Programmiersprachen dem Programmierer sowohl in der Festlegung des Scopes als auch in der Wahl von Speicherklasse und Lebensdauer seiner Variablen wenig Auswahl lassen.

1.5 Datenverwaltung

107

Diese Einschränkungen sind in Bezug auf das Scope-Attribut zwar für den Programmierer nicht sehr einschneidend. Wird jedoch — wie es etwa in COBOL oder BAL ohnehin die einzige Möglichkeit ist - der Scope jeder Variablen global für das ganze Programm festgelegt, so leidet hierdurch die Programmiersicherheit, und es besteht die Gefahr von Namenskonflikten. Wieso kann eine lokale Definition von Variablen für einzelne Blöcke oder Prozeduren die Sicherheit erhöhen? Wie realisiert man globale Variable in PASCAL, FORTRAN, ALGOL und PL/1? Das Attribut der Lebensdauer (und damit die sie begründende Speicherklasse) hat dagegen wesentlichen Einfluß auf das Verhalten des Programms. Je nach der zu lösenden Aufgabe braucht der Programmierer statische, automatische oder dynamische Speicherung eines Datums. Bietet ihm die betreffende Programmiersprache die betreffende Speicherungsform nicht, muß er sie mit Hilfe der ihm zur Verfügung stehenden simulieren, um die dem jeweiligen Programmierproblem entsprechende Lebensdauer seiner Variablen zu gewährleisten. Der einfachste Fall ist eine Lebensdauer über die gesamte Laufzeit des Programms. Diese wird benötigt, wenn sich eine Prozedur von einem Aufruf zum nächsten einen Wert, einen Zustand, merken muß. Beispiele sind etwa die Zahl der Aufrufe der Prozedur selbst oder die jeweils letzte ausgegebene „Zufalls"-Zahl bei einem (Pseudo-) RandomGenerator ([KNUT69], S. 9ff). Eine statisch gespeicherte Variable, wie sie FORTRAN, COBOL oder PL/1 vorsehen, hat diese „unendliche" Lebensdauer. Sie ist daher die geeignete Speicherungsform für eine Zustandsvariable. Wie realisieren Sie in PASCAL eine Zustandsvariable, deren Lebensdauer die gesamte Programmlaufzeit umfaßt? 1.5.1.2 D e r Keller (Stack) Welchen Sinn hat die automatische Speicherzuteilung und die mit ihr verbundene, dem statischen Scope unmittelbar entsprechende Lebensdauer einer Variablen? In PASCAL, wo der Scope grundsätzlich die Prozedur umfaßt, an deren Anfang die betreffende Variable (entweder über das Schlüsselwort var oder als formaler Parameter) erklärt wurde, ist die logische Bedeutung der Lebensdauer einer Variablen aus der physikalischen Realisierung dieser Speicherklasse ableitbar. Abb. 1.5.1.2-1 zeigt schematisch ein PASCAL-Programm mit zwei ineinander geschachtelten Prozedurerklärungen p 1 und p2, wobei im Rumpf von p t die Prozedur p2 aufgerufen wird. Im Hauptprogramm selbst wird p, aufgerufen.

108

1. Programmentwicklung

Bei jedem Prozeduraufruf wird für die neu (durch Deklaration oder als formale Parameter) eingeführten Variablen neuer Speicherplatz im Anschluß an den bereits vergebenen bereitgestellt. Handelt es sich um einen formalen Parameter, so wird dieser mit dem Wert des Ausdrucks initialisiert, welcher im Aufruf als aktueller Parameter angegeben ist. Dieser Wert wird innerhalb der Prozedur als Konstante betrachtet. PASCAL erlaubt auch variable formale Parameter — sie müssen in der Parameterliste durch ein Voranstellen des Schlüsselworts var kenntlich gemacht werden. Sie werden durch einen Zeiger auf die aktuelle, im Aufruf genannte Variable realisiert (die in diesem Fall kein Ausdruck sein darf). Allgemein unterscheidet man - call by value: Übernahme des Wertes des aktuellen Parameters bei Aufruf der Prozedur (normaler, „konstanter" Parameter in PASCAL),

1.5 Datenverwaltung

109

— call by reference: dem formalen Parameter wird ein Zeiger (Referenz) auf das bei Aufruf der Prozedur als aktueller Parameter gegebene Datum zugeordnet (var-Parameter in PASCAL), — call by name'. bei jeder Ansprache des formalen Parameters im Prozedurrumpf wird der aktuelle Wert entsprechend dem Prozeduraufruf — gleichgültig ob Variable oder beliebig komplexer Ausdruck — neu ausgewertet und übernommen (in PASCAL nicht vorgesehen). Überlegen Sie sich, wie die für den call by name verlangte Neuauswertung des formalen Parameters organisiert werden kann (THUNKMechanismus, vgl. [INGE62]). Wegen der nicht einfachen Realisierung wird der call by name unter den üblichen Programmiersprachen nur von ALGOL zur Verfügung gestellt. Jeder Datenname steht für den ihm dynamisch als letzter zugewiesenen Speicherplatz und den in ihn jeweils abgespeicherten Wert; ggf. früher definierte Daten gleichen Namens sind unzugänglich. Wie in Abb. 1.5.1.2-1 am Beispiel des Namens a zu sehen, ist dies der „unterste" Speicherplatz dieses Namens im Arbeitsspeicher. Nach Beendigung eines Prozeduraufrufs wird der ihm zugeordnete Speicher wieder freigegeben, und die Lebensdauer der lokalen Daten der Prozedur ist beendet. Damit werden auch Daten in „höheren" Prozeduren, deren Namen mit denen von lokalen Daten übereinstimmte und die deshalb nicht mehr erreichbar waren, wieder zugänglich — so z. B. die in p1 deklarierten Variablen a und c nach Abschluß des Prozeduraufrufs P2 in Abb. 1.5.1.2-1. Eine derartige, für die automatische' Datenverwaltung charakteristische Speicherorganisation wird in der deutschen Literatur als Keller bezeichnet, weil die .neuen' Speicherplätze jeweils „unten" an die alten angefugt werden. Die angelsächsische Literatur benutzt das umgekehrte Bild: sie stellt sich vor, daß die neudeklarierten Speicherplätze jeweils „auf" die alten darübergestapelt werden. Sie nennt den gleichen Speicherverwaltungsmechanismus deshalb Stack. Im Gegensatz zur statischen Speicherverwaltung werden im Keller beim Neuzuteilen eines Speicherplatzes mit einem bestimmten Namen die Datenwerte in den ggf. früher bereits unter diesem Namen angelegten .höheren' Speicherplätzen nicht zerstört. Später, nach Freigabe des neuen Speichers, stehen sie wieder zur Verfügung. Dies ist wesentlich für die in Abschn. 1.3.5 bereits besprochene Rekursion. Ein „automatisches" Funktionieren der Rekursion erfordert entweder automatische oder die in PL/1 vorgesehene kontrollierte Speicherverwaltung (vgl. Abschn. 1.5.1.1).

110

1. Programmentwicklung Fak(4)=24

11 n=4 Fak (n) Fak:=4*6

)\ 6

n=3 Fak ( n - 1 )

y

Fak:= 3 * 2 >

Fak ( n - 2 ) Fak:=2*l n=l Fak ( n - 3 ) Fak.-l*l n=0 Fak ( n - 4 ) Fak:=l

\ 2

n=2

y

\l

y i

/

Abb. 1.5.1.2-2. Keller bei Berechnung der Fakultät n! für n = 4 mit Hilfe der rekursiven Funktion FAK (n) (vgl. 1.3.5/1)

Abb. 1.5.1.2-2 zeigt dies an Hand der in Beispiel 1.3.5/1 gegebenen rekursiven Funktion FAK(n), die sich bis n=0 jeweils mit um 1 erniedrigten Werten von n selbst wieder aufruft. Jedem dieser neuen Aufrufe sind zwei Keller-Speicherplätze zugeordnet: der aktuelle Wert des formalen Parameters n und der Arbeitsspeicher FAK, in welchem der Funktionswert errechnet wird. Auf Grund der in der Sprachdefinition vorgesehenen Speicherverwaltung über einen Keller ist die Rekursion ein „natürliches" Ausdrucksmittel in PASCAL, ALGOL und PL/1 (bei Verwendung der Speicherklassen AUTOMATIC und CONTROLLED). Sie ist es nicht in FORTRAN und COBOL. Die in diesen Sprachen ausschließlich vorhandene statische Speicherverwaltung führt bei rekursivem Aufruf einer Prozedur durch sich selbst (auch bei Zwischenschalten anderer Prozedurren) zu Fehlern durch Überschreiben von Datenwerten durch tiefere Aufrufe. Vollziehen Sie dies am Beispiel der Funktion FAK(n)

111

1.5 Datenverwaltung

bei Annahme einer FORTRAN-artigen, statischen Speicherverwaltung nach. In Sprachen mit ausschließlich statischer Speicherverwaltung muß ein rekursiv formulierter Algorithmus so umgeformt werden, daß er mit einem vom Programmierer selbst verwalteten Keller arbeitet. Ein Stack (Keller) für eine Variable v des Typs T ist ein array dieses Typs, in welches ein Zeiger (spointer) jeweils auf den kleinsten unbesetzten Indexwert zeigt (vgl. Schema 1.5.1.2/1). Es gibt zwei Primitivoperationen push und pop. 1.5.1.2/1 program XYZ; var

x,v: T; stack: array [1 . . max] of T; spointer: 1 . . max;

procedure push; begin stack [spointer] :=v; spointer :=spointer + 1 end; procedure

pop; begin spointer:=spointer - 1 v:= stack [spointer] end;

begin {initialisiere Stack:} spointer:=1; v :=

{Wert v 1};

push;

end.

v := . . . .

{Wert v2};

x := v

{Wert v2};

pop; x := v

{Wert vl};

112

1. Programmentwicklung

push rettet den aktuellen Wert der Variablen v auf den durch spointer gegebenen array-Platz und erhöht spointer um 1. pop erniedrigt spointer wieder um 1 und setzt den alten Wert der Variablen v aus dem array wieder zurück. Die Variable v kann natürlich auch eine Datenstruktur sein. Wie sind der Stack und die Funktionen push und pop zu formulieren, wenn im Gegensatz zu 1.5.1.2/1 mehr als eine Variable v über einen Keller verwaltet werden muß? Kann man sich Umspeicherungen in push und pop sparen, wenn man jede derartige Variable selbst als Stack, d. h. als array, definiert und sie grundsätzlich mit spointer indiziert anspricht? Für die Umformung einer rekursiven Prozedur gibt Knuth [KNUT74] zwei oft nützliche Regeln: (1) Ist die letzte verarbeitende Anweisung einer Prozedur vor dem Rücksprung (end) ein Prozeduraufruf, so kann dieser durch einen einfachen Sprung (goto) auf den Anfang der aufgerufenen Prozedur ersetzt werden. (2) Tritt ein Prozeduraufruf nur an einer Stelle im Programm auf, so kann er durch zwei gotos ersetzt werden: einen Sprung von der Aufrufstelle zum Prozeduranfang und einen Rücksprung vom Prozedurende hinter den Aufrufpunkt. Hierbei müssen bei rekursiven Prozeduren die alten Werte der lokalen Variablen in einen Keller entsprechend 1.5.1.2/1 gerettet werden.

Als Beispiel für die Anwendung der Regeln (1) und (2) kann die in Abschnitt 1.3.5 bereits besprochene Druckprozedur tprint dienen. Man erhält so durch rein mechanische Umformung der rekursiven Prozedur tprint (1.3.5/4) die in 1.5.1.2/2 angegebene, nicht mehr rekursive Prozedur tprint 1, die ohne weiteres auch in COBOL oder FORTRAN übertragbar wäre. Man beachte, daß tprintl nicht mehr den in Abschnitt 1.3.5 geforderten Ablaufstrukturierungs-Regeln entspricht — es enthält gotos aus und in Strukturblöcke! Seine Rechtfertigung bezieht es ausschließlich aus der „sauberen" rekursiven Version tprint und ihrer mechanischen Umformung nach beweisbar korrekten Regeln. Vollziehen Sie diese Umformung nach. Verifizieren Sie den korrekten Ablauf von tprintl an Hand des in Abb. 1.3.5-2 dargestellten Baums.

113

1.5 Datenverwaltung

1.5.1.2/2 procedure tprintl (i:integer); var j.integer; stack: array [1 . . max] of integer; spointer: 1 . . max; procedure push; begin stack [spointer] := j; spointer := spointer + 1 end; procedure pop; begin spointer := spointer - 1; j := stack [spointer] end; begin {Initialisierung:}spointer := l ; j :=i; 1: if j = 0 then goto 3; push; j := Baum [j]. L ; goto 1; 2: pop; print (Baum [jj.info); j := Baum [j], R ; goto 1; 3: if spointer > 1 then {stack noch nicht leer:} goto 2 end;

1.5.1.3 D y n a m i s c h e S p e i c h e r v e r w a l t u n g ( h e a p storage) Die allgemeinste Methode zur Speicherverwaltung ist die dynamische. Hierbei verfügt der Programmierer über zwei Primitivfunktionen allocate und free19. Über allocate (length, a) kann er ein Speicherstück der Länge length anfordern und erhält dessen Adresse in der Variablen a zur Verfügung gestellt. Ist kein ausreichend großes Stück freien Speichers mehr verfügbar, wird ihm dies durch eine Fehlermeldung, z. B. Übergabe einer vereinbarten „Nulladresse" nil mitgeteilt. 19

Die entsprechenden Prozeduren heißen in PASCAL new(p) und dispose(p). Dabei ist p eine Pointer-Variable, welche durch ihre Deklaration einem Variablentyp fest zugeordnet ist. Dadurch ist die Angabe der Länge des zuzuteilenden Speicherstücks als Parameter unnötig.

114

1. Programmentwicklung

Durch free (length, a) gibt er den angegebenen Speicherabschnitt wieder frei. „Adresse" ist hier natürlich jeweils in den Termen der betreffenden Programmiersprache zu verstehen. In BAL (/360-Assembler) wäre es eine echte Arbeitsspeicher-Adresse, in PL/1 ein POINTER. In den meisten anderen Sprachen sieht man — wie wir es auch hier tun werden — den zur dynamischen Vergabe vorgesehenen Speicher als array von „Einzelblöcken" eblock an. a ist dann der Index des ersten der zugeteilten Elementarblöcke, length wird in derartigen vereinbarten Einzelblöcken gerechnet. Aus Sicherheitsgründen wird man in einer praktischen Realisierung bei der Freigabefunktion free auf den Parameter length verzichten und eine interne Buchführung über die Lange vergebener Blöcke vorsehen — dies sei dem Leser als Übungsaufgabe überlassen. Die Implementierung einer dynamischen Speicherverwaltung ist nicht einfach. Die Probleme liegen hier weniger im Vergabe- als im Rücknahme-Algorithmus: werden die Speicherstücke nicht diszipliniert vergeben und bei Rückgabe sofort wieder mit angrenzenden freien Speicherabschnitten zu größeren verschmolzen, so kommt es bald zur Atomisierung des freien Speichers — es gibt nur noch kurze, über den gesamten Speicher verstreute freie Abschnitte, und Anforderungen nach größeren Längen können nicht mehr befriedigt werden. Deshalb verzichten die meisten Programmiersprachen auf eine dynamische Speicherverwaltung oder bieten nur eine a/focaie-Funktion, aber nicht das zugehörige free. Daher muß der Programmierer oft eine dynamische Speicher-Verwaltung selbst realisieren. Ein beliebtes Verfahren hierfür ist das Buddy-System (vgl. [KNUT68], S. 442ff). Die einfache Verschmelzung zurückgegebener Stücke wird hierbei dadurch erreicht, daß — grundsätzlich die vom Programmierer gewünschte Speicherlänge von length auf die nächsthöhere Zweierpotenz 2k > length aufgerundet wird, — ein angefordertes Speicherstück der Länge 2k, falls nicht bereits vorhanden, aus einem größeren immer durch (ggf. mehrmaliges) Aufteilen in zwei gleichgroße „Buddies" („Kumpel", „Spezis") hergestellt wird, und — es sich bei Rückgabe eines Speicherstücks durch eine verhältnismäßig unkomplizierte Buchführung feststellen läßt, ob sein „Buddy" ebenfalls frei ist und (ggf. mehrmals) mit ihm zu einem doppelt so großen vereingt werden kann.

115

1.5 Datenverwaltung

Die Adresse Buddyfa) des jeweils zugehörigen Buddy läßt sich einfach feststellen. Wegen des Entstehens der Buddies durch Halbierung und der Wiedervereinigung eines Blocks ausschließlich mit seinem Buddy (und nicht mit einem ggf. freien Nachbarn an seinem anderen Ende) gilt zur Ermittlung des Buddy-Adresse eines Blocks der Länge 2k und der Adresse a folgender Algorithmus. 1.5.1.3/1 2 0 if a mod ( 2 # * ( k + l ) ) = 0

then Buddy := a+2**k else Buddy := a-2**k;

Die interne Speicherverwaltung des Buddy-Systems verwendet als freien Speicher ein array aus Einzelblöcken eblock, sowie zwei weitere arrays zur internen Buchführung. tag [a] ist eine Boolesche Variable, die den Wert true hat, wenn bei a ein freier Block (der Länge 2k mit k beliebig) anfängt, avail [&] enthält als Kettenkopf die Adresse a eines freien Blocks der Länge 2k, sofern es einen solchen gibt, andernfalls die Nulladresse nil. Falls weitere freie Blöcke dieser Länge existieren, so werden diese über einen Vorwärts-Zeiger linkf und einen Rückwärts-Zeiger lirikb miteinander verkettet. Für Zwecke der Buchführung muß jeder Einzelblock neben diesen beiden Adressen noch (mindestens) einen Wert lengthlog fassen können: ist der betreffende Einzelblock der erste eines zusammenhängenden größeren freien Blocks der Länge 2k, so enthält lengthlog den Zweierlogarithmus der Länge k. 1.5.1.3/2 gibt die Datendefinition für die Buddy-Speicherverwaltung. 1.5.1.3/2 const nil = - 1 {Nulladresse}; type adr = nil . . maxadr {maxadr = (2**m)-l}; eblock = record lengthlog:0 . . m {Blocklangen-Registrierung}; linkf, linkb : adr {Kettenzeiger} end; var storage : array [0 . . maxadr] of eblock; tag : array [0 . . maxadr] of Boolean; avail : array [0 . . m] of adr;

20

,,**" bezeichne die Potenzierung, mod die Modulo-Operation.

116

1. Programmentwicklung

Abb. 1.5.1.3-1 zeigt die Initialisierung der Speicherverwaltung. Der gesamte Speicher ist ein freier Block der Länge 8, avail [J] zeigt deshalb auf 0, in storage[0\lengthlog ist der Logarithmus der Länge (=3) und in tag[0] die Verfügbarkeit des Blocks vermerkt. Alle übrigen avail-Zeiger enthalten die Nulladresse nil, alle übrigen tag den Wert false. Die dort beginnenden Einzelblöcke sind zwar ebenfalls frei, aber nicht unmittelbar verfügbar: sie sind ja Teile eines größeren Blocks! Abb. 1.5.1.3-2 zeigt eine Momentaufnahme der freien Speicherverwaltung, wenn der Einzelblock 3 und ein Block der Länge 2, beginnend mit dem Einzelblock 6, vergeben sind. Verfolgen Sie, ausgehend von dieser Abbildung, einige allocate- und /ree-Operationen. avail [0]

storage

nil nil

tag lengthlog linkf linkb

true

false

false

false

false

false

false

false

Abb. 1.5.1.3-1. Initialisierung einer dynamischen Speicherverwaltung nach dem BuddySystem (./.: Inhalt irrelevant)

1.5 Datenverwaltung

117

storage

tag

avail [0]

Abb. 1.5.1.3-3 skizziert die Vergabeprozedur allocate, die einen Block der gewünschten Länge length, ggf. durch wiederholtes Aufteilen größerer, beschafft. Die in Abb. 1.5.1.3-4 dargestellte Prozedur free gibt ihn wieder frei, wobei sie ihn nach Möglichkeit mit freien Buddies verschmilzt. Überlegen Sie sich eine Initialisierung für eine Buddy-Speicherverwaltung in einem freien Speicherbereich, dessen Anfangslänge lanf (gerechnet in Einzelblöcken) keine Zweierpotenz ist. Diese Initialisierung teilt den darüber hinausgehenden Speicher in kleinere Blöcke auf und initialisiert damit die entsprechenden freien Listen avail [&]. Wie groß muß das tag-array sein? Können die Primitivoperationen allocate und free unverändert übernommen werden?

118

1. Programmentwicklung

BEGIN

allocate (length, a) Parameter:

length > 0 gewünschte Speicherabschnitts-Länge (Eingabe), a Adresse des zugeteilten Speicherabschnitts (Ausgabe), = nil wenn kein Speicher frei, Daten (global): storage[a] dynamisch verwalteter Speicher, avail [/] Kettenköpfe "I tag[adresse] Markierung Anfang } freier Speicherabschnitte, (lokal): j,k log 2 der Speicherstücklängen, b Adresse des „Buddy"

Suche nächsthöhere Zweierpotenz-Blocklänge 2**k

>length

Suche kleinsten verfügbaren Block 2**j mit j> k (avail\J]

nil)

Existiert derartiger Block? . ^ ^

ja

nein

Wiederhole bis Blocklänge 2**j gleich der gewünschten (j=k) Setze Adresse a :=erster Block aus Liste avail{j] Hänge Block a aus der Liste avail\j) ab Markiere Vergabe durch tag[a)'=false Richtige Blocklänge ^



"

— nein

Teile Block in zwei gleichgroße mit den Adressen a und b =Bud(ly (a) Kette Blöcke a und b in die Liste avail\j-l ] der Blöcke mit halber Länge ein. (fertig) Markiere Verfügbarkeit der Blöcke: tag[a\'= true, tag[b\ '•= true Vermerke neue Längen in erstem lengthlog-Feld der Blöcke: storage [a |. lengthlog:=j, storage [b]. lengthlog:=j

END

ausreichend großes freies Speicherstück verfügbar, Rückmeldung

(Übergebe Adresse a)

Abb. 1.5.1.3-3. Vergabe dynamischen Speichers (Buddy-System)

a'= nil

1.5 Datenverwaltung BEGIN

119

free (length, a) Parameter: length Länge a Adresse f d e s

zurück

g e g e b e n e n Speichers,

Daten (global): storage[a] dynamisch verwalteter Speicher, avail[i] Kettenköpfe freier Speicherabtag[adresse] Markierung Anfang J schnitte, (lokal): k log 2 der Speicherstücklängen, al Adresse eines freien Speicherstücks Ermittle echte Blocklänge 2**k >length ai=a

: Umspeichern der Adresse in lokale Variable

Wiederhole bis 2**k = gesamter freier Speicher oder Buddy vergeben Prüfe ob Buddy frei : tag[Buddy(al)] = true und storage [Buddy(al)]. lengthlog = kl ja

"

Buddy frei?

neirT

Kette Block Buddy (al) aus Kette ara;7[A:]aus Markiere, daß Buddy nicht mehr verfügbar: tag [Buddy (al)] :=false

Buddy vergeben

Neue Blockadresse al ist kleinere der beiden: al'= min (al, Buddy (al)), k := k+1 - neue, doppelte Blockgröße Reihe Block al in Kette avail[k] der freien Blöcke ein Markiere seine Verfügbarkeit

tag\dl] '•= true

Vermerke seine Länge : storage[al].lengthlog

'•= k

END Abb. 1.5.1.3-4. Rücknahme freien Speichers (Buddy-System)

Das Buddy-System eignet sich auch gut zur dynamischen Verwaltung eines Trommel- oder Plattenspeichers, auf dem laufend Dateien verschiedener Größe eingerichtet und wieder gelöscht werden müssen. Der Grund hierfür ist die relativ geringe Zahl der bei der allocate- und/ree-Funktion notwendigen Speicherzugriffe.

120

1. Programmentwicklung

1.5.2 D a t e n a u f Externspeichern 1.5.2.1 Residenz, Organisation und Zugriffsmethode Wir hatten bis jetzt, ohne es ausdrücklich zu betonen, bei der Diskussion der Datenattribute angenommen, daß alle Daten, die ein Programm benötigt, im Arbeitsspeicher zur Verfügung stehen. Dann kann auf sie jederzeit in verarbeitenden Anweisungen Bezug genommen werden. In der Praxis ist dies in der Regel nicht der Fall. Damit werden zwei weitere Attribute von Daten wesentlich: — die Residenz und — die Organisation. Die Residenz sagt aus, auf welchen Datenträgern (Platte, Magnetband,...) das Datum gespeichert ist. Die Ein-/Ausgabe-Geräte werden wie Externspeicher behandelt, so daß ein Datum auch auf einem Drucker oder einem Kartenleser residieren kann. In der Regel werden bei modernen Betriebssystemen Drucker, Kartenleser und Kartenstanzer ohnehin über ein SPOOLSystem [SCHN75] als virtuelle Ein-/Ausgabegeräte auf Plattendateien simuliert, so daß ihre Behandlung als Externspeicher sogar in der physikalischen Realisierung der Ein-/Ausgabeprogramme begründet ist. Zusammengehörige Daten, deren Residenz nicht der Arbeitsspeicher war, hatten wir bereits früher (Abschnitt 1.2.3) als Datei oder file bezeichnet. Die Datei selbst wurde symbolisch durch den Filenamen, ggf. noch mit einer in eckigen Klammern geschriebenen näheren Qualifikation, bezeichnet. Beispiele gibt 1.5.2.1/1. 1.5.2.1/1 var: Filename [Qualifikation]: file of type {allgemein}; Personalstammband : file of Person; Karteneingabe [in]: file of array [1 .. 80] of char; Druckerausgabe [print]: file of Listenzeile;

Wir wollen die symbolische Benennung einer Datei hier beibehalten und annehmen, daß der Bezug der (logischen) Datei auf die physikalische Residenz (Gerät, Datenträger) vom Betriebssystem des Herstellers geleistet wird und dem Anwender unsichtbar bleibt [FLOR70, IBM67],

1.5 Datenverwaltung

121

Da die Verarbeitung der Daten nur im Arbeitsspeicher erfolgen kann, müssen Externspeicher-Daten vor ihrer Verarbeitung durch vereinbarte Standard-Prozeduren zugänglich gemacht werden und/oder nach ihrer Bearbeitung vom Arbeitsspeicher auf den Externspeicher transferiert werden. Um dies zu ermöglichen, müssen die Daten auf dem Externspeicher eine definierte logische Organisation besitzen. Auf dieser Organisation beruht jeweils ein Repertoire von Standardoperationen zum Zugriff auf die Daten, das meist als Zugriffsmethode bezeichnet wird. In der Regel stellt das Hersteller-Betriebssystem mehrere Organisationsformen und Zugriffsmethoden zur Verfügung (siehe z. B. [IBMOOc]). Wir nehmen an, daß von jeder Datei zu jedem Zeitpunkt höchstens ein Datum bzw. eine Datenstruktur, ein Satz (record) des in der Dateierklärung genannten Typs, im Arbeitsspeicher verfügbar ist. Diesen aktuellen Satz zu jeder Datei bezeichnet PASCAL in verarbeitenden Anweisungen und Ausdrücken durch den Filenamen mit nachgestellten Pfeil, z. B. Personalstammband t Karteneingabe t [75]. Die Attribute Residenz und Organisation beziehen sich immer auf eine vollständige Datenstruktur, d. h. in der Regel auf ein record. Einzelne Felder oder Unterstrukturen eines records können nicht unterschiedliche Residenz haben. Eine Standard-Zugriffsoperation transferiert immer ein vollständiges record des angegebenen Typs. Die einzelnen Standardoperationen der Zugriffsmethode stellen nun entweder einen anderen, neuen Satz als aktuellen im Arbeitsspeicher zur Verfügung (z. B. get (filename)) oder schreiben den gerade aktuellen Satz vom Arbeitsspeicher auf den Externspeicher (z. B. put (filename)). Meist bieten Zugriffsmethoden auch noch andere Standardoperationen, die nicht lesen oder schreiben, sondern Steuerfunktionen haben, wie seek zur Armbewegung einer Platte oder skip zum Vorsetzen eines Magnetbandes oder Druckerformulars. Diese Funktionen sind primär in den physikalischen Eigenschaften der Externgeräte und der EDV-Anlage begründet und sollen deshalb hier übergangen werden. Je nach den verschiedenen logischen Verarbeitungswünschen des Anwenders wählt er eine bestimmte Organisation, deren Zugriffsmethode die von ihm benötigten Standardfunktionen enthält. Hierbei muß er jedoch die Residenz der Daten beachten: die in ihr ausgedrückten physikalischen Eigenschaften des Datenträgers schränken die möglichen Organisationsformen für die auf ihm residierenden Daten ein.

1. Programmentwicklung

122

Die Praxis der Datenverarbeitung kennt eine große Anzahl unterschiedlicher Organisationsformen und Zugriffsmethoden. Dies könnte den Anschein erwecken, als sei es nahezu unmöglich, die logischen Verarbeitungswünsche der Anwender mit einer geringen Anzahl von Standardoperationen abzudecken. Auch hier ist dies, wie in der Ablauf- und Datenstrukturierung, nicht der Fall. Von der Problemlogik her reichen immer — die sequentielle Organisation und — die direkte Organisation als Basis-Organisationsformen aus. Mit ihnen können grundsätzlich alle höheren, komplexeren Zugriffsmethoden bis zu anspruchsvollen Datenbasis-Systemen realisiert werden. Daß durch Hersteller und Anwender trotzdem immer wieder neue Zugriffsmethoden entwickelt und eingesetzt werden, ist darin begründet, daß zwischen der logisch einfachen und durchsichtigen Problemformulierung und der physikalischen Realisierung der Datenhaltung auf den Externspeichern oft größere Kompromisse nötig sind, als dies bei der Ablaufstrukturierung und bei der Datenstrukturierung im Arbeitsspeicher der Fall ist. Dies hat zwei Ursachen: — Der Zugriff zu Externspeichern und Ein-/Ausgabegeräten ist viel langsamer als die Verarbeitung im Arbeitsspeicher. Typische Zugriffszeiten sind Arbeitsspeicher Trommel Platte Schnelldrucker Schreibmaschine

~ ~ ~ ~ ~

1 /^sec/Wort, 10 msec/Satz, 100 msec/Satz, 100 msec/Zeile, 100 msec/Zeichen

10 sec/Zeile.

— Da Daten auf Externspeichern oft Massendaten sind, d. h. viele Millionen Zeichen umfassen, erfordern sie eine beträchtliche Anzahl an Datenträgern (vohimes). Typische Fassungsvermögen von Datenträgern sind Magnetband-Kassette Trommel Platte

~ 100 k Zeichen 1000 Zeilen, ~ 100 k . . 1 M Zeichen 1 k . . 10 k Zeilen, ~ 250 k . . 50 M Zeichen ~ 2,5 k . . 500 k Zeilen.

Hierbei sind als „Zeile" 100 Zeichen angenommen — die Größenordnung einer vollbeschriebenen Lochkarte, Drucker- oder Schreibmaschinen-Zeile. Es muß deshalb in der Regel bei der Festlegung der Organisation von Externspeicher-Daten darauf geachtet werden, daß sowohl die Zahl der nötigen physikalischen Zugriffe auf die Externspeicher minimisiert wird, als auch der für die Daten selbst, für Verschnitt (unbelegten Speicherplatz) und für interne Organisationsdaten (Verweislisten, Zeiger u.s.w.) benötigte Speicherplatz.

1.5 Datenverwaltung

123

Die Minimisierung des benötigten Speicherplatzes ist weniger im Preis der Datenträger selbst begründet, der meist vernachlässigbar ist. Wichtiger ist die Zahl der verfügbaren Geräte (Trommeln, Plattenund Magnetbandlaufwerke). In der Regel will man alle Daten eines Datenbestandes gleichzeitig im Zugriff (on line) und damit montiert halten. Andernfalls treibt der Zeitbedarf für den Wechsel der Datenträger die Zugriffszeiten schnell in die Gegend von vielen Minuten hinauf. Dieser stärkere Zwang zur Optimierung bei der Organisation von Externspeicherdaten sollte aber trotzdem nicht dazu verleiten, von der bisher gewählten (Top Down-)Entwurfsstrategie abzugehen. Die logische Problemformulierung geht der physikalischen Realisierung voran: auch eine Datenhaltung sollte zuerst in Form der logischen Organisation konzipiert werden [HART71]. Zur optimalen Umsetzung der logischen in eine (quasi-) physikalische Externspeicherorganisation soll auf die Spezialliteratur [LEFK69, WATS70, WEDE75] und auf die Herstellermanuale verwiesen werden. Die beiden Basis-Organisationsformen sequentiell und direkt, auf die wir uns hier beschränken wollen, leiten sich unmittelbar aus der Überlegung her, daß es nur zwei logisch grundsätzlich verschiedene Zugriffsverfahren geben kann. Entweder ist bereits bei der Erstellung des Datenbestandes bekannt, in welcher Reihenfolge er später verarbeitet werden soll. Dies ist in kommerziellen Routineanwendungen, wie Lohn- und Gehaltsabrechnung oder Fakturierung die Regel. Dann ist es sinnvoll, ihn auch in dieser Reihenfolge zu ordnen: die Organisation ist sequentiell und der Zugriff erfolgt (lesend oder schreibend) immer auf das nächste Datum. Oder aber die Reihenfolge der späteren Verarbeitung ist bei Erstellung des Datenbestandes noch unbekannt. Typische Fälle hierfür sind Auskunfts-, Informationsund Buchungssysteme. Soll ein Durchsuchen des gesamten Datenbestandes bei jedem logischen Zugriff vermieden werden — eine Forderung, die wegen der relativ großen physikalischen Zugriffszeit der Externspeicher und der beträchtlichen Datenbestände meist gestellt werden muß — so muß der Datenbestand direkt organisiert werden. Dies bedeutet, daß auf einen Datensatz unmittelbar durch Angabe einer Adresse zugegriffen werden soll. Diese Adresse kann die physikalische Adresse auf dem Datenträger sein oder aber eine quasi-physikalische Adresse (z. B. die relative Satznummer auf der file). In der Regel ist sie jedoch eine „logische" Adresse, eine Zeichenkette, die entweder unmittelbar im Datensatz enthalten ist oder auch nur bei Lese- und Schreibvorgängen der jeweiligen Zugriffsfunktion mitgeteilt und im Rahmen der Datenorganisation in systeminternen Tafeln verwaltet wird. Eine derartige logische Adresse wird meist als Schlüssel (key) bezeichnet.

124

1. Programmentwicklung

Eine Mischform zwischen diesen beiden Organisationen ist die von den meisten Herstellern als Standard-Zugriffsmethode bereitgestellte indexsequentielle Speicherung. Sie ermöglicht eine Speicherung und Verarbeitung eines Datenbestandes sowohl sequentiell nach der Sortierreihenfolge eines hier Index genannten Schlüssels als auch einen direkten Zugriff unter Angabe dieses Schlüssels. Komplexere Zugriffsmethoden, die Zugriffe zu den gespeicherten Daten mit mehreren verschiedenen Schlüsseln und verschiedenen sequentiellen Verarbeitungsreihenfolgen ermöglichen, werden Datenbanksysteme genannt. Auch hierfür soll auf die Spezialliteratur verwiesen werden [CODA69, DODD71, LUTZ71, RUST72, WEDE74].

1.5.2.2 Sequentielle Organisation Eine sequentielle Datei, in PASCAL durch var filename [Qualifikation]: file of type ; erklärt, kann sich in drei Zuständen befinden: — neutral, — Eingabe, — Ausgabe. Zu Beginn des Programmlaufes ist der Zustand jeder erklärten Datei neutral. In vielen Programmiersprachen (z. B. BAL und COBOL) muß, in einigen anderen (wie PL/1) kann der Übergang von neutral auf Eingabe oder Ausgabe vom Programmierer durch eine explizite OPENAnweisung, der Übergang zurück auf neutral durch eine CLOSE-Anweisung verlangt werden. PASCAL veranlaßt den Übergang zwischen diesen Zuständen (ähnlich wie FORTRAN) implizit durch die Zugriffsfunktionen — put (filename), — get (filename), — reset (filename). Abb. 1.5.2.2-1 zeigt das Zustandsdiagramm. Wie das System den Fehlerzustand weiterbehandelt, ist implementierungsabhängig - in der Regel wird der Programmlauf abgebrochen.

1.5 Datenverwaltung

125

Abb. 1.5.2.2-1. Zustandsdiagramm einer sequentiellen Datei

Neben den Zustandsänderungen bewirken diese Funktionen noch folgendes: — put (filename) schreibt den aktuellen Datensatz filenamet als sequentiell nächsten auf die Datei. — get (filename) stellt den sequentiell nächsten Datensatz von der Datei als aktuellen Satz filename\ im Arbeitsspeicher zur Verfügung. Ist nach Einlesen des letzten Datensatzes die Endemarke (eof) der Datei erreicht, so setzt das nächste get die Endfile-Bedingung eof (filename) auf true. filenamet ist dann Undefiniert, und der Dateizustand wird auf neutral zurückgesetzt. Ein Rücksetzen auf den Datei-Anfang findet hierbei nicht statt. Damit kann eine bis zur Endemarke gelesene Datei durch pwi-Anweisungen in den Ausgabezustand versetzt werden, wobei die nunmehr ausgegebenen Sätze an das Ende der bisherigen Datei angefügt werden.

126

1. Programmentwicklung

— reset (filename) schreibt eine Endemarke (eof) hinter den letzten geschriebenen Dateisatz, wenn der Dateizustand Ausgabe war. In jedem Fall wird die Datei in den Zustand neutral und die Datei auf den Anfang zurückgesetzt. Ein put schreibt, ein get liest danach wieder den ersten Satz der Datei. Am Ende eines Programmlaufs sind resef-Anweisungen nicht notwendig — das Programmende impliziert für alle „offenen" Dateien ein „Schließen", d. h. ein Rücksetzen auf neutral und Setzen der Endemarke für Ausgabedateien. Diese drei Zugriffsfunktionen erschöpfen bereits die bei der sequentiellen Verarbeitung einer Datei sinnvollen Operationen. Es sollte nochmals darauf hingewiesen werden, daß die Sortierreihenfolge, in der die Sätze durch get gelesen werden, ausschließlich durch die Reihenfolge der putOperationen bei der Dateierstellung bestimmt wird. Diese Verarbeitungsfolge ist damit ein festes Attribut der Datei und kann nur durch Umsortieren und Neuerstellen geändert werden. Deshalb ist die Entwicklung effizienter Sortierverfahren ein für die Praxis sehr wichtiger Zweig der angewandten Informatik, über den es bereits zahlreiche Veröffentlichungen gibt ([KNUT73] enthält auch eine umfassende Literaturübersicht). Im Rahmen des Betriebssystems werden zudem von allen Herstellern Sortierprogramm-Generatoren als Standard-Dienstleistungsprogramme zur Verfügung gestellt. 1.5.2.3 Direkte Organisation Eine direkt organisierte Datei unterscheidet sich von einer sequentiell organisierten nicht nur dadurch, daß jederzeit zu einem beliebigen ihrer Datensätze über einen Schlüssel (key) zugegriffen werden kann. Mindestens ebenso wichtig ist, daß es bei ihr keinen getrennten Eingabe- und Ausgabezustand gibt. Schreibund Leseoperationen können in beliebiger Reihenfolge gemischt werden. Allerdings werden zuweilen aus Sicherheitsgründen auch Direktzugriffsdateien nur zur Eingabe eröffnet, um ein Zerstören des Datenbestandes durch irrtümliches Schreiben auf sie zu vermeiden. In diesem Fall kann man auch hier die Dateizustände Eingabe, Ausgabe und neutral unterscheiden. Der Zustand bezüglich eines Benutzerprogramms ist auch dann von Bedeutung, wenn zu entscheiden ist, ob gleichzeitig noch ein anderes, im Multiprogrammbetrieb mit ihm laufendes, zur gleichen Direktzugriffsdatei zugreifen darf. Im Gegensatz zu sequentiell organisierten Dateien ist dies bei direkt organisierten grundsätzlich sowohl logisch als auch physikalisch möglich.

127

1.5 Datenverwaltung

Wann kann eine derartige gleichzeitige Zuordnung einer Datei zu mehreren unabhängigen Benutzern problemlos erlaubt werden? PASCAL sieht in seiner Definition [WIRT70] keine direkt organisierten Dateien vor. Da derartige Dateien eine große Ähnlichkeit mit arrays haben, wollen wir für ihre Deklaration die in 1.5.2.3/1 gegebene Form einfuhren. 1.5.2.3/1 var filename .file [keytype] of type {allgemein}; Material :file [1 . . maxnr] of Materialsatz; Versuch :file [1 . . 1000] of Versuchsergebnis; Hierbei ist keytype ein skalarer Typ oder ein Unterbereich (vgl. Abschn. 1.4.3). Wie bei arrays wollen wir auch eine Liste von mehreren keytype-Angaben erlauben; der gesamte Schlüssel eines Datensatzes entsteht dann durch Konkatenieren je eines Vertreters jedes keytype der Liste. 1.5.2.3/2 gibt Beispiele. 1.5.2.3/2 var Umsatzstatistik: file [1 . . 31,Monat,1970 . . 1985] of Tagesumsatz; Maschinenbelegungsplan: file [Maschine, Betriebskalendertag, Schicht] of Maschinenbelegung; Fahrplan: file [Bahnhof, 0 . . 23,0 . . 59,Bahnhof] of Zuglaufplan; Im letzten der Beispiele 1.5.2.3/2 ist der zweite Schlüssel die Abfahrtsstunde, der dritte die Abfahrtsminute. Der erste und der letzte Schlüssel sind Abfahrts- und Zielbahnhof, die entweder (wie bei der Bundesbahn) durch ganzzahlige Kennzahlen oder aber auch durch unmittelbare Aufzählung als skalarer Typ deklariert sein könnten: type Bahnhof = {Aachen,....

, Zweibrücken).

Daß diese Dateiorganisation bei den etwa 5000 Bahnhöfen in Deutschland für die Realisierung nicht praktikabel ist, braucht uns nicht zu stören — für die Planung eines Programmsystems und ggf. auch für die ersten Testversionen eines Programmes sollte jede Möglichkeit für symbolische und mnemonische Bezeichnungen ausgenützt werden. Die praktische Realisierung einer so definierten Datei Fahrplan zeigt die bereits in Abschnitt 1.5.2.1 diskutierten Probleme der Datenspeicherung. Entsprechend den Wertebereichen der angegebenen vier Schlüssel erhält man bei Annahme

1. Programmentwicklung

128

von 5000 verschiedenen Bahnhöfen 3,6 Milliarden potentielle Datensätze des Typs Zuglaufplan, von denen natürlich nur ein verschwindend kleiner Bruchteil wirklich besetzt ist — schließlich fährt ja nicht zu jeder Minute des Tages ein Zug von jedem Bahnhof zu jedem anderen! Für die entweder durch den Programmierer oder durch eine zur Verfugung stehende Zugriffsmethode zu leistende Umsetzung der symbolischen, durch die Konkatenierung der Schlüssel gegebenen Adresse in eine (quasi-) physikalische bedeutet dies, daß kein oder nur sehr wenig Speicherplatz für nicht tatsächlich belegte Datensätze verbraucht werden darf. Für diese Adreßumsetzung gibt es im wesentlichen drei Möglichkeiten. Sie kann erfolgen durch — einen vereinbarten Algorithmus (Randomisierung, Hashverfahren), welcher die physikalische Adresse aus dem Schlüssel nach einem Pseudo-Zufallsalgorithmus ermittelt [GAUT70, HOPG69, KNUT73], — eine Liste {Indexliste, indexsequentielle Speicherung), in welcher die einem Schlüssel entsprechende Adresse aufgesucht werden kann [FLOR70, IBMOOc, SCHN75], — eine Verweisstruktur {Baum, Tree in verschiedenen physikalischen Realisierungen), welche über mehrere Suchschritte vom logischen Schlüssel zur physikalischen Adresse führt [GAUT70, KNUT73, LEFK69]. Die Wahl des zweckmäßigsten Adreßumsetzungsverfahrens wird durch die verschiedenen zu lösenden Anwendungsprobleme beeinflußt: ob z. B. verlangt wird, daß — zu Abfahrts- und Zielbahnhof alle Züge oder alle Züge in einem bestimmten Zeitintervall, — die Bahnhöfe alphabetisch geordnet, — die Züge nach Abfahrtszeit geordnet ausgelistet werden können. Die Planung des Programms oder Programmsystems sollte deshalb zumindest solange auf die physikalische Realisierung der Datei Fahrplan keine Rücksicht nehmen und lediglich die symbolische Definition aus 1.5.2.3/2 benutzen, bis sämtliche die Datenorganisation auf dem Externspeicher beeinflussenden Anforderungen bekannt sind. Wie bei sequentiellen Dateien nehmen wir an, daß zu jedem Zeitpunkt nur ein aktueller Satz des Datentyps type der Datei im Arbeitsspeicher verfügbar ist. In Ausdrücken und Anweisungen werde er wieder durch filename\ bezeichnet.

1.5 Datenverwaltung

129

Die Beschränkung auf einen aktuellen Satz ist für die praktische Programmierung keine Einschränkung, filenamet repräsentiert in der physikalischen Realisierung den der Datei zugeordneten Schreib-/ Lese-Puffer, und man kann jederzeit durch einfache Umspeicherung aktuelle Sätze aufbewahren: var Zwischenspeicher : type; begin Zwischenspeicher •'= filename f\ filenamet •'= Zwischenspeicher; end; Als Zugriffsprozeduren benötigt man lediglich eine Eingabe- und eine Ausgabeprozedur für den aktuellen Datensatz: - read (filename, key) bzw. read (filename, keyl,. .. ,keyn) stellt den durch keyl bis keyn bezeichneten Datensatz als filenamei zur Verfügung, - write (filename, key) bzw. write (filename, keyl,. .. ,keynj schreibt den aktuellen Datensatz filenamet mit den angegebenen fcey-Werten auf die Datei filename. key bzw. keyl bis keyn können dabei entweder als Teil des aktuellen Datensatzes filenamei oder an anderen Stellen des Programms als Variablen erklärt sein. Auch Ausdrücke, die zu einem zulässigen key-Wert ausgewertet werden, sind erlaubt. Für die sequentielle Abarbeitung der Dateien entsprechend der vereinbarten Reihenfolge eines key können die PASCAL-Funktionen succ und pred verwendet werden: für jede Variable* mit skalarem oder Unterbereichs-Typ gibt succfx) den Nachfolger, predfxj den Vorgänger, soweit diese vorhanden sind. Kann die konkrete programmtechnische Realisierung dieser sequentiellen Verarbeitung einer direkt organisierten Datei auf Schwierigkeiten stoßen? Beispiele für Ein-/Ausgabeanweisungen für die in 1.5.2.3/1 und 1.5.2.3/2 definierten direkten Dateien gibt 1.5.2.3/3. 1.5.2.3/3 write read read write

(Versuch, laufendeNummer); (Umsatzstatistik, Tag, Monat, pred(Jahr)); (Maschinenbelegungsplan, Drehbank3, heute, Spätschicht); (Fahrplan, München, 22, 13, Frankfurt);

1. Programmentwicklung

130

1.5.3 Beispiel: ein einfaches Auskunftssystem Der Entwurf eines einfachen Auskunftssystems für Adressen oder ähnliche kurze Texte kann als Beispiel dienen. Nach Eingabe von (grundsätzlich beliebig vielen) Suchbegriffen sollen diejenigen Texte (maximal n) aus einer Textdatei ausgegeben werden, welche die angegebenen Suchbegriffe enthalten. Abb. 1.5.3-1 zeigt die obersten zwei Stufen des Grobentwurfs als HIPOs, wobei jeweils Dateierklärungen als Input und Output angegeben sind. Auf oberster Stufe ist der Output die Auskunft, ein array von maximal n Texteinträgen (textentry), das auszugeben ist. Der Input ist einmal die Anfrage, ein array von Suchbegriffen {item), zum anderen die Textdatei, eine Direktzugriffsdatei von Texteinträgen, aus der nach der Anfrage die gewünschten auszusuchen sind. Schlüssel der Textdatei ist die laufende Nummer der Texteinträge.

Auskunft

Anfrage :

array [ 1 . . imax] of item

file [1 . . ntext] of textentry

Verweisliste

Auskunft:

array [1 . . n] of textentry

2. Gibt es Texte mit allen S u c h b e g r i f f e n ? 3. Schreibe maximal ^ ^ n dieser Texte

Textdatei:

©

1. In welchen Texten der Textdatei ist Suchbegriff (item) vorhanden?

Texte aus.

für Suchbegriff

item

Suchbegriffstafel :

file [1 . . stlength] of record item:alfa; ilist : 1 . . nitem

suchen

1. Suche Suchbegriff (item) in Suchbegriffstafel 2. Finde Nummer (ilist) der zugehörigen Verweisliste

end

Abb. 1.5.3-1. HIPOs für das Auskunftssvstem

ilist {Nummer der Verweisliste zu item}

131

1.5 Datenverwaltung

2 . ) Durchschnittsbildung ilist Verweisliste: file [ 1 . . n i t e m ] of array [ 1 . . n t e x t ] of 1 . . n t e x t {Bisherige} Verweistafel: array [ 1 . . n t e x t ] of 1 . . ntext

©

der Verweislisten zu den 1. Lese Verweisliste [ilist] zu Suchbegriff

Textdatei: file [1 . . n t e x t ] of t e x t e n t r y

{Neue} Verweistafel: array [1 . . n t e x t ] of 1 . . n t e x t

Bilde n e u e Verweistafel aus alter Verweistafel ( w e n n vorh a n d e n ) und Verweisliste [ilist]

Ausschreiben der Auskunft gemäß

{Endgültige} Verweistafel: array [ 1 . . n t e x t ] of 1 . . n t e x t

Suchbegriffen

Verweistafel

1. Lese maximal n Texte (textentry) aus T e x t d a t e i entsprechend e n d gültiger Verweistafel 2. Schreibe T e x t e aus

Auskunft: array [1 . . n | of t e x t e n t r y

Y

Abb. 1.5.3-1. (Fortsetzung) HIPOs für das Auskunftssystem

Der Auskunftsprozeß benötigt drei Verarbeitungsgänge. Es muß (1) für jeden Suchbegriff festgestellt werden, in welchen Texteinträgen er vorkommt, (2) eine (interne) Verweistafel mit den Nummern derjenigen Texteinträge erstellt werden, die alle Suchbegriffe enthalten, (3) jeder dieser Texteinträge, gesteuert von der Verweistafel, aus der Textdatei gelesen und auf den Fernschreiber ausgegeben werden.

132

1. Programmentwicklung -

ADENAU

9

-

o

-

o

MAIER

2

JOSEF

4

-

o



o

HERBERT

6

-

o

FILIALE

13

KARL

1

-

o



o



o

MUELLER

KARL 2

o

BERLIN

5

UND

10

-

o

AG

12

-

o o

JOSEF MAIER BERLIN

MAIER

HERBERT MUELLER KG ADENAU

3 MUENCHEN

KARL HERBERT MUENCHEN

4 JOSEF

MAIER UND ADENAU BERLIN

5 BERLIN

HANS MAIER AG MUENCHEN FILIALE BERLIN

6 HERBERT 7

7

-

-

KARL MAIER MUENCHEN

o

MUELLER 8 KG 9 ADENAU 10 UND

MUENCHEN 3 -

o

HANS

11



o

-

o

KG

S

Suchbegriffstafel

HANS 12 AG 13 FILIALE Verweisliste

Abb. 1.5.3-2. Die Dateien des Auskunftssystems

Textdatei

1.5 Datenverwaltung

133

Die HIPOs für diese drei Verarbeitungsgänge zeigen, daß jeder von ihnen eine Direktzugriffsdatei braucht: (1) eine Suchbegriff stafel, die für jeden Suchbegriff item einen Verweis ilist auf den zu ihm gehörenden Verweislisteneintrag enthält, (2) die Verweisliste mit Verweisen auf die jeweils einen bestimmten Suchbegriff enthaltenden Texte, (3) die Textdatei selbst.

Bildung einer Verweistafel auf alle passenden Texteinträge mittels Durchschnittsbildung der Verweislisteneinträge. Anfrage: MAIER

MUENCHEN

Abb. 1.5.3-3. Der Suchalgorithmus des Auskunftssystems

BERLIN

1. Programmentwicklung

134

BEGIN Auskunftssystem

Daten: 1 oder mehr Suchbegriffe (Worte) von Fernschreiber (Eingabe), Liste von gefundenen Teilnehmereinträgen auf Fernschreiber (Ausgabe), Suchbegriffstafel Verweisliste S auf Externspeicher, Textdatei mit Texteinträgen J Verweistafel mit gefundenen Textverweisen i m A r b e i t s s p e i c h e r . Lese Suchbegriffe vom Fernschreiber Wiederhole für jeden Suchbegriff ( 7 ) Suche Suchbegriff in Suchbegriffstafel

(2) Initialisiere Verweistafel aus VerweislistenEintrag

Schreibe „Unter den Angaben kein Text verzeichnet"

Bilde Durchschnitt aus Verweislisteneintrag und alter Verweistafel

Schreibe „zu wenig Suchbegriffe"

Wiederhole für jeden Eintrag aus Verweistafel ( 3 ) Schreibe T e x t eintrage entsprechend Textdatei aus.

END Abb. 1.5.3-4. Struktogramm für das Auskunftssystem ©,

Fehler

Verweise auf die HIPOs (Abb. 1.5.3-1)

1.6 Virtuelle Maschinen

135

Abb. 1.5.3-2 skizziert ein Beispiel für die Dateien. Die Suchbegriffstafel wird zweckmäßig als Hashtafel organisiert, wobei die Eintragsnummer (Hashadresse) für jeden Suchbegriff item durch einen Hashalgorithmus (Randomisierung) aus ihm ermittelt wird [HOPG69, KNUT73, MAUR75], Abb. 1.5.3-3 skizziert die Bildung der internen Verweistafel als Durchschnitt der jeweils alten Verweistafel mit dem zum Suchbegriff gehörenden Verweislisteneintrag. Abb. 1.5.3-4 zeigt schließlich ein Struktogramm für den Auskunftsalgorithmus, welcher die Ablaufstruktur der Verarbeitungsgänge des obersten HIPOs (Abb. 1.5.3-1) zeigt.

1.6 Virtuelle Maschinen 1.6.1 Die Visualisierung der Planung eines hierarchischen Systems Nach Abschnitt 1.1.2 ist es die Aufgabe der Programmplanung, den Abstand zwischen den beiden in der Spezifikation definierten Ebenen, der Benutzermaschine und der Basismaschine, durch ein logisch konsistentes, hierarchisch strukturiertes Programm zu überbrücken. Für die Reihenfolge, in welcher man bei dieser Planung vorgeht, gibt es grundsätzlich zwei Möglichkeiten. Beginnt man mit den oberen, problemnahen Schichten, so spricht man von einer Top Down-Plaming, ein Beginnen mit den unteren, maschinennahen Schichten wird Bottom Up-Planung genannt. Die Top Down-Planung wird gegenwärtig in der Literatur bevorzugt. Viele erfolgreiche Entwickler verwenden zuweilen auch eine Mischstrategie, wobei sie einmal eine obere, dann eine untere Schicht definieren, bis diese Schichten sich dann „in der Mitte treffen". Bei nicht sehr einfachen Problemen wird dies meist ein iterativer Prozeß sein, wobei untere oder obere Ebenen wiederholt korrigiert und verbessert werden. Der letzte derartige Durchgang durch die fertiggestellte Planung sollte jedoch immer „top down" erfolgen: wie wir später sehen werden, ist dies nämlich die Richtung, in welcher bei der anschließenden Programmierung grundsätzlich vorgegangen werden muß. In der Praxis scheitern viele Systementwickler schon bei dieser Planung. Häufig verlieren sie bei dem Versuch, den Abstand zwischen Benutzermaschine und Basismaschine zu überbrücken, den Uberblick und „kommen nicht am anderen Ende an".

1. Piogrammentwicklung

136

Die Schwierigkeit beim Entwurf einer hierarchischen Programmstruktur ist die hohe Abstraktionsstufe bei der Definition der Komponenten — der Planer sieht keine anschauliche Bedeutung für sie und weiß deshalb nicht, „was er eigentlich tut". Der Abstand zwischen Benutzer- und Basismaschine läßt sich leichter überwinden, wenn den abstrakten Zwischenschichten ein konkreter Sinn unterlegt werden kann. Dies leistet die Definition von virtuellen („scheinbaren") Maschinen. Sie ist eine heuristische Methode, welche beim Verständnis des Planungsprozesses und des Planungsproduktes hilft. In der Literatur findet man hierfür auch oft die Bezeichnung abstrakte Maschine. Die letztere Bezeichnung hat den Vorzug, daß sie nicht als „Maschine mit virtuellem Speicher" mißverstanden wird - ein Hardware-Konzept, das mit dem hier gemeinten Software-Planungsverfahren nichts zu tun hat [SCHN74]. Die Vorstellung des Entwicklers bei Entwurf eines Systemteils nach dieser Methode ist, daß er eine Maschine entwirft, die einen bestimmten, logischen Unterkomplex eines Problems vollständig bearbeiten kann und deren Befehls- und Datenstrukturen entweder — hardware-näher (bei Top-Down-Planung) oder — problem-näher (bei Bottom-Up-Planung) sind als die der jeweils vorangehenden Ebene. Er definiert für diese Maschine einen Befehlsvorrat, einen Arbeitsspeicher und ggf. sonstige Betriebsmittel (z. B. einen „Drucker", der „in Wirklichkeit" sowohl ein realer Drucker, eine Datei zur Zwischenspeicherung oder auch eine Datenübertragungsleitung sein kann), sowie Datenstrukturen, auf welche die Befehle arbeiten. Die Befehle werden Primitivoperationen genannt. Der Arbeitsspeicher und die übrigen Betriebsmittel der virtuellen Maschine sind den Datenstrukturen und den Primitivoperationen angepaßt. Sie sind damit unter Umständen völlig anders organisiert als der Arbeitsspeicher der Hardware und die reale Peripherie. Es ist also durchaus möglich, einen virtuellen Arbeitsspeicher als Keller (Stack), Warteschlange, zyklischen Puffer, unendlich lange Bit- oder Zeichenkette, Tabelle mit assoziativem Zugriff über einen Schlüssel zu definieren oder wie es sonst gerade zweckmäßig und „problemnah" erscheint. Die Abbildung dieser Struktur auf den realen Spei-

1.6 Virtuelle Maschinen

137

eher ist gerade eine der Aufgaben der betreffenden virtuellen Maschine (und ggf. anderer, tieferer, auf die sie sich abstützt). Grundsätzlich könnte jede virtuelle Maschine auch per Hardware als Mikroprogramm [HUSS70] realisiert werden, wenn es sich lohnen würde, einen Spezialrechner für diesen Zweck zu bauen. Nach Abschluß der Planung ist es Aufgabe des Programmierers, die Primitivoperationen mit Hilfe der Anweisungen der benutzten Programmiersprache und der Befehle ggf. auf niedrigerer Ebene liegender virtueller Maschinen zu implementieren. Je nach den Möglichkeiten der Programmiersprache werden hierbei Makros [KENT69], Unterprogramme, Funktionsdeklarationen oder Mischformen aus diesen Techniken benutzt. Ist jede Primitivoperation als Prozedur (Unterprogramm, Funktion) realisiert, so spricht man oft von einer prozeduralen Erweiterung der Programmiersprache. Verwendet eine virtuelle Maschine A zur Realisierung ihrer Primitivoperationen diejenigen einer anderen virtuellen Maschine B, so ist es unzulässig, daß zur Realisierung der Primitivoperationen der zweiten Maschine B ihrerseits diejenigen von A verwendet werden. Zwischen den virtuellen Maschinen eines Systems besteht somit bezüglich der Relation „benutzt zu ihrer Realisierung" eine Halbordnung. Sie wird As Hierarchie der Systemkomponenten bezeichnet (vgl. Abb. 1.6.1-1). Je höher eine Ebene in dieser Hierarchie angeordnet ist, desto problemnäher, je tiefer sie ist, desto maschinennäher ist ihre funktionale Beschreibung. Jede derartige Systemkomponente ist definiert durch — den Komponentenkern, die Konventionen für den Prozedur- oder Makroaufruf, der die betreffende Primitivoperation anfordert, und - den Komponentenrumpf, die Realisierung der Primitivoperation als Prozedur (Subroutine) oder Makrodefinition in der gewählten Programmiersprache, ggf. mit Aufrufen (tieferer) Primitivoperationen. Die Gesamtheit der Primitivoperationen einer Ebene kann als Befehlsliste genau einer virtuellen Maschine angesehen werden. Der Kern einer Komponente wird in der Planung, der Komponentenrumpf als Folge der Verarbeitungsschritte bei der Programmierung realisiert. Die hierarchische Struktur befriedigt die Grundforderung einer logisch einwandfreien Systemplanung, daß in jedem Komponentenrumpf, d. h. für die Realisierung jeder Primitivoperation, nur solche tieferer, nicht aber höherer virtueller Maschinen benutzt werden dürfen. „Höhere" Maschinen existieren auf der tieferen Ebene noch nicht, und damit wäre auch eine Benutzung ihrer Primitivoperationen ein logischer Widerspruch.

1. Programmentwicklung

138 Benutzer-Schnittstelle

m I

I I

I I

Kern = Schnittstelle nach außen

I Verarbeitungsschritte = verarbeitende Anweisungen oder Aufruf einer tieferen Primitivoperation

Abb. 1.6.1-1. Hierarchisch strukturiertes System

1.6 Virtuelle Maschinen

139

1.6.2 Primitivoperationen Das Talent und die Erfahrung eines Planers zeigen sich in der zweckmäßigen Konstruktion von virtuellen Maschinen und besonders in der Festlegung der Primitivoperationen für die jeweils gerade bearbeitete Systemebene. Da Planen eine kreative Tätigkeit ist, kann man keine Rezepte hierfür angeben. Man kann sie nur durch praktische Versuche einüben und durch eine nachträgliche Kritik und darauf begründete Verbesserung der Planungsergebnisse das Erlernen dieser Fähigkeit fördern. In der industriellen Software-Entwicklung wird die formelle Überprüfung einer Planung meist als Design-Review bezeichnet. Folgende Forderungen an einen Satz von Primitivoperationen sind ein Ansatzpunkt für die Planungsüberprüfung. Exakte Definition der virtuellen Maschine: Was tut sie, und was tut sie nichts Welche Daten, Datenstrukturen und Betriebsmittel werden von ihr verwaltet? Sind diese ausschließliches „Eigentum" dieser Maschine, oder greifen auch Primitivfunktionen anderer virtueller Maschinen (Module) auf sie zu? Wenn das letztere der Fall ist — wie wird garantiert, daß sich die beiden (unabhängigen) virtuellen Maschinen nicht gegenseitig stören können? Stehen dem Benutzer (d. h. entweder dem Anwender — auf der obersten Ebene — oder dem Programmierer höherer Systemebenen) alle Funktionen zur Verfügung, die er benötigt? Was kann er falsch machen, und inwieweit schützt sich die virtuelle Maschine gegen Versorgungsfehler? Orthogonalität des Operationssatzes: Hierunter versteht man die Forderung, daß für jede Bedienungsmaßnahme nur genau eine Operation zur Verfügung steht. Es ist unzulässig, daß für eine logische Funktion (also z. B. das Schreiben eines Satzes auf eine Datei) zwei verschiedene Operationen spezifiziert werden, zwischen denen der Programmierer entweder nach Lust und Laune wählen kann oder nach irgendwelchen schwer zu verstehenden Nebenbedingungen wählen muß. Die Primitivoperationen müssen also logisch getrennt sein. Überschaubaikeit des Operationssatzes: Die Menge der verschiedenen Operationen soll in absehbarer Zeit erlernt und beherrscht werden können. Sobald die Zahl der Primitivoperationen einer der definierten Maschinen etwa über zehn liegt, ist dies nicht mehr gewährleistet. Man sollte sich dann überlegen, ob diese Maschine nicht „zu viel tut", d. h. mehrere logisch getrennte Aufgabenbereiche (z. B. Behandlung von mehreren völlig unterschiedlichen strukturierten Dateien, oder Bedienung von ganz verschiedenen Externgeräten, wie Kartenleser, Kartenstanzer, Drucker, dialogfähigen Terminals) vermischt wurden.

140

1. Programmentwicklung

Eine Verkleinerung des Operationssatzes läßt sich oft durch Differenzierung über Versorgungsparameter erreichen: statt für verschiedene Dateien oder Ausgabeströme unterschiedliche Primitivoperationen zu spezifizieren, ist es oft besser, jeweils eine Operation zu definieren, welcher als Parameter der Dateiname oder eine Identifikation des Ausgabestroms mitgegeben wird. Einfachheit des Operationssatzes: Unnötige Komplikationen und dadurch verursachte Fehler bei der Anwendung bzw. der Programmierung höherer Ebenen entstehen erfahrungsgemäß vor allem durch Nebeneffekte und Inkonsistenz sowie Interdependenz der Parameter. Nebeneffekte liegen dann vor, wenn eine Primitivoperation eine Zusatzleistung erbringt, die eigentlich nicht ihre Aufgabe ist. Ein Beispiel wäre eine Ausgaberoutine auf einen Drucker, die unter bestimmten Umständen gleich auch die nächste zu verarbeitende Karte einliest. Unter dem Deckmantel der „Effektivitätsoptimierung" findet man dergleichen Dinge viel öfter in Programmen, als man glauben sollte! Versorgungsparameter sind inkonsistent, wenn in verschiedenen Primitivoperationen der gleiche Parameter verschiedene Bedeutung hat (z. B. LAENGE einmal die Länge eines Satzes, ein andermal die Länge eines Schlüsselfeldes ist, oder wenn bei zwei Eingabeoperationen einmal das System dem Programmierer diese Länge zurückmeldet, ein andermal der Programmierer sie jedoch als Länge eines von ihm zur Verfügung zu stellenden Puffers selbst spezifizieren muß). Parameter sind interdependent, wenn die Bedeutung des einen von einem anderen abhängt (z. B., wenn LAENGE je nach dem spezifizierten Ausgabestrom einmal in Worten, ein andermal in Bytes gerechnet wird). Korrekte Initialisierung: Eine virtuelle Maschine existiert nicht „von selbst", sondern muß von der realen und tieferen virtuellen Maschinen simuliert werden. Dazu werden Betriebsmittel gebraucht, die in der Regel beschafft werden müssen, und es muß die Verwaltung dieser Betriebsmittel und der Datenstrukturen vorbereitet werden, bevor die Maschine das erste Mal benutzt wird. Verwendet die virtuelle Maschine also z. B. einen Keller als Arbeitsspeicher, so ist dieser bei der Initialisierung im realen Speicher anzulegen, und es ist die Kellerverwaltung (als Zeiger oder Index auf die oberste Keller-Zelle, vgl. Abschn. 1.5.1.2) zu initialisieren. Deshalb sollte jede virtuelle Maschine unter ihren Primitivoperationen einen Initialisierungsbefehl besitzen, und alle anderen Primitivoperationen sollten (zumindest in der Testversion) bei ihrem Aufruf prüfen, ob die vorherige Initialisierung erfolgreich durchgeführt wurde. Dies gilt auch dann, wenn bei einer konkreten Realisierung keine Initialisierung gebraucht wird. Bei der Übertragung des Systems auf eine andere Hard- und

1.6 Virtuelle Maschinen

141

Grundsoftware oder bei einer späteren Erweiterung der Aufgaben der virtuellen Maschine kann sich die Initialisierung doch als nötig erweisen [PARN72a]. Eine typische Zugriffsmethode auf eine sequentielle Datei (Magnetband) mit den Primitivoperationen OPEN, CLOSE, GET, PUT, REWIND [FLOR70, IBMOOc, SCHN75] kann als virtuelle Maschine zur Bedienung dieser Datei aufgefaßt werden. Enthält dieser Befehlssatz eine Initialisierung? Will man auf den Initialisierungsbefehl verzichten, so muß jede Primitivoperation die Initialisierung durchfuhren können und eine Fehlerrückmeldung vorsehen, falls diese — z. B. aus Mangel an Betriebsmitteln — nicht möglich war. Ob der dadurch erhöhte Benutzerkomfort den Aufwand lohnt (vor allem auch für den Test — die einwandfreie Initialisierung muß für alle Primitivoperationen geprüft werden!) ist zweifelhaft. Ein grober Planungsfehler ist es, die automatische Initialisierung nur bei derjenigen Primitivoperation vorzusehen, welche als erste aufgerufen wird. Warum? Gegen welche der obigen Forderungen würde dies verstoßen? Korrekter Abschluß: Wird — etwa am Ende des Programmlaufs - die virtuelle Maschine nicht mehr benötigt, so sind in der Regel Abschlußarbeiten (Housekeeping) zu leisten. So muß z. B. bei einer auf Platte zwischengepufferten Druckausgabe (SPOOL) die tatsächliche Ausgabe auf den (realen) Drucker veranlaßt werden. Meist müssen auch benutzte Betriebsmittel wieder freigegeben werden. Deshalb sollte eine virtuelle Maschine neben der Initialisierung auch eine Abschlußoperation (wie das übliche CLOSE für Dateien) besitzen. Transparenz der virtuellen Maschine [PARN75]: Wie weit sind mit Hilfe des gewählten Vorrats an Primitivoperationen alle wünschenswerten Funktionen der tieferen Maschinen weiterhin verfügbar? Wird etwa eine sequentiell organisierte Datei auf einem Speicher mit Direktzugriff (z. B. einer Hatte) mit Hilfe der in Abschn. 1.5.2.2 eingeführten Primitivoperationen get, put und reset verwaltet, so ist die ursprünglich vorhandene Direktzugriffsmöglichkeit auf der Ebene der virtuellen Maschine nicht mehr vorhanden. Die virtuelle Maschine ist intransparent hierfür: der Direktzugriff „wird von ihr nicht durchgelassen". Intransparenz ist in vielen Fällen erwünscht — die Beseitigung unnötiger oder gar für die konkrete Problemstellung störender Eigenschaften der Basismaschine ist ja gerade einer der wesentlichen Gründe für die Einführung virtueller Maschinen. Sie kann aber auch die Realisierung optimaler Systemlösungen empfindlich behindern.

1. Programmentwicklung

142

So kann beim Zugriff auf sequentielle Dateien der Wunsch bestehen, sich während der Verarbeitung bestimmte Sätze (etwa über die laufende Satznummer) zu merken und irgendwann später wieder auf sie zurückzusetzen. Mit den vorgegebenen Primitivoperationen ist dies nur über ein reset, gefolgt von einem Überlesen aller vorausgehenden Sätze durch entsprechend viele gef-Operationen möglich — bei Plattendateien sicher eine außerordentlich unbefriedigende Lösung. Im vorliegenden Fall kann eine Transparenz für diesen wünschenswerten Direktzugriff leicht erzielt werden, indem man zwei weitere Primitivoperationen note (filename, recordnummer) und point (filename, recordnummer) einführt [IBMOOc]. note vermerkt die aktuelle Satznummer in recordnummer, und point stellt den betreffenden Satz wieder als aktuellen zur Verfügung. Bei einer Plattendatei kann die virtuelle Maschine jetzt die direkte Zugriffsmöglichkeit ausnutzen, während bei einer Magnetbanddatei point weiterhin (ohne Wissen des Benutzers) durch Rücksetzen auf den Dateianfang und Überlesen der vorausgehenden Sätze realisiert wird. Zuweilen können auch Primitivoperationen nützlich sein, mit welchen der Benutzer die virtuelle Maschine bei der Effektivierung ihrer eigenen Verwaltung unterstützt. Werden in einer Anlage mit relativ kleinem Arbeitsspeicher gelegentlich lange Sätze mit get von einer Datei gelesen und nur kurz gebraucht, so kann es sinnvoll sein, eine Operation release einzuführen. Mit ihr teilt der Benutzer mit, daß er den eingelesenen Satz nicht mehr benötigt. Das System kann dann den Arbeitsspeicherpuffer freigeben und für andere Zwecke verwenden. Das Ziel derartiger Operationen mit „Vorschlagscharakter" an die virtuelle Maschine bezeichnet Parnas mit suggestiver Transparenz.

1.6.3 Betriebsmittelverwaltung und -transformation Eine typische Funktion für eine virtuelle Maschine ist die dynamische Vergabe von Speicherplatz. Der Befehlsvorrat hierfür ist üblicherweise (vgl. Abschn. 1.5.1.3) — initialize, — allocate, — free. Auch diese virtuelle Maschine verwaltet ein Betriebsmittel: den Arbeitsspeicher oder einen externen Speicher.

1.6 Virtuelle Maschinen

143

In fast allen Fällen läßt sich die Aufgabe einer virtuellen Maschine auf ein Betriebsmittel-Verwaltungs-Problem zurückführen: sie „veredelt" es, indem sie es zweckmäßiger aufteilt und strukturiert (z. B. die dynamische Speicherverwaltung) oder einfachere, anwenderfreundlichere Primitivoperationen zur Verfügung stellt (z. B. sequentielles Lesen und Schreiben logischer Sätze einer Plattendatei). Diese Auffassung von der Funktion einer virtuellen Maschine ist oft ein nützlicher Ansatzpunkt für die Planung: bei der Überlegung, welche virtuellen Maschinen man definieren soll, hilft eine Auflistung, welche Betriebsmittel man (als Basismaschine) hat und welche man (als Benutzermaschine) gerne haben möchte. Sie begründet ferner die oben aufgestellte Hierarchie-Forderung, daß eine „tiefere" virtuelle Maschine nicht die Primitivoperationen einer höheren verwenden darf. Wenn jede virtuelle Maschine „höhere" Betriebsmittel aus anderen, tieferen erzeugt, so würde sie bei Verwendung einer in der Hierarchie höheren Maschine ein Betriebsmittel benutzen, welches es auf ihrer Ebene noch gar nicht gibt. Sie arbeitet vielmehr erst an seiner Herstellung mit — vergleichbar einem Werkzeugmacher, der ja zur Anfertigung von Teilen einer Drehbank dieses erst später entstehende Endprodukt auch noch nicht einsetzen kann. Der Aspekt der Betriebsmitteltransformation durch virtuelle Maschinen führt aber noch auf eine weitere Forderung. Nicht nur darf eine virtuelle Maschine keine höheren Primitivoperationen zu ihrer Realisierung benutzen, sie muß die von ihr benötigten, zu transformierenden Betriebsmittel auch von der nächst möglichen tieferliegenden beziehen. Würde sie zuweilen eine Betriebsmitteltransformation auf tieferer Ebene überspringen, so würde die Betriebsmittelverwaltung der übersprungenen Maschinen gestört, und es käme zu Fehlern. Überlegen sie sich, was geschähe, wenn bei einer dynamischen Speicherverwaltung (Abschn. 1.5.1.3) freie Speicherstücke nicht immer explizit über allocate angefordert würden, sondern höhere Routinen zuweilen auch unmittelbar die Ketten freier Blöcke avail[k] ansprechen und verändern würden.

1.6.4 Beispiel zur hierarchischen Gliederung Als Beispiel für die Planung eines hierarchischen Systems betrachten wir ein einfaches Text-Speicherungs- und -Wiedergewinnungs-System (Information RetrievalSystem). Der Benutzer bediene das System von einem Bildschirm-Terminal aus. Es stehe ihm eine Bedienungssprache zur Verfügung, mit deren Anweisungen er Texte von grundsätzlich unbeschränkter Länge unter alphanumerischen Namen zur Speicherung eingeben, wieder aufrufen, ändern und löschen können soll. Die

144

1. Programmentwicklung

genaue Syntax der Bedienungssprache ist in diesem Zusammenhang irrelevant — unser Planungsbeispiel beschäftigt sich ausschließlich mit dem Entwurf der dazu nötigen Datenhaltung auf einem Externspeicher mit wahlfreiem Zugriff, einer Platte oder Trommel. Die Basismaschine sei definiert durch die — für das Beispiel ebenfalls irrelevante — Programmiersprache und ein sehr einfaches Betriebssystem. Dieses erlaube keine logische Ein-/Ausgabe auf Dateien, sondern nur eine Externspeicherbedienung auf physikalischer Ebene (entsprechend etwa dem EXCP-Makro im OS /360 [IBMOOc]). Ein Anhaltspunkt für den Entwurf der Primitivoperationen auf den einzelnen Ebenen ist die im vorigen Abschnitt angesprochene Vorstellung der Betriebsmitteltransformation, welche jede virtuelle Maschine mit den von der unmittelbar tieferen Ebene realisierten Betriebsmitteln vornimmt. Abb. 1.6.4-1 zeigt eine dreistufige hierarchische Struktur und die von jeder der Ebenen simulierte Datenhaltung. virtuelle Maschine (Systemebene)

(simulierte) Datenstruktur

®

type name = alfa; text = file of char; Text-Speicherung und var Textbestand : file [name] of text; Wiedergewinnung

©

type textsegment = array [1 . . slength] of char; var index:array [ 1 . . ilength] Verwaltung einer in of record numerierte Segmente name : alfa; aufgeteilten Textdatei anfangssegment : 0 . . maxblock end; textfile:file [1 . . maxblock] of record textblock : textsegment; folgesegment : 0 . . maxblock end;

® Logische Ein-/Ausgabe (Zugriffsmethode)

type key = 1 . . filelength; block = array [blocklength] of Maschinenwort; filename = alfa; dafile = file [key] of block; var filesystem : file |filename] of dafile;

Abb. 1.6.4-1. Datenstiukturen für drei Ebenen eines Text-Speicherungs- und Wiedergewinnungs-Systems

Man beachte, daß die Datendefinitionen keine zulässigen PASCALSprachkonstruktionen sind — dies ist ja gerade der Grund dafür, die Programmiersprache mit entsprechenden Primitivoperationen zu ihrer Bedienung zu erweitern.

1.6 Virtuelle Maschinen

145

Die oberste Ebene (T) simuliert die zur Implementierung des Information Retrieval-Systems wohl bequemste denkbare Datenorganisation. Für jeden text wird eine sequentielle Datei angelegt, die bei Eingabe eines Textes zeichenweise vollgeschrieben wird und bei seiner Abfrage wieder auf das Terminal ausgegeben wird. Die gesamte Datenhaltung ist nun selbst eine Direktzugriffsdatei derartiger sequentieller Textdateien, wobei der jeweilige name des Textes der Zugriffsschlüssel ist. Abbildung 1.6.4-2 zeigt die Primitivoperationen für die Verwaltung der Datenstrukturen auf jeder Ebene. Die Ebene (T) bietet neben der Initialisierung und dem Abschluß die Funktionen Katalogisiere und Lösche, die eine neue sequentielle Textdatei in den Textbestand unter tiame einreihen und entfernen, Textanfang, welche die sequentielle Datei name auf den Anfang zurücksetzt, sowie Schreibe und Lese zum zeichenweisen sequentiellen Zugriff auf sie. Zur Realisierung der Primitivoperationen der obersten Ebene ist auf Stufe (2) eine Direktzugriffsdatei textfile zweckmäßig. Diese besteht aus maxblock Segmenten, von denen jedes einen textblock fester Maximallänge und einen Verweis folgesegment auf die Fortsetzung des Textes enthält. Ist ein Text abgeschlossen virtuelle Maschine (Systemebene)

Primitivoperationen (Prozedur- oder Makroaufrufe)

©

InitialisiereIRS SchließeIRS Text-Speicherung Katalogisiere (name) und Wiedergewinnung Lösche (name) Textanfang (name) Schreibe (name, Zeichen) Lese (name, Zeichen)

© Verwaltung einer in numerierte Blöcke aufgeteilten Textdatei

© Logische Ein/Ausgabe (Zugriffsmethode)

EröffneTexthaltung SchließeTexthaltung Reservieren (segment) Freigeben (segment) Eintragenindex (name, anfangssegment) Lesenindex (name, anfangssegment) Löschenindex (name) Schreiben (segment, arbeitsspeicherpuffer) Lesen (segment, arbeitsspeicherpuffer) open (filename) close (filename) read (filename, key, block) write (filename, key, block) außerdem: Konversionsroutinen Maschinenwort in char, alfa, integer und umgekehrt

Abb. 1.6.4-2. Primitivoperationen für die Ebenen eines Text-Speicherungs- und Wiedergewinnungs-Systems

1. Programmentwicklung

146

(eo/einer iexi-Datei auf oberster Ebene), so ist der Eintrag in folgesegment = 0. Das erste Segment zu einem unter name abgespeicherten Text ist über einen index aufzufinden. Abb. 1.6.4-3 zeigt ein Beispiel für die segmentierte Textdatei, name eines Textes ist jeweils eine römische Zahl, der Text der Anfang des entsprechenden Shakespeare-Sonnets. Wie der index und die jeweils freien Segmente der textfile verwaltet werden, ist auf der Ebene (2) unsichtbar: es ist eine Planungsentscheidung, die der Implementierer trifft und ein Warter, wenn nötig, ohne irgendwelche Rückwirkung auf höhere oder tiefere Ebenen abändern kann. Überlegen Sie sich einige alternative Möglichkeiten. index name 1 2 3 5 6 7 8 9 10 11 12

textfile anfangssegment

folge segment

1 WH E N FORTY W I N T5 E 2 N D , Y 0 U NE V E R 7CA T H Y BR 0W 3 0 S . M Y L 0 V E , Y E 15 A . 5 RS S H A L L BESIEGE3 6 ^freies S e g m e n t « « 7 N B E 0L D 0 8 A C T 0 R 0 N THE STAK 9 TAKE ALL MY LOVE 10 ^freies Segment 11 ^freies Segment w. 12 AS AN U N P E R F E C 8 T 13 ^freies Segment K G E 0 15 TAKE T H E M ALL 0 maxblock = 16 TO ME. FAIR F R I E2

1 9 16 12 0 w / M 0 Wì 0 W//Â m 0 0 W///////A v/// 0 0 'WW W/M m 0 II X L C I V XXIII

textblock

K

m

i

^

Abb. 1.6.4-3. In Segmente aufgeteilte Textdatei mit Index

Die Primitivoperationen zur Bedienung dieser Datenstrukturen (Abb. 1.6.4-2) sind neben Initialisierung und Abschluß (.EröffneTexthaltung und SchließeTexthaltung) - Reservieren und Freigeben von textfile-Segmenten, - Eintragenindex¿esenlndex und Löschenindex für das Aufnehmen, Finden und Löschen von name - anfangssegment-Paaren im Index, - Schreiben und Lesen für den Transport von iexi/i'/e-Segmenten in den und aus dem Arbeitsspeicher.

1.6 Virtuelle Maschinen

147

Die tiefste Systemebene (3) schließlich ist ein mit Hilfe der physikalischen Ein-/ Ausgabe realisiertes filesystem. Es implementiert eine Menge von DirektzugriffsDateien dafile, welche jeweils filelength physikalische Blöcke aus blocklength Maschinenworten enthalten und über einen filename identifiziert werden können. Die Primitivoperationen sind die üblichen open-, close-, read- und wW?e-Operationen. Außerdem muß diese Ebene, welche den Übergang vom physikalischen Externspeicher zur logischen Datei realisiert, Primitivoperationen zur Überführung der Maschinenworte in die höheren Datenstrukturen wie char, alfa, integer und umgekehrt bereitstellen. Skizzieren Sie als Struktogramme oder in PASCAL die Realisierung der Primitivoperationen der Ebenen ( 7 ) und (2) von Abb. 1.6.4-1 und 1.6.4-2 unter Verwendung der Primitivoperationen der jeweils tieferen Ebene. Überlegen Sie sich, welche Informationen über die tatsächliche Struktur der Datenhaltung und die genauen Abläufe der E/A-Vorgänge Sie auf jeder der Ebenen nicht haben und auch nicht benötigen. Auf welcher Ebene wissen Sie, daß es im System mehr als nur eine einzige Datei gibt? Üblicherweise wird man als weiteren, in den Abbildungen nicht angegebenen Standard-Parameter der Primitivoperationen noch einen Rückmeldecode vorsehen. Über ihn werden der jeweils aufrufenden Prozedur Sonder- und Fehlerbedingungen mitgeteilt, welche von der virtuellen Maschine nicht selbst abgehandelt werden konnten (vgl. hierzu Abschnitt 2.3.6).

2. Systementwicklung 2.1 Der Projektablauf 2.1.1 Die organisatorische Umgebung Arbeitet der Programmierer an einem größeren Softwareprojekt, einem Programmsystem, so steht er nicht mehr für sich allein, sondern in einer Umgebung von Auftraggebern, Mitarbeitern, Benutzern, Wartern . . . In der kommerziellen Softwareentwicklung ist diese Umgebung die Gesamtorganisation des EDV-Einsatzes in einem Unternehmen oder einer öffentlichen Verwaltung. Sie umfaßt damit einen Bereich, der weit über den Rahmen dieses Buches hinausreicht. Der Programmierer sollte um diese größeren Zusammenhänge seiner Arbeit wissen — er sollte daran denken, daß nichttechnische Gesichtspunkte, wie gesetzliche Vorschriften, vorhandene Organisationsformen und vielleicht auch liebgewordene Gewohnheiten, die Aufgabenstellung wesentlich beeinflussen und daß Wartungs- und Änderungsprobleme den Einsatz seines Programms beenden können, bevor sich der Entwicklungsaufwand amortisiert hat. Die in diesem Buch zu behandelnden Programm- und Systementwicklungstechniken fallen in einen engeren zeitlichen Rahmen, zwischen — die Spezifikation des zu erstellenden Softwareprodukts und — seine Freigabe für den Einsatz. Kein Gegenstand dieses Buchs sind die der Programmspezifikation vorausgehenden Tätigkeiten wie — Aufgabenanalyse, — Aufnahme und Analyse des Ist-Zustandes, — Analyse der Umgebungsbedingungen (gesetzliche Vorschriften, betriebliche Normen u. ä.), — Festlegung der bereits vorhandenen Betriebsmittel (Hardware, bereits eingeführtes und damit vorgeschriebenes Betriebssystem, Einschränkungen in den zu verwendenden Programmiersprachen, bereits vorhandene und damit zu verwendende Dateien sowie auf sie abgestimmte Programmsysteme), — Systemauswahl noch zu beschaffender Hard-/Software, — Wirtschaftlichkeitsanalysen (Kosten-Nutzen-Betrachtung), — Vorbereitung und Planung der Datenübernahme, Datenbeschaffung und Datenerfassung.

2.1 Der Projektablauf

149

Ebenfalls nicht behandelt werden die der Programmübergabe an den Auftraggeber folgenden Tätigkeiten wie — Installation, Einführung, Schulung, — Datenübernahme und -beschaffung, — Wartung, — Weiterentwicklung.

2.1.2 Die Entwicklungsphasen Innerhalb dieser Grenzen zerfällt eine Systementwicklung in die in Abb. 2.1.2-1 dargestellten Tätigkeiten — Spezifikation, — Planung, — Realisierung (Feinplanung, Programmierung, Test), — Dokumentation. Die Abbildung zeigt zugleich die gegenseitigen Abhängigkeiten dieser Tätigkeiten. Es ist möglich, daß die Realisierung Rückwirkungen auf die Planung hat, weil sich herausstellt, daß eine Änderung der Planung eine wesentlich einfachere und ökonomischere Programmierung ermöglicht. Dagegen sollte bei der Planung die Spezifikation nicht mehr abgeändert werden. Als exakte Formulierung der Aufgabenstellung liegt die Spezifikation nämlich teilweise außerhalb des hier betrachteten Rahmens der Programmentwicklung: ihre Änderung bedeutet zugleich eine Änderung der Aufgabenstellung. Diese hat beträchtliche Folgen auch für nicht unmittelbar mit der Programmierung befaßte Personen oder Instanzen (Auftraggeber, Anwender, Kunden). Sie sollte deshalb als Ausnahmebedingung, als „Fehler" in der Spezifikation oder in den die Spezifikation vorbereitenden Tätigkeiten (vgl. Abschnitt 2.1.1) betrachtet werden. Die in Abb. 2.1.2-1 gewählte Aufteilung in Entwicklungsphasen entspricht nicht der „klassischen" Abschnittsaufteilung einer Programmentwicklung. Abb. 2.1.2-2 stellt den herkömmlichen und gegenwärtigen Ablauf einer Programmentwicklung einander gegenüber. Die wesentlichen Unterschiede zwischen diesen beiden Entwicklungskonzepten sind die folgenden: — Die früher als getrennt angesehenen Tätigkeiten Feinplanung, Kodierung und Modul-Test werden nicht mehr unterschieden. Dies führt zu einer „Aufwertung" des Programmierers und damit einmal zu einer besseren Motivation des Programmierpersonals, zum anderen aber auch zu einer klareren Abgrenzung der Tätigkeiten, Verantwortlichkeiten und Berichtswege.

150

2. Systementwicklung

— Der Integrationstest als solcher ist nicht mehr vorhanden. Er wird durch geeignete Planung (Top Down-Entwicklung) und laufende Überwachung der entstehenden Module und ihrer Einfügung in das Gesamtprojekt über die gesamte Entstehungsdauer des Programms verteilt. Dies ermöglicht eine laufende Information des Projektmanagements über die Termin- und Kostensituation, Engpässe und Fehlschätzungen. Gegebenenfalls auftretende Mißverständnisse bezüglich der Schnittstellen von Komponenten werden frühzeitig erkannt. — Die Dokumentation entsteht nicht nach, sondern während der Programmplanung und -realisierung. Damit steht sie der Projektleitung frühzeitig als Informationsmaterial zur Verfugung und wird auch bei Wechsel des Programmierpersonals im wesentlichen von den mit der Ausführung der Planungsund Programmierarbeiten betrauten Personen selbst erstellt und gepflegt. Die

Abb. 2.1.2-1. Die Phasen einer Systementwicklung

2.1 Der Projektablauf herkömmlich

151 gegenwärtig

Abb. 2.1.2-2. Herkömmliche und gegenwärtige Phasenaufteilung einer Programmentwicklung

2. Systementwicklung

152

kontinuierliche, dem Projektfortschritt parallele „Top Down"-Entwicklung der Dokumentation vermeidet Doppelarbeiten. Schließlich ermöglicht die Reduktion der „abschließenden" Dokumentationsphase auf eine reine Editierung und Aufbereitung der bereits vorhandenen Unterlagen eine schnelle Bereitstellung der Dokumentation mit oder kurz nach der Übergabe des Programmprodukts und eine rasche Verfügbarkeit des Programmierpersonals für neue Aufgaben.

2.2 Die Spezifikation 2.2.1 Die Aufgabe einer Spezifikation 2.2.1.1 Das Benutzermodell und die Schnittstellenbeschreibung Die Spezifikation ist die exakte Definition der zu lösenden Programmieraufgabe. Nach Abschnitt 1.1.2 ist diese die Transformation einer Basismaschine in eine Benutzermaschine. Dies gilt für die kooperative Systementwicklung ebenso wie für die „Solo-Programmierung"; nur werden hier an die Spezifikation wesentlich höhere formale Anforderungen gestellt. In der industriellen Softwareentwicklung ist sie keine informelle Skizze mehr, mit der sich der Programmierer klar wird, was seine Ziele sind. Sie ist vielmehr das wichtigste Dokument des Projekts überhaupt: die verbindliche Grundlage für jede Kommunikation zwischen Auftraggeber, Benutzer, Programmierer und allen anderen an dem Projekt interessierten Stellen. Die Spezifikation schließt den vorbereitenden Teil der Projektdurchführung ab. Nach Abschnitt 2.1.1 dokumentiert sie das Ergebnis der problembezogenen Aufgaben-, Umgebungs-, System- und Wirtschaftlichkeitsanalyse. Da bei einer industriellen Softwareentwicklung Programmplaner und Programmierer nicht mehr selbst die Benutzer des Produkts sind, erfordert die zweckmässige Festlegung der Benutzerschnittstelle, der zu realisierenden Benutzermaschine, gründliche Vorarbeiten. Ihr wichtigstes Ergebnis und damit Ausgangspunkt für die Spezifikation ist das Benutzermodell für das zu entwickelnde System. Dieses beschreibt möglichst exakt die Eigenschaften und Wünsche der verschiedenen in Aussicht genommenen Anwender. Ein industrielles Softwareprodukt hat in der Regel mehr als eine hier zu berücksichtigende Bemitzerklasse. Typische, in ihren Eigenschaften, Aufgaben und Anforderungen völlig unterschiedliche Benutzer desgleichen Softwareproduktes sind in Tabelle 2.2.1.1-1 aufgeführt.

2.2 Die Spezifikation

153

Tab. 2.2.1.1-1. Die wichtigsten Benutzerklassen eines Software Produkts besitzt er in der Regel

Benutzer

fachliche Problemkenntnisse?

EDVKenntnisse? primäre Interessen

Auftraggeber

ja

(meistens) nein

Optimisierung der Nutzen/ KostenRelation

(problembezogene) Benutzer

ja

nein

Erfüllung seiner fachlichen Anforderungen, Zuverlässigkeit, Ökonomie im Einsatz

Systembediener (Operateure)

nein

ja

leichte Bedienbarkeit, Zuverlässigkeit

AnwendungsProgrammierer

ja ja (mit Einschränkung)

bequeme Anwendung, Verständlichkeit der Funktionen, Zuverlässigkeit

Hauptbenutzer vor allem bei Programmen, die (zumindest teilweise) Basis-Software-Charakter für auf sie aufbauende Anwendersysteme haben.

WartungsProgrammierer

nein

Verständlichkeit der Struktur, Änderungsund Anpassungsfreundlichkeit

ändert, ergänzt und korrigiert das fertig erstellte Produkt, hat es meist nicht selbst mitentwickelt.

ja

Bemerkungen

Auftraggeber selbst, Untergebene des Auftraggebers, Kunden des Auftraggebers.

D a s B e n u t z e r m o d e l l für j e d e dieser Benutzerklassen beschreibt — die A u f g a b e n des Benutzers, — seine p r o b l e m - u n d E D V - f a c h l i c h e n K e n n t n i s s e , — seine Wünsche u n d A n f o r d e r u n g e n , — v o n i h m zu b e a c h t e n d e V o r s c h r i f t e n u n d E i n s c h r ä n k u n g e n (Weisungsgebundenheit, gesetzliche Vorschriften, Zugriffsbeschränkungen und Datenschutzanforderungen, Überwachungsmöglichkeiten), — die v o n d e m B e n u t z e r an das S y s t e m z u g e b e n d e n A n w e i s u n g e n u n d D a t e n , — die v o m B e n u t z e r v o m S y s t e m g e w ü n s c h t e n R ü c k m e l d u n g e n , D a t e n u n d sonstigen Funktionen.

154

2. Systementwicklung

Ein Benutzermodell wird in der Regel verbal beschrieben, allenfalls kommen erläuternde Diagramme und Zeichnungen (z. B. übliche oder gewünschte Formular- und Listenbilder, Organisations- und Informationsfluß-Diagramme) hinzu. Es gibt jedoch auch Ansätze zu einer formalen Beschreibung von Benutzermodellen [GEIS72, OBER73]. Ausgehend von den Benutzermodellen kann dann die Benutzermaschine definiert werden: die exakte Beschreibung der Maschine, welche die verschiedenen Benutzer von ihren Standpunkten aus „sehen wollen", nachdem das zu erstellende Software-Produkt in die EDV-Anlage geladen und gestartet wurde. Entsprechend Abschnitt 1.1.2 legt diese Beschreibung fest: — Welche Eingaben (Anweisungen, Daten) der betreffende Benutzer von welchen Endgeräten der EDV-Anlage aus (Terminal, Kartenleser, Magnetbändern, Bedienungskonsolen) vornehmen kann oder muß. — Wie die EDV-Anlage auf diese Eingaben normalerweise und in Sonderfällen (z. B. bei Überlastung, falscher Bedienung) reagiert. — Welche Ausgaben und Meldungen der Benutzer von der EDV-Anlage erhält. Auch an die Festlegung der Basismaschine werden in der industriellen Softwareproduktion höhere formale Anforderungen gestellt. Sie wird beschrieben durch die zur Verfügung stehende Konfiguration, d. h. — Zentraleinheit oder Teil von ihr (Partition, Region), — Externspeicherdateien und -geräte, — Ein-/Ausgabegeräte, — sonstige Einrichtungen (z. B. angeschlossene Meßgeräte, Zähler), sowie dem zu ihrer Bedienung nötigen

Anweisungsvorrat.

Dieser Anweisungsvorrat ist in der Regel durch die (niedere oder höhere) Programmiersprache(n) gegeben, in welcher das Softwareprodukt zu erstellen ist. Die wichtigste formale Forderung an eine Spezifikation ist, daß in ihr ausschließlich die Benutzer- und die Basismaschine, d. h. die Schnittstellen des Programms zu Benutzern und zur Hardware beschrieben werden, nicht die Realisierungsvorstellungen für die Software. Erfahrungsgemäß führt ein Vermischen von Schnittstellen- und Realisierungsbeschreibung in der Spezifikation zu Schwierigkeiten in der Planung und in der Kommunikation mit dem Auftraggeber. Die Spezifikation verliert dann ihre Funktion als exakte und verbindliche Definition der Aufgabenstellung', es läßt sich nicht mehr genau feststellen,

2.2 Die Spezifikation

155

was Schnittstelle (und damit eine für den Auftraggeber wesentliche, vielleicht sogar unabdingbare Vorgabe) und was Realisierungsvorstellung (und damit dem Auftraggeber ziemlich gleichgültig und leicht abänderbar) ist. Realisierungsvorstellungen werden grundsätzlich erst in der anschließenden Planung erarbeitet. Nur in zwei Fällen sind als Ausnahme von dieser Regel Hinweise auf die Realisierung bereits in der Spezifikation zulässig: — Werden auf Grund von Realisierungsproblemen Kompromisse in der Benutzermaschine gemacht, d. h. wünschenswerte Funktionen weggelassen oder nicht optimal realisiert, so ist ein kurzer Hinweis hierauf angebracht (am besten als Anmerkung bei der „Funktionalen Beschreibung", vgl. Schema 2.2.2.2-1). — Fordert der Auftraggeber zur Realisierung einer Funktion einen bestimmten Algorithmus (etwa aus internen Standardisierungsgründen), so ist seine Verwendung Teil der Aufgabenstellung und damit der Spezifikation. Diese Vorgabe muß dann im Abschnitt „Programmaufbau" (vgl. Schema 2.2.2.2-1) spezifiziert werden. 2.2.1.2 D e r Spezifikationsrahmen und die endgültige Spezifikation Die Exaktheit und Genauigkeit der Beschreibung von Benutzermaschine und Basismaschine kann meist in frühen Versionen einer Spezifikation noch nicht in dem Maß gewährleistet werden, wie in der endgültigen, zu Beginn der Planungsarbeiten erforderlichen Fassung. Man schreibt deshalb als Vorstufe zur vollständigen, verbindlichen Spezifikation oft einen Spezifikationsrahmen. Dieses Dokument wird zuweilen auch Rahmenspezifikation genannt — nach den Wortbildungs-Regeln der deutschen Sprache ist diese Bezeichnung jedoch wenig glücklich. Es handelt sich ja nicht um eine Spezifikation für einen Rahmen, sondern um einen „Rahmen" für die ausführliche Spezifikation. Ein Spezifikationsrahmen unterscheidet sich von der endgültigen Fassung hauptsächlich dadurch, daß Benutzer- und Basismaschine zwar in ihrer allgemeinen Leistungsfähigkeit, aber noch nicht detailliert beschrieben werden. In der Beschreibung der Basismaschine wird z. B. bereits gesagt, daß man eine Zentraleinheit mit n kBytes Arbeitsspeicher, einen Kartenleser, einen Drucker und Platten benötigt, es ist aber u. U. noch nicht festgelegt, welche Modelle welches Hardwareherstellers und welche Programmiersprachen tatsächlich benutzt werden.

2. Systementwicklung

156

Für die Benutzerschnittstelle beschränkt sich der Spezifikationsrahmen beispielsweise auf eine qualitative Aufführung der als Bewegungsdaten einzugebenden Informationen, ohne auf die tatsächlichen Feld- und Satzformate und den Eingabedaten-Träger (Karten, Bänder, beides?) einzugehen. Der Spezifikationsrahmen kann deshalb zwar bereits für Entscheidungen über die Realisierung des Projekts, spätere Einsatzbreite u. ä., nicht aber als Planungsunterlage oder zur Einsatzvorbereitung dienen. Ais Grundlage zur Planung reicht ein Spezifikationsrahmen keinesfalls aus. Diese darf erst nach Erstellung der endgültigen Spezifikation und ihre verbindliche Genehmigung durch den Auftraggeber begonnen werden. Zwar werden sich nachträgliche Änderungen der Spezifikation durch den Auftraggeber nie ganz vermeiden lassen. Ein Spezifikationsrah: men ist aber ein viel zu schwankender Grund, um auf ihm eine Planung aufzubauen — die hohe Zahl der noch zu erwartenden Änderungen und Detailspezifizierungen führen sonst fast zwangsläufig zu einer durch dauernde Anpassungen und Korrekturen inkonsistenten, unsauberen und mangelhaften Planung. Die endgültige Spezifikation muß die von dem zu erstellenden Softwareprodukt zu leistende Abbildungsaufgabe exakt und vollständig beschreiben. Dies ist erst dann der Fall, wenn sowohl -

die Anwenderfunktionen (etwa in Form einer „Befehlsliste für die Benutzermaschine") als auch

-

die Basismaschine (Hardwarekonfiguration, Hersteller-Betriebssystem, Programmiersprache)

eindeutig festliegen (vgl. Abb. 2.2.1.2-1).

2.2.2 Der Aufbau einer Spezifikation 2.2.2.1 Der Leserkreis und die Strukturforderungen Die meisten Regeln für den formalen Aufbau einer Spezifikation sind in zwei Tatsachen begründet: - Eine Spezifikation richtet sich an einen breiten Leserkreis mit sehr unterschiedlichen Kenntnissen, Anforderungen und Informationswünschen. - Eine Spezifikation sollte als Grundlage für alle späteren Dokumente und Beschreibungen des fertigen Softwareprodukts dienen (vgl. hierzu [SCHN74b]).

2.2 Die Spezifikation

157

Anwender-Eingabe- und Ausgabe-Daten, Anwenderfunktionen

Daten- und Speicherstruktur, Anweisungsvorrat Abb. 2.2.1.2-1. Die Definitionsaufgabe einer Spezifikation

In Abschnitt 2.2.1.1 wurden bereits die verschiedenen Benutzerklassen eines Softwareprodukts diskutiert. Alle diese Benutzer sollen später das Produkt als „benutzerfreundlich" empfinden: sie sollen es leicht und angenehm bedienen können und genau die Ausgaben und Meldungen erhalten, die sie sich wünschen. Das ist nur zu erreichen, wenn Angehörige jeder Benutzerklasse die Spezifikation lesen und kommentieren. Deshalb gelangt eine Spezifikation in viele Hände, z. B. — Software- und Hardware-Spezialisten (wie die späteren Implementierer), — Anwendungsfachleute (die — vielleicht ohne irgendwelche EDV-Kenntnisse zu besitzen — die funktionelle Beschreibung, d. h. die „Benutzermaschine" auf ihre Zweckmäßigkeit prüfen müssen), — Laien (z. B. Kaufleute, die lediglich einen ungefähren Eindruck von den Anwendungsgebieten und dem wirtschaftlichen Potential gewinnen wollen).

158

2. Systementwicklung

Das in der Spezifikation vorgelegte Material muß deshalb so aufgeteilt werden, daß jede dieser Gruppen das sie Interessierende leicht in eigenen Abschnitten findet. Daß dabei derselbe Sachverhalt zuweilen in mehreren Abschnitten mehr oder minder detailliert oder mit jeweils anderem Schwerpunkt beschrieben wird, schadet nichts. Eine Spezifikation wird sowohl von Personen gelesen, die sich gründlich in die Details vertiefen müssen (z. B. die Implementierer des Systems), andererseits aber auch von solchen, die sich nur oberflächlich orientieren wollen. Diese sind häufig sogar sehr wichtig — Personen, die grundlegende Entscheidungen treffen, haben leider meist nicht die Zeit, der Spezifikation eines Produkts mehr als etwa eine halbe Stunde zu widmen. Trotzdem sollen sie ausreichend informiert werden. Flüchtige Leser müssen die sie interessierenden Abschnitte (z. B. die funktionelle Beschreibung oder die Beschreibung der Basismaschine) überfliegen können. Dabei müssen sie geführt werden. Die Mittel dazu sind — ein detailliertes und übersichtlich strukturiertes Inhaltsverzeichnis, — Einrücken von Exkursen, Beschreibungen von Sonder- und Fehlerbedingungen u. ä. gegenüber dem Haupttext, — Verlegung allen ergänzenden Materials, formaler Beschreibungen u. ä. in Anhänge, — Übersichten, Schemata und Beispiele, die das Wesentlichste zusammenfassen. Eine Spezifikation dient aber nicht nur vielen unterschiedlichen Lesern und Interessenten. Sie ist als Basisdokument für das spezifizierte Produkt auch die Keimzelle für fast alle späteren Druckschriften, Manuale und Beschreibungen, die für dieses Produkt angefertigt werden. Die Abschnittsaufteilung, der Schreibstil und die Struktur der Spezifikation sollten also so eingerichtet werden, daß diese Dokumente aus den entsprechenden Teilen der Spezifikation ohne viel Redaktionsarbeit entwickelt werden können. Es muß deshalb darauf geachtet werden, daß jeder Abschnitt der Spezifikation möglichst selbständig ist und nur wenig Referenzen auf andere enthält. 2.2.2.2 Die Hauptabschnitte einer Spezifikation Eine Spezifikation zerfällt in Abschnitte, die sich — teils im Inhalt unterscheiden (z. B.: Beschreibung der Hardware - Beschreibung der Software) und

2.2 Die Spezifikation -

159

teils sich zwar überdecken, aber an andere Leser richten (z. B. Kurzbeschreibung für den flüchtigen Leser oder als erste V e r k a u f s i n f o r m a t i o n , ausführliche f u n k t i o n a l e Beschreibung für den näher interessierten Leser, formale Beschreibung als Implementierungsgrundlage u n d als definitive Unterlage zur Klärung von Unklarheiten oder Mehrdeutigkeiten).

Für die Aufteilung in Haupt- u n d Unterabschnitte kann keine bindende Vorschrift angegeben werden, da sie selbstverständlich weitgehend von dem zu spezifizierenden P r o d u k t abhängt. Ein in der Praxis bewährtes Muster für die G r o b s t r u k t u r einer Spezifikation zeigt Schema 2.2.2.2-1. Es k a n n als Ausgangspunkt für den E n t w u r f dienen u n d durch Weglassen, Ergänzen u n d Modifikation der Abschnitte auf den k o n k r e t e n Einzelfall angepaßt werden.

(1) (2)

Inhaltsverzeichnis Systemziele („Objectives") Anwenderanforderungen und -verhalten, Systemumgebung („Benutzermodelle")

(3) (4) (5) (6) (7) (8) (A) (B)

Kurzbeschreibung des Systems Hardware-Konfiguration und sonstige Betriebsmittel (Basismaschine") Funktionale Beschreibung („Benutzermaschine") Erweiterte Betriebs- und Ausbaumöglichkeiten Programmaufbau Literaturverzeichnis Allgemeiner Anhang Anhang für beschränkten Verteilerkreis

(0)

Schema 2.2.2.2-1. Muster für die Abschnittsaufteilung einer Spezifikation

Das Inhaltsverzeichnis ist einer der wichtigsten Bestandteile einer Spezifikation. Es hat zentrale Bedeutung sowohl während des Schreibens der Spezifikation als auch später für den Leser. Das Inhaltsverzeichnis einer gut geschriebenen Spezifikation hat Ähnlichkeit mit der „Hauptsteuerleiste" eines Programms - es sorgt dafür, daß sowohl beim Schreiben als auch beim Lesen (und noch später bei der „Wartung") die logische S t r u k t u r des Ganzen jederzeit erkennbar ist.

160

2. Systementwicklung

Während des Schreibens der Spezifikation hilft das Inhaltsverzeichnis, einen Überblick über deren Einzelkomponenten (Abschnitte und Unterabschnitte) zu bekommen und zu behalten. Dem Leser soll das Inhaltsverzeichnis helfen — sich einen ersten Überblick über den Inhalt des Dokumentes zu verschaffen, — leicht die ihn interessierenden Abschnitte auszuwählen, — gezielt Detailinformationen zu finden. Es ist deshalb wichtig, daß die im Inhaltsverzeichnis aufgeführten Abschnittsund Unterabschnittsüberschriften möglichst sinnfällig und nicht zu aussagearm (nicht „Allgemeines", „Sonstiges" o. ä.) sind. Sie sollten auch so gewählt sein, daß sie bereits beim sequentiellen Lesen des Inhaltsverzeichnisses einen Eindruck von der logischen Struktur des Dokuments geben. Dies wird am besten durch eine Redaktion und ggf. entsprechende Abänderungen des Inhaltsverzeichnisses nach Fertigstellung des gesamten Textm^terials erreicht. Die Systemziele (im angloamerikanischen Sprachgebrauch Objectives genannt) beschreiben kurz, was die wesentlichen Leistungen des Systems sind, warum also das System überhaupt entwickelt wird. Aus den übrigen Abschnitten der Spezifikation geht nämlich in der Regel nicht klar hervor, welche Funktionen für den Auftraggeber wirklich wichtig und unabdingbar sind und welche lediglich als „Beigabe" empfunden werden. Sind später (z. B. bei Realisierungsproblemen, Etatkürzungen) Abänderungen der Spezifikation nötig, so bewahrt eine klare Formulierung der Systemziele vor der häufigen Erscheinung, daß hierbei die wesentliche „Substanz" des Produkts und damit seine Brauchbarkeit für den tatsächlichen Einsatz zerstört wird. Der Abschnitt Anwenderanforderungen und -verhalten, Systemumgebung beschreibt die Benutzermodelle soweit, daß der Leser sich ein Bild über Einsatzart und -zweck des Systems machen und damit seine funktionale Beschreibung verstehen kann. Die Kurzbeschreibung des Systems faßt die folgenden Abschnitte in einer Form zusammen, daß ein Leser, der sich nur oberflächlich informieren will, nach ihrer Lektüre einen ersten, groben Überblick hat. Die Kurzbeschreibung ist vor allem bei Systemen wesentlich, die nicht nur für einen einzigen Anwender entwickelt sondern kommerziell vertrieben oder innerhalb einer größeren Benutzerorganisation kostenlos zur Verfügung gestellt werden sollen. Sie dient dann, aus der Spezifikation herausgelöst, als Vorab-Information und Grundlage für die spätere Einführungsschrift oder den Prospekt.

2.2 Die Spezifikation

161

In Hardware-Konfiguration und sonstige Betriebsmittel wird die Basismaschine beschrieben (vgl. Abschnitt 2.2.1.1). Die Funktionale Beschreibung beschreibt die Benutzermaschine, d. h. — die Bedienungsmaßnahmen der verschiedenen Anwender, — die Normal-, Ausnahme- und Fehlerreaktionen des Systems, ^ Ein- und Ausgabe-Datenformate in Prosa mit Beispielen und Abbildungen. In diesem Abschnitt sind formale Beschreibungsmethoden (Backus-Naur-Notation, Entscheidungstabellen, SyntaxGraphen u. ä.) grundsätzlich zu vermeiden. Sie gehören in den Anhang. Die Funktionale Beschreibung sollte so geschrieben werden, daß sie später praktisch unverändert als Benutzer-Manual übernommen werden kann. Erweiterte Betriebs- und Ausbaumöglichkeiten beschreibt das Entwicklungspotential des Softwareprodukts, wie Umstellung auf interaktiven Terminal-Betrieb, mögliche Ergänzung durch Listengeneratoren oder Information Retrieval-Hilfsprogramme für die durch es aufgebauten und gepflegten Datenbestände o. ä. Der Abschnitt Programmaufbau ist in einer Spezifikation in der Regel sehr kurz. Er beschreibt lediglich diejenigen Eigenschaften des geplanten Programms, die sich bereits ohne jede Planungsaktivität aussagen lassen, wie — benutztes Betriebssystem und Programmiersprachen (wenn durch den Auftraggeber entsprechende Vorgaben gemacht werden), — bereits vorhandene und in das System zu integrierende Hilfs- und Dienstleistungsprogramme , — sich aus der logischen Systemstruktur ergebende Komponenten- oder Modulaufteilung (z. B. Dateierstellungs-Programm, Update-Programm, Auswertprogiamm, Dateisicherungs-Programm). Das Literaturverzeichnis führt die zum Verständnis der Spezifikation und für die Planung und Implementierung empfohlene Literatur wie — problembezogene und EDV-Fachaufsätze, — Lehrbücher und Nachschlagwerke über einzusetzende Methoden und Verfahren, — Herstellermanuale auf.

162

2. Systementwicklung

In den allgemeinen Anhang gehört alles zusätzliche Material, das für das Verständnis der Spezifikation nützlich erscheint und gegen dessen allgemeine Verbreitung keine Bedenken bestehen. Dazu könnten z. B. gehören — eine technische Beschreibung der Hardware, — formale Beschreibungen wie, Syntaxgraphen, Backus-Naur-Sprachdefinitionen, Entscheidungstabellen, Algorithmen in einer Dokumentationssprache wie ALGOL oder PASCAL, HIPOs, Struktogramme, — relevante Abschnitte aus der Herstellerliteratur (z. B. die Beschreibung verwendeter Systemmakros), — für das Produkt interessante Verordnungen, Vorschriften, DIN-Normen u. ä. Im Anhang für beschränkten Verteilerkreis wird das Material gesammelt, das vertraulichen Charakter hat und deshalb (als letzte Seiten in der Spezifikation!) leicht und unauffällig abtrennbar sein soll. Wie bereits gesagt, ist die Spezifikation nämlich meist kein vertrauliches Dokument, sondern wird im Gegenteil von einer großen Zahl unterschiedlicher Personen inner- und außerhalb des Hauses des Auftraggebers gelesen. Derartiges vertrauliches Material ist z. B. — Preise, Kosten, Termine, Personalplanungen, — Ergebnisse von Marktuntersuchungen oder Benutzerbefragungen, — Konkurrenzvergleiche, — Hinweise auf Mängel oder Unvollkommenheiten in der vorhandenen Hard- oder Software, welche behoben werden müssen oder Einflüsse auf Planung und Implementierung haben, — etwaige nicht vermeidbare Mängel oder sonstige Probleme des spezifizierten Produkts, auf welche der Auftraggeber vorsorglich aufmerksam gemacht werden soll.

2.2.3 Formale Schreibregeln 2.2.3.1 Sinn der formalen Regeln Bei der Abfassung einer Spezifikation sollten einige Regeln für die Aufmachung und den Schreibstil befolgt werden. Sie haben zwei häufig überlappende Ziele:

2.2 Die Spezifikation

163

— Sie sollen der Spezifikation ein gefälliges, leicht lesbares und übersichtliches Äußeres geben. - Sie sollen bei Änderungen, Ergänzungen, Streichungen sowie bei der Extrahierung von Auszügen für die spätere Produktdokumentation den Aufwand für Editions- und Umschreibarbeiten verringern. Spezifikationen werden laufend geändert und ergänzt. Die meisten derartigen Änderungen sollten mit Tipp-Ex, Schere und Klebstoff ohne Neuschreiben größerer Abschnitte durchzuführen sein. Andernfalls wird nicht nur das Sekretariat überlastet. Das immer wieder notwendige Korrekturlesen größerer, eigentlich unveränderter Textabschnitte verbraucht viel wertvolle Zeit unproduktiv und wird deshalb in der Regel nicht sorgfältig durchgeführt - dies ist einer der Gründe der hohen Fehlerzahl in vielen Spezifikationen und Dokumentationen.

2.2.3.2 Die Abschnittsstrukturierung und die äußere Form des Inhaltsverzeichnisses Die logische Struktur der Abschnitte, Unterabschnitte usw. spiegelt sich in der Abschnittsnumerierung wieder: -

Hauptabschnitte 1 , 2 , . . . , Anhänge A, B , . . . Unterabschnitte erster Stufe 1.1, 1 . 2 , . . . , 2.1 . . . , A . l , A . 2 , . . . Unterabschnitte zweiter Stufe 1.1.1, 1 . 1 . 2 , . . . , A . l . 1, A . l . 2 , . . .

Werden beim Schreiben des Inhaltsverzeichnisses prinzipiell zwischen der letzten Ziffer der Abschnittsnummer und dem ersten Buchstaben des Abschnittstitel die gleiche Anzahl Zwischenräume gesetzt (z. B. 12), so sind die Uberschriften der Unterabschnitte im Inhaltsverzeichnis automatisch entsprechend ihrer „Hierarchiestufe" eingerückt, ähnlich der optischen Strukturierung eines übersichtlich geschriebenen PASCAL-, ALGOL- oder PL/1-Programms. 2.2.3.3 Anordnungslogik im Inhaltsverzeichnis und im laufenden Text Beim Schreiben einer Spezifikation steht man häufig vor den folgenden Fragen: — In welche Reihenfolge ordnet man Alternativen? — Wie erreicht man, daß Exkurse, die Beschreibung von Sonderfällen oder Randbemerkungen den logischen Fluß des Haupttextes nicht stören? Hier helfen die folgenden Regeln: — Alternativen werden grundsätzlich so geordnet, daß zuerst die „normale", häufigste oder einfachste Möglichkeit beschrieben wird. Dann kommen die weniger häufigen, und schließlich Ausnahme- und Fehlerfälle.

164

2. Systementwicklung

Abb. 2.2.3.3-1 zeigt dies an einem Ausschnitt aus dem Inhaltsverzeichnis der Spezifikation einer Telex-Speichervermittlung. Diese ersetzt die üblichen Relais-Schaltkästen für Nebenstellenanlagen durch einen Rechner und ermöglicht neben dem herkömmlichen Direktdurchschaltungs-Verkehr neue, bis jetzt unübliche Betriebsweisen. — Innerhalb eines (Unter-) Abschnitts werden gegenüber dem die Hauptsache darstellenden Text alle Bemerkungen, Beschreibungen von Sonderfällen u. ä. durch Einrückung abgesetzt. Der flüchtige oder eilige Leser sieht auf diese Weise sofort, was er auslassen kann, ohne grundlegend Wichtiges zu versäumen. Dieser eingerückte Text ist eine derartige, als Beispiel dienende Bemerkung!

Betriebsmöglichkeiten 4.2 4.2.1 Durchschaltebe trieb

4.2.2

Speicherbetrieb

4.2.3 4.2.4

Gebührenerfassung Verhalten bei Besetztfällen und Überlastung

(die „normale", auch in konventionellen Vermittlungen übliche Betriebsweise) (die unkonventionelle, neue Betriebsweise) (ein Sonderdienst) (Ausnahme- und Fehlerverhalten)

Abb. 2.2.3.3-1. Ausschnitt aus dem Inhaltsverzeichnis der Spezifikation eines Speichervermittlungs-Systems für Telex-Anlagen (Beispiel für die Anordnung von Alternativen)

2.2.3.4 Präsens oder Futur? Unwillkürlich schreibt man eine Spezifikation, die ja etwas Zukünftiges darstellt, mit futuralen Verbformen: — „das System wird es zusätzlich ermöglichen . . .", — „das Programm soll in eine Reihe von Overlaysegmenten aufgeteilt werden . . . ", — „es werden folgende Fehlermeldungen vorgesehen . . . ". Dies ist falsch. Die Spezifikation dient auch als Grundlage für alle später zu schreibenden Druckschriften über das Produkt: Einführungen, Benutzerbeschreibungen, technische Dokumentation u. ä.

2.2 Die Spezifikation

165

In alle diese Druckschriften sollten Textabschnitte aus der Spezifikation praktisch unverändert übernommen werden können. Eine mühsame Redaktionsarbeit, die alle Futurformen in die Gegenwart übersetzt, ist aufwendig und, in Ermangelung geeigneter DV-Textmanipulationshilfen, fehleranfällig. Deshalb müssen alle Verben ins Präsens gesetzt werden (oder sogar in die Vergangenheit: „der Modul XYZ wurde maschinenunabhängig konzipiert"). Richtig hießen die obigen Beispiele: — „das System ermöglicht es zusätzlich . . . ", — „das Programm ist (oder wurde) in eine Reihe von Overlay-Segmenten aufgeteilt — „folgende Fehlermeldungen sind (oder wurden) vorgesehen . . . ".

2.2.3.5 Seiten-, Formel- und Abbildungsnumerierung Eine Spezifikation wird laufend ergänzt und geändert. Deshalb — sollte in der Regel jeder neue Abschnitt und Unterabschnitt auf einer neuen Seite anfangen, — werden die Seiten zweckmäßigerweise nicht absolut, sondern nur relativ zum Abschnitt oder Unterabschnitt numeriert. Abbildungen und Tabellen werden ebenfalls nicht durchgehend „spezifikationsglobal", sondern „unterabschnittslokal" numeriert, wobei die Abschnittsnummer von der relativen Abbildungsnummer durch einen Bindestrich getrennt ist: Abb. 2.4.1-2 ist die zweite Abbildung im Unterabschnitt 2.4.1. Entsprechend sollten auch Formeln, Algorithmen u. ä. lokal numeriert werden, wobei Abschnitts- und Formel-Nummern etwa (wie im vorliegenden Buch) durch einen Querstrich getrennt werden können: Formel 1.2.3/2 ist die zweite im Unterabschnitt 1.2.3.

2.2.3.6 Übernahme fremder Texte Besonders im Anhang wird in der Regel fremdes Material unverändert oder nur leicht abgeändert verwendet. Dies ist erwünscht — eine gute Spezifikation verlangt vom Leser nicht, wichtiges ergänzendes Material erst mühsam zu beschaffen, sondern präsentiert es ihm im Anhang. Unverändert übernommene Dokumente werden vom Originaltext abkopiert. Abgeänderte Dokumente sollten nicht neu geschrieben werden, sondern sichtbar aus den kopierten, übernommenen Textteilen und den Ergänzungen und Ände-

166

2. Systementwicklung

rungen montiert werden. Dies vermeidet von vornherein den Eindruck, man „schreibe nur a b " oder verwende das fremde Material als Füllsel zum Auspolstern der zu geringen eigenen Substanz.

2.2.3.7 Einsatz von Kopiergeräten Eine gute Spezifikation ist modular aufgebaut: Teile aus ihr müssen abgetrennt und ohne viel Redaktionsaufwand als selbständige Druckschriften verwendet werden können. Das hat zur Folge, daß sich Texte an verschiedenen Stellen, vielleicht mit leichten Abänderungen, wiederholen. Diese wiederholten Texte — sollen immer gleichen Aufbau und Aufmachung haben, um Vergleiche zu vereinfachen, und — sollen möglichst wenig Mühe und Schreibarbeit machen. Als Beispiel können etwa die technischen Beschreibungen der verschiedenen Modelle in einem gut gestalteten Herstellerprospekt für Autos, Radios oder Kühlschränke dienen. Auch hier ist jedes Einzelprodukt vollständig, ohne Verweise auf andere, aber immer in der gleichen Weise beschrieben. Es ist deshalb zweckmäßig, den sich ständig wiederholenden Grundtext ein einziges Mal als Vorlage zu schreiben, ihn entsprechend häufig zu kopieren und auf der Kopie jeweils die veränderlichen Teile zu ergänzen.

2.2.4 Zusammenfassung der Grundregeln Die wichtigsten Gesichtspunkte bei Entwurf und Schreiben der Spezifikation sind im folgenden zusammengestellt. — Eine Spezifikation enthält nur Aussagen über das was (die Anforderungen an das Softwareprodukt), aber keine Aussagen über das wie (die programmtechnische Realisierung). — Eine Spezifikation muß eine exakte und vollständige Beschreibung der von dem Softwareprodukt zu lösenden Aufgabe darstellen. Deshalb müssen die Funktionen der Benutzer- und der Basismaschine in allen Einzelheiten und nicht nur in ihrer generellen Leistungsfähigkeit (wie im Spezifikationsrahmen) festgelegt werden. — Die verschiedenen Lesergruppen einer Spezifikation müssen das sie interessierende Material leicht und möglichst übersichtlich zusammengefaßt vorfinden. Sie sollen dabei durch das Inhaltsverzeichnis geführt werden.

2.3 Die Planung

167

— Die Mehrzahl der Leser der Spezifikation sind keine ED V-Fachleute oder zumindest keine Fachleute für das spezielle EDV-Gebiet. Deshalb sind Fachausdrücke auf das notwendigste zu beschränken, formale Darstellungsmittel in den Anhang zu verweisen, Beispiele zu bringen. — Eine Spezifikation soll modular aufgebaut werden. Alle späteren Dokumente zur Beschreibung des fertigen Produkts sollen aus Teilen der Spezifikation ohne viel Redaktionsarbeit entwickelbar sein. Alle Abschnitte sollen deshalb weitgehend selbständig und in sich vollständig sein. — Eine Spezifikation soll leicht wartbar sein. Sie ist so zu schreiben, daß Ergänzungen, Änderungen und Streichungen wenig Aufwand (z. B. Umnumerieren vieler Seiten oder Abbildungen, Verschieben großer Textteile auf der Seite) erfordern.

2.3 Die Planung 2.3.1 Abgrenzung und Definition der Planungsphase Abb. 2.1.2-1 stellte die Planungsphase zwischen die Spezifikation und die Realisierung. Diese Abgrenzung ist nicht sehr aussagekräftig. Die Spezifikationsphase und die Realisierung liefern definierte Endprodukte: die Spezifikation und das ausgetestete, übergabereife Programm. Damit sind auch die Endtermine eindeutig festgelegt, und Erfolg oder Mißerfolg sind zumindest näherungsweise überprüfbar. Eine allgemein akzeptierte Festlegung des Endproduktes einer Planung und damit ihrer Fertigstellung fehlt dagegen bis heute. Häufig wird hierfür in Ausschreibungen, Verträgen u. ä. das Vorliegen von „programmierungsreifen Flußdiagrammen" angenommen. Auch abgesehen davon, daß das Flußdiagramm ein der modernen Softwaretechnologie nicht mehr entsprechendes und damit immer seltener angewendetes Hilfsmittel ist (vgl. hierzu [SCHN74a]), kann dies allenfalls als eine Verlegenheitsdefinition angesehen werden: wann ist ein Flußdiagramm „programmierungsreif"? Auch die Realisierung ist keine rein mechanische Arbeit, sondern eine — sogar sehr anspruchsvolle —geistige Leistung. Sie umfaßt damit zwangsläufig ebenfalls planerische Komponenten: die Feinplanung, welche wie der Test mit der eigentlichen Programmierung (der „Kodierung") eng verwoben ist. Die der Realisierung vorausgehende Planungsphase wird deshalb oft auch als Grobplanung bezeichnet.

168

2. Systementwicklung

Als Endprodukt dieser (Grob-) Planung ist das Systemkonzept und als ihr zeitlicher Abschluß dessen Vorliegen in einer dokumentierten und von den projektverantwortlichen Stellen verabschiedeten Form anzusehen. Während der Weiterentwicklung der Spezifikation zur Produktdokumentation (vgl. Abschn. 2.2.2.1) wird dieses verabschiedete Systemkonzept mit der Spezifikation zu einem Dokument integriert; am einfachsten geschieht dies durch Aufnahme in den Abschnitt Programmaufbau der Spezifikation (Abschnitt (7) im SpezifikationsSchema 2.2.2.2-1). Das Systemkonzept teilt die in der Spezifikation definierte Aufgabe in einzeln realisierbare und in ihrem Zusammenwirken leicht übersehbare Komponenten auf. 2.3.2 Modularisierung Der Grund für die Aufteilung des Gesamtsystems und ihre verbindliche Festlegung vor Beginn der Realisierung ist die Komplexität der Aufgabenstellung: in der Regel ist es nicht möglich, die in der Spezifikation geforderte Benutzermaschine durch einen einzigen Programmierer unmittelbar als Programm in der gewählten Programmiersprache (Basismaschine) realisieren zu lassen. Normale kommerzielle oder technische Anwendungsprogramme liegen bei 1000 bis 10 000, Übersetzer, Compiler, Datenbanksysteme zwischen 10 000 und 100 000 Anweisungen. Dies stellt wohl eine obere Grenze dar, weil noch umfangreichere Programmsysteme (Betriebssysteme für Großrechner) erfahrungsgemäß auf „natürliche Weise" in logisch abgeschlossene Einzelbausteine zerfallen, deren größte dann diesen Umfang erreichen (z. B. PL/1-Compiler). Geht man von einer Jahresleistung von etwa 2000 Anweisungen/Programmierer aus, so kann etwa bei einer Fertigstellungsfrist von einem halben Jahr jeder Programmierer insgesamt ca 1000 Anweisungen liefern. Ein größeres Programmsystem wird deshalb meist von Teams aus mehreren Programmierern entwickelt. Dividiert man die obigen Zahlen für typische Programmgrößen durch diese Programmiererleistung pro Halbjahr, so ergeben sich für Anwendungsprogramme Teamgrößen von 1 bis 10, für größere Systemprojekte von 10 bis 100 parallel arbeitenden Programmierern. Auf die Problematik dieser Abschätzung und die Schwierigkeiten der Leitung größerer Teams wird in Abschnitt 2.6.1.1 eingegangen.

2.3 Die Planung

169

Jeder mitarbeitende Programmierer muß ein oder mehrere Teilkomponenten des Gesamtsystems zur Realisierung zugeteilt erhalten, von denen jede einen Umfang von etwa 100 bis maximal 1000 Anweisungen haben sollte. Die Gesamtaufgabenstellung, welche durch die Spezifikation definiert wird, ist dabei für den einzelnen Programmierer im Grunde uninteressant. Vom Standpunkt der Mehrfachverwendbarkeit der Komponenten, ihrer Unabhängigkeit von anderen Systembausteinen, der späteren Wartung und der Minimisierung der Kommunikationsnotwendigkeiten (ein sehr wichtiger Faktor bei der kooperativen Programmierung, vgl. Abb. 2.6.1.1-2) wäre es sogar am besten, wenn der Programmierer die Gesamtaufgabe gar nicht kennen würde. Aus psychologischen Gründen ist dies natürlich nicht sinnvoll: ein Programmierer, der nicht weiß, ob er an einem Compiler, einem Lohnprogramm oder an einem Betriebssystem arbeitet, ist sicher nicht sehr m o t i v i e r t . . . Die Aufgabenstellung für jeden an der Realisierung arbeitenden Programmierer ist eine Vorgabe für seine Komponente(n): — ihre Benutzerschnittstelle (Prozedur- oder Makroaufrufe, Ein- und AusgabeDaten), — ihre Basismaschine (zu benutzende Programmiersprachen, zur Verfügung stehende Betriebsmittel, zu verwendende „tiefere" Prozeduren), — ihre gewünschte Leistung (verbal oder formal durch Grobstruktogramme, Entscheidungstabellen [HUMB73] o. ä. beschrieben). Eine derartige Komponentenbeschreibung kann als „Spezifikation" einer virtuellen Maschine im Sinne von Abschnitt 1.6 aufgefaßt werden. Es liegt damit nahe, dieses Konzept zu einer generellen Planungsmethode für größere Softwareprojekte auszubauen. Jeder Programmierer realisiert ein oder mehrere virtuelle Maschinen. Seine Vorgabe ist eine exakte Formulierung der Kerne (vgl. Abb. 1.6.1-1) der von ihm zu entwickelnden Primitivoperationen, sowie derjenigen tieferen, die er verwenden darf. Seine Aufgabe ist die Programmierung des Komponentenrumpfs als Folge von Verarbeitungsschritten. Die Top Down-Planung ist damit nicht viel anders als eine mehrfache Wiederholung der Spezifikationsphase auf immer tieferer, hardwarenäherer Ebene. Die Aufteilung des Gesamtsystems in getrennt zu realisierende Einzelkomponenten wird als Modularisierung bezeichnet.

170

2. Systementwicklung

Schon in der konventionellen Planung und Programmierung war eine zweistufige Modularisierung üblich, wobei die größeren Einheiten meist Module, die kleineren funktionale Einheiten (functional units) genannt wurden. Die funktionalen Einheiten entsprechen ungefähr den einzelnen Primitivoperationen einer virtuellen Maschine, oder sie sind Hilfsfunktionen, die von außerhalb des Moduls nicht angesprochen werden können und irgendwelche modul-lokalen Aufgaben erfüllen. Sie sollten so gewählt sein, daß ihre Realisierung nicht mehr als eine Seite in der Programmliste einnimmt; nur dann ist diese von einem Menschen leicht zu überschauen und voll in allen Zusammenhängen zu verstehen. Damit ergeben sich folgende sinnvolle Größenordnungen für eine wünschenswerte Modularisierung: — funktionale Einheit ^ 30 bis 100 Anweisungen, — Modul « 1 0 funktionale Einheiten « 300 bis 1000 Anweisungen ^ 2 bis 6 Bearbeitermonate eines Durchschnittsprogrammierers. Die Aufteilung in Module erfolgte früher jedoch meist nicht nur nach problemlogischen Gesichtspunkten, sondern auch oder sogar überwiegend nach Gegebenheiten der Basis-Maschine (z. B. Größe des vorhandenen Arbeitsspeichers, Notwendigkeit des Einschiebens eines Sortierlaufs als eigenen Job-Step). Damit existierte auch kein logisches Aufteilungskonzept der Module in funktionale Einheiten. Diese wurden in der Regel als Prozeduren (geschlossene Subroutinen) realisiert, die wiederum andere aufriefen. Welche Codestücke in dieser Weise ausgelagert wurden, bestimmte der Programmierer ad hoc, wobei er sich einmal von problemlogischen, ein anderes mal von Realisierungs-Gesichtspunkten und oft auch einfach von seiner Bequemlichkeit (Vorhandensein eines Codestücks, das als Haupt- oder Nebeneffekt mehr oder weniger die gewünschte Funktion erfüllte) leiten ließ. Ein konventionell modularisiertes Softwareprodukt bestand deshalb zwar aus einer Reihe von Einzelkomponenten, die untereinander über mehr oder weniger exakt definierte Schnittstellen kommunizierten, es erfüllte aber auf Grund der nicht einheitlichen und deshalb schwer verständlichen Aufteilungslogik nicht die folgenden wichtigen Bedingungen für eine klare und von anderen nachvollziehbare Planung: — Die Zerlegung der Aufgabe in Teilaufgaben soll die Lösung der Aufgabenstellung verständlich und damit ihre Korrektheit leichter nachprüfbar machen. — Die Komponenten sollen gefahrlos unabhängig voneinander entwickelt und erst später „integriert" werden können.

2.3 Die Planung

171

— Jede Komponente eines Systems soll gegen eine technisch anders realisierte Komponente mit gleicher Benutzerschnittstelle auswechselbar sein (z. B. bei Umstellung der Datenhaltung von einem Datenbanksystem auf ein anderes). — Zur Erleichterung des logischen Verständnisses, des Tests und der Fehlersuche sollen jeweils Untermengen von Komponenten angebbar sein, welche das geplante System vollständig, wenn auch nicht im Detail, beschreiben. Eine derartige Komponente war in einem gut geschriebenen konventionellen System die Hauptsteuerleiste, detailliertere Komponentenmengen mit dieser Eigenschaft waren aber in der Regel nicht mehr angebbar. All diese Forderungen erfüllt die Strukturierung in hierarchische Ebenen, wie sie in Abschnitt 1.6 für den Programmentwurf empfohlen und in Abb. 1.6.1-1 skizziert wurde. Das dort eingeführte Konzept der virtuellen Maschine ist deshalb auch der geeignete Ausgangspunkt für eine Modularisierung: man faßt in einem Modul jeweils eine (oder mehrere) virtuelle Maschinen zusammen, und eine funktionale Einheit ist die Realisierung einer ihrer Primitivoperationen. Damit soll aber keineswegs etwa eine Identität zwischen den Begriffen „Modul" und „virtuelle Maschine" postuliert werden. Im Gegenteil: — ein Modul ist ein Systemteil bei der Aufteilung des Gesamtprogramms nach physikalischen Gesichtspunkten (z. B. getrennte Herstellung, Größe des für die Einzelkomponenten benötigten Arbeitsspeichers, Notwendigkeit der Realisierung in verschiedenen Programmiersprachen oder durch verschiedene Programmierer), — eine virtuelle Maschine ist ein Systemteil bei der Aufteilung des Gesamtprogramms nach logischen Gesichtspunkten (Menge der Primitivfunktionen für eine bestimmte Aufgabe, Verwaltung eines oder mehrerer Datenbereiche oder Betriebsmittel.. .). Nichts zwingt den Systemplaner, eine virtuelle Maschine auch als einen Modul realisieren zu lassen oder in einem Modul jeweils eine vollständige virtuelle Maschine zusammenzufassen. Dies ist lediglich ein zweckmäßiger Ansatzpunkt für die Modularisierung, wie man sich auch allgemein in der EDV um eine möglichst weitgehende Parallelität der logischen und physikalischen Lösung bemühen sollte. Es kann jedoch durchaus Gründe geben, hiervon abzuweichen [PARN74], Während das Konzept der virtuellen Maschine bereits für den „Solo-Programmierer" eine wertvolle Planungshilfe darstellt, ist die Modularisierung für ihn vergleichsweise uninteressant. Fast alle Gründe für die (physikalische) Modularisierung eines Softwareprodukts stammen aus den Erfordernissen der kooperativen Programmierung:

172

2. Systementwicklung

— die Aufgabenteilung zwischen mehreren Programmierern, — die Verwendung oder Wiederverwendung bereits vorhandener Komponenten, — der Ersatz eines Programmteüs durch einen anderen bei der Wartung oder Übertragung auf eine andere Hardware. Allenfalls die Größe seines Programms kann bei zu kleinem Arbeitsspeicher der verfügbaren Maschine auch schon den Einzelprogrammierer zu einer Programmzerlegung nach physikalischen Gesichtspunkten zwingen. Selbst bei kleinen Maschinen (ca. 16k Byte Arbeitsspeicher) tritt dies aber nur in Sonderfällen (sehr große Datenbereiche) für Programme mit weniger als etwa 4000 Befehlen auf, und diese Größenordnung bedingt meist auch schon die Kooperation mehrerer Programmierer.

2.3.3 Konventionelle und hierarchische Planung Den Unterschied zwischen der konventionellen und einer hierarchischen Planungsmethode und den daraus jeweils hervorgehenden Modularisierungen eines Programms soll ein Beispiel zeigen. Es stammt von Parnas und wird hier in etwas abgewandelter Form dargestellt. Deshalb wird als ergänzende Lektüre die Originalarbeit [PARN72a] empfohlen. Die Aufgabe ist die Planung eines Programms zur Erzeugung eines KWIC-Index (,JCeyword in Context") zu einem sequentiell gespeicherten Datenbestand aus kurzen Texten, etwa Buchtiteln oder Zeitungsartikel-Überschriften. Ein derartiger KWIC-Index ist eine alphabetisch geordnete Liste aller Texte, die sich aus den Originaltexten durch wortweise zyklische Verschiebung erzeugen lassen. Abb. 2.3.3-1 zeigt Artikelüberschriften aus einer Nummer einer bekannten deutschen Tageszeitung, Abb. 2.3.3-2 den zugehörigen KWIC-Index. Ein- und Ausgabedatei sind folgendermaßen definiert: 2.3.3/1 input: file of char; output: file of record KWICline array [1 . . maxl] of char ; index : 1 . . maxs end;

2.3 Die Planung

( I ) DEUTSCHE LUXUSJACHT VOR IBIZA VERSCHOLLEN. ©

SOLDATEN ASSEN IHREN ZAHLMEISTER.

(3) MAKRELENHECHTE UBERFIELEN URLAUBER. @ DAS FOLTERHAUS VON DARMSTADT. (5) VW: HÖHERE PREISE. ©

PISTOLENMANN VERGING SICH AN STUDENTIN.

Abb. 2.3.3-1. Zeitungsartikel-Überschriften (Bild, 7. April 1975)

In der Eingabedatei input seien folgende Sonderzeichen vereinbart: Zwischenraum, Komma, Doppelpunkt = Worttrennzeichen, Punkt = Satztrennzeichen. maxs ist die maximal zulässige Satzzahl in dieser Datei. Die Ausgabedatei output besteht aus — Druckzeilen KWICline von maximal maxi Zeichen, die jeweils eine zyklische Verschiebung eines Textes enthalten (überlange Zeilen werden rechts abgeschnitten), sowie — dem zugehörigen Index, d. h. der laufenden Nummer des ursprünglichen Textes in der Datei input. Die konventionelle Planung geht meist von einem groben Flußdiagramm, dem Datenflußplan, aus, wie ihn Abb. 2.3.3-3 für die KWIC-Index-Erstellung zeigt. Er enthält vier Komponenten: (1)

(2)

(3) (4)

Einlesen überführt die Texte von der sequentiellen Datei input in eine verarbeitungsfähige Form text im Arbeitsspeicher. Hierbei wird gleichzeitig eine Index-Liste satzanfang angelegt, welche das erste Zeichen jedes Satzes zu finden ermöglicht. Zyklische Verschiebung generiert entweder die zyklisch verschobenen Texte oder, besser (um Arbeitsspeicher zu sparen), eine Liste wortanfang der Wortanfänge im text. Alphabetisierung sortiert diese Liste der Wortanfänge in eine alphabetische Sortierfolge sortwortanfang entsprechend dem text. Ausgabe generiert schließlich die Datei output aus dem text und der IndexListe satzanfang, gesteuert von dem sortierten Wortanfangs-Verzeichnis sortwortanfang.

174

2. Systementwicklung

AN STUDENTIN. PISTOLENMANN VERGING SICH

©

ASSEN IHREN ZAHLMEISTER. SOLDATEN

©

DARMSTADT. DAS FOLTERHAUS VON

©

DAS FOLTERHAUS VON DARMSTADT.

©

DEUTSCHE LUXUSJACHT VOR IBIZA VERSCHOLLEN.

©

FOLTERHAUS VON DARMSTADT. DAS

©

HÖHERE PREISE. VW:

©

IBIZA VERSCHOLLEN. DEUTSCHE LUXUSJACHT VOR

©

IHREN ZAHLMEISTER. SOLDATEN ASSEN

©

LUXUSJACHT VOR IBIZA VERSCHOLLEN. DEUTSCHE

©

MAKRELENHECHTE ÜBERFIELEN URLAUBER.

©

PISTOLENMANN VERGING SICH AN STUDENTIN.

©

PREISE. VW: HÖHERE

©

SICH AN STUDENTIN. PISTOLENMANN VERGING

©

SOLDATEN ASSEN IHREN ZAHLMEISTER.

©

STUDENTIN. PISTOLENMANN VERGING SICH AN

©

ÜBERFIELEN URLAUBER. MAKRELENHECHTE

©

URLAUBER. MAKRELENHECHTE ÜBERFIELEN

©

VERGING SICH AN STUDENTIN. PISTOLENMANN

©

VERSCHOLLEN. DEUTSCHE LUXUSJACHT VOR IBIZA

©

VON DARMSTADT. DAS FOLTERHAUS

©

VOR IBIZA VERSCHOLLEN. DEUTSCHE LUXUSJACHT

©

VW: HÖHERE PREISE.

©

ZAHLMEISTER. SOLDATEN ASSEN IHREN

©

Abb. 2.3.3-2. KWIC-Index der Zeitungsartikel-Uberschriften (Abb. 2.3.3-1)

Abb. 2.3.3-4 zeigt HIPOs für die Ablaufsteuerung (die „Hauptsteuerleiste") und die einzelnen Untermodule gemäß der „naiven" Modularisierung von Abb. 2.3.3-3 maxt ist die größte zulässige Länge des text\in der praktischen Realisierung wird sie durch das (physikalische) Fassungsvermögen des bereitgestellten Arbeitsspeichers bestimmt sein. Die (höchstens maxs) Einträge der Liste satzanfang sind die (zwischen 1 und maxt liegenden) Indizes der Satzanfänge. Für die Liste wortanfang sind maximal maxw Einträge erlaubt, auch diese Größe wird später entspre-

2.3 Die Planung

175

Abb. 2.3.3-3. Naive Modularisierung der Erstellung eines KWIC-Index

chend dem zur Verfügung stehenden (physikalischen) Arbeitsspeicher festgelegt werden. Jeder der Einträge definiert eine zyklische Verschiebung durch die Nummer satzindex des Satz-Verweises in satzanfang und den Verweis wortindex auf das erste Wort der betreffenden zyklischen Verschiebung. Legen Sie sinnvolle Werte für maxt, maxs und maxw fest, wenn insgesamt 20 000 Worte freier Arbeitsspeicher (ASP) zur Verfügung stehen, und jedes ASP-Wort 4 Zeichen oder eine ganze Zahl (Index) faßt. Nehmen Sie an, daß ein Satz im Durchschnitt 35 Zeichen lang ist und 4 Worte enthält.

2. Systementwicklung

176 KWIC-Index-Erstellung

input: C file of char;

1. Einlesen 2. Zyklische Verschiebung 3. Alphabetisierung 4. Ausgabe C

output: file of record KWICline: array [ 1 . m a x i ] of char; index: 1 . . maxs end;

©Ei

Einlesen

input: ; file of char;

©

^N

1. Lese Eingabe in Arbeitsspeicher

1

2. Baue Indexliste der Satzanfänge auf.

v ^ ^

text: array [ 1 . . m a x t ] of char; satzanfang array [ 1 . . m a x s ] of 1 . . m a x t ;

Zyklische Verschiebung

text: array [ 1 . . m a x t ] of char; satzanfang: array [ 1 . . maxs] of 1 . . m a x t ;

r

Erstelle Liste der W o r t a n f ä n g e für j e d e n Satz

1

wortanfang: array [ 1 . . m a x w ] ^ of record IS satzindex:l . . maxs; wortindex: 1 . . maxt end;

Abb. 2.3.3-4. HIPOs der naiven Modularisierung der KWIC-Index-Erstellung

177

2.3 Die Planung ( 3. )

Alphabetisierung

text:

sortwortanfang:

array [1 . maxt]

array [ 1 . . maxw]

of char; wortanfang:

array [ 1 . . max' of record



Sortiere Liste der Wortanfange alphabetisch entsprechend text

of record pv. satzindex:1 . . maxs; wortindex: 1 . . maxt

end;

satzindex:l . maxs; wortindex: 1 . maxt

end;

©

Ausgabe

text:

array [1 . . maxt] of char; satzanfang:

array [ 1 . . maxs]

of 1 . . maxt; sortwortanfang:

array [1 . . maxw]

of record satzindex :1 . . maxs; wortindex: 1 . . maxt

Gebe Sätze aus output: text entsprechend file of ^ Reihenfolge der Liste record sortwortanfang aus KWICline: wortindex gibt array [1 . . maxl] Anfang der zyklischen of char; Verschiebung, satzindex index: 1 . . maxs gibt Output.index end;

J?*

end; Abb. 2.3.3-4. (Fortsetzung) HIPOs der naiven Modularisierung der KWIC-Index-Erstellung

Die HIPOs zeigen recht deutlich die Nachteile einer aus dieser konventionellen Planung entwickelten Modularisierung: - Die Datenstrukturen text, satzanfang, wortanfang, sortwortanfang werden jeweils von einem Modul zum folgenden weitergereicht und definieren damit gleichzeitig auch die Schnittstellen zwischen ihnen. — Da in ihnen die vorwiegend aus physikalischen Erwägungen (zur Verfügung stehender Arbeitsspeicher) festgelegten Größen maxt, maxs und maxw eine wesentliche Rolle spielen, zwingt schon eine so triviale Wartungsaufgabe wie eine Anpassung an ein anderes Fassungsvermögen des Arbeitsspeichers zu einem Eingriff in allen Modulen.

178

2. Systementwicklung

— Noch größer ist der Aufwand bei einer Änderung der logischen Datenstrukturen oder ihrer physikalischen Realisierung, wie etwa einer direkten Speicherang der zyklischen Verschiebungen anstelle der Indexliste wortanfang oder einer Auslagerung des text auf einen Externspeicher bei nicht ausreichendem Arbeitsspeicher. - Planungsentscheidungen, welche die zeitliche Abfolge der Einzelfunktionen betreffen, sind schließlich überhaupt nicht mehr zu ändern, selbst wenn sie nur eine logische Teilfunktion betreffen. So wäre es grundsätzlich denkbar, die Alphabetisierung nicht als einmalige, zeitlich zusammenhängende Sortierung durchführen zu wollen, sondern einen Algorithmus einzusetzen, der erst bei jedem Aufruf entsprechend der alphabetischen Reihenfolge die sequentiell nächste zyklische Verschiebung ermittelt. Eine Modularisierung entsprechend Abb. 2.3.3-3 macht dies unmöglich — ein Flußdiagramm zementiert nun einmal die zeitliche Abfolge der Einzelaktionen als grundlegende Basisentscheidung. Die hierarchische Modularisierung, wie sie Abb. 2.3.3-5 zeigt, hat nicht diese Nachteile, da sie statt auf der zeitlichen auf der funktionslogischen Struktur der Komponentenabhängigkeiten beruht. Zwar haben die Einzelkomponenten noch die gleichen Namen und im Grunde auch die gleichen logischen Funktionen wie in der naiven Datenflußplan-Modularisierung, ihre Definition und ihre gegenseitigen Schnittstellen sind jedoch anders gewählt. Jeder Modul realisiert eine virtuelle Maschine und stellt den höheren einen Satz von Primitivoperationen als Prozeduraufrufe zur Verfügung. In Abschnitt 1.6 wurde bereits die Aufgabe einer virtuellen Maschine als „Veredelung" eines oder mehrerer Betriebsmittel definiert — die Primitivoperationen manipulieren höhere, problemnähere und bequemere Betriebsmittel als diejenigen, welche die virtuelle Maschine selbst vorfindet. Die Betriebsmittel, um die es in diesem Fall geht, sind Datenstrukturen: Zeichen, Sätze, Indizes, Listenzeilen. Dementsprechend sind die Primitivoperationen Zugriffsfunktionen zu diesen Datenstrukturen. Auf der Eingabeseite (linker Ast der Abb. 2.3.3-5) werden diese jeweils „von oben" angefordert, und es ist die Aufgabe des Moduls, der virtuellen Maschine, sie irgendwie aus den tieferen Datenstrukturen zu generieren und zur Verfügung zu stellen. Auf der rechten Seite werden Datenstrukturen „nach unten" zur Ausgabe abgegeben. Abb. 2.3.3-6 zeigt die von jeden Modul verwaltete virtuelle Datenstruktur: (1) Einlesen simuliert einen interntext : ein array von maximal maxs Sätzen. Jeder Satz besteht aus maximal satzig Worten aus maximal wortig Zeichen. (2) Zyklische Verschiebung simuliert eine (unsortierte) Liste zvtext als array von maximal zvlg (= maxs*satzlg, warum?) zyklischen Verschiebungen; jede besteht aus einem Satz (array) von maximal satzig Worten zu wortig

2.3 Die Planung

179

Abb. 2.3.3-5. Hieraichische Modularisierung der Erstellung eines KWIC-Index

Zeichen sowie dem index, d. h. der laufenden Nummer des Originalsatzes im interntext. (3) Alphabetisierung erzeugt und verwaltet (oder simuliert) ein array alphindex von Verweisen auf die zyklischen Verschiebungen in zvtext, das entsprechend ihrer alphabetischen Folge sortiert ist. Sie arbeitet eng mit dem Modul (2) zusammen, und man wird überlegen, ob man (2) und (3) nicht in einen Modul zusammenfassen soll.

180

2. Systementwicklung KWIC-Index-Erstellung

Abb. 2.3.3-6. Datenstruktur-Transformationen bei der Erstellung eines KWIC-Index

(4) Ausgabe verwaltet ein array zyklischer Verschiebungen, das gleich dem des Moduls Zyklische Verschiebung aufgebaut ist. Da es die sortierten Verschiebungen aufnehmen soll, wird es sortzvtext genannt. Seine Aufgabe ist es, diese Verschiebungen der Reihe nach in Ausgabezeilen für die file Output aufzubereiten und auf sie auszuschreiben.

2.3 Die Planung

181

Man beachte, daß im Unterschied zur naiven Modularisierung die bei der Definition der Datenstrukturen angegebenen Maximalwerte maxs, satzig, wortig, zvlg wesentlich unabhängiger von der physikalischen Realisierung bestimmt werden können — es handelt sich ja um virtuelle Datenstrukturen. Im Falle nicht ausreichender physikalischer Betriebsmittel ist es Aufgabe der jeweiligen virtuellen Maschine — eine entsprechende Fehlermeldung zu geben — oder (besser) einen Überlaufmechanismus vorzusehen. Abb. 2.3.3-7 zeigt HIPOs für die einzelnen Module. Auf Grund der Problemnahen virtuellen Datenstrukturen zvtext, sortzvtext und alphindex reduziert sich der Hauptmodul auf ein einfaches Füllen des array sortzvtext aus zvtext, gesteuert durch alphindex (vgl. Abb. 2.3.3-6).

KWIC-Index-Erstellung 1. Bestimme Index des nächsten auszugebenden Textes gemäss A Iphabetisierung 2. Hole zyklische Verschiebung mit diesem Index 3. Gebe diese an Ausgabe

©

Einlesen

Ubernehme Eingabedatei in ASP (oder Externspeicher mit wahlfreiem Zugriff) in verarbeitungsfähiger Form

Abb. 2.3.3-7. HIPOs der hierarchischen Modularisierung der KWIC-Index-Erstellung

2. Systementwicklung

182

(?)

Zyklische Verschiebung

interntext

©

Generiere (bei Einlesen oder später) eine Liste (array) sämtlicher zyklischen Verschiebungen des Interntext

Alphabetisierung

Ermittle (bei Einlesen oder später) die alphabetische Reihenfolge der zyklischen Verschiebungen

Ausgabe

sortzvtext

Gebe die zyklischen Verschiebungen auf Drucker aus

output

Abb. 2.3.3-7. (Fortsetzung) HIPOs der hierarchischen Modularisierung der KWIC-IndexErstellung

2.3 Die Planung

183

Legen Sie geeignete Primitivoperationen für jeden Modul (jede virtuelle Maschine) fest. Denken Sie daran, daß auch Abfragefunktionen für die Füllungsgrade der einzelnen Datenstrukturen zweckmässig sind : wenn die tatsächliche Realisierung nicht zu uneffektiv werden soll, muß man — entweder feststellen können, wieviel Sätze der interntext enthält, wie lang ein konkreter Satz aus zvtext ist u. ä., - oder durch Rückmeldungen jeweils auf das Eintreten entsprechender Endebedingungen aufmerksam gemacht werden. In Abschnitt 1.6.2 wurde schon daraufhingewiesen, daß zum Befehlsvorrat jeder virtuellen Maschine grundsätzlich eine Initialisierungsanweisung gehört. Diese kann hier jeweils als OPEN für die virtuelle Datenstruktur angesehen werden, welche die virtuelle Maschine simuliert. Je nach der konkreten Realisierung kann dieses OPEN eine mehr oder weniger komplexe Funktion sein. Im Falle des Moduls Zyklische Verschiebung kann es veranlassen — die tätsächliche Generierung aller zyklisch verschobenen Texte als Strings im Arbeitsspeicher oder auf einem Externspeicher, — die Herstellung einer Liste entsprechend wortanfang in der naiven Modularisierung (Abb. 2.3.34), welche später bei der Ausführung der tatsächlichen Anforderung einer zyklischen Verschiebung verwendet werden kann, — nichts, wenn die zyklische Verschiebung erst nach Anforderung aus dem interntext abgeleitet wird. Die bei der naiven Modularisierung unmögliche Ersetzung des Sortieralgorithmus für die Alphabetisierung durch eine Generierung des jeweils nächsten Verweises erst bei Anforderung ist hier problemlos und für die anderen Module unsichtbar. Im einen Fall wird bereits bei der Initialisierung die sortierte Wortanfangsliste (entsprechend Abb. 2.3.3-4 in der naiven Modularisierung) aufgebaut, im anderen Fall ist die Initialisierung eine reine Dummy-Funktion. Normalerweise wird man nicht alle zirkulären Verschiebungen in einen KWICIndex aufnehmen. Entsprechend der Bezeichnung ,JCeyword in Context" wird man sich auf diejenigen beschränken, welche mit einem Schlüsselwort (Keyword) beginnen (vgl. Abb. 2.3.3-8). Die Schlüsselworte müssen hierbei entweder auf einer Schlüsselwort-Datei als Liste vorgegeben oder bei der Texterfassung (z. B. durch Sonderzeichen) kenntlich gemacht werden. Eine Alternative ist, eine Liste der nicht zu berücksichtigenden Worte (der, die. das, und . . . ) zu verwenden. Erweitern Sie die Planung einer KWIC-Index-Erstellung um diese beiden Varianten.

2. Systementwicklung

184

DARMSTADT. DAS FOLTERHAUS VON DEUTSCHE LUXUSJACHT VOR IBIZA VERSCHOLLEN. FOLTERHAUS VON DARMSTADT. DAS IBIZA VERSCHOLLEN. DEUTSCHE LUXUSJACHT VOR LUXUSJACHT VOR IBIZA VERSCHOLLEN. DEUTSCHE MAKRELENHECHTE ÜBERFIELEN URLAUBER. PISTOLENMANN VERGING SICH AN STUDENTIN. PREISE. VW: HÖHERE SOLDATEN ASSEN IHREN ZAHLMEISTER. STUDENTIN. PISTOLENMANN VERGING SICH AN URLAUBER. MAKRELENHECHTE ÜBERFIELEN VW: HÖHERE PREISE. ZAHLMEISTER. SOLDATEN ASSEN IHREN

© © © © © © © © © © © © ©

Abb. 2.3.3-8. KWIC-Index der Zeitungsartikel-Überschriften mit ausgewählten Schlüsselworten

2.3.4 Schnittstellen und Beziehungen zwischen Modulen Der Zusammenhang zwischen den einzelnen Modulen ist gegeben durch die Menge der Annahmen, welche jeder Modul über das Verhalten jedes anderen macht. Diese Annahmen werden Schnittstellen genannt. Schnittstellen sind in der Regel Funktionsaufrufe, sonstige KontrollÜbergaben (z. B. Koroutinen-Aufruf [KNUT68], S. 190ff., TaskAktivierungen bei Auftreten bestimmter Ereignisse) oder gemeinsam benutzte Datenstrukturen. In der konventionellen Modularisierung besitzt jeder Modul Schnittstellen mit vielen, wenn nicht gar allen anderen, und es gibt kein Kriterium, von welchen anderen er auf diese Weise abhängig sein darf und von welchen nicht (Abb. 2.3.4-1). Sofern man mit dem System nicht eng vertraut ist, muß man dann annehmen, daß potentiell jeder Modul irgendwelche Annahmen über jeden anderen voraussetzt. Der Aufwand für das Verständnis eines komplexen Systems ist aber ungefähr proportional der Zahl der zu prüfenden und zu verstehenden Querbeziehungen zwischen den

2.3 Die Planung

185

Abb. 2.3.4-1. Konventionelle Modularisierung

Einzelkomponenten. In der konventionellen Modularisierung wächst er folglich etwa quadratisch mit der Komponentenzahl, so daß dann ein im Code-Umfang 10 mal größeres System als etwa 100 mal komplexer zu betrachten ist. Die Leitidee einer Modulaufteilung ist also eine Minimisierung der Beziehungen zwischen den Komponenten, und damit eine Minimisierung nahezu aller wesentlichen Kostenfaktoren für die Produktentwicklung und Wartung: — Weniger Schnittstellen bedeuten auch weniger Aufwand für ihre Planung, Festlegung und Dokumentation. - Der Nachweis der korrekten Zusammenarbeit der Komponenten erfordert weniger Verifikation und Test.

186

2. Systementwicklung

— Änderungen bei der Wartung beeinflussen in der Regel nur einen einzigen Modul. — Die Einarbeitung neuer Mitarbeiter in das System und seine Struktur wird beschleunigt. — Die Kommunikationsnotwendigkeit zwischen den Bearbeitern wird minimisiert (vgl. hierzu das Brooks'sche Gesetz, Abschnitt 2.6.1.1). Die Minimisierung der Wechselbeziehungen zwischen den Modulen impliziert zwei Forderungen: (1) (2)

Jeder Modul soll über die Funktion möglichst weniger anderer Module Annahmen machen müssen. Die Zahl der notwendigen Annahmen sollte möglichst gering sein.

Daß die hierarchische Modularisierung, in welcher jeder Modul ausschließlich mit den unmittelbar unter und über ihm liegenden kommunizieren darf, die erste Forderung erfüllt, ist evident (vgl. Abb. 1.6.1-1 und Abb. 2.3.3-5). Die zweite Minimisierungsforderung begründet die Wahl der virtuellen Maschinen als logischen Ausgangspunkt zur Modularisierung. Jede virtuelle Maschine soll die Verwaltung und Benutzung eines oder mehrerer Betriebsmittel in einer der logischen Problemstellung angepaßten Weise idealisieren und vereinfachen. Das Ziel ihrer Planung ist damit auch bereits die Unterdrückung möglichst aller Eigenschaften, Strukturen und Abhängigkeiten dieser Betriebsmittel, deren Kenntnis und Manipulation für die konkrete Aufgabe nicht unbedingt nötig ist. Damit reduziert eine optimal geplante virtuelle Maschine auch die Zahl der notwendigen Annahmen über sie auf ein Minimum. Durch die gezielte Abstimmung auf das gerade vorliegende Problem sind diese Annahmen außerdem noch plausibel, naheliegend und leicht zu merken. Virtuelle Maschinen erfüllen also die folgenden Grundforderungen für die Modularisierung eines Systems: — Schnittstellen zwischen Modulen sind „abstrakt". Sie werden nicht durch komplexe, änderungsanfällige Formate, sondern durch Funktionsnamen (Primitivoperationen) sowie Anzahl und jeweilige Attribute der Parameter definiert. — Die gegenseitigen Abhängigkeiten der Module implizieren nicht eine zeitliche Reihenfolge sondern eine (gegebenenfalls mehrstufige) Transformation von Betriebsmitteln von der logischen Problemlösung (Algorithmus) zur physikalischen Realisierung. — Jeder Modul kennt und realisiert (mindestens) eine Planungsentscheidung, die er vor allen anderen Modulen „versteckt".

2.3 Die Planung

187

Pamas [PARNOO, PARN71, PARN72 a] sieht in der letzten Forderung, die er als Information Hiding bezeichnet, das wichtigste Modularisierungsprinzip und eine Grundtechnik des „Software Engineering". Seine formale Sprache zur Modulspezifikation ([PARN72]) ist darauf abgestellt, keinerlei Information über die Realisierung eines Moduls zu offenbaren.

Abb. 2.3.4-2. Zugriff zu einer Datenstruktur (z. B. Tabelle) von verschiedenen Modulen aus

188

2. Systementwicklung

Beispiele für dieses „Verbergen von Informationen" als Modularisierungskriterium finden sich in jedem System: — Eine Datenstruktur (z. B. eine Tabelle) wird mit ihren Zugriffs- und Manipulationsprozeduren in einen Modul zusammengefaßt. Er verbirgt die konkrete Realisierung der Datenstruktur (Abb. 2.3.4-2). — Die Formate von Kontrollblöcken zur Kommunikation zwischen Modulen werden in einem Kontrollblockmodul „versteckt". — Zeichenverschliisselungen, Sortierreihenfolgen u. ä. werden in einem Modul versteckt. — Die zeitliche Reihenfolge der Verarbeitung wird, soweit möglich, in einem Modul verborgen, indem es ihm überlassen wird, welche Arbeiten zur Erfüllung seiner Primitivfunktionen er bereits bei der Initialisierung oder erst später bei der tatsächlichen Anforderung leistet. 2.3.5 Nicht-hierarchische Abhängigkeiten Selbstverständlich ist eine hierarchische nicht die einzig mögliche oder logisch sinnvolle Systemstruktur. Vor allem synchron oder asynchron zusammenarbeitende Module auf gleicher hierarchischer Ebene (Koroutinen [KNUT68], kooperierende Prozesse [DIJK68a, BRIN72]) sind — besonders in der Systemprogrammierung — häufig die natürlichste Realisierung einer EDV-Aufgabe. Dies gilt vor allem für Produzent-Konsument-Beziehungen, für die Abb. 2.3.5-1 ein einfaches Beispiel zeigt: Der Modul PRODUZENT liest von einem Magnetband Daten ein und bereitet sie auf, der Modul KONSUMENT verarbeitet sie weiter und druckt sie schließlich aus. Die Schnittstelle sind Warteschlangen, in welchen gefüllte Puffer vom Produzent zum Konsument und leere zurücklaufen.

Abb. 2.3.5-1. Produzent-Konsument-Beziehung

Nicht-hierarchische Modulabhängigkeiten verlangen jedoch große Sorgfalt vom Systemplaner. Da nicht festgelegt ist „wer für w e n " arbeitet, provozieren sie leicht Konfliktsituationen. Ein Beispiel hierfür bietet ein Realzeit-Betriebssystem eines EDV-Herstellers, der ungenannt bleiben soll. Das System sollte eine Anzahl gleichzeitig aktiver Pro-

2.3 Die Planung

189

zesse verwalten und koordinieren. Hierzu diente, wie üblich, eine Prozeßliste, in welcher für jede neue Task ein Prozeßkontrollblock angelegt und nach ihrem Abschluß wieder gelöscht wird. Das Fassungsvermögen der Prozeßliste, d. h. der für sie bereitgestellte Arbeitsspeicher, bestimmte (als Betriebsmittel des Moduls PROZESSVERWALTUNG) die maximale Anzahl der gleichzeitig zu bearbeitenden Prozesse. Das. System blieb bei hoher Belastung immer wieder überraschend stehen, wenn auf Grund von statistischen Schwankungen in den Anforderungen für Prozeßaktivierungen das zulässige Maximum überschritten wurde. Wie Abb. 2.3.5-2 zeigt, war der Grund eine nicht konsequente Hierarchie dreier Module: PROZESSVERWALTUNG verständigte FEHLERBEHANDLUNG vom Prozeßlistenüberlauf, diese verlangte von BEDIENUNG KONSOLSCHREIBMASCHINE das Ausschreiben einer Fehlermeldung, und dieser Modul wiederum gab der - ohnehin überlasteten - PROZESSVERWALTUNG einen Auftrag zur Aktivierung des Ausgabe-Prozesses.

Abb. 2.3.5-2. Blockierung in einem nicht-hierarchischen System

190

2. Systementwicklung

Diese Aufruffolge ist eine - in diesem Falle unerwünschte - Rekursion. Rekur-. sive Prozeduren sind zwar häufig ein sehr nützliches Mittel zur einfachen und durchsichtigen Programmstrukturierung (vgl. Abschnitt 1.3.5). Sie sollten aber auf funktionale Einheiten innerhalb eines Moduls beschränkt werden. Rekursive Abhängigkeiten zwischen Modulen sind immer gefährlich und grundsätzlich zu vermeiden [PARN74]. 2.3.6 Fehlerbehandlung in hierarchischen Systemen In der „Solo-Programmierung" ist die Fehlerbehandlung weitgehend unproblematisch. In der Regel wird eine einfache Beendigung des Programmlaufs mit einer kurzen Meldung (etwa einer Fehlernummer) und ggf. Ausgabe der vom System vorgesehenen Diagnostikhilfen (z. B. ASP-Dump) vorgesehen. Da Benutzer und Programmierer identisch sind, lohnt sich im allgemeinen kein größerer Aufwand: eine ad hoc-Korrektur des Fehlers ist meist wesentlich zeitsparender als die Planung und Realisierung einer anspruchsvollen Fehlerbehandlung. Sie kann die Menge des zu entwickelnden Codes mehr als verdoppeln: in einem Realzeitsystem-Steuerprogramm dienten 40% der 26 000 residenten und 70% der 52 000 nicht-residenten Befehle der Fehlerbehandlung [MART00]. Hinzu kommt ein sogar noch überproportionaler Testaufwand. Für jeden denkbaren Fehler und ihre häufigsten Kombinationen sollte mindestens ein Beispiel ausprobiert werden, denn eine unzuverlässige Fehlerbehandlung ist schlechter als gar keine. Sobald die Programmentwicklung jedoch kooperativ wird, ist ein Programmabbruch bei Auftreten eines Fehlers nicht zulässig. Es gibt jetzt nur noch zwei Alternativen für die Fehleraktion eines Programm(-Moduls): — entweder muß der Fehler korrigiert werden und gehört dann zu den Informationen, die der Modul „versteckt", — oder er muß an den Aufträggeber, d. h. den aufrufenden Modul oder (auf oberster Ebene) an den Benutzer weitergemeldet werden. Bei der Weitermeldung eines Fehlers muß dieser ebenso in eine für den Auftraggeber verständliche Form transformiert werden, wie der Modul es mit den Betriebsmitteln tut. Eine Funktion zum Ausgeben von Zeilen auf einen Schnelldrucker darf dem Benutzer nicht melden, daß eine Platteneinheit gestört ist nur weil der betreffende Modul diese zum Zwischenpuffern der Druckzeilen benutzt. Falls dem Leser dieses Beispiel weit hergeholt vorkommt: die Verfasser kennen PL/1 -Compiler, welche dem verblüfften Benutzer

2.3 Die Planung

191

melden, daß ein „OC4-ERROR" aufgetreten ist. Es versteht sich von selbst, daß aus den PL/1-Beschreibungen die Bedeutung dieser Meldung nirgendwo auch nur erraten werden kann — sie ist keine Fehlermeldung der virtuellen PL/1-Maschine, sondern der inzwischen durch Betriebssystem und Compiler bis zur völligen Unkenntlichkeit transformierten Hardware! Wenn das Programmsystem auf Fehler nicht mehr einfach durch Abbruch reagieren darf, dann gibt es auch keinen Grund mehr, sie als eine Ausnahme anzusehen und irgendwie anders zu behandeln, als jedes andere „normale" Ereignis im Ablauf eines Programms — etwa das Auftreten einer Endebedingung bei der Abarbeitung einer Datei. Der beste Planungsansatz für ein komplexes System ist, es als einen Mechanismus anzusehen, der auf alles reagiert, was ihm von seiner Umwelt zustößt, und für den sich normale und abnorme Fälle nur durch die Häufigkeit ihres Auftretens unterscheiden. Dies ist vor allem bei Realzeit- und Mehrbenutzer-Systemen wichtig, weil andernfalls nicht garantiert werden kann, daß ein Fehler lokal, d. h. auf den gerade bedienten Datenendplatz, die aktuelle Transaktion, einen einzigen Dateisatz beschränkt bleibt. Fehler müssen so früh wie möglich entdeckt werden, damit sie sich nicht über das Gesamtsystem ausbreiten können. Auch bei dieser Lokalisierung hilft das Konzept der Hierarchie virtueller Maschinen. Auf jeder Ebene — vom Elementarbefehl der Hardware bis zum Funktionsrepertoire der Benutzermaschine — betrifft ein Fehler immer nur eine Operation der (realen oder virtuellen) Maschine. Zwischen Beginn und Abschluß der Operation muß — der Fehler erkannt und — korrigiert (Wiederholung mit ggf. abgeänderten Parametern oder Bedingungen) oder — „nach oben" gemeldet werden. Die Forderung, daß auf jeder Stufe der Hierarchie eine Wiederholung einer fehlerhaft abgelaufenen Funktion möglich sein muß, hat eine sehr einschneidende und nicht leicht zu realisierende Folge für den Entwurf jeder Fehlerbehandlung: eine virtuelle Maschine darf bei Erkennen eines Fehlers nach seiner Weitermeldung „nach oben" keinerlei Erinnerung an den Aufruf behalten. Sie muß also entweder einen Fehler schon vor jeder Zustands- oder Datenänderung erkennen, oder ihre interne Fehlerbehandlung muß diese Änderungen rückgängig machen [PARN72].

192

2. Systementwicklung

Wenn man von Fehlern innerhalb einer virtuellen Maschine selbst, den leidigen Programmierfehlern, absieht, können Fehler in einem hierarchischen System eine virtuelle Maschine entweder von „unten" oder von „oben" erreichen. Aus tieferen Schichten stammt der Fehler, wenn entweder die Hardware oder eine aufgerufene virtuelle Maschine gestört ist, d. h. ein reales oder simuliertes Betriebsmittel nicht oder nur unzuverlässig funktioniert. Die virtuelle Maschine (der Modul) hat dann folgende Möglichkeiten der Fehlerbehandlung. — Reflexion: die virtuelle Maschine gibt die fehlerhaft ausgeführte Anweisung, evtl. modifiziert, noch einmal an die gleiche oder eine andere tiefere Komponente. Im einen Fall hofft sie, daß der Fehler inzwischen beseitigt ist — ein Magnetband- oder Platten-Lesefehler verschwindet oft bei wiederholten Versuchen, ein Mangel an freiem Speicher in einer dynamischen Speicherverwaltung kann inzwischen behoben sein. Im anderen Fall handelt es sich um eine Back Up-Einrichtung, wie sie in Systemen mit hohen Zuverlässigkeitsanforderungen üblich sind — eine parallel geführte zweite Datei identischen Inhalts oder ein Reserve-Terminal. — Degradation: die virtuelle Maschine bemüht sich, ihren Auftrag auf andere Weise wenigstens einigermaßen zu erfüllen. Statt einer gestörten Trommel benutzt sie eine langsamere Platte, oder sie liefert ihrem Auftraggeber nur einen Teil der gewünschten Leistungen, unter Weglassen derjenigen Informationen, die sie ihrerseits von den gestörten tieferen Komponenten auch nicht erhalten konnte. — Weitermeldung: die virtuelle Maschine übersetzt die Fehlermeldung, die sie erhalten hat, in die Begriffe ihrer eigenen logischen Ebene und gibt sie an den Auftraggeber weiter. Höhere Systemschichten (einschließlich dem menschlichen Benutzer selbst) machen Fehler, indem sie Primitivoperationen falsch, z. B. in unerlaubter Reihenfolge ansprechen oder sie mit fehlerhaften Parametern versorgen. Typische Benutzerfehler sind die folgenden: — Überschreitung erlaubter Wertebereiche. Module sollten grundsätzlich ihnen übergebene Eingabedaten prüfen („Schutzcode"). — Überlastung der vorhandenen Betriebsmittel, wie Überlauf des Arbeitsspeichers oder eines Externspeichers. — Anforderung Undefinierter Information, etwa vergessene oder unvollständige Initialisierung oder falsche Vorbesetzung einer Variablen. — Unerlaubter Zugriff. Der Auftraggeber ist zum Empfang der angeforderten Dienstleistung nicht privilegiert und wurde von einem Schutzmechanismus ([SCHN75], S. 164ff) entdeckt. Derartige Fehler werden grundsätzlich reflektiert und an den Auftraggeber zurückgemeldet. Gegebenenfalls werden sie auch noch an anderer Stelle, z. B. auf

193

2.3 Die Planung

der Bedienungskonsole des Systems, registriert. Dies gilt vor allem bei unerlaubten Zugriffen auf Informationen, die einem Datenschutz unterliegen und bei denen ein beabsichtigter Versuch des Durchbrechens der Sicherung vermutet werden kann. Auch wenn eine ernstliche Störung der auftraggebenden Komponente befürchtet werden muß, so daß diese die Fehlerrückmeldung ohnehin nicht mehr auswerten kann, sollte eine weitere Instanz benachrichtigt werden. Einen generellen, aber nicht in jeder Programmiersprache unmittelbar implementierbaren Fehlerrückmelde-Mechanismus zeigt Abb. 2.3.6-1. Jeder Modul

Aufrufender Modul Normale Funktionen A

B

Fehler-Funktionen (Traps) El

E2

X

Normalriicksprung

Aufruf von X „Trap", wenn Fehler E2 erkannt

Ausgang aus Fehlerbehandlung E2: -Reflexion, _Degradation (Fortsetzung A), -Weitermeldung nach oben.

Abb. 2.3.6-1. Prinzip der Fehlermeldung an höheren Modul

194

2. Systementwicklung

(jede virtuelle Maschine) muß neben den „normalen" Funktionen noch mindestens eine weitere zur Behandlung von Fehler- und Ausnahmebedingungen besitzen. Diese werden meist Traps („Fallen") oder Exits genannt. Sie müssen tieferen, aufgerufenen Modulen bekannt sein. Beim Erkennen eines Fehlers springt der tiefere Modul nicht — wie es normal wäre — unmittelbar hinter den Aufruf zurück, sondern übergibt die Kontrolle der zuständigen Fehlerbehandlungsfunktion. Diese ergreift die erforderlichen Maßnahmen und entscheidet dann, ob sie die erfolglos gebliebene tiefere Dienstleistungsfunktion nochmals aufrufen, die ursprünglich aufrufende Funktion ihres eigenen Moduls fortsetzen oder ihrerseits einen zuständigen Exit eines noch höheren Moduls anspringen soll. PL/1 stellt in den ON-CONDITIONs und der SIGNAL-Anweisung ein sprachliches Mittel zur Realisierung von Traps zur Verfügung. In üblichen Assemblern lassen sie sich leicht durch Software-Interrupts realisieren : für jeden Trap wird eine Speicherzelle vereinbart, die einen Sprungbefehl enthält und in deren Adreßteil jeweils die Anfangsadresse der zuständigen Behandlungsroutine eingetragen wird. Ein Sprung auf die Trap-Zelle führt dann immer auf diese Routine weiter. In den meisten anderen Sprachen ist die Formulierung eines bequemen TrapMechanismus nicht möglich. Er läßt sich aber immer durch einen Rückmeldecode im Prozeduraufruf simulieren: 2.3.6/1 procedure p (var retcode:integer,. . . {weitere Parameter}.. .); retcode ist, wie in PASCAL das Schlüsselwort var angibt, eine integer-Größe, die von der Prozedur p gesetzt wird und zur Übergabe eines ganzzahligen Rückmeldecodes an die aufrufende Funktion dient. Bewährt hat sich folgende Konvention: 2.3.6/2

retcode

= 0 : Normalrücksprung > 0 : Ausnahmebedingung (z. B. Dateiende) < 0 : Fehlerbedingung (z. B. Block nicht lesbar).

Die aufrufende Funktion wertet nun nach der Rückgabe der Kontrolle als erstes diesen Rückmeldecode retcode aus und verzweigt selbst zur zuständigen Ausnahme- oder Fehlerbehandlung im eigenen Modul.

2.4 Die Realisierung

195

2.4 Die Realisierung 2.4.1 Die Top Down-Programmierung Bei der Realisierung zeigt sich die Qualität der Planung. Hat sie zu einer logisch durchsichtigen und vollständigen hierarchischen Modularisierung geführt, so können die Module oder geeignete Untermengen ihrer funktionalen Einheiten einzeln an Programmierer zur Bearbeitung vergeben werden. Für jeden Modul wurde in der Planungsphase eine exakte Aufgabenstellung im Sinne von Abschnitt 1.1 mit einer Spezifikation der zu realisierenden oberen Schnittstelle und den dabei zu verwendenden tieferen erarbeitet. Der Programmierer befindet sich damit in der Lage eines Solo-Programmierers — er kann seine Programmentwicklung weitgehend ohne Rücksicht auf die übrigen Bearbeiter nach den in Abschnitt 1 besprochenen Methoden durchführen. Die kooperative Programmierung bringt dann nur wenige neue Probleme für die Realisierung. Sie beziehen sich vorwiegend auf die Erfordernisse einer reibungslosen Projektführung und -Überwachung: (1) In welcher Reihenfolge sollen die Module bzw. Modulteile zur Bearbeitung vergeben werden? (2) Wie erreicht man eine reibungslose Integration der einzelnen, der Reihe nach entstehenden Komponenten zum Gesamtsystem? (3) Welche Regeln für Programmaufbau und Test sollten im Interesse der bei einer kooperativen Programmentwicklung gesteigerten Ansprüche an Programmqualität und Wartbarkeit beachtet werden? (4) Wie überwacht die Projektleitung die Produktqualität? Die beiden letzten Punkte sollen einstweilen zurückgestellt werden und zuerst die Fragen der zeitlichen Reihenfolge für die Bearbeitung von Einzelkomponenten und deren Integration besprochen werden. Wie bei der Planung gibt es auch hier grundsätzlich zwei Möglichkeiten: — Bei der Top Down-Programmierung werden die hierarchisch obersten Module (Auftragsbeschreibung in JCL [FLOR71, BARR72], Hauptsteuerleiste o. ä.) zuerst realisiert; danach, iterativ, immer tiefere, bis nach Fertigstellung der hardwarenächsten Komponenten das System vollständig implementiert ist. — Die Bottom Up-Programmierung verfährt umgekehrt. Man beginnt mit den hierarchisch tiefsten Komponenten und arbeitet sich schrittweise nach „oben" vor, bis schließlich der höchste Modul programmiert werden kann und im Integrationstest das einwandfreie Zusammenwirken der einzelnen Systemteile unter seiner Steuerung erprobt wird. Bis vor einigen Jahren war die Bottom Up-Programmierung allgemein üblich und wurde teilweise sogar verbindlich vorgeschrieben.

196

2. Systementwicklung

Die Verfasser haben die Bottom Up-Methode nie praktiziert und sind deshalb auch schlechte „advocati diaboli". Sie vermuten ihren Grund in einer nicht vorhandenen oder mangelhaften Spezifikation der Aufgabenstellung vor Beginn der Realisierungsarbeiten. Die Basismaschine ist fast immer bekannt: die konkrete Hardware, das Betriebssystem, für welche das Programmprodukt gedacht ist, und die Programmiersprache, auf welche man sich schnell einigen kann. „Bottom Up" kann man also meistens „sofort loslegen". Was das Programm später genau tut, wird sich im Lauf der Entwicklung schon zeigen, zumal der Auftraggeber „ja doch noch nicht genau weiß, was er eigentlich will". Die moderne Softwaretechnologie fordert hingegen die Top Down-Programmierung als verbindliche Vorgehensweise. Das wesentliche Argument hierfür ist, daß nur dieses Verfahren eine kontinuierliche, zuverlässige Führung und Überwachung des Projektfortschritts durch die Projektleitung ermöglicht. Hierauf wird in Abschnitt 2.6.2 noch eingegangen werden. Intuitiv ist dies plausibel. Stellt sich bei einer Bottom Up-Entwicklung während des Integrationstests heraus, daß die bis dahin entwickelten (angeblich vollständig fertigen!) Systemteile etwa wegen eines Mißverständnisses über die Schnittstellen oder wegen eines grundlegenden Planungsfehlers in einer oberen Systemebene nicht zusammenpassen, so muß ein großer Teil der Komponenten neu geplant und realisiert werden. Es ergeben sich dann — fast am Ende der geplanten Entwicklungszeit — Nacharbeiten und Terminverschiebungen, welche die Grössenordnung der ursprünglichen Aufwandschätzungen erreichen können. Eine Top Down-Realisierung hingegen integriert jede neu entwickelte, tiefere Komponente sofort mit den bereits existierenden höheren. Stellen sich hierbei ernste Mißverständnisse oder Fehlplanungen heraus, sind der Verlust an bereits erbrachter Programmierleistung und damit die Folgen für die Termin- und Kostenplanung viel harmloser. Die Forderung nach einer reibungslosen Integration der Einzelkomponenten wird also von der Top Down-Realisierung weit besser erfüllt: sie eliminiert gegenüber der herkömmlichen Programmentwicklung den Integrationstest als eigenständige Projektphase (vgl. Abb. 2.1.2-2). Sowohl die Bottom Up- als auch die Top Down-Realisierung stehen vor der Schwierigkeit, daß bei der Entwicklung einer Komponente noch nicht alle anderen realisiert sind, die zu ihrem Betrieb gebraucht werden. Die Bottom Up-Programmierung erfordert für den Test einer Programmkomponente jeweils die Anfertigung eines Testrahmens (in der angelsächsischen Literatur meist test bed genannt): ein Programm, das sämtliche Funktionen der Komponente durchprobiert und auftretende Fehler meldet. Die Herstellung eines Testrahmens erfor-

2.4 Die Realisierung

197

dert einen mit der eigentlichen Komponente vergleichbaren Aufwand, sofern nicht ein Generator- oder Baukasten-System hierfür zur Verfügung steht. Umgekehrt kann jede Komponente bei der Top Down-Programmierung in der Umgebung der bereits entwickelten höheren Module getestet werden. Dafür existieren aber die tieferen noch nicht, auf deren Funktionen sie sich abstützt. Wie Abb. 2.4.1-1 skizziert, müssen diese jeweils durch Platzhalter (Dummies) simuliert werden.

Abb. 2.4.1-1. Top Down-Programmierung mit sukzessivem Ersatz der Platzhalter durch die endgültigen Komponenten

198

2. Systementwicklung

Die Erfahrung hat gezeigt, daß der durchschnittliche Aufwand für die Programmierung von Platzhaltern geringer ist als der für die Herstellung von Testrahmen. Für die Programmierung des Text-Speicherungs- und Wiedergewinnungs-Systems (Abschn. 1.6.4) könnte man z. B. für den Test des Hauptprogramms die Primitivoperationen der obersten Ebene (Katalogisiere, Schreibe, Lese,. . .) durch einfache Unterprogramme simulieren, welche alle zu speichernden Texte auf den Drucker ausschreiben und alle zu lesenden Texte vom Kartenleser einlesen. Der Fortschritt der Realisierung des Gesamtsystems besteht in einem schrittweisen Ersetzen der Platzhalter durch die endgültigen Versionen der Komponenten, wobei zugleich ggf. wieder tiefere Platzhalter definiert werden müssen. In Abschnitt 2.6.3.2 wird gezeigt, wie dieser iterative Ersetzungsprozeß weitgehend automatisch durchgeführt und überwacht werden kann. Obgleich die kontinuierliche Systemintegration und die dadurch erzielte zuverlässigere Projektfuhrung und -Überwachung das Hauptargument für eine Top Down-Realisierung ist, sollte ein weiterer Vorteil nicht unerwähnt bleiben. Da bei ihr mit der Benutzerschnittstelle begonnen wird, entsteht meist bereits sehr früh ein Simulationsmodell, das aus der Sicht des Anwenders bereits alle Funktionen des endgültigen Produkts ausführt. Die nicht realisierten Teile sind für ihn unsichtbar, auch wenn sie vielleicht den weit überwiegenden Realisierungsaufwand erfordern: die Datenhaltung erfolgt etwa noch ausschließlich im Arbeitsspeicher und nicht auf Externspeichern, statt der Datenübertragung ist nur lokale Ein-/Ausgabe möglich, es sind noch keine Fehlerbehandlungen realisiert und dergleichen mehr. Für die Ausbildung der späteren Benutzer und eine Überprüfung der Einsatzfähigkeit ist dieses Simulationsmodell oft von Nutzen. Gerade wenn der Auftraggeber noch nicht genau weiß, „was er will", bewährt sich die Top Down-Realisierung: sie liefert frühzeitig ein leicht zu änderndes und zu verbesserndes Modellsystem, mit dem der Benutzer „spielen", Alternativen ausprobieren und sich über Wert oder Unwert einzelner Systemfunktionen klar werden kann. Selbst wenn ein derartiger Probebetrieb zu einem völligen Verwerfen der gewählten Lösung einschließlich der bisherigen Spezifikation führt — der Verlust ist ungleich geringer als der durch ein unbrauchbares, fertig entwickeltes System. Die Simulation endgültiger Komponenten durch Platzhalter bewährt sich auch bei Test und Erprobung zeit- und speicherkritischer Programme (z. B. Realzeit-

2.4 Die Realisierung

199

Systemen). Hierzu muß man lediglich den geschätzten späteren Arbeitsspeicherbedarf durch entsprechend große Leerspeicher-Bereiche und die Laufzeit durch genügend oft durchlaufene Zählschleifen oder einen Warte- und Weckaufruf an das Betriebssystem nachspielen. 2.4.2 Kodierregeln Zwar braucht der Bearbeiter einer abgeschlossenen Komponente nicht allzuviel Rücksicht auf parallel zu ihm arbeitende, andere Programmierer zu nehmen — das heißt aber nicht, daß er sich nicht einer strengeren Programmierdisziplin unterwerfen muß als der Solo-Programmierer. Dies vor allem im Interesse einer hohen Flexibilität des Produkts. Unter Flexibilität wird hier die Änderbarkeit des Programms im weitesten Sinne verstanden: Änderbarkeit zur Fehlerkorrektur, zur Verallgemeinerung der Lösung sowie zur Anpassung an geänderte Aufgabenstellungen (Benutzermaschine) oder geänderte Hardware oder Grundsoftware (Basismaschine). Für eine Änderung der Aufgabenstellung, d. h. der Spezifikation oder in der Planung festgelegter Primitivfunktionen, kann es verschiedene Gründe geben: — Abwandlung — die Funktion des Programmes wird ganz oder teilweise neu definiert. — Modifikation — die Werte von fest eingebauten Konstanten (Parametern) des Programms haben sich geändert. — Verpflanzung — das Programm soll mit anderen Betriebsmitteln (Hardware, Betriebssystem, Compiler) arbeiten als ursprünglich vorgesehen war. Der Aufwand für derartige Änderungen in einem fertigen Programm kann sehr hoch sein, wenn ihre Möglichkeit nicht schon bei der ersten Erstellung berücksichtigt wird - in vielen Softwareentwicklungsstellen werden zwischen 25 und 75% des Arbeitsaufwands für Wartung und Änderung existierender Programme verbraucht. Dies liegt auch an der geringen Produktivität der Programmierer bei Eingriffen in komplexe, schlecht strukturierte und wartungsunfreundliche Programme. Untersuchungen ergaben, daß hierbei zuweilen nur Leistungen von 10 bis 20 Befehlen pro Bearbeiter jähr erreicht wurden! Der Erhöhung der Flexibilität dient neben der Programmstrukturierung die Einhaltung folgender Regeln. — Parametrisierung: In der Aufgabenstellung sind viele Datenwerte scheinbar konstant, wie etwa Lohnsteuersätze, Adresse der Hauptverwaltung eines Unternehmens, Zahl der zu verarbeitenden Datenelemente, Länge eines Eingabe- oder Ausgabesatzes, Modell und Fassungsvermögen

200

2. Systementwicklung

eines Plattenspeichers. Werden solche Konstanten im Programm durch Zahlen bzw. Zeichenfolgen dargestellt, so muß bei einer etwaigen Änderung jeder Befehl, in dem die „Konstante" benutzt wird, gefunden und geändert werden. Durch Parametrisierung wird die Konstante im Programm manifest. Es wird ihr ein Name (z. B. ,PI') gegeben, dem der entsprechende Wert (z. B. ,3.14') einmal zugewiesen wird. Ändert sich dieser Wert (weil man etwa eine größere Rechengenauigkeit braucht), so muß nur die eine Anweisung, die den Wert des Parameters festlegt, neu geschrieben werden. — Reserven und Notausgänge: Der Wartungsprogrammierer muß an jeder Stelle des Programms Ergänzungen einfügen können. Dazu braucht er mindestens — ein Bit in einem Datensatz oder einer Tabelle (als Anzeige, daß irgendwo weitere, ergänzende Information steht), — eine weitere Anweisung im Programmtext (als Aufruf eines entsprechenden Unterprogramms). Die (physikalische) Realisierung eines Programms oder einer Datenstruktur darf deshalb nie so ausgelegt werden, daß in einem Datensegment nicht noch mindestens ein Bit und in einem Programmsegment mindestens eine Anweisung frei ist. — Einfache sprachliche Mittel: Stehen zur Formulierung einer Aktion verschiedene sprachliche Mittel zur Verfügung, so sollte das einfachste gewählt und im Rahmen des Projekts möglichst auch als einziges erlaubtes normiert werden. Eine übliche Ausdrucksweise versteht jeder Wartungsprogrammierer und unterstützt jeder Compiler. Außerdem ist für ein alltägliches sprachliches Mittel ein Compiler- oder Systemfehler unwahrscheinlich. Auf keinen Fall sollte man über den jeweiligen Sprachstandard (bei COBOL z. B. ANS68) hinausgehen. Andernfalls ist das Programm mit Sicherheit an den betreffenden Compiler gebunden und nicht verpflanzbar.

2.4.3 Testmethodik Testen ist teuer. Es verbraucht viel Maschinenzeit und beschäftigt den Bearbeiter unproduktiv: im Gegensatz zur logischen Verifikation des Programms (vgl. 1.2.4) erbringt es keine positiven Resultate, wie etwa ein erhöhtes Vertrauen in die Korrektheit des Programms, sondern negative, nämlich die Aufdeckung immer neuer Fehler oder - andernfalls - die Frustration völlig ergebnisloser Betätigung. Es sollte deshalb auf ein Minimum beschränkt werden, und dazu gibt es zwei Methoden.

2.4 Die Realisierung

201

— Es darf nicht statistisch, sondern es muß gezielt nach Fehlern gesucht werden, deren Auftreten relativ wahrscheinlich ist. - Ein Programm soll sich „selbst testen", indem es an allen wichtigen Stellen über Schutzcode [MAIB73] prüft, ob Zustandsvariablen und Daten noch sinnvolle Werte haben, und indem es am Ende den korrekten Abschluß aller seiner Aufgaben, überprüft. Zustandsvariable sind diejenigen Daten, welche die Kommunikation zwischen den Strukturblöcken übernehmen und den Kontrollfluß durch die einzelnen Programmzweige über Verzweigungen und Fallunterscheidungen steuern. Sie haben zentrale Bedeutung für den Programmtest. Alle Zustandsparameter sind zu Kontrollblöcken zusammenzufassen, wobei jeweils ein Kontrollblock alle nach einem logischen Kriterium zusammengehörenden Zustandsvariablen umfaßt (z. B. den Zustand einer Datei, eines Terminals, eines Prozesses, eines Externgeräts). Eine derartige Zusammenfassung aller Zustandsparameter in Kontrollblöcke ist in der Systemprogrammierung seit langem üblich. Die Kontrollblöcke des OS/360 [IBMOOa] können als Muster für diese Technik dienen. Für jeden Kontrollblock sollte für den Test ein Unterprogramm oder ein Makro (je nach der verwendeten Programmiersprache) existieren, welches den aktuellen Inhalt aller Zustandsvariablen des Blocks protokolliert und die Einhaltung der zulässigen Wertebereiche prüft. An allen Steuerflußverzweigungen sollten während des Tests Aufrufe dieses Unterprogramms oder dieses Makros eingefügt werden. Diese Aufrufe sollten auch in der Produktionsversion des Programms belassen werden. Besitzt die verwendete Sprache einen Makrogenerator (BAL, PL/1), so kann ihre Generierung zur Übersetzungszeit einund ausgeschaltet werden. Sprachen wie COBOL und FORTRAN bieten diese Möglichkeit nicht. Sofern der Laufzeit- und Arbeitsspeicher-Mehraufwand in Kauf genommen werden können, sollte man dann prüfen, ob man hier die Unterprogramme und ihre Anspränge ebenfalls in der Produktionsversion des Programms beläßt und lediglich die tätsächliche Ausgabe des Testausdrucks durch eine Boole'sche Laufzeit-Variable TEST abschaltbar macht. Für jede Zustandsvariable sollten Grenzwerte festgelegt und möglichst auch im Programmtext als Kommentare dokumentiert werden. Dies sind die Grenzen der erlaubten Wertebereiche sowie diejenigen Werte, bei denen sich der Kontrollfluß ändert (also z. B. Null bei einer Fallunterscheidung auf negativen oder

2. Systementwicklung

202

positiven Wert der Variablen). Diese Grenzwerte der Zustandsvariablen führen dann den Programmierer beim Austesten seines Programms. Grenzwerte wie — der kleinste und größte erlaubte Wert, — der um eins kleinere oder größere Wert, — der leere Text, — die leere Eingabekarte, — die bis zur letzten Spalte vollgeschriebene Karte sind immer die wahrscheinlichsten Fehlerquellen. Testen von Zwischenwerten hat kaum Sinn - sollte bei einer zwischen 0 und 1 000 000 liegenden Variablen ausgerechnet bei Werten zwischen 1000 und 2000 ein Fehler auftreten, so ist die Wahrscheinlichkeit, ihn mit 10 statistisch ausgewählten Testfällen zu finden, etwa 1 % und lohnt nicht die Testzeit. Deshalb sind auch Testgeneratoren, die statistisch Testwerte generieren, ziemlich wertlos und konsumieren lediglich Rechenzeit und Personalkapazität. Für einen vollständigen Test muß gefordert werden, daß alle Grenzwerte und alle unmittelbar an Grenzwerte anschließenden Werte der Zustandsvariablen mindestens einmal ausprobiert wurden. Dies gewährleistet, daß jeder Zweig des Programms einmal durchlaufen wird. Die Vereinfachung des Tests von Grenzwerten und den bei ihrer Überschreitung vorgesehenen Überlauf- und Fehlerbehandlungen ist ein weiteres Motiv für die im vorigen Abschnitt zur leichten Änderung empfohlene Parametrisierung von „Konstanten". Es ist nahezu unmöglich, die Fehlerbehandlung für den Überlauf einer auf 1 Million Datensätze ausgelegten Datei zu testen. Ist die Größe der Datei aber ein manifester Parameter, so erfordert es einen Aufwand von Minuten, ihn für den Test auf 1 oder 2 Sätze umzusetzen, womit das Testproblem verschwindet. Eine der besten Stellen für die Einfügung von Schutzcode ist immer die Abschlußroutine („Housekeeping") eines Programms, die bei seinem Ende durchlaufen wird. Diese sollte grundsätzlich prüfen, ob alle Aussagen über den Programmzustand an dieser Stelle auch tatsächlich zutreffen: — Stehen alle Zähler auf dem Endwert (Null, Maximalwert o. ä.)? — Wurden alle dynamisch zugeteilten Speicher wieder freigegeben? — Sind alle Sätze verarbeitet?

2.4 Die Realisierung

203

— Wurde der letzte Satz vollständig verarbeitet? — Sind alle Dateien abgeschlossen? — Stehen noch unbeantwortete Anfragen eines Terminals aus? — Gingen „unterwegs" Betriebsmittel verloren, d. h. ist etwa ein Bestand an freien Puffern bei Programmabschluß kleiner als am Anfang? Diese Prüfungen sind wichtig, weil sie Fehler aufdecken, die sonst unbeachtet bleiben. Schließlich spart „defensives Programmieren" viele Fehlermöglichkeiten und damit viel Testzeit — wie der erfahrene Autofahrer kennt auch der erfahrene Programmierer risikoreiche Situationen und vermeidet sie oder erhöht bei ihrem Eintreten seine Aufmerksamkeit. Häufige Gefahrenquellen sind die folgenden. — Verzählen, besonders um +1 oder - 1 , tritt immer wieder bei Zählschleifen zur Abarbeitung von Textzeilen, Tabellen o. ä. auf. Ein Ende-Kennzeichen ermöglicht einen sichereren Schleifenabschluß: statt bei der Verarbeitung einer Lochkarte die 80 Spalten zu zählen, ist es besser, die Karte in einen 81 Zeichen langen Puffer einzulesen, dessen letzte Stelle auf ein unzulässiges Zeichen (z. B. X'FF') initialisiert ist. Dies ermöglicht eine Schleifenkonstruktion wie while Zeichen [i]i=Endezeichen

do . . . .

— Vergessene Initialisierung von Variablen sollte bei der Verifikation des Programms (vgl. Abschn. 1.2.4) grundsätzlich überprüft werden. Sie ist eine Fehlerquelle, die oft beim Test nicht gefunden wird, weil zufällig die Arbeitsspeicherinhalte immer richtig waren oder weil ein Compiler verwendet wurde, der automatisch alle Variablen auf Nullwerte initialisiert. Auch dies ist ein Pseudokomfort, den manche Hersteller bieten und der durch dadurch verborgene Programmierfehler die Benutzer sicher bereits Millionen gekostet hat. Dagegen ist ein Testcompiler, welcher die korrekte Initialisierung prüft und andernfalls den Fehler meldet eine sehr wertvolle Hilfe (WATFOR [SIEG71 ], PL/C [MORG71, WEIN73]). — Überschreitung von array-Grenzen ist ein Sonderfall der aus dem zulässigen Wertebereich herausfallenden Steuervariablen. Zur Abhilfe dient Schutzcode, den manche Compiler, wie etwa die oben bereits erwähnten Testcompiler, sowie PL/1 bei Einschalten der ON SUBSCRIPTRANGE-Bedingung automatisch erzeugen. — Nicht abbrechende Schleifen sind einer der häufigsten Programmfehler, vor allem bei der Wiederholung mit Unterbrechung (BREAK, vgl. Abb. 1.3.3.5-4).

204

2. Systementwicklung

Das einfachste Mittel dagegen ist, solche Schleifen prinzipiell als Zählschleifen mit einem hohen Wert für die maximale Wiederholungszahl zu formulieren: for i."= 1 toi0 000 do . . . . Nach 10 000 Wiederholungen wird die Schleife dann am Ende verlassen, was in diesem Fall natürlich nun ein Fehlerausgang ist und zu einer Meldung führen muß. - Fehler in logischen Ausdrücken entstehen oft dadurch, daß die logischen Operatoren A (AND) und V(OR) fast das Gegenteil der umgangssprachlichen Bedeutung von „und" und „oder" haben. Dies wird vor allem dann verwirrend, wenn noch viele —i (NOT) und Klammerungen hinzukommen. Deshalb sollten komplexe logische Ausdrücke vermieden werden (vgl. de Morgan's Regeln [KERN74, S. 22ff]).

2.4.4 Überprüfung und Verbesserung von Programmen Wie in jedem Bereich der Technik sollte auch die Qualitätsüberwachung eines Programms nicht nur feststellen, ob das Produkt überhaupt funktioniert. Die folgende Liste stellt einige der häufigsten Verstöße gegen die Grundsätze einer sauberen Programmstrukturierung zusammen. Sie kann zur ersten, oberflächlichen Qualitätsbeurteilung eines Programmproduktes dienen: — Gehen häufig logische Einheiten des Programms (Makro-Deklarationen, Unterprogramme, Schleifen, DO-END- oder BEGIN-END-Blöcke in PL/1 bzw. Paragraphen oder Kapitel in COBOL) über mehr als eine oder gar über viele Seiten der Liste? — Ist die logische Bedeutung jedes Sprungbefehls (GOTO) sofort aus dem Programmtext zu ersehen? Kann der Programmierer sie wenigstens sofort erklären? — Wird der (ausführbare) Programmtext während der Laufzeit verändert (z. B. ALTER-Anweisung in COBOL, Umsetzen von NOPs in Sprungbefehle im Assembler)? — Gibt es viele Variablen, deren Bedeutung nicht aus dem Namen ersichtlich ist (z. B. VAR1, VAR15, XXX, MP13X5A)? — Gibt es im besonderen Schalter, deren logische Bedeutung nicht aus dem Namen hervorgeht (z. B. SWITCH 13 statt ENDFILE oder EOF)? Verwendet der Programmierer in IF-Anweisungen nicht ohne weiteres verständliche Boolesche Ausdrücke statt logische Variable („Bedingungsnamen" in COBOL): if feld3 = 2 .. . statt if verheiratet. . . ? — Gibt es in dem Programm numerische Literale? Wieviele Anweisungen müssen geändert werden, wenn eine „Konstante" (z. B. Zahl von Verkaufsbezirken, maximale Länge einer Tabelle, Länge eines Eingabesatzes) einen anderen Wert erhält?

2.5 Die Dokumentation

205

— Übernehmen Programm-Module ihre Eingangsparameter ohne Prüfung auf Einhaltung des erlaubten Wertebereichs? — Überläßt das Programm die „Aufräumarbeiten" am Ende seines Laufs (Schliessen von Dateien, Freigabe von Speicher u. ä.) dem Betriebssystem, oder führt es sie selbst durch und prüft dabei die Korrektheit des erreichten Endzustands? — Braucht der Programmierer mehr als eine Minute, um anzugeben, ob eine bestimmte (beliebige) Variable an einer beliebig herausgegriffenen Stelle des Programms einen bestimmten Wert haben kann (z. B. kleiner als 0 sein kann)? Bei der Überprüfung und Verbesserung des Programms kann auch ggf. eine Effektivitätsoptimierung erfolgen. Grundsätzlich lohnt diese nur bei häufig durchlaufenen Befehlsfolgen: etwa 3 % des Gesamtcodes sind erfahrungsgemäß für die Optimierung überhaupt interessant. Eine entsprechend Abschnitt 1.3 konsequent durchgeführte Steuerfluß-Strukturierung ermöglicht die Lokalisierung dieser Stellen: es sind diejenigen innersten Wiederholungsschleifen, für die auf Grund der Programmlogik jeweils eine Vielzahl von Durchläufen zu erwarten ist. Nur an diesen Stellen ist es vielleicht sinnvoll, durch „Tricks" und Einführung unstrukturierter Steuerflußkonstruktionen eine lokale Optimierung durchzuführen. Hierbei ist darauf zu achten, daß — die „unsauberen" Stellen ausgiebig kommentiert werden, — die ursprüngliche Version als Dokumentationsunterlage beibehalten wird, — zur Programmwartung und Weiterentwicklung grundsätzlich die nicht optimierte Version als Grundlage dient.

2.5 Die Dokumentation 2.5.1 Sinn und Entstehen der Dokumentation Die häufigsten Klagen über Softwareprodukte beziehen sich auf schlechte und nicht ausreichende Dokumentation. Dies ist weiter nicht verwunderlich; die Dokumentation eines Programmes ist das einzige, was dem Menschen, d. h. dem Anwender und Warter, sichtbar wird (der Quelltext des Programms ist, was häufig vergessen wird, auch und vor allem Dokumentation - die Maschine benutzt die Objektmodule!). Die Folge ist, daß immer mehr Dokumentation gefordert und produziert wird. Das ist falsch. In der Regel gibt es zuviel Dokumentation, nicht zu wenig. Über folgende Aspekte des Programmprodukts muß es verständliche und verläßliche Unterlagen geben: (1) Zweck des Programms, (2) Bedienung des Programms durch Anwender und Systembediener (Operator),

206

2. Systementwicklung

(3) benötigte Hardwarekonfiguration, Betriebssystem, Programmiersprache und sonstige Angaben über die ,Systemumgebung', (4) Idee und Struktur der logischen Systemlösung, (5) Modulstruktur, Funktion und Schnittstellen der Module, (6) nicht aus dem Programmtext zu ersehende Informationen über die Realisierung der Einzelmodule (Name des Autors, Versionsnummern, Freigabedaten, benutzte Algorithmen und Verfahren, Voraussetzungen und Einschränkungen für die Anwendbarkeit), (7) nicht sofort verständliche Details der technischen Lösung, (8) durchgeführte Tests, Testdaten und Testprotokolle. Wurde die Entwicklung des Programmsystems nach den hier beschriebenen Methoden durchgeführt, existieren die meisten Dokumente bereits in vollständiger Form, bevor überhaupt mit der Realisierung des Programms begonnen wurde: (1), (2), (3) beschreibt die Spezifikation (Abschnitt 2.2), (4), (5) die Planung (Abschnitt 2.3). Diese Schriften sollten nach Abschluß der Realisierung auf keinen Fall neu geschrieben, sondern lediglich überprüft, editiert, ergänzt und korrigiert werden. (6) und (7) sind ergänzende Angaben zum Programmtext, der verbindlichen (und in der Regel einzigen bei der Wartung gepflegten) Dokumentation der Realisierung. Es besteht kein Grund, diese Informationen getrennt vom Quellprogramm zu verwalten. Das Material zu (8) schließlich fällt ohnehin während des Tests an. Die einzigen notwendigen Maßnahmen zur Erstellung vollständiger und übersichtlicher Testunterlagen sind Vorsicht beim Wegwerfen von Testdaten und -Protokollen und Sorgfalt bei ihrer Archivierung. 2.5.2 Programmtext und Kommentare Um als Dokumentationsunterlage dienen zu können, muß der Programmtext lesbar sein. Die Grundregel für ein lesbares Programm ist, daß die statische Niederschrift der Anweisungen die dynamischen Abläufe und die logische Struktur der Problemlösung aufzeigt. Alle in diesem Buch enthaltenen Methoden haben dies zum Ziel. Ein mit ihnen erstelltes Programm wird immer lesbar sein, wenn bei der Niederschrift noch einige einfache Regeln eingehalten werden. — Die Gliederung des Programms sollte so konsequent durchgeführt werden, daß logisch in sich abgeschlossenen Komponenten entstehen, die nicht größer als eine Seite Programmliste sind. Soweit die verwendete Programmiersprache und ihr Compiler es gestatten, sollten durch entsprechende Compileranweisungen (PL/1 : Steuerzeichen in Spalte 1, BAL : EJECT) die Seitenvorschübe der Liste auch entsprechend gesetzt werden.

2.5 Die Dokumentation

207

— Zur Vereinheitlichung der verwendeten Konstruktionen bei der Beschreibung des Steuerflusses sollte für jeden Strukturblock-Typ eine verbindliche, immer zu verwendende Befehlsfolge vereinbart werden. Anhang A.l bis A.3 bringt Vorschläge für die üblichen Programmiersprachen. — Die Verwendung problemnaher Namen für Prozeduren und Datenbereiche kann ihre Bedeutung veranschaulichen. Bei Verwendung langer Namen entsteht zwar zusätzliche Schreibarbeit, diese macht sich aber später beim Lesen des Programmes bezahlt. — Das Einrücken von Befehlsfolgen verdeutlicht Programmzusammenhänge bei geschachtelten Steuerflußstrukturen. Anweisungen gleicher Schachtelungstiefe sollten direkt untereinander geschrieben werden. Merkwürdigerweise wird dieses einfache Mittel zur Lesbarkeitserhöhung fast nur von ALGOL-, PL/1- und PASCAL-Programmierem verwendet. Praktisch alle Sprachen erlauben aber seine Verwendung, auch FORTRAN, COBOL und BAL (/360-Assembler). — Kommentare müssen überall dort eingesetzt werden, wo die Mittel der Programmiersprache zur eindeutigen Kommunikation vollständiger Informationen an einen anderen Menschen nicht ausreichen. Sie müssen hierfür grundsätzlich den Sinn einer Anweisung oder Anweisungsfolge, den Grund für die Wahl dieser und nicht einer vielleicht plausibleren anderen Realisierung oder eine Warnung bezüglich möglicher Fehlerquellen, Wartungsirrtümer o. ä. enthalten. Im besonderen sollten Einschränkungen im Wertebereich oder in der Verwendung von Variablen immer durch Kommentare erläutert werden, sofern dies nicht in der verwendeten Programmiersprache, wie durch das Typenkonzept in PASCAL (vgl. Abschnitt 1.2.2), Scope-Regeln etc., unmittelbar ausgedrückt werden kann. Kommentare sollten nicht in Prosa noch einmal erläutern, was die betreffende Anweisung nach den Regeln der Programmiersprache tut. In vielen Programmen findet man nahezu ausschließlich Kommentare der Art: i := i + 1 L 1 ,A

{Erhöhe i um 1}, LADE REGISTER 1 MIT A.

Nützliche Kommentare für diese beiden Fälle wären etwa {Vorsetzen auf nächsten Tabelleneintrag}, VERSORGUNG FUER SUBROUTINE XYZ. Allgemeine Angaben über einen Modul, wie Autornamen, Freigabedaten, Voraussetzungen und Einschränkungen seiner Anwendung u. ä. sollten in standardisierter Form als Kommentare an seinen Anfang gesetzt werden. Man nennt diese Kopfinformation meist Modulehead [JONAOO].

208

2. Systementwicklung

2.6 Die Projektleitung 2.6.1 Das „klassische" Projektmanagement 2.6.1.1 Führungsprobleme bei Software-Projekten Jede nicht völlig unbedeutende kooperative Software-Entwicklung wird von mehreren Personen mit unterschiedlichen Kenntnissen, Fähigkeiten und Aufgaben durchgeführt. Um das gemeinsame Vorhaben innerhalb eines gegebenen Terminund Kostenrahmens erfolgreich zu beenden, muß es ein Management innerhalb des Teams geben, welches die — Anleitung, — fachliche Überwachung und — wirtschaftliche Kontrolle wahrnimmt. Die fachliche Überwachung hat hierbei die technischen Eigenschaften des Produktes wie Übereinstimmung mit der Spezifikation, Fehlerfreiheit und Erfüllung der Qualitätsforderungen zum Gegenstand. Die wirtschaftliche Kontrolle sucht zu erreichen, daß die zu Beginn des Projektes aufgestellten Kosten- und Terminvorgaben nicht überschritten werden. Die hier vorgenommene Zweiteilung der Management-Aufgabe in einen fachlichen und einen wirtschaftlichen Aspekt wird meist nicht explizit ausgesprochen — vermutlich, weil sie als trivial empfunden wird. Sie darf aber nie aus dem Auge verloren werden, da die beiden Teilaufgaben völlig unterschiedliche Kenntnisse und Qualifikationen erfordern, die nur selten in einer einzigen Person vereint sind. Die Erfahrung zeigt, daß die wirtschaftliche Kontrolle eines Softwareprojekts offenbar die schwierigere der beiden Management-Aufgaben ist. Während es in der Regel gelingt, irgendwann ein geplantes Programm in ausreichender Qualität fertigzustellen und zum Einsatz zu bringen, werden die ursprünglichen Zeit- und Aufwandsschätzungen sehr selten auch nur annähernd realisiert. Termin- und Kostenschätzungen für Softwareprojekte gehen meist von dem von erfahrenen Programmierern recht zuverlässig abzuschätzenden Umfang des zu erstellenden Programmsystems in Lines of Code (LOC) aus, d. h. der Zahl von Programmzeilen (Anweisungen, ohne Kommentare) des zu erstellenden Softwareprodukts.

209

2.6 Die Projektleitung

In der Assembler-Programmierung ist eine LOC zumindest für die grobe Abschätzung gleich einer Anweisung oder einer Datendefinition (DC oder DS in BAL). Bei höheren Sprachen reichen hingegen Anweisungen und Datendefinitionen oft über viele Einzelzeilen des Programmtextes hinweg. Deshalb ist die „LOC" eine etwas bessere Basis für die Abschätzung des Programmieraufwandes als die „Anweisung". Der geschätzte Umfang wird dann durch einen Erfahrungswert für die Programmierleistung eines Bearbeiters pro Jahr oder Monat geteilt, woraus sich ein geschätzter Aufwand in Bearbeitermonaten (BM) oder -jähren (BJ) ergibt. Wegen Urlaubs- und sonstiger Fehlzeiten wird dabei das Bearbeiterjahr meist mit 10 bis 11 BM gleichgesetzt. Schließlich wird der so ermittelte Aufwand noch durch die nach der Terminvorgabe zur Verfügung stehende Entwicklungszeit geteilt. Daraus soll sich dann die Zahl der für das Projekt einzusetzenden, parallel arbeitenden Bearbeiter ergeben. Diese Rechnung ist aus verschiedenen Gründen unzuverlässig. Die Fehlschätzungen machen sich (nach der ungünstigen Seite hin!) umso stärker bemerkbar, je größer das eingesetzte Team ist. Sie haben zwei Ursachen, — statistische Schwankungen und — das Brooks'sche Gesetz. Die statistischen Schwankungen stammen aus der sehr unterschiedlichen Leistungsfähigkeit von Programmierern, und zwar auch von guten Programmierern je nach dem Schwierigkeitsgrad des Projektes, der Vertrautheit mit der Aufgabe, der Programmiersprache, der verwendeten Hardware und dem Betriebssystem, sowie sicher auch menschlichen Faktoren wie persönlichem Befinden und Harmonie oder Disharmonie mit den Mitarbeitern und Vorgesetzten. Tab. 2.6.1.1-1 zeigt die Schwankung von Mittelwerten (!) für die Produktivität in der Programmentwicklung nach verschiedenen Quellen und bei mehreren unterschiedlichen Projekten. Zu dieser bereits beträchtlichen Streuung der Mittelwerte kommen dann noch die Unterschiede in der persönlichen Leistung des Programmierpersonals. Eine Studie [SACK68], bei welcher verschiedenen Programmierern die gleichen Aufgaben gestellt wurden, ermittelte die folgenden Verhältniszahlen zwischen jeweils bester und schlechtester Leistung: benötigte Testzeit benötigte Rechnerzeit Kodierzeit Programmumfang Laufzeit des fertigen Programms

26:1 , 11:1, 25:1 , 5:1, 13:1 .

210

2. Systementwicklung

Tab. 2.6.1.1-1. Produktivitätszahlen für die Programmentwicklung

Systemprogrammierung

Anwendung

PROJEKT

Gesamtumfang Entwicklungsin LOC dauer in Monaten

Gesamtaufwand Produktivität in BM in LOC/BJ

Mittelwert über 17.882 174 Anwendungs- (versch. Proprogrammentwick- grammierlungen sprachen) (Quelle [WEIN70])

10

75

2.600

New York Times Information Retrieval System

83.000 (PL/1)

22

132

6.900

350 Anwendungsprogramme eines Realzeitsystems (Quelle [MART00])

150.000 (Assembler)

unbekannt

738

2.240

• /.

./.

200 bis 2.000

unbekannt

595

1.440

Systemprogrammierungsaufgaben (Compiler, Betriebssysteme, Datenübertragung) nach unveröffentlichten Herstellerstatistiken im Durchschnitt Steuerprogramm eines Realzeitsystems (Quelle [MART00])

77.700 (Assembler)

DateikatalogSystem

7.000 (Assembler)

6

25

3.100

SPOOL-System

24.000 (Assembler)

9

70

3.800

DatenbankÜbersetzer

7.000 (Assembler)

4

9

8.600

Betriebssystem (Quelle [LAUE75))

26.000 (Assembler)

24

115

2.530

LOC : „Lines of Code", BM: Bearbeitermonate, BJ: Bearbeiterjahr = 11 BM

Aus den offensichtlich unvermeidlichen statistischen Schwankungen der Programmierleistungen ergeben sich zwei Grundforderungen für ein erfolgreiches Projektmanagement.

211

2.6 Die Rrojektleitung

— Es muß vermieden werden, daß das Versagen eines Bearbeiters nennenswert auf die Termin- und Kostensituation des Gesamtprojekts durchschlägt oder, im Extremfall, gar erst beim Integrationstest kurz vor dem Endtermin bemerkt wird und nun durch völliges Neuerstellen vieler Komponenten eine Verzögerung um eine der bisherigen Projektdauer vergleichbare Zeit verursacht. — Es muß frühzeitig und zuverlässig erkannt werden, bei welchen Komponenten die unvermeidbaren statistischen Schwankungen auftreten, damit rechtzeitig entsprechende Abhilfen getroffen oder Plankorrekturen durchgeführt werden können. Das Brooks 'sehe Gesetz war in seiner ursprünglichen Form „Adding manpower to a late Software project makes it later (Das Hinzuziehen weiterer Bearbeiter zu einem in Terminnot geratenen Software-Projekt verzögert es noch mehr)" wohl als Software-Äquivalent zum Parkinson'sehen Gesetz gemeint, wird heute aber zusehends ernster genommen [BR0074]. Es faßt in einer kurzen Formulierung die Tatsache zusammen, daß der Zeitbedarf für jede im Team geleistete Tätigkeit aus zwei Grundkomponenten besteht: (a) der produktiven Arbeit am Projektfortschritt selbst sowie (b) der Kommunikation und der gegenseitigen Abstimmung der Teammitglieder untereinander. Gäbe es nur die Komponente (a), so würde der Zeitbedarf t für ein Projekt mit der Anzahl n der eingesetzten Mitarbeiter entsprechend t ~l/n absinken (vgl. Abb. 2.6.1.1-2 a). Nimmt man hingegen an, daß jeder Mitarbeiter mit jedem anderen eine bestimmte Menge an Kommunikation (Besprechungen, informelle Unterhaltung, schriftliche Memoranden und Berichte) austauschen muß, so ergibt sich für die Entwicklungszeit ein Gesetz der Form t~l/n + k(3)~l/n + k ^

2.6.1.1/1

Diese Kurve ist in Abb. 2.6.1.1-1 b gezeigt. Ist der durchschnittliche Zeitaufwand für die Kommunikation k nicht vernachlässigbar, d. h. handelt es sich um ein intellektuell anspruchsvolles Projekt mit starker gegenseitiger Abhängigkeit der Einzelarbeiten voneinander, so existiert ein Minimum der Kurve und eine zugehörige (termin-) optimale Mitarbeiterzahl, deren Steigerung sowohl die Kostenais auch die Terminsituation ungünstiger macht. Diese dürfte bei der Softwareentwicklung verhältnismäßig tief (etwa 2 bis 5 Bearbeiter) liegen. Die Konsequenz für die Systemplanung ist es, den nötigen Kommunikationsaufwand zwischen Bearbeitern so klein wie möglich zu halten. Man beachte, daß sowohl die Kontrollfluß-Einschränkungen bei der Ablaufstrukturierung (Abschnitt 1.3) als auch die Einführung vir-

212

2. Systementwicklung

(j^) M C 3

keine Kommunikation notwendig

C

W

Anzahl der Mitarbeiter

© b )

kürzeste Entwicklungszeit

Kommunikationsaufwand groß

keine Terminverkürzung durch Erhöhung der Anzahl der Programmierer über einen bestimmten Optimalwert

n Anzahl der Mitarbeiter Abb. 2.6.1.1-2. Einfluß des Kommunikationsaufwandes auf den Gesamtaufwand

2.6 Die Projektleitung

213

tueller Maschinen (Abschnitt 1.6) und eine darauf aufbauende hierarchische Modularisierung (Abschnitt 2.3.4) gezielte Verbesserungen gerade in diesem kritischen Punkt erbringen. 2.6.1.2 Der Berichtsweg und die Kontrolle des Entwicklungsfortschritts

Abb. 2.6.1.2-1 skizziert die übliche Management-Struktur eines Softwareprojekts mit den Bearbeitungs-, Anleitungs- und Berichtswegen an Hand der bereits in Abb. 2.1.2-2 dargestellten Phasenaufteilung einer Systementwicklung. Die am Projekt arbeitenden Personen (-gruppen) und ihre Aufgaben sind folgende: — Der Auftraggeber ist nicht formelles Mitglied des Teams, aber die maßgebliche Instanz für die Aufgabenstellung und die Beurteilung der Lösung. Er erstellt — meist in Zusammenarbeit mit dem (technischen) Projektleiter — die Spezifikation als Vorgabe und erhält Berichte über den wirtschaftlichen Projektstand vom Systemmanager und über den technischen Entwicklungsfortschritt vom Projektleiter. — Der Systemmanager* ist für den wirtschaftlichen Aspekt der Projektdurchführung verantwortlich und wird meist auch dem (technischen) Projektleiter übergeordnet. Allerdings ist seine Anleitungsfunktion gegenüber dem Projektleiter gering und beschränkt sich auf Vorgabe und Überwachung des Termin- und Kostenrahmens. Er erhält entsprechende Berichte vom Projektleiter und berichtet an den Auftraggeber. — Der Projektleiter ist für die technische Projektabwicklung zuständig und meist an keine Weisungen, abgesehen von Termin- und Kostenvorgaben, gebunden. Er arbeitet in der Regel an der Spezifikation mit und ist immer der verantwortliche Autor der Planung (wobei hierunter die in Abschnitt 2.3 definierte Projektphase und ihr Ergebnis verstanden werden — nicht die Feinplanung, die Aufgabe der Programmierer ist). Er leitet die Programmierer an und überwacht sie. Er berichtet an den Systemmanager über die für den wirtschaftlichen Projekterfolg relevanten Fakten sowie u. U. über die technische Realisierung an den Auftraggeber, sofern dieser „technisch orientiert" ist und sich dafür interessiert. — Die Programmierer schließlich führen nach Anweisung des Projektleiters die Feinplanung, die Programmierung und den Test der Komponenten aus und berichten hierüber an den Projektleiter. In der konventionellen Projektorganisation war es üblich, jedem Programmierer einen oder mehrere ProgrammModule zuzuteilen (z. B. die Dateierstellung, die Auswertungsprogramme, die Dateisicherung), für die er voll und allein verantwortlich war. Beim Integrationstest wurden die so erstellten Einzelmodule dann zum Gesamtsystem zusammengefügt. * Für diese Funktion hat sich noch kein allgemein eingeführter Name durchgesetzt.

214

2. Systementwicklung

(formalisierter) Berichtsweg über den Projektfortschritt

Abb. 2.6.1.2-1. Konventionelle Managementstruktur eines Softwaieprojekts

2.6 Die Projektleitung

215

Bei kleinen Projekten sind häufig mehrere dieser Positionen durch einen einzigen Mitarbeiter ausgefüllt: der Projektleiter ist gleichzeitig auch für die wirtschaftliche Projektabwicklung zuständig und damit sein eigener Systemmanager, oder er arbeitet als Programmierer aktiv an der Erstellung einer oder mehrerer Programmkomponenten mit. Nur in den seltensten Fällen bewährt sich ein Zusammenlegen von technischer und wirtschaftlicher Leitung und Kontrolle, d. h. die gleichzeitige Wahrnehmung der Systemmanager- und Projektleiter-Funktion durch eine Person. Es zeigt sich meistens, daß dann eine der beiden gleich wichtigen Aufgaben aus Mangel an — Spezialkenntnissen, — Interesse oder — Zeit vernachlässigt wird. Die personelle Trennung von wirtschaftlichem und fachlichem Management führt nun zu einem Problem: der Systemmanager ist in der Regel nicht in der Lage, selbst den technischen Projektstand zu beurteilen und zu kontrollieren. Deshalb ist er auf einen funktionierenden und verläßlichen Berichtsweg vom Projektleiter angewiesen. Ohne diesen kann er keinen laufenden Soll-Ist-Vergleich der Produktion mit den Vorgaben durchführen und damit auch seine Kontroll- und Leitungsfunktion nicht wahrnehmen. Jedes Management kann als Teil eines Regelkreises angesehen werden (Abb. 2.6.1.2-2): es muß über den Berichtsweg laufend zuverlässige Meßdaten über den Projektfortschritt erhalten, diese mit den Sollvorgaben vergleichen und dementsprechend Anweisungen, Aufträge und Korrekturen an die Projektdurchführung, das technische Team geben. Die üblichen Aussagen, die der Systemmanager vom Projektleiter und dieser von seinen Programmierern erhält, wie „80% kodiert, 50% ausgetestet" oder „noch 14 Tage bis zur Fertigstellung" können nicht als „funktionierender Berichtsweg" angesehen werden. Sie sind keine exakten Daten, sondern bestenfalls eine subjektive Meinungsäußerung und schlimmstenfalls eine bewußte Lüge des Bearbeiters. Sie können schon vom Projektleiter, geschweige denn vom Systemmanager, nicht überprüft werden. Die nur selten realisierte Vorbedingung für ein nicht nur auf Hoffnung und blindes Vertrauen gegründetes wirtschaftliches Projektmanagement ist deshalb eine Berichtswegeorganisation, welche dem Systemmanager die Softwareentwicklung transparent macht. Dazu sind folgende Forderungen zu stellen: — Reduzierung der Meldungen für die wirtschaftliche Projektüberwachung auf eine kontinuierliche Folge von Ereignissen, deren fachliche Bedeutung der Systemmanager nicht zu verstehen braucht;

216

2. Systementwicklung

Abb. 2.6.1.2-2. Management als Regelkreis

— Vertrauen in die exakte Definition und Feststellung des Eintritts dieser Ereignisse durch den Projektleiter oder das gesamte fachlich qualifizierte Team; — lückenlose und möglichst automatische Buchführung über diese Ereignisse während des normalen Softwareentwicklungsprozesses ohne zusätzliche Belastung der Programmierer und des Projektleiters durch ein nennenswertes Berichtswesen, das erfahrungsgemäß vergessen oder sogar planmäßig als „Bürokratisierung" sabotiert wird. Diese drei Forderungen werden vom konventionellen Projektmanagement nicht erfüllt. Das einzige wirklich zuverlässig definierte Ereignis ist hier „Erfolgreicher Abschluß des Integrationstests", und dies bedeutet zugleich den Abschluß der Management-Aufgabe! 2.6.2 Das Chef-Programmierer-Team 2.6.2.1 Die Top Down-Programmierung und das Zustandsdiagramm einer Software-Komponente Für die Kontrolle des Projektfortschritts sind nach Abschnitt 2.6.1.2 Ereignisse notwendig, deren Auftreten

217

2.6 Die Projektleitung

— über die gesamte Projektdauer zeitlich ungefähr gleichverteilt, — exakt definiert und dokumentierbar sowie — als „Fertigstellung" einer Produktkomponente zu betrachten und als solche verifizierbar („abnehmbar") ist. Derartige Ereignisse können bei der in Abschnitt 2.4.1 eingeführten Top DownProgrammierung im Gegensatz zur herkömmlichen Bottom Up-Entwicklung als Freigabe einer Softwarekomponente (Modul, Modulteil, auch Definition eines Datenbereichs oder Kontrollblocks) festgelegt und formalisiert werden. Abb. 2.6.2.1-1 skizziert den zu überwachenden und zu leitenden Ablauf der Programmierphase als Struktogramm. Programmiere oberste Systemebene (Hauptsteuerleiste oder JCL-Auftragsbeschreibung) Codiere Platzhalter (Dummies) für alle aufgerufenen Komponenten (Unterprogramme, Makros) Teste oberste Systemebene (Erster „Integrationstest")

Wiederhole, bis alle Platzhalter durch endgültige Programme ersetzt

Wähle einen Platzhalter und ersetze ihn durch endgültigen Code Codiere ggf. neue Platzhalter für die von ihm aufgerufenen Komponenten Wiederhole, bis System wieder fehlerfrei (Freigabe des endgültigen Codes) Teste System und neue Komponente

Korrigiere ggf. Zwischenebenen entsprechend Baumstruktur Abb. 2.6.2.1-1. Der Ablauf der Programmierphase

Korrigiere Fehler in neuem Code

Freigabe

218

2. Systementwicklung

Die Wiederholungsschleife ( T ) ist hierbei die sukzessive Übergabe der zur programmierenden Komponenten an den Bearbeiter durch den Projektleiter, die Wiederholungsschleife (2) der Test dieser Komponente durch den Bearbeiter. Besteht das Team aus mehr als einem Programmierer, so arbeitet jeder von ihnen (als „asynchroner Prozessor" in der Sprache der EDV) weitgehend unabhängig von den anderen an „seiner" Komponente in der Schleife (T) . Die Schleife (2) ist sowohl Komponententest für den neuerstellten Code (mit den bereits vorhandenen höheren Systemschichten als „Testrahmen") als auch Integrationstest für die neue, den Platzhalter ersetzenden Komponente. Hierfür muß jeder Bearbeiter eine private Entwicklungs-Bibliothek für die noch nicht freigegebenen Komponenten haben, die er gerade programmiert und testet. Die parallel an anderen Komponenten arbeitenden Programmierer benutzen noch die in der öffentlichen Projektbibliothek stehenden Platzhalter und werden deshalb durch Fehler in der Testversion der neuen Komponente nicht gestört. Die innere Wiederholungsschleife (2) wird jeweils abgeschlossen durch die Feststellung der Fehlerfreiheit der neuerstellten Komponente und ihre Freigabe durch den mit ihrer Anfertigung betrauten Programmierer. Die Freigabe ist noch immer subjektiv. Zu einem objektiven, für die Projektüberwachung im Sinne von Abschnitt 2.6.1.2 brauchbaren Ereignis wird sie durch die formelle Abnahme der neuen Komponente durch das gesamte fachlich qualifizierte Team. Auf die Abnahmeprozedur wird in Abschnitt 2.6.2.3 eingegangen. Jede Software-Komponente durchläuft in ihrer Entwicklung also vier Zustände, die in Abb. 2.6.2.1-2 als Zustandsdiagramm dargestellt sind. Nach der Erteilung des Auftrags zur Programmierung der Komponente beginnt der Programmierer mit der Feinplanung und Kodierung. Anschließend katalogisiert er seine Komponente in seiner privaten Bibliothek (T), womit die Komponente in den zweiten Zustand (im Test) übergeht. Es ist hier angenommen, daß es sich um unabhängig kompilierte und durch den Binder (Linkage-Editor) oder einen Makrogenerator (Assembler oder PL/1) in das Gesamtprodukt einzufügende Komponenten handelt. Sind die Komponenten jedoch mitzukompilierende Ergänzungen bereits bestehender Programmbausteine (z. B. PASCALProzeduren oder in COBOL mittels PERFORM anzuschließende Codeabschnitte), so übernimmt der Programmierer aus der Projektbibliothek die alten Bausteine in seine private Bibliothek und ergänzt sie durch den neuen Code. Hierbei hilft ein Quellbibliothek-Pflegesystem [ANON73, SZAL74],

219

2.6 Die Projektleitung

private Bibliothek des Bearbeiters

i '

öffentliche Projekt — Bibliothek

Auftragsvergabe an Bearbeiter

Abb. 2.6.2.1-2. Zustandsdiagramm einer Software-Komponente

220

2. Systementwicklung

Anschließend wird die neue Komponente im Rahmen des bereits bestehenden Systems ausgetestet (2). Nach Abschluß des Tests wird sie freigegeben (3), wobei sie den bisherigen Platzhalter ersetzt. Sie wird dann etwa 14 Tage lang vom Entwicklungsteam erprobt und anschließend abgenommen (4) oder — bei zu großer Fehlerhaftigkeit — an den Programmierer zurückgegeben und in der öffentlichen Projektbibliothek wieder durch den Platzhalter ersetzt (?). 2.6.2.2 Der Projektsekretär und die Team-Organisation Die im vorigen Abschnitt dargestellte Top Down-Realisierung mit kontinuierlicher Integration und formeller Freigabe und Abnahme der neuerstellten Komponenten erfordert auch ein kontinuierliches Management, sie „läuft nicht von selbst ab". Die klassische Projektorganisation, bei der am Anfang der Programmierphase die Module an die Programmierer zur Anfertigung verteilt und gegen Ende zur Integration „eingesammelt" wurden, erweckte die Illusion, von selbst abzulaufen. Zwischen diesen beiden Zeitpunkten war es im Grunde die einzige Funktion des Projektleiters, Anwesenheit und Arbeitseifer der ihm unterstellten Mitarbeiter durch Stichproben zu prüfen sowie gegebenenfalls Fragen zu beantworten und Diskussionen über Schnittstellendefinitionen und ähnliche technische Probleme zu entscheiden. Diese Illusion des automatischen Projektfortschritts wurde dann leider meist beim Integrationstest jäh zerstört. Die Verwirklichung einer kontinuierlichen Projektleitung und -Überwachung stößt auf zwei Schwierigkeiten: - Die Programmierer müssen laufend mit der Erstellung relativ kleiner Komponenten (jeweils einer Primitivoperation einer virtuellen Maschine, vgl. Abschnitt 1.6.2) beauftragt und deren Freigabe und Abnahme müssen technisch überwacht werden. — Die Verfolgung des Zustands jeder dieser Komponenten, ihre Übernahme aus der privaten Bibliothek in die öffentliche sowie die Dokumentation des Entwicklungsstandes können weder dem Programmierer überlassen werden (weil dann eine verläßliche und vollständige Erledigung dieser Verwaltungsarbeit vor allem in Streß-Zeiten nicht gewährleistet ist), noch kann der Projektleiter diese Aufgabe wahrnehmen (da es ihn überlasten und ihm jede Zeit zur Leistung seiner eigentlichen, technischen Arbeit nehmen würde). Es ist das Verdienst von Baker und Mills [BAKE72a, BAKE73, IBM71 ], durch Entwicklung der Chef-Programmierer-Team-Orgamsution dieses Managementproblem gelöst zu haben.

2.6 Die Projektleitung

221

Die praktische Brauchbarkeit ihres Konzepts zeigte die Entwicklung des New York-Times Information Retrieval Systems. Dieses System wurde nach den Grundsätzen der Top Down-Realisierung und der Chef-Programmierer-Organisation in bemerkenswert kurzer Zeit erstellt (vgl. die Produktivitätszahlen in Tab. 2.6.1.1-1). Auch die Produktqualität war von vorher kaum je erreichter Güte: der erste Fehler wurde ein Jahr nach der Übergabe gefunden. Die in Abb. 2.6.2.2-1 (im Vergleich zu Abb. 2.6.1.2-1) dargestellte Chef-Programmierer-Projektorganisation löst die beiden oben skizzierten Probleme durch die Einführung von zwei neuen Funktionen im Team, welche jeweils in der Regel von einem Bearbeiter wahrgenommen werden: — Der Projektassistent (Backup Programmer) ist der engste technische Mitarbeiter des Projektleiters (Chief Programmer). Seine fachliche Qualifikation sollte möglichst auf gleicher Ebene stehen, denn es ist seine Hauptaufgabe, den Projektleiter nicht nur zu unterstützen, sondern ihn bei kurzzeitigem oder dauerndem Ausfall (Urlaub, Krankheit, unvermeidliche vorfristige Versetzung, Kündigung) zu ersetzen. Deshalb muß er im Unterschied zu den Programmierern die Planung und Implementierung des gesamten Projekts überschauen — er sollte möglichst auch zusammen mit dem Projektleiter bereits an der Planung mitwirken. Die Einführung des Projektassistenten gewährleistet, daß auch bei Abwesenheit des Projektleiters das technische Management, d. h. die Aufgabenverteilung an die Programmierer und die Abnahme ihrer Komponenten, weiter laufen kann. — Der Projektsekretär (Programming Secretary) nimmt sowohl dem Projektleiter als auch den Programmierern die gesamte Verwaltungsarbeit ab und bereitet die statistischen Daten für die Projektüberwachung auf. Im besonderen berichtet er dem Projektleiter über die erfolgte Freigabe und Abnahme der Komponenten, die dieser dann dem Systemmanager zur wirtschaftlichen Projektkontrolle weitermelden kann. Die zentrale Aufgabe des Sekretärs ist die Erstellung und Wartung der Projektbibliothek. Die Projektbibliothek enthält zu jedem Zeitpunkt den aktuellen Projektstand in Form von Quellprogrammen, Maschinenprogrammen, Dokumentationen, Projektnotizen. Die Programmierer arbeiten an der Entwicklung des Systems, indem sie Systemteile implementieren, existierende Programme ändern und erweitern, im Projekt-Handbuch Änderungen anbringen, Programmläufe mit Testdaten verlangen. Für die tatsächliche Durchführung all dieser Systemänderungen und -erweiterungen in der Projektbibliothek sowie für die Aufbereitung

222

2. Systementwicklung

Bearbeitung Anleitung

(Formalisierter) Berichtsweg über den Projektfortschritt

Abb. 2.6.2.2-1. Chef-Programmierer-Projektorganisation nach Baker und Mills

2.6 Die Rrojektleitung

223

der Ergebnisse ist im Mills'sehen Chef-Programmierer-Team der Sekretär zuständig. Der Programmierer geht mit jeder Anforderung für einen Test oder Programmlauf zum Sekretär. Eine derartige Anforderung kann etwa ein Kartenstapel sein, den er als Quellprogramm in der Projektbibliothek katalogisiert haben möchte, oder eine Programmänderung, die auf Kodierblättern festgehalten ist. Die Zusammenstellung der entsprechenden Jobs ist die Aufgabe des Sekretärs (vgl. Abb. 2.6.2.2-2). Der Programmierer hat also überhaupt keinen direkten Zugang zur Rechenanlage. Er erhält auch die Ergebnisse, seine Ausgabelisten und -Protokolle, grundsätzlich über den Sekretär zugestellt.

Abb. 2.6.2.2-2. Die Rolle des Projektsekretärs im Chef-Programmierer-Team

224

2. Systementwicklung

Dies hat den Sinn, daß der Sekretär alle für die Erstellung von Testjournalen, Statistiken und Dokumentation nötigen Informationen unmittelbar aus den Listen entnehmen kann und nicht auf die erfahrungsgemäß unzuverlässigen Aufzeichnungen und Meldungen der Programmierer angewiesen ist. Wie Abb. 2.6.2.2-1 zeigt, ersetzt die Einführung des Projektsekretärs die vielen, schlecht nachzuprüfenden und mühsam zu korrelierenden Berichte der einzelnen Programmierer im „klassischen" Management-Konzept (Abb. 2.6.1.2-1) durch einen bereits vorgeprüften und aufbereiteten Bericht des Projektsekretärs an den Projektleiter.

2.6.2.3 Freigabe und Abnahme einer Komponente Nach Abschnitt 2.6.2.1 ist nicht die Freigabe einer Komponente durch den Programmierer sondern deren Abnahme durch den Projektleiter und das gesamte fachlich qualifizierte Team dasjenige Ereignis, welches für die Überwachung des Projektfortschritts objektiv festgestellt werden kann. Die Abnahme spielt somit eine zentrale Rolle im wirtschaftlichen Projektmanagement und verlangt eine formal festgelegte Prozedur. Mit der Abnahme geht die fachliche Verantwortung für die Richtigkeit der Komponente von dem einzelnen Bearbeiter auf das gesamte Team über. Tab. 2.6.2.3-1 stellt den zeitlichen Ablauf von Freigabe, Abnahmediskussion (von IBM als Walk Through bezeichnet [IBMOO]) und Abnahme zusammen. An der Abnahmediskussion sollten grundsätzlich nur die „technischen" Teammitglieder, d. h. nicht der Systemmanager, der Auftraggeber und sonstige Personen mit Personalverantwortung teilnehmen, um die unbefangene Fachdiskussion nicht zu stören. Bei der Abnahmediskussion wird vom Projektsekretär oder einem von ihm beauftragten Teammitglied ein Kurzprotokoll über gemeldete Fehler und sonstige Kritik angefertigt. Der für die Komponente verantwortliche Programmierer erhält eine handschriftliche Kopie dieses Protokolls. Er muß innerhalb der nächsten Woche die Mängel beheben und dies auf dem Protokoll vermerken. Nach befriedigender Beseitigung dieser Mängel meldet der Projektsekretär die Abnahme an die Projektleitung.

2.6 Die Rrojektleitung

225

Tab. 2.6.2.3-1. Freigabe und formelle Abnahme einer Programmkomponente TERMIN

PROJEKTSEKRETÄR

ca. 1 Woche vor Freigabe

gibt durch Rundschreiben oder Aushang die bevorstehende Freigabe der Komponente mit Kurzdokumentation bekannt.

Freigabe

ersetzt Platzhalter in öffentlicher Projektbibliothek durch neue Komponente.

benutzt die neue Komponente in jedem neu übersetzten bzw. gebundenen Testprogramm.

ruft Team zur Abnahmediskussion („Walk Through") zusammen.

Diskussion: Fragen der Teammitglieder zur Funktion der Komponente, Mitteilung aufgetretener Fehler, Erläuterung des Struktogramms und ggf. des Codes der Komponente durch ihren Programmierer, ggf. Kritik des Produkts und seiner Dokumentation (vgl. hierzu Abschnitt 2.4.4).

Abnahmeprozedur ca. 2 Wochen nach Freigabe

in der Woche nach Abnahmeprozedur

überwacht Beseitigung evt. beanstandeter Mängel durch den zuständigen Programmierer

Abnahme

meldet Abnahme an Projektleiter und Systemmanager

technisches TEAM

Bei schwerwiegenden, nicht innerhalb einer Woche zu behebenden Fehlern der Komponente wird sie aus der Projektbibliothek in die private Bibliothek des Programmierers zurückübertragen, und der Platzhalter nimmt wieder ihre Stelle ein.

2.6.3 Die Projektbibliotheks-Verwaltung 2.6.3.1 „Projektsekretär" vs. „Projektverwalter" Im Mills'schen Chef-Programmierer-Team sind zwar die Programmierer von der Routinearbeit für die Verwaltung entlastet, dem Sekretär aber werden alle diese Routinearbeiten aufgebürdet. Es ist deshalb nicht einfach, qualifizierte Mitarbeiter zu finden, die diese Aufgabe übernehmen wollen. Außerdem ist der Sekretär ein potentieller Engpaß in der Systementwicklung. Wenn der Projektleiter nicht verfügbar ist, kann der Projektassistent seine Stelle einnehmen. Wer aber nimmt die Stelle des Projektsekretärs ein, wenn dieser ausfällt?

226

2. Systementwicklung

Nun ist etwa 90% der vom Projektsekretär zu leistenden Arbeit die routinemäßige Pflege der Projektbibliothek, ein Sammeln von Statistiken über Testläufe, Programmfreigaben und -abnahmen und ein Aufbereiten von Dokumentationsunterlagen. Routinearbeit kann aber automatisiert und dem Computer übertragen werden. Durch ein Bibliotheksverwaltungs-System kann die Projektbibliothek zu einer Datenbasis für das Projektmanagement ausgebaut werden, welche die nötigen Statistiken und Dokumentationsunterlagen automatisch sammelt und vom Projektsekretär mit einer eigenen Manipulationssprache gepflegt und abgefragt werden kann. Dies ist vor allem bei Programmentwicklung in einer interaktiven Umgebung wichtig, in welcher der Programmierer von einem Terminal aus seine privaten Bibliotheken pflegt und seine Jobs über ein RJE oder ein ähnliches System direkt in die Auftragsverwaltung der EDV-Anlage einschleust. Das Mills'sche Konzept des Sekretärs, über den grundsätzlich jede Kommunikation des Programmierers mit dem Rechner läuft, ist hier nicht mehr praktikabel - man kann ihn schlecht zwischen den Programmierer und sein Terminal setzen! Mit Hilfe eines derartigen Systems kann der Projektsekretär aus seiner, nach Abb. 2.6.2.2-1 den Programmierern untergeordneten Rolle befreit werden. Er erhält damit im Rahmen des Projektmanagements eine verantwortliche und auch für hochqualifizierte Mitarbeiter befriedigende Stellung als Verwalter der Projektbibliothek. Abb. 2.6.3.1-1 zeigt die entsprechende Variante der Chef-Programmierer-Projektorganisation [HELD74], in welcher der Projektsekretär durch den Projektverwalter ersetzt ist. Der Projektverwalter kann produktiv an der Projektführung mitarbeiten. Er wertet die in der Projektbibliothek abgelegten Dokumente aus und kann damit jederzeit Auskunft über den Projektstand geben. Er kann sich die Dokumentation eines bestimmten Moduls auslisten lassen, sie überprüfen oder dem Projektleiter vorlegen. Der Projektverwalter hat damit alle Möglichkeiten, um die notwendige Kontroll- und Informationsfunktion im Projekt erfüllen zu können. Er kann außerdem die psychologisch-menschliche Aufgabe des „Anmahnens" benötigter Bausteine, Testdaten und Dokumente wahrnehmen und organisatorische Funktionen wie die Planung des Rechenzentrum-Betriebs übernehmen. Er ist auch für Änderungen und Erweiterungen des Bibliotheksverwaltungssystems zuständig, falls sich diese als notwendig oder wünschenswert erweisen. Bei der so definierten Rolle des Projektverwalters ist ein zeitweiser Ausfall seiner Person zwar störend, gefährdet aber nicht, wie im Fall des Mills'schen Projektsekretärs, den Projektfortschritt. Die Projektbibliothek wird vom Verwaltungssystem automatisch weitergeführt.

2.6 Die Projektleitung

Bearbeitung Anleitung (Formalisierter) Berichtsweg über den Projektfortschritt

Abb. 2.6.3.1-1. Variante der Chef-Programmierer-Projektorganisation

227

228

2. Systementwicklung

2.6.3.2 Das Projektbibliotheks-Verwaltungssystem

Die Projektbibliothek besteht aus den in Abb. 2.6.3.2-1 gezeigten Dateien: — Die Produktionsbibliothek enthält die neueste Version jeder freigegebenen Systemkomponente (Programm-Modul, Datendefinition u. ä.) im Quelltext sowie als Objektmodul und ladefähige Phase. — Die Archivierungsbibliothek enthält die jeweils vorige Version jeder freigegebenen Systemkomponente; sie dient Sicherungszwecken und als „Fall Back" bei überraschend auftretenden, schwerwiegenden Fehlern einer neu freigegebenen Komponente. — Die Dokumentations- und Statistik-Bibliothek(en) enthalten maschinenlesbare Projektinformationen, die nicht Komponenten des entstehenden Systems sind (Dokumentation, Statistiken, Projektkontrolldaten). Zahl und Aufbau ist abhängig von den eingesetzten Dokumentations- und Projektfuhrungssystemen [MAUR74], — Je eine (private) Entwicklungsbibliothek für jeden Programmierer enthält Quelltext, Objekt- und Lademodule der von ihm gerade bearbeiteten Komponente^). Diese sind ausschließlich dem betreffenden Programmierer zugänglich und werden nur von ihm benutzt und gepflegt. Alle anderen am Projekt arbeitenden Programmierer verwenden statt ihrer entsprechende Komponenten in der Produktionsbibliothek (Dummy, Platzhalter).

Abb. 2.6.3.2-1. Aufbau der Projektbibliothek

229

2.6 Die Projektleitung

Abb. 2.6.3.2-2 zeigt die von einem Programmierer bei der Programmentwicklung in seiner Entwicklungsbibliothek durchzuführenden Aktionen. Automatische Projektbibliotheksführungssysteme stellen sie als Grundoperationen bereit. Andernfalls müssen entsprechende Steuerkarten-Sätze oder katalogisierte Prozeduren hierzu vorgesehen werden. CATAL DELETE LIST COPY

— Neuaufnahme (Katalogisieren) einer Komponente bei Beginn ihrer Entwicklung — Löschen einer Komponente, — Auslisten einer Komponente, — Kopieren einer Komponente aus der Produktionsbibliothek (Wartung, Fehlerbehebung, Weiterentwicklung),

Abb. 2.6.3.2-2. Aktionen des Programmierers bei der Komponentenentwicklung in der Entwicklungsbibliothek

230

EDIT COMPILE/LINK/GO

2. Systementwicklung

— Korrigieren, Editieren und Ergänzen einer Komponente, - Übersetzen, Binden und Ausführen von Testläufen, wobei für alle nicht in der Entwicklungsbibliothek des Programmierers enthaltenen Komponenten die freigegebenen Komponenten entsprechenden Namens aus der Produktionsbibliothek entnommen werden.

Abb. 2.6.3.2-3 zeigt die vom Projektverwalter bei der Pflege und Benutzung der Produktionsbibliothek vorzunehmenden Aktionen. Auch für sie sind, soweit sie nicht durch ein Projektbibliotheksführungssystem automatisiert sind, Steuerkartensätze oder katalogisierte Prozeduren bereitzustellen. RELEASE

— Überfuhren des Quelltextes einer fertiggestellten Komponente aus der Entwicklungs- in die Produktionsbibliothek. Diese ersetzt die frühere Version (Platzhalter), die in der Archivierungs-Bibliothek aufbewahrt wird (Freigabe, vgl. Abschn. 2.6.2.3).

Abb. 2.6.3.2-3. Aktionen des Projektverwalters bei Komponenten-Freigabe und Integration in die Produktionsbibliothek

231

2.6 Die Projektleitung

COMPILE/LINK APPROVE LIST DELETE

— Integration der neuen Komponente in die Objekt- und Lademodule der Produktionsbibliothek. - Formelle Abnahme einer Komponente (vgl. Abschnitt 2.6.2.3). — Auslisten einer Komponente. — Löschen einer Komponente aus der ProduktionsBibliothek (ersatzlos).

Ein automatisches Projektbibliotheksführungs-System [FRID74, FRID75] stellt diese Operationen den Programmierern und dem Projektverwalter als Bedienungsanweisungen zur Verfügung (vgl. Abb. 2.6.3.24).

Abb. 2.6.3.2-4. Die Automatisierung der Projektbibliotheksverwaltung

2. Systementwicklung

232

Bei Ausführung der Anweisungen sammelt das System alle Informationen, die es für die Statistiken über den Projektfortschritt braucht. Außerdem überwacht es die Zugriffe zur Projektbibliothek. Es stellt damit sicher, daß ein Programmierer ausschließlich seinen privaten, nicht aber den öffentlichen oder einen fremden, nicht ihm selbst gehörenden Teil der Projektbibliothek verändern kann. Neben den in Abb. 2.6.3.2-2 gezeigten Manipulationsanweisungen verfugt der Projektverwalter noch über Auswertungsanweisungen für die Dokumentationsund Statistikbibliothek. Mit ihnen kann er die Informationen abrufen, die er für die Projektstatistik, -Überwachung und -dokumentation benötigt. Abb. 2.6.3.2-5 zeigt eine so erstellte Analyse des Status einer Komponente mit verschiedenen, für unterschiedliche Zwecke benötigten Informationen. ANALYSIS OF SOURCE-MODULE MCINT

PERFORMED ON 19 FEB 74 AT 18:09:40

/* 11 FEB 74 23:08:09 MCINT

WOERZ

SOURCE RELEASE*/

/* 03 FEB 74 11:11:39 MCINT

WOERZ

PL/1 COMPILE*/

/* 30 JAN 74 20:07:33 MCINT

WOERZ

CATALOG*/

STA TUSBERICHT, -»• TESTJOURNALE _

MODULE COMMENT: /*PROC: MCINT /* MCINT IS THE INTERPRETER PART OF THE SOFTLAB MACRO/* PROCESSOR. FOR EACH OPERATOR TO BE EXECUTED MCINT IS /* CALLED ONCE BY THE ANALYSER. THE INTERFACE 1ST DEFINED /* IN MODULE @MPEXDEF (SEMBUF, ISEMBUF, OPERATR, NUMOPDS). /* MCINT EXECUTES THE OPERATOR, RESETS THE STACK POINTER /* ISEMBUF AND SETS THE RETURNCODE-PARAMETER TO INDICATE /* THE RESULT OF THE OPERATION (RETURNCODE IS FIXED BIN, /* POSSIBLE VALUES ARE EO/ E l / E2/ E3/ TRUE/ FALSE). PROJEKT-DOKUMENTA

*/ */ */ */ */ */ */ */ */

TION

ITEM(S) INCLUDED: @MPERROR @MPEXDEF SMPTABLS SMPLIB SMPOUT @SYMTYP"|] END OF ANALYSIS OF MCINT.

> PROJEKTHANDBUCH 100187 SOURCE RECORDS SCANNED.| PROJEKT-STA

TISTIK

Abb. 2.6.3.2-5. Automatisch erstellte Komponentenanalyse

Aus den automatisch gesammelten Angaben können somit über die Auswertprozeduren jederzeit alle die Fragen beantwortet werden, welche für eine Kontrolle des Projektfortschritts und für die Meldung der relevanten Ereignisse (Abnah-

2.6 Die Projektleitung

233

men von Komponenten, vgl. Abschnitt 2.6.2.3) an das Projektmanagement von Bedeutung sind: — Was ist der aktuelle Projektstand? — Wie groß war der Projektfortschritt im letzten Monat? — Der Systemkern lief bis gestern fehlerlos und heute nicht mehr. Was wurde geändert? — Wieviel Compilierungen und Testläufe wurden für die Erstellung der Kompo- • nente A benötigt? — Wie kann die Produktivität des Programmierers X gemessen werden? — Wie kann der echte Projektfortschritt aus der großen Anzahl unterschiedlicher Aktivitäten am Projekt ausgesondert und gemessen werden? — Entspricht der Projektfortschritt dem Projektplan? — Wie können neue Komponenten in das entstehende Gesamtsystem integriert werden? Wie sollen bereits integrierte Komponenten erweitert und getestet werden? Die Auswertung der lückenlos gesammelten projektbezogenen Daten und Informationen bietet gleichzeitig auch eine empirische Grundlage für eine Aufwandsschätzung künftiger Software-Projekte. Die außerordentlichen Schwankungen in der Produktivität von Programmierern wurden bereits in Tab. 2.6.1.1-1 an Hand von einigen Beispielprojekten gezeigt. Daher ist die Auswertung von exakt gemessenen und leicht auswertbaren Daten, aus welchen auf die Einflußfaktoren für die Produktivität geschlossen werden kann, von Bedeutung für eine Verbesserung der Schätzgenauigkeit. Nach Abschluß eines Projekts können die gesammelten Informationen nach folgenden Kriterien untersucht werden: — persönliche Leistung der einzelnen beteiligten Programmierer, — Einfluß der Programmiersprache auf die Produktivität, — Einfluß sonstiger Umgebungsfaktoren oder Arbeitsbedingungen (z. B. interaktive Testmöglichkeit, Ferntest, Testnotwendigkeit an auswärtigen Rechenzentren), — zeitliche Veränderung (Zunahme oder Abnahme) der Produktivität durch wachsende Vertrautheit mit dem Problem oder wachsende Schwierigkeiten durch den bereits erreichten Codeumfang, durch Schulungen oder Organisationsänderungen.

A. Anhänge A.1 Realisierung der Strukturblöcke in C O B O L Die sprachlichen Strukturierungsmittel sind PERFORM Kapitel oder Paragraph. SECTION. EXIT. PERFORM a UNTIL (NOT) b. IF a THEN b (ELSE c). GO TO DEPENDING ON. GO TO auf Zyklus-Anfang und -Ende sowie auf Fallunterscheidungs-Ende. Vergleiche auch [FLOY74, ANONOO, MCCL75]

Wiederhole solange Bedingung b Aktion i

Aktion n Auslagerung durch PERFORM UNTIL: PERFORM Aktionsfolge UNTIL NOT b. Aktionsfolge SECTION. Beginn-Kapitelname. Aktion l . Aktion n . ENDE-Aktionsfolge. EXIT. Nächste SECTION.

Bemerkung: Enthält die Aktionsfolge kein GO TO, so kann sie auch in einen Paragraph statt in ein Kapitel (SECTION) zusammengefaßt werden.

236

A. Anhänge

Wiederhole solange Bedingung b t Aktion! Wiederhole solange Bedingung b 2 Aktion 2 Aktion 3 Auslagerung durch PERFORM UNTIL: PERFORM Aktion UNTIL NOT b j . Aktion SECTION. Beginn-Kapitelname!. Aktion!. PERFORM Aktion 2 UNTIL NOT b 2 . Aktion 3 . ENDE-Aktion. EXIT. Aktion 2 SECTION. Aktion 2 . ENDE-Aktion 2 . EXIT. Wiederhole Aktionsfolge i BREAK wenn Bedingung b Aktionsfolge 2 Cycle-Label. Aktionsfolge j. IF b THEN GO TO Ende-Cycle-Label. Aktionsfolge 2 . GO TO Cycle-Label. Ende-Cycle-Label. Bemerkung: Diese Konstruktion kann nicht innerhalb eines einzelnen Paragraphs oder in einer bedingten Anweisung angewendet werden.

A.1 Realisierung der Strukturblöcke in COBOL

237

Wiederhole solange Bedingung b j Aktionsfolge! BREAK wenn Bedingung b 2 Aktionsfolge 2 Cycle-Label. IF NOT bjTHEN GO TO Ende-Cycle-Label. Aktionsfolge!IF b 2 THEN GO TO Ende-Cycle-Label. Aktionsfolge 2 . GO TO Cycle-Label. Ende-Cycle-Label. Bemeikung: Diese Konstruktion kann nicht innerhalb eines einzelnen Paragraphs angewendet werden.

Bemerkung: Innerhalb von Aktionj und Aktion n können geschachtelte IF-Anweisungen nicht frei verwendet werden. Die Schachtelungstiefe kann nur erhöht, nicht aber erniedrigt werden. Ein Punkt schließt die ganze IF-Konstruktion ab. Deshalb müssen Aktionen, die über die erlaubten Anweisungsfolgen hinausgehen, mittels PERFORM ausgelagert werden.

IF b THEN Anweisung.

238

A. Anhänge

Aktion Aktion,'n

FehlerAktion

GO TO Fal^ , . . . , Fall n DEPENDING ON Fall. Fehler-Aktion. GO TO ENDE-Fallunterscheidung. Fall i. Aktion^ GO TO ENDE-Fallunterscheidung.

Aktion n . GO TO ENDE-Fallunterscheidung. ENDE-Fallunterscheidung. Bemerkung: Wenn der Wert von Fall außerhalb des Bereichs 1 bis n liegt, wird die unmittelbar hinter dem GO TO DEPENDING ON stehende Fehler-Aktion ausgeführt.

A.2 Realisierung der Strukturblöcke in FORTRAN Die sprachlichen Strukturierungsmittel sind CALL und SUBROUTINE logisches IF (b), IF (.NOT.b) GO TO und CONTINUE COMPUTED GO TO Vergleiche auch [TENN74],

A. 2 Realisierung der Strukturblöcke in FORTRAN

239

Wiederhole solange Bedingung b Aktion j

Aktion n 10 IF (.NOT.b) GO TO 11 Aktion! Aktion n GO TO 10 11 CONTINUE Bemerkungen: (1) Es wird empfohlen, für die beiden zur Realisierung der Wiederholungsschleife nötigen Labels eine Konvention ähnlich der obigen (n*10 und n*10+l) zu wählen. (2) Die FORTRAN-Zählschleife (DO-LOOP) ist bei den meisten Compilern (u. a. IBM) keine äquivalente Schleife, da die Abfrage am Ende durchgeführt wird. Wiederhole solange Bedingung b l Aktion! Wiederhole solange Bedingung b 2 Aktion 2 Aktion 3 Innere Wiederholungsschleife durch Einrücken absetzen: 10 IF (.NOT.b,)GO TO 11 Aktion j 20 IF(.NOT.b 2 ) GOTO 21 Aktion 2 GO TO 20 21 CONTINUE Aktion 3 GO TO 10 11 CONTINUE

240

A. Anhänge

Wiederhole Aktionsfolge j BREAK wenn Bedingung b Aktionsfolge 2

DO 10 1 = 1,10000 Aktionsfolge! IF (b) GO TO 11 Aktionsfolge 2 10 CONTINUE WRITE (6,1000) 1000 FORMAT ('0 SCHLEIFE') STOP 11 CONTINUE Bemerkung: Die DO-Anweisung begrenzt die Zahl der Iterationen und schützt vor einem „in einer Schleife" hängenden Programm, sofern die Bedingung b nicht eintritt.

Wiederhole solange Bedingung b j Aktionsfolge j BREAK wenn Bedingung b 2 Aktionsfolge 2

l O I F C N O T . b ! ) GO TO 11 Aktionsfolge j IF (b 2 ) GO TO 11 Aktionsfolge 2 GO TO 10 11 CONTINUE

A. 2 Realisierung der Strukturblöcke in FORTRAN

IF (.NOT.b) GO TO 10 Aktiorij GO TO 11 10 CONTINUE | oder 10 Aktion n Aktion n 11 CONTINUE

IF (b) Anweisung Bemerkungen (1) „Anweisung" kann auch ein Subroutinen-Aufruf sein: IF (b) CALL subroutine (2) „Anweisung" darf nur eine verarbeitende Anweisung, d. h. kein weiteres IF (und erst recht kein GO TO!) sein. (Als Platzhalter für den Code der Subroutine ist ein CALL als verarbeitende Anweisung anzusehen).

241

242

A. Anhänge

GO TO (101,102,. . . , 10n), i 101 Aktion! GO TO 199 102 Aktion 2 GO TO 199 lOn Aktion n GO TO 199 199 CONTINUE

Bemerkungen: (1) Es wird eine Label-Standardisierung entsprechend dem Beispiel empfohlen. (2) Das letzte GO TO vor dem abschließenden CONTINUE ist zwar redundant, im Interesse der Wartungssicherheit aber trotzdem wünschenswert.

A.3 Realisierung der Strukturblöcke in PL/1 Die sprachlichen Strukturierungsmittel sind DO; BEGIN; END; DO WHILE (b); END; GO TO ENDLABEL; ENDLABEL:, (Leeranweisung entspricht CONTINUE in FORTRAN) IF b THEN a (ELSE c): GO TO label (i); Vergleiche auch [WEIN73].

A. 3 Realisierung del Strukturblöcke in PL/1

BEGIN (lokale Variablen-Deklarationen) Aktion t

Aktion n END

BEGIN; DCL lokale Variablen; Aktion!; Aktion,,; END;

Wiederhole solange Bedingung b Aktion!

Aktion,, DO WHILE (b); Aktion j ; Aktion n ; END;

243

244

A. Anhänge

Wiederhole solange Bedingung b j Aktion! Wiederhole solange Bedingung b 2 Aktion 2 Aktion 3

DO WHILE ( b t ) ; Aktion!; DO WHILE ( b 2 ) ; Aktion 2 ; END; Aktion 3 ; END;

Bemeikung: Die Reichweite jeder DO-Gruppe sollte durch Einrücken um 2 bis 3 Stellen kenntlich gemacht werden. Wiederhole Aktionsfolge! BREAK wenn Bedingung b Aktionsfolge 2

DO WHILE ( T B ) ; Aktionsfolge j ; IF b THEN GO TO ENDDOx; Aktionsfolge 2 ; END; ENDDOx:;

A. 3 Realisierung der Strukturblöcke in PL/1

245

Wiederhole solange Bedingung b t Aktionsfolge j BREAK wenn Bedingung 2 Aktionsfolge 2

DO WHILE ( b j ) ; Aktionsfolge i ; IF b 2 THEN GO TO ENDDOx; Aktionsfolge 2 ; END; ENDDOx:;

IF b THEN Aktiorij; ELSE Aktion n ; Wenn Aktionj und Aktion n aus mehreren Anweisungen bestehen (komplexere Strukturblöcke): IF b THEN DO; Aktionj; END; ELSE DO; Aktion n ; END;

A. Anhänge

246

Bedingung b ? Nein

Ja Anweisung (kein NEIN-Zweig) IF b THEN Anweisung;

v Fall i

Aktion!

Aktion n GO TO label (i); label(l):

label (n):

Aktion j ; GO TO ENDCASEx;

Aktion,,; GO TO ENDCASEx; ENDCASEx:;

A. 3 Realisierung der Strukturblöcke in Pl/1

Symbolische Benennung der Bedingung und der Labels: DCL FAMILIENSTAND BIN FIXED, LEDIG BIN FIXED INIT(l), VERH BIN FIXED INIT(2), GESCH BIN FIXED INIT(3), LAB(3) LABEL INIT (L-LEDIG, L.VERH, L.GESCH); FAMILIENSTAND = VERH; GO TO LAB (FAMILIENSTAND); LLEDIG: LVERH: L-GESCH: •• • ENDCASE:;

247

248

A. Anhänge

A.4 PL/1-Version des Beispielprogramms „Häufigkeitszählung von Worten" HAEUFIGKEITSZAEHLUNG : PROCEDURE OPTIONS (MAIN); DCL

CARDLENGTH WORDLENGTH LISTLENGTH

BIN FIXED BIN FIXED BIN FIXED

DCL 1 LIST(/*LISTLENGTH »/ 200), 2 WORD 2 COUNTER

INIT (80), INIT (20), INIT (200);

CHAR (/»WORDLENGTH*/ 20) BIN FIXED; /«WERTE : 1 BIS . . . */

DCL

ACTUALWORP CARD NEXTCHAR

CHAR (/*WORDLENGTH*/ 20), CHAR (/»CARDLENGTH*/ 80), CHAR (1);

DCL

INPUT

FILE;

DCL

CARDEND CHAR (1) INIT ('#'), /»MELDUNG AUS GETCHAR*/ EOF.WORD CHAR (4) INIT C@@@@'); /»MELDUNG AUS GETJNPUT*/

DCL

TRUE BIT (1) FALSE BIT (1)

INIT('l'B), INIT ('0'B);

DCL

ENTRYCOUNTER LISTPOS CARDPOS WORDPOS

BIN BIN BIN BIN

FIXED, FIXED, FIXED, FIXED;

/»WERTE /»WERTE /»WERTE /»WERTE

^**** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

: 0 BIS : 1 BIS : 1 BIS : 1 BIS

LISTLENGTH*/ LISTLENGTH+1 */ CARDLENGTH+1 */ ___ */

*************************^

GETJNPUT : PROCEDURE; ON ENDFILE (INPUT) CARD = EOF_WORD; READ FILE (INPUT) INTO (CARD); IF CARD = EOF.WORD THEN PUT SKIP LIST (CARD) ; END GETJNPUT; GETCHAR: PROCEDURE; IF CARDPOS > CARDLENGTH THEN DO; NEXTCHAR = CARDEND; CALL GETJNPUT; CARDPOS = 1 ; END; ELSE DO; NEXTCHAR = SUBSTR (CARD, CARDPOS, 1); CARDPOS = CARDPOS + 1; END; END GETCHAR; y******** ****************************

***********************************^

A.4 PL/l-Version des Beispielprogramms „Häufigkeitszählung von Worten"

249

WORT AUFBEREITEN : PROCEDURE; DO

WHILE (NEXTCHAR = ' ' INEXTCHAR = CALL GETCHAR; END;

I NEXTCHAR = CARDEND);

ACTUALWORD = NEXTCHAR; WORDPOS = 1; CALL GETCHAR; DO WHILE H(NEXTCHAR = " I NEXTCHAR = '.' I NEXTCHAR = CARDEND)); WORDPOS = WORDPOS + 1 ; IF WORDPOS