Die Reihe Xpert.press vermittelt Professionals in den Bereichen Softwareentwicklung, Internettechnologie und IT-Management aktuell und kompetent relevantes Fachwissen über Technologien und Produkte zur Entwicklung und Anwendung moderner Informationstechnologien.
Bernhard Rumpe
Agile Modellierung mit UML Codegenerierung, Testfälle, Refactoring
Mit 160 Abbildungen und 30 Tabellen
123
Bernhard Rumpe Institut für Software Systems Engineering Technische Universität Braunschweig Mühlenpfordtstr. 23 38106 Braunschweig e-mail:
[email protected]
Bibliografische Information der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
ISSN 1439-5428 ISBN 3-540-20905-0 Springer Berlin Heidelberg New York
Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere d ie der Übersetzung, des Nachdr ucks , des Vortrags, der Entnahme von Abbildungen und Tabellen, der Funksendung, der Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherung in Datenverarbeitungsanlagen bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Vervielfältigung dieses Werkes oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzen der gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9. September 1965 in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig. Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechtsgesetzes. Springer ist ein Unternehmen von Springer Science+Business Media springer.de © Springer-Verlag Berlin Heidelberg 2005 Printed in Germany Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutzgesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften. Text und Abbildungen wurden mit größter Sorgfalt erarbeitet. Verlag und Autor können jedoch für eventuell verbliebene fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Satz: Druckfertige Daten des Autors Herstellung: LE-TeX Jelonek, Schmidt & Vöckler GbR, Leipzig Umschlaggestaltung: KünkelLopka Werbeagentur, Heidelberg Gedruckt auf säurefreiem Papier 33/3142/YL - 5 4 3 2 1 0
Vorwort
Softwaresysteme sind heutzutage in der Regel komplexe Produkte, fur ¨ deren erfolgreiche Produktion der Einsatz ingenieurm¨aßiger Techniken unerl¨asslich ist. Diese nun mittlerweile mehr als 30 Jahre alte und h¨aufig zitierte Erkenntnis hat dazu gefuhrt, ¨ dass in den letzten drei Jahrzehnten innerhalb der Informatik im Gebiet des Software Engineering intensiv an Sprachen, Methoden und Werkzeugen zur Unterstutzung ¨ des Softwareerstellungsprozesses gearbeitet wird. Trotz großer Fortschritte hierbei muss allerdings festgestellt werden, dass im Vergleich zu anderen, durchg¨angig viel a¨ lteren Ingenieursdisziplinen noch viele Fragen unbeantwortet sind und immer neue Fragestellungen auftauchen. So macht auch ein oberfl¨achlicher Vergleich zum Beispiel mit dem Gebiet des Bauwesens schnell deutlich, dass dort internatonale Standards eingesetzt werden, um Modelle von Geb¨auden zu erstellen, die Modelle zu analysieren und anschließend die Modelle in Bauten zu realisieren. Hierbei sind dann auch die Rollen- und Aufgabenverteilungen allgemein akzeptiert, so dass etwa Berufsgruppen wie Architekten, Statiker sowie Spezialisten fur ¨ den Tiefund Hochbau existieren. Eine derartige modellbasierte Vorgehensweise wird zunehmend auch in der Softwareentwicklung favorisiert. Dies bedeutet insbesondere, dass in den letzten Jahren international versucht wird, eine allgemein akzeptierte Modellierungssprache festzulegen, so dass etwa wie im Bauwesen, von einem Software-Architekten erstellte Modelle von einem Software-Statiker“ ” analysiert werden konnen, ¨ bevor sie von Spezialisten fur ¨ die Realisierung, also Programmierern in ausfuhrbare ¨ Programme umgesetzt werden.
VI
Vorwort
Diese standardisierte Modellierungssprache ist die Unified Modeling Language, die in einem schrittweisen Prozess durch ein international besetztes Konsortium stetig weiterentwickelt wird. Aufgrund der vielf¨altigen Interessenlagen im Standardisierungsprozess ist mit der aktuellen Version 2.0 der UML eine Sprachfamilie entstanden, deren Umfang, semantische Fundierung und methodische Verwendung noch viele Fragen offen l¨asst. Diesem Problem hat sich Herr Rumpe in den letzten Jahren in seinen wissenschaftlichen und praktischen Arbeiten gewidmet, deren Ergebnisse er nun in zwei Buchern ¨ einer breiten Leserschaft zug¨anglich macht. Hierbei hat Herr Rumpe das methodische Vorgehen in den Vordergrund gestellt. Im Einklang mit der heutigen Erkenntnis, dass leichtgewichtige, agile Entwicklungsprozesse insbesondere in kleineren und mittleren Entwicklungsprojekten große Vorteile bieten, hat Herr Rumpe Techniken fur ¨ einen agilen Entwicklungsprozess entwickelt. Auf dieser Basis hat er dann eine geeignete Modellierungssprache definiert, in dem er ein so genanntes Sprachprofil fur ¨ die UML definiert hat. In diesem Sprachprofil UML/P hat er die UML geeignet abgespeckt und an einigen Stellen so abgerundet, dass nun eine handhabbare Version der UML insbesondere fur ¨ einen agilen Entwicklungsprozess vorliegt. Herr Rumpe hat diese Sprache UML/P ausfuhrlich ¨ in dem diesem Buch vorangehenden Buch Modellierung mit UML“ erl¨autert. Das Buch bietet ” eine wesentliche Grundlage fur ¨ das hier vorliegende Buch, deren Inhalt allerdings auch in diesem Buch noch einmal kurz zusammengefasst wird. Das hier vorliegende Buch mit dem Titel Agile Modellierung mit UML“ widmet ” sich nun in erster Linie dem methodischen Umgang mit der UML/P. Hierbei behandelt Herr Rumpe drei Kernthemen einer modellbasierten Softwareentwicklung. Dies sind • • •
¨ die Codegenerierung, also der automatisierte Ubergang vom Modell zu einem ausfuhrbaren ¨ Programm, das systematische Testen von Programmen mithilfe einer modellbasierten, strukturierten Festlegung von Testf¨allen sowie die Weiterentwicklung von Modellen durch den Einsatz von Transformations- und Refactoring-Techniken.
Alle drei Kernthemen werden von Herrn Rumpe zun¨achst systematisch aufgearbeitet und die zugrunde liegenden Begriffe und Techniken werden eingefuhrt. ¨ Darauf aufbauend stellt er dann jeweils seinen Ansatz auf der Basis der Sprache UML/P vor. Diese Zweiteilung und klare Trennung zwischen Grundlagen und Anwendungen machen die Darstellung außerordentlich gut verst¨andlich und bieten dem Leser auch die Moglichkeit, ¨ diese Erkenntnisse unmittelbar auf andere modellbasierte Ans¨atze und Sprachen zu ubertragen. ¨
Vorwort
VII
Insgesamt hat dieses Buch einen großen Nutzen sowohl fur ¨ den Praktiker in der Softwareentwicklung, fur ¨ die akademische Ausbildung im Fachgebiet Softwaretechnik als auch fur ¨ die Forschung im Bereich der modellbasierten Entwicklung der Software. Der Praktiker lernt, wie er mit modernen modellbasierten Techniken die Produktion von Code verbessern und damit die Qualit¨at erheblich steigern kann. Dem Studierenden werden sowohl wichtige wissenschaftliche Grundlagen als auch unmittelbare Anwendungen der dargestellten grundlegenden Techniken vermittelt. Dem Wissenschaftler bie¨ tet das Buch einen umfassenden Uberblick uber ¨ den heutigen Stand der Forschung in den drei Kernthemen des Buchs. Das Buch stellt somit einen wichtigen Meilenstein in der Entwicklung von Konzepten und Techniken fur ¨ eine modellbasierte und ingenieurm¨aßige Softwareentwicklung dar und bietet somit auch die Grundlage fur ¨ weitere Arbeiten in der Zukunft. So werden praktische Erfahrungen mit dem Umgang der Konzepte ihre Tragbarkeit validieren. Wissenschaftliche, konzeptionelle Arbeiten werden insbesondere das Thema der Modelltransformation etwa auf der Basis von Graphtransformationen genauer erforschen und daruber ¨ hinaus das Gebiet der Modellanalyse im Sinne einer Modellstatik vertiefen. Ein derartiges vertieftes Verst¨andnis der Informatik-Methoden im Bereich der modellbasierten Softwareentwicklung ist eine entscheidende Voraussetzung fur ¨ eine erfolgreiche Kopplung mit anderen ingenieurm¨aßigen Methoden etwa im Bereich von eingebetteten Systemen oder im Bereich von intelligenten, benutzungsfreundlichen Produkten. Die Dom¨anenunabh¨angigkeit der Sprache UML/P bietet auch hier noch viele Moglichkeiten. ¨
Gregor Engels Paderborn im September 2004
Inhaltsverzeichnis
1
Einfuhrung ¨ ................................................. 1.1 Ziele und Inhalte von Band 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Erg¨anzende Ziele dieses Buchs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ¨ 1.3 Uberblick ................................................ 1.4 Notationelle Konventionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1 2 4 6 7
2
¨ Kompakte Ubersicht zur UML/P . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 Klassendiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Klassen und Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.2 Assoziationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.3 Repr¨asentation und Stereotypen . . . . . . . . . . . . . . . . . . . . . 2.2 Object Constraint Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ¨ 2.2.1 Ubersicht uber ¨ OCL/P . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.2 Die OCL-Logik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.3 Container-Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.4 Funktionen in OCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Objektdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 Einfuhrung ¨ in Objektdiagramme . . . . . . . . . . . . . . . . . . . . 2.3.2 Komposition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.3 Bedeutung eines Objektdiagramms . . . . . . . . . . . . . . . . . . 2.3.4 Logik der Objektdiagramme . . . . . . . . . . . . . . . . . . . . . . . . 2.4 Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Eigenschaften von Statecharts . . . . . . . . . . . . . . . . . . . . . . . 2.4.2 Darstellung von Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . 2.5 Sequenzdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9 10 10 11 14 15 15 18 19 26 28 29 30 31 32 33 33 37 44
3
Prinzipien der Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1 Konzepte der Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Konstruktive Interpretation von Modellen . . . . . . . . . . . . 3.1.2 Tests versus Implementierung . . . . . . . . . . . . . . . . . . . . . . . 3.1.3 Tests und Implementierung aus dem gleichen Modell .
49 52 54 56 59
X
Inhaltsverzeichnis
3.2 Techniken der Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Plattformabh¨angige Codegenerierung . . . . . . . . . . . . . . . 3.2.2 Funktionalit¨at und Flexibilit¨at . . . . . . . . . . . . . . . . . . . . . . . 3.2.3 Steuerung der Codegenerierung . . . . . . . . . . . . . . . . . . . . . 3.3 Semantik der Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4 Parametrisierung eines Werkzeugs . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Implementierung von Werkzeugen . . . . . . . . . . . . . . . . . . 3.4.2 Darstellung von Skripttransformationen . . . . . . . . . . . . . 4
60 60 63 67 68 71 71 73
Transformationen fur ¨ die Codegenerierung . . . . . . . . . . . . . . . . . . . . 77 ¨ 4.1 Ubersetzung von Klassendiagrammen . . . . . . . . . . . . . . . . . . . . . . 78 4.1.1 Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 4.1.2 Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 4.1.3 Assoziationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.1.4 Qualifizierte Assoziation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 4.1.5 Komposition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 4.1.6 Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 4.1.7 Objekterzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 ¨ 4.2 Ubersetzung von Objektdiagrammen . . . . . . . . . . . . . . . . . . . . . . . 102 4.2.1 Konstruktiv eingesetzte Objektdiagramme . . . . . . . . . . . 102 4.2.2 Beispiel einer konstruktiven Codegenerierung . . . . . . . . 104 4.2.3 Als Pr¨adikate eingesetzte Objektdiagramme . . . . . . . . . . 106 4.2.4 Objektdiagramm beschreibt Strukturmodifikation . . . . 109 4.2.5 Objektdiagramme und OCL . . . . . . . . . . . . . . . . . . . . . . . . . 112 4.3 Codegenerierung aus OCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 4.3.1 OCL-Aussage als Pr¨adikat . . . . . . . . . . . . . . . . . . . . . . . . . . 113 4.3.2 OCL-Logik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 4.3.3 OCL-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 4.3.4 Typen als Extension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 4.3.5 Navigation und Flattening . . . . . . . . . . . . . . . . . . . . . . . . . . 120 4.3.6 Quantoren und Spezialoperatoren . . . . . . . . . . . . . . . . . . . 121 4.3.7 Methodenspezifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 4.3.8 Vererbung von Methodenspezifikationen . . . . . . . . . . . . . 125 4.4 Ausfuhrung ¨ von Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 4.4.1 Methoden-Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 4.4.2 Umsetzung der Zust¨ande . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 4.4.3 Umsetzung der Transitionen . . . . . . . . . . . . . . . . . . . . . . . . 132 ¨ 4.5 Ubersetzung von Sequenzdiagrammen . . . . . . . . . . . . . . . . . . . . . 136 4.5.1 Sequenzdiagramm als Testtreiber . . . . . . . . . . . . . . . . . . . . 136 4.5.2 Sequenzdiagramm als Pr¨adikat . . . . . . . . . . . . . . . . . . . . . . 138 4.6 Zusammenfassung zur Codegenerierung . . . . . . . . . . . . . . . . . . . 140
Inhaltsverzeichnis
XI
5
Grundlagen des Testens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 5.1 Einfuhrung ¨ in die Testproblematik . . . . . . . . . . . . . . . . . . . . . . . . . 144 5.1.1 Testbegriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 5.1.2 Ziele der Testaktivit¨at . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 5.1.3 Fehlerkategorien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 5.1.4 Begriffsbestimmung fur ¨ Testverfahren . . . . . . . . . . . . . . . 150 5.1.5 Suche geeigneter Testdaten . . . . . . . . . . . . . . . . . . . . . . . . . . 152 5.1.6 Sprachspezifische Fehlerquellen . . . . . . . . . . . . . . . . . . . . . 153 5.1.7 UML/P als Test- und Implementierungssprache . . . . . . 155 5.1.8 Eine Notation fur ¨ die Testfalldefinition . . . . . . . . . . . . . . . 158 5.2 Definition von Testf¨allen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161 5.2.1 Operative Umsetzung eines Testfalls . . . . . . . . . . . . . . . . . 161 5.2.2 Vergleich der Testergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . 163 5.2.3 Werkzeuge JUnit und VUnit . . . . . . . . . . . . . . . . . . . . . . . . . 166
6
Modellbasierte Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 6.1 Testdaten und Sollergebnis mit Objektdiagrammen . . . . . . . . . . 172 6.2 Invarianten als Codeinstrumentierungen . . . . . . . . . . . . . . . . . . . 175 6.3 Methodenspezifikationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 6.3.1 Methodenspezifikationen als Codeinstrumentierung . . 177 6.3.2 Methodenspezifikationen zur Testfallbestimmung . . . . 178 6.3.3 Testfalldefinition mit Methodenspezifikationen . . . . . . . 181 6.4 Sequenzdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 6.4.1 Trigger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 6.4.2 Vollst¨andigkeit und Matching . . . . . . . . . . . . . . . . . . . . . . . 185 6.4.3 Nicht-kausale Sequenzdiagramme . . . . . . . . . . . . . . . . . . . 186 6.4.4 Mehrere Sequenzdiagramme in einem Test . . . . . . . . . . . 186 6.4.5 Mehrere Trigger im Sequenzdiagramm . . . . . . . . . . . . . . . 187 6.4.6 Interaktionsmuster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 6.5 Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 6.5.1 Ausfuhrbare ¨ Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190 6.5.2 Statechart als Ablaufbeschreibung . . . . . . . . . . . . . . . . . . . 192 6.5.3 Testverfahren fur ¨ Statecharts . . . . . . . . . . . . . . . . . . . . . . . . 194 ¨ 6.5.4 Uberdeckungsmetriken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 6.5.5 Transitionstests statt Testsequenzen . . . . . . . . . . . . . . . . . . 199 6.5.6 Weiterfuhrende ¨ Ans¨atze . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 6.6 Zusammenfassung und offene Punkte beim Testen . . . . . . . . . . 201
7
Testmuster im Einsatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 7.1 Dummies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 7.1.1 Dummies fur ¨ Schichten der Architektur . . . . . . . . . . . . . . 211 7.1.2 Dummies mit Ged¨achtnis . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 7.1.3 Sequenzdiagramm statt Ged¨achtnis . . . . . . . . . . . . . . . . . . 214 7.1.4 Abfangen von Seiteneffekten . . . . . . . . . . . . . . . . . . . . . . . . 215 7.2 Testbare Programme gestalten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
XII
Inhaltsverzeichnis
7.3
7.4
7.5
7.6
7.2.1 Statische Variablen und Methoden . . . . . . . . . . . . . . . . . . . 7.2.2 Seiteneffekte in Konstruktoren . . . . . . . . . . . . . . . . . . . . . . 7.2.3 Objekterzeugung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2.4 Vorgefertigte Frameworks und Komponenten . . . . . . . . Behandlung der Zeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.3.1 Simulation der Zeit im Dummy . . . . . . . . . . . . . . . . . . . . . 7.3.2 Variable Zeiteinstellung im Sequenzdiagramm . . . . . . . 7.3.3 Muster zur Simulation von Zeit . . . . . . . . . . . . . . . . . . . . . 7.3.4 Timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nebenl¨aufigkeit mit Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.4.1 Eigenes Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.4.2 Sequenzdiagramm als Scheduling-Modell . . . . . . . . . . . . 7.4.3 Behandlung von Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.4.4 Muster fur ¨ die Behandlung von Threads . . . . . . . . . . . . . 7.4.5 Probleme der erzwungenen Sequentialisierung . . . . . . . Verteilung und Kommunikation . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.5.1 Simulation der Verteilung . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.5.2 Simulation von Singletons . . . . . . . . . . . . . . . . . . . . . . . . . . 7.5.3 OCL-Bedingungen uber ¨ mehrere Lokationen . . . . . . . . . 7.5.4 Kommunikation simuliert verteilter Prozesse . . . . . . . . . 7.5.5 Muster fur ¨ Verteilung und Kommunikation . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
216 219 219 220 222 224 224 227 228 229 230 231 232 234 235 237 237 239 241 242 244 245
8
Refactoring als Modelltransformation . . . . . . . . . . . . . . . . . . . . . . . . . 247 8.1 Einfuhrende ¨ Beispiele fur ¨ Transformationen . . . . . . . . . . . . . . . . 248 8.2 Methodik des Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 8.2.1 Technische und methodische Voraussetzungen fur ¨ Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 8.2.2 Qualit¨at des Designs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 8.2.3 Refactoring, Evolution und Wiederverwendung . . . . . . 256 8.3 Theorie der Modelltransformationen . . . . . . . . . . . . . . . . . . . . . . . 258 8.3.1 Modelltransformationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258 8.3.2 Semantik einer Modelltransformation . . . . . . . . . . . . . . . . 259 8.3.3 Beobachtungsbegriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 8.3.4 Transformationsregeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270 8.3.5 Korrektheit von Transformationsregeln . . . . . . . . . . . . . . 272 8.3.6 Ans¨atze der transformationellen Softwareentwicklung 274
9
Refactoring von Modellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 9.1 Quellen fur ¨ UML/P-Refactoring-Regeln . . . . . . . . . . . . . . . . . . . . 278 9.1.1 Definition und Darstellung von Refactoring-Regeln . . . 280 9.1.2 Refactoring in Java/P . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282 9.1.3 Refactoring von Klassendiagrammen . . . . . . . . . . . . . . . . 288 9.1.4 Refactoring in der OCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294 9.1.5 Einfuhrung ¨ von Testmustern als Refactoring . . . . . . . . . . 296
Inhaltsverzeichnis
9.2 Additive Methode fur ¨ Datenstrukturwechsel . . . . . . . . . . . . . . . . 9.2.1 Vorgehensweise fur ¨ den Datenstrukturwechsel . . . . . . . 9.2.2 Beispiel: Darstellung von Geldbetr¨agen . . . . . . . . . . . . . . 9.2.3 Beispiel: Einfuhrung ¨ des Chairs im Auktionssystem . . . 9.3 Zusammenfassung der Refactoring-Techniken . . . . . . . . . . . . . .
XIII
299 299 302 306 314
10 Zusammenfassung und Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317 Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321 Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
1 Einfuhrung ¨
Der wahre Zweck eines Buches ist, den Geist hinterrucks ¨ zum eigenen Denken zu verleiten. Christopher Darlington Morley
Jungste ¨ Beispiele zeigen eindrucksvoll, wie teuer falsche oder fehlerhafte Software beim Absturz einer Rakete oder bei Maut-Projekten werden konnen. ¨ Um die stetig wachsende Komplexit¨at von Software-basierten Projekten und Produkten sowohl im Bereich betrieblicher Informations- als auch eingebetterer Systeme beherrschbar zu machen, wurde in den letzten Jahren ein wirkungsvolles Portfolio an Konzepten, Techniken und Methoden entwickelt, die die Softwaretechnik zu einer erwachsenen Ingeniersdisziplin heranreifen lassen. Das Portfolio ist zwar noch nicht vollst¨andig ausgereift, muss aber insbesondere in dem derzeitigen industriellen Softwareentwicklungsprozess sehr viel mehr noch etabliert werden. Die F¨ahigkeiten moderner Programmiersprachen, Klassenbibliotheken und vorhandener Softwareentwicklungswerkzeuge erlauben uns heute den Einsatz von Vorgehensweisen, die noch vor kurzer Zeit nicht realisierbar schienen. Als Teil dieses Portfolios behandelt dieses Buch eine auf der UML basierende Methodik, bei der vor allem Techniken zum praktischen Einsatz der UML im Vordergrund stehen. Als wichtigste Techniken werden dabei • • •
Generierung von Code aus Modellen, Modellierung von Testf¨allen und Weiterentwicklung durch Refactoring von Modellen
erkannt und in diesem Buch studiert. Dabei basiert dieser Band 2 sehr stark auf dem ersten Band Modellie” rung mit UML. Sprache, Konzepte und Methodik.“ [Rum04c], in dem das Sprachprofil UML/P detailliert erkl¨art ist. Es ist daher zu empfehelen, bei der Lekture ¨ dieses Bands [Rum04b] den ersten Band [Rum04c] griffbereit zu
2
1 Einfuhrung ¨
halten, obwohl Teile von Band 1 in kompakter Form in Kapitel 2 wiederholt werden.
1.1 Ziele und Inhalte von Band 1 Gemeinsames Mission Statement beider B¨ande: Es ist ein Kernziel, fur ¨ das genannte Portfolio grundlegende Techniken zur modellbasierten Entwicklung zur Verfugung ¨ zu stellen. Dabei wird in Band 1 eine Variante der UML vorgestellt, die speziell zur effizienten Entwicklung qualitativ hochwertiger Software und Software-basierter Systeme geeignet ist. Darauf aufbauend enth¨alt dieser Band Techniken zur Generierung von Code, von Testf¨allen und zum Refactoring der UML/P. UML-Standard: Der UML 2.0-Standard muss sehr viele Anforderungen aus unterschiedlichen Gegebenheiten heraus erfullen ¨ und ist daher notwendigerweise uberladen. ¨ Viele Elemente des Standards sind fur ¨ unsere Zwecke nicht oder nicht in der gegebenen Form sinnvoll, w¨ahrend andere Sprachkonzepte ganz fehlen. Deshalb wird in diesem Buch ein angepasstes und mit UML/P bezeichnetes Sprachprofil der UML vorgestellt. UML/P wird dadurch fur ¨ die vorgeschlagenen Entwicklungstechniken im Entwurf, in der Implementierung und in der Wartung optimiert und so in agilen Entwicklungsmethoden besser einsetzbar. Band 1 konzentriert sich vor allem auf die Einfuhrung ¨ des Sprachprofils ¨ und einer allgemeinen Ubersicht zur vorgeschlagenen Methodik. Die UML/P ist als Ergebnis mehrerer Grundlagen- und Anwendungsprojekte entstanden. Insbesondere das in Anhang D, Band 1 dargestellte Anwendungsbeispiel wurde soweit moglich ¨ unter Verwendung der hier beschriebenen Prinzipien entwickelt. Das Auktionssystem ist auch deshalb zur Demonstration der in den beiden Buchern ¨ entwickelten Techniken geeignet ideal, weil Ver¨anderungen des Gesch¨aftsmodells oder der Unternehmensumgebung in dieser Anwendungsdom¨ane besonders h¨aufig sind. Flexible und dennoch qualitativ hochwertige Softwareentwicklung ist fur ¨ diesen Bereich essentiell. Objektorientierung und Java: Fur ¨ neue Gesch¨aftsanwendungen wird heute prim¨ar Objekttechnologie eingesetzt. Die Existenz vielseitiger Klassenbibliotheken und Frameworks, die vorhandenen Werkzeuge und nicht zuletzt der weitgehend gelungene Sprachentwurf begrunden ¨ den Erfolg der Programmiersprache Java. Das UML-Sprachprofil UML/P und die darauf aufbauenden Entwicklungstechniken werden daher auf Java zugeschnitten. Brucke ¨ zwischen UML und agilen Methoden: Gleichzeitig bilden die beiden Bucher ¨ zwischen den eher als unvereinbar geltenden Ans¨atzen der agilen Methoden und der Modellierungssprache UML eine elegante Brucke. ¨ Agile Methoden und insbesondere Extreme Programming besitzen eine Reihe von interessanten Techniken und Prinzipien, die das Portfolio der Softwaretechnik fur ¨ bestimmte Projekttypen bereichern. Merkmale dieser Tech-
1.1 Ziele und Inhalte von Band 1
3
niken sind der weitgehende Verzicht auf Dokumentation, die Konzentration auf Flexibilit¨at, Optimierung der Time-To-Market und Minimierung der verbrauchten Ressourcen bei gleichzeitiger Sicherung der geforderten Qualit¨at. Damit sind agile Methoden fur ¨ die Ziele dieses Buchs als Grundlage gut geeignet. Neue agile Vorgehensweise auf Basis der UML/P: Die UML wird als Notation fur ¨ eine Reihe von Aktivit¨aten, wie Gesch¨aftsfallmodellierung, Sollund Ist-Analyse sowie Grob- und Fein-Entwurf in verschiedenen Granularit¨atsstufen eingesetzt. Die Artefakte der UML stellen damit einen wesentlichen Grundstein fur ¨ die Planung und Kontrolle von Meilenstein-getriebenen Softwareentwicklungsprojekten dar. Deshalb wird die UML vor allem in plan-getriebenen Projekten mit relativ hoher Dokumentationsleistung und der daraus resultierenden Schwerf¨alligkeit eingesetzt. Nun ist die UML aber kompakter, semantisch reichhaltiger und besser geeignet, komplexe Sachverhalte darzustellen, als eine normale Programmiersprache. Sie bietet dadurch fur ¨ die Modellierung von Testf¨allen sowie fur ¨ die transformationelle Evolution von Softwaresystemen wesentliche Vorteile. Auf Basis einer Diskussion agiler Methoden und der darin enthaltenen Konzepte wird in Band 1 eine neue agile Methode skizziert, die das UML/P-Sprachprofil als Grundlage fur ¨ viele Aktivit¨aten nutzt, ohne die Schwerf¨alligkeit typischer UML-basierter Methoden zu importieren. Die beschriebenen Ziele wurden in Band 1 in folgenden Kapitel umgesetzt: 1 Einfuhrung ¨ 2 Agile und UML-basierte Methodik beschreibt die Vorgehensweise zum Einsatz der UML/P im Kontext vorhandener agiler Methoden. 3 Klassendiagramme fuhrt ¨ Form und Verwendung von Klassendiagrammen ein. 4 Object Constraint Language diskutiert eine syntaktisch auf Java angepasste, sprachlich erweiterte und semantisch konsolidierte Fassung der textuellen Beschreibunsgsprache OCL. 5 Objektdiagramme diskutiert Sprache und methodischen Einsatz der Objektdiagramme sowie deren Integration mit der OCL-Logik, um damit eine “Logik der Diagramme“ zu ermoglichen, ¨ in der unerwunschte ¨ Situationen, Alternativen und Kombinationen beschrieben werden konnen. ¨ 6 Statecharts beinhaltet neben der Einfuhrung ¨ der Statecharts eine Sammlung von Transformationen zur deren semantikerhaltender Vereinfachung. 7 Sequenzdiagramme beschreibt Form, Bedeutung und Verwendung von Sequenzdiagrammen.
4
1 Einfuhrung ¨
A Sprachdarstellung durch Syntaxklassendiagramme bietet eine Kombination aus Extended-Backus-Naur-Form (EBNF) und spezialisierten Klassendiagrammen zur Darstellung der abstrakten Syntax (Metamodell) der UML/P. B Java beschreibt die abstrakte Syntax des genutzten Teils von Java. C Syntax der UML/P beschreibt die abstrakte Syntax der UML/P. D Anwendungsbeispiel: Internetbasiertes Auktionssystem erl¨autert Hintergrundinformation zu dem in beiden B¨anden verwendeten Beispiel des Auktionssystems.
1.2 Erg¨anzende Ziele dieses Buchs Um die Effizienz in einem Projekt zu steigern, ist es notwendig, den Entwicklern effektive Notationen, Techniken und Methoden zur Verfugung ¨ zu stellen. Weil das prim¨are Ziel jeder Softwareentwicklung das lauff¨ahige und korrekt implementierte Produktionssystem ist, sollte der Einsatz der UML nicht nur zur Dokumentation von Entwurfen ¨ dienen. Stattdessen ist die automatisierte Umsetzung in Code durch Codegeneratoren, die Definition von Testf¨allen mit der UML/P zur Qualit¨atssicherung und die Evolution von UML-Modellen mit Refactoring-Techniken essentiell. Die Kombination von Codegenerierung, Testfallmodellierung und Refactoring bietet dabei wesentliche Synergie-Effekte, die zum Beispiel bei der Qualit¨atssicherung, bei der erhohten ¨ Wiederverwendung und der gesteigerten F¨ahigkeit zur Weiterentwicklung beitragen. Codegenerierung: Zur effizienten Erstellung eines Systems ist eine gut parametrisierte Codegenerierung aus abstrakten Modellen essentiell. Die diskutierte Form der Codegenerierung erlaubt die kompakte und von der Technik weitgehend unabh¨angige Entwicklung von Modellen. fur ¨ spezifische Dom¨anen und Anwendungen. Erst bei der Generierung werden technologieabh¨angige Aspekte wie zum Beispiel Datenbankanbindung, Kommunikation oder GUI-Darstellung hinzugefugt. ¨ Dadurch wird die UML/P als Programmiersprache einsetzbar, und es entsteht kein konzeptueller Bruch zwischen Modellierungs- und Programmiersprache. Allerdings ist es wichtig, ausfuhrbare ¨ und abstrakte Modelle im Softwareentwicklungsprozess explizit zu unterscheiden und jeweils ad¨aquat einzusetzen. Modellierung automatisierbarer Tests: Die systematische und effiziente Durchfuhrung ¨ von Tests ist ein wesentlicher Bestandteil zur Sicherung der Qualit¨at eines Systems. Ziel ist dabei, dass Tests nach ihrer Erstellung automatisiert ablaufen konnen. ¨ Codegenerierung wird daher nicht nur zur Entwicklung des Produktionssystems, sondern insbesondere auch fur ¨ Testf¨alle eingesetzt, um so die Konsistenz zwischen Spezifikation und Implementierung zu prufen. ¨ Der Einsatz der UML/P zur Testfallmodellierung ist daher
1.2 Erg¨anzende Ziele dieses Buchs
5
ein wesentlicher Bestandteil einer agilen Methodik. Dabei werden insbesondere Objektdiagramme, die OCL und Sequenzdiagramme zur Modellierung von Testf¨allen eingesetzt. Abbildung 1.1 skizziert die ersten beiden Ziele dieses Buchs.
Abbildung 1.1. Notationen der UML/P
¨ Evolution mit Refactoring: Die diskutierte Flexibilit¨at, auf Anderungen der Anforderungen oder der Technologie schnell zu reagieren, erfordert eine Technik zur systematischen Anpassung des bereits vorhandenen Modells beziehungsweise der Implementierung. Die Evolution eines Systems in Bezug auf neue Anforderungen oder einem neuen Einsatzgebiet sowie der Behebung von Strukturdefiziten der Softwarearchitektur erfolgt idealerweise durch Refactoring-Techniken. Ein weiterer Schwerpunkt ist daher die Fundierung und Einbettung der Refactoring-Techniken in die allgemeinere Vorgehensweise zur Modelltransformation und die Diskussion, welche Arten von Refactoring-Regeln fur ¨ die UML/P entwickelt oder von anderen Ans¨atzen ubernommen ¨ werden konnen. ¨ Besonders betrachtet werden dabei Klassendiagramme, Statecharts und OCL. Sowohl bei der Testfallmodellierung als auch bei den Refactoring-Techniken werden aus den fundierenden Theorien stammende Erkenntnisse dargestellt und auf die UML/P transferiert. Ziel des Buchs ist es, diese Konzepte anhand zahlreicher praktischer Beispiele zu erkl¨aren und in Form von Testmustern beziehungsweise Refactoring-Techniken fur ¨ UML-Diagramme aufzubereiten. Model Driven Architecture (MDA): Initiiert durch das Industriekonsortium OMG1 wird derzeit an der Intensivierung des werkzeugbasierten Einsatzes der UML in der Softwareentwicklung gearbeitet. Die als MDA be1
Die Object Management Group (OMG) ist fur ¨ die Definition der UML in Form einer Technical Recommendation“ zust¨andig. ”
6
1 Einfuhrung ¨
zeichnete Technik bietet im Bereich Codegenerierung a¨ hnliche Charakteristiken wie der hier diskutierte Ansatz. Jedoch fehlt eine geeignet angepasste Methodik. Der in diesem Buch vorgestellte Ansatz zum Refactoring bietet dazu eine ad¨aquate Erg¨anzung zur horizontalen“ Transformation. ” Abgrenzung: Mit den behandelten Techniken konzentriert sich dieses Buch vor allem auf die Unterstutzung ¨ der Entwurfs-, Implementierungsund Wartungsaktivit¨aten, allerdings ohne diese in allen Facetten abzudecken. Wichtig, aber unbehandelt sind auch Techniken und Notationen zur Erhebung und zum Management von Anforderungen, zur Projektplanung ¨ und -durchfuhrung, ¨ zur Kontrolle sowie zum Versions- und Anderungsmanagement. Stattdessen wird an geeigneten Stellen auf entsprechende weiterfuhrende ¨ Literatur verwiesen.
¨ 1.3 Uberblick ¨ Abbildung 1.2 erlaubt einen guten Uberblick uber ¨ den Matrix-artigen Aufbau weiter Teile beider B¨ande. W¨ahrend dieser Band sich verst¨arkt um methodische Fragestellungen kummert ¨ wird die UML/P im Band 1 erkl¨art.
¨ Abbildung 1.2. Ubersicht uber ¨ den Inhalt beider B¨ande
Das Kapitel 2 gibt eine kurze und kompakte Zusammenfassung von Band 1, [Rum04c]. Dabei wird vor allem das dort eingefuhrte ¨ Sprachprofil der UML/P sehr kompakt und daher unvollst¨andig vorgestellt. Die Kapitel 3 und 4 diskutieren prinzipielle und technische Problemstellungen zur Codegenerierung. Dies beinhaltet die Architektur und die Steuerung eines Codegenerators genauso wie die Frage, welche Teile der UML/P fur ¨ Test- beziehungsweise fur ¨ Produktionscode geeignet sind. Darauf aufbauend wird ein Mechanismus zur Beschreibung von Codegenerierung eingefuhrt. ¨ Erg¨anzend dazu wird anhand ausgew¨ahlter Teile der verschiedenen UML/P-Notationen gezeigt, wie daraus Test- und Produktionscode ge-
1.4 Notationelle Konventionen
7
neriert werden kann. Dabei werden sowohl Alternativen diskutiert als auch die Kombination von Transformationen demonstriert. Die Kapitel 5 und 6 erortern ¨ die aus der Literatur bekannten Begriffsbildungen fur ¨ Testverfahren und die beim Testen zu beachtenden Besonderheiten, die aufgrund der Nutzung der UML/P als Test- und Implementierungssprache entstehen. Es wird beschrieben, wie eine Architektur fur ¨ die automatisierte Testausfuhrung ¨ aussieht und wie UML/P-Diagramme eingesetzt werden, um Testf¨alle zu definieren. Kapitel 7 zeigt anhand von Testmustern die Verwendbarkeit der UML/PDiagramme zur Definition von Testf¨allen. Diese Testmuster enthalten Erfahrungswissen zur Definition von testbaren Programmen insbesondere fur ¨ funktionale Tests von verteilten und nebenl¨aufigen Softwaresystemen. Die Kapitel 8 und 9 diskutieren Techniken zur Transformation von Modellen und Code und stellen damit Refactoring als semantikerhaltende Transformation auf eine fundierte Basis. Fur ¨ Refactoring wird ein expliziter und praktisch verwendbarer Beobachtungsbegriff eingefuhrt ¨ und es wird diskutiert, wie vorhandene Refactoring-Techniken auf die UML/P ubertragen ¨ werden konnen. ¨ Schließlich wird eine additive Vorgehensweise vorgeschlagen, die großere ¨ Refactorings unter Verwendung von OCL-Invarianten unterstutzt. ¨
1.4 Notationelle Konventionen In diesem Buch werden mehrere Diagrammarten und textuelle Notationen genutzt. Damit sofort erkennbar ist, welches Diagramm oder welche textuelle Notation jeweils dargestellt ist, wird abweichend von der UML 2.0 rechts oben eine Marke in einer der in Abbildung 1.3 dargestellten Formen angegeben. Diese Form ist auch zur Markierung textueller Teile geeignet und flexibler als die UML 2.0-Markierung. Eine Marke wird einerseits als Orientierungshilfe und andererseits als Teil der UML/P eingesetzt, da ihr der Name des Diagramms und Diagramm-Eigenschaften in Form von Stereotypen beigefugt ¨ werden konnen. ¨ Vereinzelt kommen Spezialformen von Marken zum Einsatz, die weitgehend selbsterkl¨arend sind. Die textuellen Notationen wie Java-Code, OCL-Beschreibungen und textuelle Teile in Diagrammen basieren ausschließlich auf dem ASCII-Zeichensatz. Zur besseren Lesbarkeit werden einzelne Schlusselw ¨ orter ¨ hervorgehoben oder unterstrichen. Im Text werden folgende Sonderzeichen genutzt: • •
c “ sind formaler Teil der Die Repr¨asentationsindikatoren . . .“ und ” ” UML/P und beschreiben, ob die in einem Diagramm dargestellte Repr¨asentation vollst¨andig ist. Stereotypen werden in der Form Stereotypname angegeben. Merkmale haben die Form {Merkmalsname=Wert} oder {Merkmalsname}.
8
1 Einfuhrung ¨
Abbildung 1.3. Marken fur ¨ Diagramme und Textteile
Danksagung Wie auch an Band 1 haben an der Erstellung dieses Buchs eine Reihe von Personen direkt oder indirekt mitgewirkt. Neben einem Verweis auf die Danksagung von Band 1 mochte ¨ ich insbesondere meinen Kolleginnen und Kollegen an der Technischen Universit¨at Braunschweig fur ¨ die freundliche Aufnahme sowie meinen Mitarbeiterinnen und Mitarbeitern fur ¨ die hervorragende Unterstutzung ¨ bei der Fertigstellung dieses Buchs bedanken. Im Kontext des gerade in Aufbau befindlichen neuen Instituts fur ¨ Software Systems Engineering ist es durchaus ambitioniert zwei Bucher ¨ parallel zu einer Reihe neuer Vorlesungen, Seminaren und Praktika fertigzustellen.
2 ¨ Kompakte Ubersicht zur UML/P
Die Grenzen meiner Sprache sind die Grenzen meiner Welt. Ludwig Wittgenstein
Dieses Kapitel beinhaltet eine kompakte Zusammenfassung der in Band 1 [Rum04c] eingefuhrten ¨ UML/P. Diese Zusammenfassung beschreibt einige, aber nicht alle Besonderheiten und Abweichungen des UML/P-Profils vom UML 2.0 Standard. Fur ¨ eine genauere Lekture ¨ sei Band 1 empfohlen. Kenner der UML konnen ¨ dieses Kapitel bei Bedarf sp¨ater nachschlagen. Die verwendeten Beispiele zur Einfuhrung ¨ der Sprache beziehen sich im Wesentlichen auf die in Band 1 beschriebene Auktionssystem-Anwendung.
2.1 2.2 2.3 2.4 2.5
Klassendiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Object Constraint Language . . . . . . . . . . . . . . . . . . . . . . . . . . Objektdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sequenzdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10 15 28 33 44
10
¨ 2 Kompakte Ubersicht zur UML/P
2.1 Klassendiagramme Klassendiagramme bilden das architekturelle Ruckgrat ¨ vieler Systemmodellierungen. Dementsprechend haben Klassendiagramme und darin besonders Klassen eine Reihe von Aufgaben zu erfullen ¨ (Abbildung 2.1): Klassendiagramme beschreiben die Struktur beziehungsweise Architektur eines Systems, auf der nahezu alle anderen Beschreibungstechniken basieren. Das Konzept Klasse ist durchg¨angig in Modellierung und Programmierung eingesetzt und bietet daher ein Ruckgrat ¨ fur ¨ Traceability von Anforderungen und Fehlern entlang der verschiedenen Aktivit¨aten eines Projekts. Klassendiagramme bilden das Skelett fur ¨ fast alle weiteren Notationen und Diagrammarten, da diese sich jeweils auf die in Klassendiagrammen definierten Klassen und Methoden stutzen. ¨ So werden in der Analyse Klassendiagramme genutzt, um Konzepte der realen Welt zu strukturieren, w¨ahrend in Entwurf und Implementierung Klassendiagramme vor allem zur Darstellung einer strukturellen Sicht des Softwaresystems genutzt werden. Die Aufgaben einer Klasse sind: • • • • • • •
Kapselung von Attributen und Methoden zu einer konzeptuellen Einheit Auspr¨agung von Instanzen als Objekte Typisierung von Objekten Implementierungsbeschreibung Klassencode (die ubersetzte, ¨ ausfuhrbare ¨ Form der Implementierungsbeschreibung) Extension (Menge aller zu einem Zeitpunkt existierenden Objekte) Charakterisierung der mo¨ glichen Strukturen eines Systems Abbildung 2.1. Aufgabenvielfalt einer Klasse
2.1.1 Klassen und Vererbung Die Abbildung 2.2 enth¨alt eine Einordnung der wichtigsten Begriffe fur ¨ Klassendiagramme. In Abbildung 2.3 ist ein einfaches Klassendiagramm bestehend aus einer Klasse zu sehen. Je nach Grad der Detaillierung konnen ¨ Attribute und Methoden der Klassen weggelassen oder nur teilweise angegeben werden. Auch Typen von Attributen und Methoden, Sichtbarkeitsangaben und weitere Modifikatoren sind optional. Zur Strukturierung von Klassen in uberschaubare ¨ Hierarchien wird die Vererbungsbeziehung eingesetzt. Abbildung 2.4 demonstriert Vererbung und Interface-Implementierung anhand der Gemeinsamkeiten mehrerer Nachrichtenarten des in Kapitel D, Band 1 beschriebenen Auktionssystems. Die Erweiterung von Interfaces ist darin nicht abgebildet. Sie wird wie die Vererbung zwischen Klassen dargestellt.
2.1 Klassendiagramme
11
Klasse. Eine Klasse besteht aus einer Sammlung von Attributen und Methoden, die den Zustand und das Verhalten ihrer Instanzen (Objekte) festlegt. Klassen sind durch Assoziationen und Vererbungsbeziehungen miteinander verknupft. ¨ Ein Klassenname erlaubt es, die Klasse zu identifizieren. Attribut. Die Zustandskomponenten einer Klasse werden als Attribute bezeichnet. Sie beinhalten grunds¨atzlich Name und Typ. Methode. Die Funktionalit¨at einer Klasse ist in Methoden abgelegt. Eine Methode besteht aus einer Signatur und einem Rumpf, der die Implementierung beschreibt. Bei einer abstrakten Methode fehlt der Rumpf. Modifikator. Zur Festlegung von Sichtbarkeit, Instanziierbarkeit und Ver¨anderbarkeit des modifizierten Elements konnen ¨ die Modifikatoren public, protected, private, readonly, abstract, static und final auf Klassen, Methoden und Attribute angewandt werden. Fur ¨ die ersten vier genannten Modifikatoren gibt es in UML/P die graphischen Varianten +“, #“ und -“ ” ” ” und ?“. ” Konstanten sind als spezielle Attribute mit den Modifikatoren static und final definiert. Vererbung. Stehen zwei Klassen in Vererbungsbeziehung, so vererbt die Oberklasse ihre Attribute und Methoden an die Unterklasse. Die Unterklasse kann weitere Attribute und Methoden hinzufugen ¨ und Methoden redefinieren – soweit die Modifikatoren dies erlauben. Die Unterklasse bildet einen Subtyp der Oberklasse, der es nach dem Substitutionsprinzip erlaubt, Instanzen der Unterklasse dort einzusetzen, wo Instanzen der Oberklasse erforderlich sind. Interface. Ein Interface (Schnittstelle) beschreibt die Signaturen einer Sammlung von Methoden. Im Gegensatz zur Klasse werden keine Attribute (nur Konstanten) und keine Methodenrumpfe ¨ angegeben. Interfaces sind verwandt zu abstrakten Klassen und konnen ¨ untereinander ebenfalls in einer Vererbungsbeziehung stehen. Typ ist ein Grunddatentyp wie int, eine Klasse oder ein Interface. Interface-Implementierung ist eine der Vererbung a¨ hnliche Beziehung zwischen einem Interface und einer Klasse. Eine Klasse kann beliebig viele Interfaces implementieren. Assoziation ist eine bin¨are Beziehung zwischen Klassen, die zur Realisierung struktureller Information verwendet wird. Eine Assoziation besitzt einen Assoziationsnamen, fur ¨ jedes Ende einen Rollennamen, eine Kardinalit¨at und eine Angabe uber ¨ die Navigationsrichtungen. Kardinalit¨at. Die Kardinalit¨at (Multiplicity) wird fur ¨ jedes Assoziationsende angegeben. Sie ist von der Form 0..1“, 1“ oder *“ und beschreibt, ob eine Asso” ” ” ziation in dieser Richtung optional oder eindeutig ist beziehungsweise mehrfache Bindung erlaubt. Abbildung 2.2. Begriffsdefinition fur ¨ Klassendiagramme
2.1.2 Assoziationen Eine Assoziation dient dazu, Objekte zweier Klassen in Beziehung zu setzen. Mithilfe von Assoziationen konnen ¨ komplexe Datenstrukturen gebildet werden und Methoden benachbarter Objekte aufgerufen werden. Abbildung 2.5
12
¨ 2 Kompakte Ubersicht zur UML/P
Abbildung 2.3. Eine Klasse im Klassendiagramm
Abbildung 2.4. Vererbung und Interface-Implementierung
beschreibt einen Ausschnitt aus dem Auktionssystem mit mehreren Assoziationen in unterschiedlichen Formen. Eine Assoziation besitzt im Normalfall einen Assoziationsnamen und fur ¨ jedes der beiden Enden je eine Assoziationsrolle, eine Angabe der Kardinalit¨at und eine Beschreibung der moglichen ¨ Navigationsrichtungen. Einzelne Angaben konnen ¨ im Modell auch weggelassen werden, wenn sie zur Darstellung des gewunschten ¨ Sachverhalts keine Rolle spielen und die Eindeutigkeit nicht verloren geht. Assoziationen konnen ¨ grunds¨atzlich uni- oder bidirektional sein. Ist keine explizite Pfeilrichtung angegeben, so wird von einer bidirektionalen Assoziation ausgegangen. Formal werden die Navigationsmoglichkeiten ¨ in dieser Situation als unspezifiziert und damit nicht als eingeschr¨ankt betrachtet.
2.1 Klassendiagramme
13
Abbildung 2.5. Klassendiagramm mit Assoziationen
Eine besondere Form der Assoziation ist die Komposition. Sie wird durch eine ausgefullte ¨ Raute an einem Assoziationsende dargestellt. In einer Komposition sind die Teilobjekte in einer starken auch den Lebenszyklus betreffenden Abh¨angigkeit vom Ganzen. Der UML-Standard bietet eine Reihe zus¨atzlicher Merkmale fur ¨ Assoziationen an, die Assoziationseigenschaften genauer regeln. Abbildung 2.6 beinhaltet einige h¨aufig eingesetzte Merkmale, wie zum Beispiel {ordered}, das einen geordneten Zugriff mit Index erlaubt. Mit {frozen} wird angezeigt, dass nach der Initialisierung eines Auktionsobjekts die beiden Assoziationen zu den Policy-Objekten nicht mehr ver¨andert werden. Mit {addOnly} wird modelliert, dass in der Assoziation nur Objekte hinzugefugt ¨ werden durfen, ¨ das Entfernen jedoch verboten ist.
Abbildung 2.6. Merkmale fur ¨ Assoziationen
Qualifizierte Assoziation bieten die Moglichkeit, ¨ aus einer Menge von zugeordneten Objekten mithilfe des Qualifikators ein einzelnes Objekt zu selektieren. Abbildung 2.7 zeigt mehrere auf unterschiedliche Weise qualifizierte Assoziationen. W¨ahrend in explizit qualifizierten Assoziationen die Art des Qualifikators w¨ahlbar ist, stehen in geordneten Assoziationen ganzzahlige Intervalle beginnend mit der 0 zur Verfugung. ¨
14
¨ 2 Kompakte Ubersicht zur UML/P
Abbildung 2.7. Qualifizierte Assoziationen
2.1.3 Repr¨asentation und Stereotypen Klassendiagramme haben oft das Ziel, die fur ¨ eine bestimmte Aufgabe notwendige Datenstruktur einschließlich ihrer Zusammenh¨ange zu beschrei¨ ben. Eine vollst¨andige Liste aller Methoden und Attribute ist fur ¨ einen Uberblick dabei eher hinderlich. Ein Klassendiagramm stellt daher meist eine unvollst¨andige Sicht des Gesamtsystems dar. So konnen ¨ einzelne Klassen oder Assoziationen fehlen. Innerhalb der Klassen ko¨ nnen Attribute und Methoden weggelassen oder unvollst¨andig dargestellt werden.
Abbildung 2.8. Formen der Darstellung einer Klasse
Um einem UML-Diagramm anzusehen, ob die darin enthaltene Information vollst¨andig ist, werden in UML/P die Repr¨asentationsindikatoren
2.2 Object Constraint Language
15
c “ zur Darstellung . . .“ zur Markierung unvollst¨andiger Information und ” ” vollst¨andiger Information verwendet (Abbildung 2.8). c “ und . . .“ wirken nicht auf die Klasse selbst, sonDie Indikatoren ” ” c “ beim dern auf ihre Darstellung innerhalb des Klassendiagramms. Ein ” Klassennamen besagt, dass sowohl die Attribut- als auch die Methodenliste vollst¨andig ist. Demgegenuber ¨ bedeutet der Unvollst¨andigkeits-Indikator . . .“, dass die Darstellung unvollst¨andig sein kann. ” Die beiden Repr¨asentationsindikatoren sind nur eine spezielle From von Merkmalen mit eigener syntaktischer Darstellung. Die UML bietet sehr allgemein die Moglichkeit, ¨ Modellelemente mit Stereotypen und Merkmalen zu markieren (Abbildung 2.9), die deren allgemeine Syntax die Formen stereotyp, {merkmal} oder {merkmal=wert} hat. Stereotyp. Ein Stereotyp klassifiziert Modellelemente wie beispielsweise Klassen oder Attribute. Durch einen Stereotyp wird die Bedeutung des Modellelements spezialisiert und kann so beispielsweise bei der Codegenerierung spezifischer behandelt werden. Ein Stereotyp kann eine Menge von Merkmalen besitzen. Merkmal. Ein Merkmal beschreibt eine Eigenschaft eines Modellelements. Ein Merkmal wird notiert als Paar bestehend aus Schlusselwort ¨ und Wert. Mehrere solche Paare konnen ¨ zu einer Liste zusammengefasst werden. Modellelemente sind die (wesentlichen) Bausteine der UML-Diagramme. Beispielsweise hat das Klassendiagramm als Modellelemente Klassen, Interfaces, Attribute, Methoden, Vererbungsbeziehungen und Assoziationen. Merkmale und Stereotypen konnen ¨ auf Modellelemente angewandt werden, sind aber selbst keine Modellelemente. Abbildung 2.9. Begriffsdefinition Merkmal und Stereotyp
2.2 Object Constraint Language Die Object Constraint Language (OCL) ist eine eigenschaftsorientierte Modellierungssprache, die eingesetzt wird, um Invarianten sowie Vor- und Nachbedingungen von Methoden zu modellieren. Die in der UML/P enthaltene OCL/P ist eine syntaktsich an Java angepasste und erweiterte Variante des OCL-Standards. ¨ 2.2.1 Ubersicht uber ¨ OCL/P Abbildung 2.10 erl¨autert die wichtigsten Begriffe der OCL. Eines der herausragenden Merkmale von OCL-Bedingungen ist ihre grunds¨atzliche Einbettung in einen Kontext, bestehend aus UML-Modellen. Das Klassendiagramm in Abbildung 2.11 stellt einen solchen Kontext bereit, in dem zum Beispiel folgende Aussage formuliert werden kann:
16
¨ 2 Kompakte Ubersicht zur UML/P
Bedingung. Eine Bedingung ist eine boolesche Aussage uber ¨ ein System. Sie beschreibt eine Eigenschaft, die ein System oder ein Ergebnis besitzen soll. Ihre Interpretation ergibt grunds¨atzlich einen der Wahrheitswerte true oder false. Kontext. Eine Bedingung ist in einen Kontext eingebettet, uber ¨ den sie Aussagen macht. Der Kontext wird definiert durch eine Menge von in der Bedingung verwendbaren Namen und ihren Signaturen. Dazu geh¨oren Klassen-, Methodenund Attributnamen des Modells und insbesondere im Kontext einer Bedingung explizit eingefuhrte ¨ Variablen. Interpretation einer Bedingung wird anhand einer konkreten Objektstruktur vorgenommen. Dabei werden die im Kontext eingefuhrten ¨ Variablen mit Werten beziehungsweise Objekten belegt. Invariante beschreibt eine Eigenschaft, die in einem System zu jedem (beobachteten) Zeitpunkt gelten soll. Die Beobachtungszeitpunkte konnen ¨ eingeschr¨ankt sein, um zeitlich begrenzte Verletzungen zum Beispiel w¨ahrend der Ausfuhrung ¨ einer Methode zu erlauben. Vorbedingung einer Methode charakterisiert die Eigenschaften, die gelten mussen, ¨ damit diese Methode ein definiertes und sinnvolles Ergebnis liefert. Ist die Vorbedingung nicht erfullt, ¨ so wird uber ¨ das Ergebnis keine Aussage getroffen. Nachbedingung einer Methode beschreibt, welche Eigenschaften nach Ausfuhrung ¨ der Methode gelten. Dabei kann auf Objekte in dem Zustand zuruckgegriffen ¨ werden, der unmittelbar vor dem Methodenaufruf (zur Zeit“ der Interpreta” tion der Vorbedingung) gultig ¨ war. Nachbedingungen werden anhand zweier Objektstrukturen interpretiert, die die Situationen vor und nach dem Methodenaufruf darstellen. Methodenspezifikation ist ein Paar bestehend aus Vor- und Nachbedingung. Query ist eine von der Implementierung angebotene Methode, deren Aufruf keine Ver¨anderung des Systemzustands hervorruft. Es durfen ¨ neue Objekte als Aufrufergebnis erzeugt werden. Allerdings durfen ¨ diese nicht mit dem Systemzustand durch Links verbunden sein. Dadurch sind Queries ohne Seiteneffekte und konnen ¨ in OCL-Bedingungen verwendet werden. Abbildung 2.10. Begriffsdefinitionen zur OCL
Abbildung 2.11. Ausschnitt des Auktionssystems
OCL
context Auction a inv Bidders2: a.activeParticipants == { p in a.bidder | p.isActive }.size
2.2 Object Constraint Language
17
Die Aussage mit dem Namen Bidders2 wird quantifiziert uber ¨ alle Objekte a der Klasse Auktion. Darin enthalten sind Navigationsausdrucke ¨ wie a.bidder, die es erlauben uber ¨ Assoziationen zu navigieren. Eine Mengenkomprehension {p in ...| ...} selektiert alle aktiven Personen einer Auktion. Diese Mengenkomprehension ist eine komfortable Erweiterung gegenuber ¨ dem OCL-Standard. Sie wird sp¨ater in mehreren Formen ausfuhrli¨ cher besprochen. Wird in einem Kontext statt einem expliziten Namen nur die Klasse festgelegt, so wird implizit der Name this als vereinbart angenommen. In diesem Fall kann auf Attribute auch direkt zugegriffen werden. Geschlossene Bedingungen besitzen einen leeren Kontext: inv: forall a in Auction: a.startTime <= Time.now() implies a.numberOfBids == 0
OCL
Mithilfe des let-Konstrukts konnen ¨ Zwischenergebnisse einer Hilfsvariablen zugewiesen werden, um diese im Rumpf des Konstrukts gegebenenfalls mehrfach zu nutzen: context inv SomeArithmeticTruth: let middle = (a+b)/2 in a<=b implies a<=middle and
OCL
middle<=b
Das let-Konstrukt kann auch dazu genutzt werden, Hilfsfunktionen zu vereinbaren: context Auction a inv Time2: let min(Time x, Time y) = (x<=y) ? x : y in min(a.startTime, min(a.closingTime,a.finishTime)) == a.startTime
OCL
Wie bereits gesehen, kann die Fallunterscheidung .?.:. aber auch die verbose, a¨ quivalente Fassung if-then-else eingesetzt werden. Im Gegensatz zur imperativen Fallunterscheidung sind immer der then- und der else-Zweig anzugeben. Eine spezielle Form der Fallunterscheidung erlaubt die Behandlung von Typkonversionen, wie sie bei Subtyphierarchien gelegentlich auftreten. OCL/P bietet dafur ¨ eine typsichere Konstruktion an, die eine Kombination einer Typkonversion und einer Abfrage nach dessen Konvertierbarkeit darstellt: OCL
context Message m inv: let Person p = m instanceof BidMessage ? m.bidder : null in ...
18
¨ 2 Kompakte Ubersicht zur UML/P
Zus¨atzlich zu einer normalen Fallunterscheidung wird im then-Zweig der Fallunterscheidung der Typ der Variable m auf den Subtyp gesetzt und ermoglicht ¨ dadurch die Selektion m.bidder. Die Typsicherheit bleibt trotz Konversion erhalten. Als Grunddatentypen stehen die aus Java bekannten Sorten boolean, char, int, long, float, byte, short und double und darauf arbeitende Operatoren ohne Seiteffekte zur Verfugung. ¨ Ausgeschlossen sind damit die Inkrementoperatoren ++ und -- sowie alle Zuweisungsformen. Neu sind die booleschen Operatoren implies und <=>, die zur Darstellung von Im¨ plikationen und Aquivalenzen dienen, und die Postfixoperatoren @pre und **. Priorit¨at Operator
Assoziativit¨at Operand(en), Bedeutung
14
links links rechts rechts rechts links links links links links links links links links links links links links links rechts
13
12 11 10 9
8 7 6 5 4 3 2,7 2,3 2
@pre ** +, -, ˜ ! (type) *, /, % +, <<, >>, >>> <, <=, >, >= instanceof in ==, != & ˆ | && || implies <=> ? :
Wert des Ausdrucks in der Vorbedingung Transitive Hulle ¨ einer Assoziation Zahlen Boolean: Negation Typkonversion (Cast) Zahlen Zahlen, String (+) Shifts Vergleiche Typvergleich Element von Vergleiche Zahlen, Boolean: striktes und Zahlen, Boolean: xor Zahlen, Boolean: striktes oder Boolesche Logik: und Boolesche Logik: oder Boolesche Logik: impliziert Boolesche Logik: a¨ quivalent Auswahlausdruck (if-then-else)
Tabelle 2.12: Prioritäten der OCL-Operatoren
Der Datentyp String wird wie in Java nicht als Grunddatentyp, sondern als standardm¨aßig zur Verfugung ¨ stehende Klasse verstanden. 2.2.2 Die OCL-Logik Die richtige Definition der einer Bedingungssprache zugrunde liegenden Logik ist fur ¨ einen praxistauglichen Einsatz der Sprache wichtig. Deshalb wurde fur ¨ OCL/P eine zweiwertige Logik und ein spezieller Umgang mit dem
2.2 Object Constraint Language
19
problematischen Undefined“ gew¨ahlt. Eine detaillierte Diskussion dieser ” Aspekte ist in Band 1 zu finden. Fur ¨ die Logik ist die Abbildung des undefinierten Werts auf den Wahrheitswert false am elegantesten, weil damit eine zweiwertige Logik entsteht. Sowohl Spezifikationen als auch Beweisfuhrungen ¨ werden dadurch besonders einfach, weil ein dritter Fall nicht existiert. Unglucklicherweise ¨ ist diese, fur ¨ die Spezifikation so angenehme Semantik nicht vollst¨andig implementierbar, da zu bestimmen w¨are, ob eine Berechnung nicht terminiert, und dann der Wert false auszugeben w¨are.1 Dennoch ist die gew¨ahlte Form praktikabel (siehe Band 1). Die Wahrheitstabellen der OCL-Operatoren mit dieser impliziten Abbildung von undef auf false sind in Abbildung 2.13 festgelegt.
Abbildung 2.13. Interpretationen der booleschen Operatoren
2.2.3 Container-Datenstrukturen Container sind die Grundlage der in der OCL so wichtigen Navigation uber ¨ Assoziationen. Ausgehend von einem einzelnen Objekt konnen ¨ durch einen Navigationsausdruck eine Menge oder eine Liste von erreichbaren Objekten beschrieben und ihren Elementen gewisse Eigenschaften zugeordnet werden. Die OCL/P bietet drei Typkonstruktoren fur ¨ Container an (Abbildung 2.14). Fur ¨ einen Vergleich von Containern ist eine bin¨are Operation notwendig, die auf den Elementen einen Test auf Gleichheit durchfuhrt. ¨ Sind die Elemente Grunddatentypen oder wieder Container, dann wird fur ¨ die Elemente der Vergleich == verwendet. Sind die Elemente aber Objekte, so wird der Vergleich equals eingesetzt. Das entspricht einem Wertevergleich fur ¨ Grunddatentypen und Container sowie normalerweise einem Vergleich der 1
Das Terminierungsproblem ist jedoch unentscheidbar.
20
¨ 2 Kompakte Ubersicht zur UML/P
Set(X) beschreibt Mengen uber ¨ einem Datentyp X. Auf diesen Mengen stehen die ublichen ¨ Operatoren wie Vereinigung oder Hinzufugen ¨ zur Verfugung. ¨ Fur ¨ den Typ X kann jeder Grunddatentyp, jede Klasse und jeder Containertyp eingesetzt werden. Als Vergleich werden Wertevergleich fur ¨ Grunddatentypen und die Objektidentit¨at fur ¨ Klassen verwendet. Allerdings konnen ¨ Objekte ausgew¨ahlter Klassen wie zum Beispiel String den Vergleich equals nutzen und uber¨ schreiben und damit ebenfalls einen Wertevergleich uber ¨ ihre Attribute anbieten. List(X) beschreibt geordnete Listen und die dafur ¨ sinnvollen Operationen. List(X) erlaubt die Verwaltung ihrer Objekte in einer linearen Ordnung, beginnend bei dem Index 0. Collection(X) ist ein Supertyp fur ¨ die beiden genannten Typen Set(X) und List(X). Er beinhaltet deren gemeinsame Funktionalit¨at. Abbildung 2.14. Typkonstruktoren der OCL
Objektidentit¨at auf Objekten. Fur ¨ spezielle Objekttypen, wie zum Beispiel String, wird jedoch equals uberschrieben ¨ und damit ein Wertevergleich angeboten. Die Subtypbeziehung von Set(X) und List(X) zu Collection(X) erlaubt es, fur ¨ jeden beliebigen Typ X statt Werten des Typs Collection(X) Werte der Typen Set(X) oder List(X) einzusetzen. Die Aufschreibung von Mengen und Listen basiert im Wesentlichen auf der Extension von Klassen, Mengenkomprehensionen und Navigationsausdrucken: ¨ Auction.size < 1700; Set{}; Set{"text",""} == {"text",""}; List{8,5,8}; List{’a’..’c’} == List{’a’,’b’,’c’};
OCL
Mengen- und Listenkomprehension OCL/P bietet gegenuber ¨ dem OCL-Standard eine funktionalen Sprachen entlehnte reichhaltige Sammlung von Moglichkeiten ¨ zur eigenschaftsorientierten und zur aufz¨ahlenden Beschreibung von Mengen und Listen. Die allgemeine Syntax einer Listenkomprehension ist von der Form: List{ expr | characterization }
OCL
Dabei werden in der Charakterisierung (rechts) neue Variablen definiert, die in dem Ausdruck (links) genutzt werden konnen. ¨ Die Charakterisierung besteht deshalb aus mehreren, durch Kommata getrennten Generatoren, Filtern und lokalen Variablendefinitionen. Ein Generator v in list l¨asst eine neue Variable v uber ¨ einer Liste list variieren. Beispielsweise konnen ¨ so Quadratzahlen beschrieben werden:
2.2 Object Constraint Language inv: List{ x*x | x in List{1..5} } == List{1,4,9,16,25}
21 OCL
Der Filter beschreibt eine Einschr¨ankung auf einer Liste von Elementen. Ein solcher Filter evaluiert zu einem Wahrheitswert, der daruber ¨ entscheidet, ob ein Element in einer Liste aufgenommen ist. In Kombination mit einem Generator konnen ¨ so Filter fur ¨ Teillisten beschrieben werden: OCL
inv: List{ x*x | x in List{1..8}, !even(x) } == List{1,9,25,49}
Zur weiteren Unterstutzung ¨ des Beschreibungskomforts ko¨ nnen Zwischenergebnisse berechnet und lokalen Variablen zugewiesen werden. inv: List{ y | x in List{1..8}, int y = x*x, !even(y) } == List{1,9,25,49}
OCL
Container-Operationen Mengen, Listen und Container allgemein besitzen die in den Abbildungen 2.15, 2.16 und 2.17 aufgelisteten Operationen. Deren Signatur stellt eine Integration der aus der Java-Realisierung von Mengen bekannten und der vom OCL-Standard angebotenen Funktionalit¨at dar. Die Operationen size, isEmpty und asList konnen ¨ in OCL wie Attribute ohne Klammern geschrieben werden, denn eine Query ohne Argumente kann grunds¨atzlich wie ein Attribut behandelt werden. Eine Schreibweise als Query mit Klammern ist wegen der Kompatibilit¨at zu Java ebenfalls moglich. ¨ Set(X) Set(X) boolean boolean int boolean Set(X) Set(X) Set(X) Set(X) int X List(X)
add(X o); Signatur addAll(Collection(X) c); contains(X o); containsAll(Collection(X) c); count(X o); isEmpty; remove(X o); removeAll(Collection(X) c); retainAll(Collection(X) c); symmetricDifference(Set(X) s); size; flatten; // NB: X ist ein Collection-Typ asList; Abbildung 2.15. Signatur von Mengen des Typs Set(X)
Im Gegensatz zu einer Java-Implementierung ist fur ¨ OCL-Ausdrucke ¨ das Konzept der Exceptions nicht vorhanden. Stattdessen sind alle Operatoren
22
¨ 2 Kompakte Ubersicht zur UML/P
der OCL robust, so dass ihre Interpretation immer sinnvolle Ergebnisse ergibt. List(X) List(X) List(X) List(X) List(X) boolean boolean X X X List(X) int int boolean int List(X) List(X) List(X) List(X) List(X) int List(X) List(Y) Set(X)
add(X o); Signatur add(int index, X o); prepend(X o); addAll(Collection(X) c); addAll(int index, Collection(X) c); contains(X o); containsAll(Collection(X) c); get(int index); first; last; rest; indexOf(X o); lastIndexOf(X o); isEmpty; count(X o); remove(X o); removeAtIndex(int index); removeAll(Collection(X) c); retainAll(Collection(X) c); set(int index, X o); size; subList(int fromIndex, int toIndex); flatten; // NB: X hat die Form Collection(Y) asSet; Abbildung 2.16. Signatur von Listen des Typs List(X)
Weil in der OCL Container keine Objektidentit¨at besitzen, sind auf Containern beide Gleichheits-Operatoren == und equals identisch: context Set(X) sa, Set(X) sb inv: sa.equals(sb) <=> sa==sb
OCL
Bei dem Vergleich von Mengen wird, wie bereits besprochen, auf den Vergleich der Elemente equals fur ¨ Objekte und == fur ¨ Grunddatentypen zuruckgegriffen. ¨ In der OCL ist == fur ¨ Container von Objekten von der frei definierbaren Gleichheit equals auf den Elementen abh¨angig und unterscheidet sich von dem Vergleich in Java. Fur ¨ Listen sicher ungewohnlich, ¨ aber fur ¨ praktische Belange angenehm, da systematischer, ist die aus Java bekannte Indizierung der Listenelemente beginnend mit 0: List{0,1,2}.add(3) List{’a’,’b’,’c’}.add(1,’d’)
== List{0,1,2,3}; == List{’a’,’d’,’b’,’c’};
OCL
2.2 Object Constraint Language
23
List{0,1,2}.prepend(3) == List{3,0,1,2}; List{0,1,2}.set(1,3) == List{0,3,2}; List{0,1,2}.get(1) == 1; List{0,1,2}.first == 0; List{0,1,2}.last == 2; List{0,1,2}.rest == List{1,2}; List{0,1,2,1}.remove(1) == List{0,2}; List{0,1,2,3}.removeAtIndex(1) == List{0,2,3}; List{0,1,2,3,2,1}.removeAll(List{1,2}) == List{0,3}; List{0..4}.subList(1,3) == List{1,2};
Collection(X) Collection(X) boolean boolean boolean int Collection(X) Collection(X) Collection(X) int Collection(Y) List(Y) Set(X) List(X)
add(X o); Signatur addAll(Collection(X) c); contains(X o); containsAll(Collection(X) c); isEmpty; count(X o); remove(X o); removeAll(Collection(X) c); retainAll(Collection(X) c); size; flatten; // NB: X hat die Form Collection(Y) oder Set(Y) flatten; // NB: X hat die Form List(Y) asSet; asList;
Abbildung 2.17. Signatur von Containern des Typs Collection(X)
Tief geschachtelte Container-Strukturen enthalten eine gewisse Strukturierungsinformation, die in manchen F¨allen zur Spezifikation von Systemen hilfreich ist. So kann mit dem Typ Set(Set(Person)) eine Gruppierung von Mengen von Personen beschrieben werden. let Set(Set(Person)) ssp = { a.bidder | a in Auction } in ...
OCL
Der Operator flatten erlaubt es, solche Container-Strukturen großerer ¨ Schachtelungstiefe flach zu drucken. ¨ Der flatten-Operator verschmilzt die beiden oberen“ Ebenen von ” Containern, ohne sich um die interne Struktur des darin verborgenen Elementtyps X zu kummern. ¨ Man spricht auch von schmalem (engl.: shallow) Flachdrucken. ¨ Eine genaue Beschreibung dazu ist in Band 1 zu finden. Bei Navigationsketten wird der flatten-Operator implizit eingesetzt, so dass das Ergebnis einer Navigationskette nie eine Container-Struktur darstellt, die tiefer verschachtelt ist als die Ausgangsstruktur. Einzige Ausnah-
24
¨ 2 Kompakte Ubersicht zur UML/P
me bildet die Navigationskette ausgehend von einem einzelnen Objekt, die je nach Form der Assoziation zu einer Menge oder Sequenz fuhren ¨ kann.
Abbildung 2.18. Abstraktes Modell zur Erkl¨arung von Navigationsergebnissen
Ausgehend von einem einfachen Objekt h¨angt das Ergebnis von der Kardinalit¨at der Assoziation gem¨aß Abbildung 2.18 ab: let Auction a Policy po Set(Person) spe List(Message) lm in ...
= = = =
...; a.policy; a.person; a.message
OCL
Quantoren und spezielle Operationen Die beiden Quantoren forall und exists erlauben die Beschreibung einer Eigenschaft, uber ¨ alle Elemente einer Gruppe beziehungsweise wenigstens einem der Elemente. In einigen vorhergehenden Beispielen wurde bereits gezeigt, wie Quantoren eingesetzt werden, um uber ¨ die Elemente eines Containers Aussagen zu formulieren. Quantoren konnen ¨ uber ¨ mehrere Variablen kombiniert werden: inv Nachrichten1: forall a in Auction, p in Person, m in a.message: p in a.bidder implies m in p.message
OCL
Der dritte Quantor dieses Beispiels zeigt, dass die Ausgangsmenge, uber ¨ die quantifiziert wird, nicht nur die Extension einer Klasse, sondern ein beliebiger mengen- oder listenwertiger Ausdruck sein kann. Der Existenzquantor ist dual zum Allquantor: inv: (exists var in setExpr: expr) <=> !(forall var in setExpr: !expr)
OCL
Beide Quantoren werden meist nur auf endliche Container oder Extensionen angewandt. Dies hat den Vorteil der (zumindest prinzipiellen) Berechenbarkeit, indem die quantifizierte Variable mit allen Werten beziehungsweise
2.2 Object Constraint Language
25
Objekten belegt und der Rumpf damit interpretiert wird. Fur ¨ die quantifizierte Menge ist wesentlich, dass den Klassen, wie zum Beispiel Person, die Extension in Form aller aktuell existierenden Objekte zugeordnet wird, die zwar unbeschr¨ankt aber endlich ist. Neben endlichen Mengen von Objekten erlaubt die OCL/P auch die Benutzung von Quantoren uber ¨ unendlichen Grunddatentypen wie zum Beispiel int und ist hier naturlich ¨ unberechenbar. Bei Mengen und Listen wird ebenfalls uber ¨ potentielle (und nicht die gerade im System befindlichen Mengen-Implementierungen) quantifiziert. Die Aussage inv ListQuantor: forall Set(Person) lp: lp.size != 5
OCL
interpretiert die Variable lp uber ¨ alle potentiellen Listen uber ¨ die tats¨achlich zu einem Zeitpunkt existierenden Objekte. Damit ist eine Quantifizierung uber ¨ Set(Person) eine Kombination der Interpretation eines unendlichen Quantors auf einem Grunddatentyp und einem endlichen Quantor uber ¨ den darin befindlichen Referenztyp. Entsprechend sind Quantoren uber ¨ Listen unendlich und konnen ¨ daher nur zur Spezifikation eingesetzt werden. Da die Potenzmenge einer endlichen Menge ebenfalls endlich ist, ist die obige Quantifizierung uber ¨ Set(Person) endlich und sogar erfull¨ bar. Fur ¨ die Selektion eines Elements aus einer Container-Struktur gibt es den speziellen Operator any. Dieser Operator ist fur ¨ Mengen nicht eindeutig festgelegt, wie die folgenden definierenden Gleichungen zeigen: (any listExpr) == listExpr.get(0); (any setExpr) == any setExpr.asList; (any var in collection: expr) == any { var in collection | expr }
OCL
Fur ¨ eine elementweise Bearbeitung von Mengen und Listen gibt es neben den Komprehensionsformen den iterate-Operator, der eine Schleife mit Zustandsspeicher aus der funktionalen wie auch der imperativen Programmierung nachbildet: iterate { elementVar in setExpr; Type accumulatorVar = initExpr : accumulatorVar = expr }
OCL
Der iterate-Operator kann zum Beispiel eingesetzt werden, um die Summe einer Menge von Zahlen zu berechnen: inv Summe: let int total = iterate { elem in Auction; int acc = 0 : acc = acc+elem.numberOfBids } in ...
OCL
26
¨ 2 Kompakte Ubersicht zur UML/P
Fur ¨ die Behandlung undefinierter Werte wird der defined-Operator eingefuhrt, ¨ der genau dann true liefert, wenn sein Argument definiert ist. Fur ¨ undefinierte Argumente evaluiert dieser Operator zu false: context Auction a inv: let Message mess = a.person.message[0] in defined(mess) implies ...
OCL
Ein ganz spezifisches Problem der OCL stellt die Definition transitiver Hullen ¨ uber ¨ Navigationswege dar: OCL ist eine Logik erster Ordnung und daher nicht in der Lage, eine transitive Hulle ¨ zu spezifizieren. Deshalb existiert in OCL/P ein spezieller Operator **, der auf eine Asssoziation angewandt deren Signatur beibeh¨alt. In Kombination mit Vererbung gibt es deshalb die vier in Abbildung 2.19 angegebenen F¨alle.
Abbildung 2.19. Typisierung der transitiven Hulle ¨
2.2.4 Funktionen in OCL Mithilfe von Paaren aus Vor- und Nachbedingung ko¨ nnen einerseits die Effekte von Methoden des zugrunde liegenden Modells spezifiziert werden. Umgekehrt kann das Modell Methoden fur ¨ die Spezifikation von OCLBedingungen zur Verfugung ¨ stellen. Eine solche Methode wird Query genannt und wird durch den Stereotyp query gekennzeichnet. Queries sind seiteneffektfrei im Sinne der in Band 1 beschriebenen Form. Fur ¨ die Modellierung komplexer Eigenschaften ist es oft sinnvoll oder sogar notwendig, zus¨atzliche Queries zu definieren, die jedoch nicht implementiert werden. Solche Spezifikationsqueries werden mit dem Stereotyp OCL gekennzeichnet.
2.2 Object Constraint Language
27
Methodenspezifikation Fur ¨ die Beschreibung einzelner Methoden wird eine Methodenspezifikation eingesetzt, die im Vor-/Nachbedingungstil formuliert wird. Der Kontext einer Methodenspezifikation wird durch eine Methode, der sie beinhaltenden Klasse sowie den zugehorigen ¨ Parametern festgelegt. Zwei Bedingungen, die Vorbedingung und die Nachbedingung, die beide mit Namen versehen werden konnen, ¨ charakterisieren den Effekt der Methode. Die Vorbedingung beschreibt, unter welchen Bedingungen eine Methode aufgerufen werden kann, so dass eine definierte und robust implementierte Reaktion entsteht. Die Nachbedingung charakterisiert das Ergebnis des Methodenaufrufs und die Ver¨anderungen auf dem Objektzustand. Das Methodenergebnis wird in der Nachbedingung mit result bezeichnet. Statische Methoden werden mit static markiert. In ihrem Rumpf darf wie bei einer Implementierung kein this eingesetzt werden.
Abbildung 2.20. Ausschnitt der Personenklasse
Die Nachbedingung der Methodenspezifikation kann einen expliziten Bezug auf den Zustand vor dem Methodenaufruf durch Verwendung des Postfix @pre enthalten. Methode addMessage aus Abbildung 2.20, dient dazu, bei einer Person eine weitere Nachricht hinzuzufugen: ¨ context Person.addMessage(Message m) pre: m.time >= messageList.last.time post: messageList ==
[email protected](m)
OCL
Der Operator @pre darf nur auf einzelne Attribute oder Navigationselemente angewandt werden. a@pre evaluiert zu dem Wert des Attributs a zur Zeit des Methodenaufrufs und erlaubt so den Vergleich zwischen altem und neuen Wert. Ist die Vorbedingung nicht erfullt, ¨ so ist mit der Spezifikation uber ¨ das Verhalten der Methode nichts ausgesagt. Deshalb ist es moglich ¨ eine komplexe Methode durch mehrere Spezifikationen zu beschreiben. Es wird festgelegt, dass bei uberlappenden ¨ Vorbedingungen beide Nachbedingungen zu erfullen ¨ sind. Mit dieser Kompositionstechnik lassen sich Methodenspezifikationen sogar vererben und in Subklassen spezialisieren. Details dazu lassen sich in Band 1 finden.
28
¨ 2 Kompakte Ubersicht zur UML/P
Fur ¨ komplexe Methodenspezifikationen existiert eine erweiterte Form des let-Konstrukts, das es erlaubt Variablen zu definieren, die in beiden Bedingungen belegt sind: context Class.method(parameters) let var = expression pre: ... var ... post: ... var ...
OCL
2.3 Objektdiagramme Objektdiagramme modellieren tats¨achliche Strukturen auf exemplarischer Basis. Sie sind daher besonders geeignet, statische Strukturen in eigentlich dynamischen objektorientierten Systemen oder spezielle Situationen darzustellen, die etwa als Vor- oder Nachbedingung bei Tests verwendet werden. Die Integration mit der OCL fuhrt ¨ zu einer Logik fur ¨ Objektdiagramme“, ” die einen methodisch verbesserten Einsatz von Objektdiagrammen erlaubt. Objekt im Objektdiagramm. Ein Objekt ist Instanz einer Klasse und enth¨alt die von ihr festgelegten Attribute. Diese Attribute sind mit einem Wert initialisiert (ggf. noch uninitialisiert). Im Objektdiagramm werden prototypische Objekte verwendet, um exemplarische Situationen zu illustrieren. Zwischen den im Diagramm sichtbaren prototypischen Objekten und den echten Objekten des Systems besteht normalerweise keine 1:1-Beziehung (siehe dazu auch die Diskussion in Abschnitt 5.2, Band 1). Objektname erlaubt die eindeutige Benennung des Objekts im Objektdiagramm. Als Objektname wird ein sprechender Name gew¨ahlt, der normalerweise im echten System nicht wiederzufinden ist, da dort systemspezifische Objektidentifikatoren verwendet werden. Attribut beschreibt eine Zustandskomponente eines Objekts. Ein Attribut im Objektdiagramm ist charakterisiert durch den Attributnamen, den Typ und einen konkreten Wert. Weitere Charakteristika, wie Sichtbarkeit, konnen ¨ dem Attribut angeheftet werden. In abstrakten Objektdiagrammen konnen ¨ statt konkreten Werten auch Variablennamen oder Ausdrucke ¨ eingesetzt werden, deren Inhalt im Diagramm unspezifiziert“ bleibt. Attributtyp oder -wert ko¨ nnen auch ausge” lassen werden. Link ist eine Auspr¨agung einer Assoziation zwischen zwei Objekten, deren Klassen jeweils an der Assoziation partizipieren. Navigationsrichtung, Assoziationsund Rollennamen konnen ¨ auf dem Link ebenfalls dargestellt werden. Kompositionslink ist eine Sonderform des Links, bei dem neben der reinen Verbindung weitere Abh¨angigkeiten des Teilobjekts vom Ganzen bestehen. Ein Kompositionslink ist eine Auspr¨agung einer Komposition. Abbildung 2.21. Begriffsdefinitionen fur ¨ Objektdiagramme
2.3 Objektdiagramme
29
2.3.1 Einfuhrung ¨ in Objektdiagramme Eine Kurzbeschreibung der wichtigsten Begriffe bei Objektdiagrammen ist in Abbildung 2.21 enthalten. Diese Begriffe werden nachfolgend detaillierter erkl¨art. Abbildung 2.22 zeigt ein einfaches Objektdiagramm aus dem Auktionssystem, das nur aus einem Objekt besteht. Dieses Objekt beschreibt eine Strom-Auktion.
Abbildung 2.22. Einzelnes Auktionsobjekt
Objektdiagramme sind grunds¨atzlich konform zu der durch Klassendiagramme vorgegebenen Struktur. Objektdiagramme sind Klassendiagrammen sehr a¨ hnlich, unterscheiden sich aber in einigen substantiellen Punkten und sind daher eine eigenst¨andige Notation. Objekte beinhalten Attribute, die meistens mit konkreten Werten angegeben werden. Es konnen ¨ mehrere Objekte derselben Klasse auftreten, weshalb jedes Objekt einen eigenen (gegebenenfalls anonymen) Namen hat.
Abbildung 2.23. Darstellungsformen des Auktionsobjekts
30
¨ 2 Kompakte Ubersicht zur UML/P
In einem Objektdiagramm werden keine Vererbungsbeziehungen dargestellt. Stattdessen konnen ¨ die aus Oberklassen geerbten Attribute in der Unterklasse explizit aufgelistet werden. Die Vererbungsstruktur zwischen Klassen wird also in einem Objektdiagramm expandiert“. W¨ahrend Klassendia” gramme ein drittes Feld zur Darstellung von Methoden kennen, werden Methoden im Objektdiagramm nicht aufgefuhrt. ¨ Eine Methodenliste existiert also bei Objekten im Objektdiagramm nicht. Ein Link verbindet zwei Objekte. So wie ein Objekt Instanz einer Klasse ist, so ist ein Link Instanz einer Assoziation. Abbildung 2.24 zeigt eine Objektstruktur fur ¨ eine Auktion, die mindestens die angegebenen drei Personen beinhaltet. Eine davon ist nur als Beobachter zugelassen.
Abbildung 2.24. Objektstruktur einer Auktion
Besitzt eine Assoziation an einem Ende einen Qualifikator, so kann in einem Objektdiagramm der konkrete Qualifikatorwert an jedem Link der Assoziation angegeben werden. Abbildung 2.25 zeigt ein solches Objektdiagramm. 2.3.2 Komposition Die Komposition wird auch im Objektdiagramm durch eine ausgefullte ¨ Raute an einem Assoziationsende dargestellt. W¨ahrend in Klassendiagrammen eine Klasse in mehreren Kompositionen vorkommen darf, ist es nicht erlaubt, ein Teilobjekt im Objektdiagramm mehreren Kompositionslinks zuzuordnen. Um die Abh¨angigkeit der Teilobjekte vom Ganzen und den Kompositionscharakter st¨arker hervorzuheben, kann die alternative Darstellung aus Abbildung 2.26 rechts verwendet werden.
2.3 Objektdiagramme
31
Abbildung 2.25. Qualifizierte Links
Abbildung 2.26. Alternative Darstellungen einer Komposition
2.3.3 Bedeutung eines Objektdiagramms Aufgrund der Dynamik von Objektsystemen variiert die Anzahl der Objekte in einem System st¨andig. Außerdem konnen ¨ Objektstrukturen ihre Links dynamisch ver¨andern. Dadurch konnen ¨ unbeschr¨ankt viele Variationen von Objektstrukturen entstehen, die normalerweise nicht vollst¨andig dargestellt werden konnen. ¨ Ein Objektdiagramm modelliert daher einen Systemausschnitt mit zeitlich limitierter Gultigkeit, ¨ im Extremfall einen zu einem einzigen Zeitpunkt gultigen ¨ Snapshot Wie in Band 1 diskutiert, haben die mit Objektdiagrammen modellierten Strukturen einen prototypischen, musterartigen Charakter. Sie zeigen exemplarisch eine Situation, ohne dass diese Situation notwendigerweise normativen Charakter annimmt. Die im Objektdiagramm dargestellte Situation muss im tats¨achlich ablaufenden System nicht auftreten. Sie kann jedoch auch mehr als einmal, zeitlich aufeinanderfolgend oder zur gleichen Zeit auftreten. In jeder der auftretenden Situationen konnen ¨ unterschiedliche oder auch (teilweise) dieselben Objekte beteiligt sein. Objektdiagramme konnen ¨ also im System uberlappende ¨ Strukturen darstellen. Deshalb ist zwischen der Darstellung eines Objekts im Objektdiagramm und den echten Objekten eines
32
¨ 2 Kompakte Ubersicht zur UML/P
Systems deutlich zu unterscheiden. Die in den Objektdiagrammen dargestellten Objekte werden daher auch als prototypisch bezeichnet. Die Moglichkeit ¨ zu Instantiierung von prototypischen Objekten bildet eine Grundlage fur ¨ die Einbindung der Objektdiagramme in die OCL-Logik. 2.3.4 Logik der Objektdiagramme Kernelement der Einbindung der exemplarischen Objektdiagramme in die OCL ist die Moglichkeit, ¨ ein Objektdiagramm in einfacher Weise selbst als Aussage uber ¨ die beschriebenen Objekte zu verstehen. Wie in Band 1 durch eine Transformation von Objektdiagrammen nach OCL beschrieben, sind die benannten Objekte freie Variablen solcher Aussagen, w¨ahrend anonyme Objekte existenzquantifiziert sind. Deshalb sind alle im Objektdiagramm verwendeten Namen in der nutzenden OCL-Bedingung einzufuhren. ¨ Der Name eines Objektdiagramms steht fur ¨ die darin festgestellte Aussage und kann in der OCL frei eingesetzt werden. So kann mit der folgenden Aussage gefordert werden, dass in jeder Auktion, die bereits begonnen hat, eine Willkommensnachricht versandt wurde. Dabei werden die zwei Objektdiagramme aus Abbildung 2.27 verwendet, von denen Welcome1A die Voraussetzung fur ¨ die Forderung Welcome1B darstellt.
Abbildung 2.27. Objektdiagramme fur ¨ die Willkommensnachricht
inv Welcome1: forall Auction a, TimingPolicy timePol: OD.Welcome1A implies exists Message welcome: OD.Welcome1B
OCL
Die Aussage lautet: Wenn Objekte a und timePol existieren und dem ” Objektdiagramm Welcome1A genugen, ¨ dann existiert Objekt welcome und es gelten alle im Objektdiagramm Welcome1B formulierten Eigenschaften“. Durch das Ersetzen konkreter Werte durch Variablen oder OCL-Ausdrucke ¨ konnen ¨ abstrake Objektdiagramme gebildet werden. Die abstrakten Werte konnen ¨ in eine OCL-Bedingung importiert und dort zur Spezifikation
2.4 Statecharts
33
von Eigenschaften genutzt werden. Um zum Beispiel eine Testauktion aufzusetzen, die 100 Personen hat, kann das Objektdiagramm NPersons aus Abbildung 2.28 verwendet werden.
import Auction test32 inv Test32: forall int x in {1..100}: OD.NPersons
OCL
Abbildung 2.28. Auktion mit parametrisiertem Personen-Objekt
W¨ahrend sich der Beschreibungskomfort der aus OCL und Objektdiagrammen integrierten Notation gegenuber ¨ der OCL verbessert hat, ist die Beschreibungsm¨achtigkeit nur gegenuber ¨ den Objektdiagrammen besser geworden, weil so • • • •
Alternativen, unerwunschte ¨ Situationen Kompositionen und Verallgemeinerungen (Muster)
moglich ¨ geworden sind. Dazu werden im Wesentlichen die Logik-Operatoren Disjunktion, Negation, Konjunktion und Quantoren eingesetzt. Damit steigt die Ausdruckskraft von Objektdiagrammen von exemplarischen zu allgemein gultigen ¨ Aussagen.
2.4 Statecharts Statecharts sind eine Weiterentwicklung der Automaten zur Beschreibung von Objektverhalten. Jedes komplexere System besitzt steuernde und kontrollierende Anteile, die mit Statecharts modelliert werden konnen. ¨ Die hier vorgestellte Variante der Statecharts nutzt die OCL als Bedingungssprache und Java-Anweisungen als Aktionen. 2.4.1 Eigenschaften von Statecharts Automaten gibt es in unterschiedlichsten Auspr¨agungen. So konnen ¨ Automaten ausfuhrbar ¨ sein, zur Erkennung von Sequenzen von Buchstaben oder Nachrichten, zur Beschreibung des Zustandsraums eines Objekts oder zur Spezifikation von Antwortverhalten auf einen Stimulus genutzt werden. Der
34
¨ 2 Kompakte Ubersicht zur UML/P
¨ Ubersichtsartikel [vdB94] zeigt, dass es fur ¨ Statecharts eine Reihe syntaktischer Varianten und semantischer Interpretationsmo¨ glichkeiten gibt, die den jeweiligen Anwendungsgebieten angepasst sind. Die Statecharts der UML/P lassen sich durch Steuerung mit geeigneten Stereotypen fur ¨ die Modellierung, Codegenerierung oder Testfallbeschreibung einsetzen. Abbildung 2.29 zeigt ein Statechart, das eine vereinfachende Abstraktion des Zustandssystems einer Auktion darstellt. Abbildung 2.30 beinhaltet eine uberlappende ¨ Liste von Aufgaben, die ein Statechart ubernehmen ¨ kann.
Abbildung 2.29. Statechart
Die (uberlappenden) ¨ Aufgaben eines Statecharts konnen ¨ sein: • • • • • • •
Darstellung des Lebenszyklus eines Objekts Implementierungsbeschreibung einer Methode Implementierungsbeschreibung des Verhaltens eines Objekts Abstrakte Anforderungsbeschreibung an den Zustandsraum eines Objekts Darstellung der Reihenfolge von erlaubten Eintreten von Stimuli (Aufrufreihenfolge) Charakterisierung der mo¨ glichen oder erlaubten Verhalten eines Objekts Bindeglied zwischen Zustands- und Verhaltensbeschreibung Abbildung 2.30. Aufgabenvielfalt eines Statecharts
¨ Eine kompakte Ubersicht der Statechart-Konstrukte ist in den Abbildungen 2.31, 2.32 und 2.33 zusammengefasst. Ein Statechart beschreibt das Antwortverhalten eines Objekts, das entsteht, wenn ein Stimulus auf dieses Objekt trifft. Ein Stimulus kann dabei
2.4 Statecharts
35
Zustand. Ein Zustand (synonym: Diagrammzustand) repr¨asentiert eine Teilmenge der moglichen ¨ Objektzust¨ande. Ein Diagrammzustand wird durch einen Zustandsnamen und je einer optionalen Zustandsinvariante, entry-Aktion, exit-Aktion und do-Aktivit¨at modelliert. Startzustand. In einem Startzustand beginnen Objekte ihren Lebenszyklus. Mehrere Startzust¨ande erlauben mehrere Arten der Objekterzeugung darzustellen. In einem Methoden-Statechart markiert der Startzustand den Beginn der Methode. Die Bedeutung eines Startzustands als Teil eines anderen Zustands ist in Abschnitt 6.4.2, Band 1 beschrieben. Endzustand. Ein Endzustand beschreibt, dass das Objekt in diesem Zustand seine Pflicht erfullt ¨ hat und nicht mehr gebraucht wird. Allerdings konnen ¨ Endzust¨ande wieder verlassen werden. In einem Methoden-Statechart markiert ein Endzustand das Ende der Methodenbearbeitung. Die Bedeutung eines Endzustands als Teil eines anderen Zustands ist in Abschnitt 6.4.2, Band 1 beschrieben. Teilzustand. Zust¨ande konnen ¨ hierarchisch geschachtelt werden. Ein Gesamtzustand enth¨alt mehrere Teilzust¨ande. Zustandsinvariante ist eine OCL-Bedingung, die fur ¨ einen Diagrammzustand charakterisiert, welche Objektzust¨ande ihm zugeordnet sind. Zustandsinvarianten verschiedener Zust¨ande durfen ¨ im Allgemeinen uberlappen. ¨ Abbildung 2.31. Begriffsdefinitionen fur ¨ Statecharts, Teil 1: Zust¨ande Stimulus wird von anderen Objekten oder der Laufzeitumgebung verursacht und fuhrt ¨ zur Ausfuhrung ¨ einer Transition. Ein Stimulus kann zum Beispiel ein Methodenaufruf, ein Remote Procedure Call, der Empfang einer asynchron versandten Nachricht oder die Mitteilung einer Zeituberschreitung ¨ (Timeout) sein. Transition. Eine Transition fuhrt ¨ von einem Quellzustand in einen Zielzustand und beinhaltet eine Beschreibung des Stimulus sowie der Reaktion in Form einer Aktion. Zus¨atzliche OCL-Bedingungen erlauben die Schaltbereitschaft und die Reaktion einer Transition weiter zu pr¨azisieren. Schaltbereitschaft. Eine Transition ist genau dann schaltbereit, wenn sich das Objekt im Quellzustand der Transition befindet, der Stimulus korrekt ist und die Vorbedingung der Transition zutrifft. Sind mehrere Transitionen in derselben Situation schaltbereit, so ist das Statechart nichtdeterministisch und es ist nicht festgelegt, welche Transition ausgew¨ahlt wird. Vorbedingung der Transition. Zus¨atzlich zum Quellzustand und dem Stimulus ist es moglich, ¨ die Schaltbereitschaft einer Transition durch eine OCL-Bedingung einzuschr¨anken, die fur ¨ die Attributwerte und den Stimulus erfullt ¨ sein muss. Nachbedingung der Transition (auch: Aktionsbedingung). Neben einer operationellen Beschreibung der Reaktion auf einen Stimulus kann durch eine OCLBedingung eine eigenschaftsorientierte Einschr¨ankung der moglichen ¨ Reaktion gegeben werden. Abbildung 2.32. Begriffsdefinitionen fur ¨ Statecharts, Teil 2: Transitionen
einen Methodenaufruf, eine asynchrone Nachricht oder einen Timeout darstellen. Die Bearbeitung des Stimulus wird atomar durchgefuhrt. ¨ Sie ist also
36
¨ 2 Kompakte Ubersicht zur UML/P
Aktion. Eine Aktion ist eine durch operationellen Code (also zum Beispiel Java) oder durch eine OCL-Bedingung beschriebene Ver¨anderung des Zustands eines Objekts und seiner Umgebung. Eine Transition enth¨alt ublicherweise ¨ eine Aktion. Entry-Aktion. Ein Zustand kann eine entry-Aktion beinhalten, die ausgefuhrt ¨ wird, wenn der Zustand betreten wird. Sind Aktionen operationell beschrieben, so wird die entry-Aktion nach der Transitionsaktion ausgefuhrt. ¨ Liegt eine eigenschaftsorientierte Beschreibung vor, so gilt die Konjunktion beider Beschreibungen. Exit-Aktion. Analog zur entry-Aktion kann ein Zustand eine exit-Aktion beinhalten. In einer operationellen Form wird diese vor der Transitionsaktion ausgefuhrt, ¨ in der eigenschaftsorientierten Form gilt ebenfalls die Konjunktion. Do-Aktivit¨at. Ein Zustand kann eine permanent andauernde Aktivit¨at beinhalten, die do-Aktivit¨at genannt wird. Ihre Implementierung kann uber ¨ verschiedene Mechanismen zur Erzeugung oder Simulation von Parallelit¨at, wie eigene Threads, Timer, o.¨a. vorgenommen werden. Nichtdeterminismus. Existieren in einer Situation mehrere alternative Transitionen, die schaltbereit sind, so spricht man von Nichtdeterminismus des Statecharts. Das Verhalten des Objekts ist damit unterspezifiziert. Es gibt mehrere methodisch sinnvolle Moglichkeiten, ¨ Unterspezifikation im Verlauf eines Softwareentwurfs einzusetzen und aufzuheben. Abbildung 2.33. Begriffsdefinitionen fur ¨ Statecharts, Teil 3: Aktionen
nicht unterbrechbar und nicht parallel zu anderen Transitionen desselben Statecharts. W¨ahrend ein Objekt typischerweise einen unendlichen Zustandsraum hat, besteht ein Statechart aus einer endlichen, typischerweise sogar sehr kleinen Menge von Diagrammzust¨anden. Die Diagrammzust¨ande stellen daher eine Abstraktion des Objektzustandsraums dar. Die Beziehung zwischen Diagramm- und Objektzust¨anden kann durch Hinzufugen ¨ von OCLBedingungen pr¨azise definiert werden. Gleiches gilt fur ¨ die Vorbedingungen und Effekte von Transitionen. Abh¨angig vom Detaillierungsgrad und der Darstellungsform dieser Bedingungen kann ein Statechart daher als ausfuhr¨ bare Implementierung oder als abstrakte Anforderungsbeschreibung angesehen werden. Statecharts konnen ¨ deshalb von der Anforderungsanalyse bis zur Implementierung eingesetzt werden. Abbildung 2.34 illustriert, wie die endlich vielen Diagrammzust¨ande und Transitionen eines Statecharts mit einem darunter liegenden unendlichen Transitionssystem in Beziehung gesetzt werden, das das Verhalten des Objekts beschreibt. Die Interpretation eines Diagrammelements durch jeweils eine Menge von Zust¨anden beziehungsweise Transitionen hat einige Effekte, auf deren detaillierte Untersuchung in Band 1 hingewiesen wird. Hier seien nur zusammenfassend wesentliche Eigenschaften festgestellt:
2.4 Statecharts
37
Abbildung 2.34. Interpretation eines Diagramms
• • •
•
•
•
Statecharts basieren auf Mealy-Automaten, die neben der Verarbeitung von Stimuli auch die Ausgabe von Ergebnissen, zum Beispiel in Form ¨ von Methodenaufrufen und die Anderung des Objektzustands zulassen. Innerhalb des durch das Statechart beschriebenen Objekts existiert keine Parallelit¨at. Die Statcharts der UML/P haben deshalb keine Parallelzust¨ande ( And-States“). ” Statecharts erlauben grunds¨atzlich Nichtdeterminismus. Dieser kann zu echtem Nichtdeterminismus der Implementierung fuhren, ¨ aber auch als Unterspezifikation des Modells interpretiert und durch detailliertere Angaben sp¨ater behoben werden. Spontane (ε-)Transitionen modellieren Beobachtungen, bei der der auslosende ¨ Stimulus unsichtbar bleibt (zum Beispiel ein interner Methodenaufruf oder ein Timer). Damit sind spontane Transitionen nur eine besondere Form des Nichtdeterminismus. Ein Statechart kann unvollst¨andig sein, indem es fur ¨ bestimmte Kombinationen aus Zust¨anden und Stimuli keine Transitionen zur Verfugung ¨ stellt. In diesem Fall ist uber ¨ die Implementierung nichts ausgesagt, also eine robuste Implementierung moglich. ¨ Insbesondere wird nicht gefordert, dass die Implementierung den Stimulus ignoriert. Mit einem geeigneten Stereotyp l¨asst sich bei Unvollst¨andigkeit ein spezifisches Verhalten, zum Beispiel durch Transition in einen Fehlerzustand festlegen.
Die beiden prim¨aren Aufgaben von Statecharts sind die Darstellung von Lebenszyklen von Objekten und des Verhaltens einzelner Methoden. 2.4.2 Darstellung von Statecharts Abbildung 2.35 zeigt einen einzelnen Zustand, der von der Klasse Auction eingenommen werden kann und neben dem Namen AuctionOpen eine Zustandsinvariante, je eine entry-Aktion und exit-Aktion sowie eine do-Aktivit¨at beinhaltet.
38
¨ 2 Kompakte Ubersicht zur UML/P
Abbildung 2.35. Ein Zustand der Klasse Auktion mit Invariante und Aktionen
Da ein Diagrammzustand einer Menge von Objektzust¨anden entspricht, kann die in OCL beschriebene Zustandsinvariante dazu verwendet werden, diese Beziehung herzustellen. Zustandsinvarianten mussen ¨ nicht disjunkt sein. Sind sie disjunkt, so spricht man von Datenzust¨anden, ansonsten von Kontrollzust¨anden. Der Datenzustand eines Objekts wird durch die Attribute des eigenen und gegebenenfalls abh¨angiger Objekte bestimmt. Der Kontrollzustand manifestiert sich im laufenden System zus¨atzlich durch den Programmz¨ahler und den Aufrufkeller. Eine Zustandsinvariante kann nur uber ¨ den Datenzustand sprechen. Hierarchie kann bei Zust¨anden zur Verhinderung einer Zustandsexplosion eingesetzt werden. Ein hierarchisch unterteilter Zustand hat wie jeder andere Zustand einen Namen und kann eine Zustandsinvariante, eine entryund exit-Aktion und eine do-Aktivit¨at enthalten. Die flache und hierarchische Darstellung von Zust¨anden im Statechart sind wie in Abbildung 2.36 illustriert a¨ quivalent, wenn die Zustandsinvarianten entsprechend beachtet werden.
Abbildung 2.36. Einfuhrung ¨ und Expansion von Hierarchie
2.4 Statecharts
39
Abbildung 2.29 zeigt die Markierung fur ¨ Start- und Endzust¨ande auf oberster Ebene. Innerhalb eines hierarchisch zergliederten Zustands konnen ¨ ebenfalls Start- und Endzust¨ande markiert werden. Sie haben dann allerdings eine etwas andere Bedeutung (siehe Band 1). Wenn das Objekt im Quellzustand einer Transition ist und die Schaltbedingung erfullt, ¨ dann kann die Transition durchgefuhrt ¨ werden. Dabei wird eine Aktion ausgefuhrt ¨ und der Zielzustand der Transition eingenommen. Stimuli Fur ¨ Stimuli, die zur Auslosung ¨ einer Transition fuhren, ¨ werden funf ¨ verschiedene Kategorien unterschieden: • • • • •
Eine Nachricht wird empfangen, ein Methodenaufruf erfolgt, das Ergebnis eines Return-Statements wird als Antwort auf einen fruher ¨ abgeschickten Aufruf zuruckgegeben, ¨ eine Exception wird abgefangen oder die Transition tritt spontan auf.
Fur ¨ das empfangende Objekt macht es keinen Unterschied, ob ein Methodenaufruf asynchron oder als normaler Methodenaufruf ubermittelt ¨ wird. Im Statechart wird deshalb auch keine Unterscheidung zwischen diesen beiden Arten von Stimuli getroffen. Es ergeben sich deshalb die in Abbildung 2.37 dargestellten Arten von Stimuli fur ¨ Transitionen.
Abbildung 2.37. Arten von Stimuli fur ¨ Transitionen
Die letztgenannten drei Formen von Stimuli spielen nur in Methodenstatecharts eine Rolle, in denen das Innenleben von Methoden beschrieben wird. Die Transitionen spiegeln dann eine Abstraktion des Kontrollflusses der Methode wider. Abbildung 2.38 enth¨alt eine vereinfachte Darstellung des Ablaufs der Methode login(), die im Client-Applet verwendet wird, um nach eingegebenen Namen und Passwort die Verbindung zum Server herzustellen und die Auktionsdaten zu laden.
40
¨ 2 Kompakte Ubersicht zur UML/P
Abbildung 2.38. Statechart fur ¨ die login-Methode
Schaltregeln Die Schaltbereitschaft einer Transition l¨asst sich wie folgt charakterisieren: 1. Das Objekt muss in einem Objektzustand sein, der zum Quellzustand der Transition korrespondiert. 2. Entweder ist die Transition spontan und beno¨ tigt daher keinen auftretenden Stimulus oder der fur ¨ die Transition notwendige Stimulus ist aufgetreten. 3. Die in der Stimulusbeschreibung angegebenen Werte (zum Beispiel Methodenparameter) stimmen mit den tats¨achlichen Werten des angekommenen Stimulus uberein. ¨ Sind in der Stimulusbeschreibung Variablen angegeben, so werden diese mit den tats¨achlichen Werten belegt. 4. Die Vorbedingung, die uber ¨ den Objektzustand und die Parameter des angekommenen Stimulus evaluiert wird, gilt. Es kann vorkommen, dass eine Vorbedingung in keiner Situation erfullt ¨ werden kann. In diesem Fall ist die Transition sinnlos, da sie nie durchgefuhrt ¨ wird. Ist eine Transition schaltbereit so muss sie nicht notwendigerweise auch durchgefuhrt ¨ werden. Es ist durch Nichtdeterminismus (Unterspezifikation) moglich, ¨ dass mehrere Transitionen gleichzeitig schaltbereit sind. Nichtdeterminismus im Statechart bedeutet aber nicht notwendigerweise, dass die Implementierung nichtdeterministisch ist. Abbildung 2.39 zeigt zwei erlaubte Situationen uberlappender ¨ Schaltbereiche. In beiden F¨allen (a) und (b) sind jeweils beide Alternativen moglich. ¨ Mit expliziten Priorit¨aten kann Fall (a) deterministisch aufgelost ¨ werden. Fur ¨
2.4 Statecharts
41
Abbildung 2.39. Situationen uberlappender ¨ Schaltbereiche
Nichtdeterminismus auf verschiedenen Hierarchiestufen wird daruber ¨ hinaus mit dem Stereotyp prio:inner bzw. prio:outer allgemein festgelegt, ob innere oder a¨ ußere Transitionen den Vorzug erhalten. Im Fall eines unvollst¨andigen Statechart kann ebenfalls durch Einsatz eines Stereotypen das Verhalten pr¨azisiert werden. 1. Mit completion:ignore wird der Stimulus ignoriert. 2. Ein Fehlerzustand wird eingenommen, wenn ein solcher Zustand mit error markiert ist. Fur ¨ das Bearbeiten von Exceptions kann ein weiterer Zustand mit exception markiert werden. 3. Vollige ¨ Unterspezifikation am Statechart wird durch den Stereotyp completion:chaos zugelassen. Wie in Abschnitt 6.2.6, Band 1 diskutiert, ergeben sich damit Unterschiede in der Interpretation des Lebenszyklus eines Objekts. In den ersten beiden Interpretationen wird der Lebenszyklus als maximal m¨oglich, in der letzten als minimal zugesichert verstanden. Die Verwendung von completion:chaos ist vor allem in der Spezifikationsphase interessant, wenn durch Verfeinerungen das Verhalten noch detailliert, aber nicht ver¨andert werden soll. Aktionen Aktionen beschreiben die Reaktion auf den Empfang eines Stimulus in einem bestimmten Zustand, indem sie dem Zustand als entry- beziehungsweise exit-Aktion oder der Transition als Reaktion hinzugefugt ¨ werden. UML/P stellt zwei Arten von Aktionen zur Verfugung. ¨ Eine prozedurale Form erlaubt die Nutzung von Zuweisungen und Kontrollstrukturen und eine beschreibende Aktionsform ermoglicht ¨ es, den Effekt einer Aktion zu charakterisieren, ohne festzulegen, wie diese Aktion tats¨achlich realisiert wird. Prozedurale Aktionen werden mit Java realisiert, beschreibende Aktionen ( Aktionsbedingungen“) mit OCL spezifiziert. Abbildung 2.40 beinhal” tet einen Ausschnitt aus einem Statechart fur ¨ die Klasse Auction, in der eine
42
¨ 2 Kompakte Ubersicht zur UML/P
Transition mit prozeduraler Aktionsbeschreibung und einer Nachbedingung zu sehen ist.
Abbildung 2.40. Transition mit prozeduraler und beschreibender Aktion
Aktionsbedingungen konnen ¨ einerseits als redundantes Addendum zu Aktionsanweisungen formuliert werden, um damit zum Beispiel bei Tests eingesetzt zu werden, und andererseits konnen ¨ Aktionsbedingungen die Anweisungen erg¨anzen. So kann ein Teil des Verhaltens bereits prozedural festgelegt und ein anderer Teil noch durch eine OCL-Bedingung beschreibend charakterisiert sein. Die Kombination von entry- und exit-Aktionen aus Zust¨anden und Transitionsaktionen h¨angt von der Form der vorgegebenen Transition ab. Abbildung 2.41 zeigt den Transfer prozeduraler Aktionen auf die Transitionen und demonstriert, in welcher Reihenfolge entry- beziehungsweise exit-Aktionen in hierarchischen Zust¨anden ausgefuhrt ¨ werden. Prozedurale Aktionen werden also sequentiell komponiert.
Abbildung 2.41. Entry- und exit-Aktion in hierarchischen Zust¨anden
2.4 Statecharts
43
Sind die Aktionen eines Statecharts durch OCL-Aktionsbedingungen spezifiziert, so wird, wie in Abbildung 2.42 gezeigt, statt der sequentiellen Komposition die logische Konjunktion verwendet.
Abbildung 2.42. Bedingungen als entry- und exit-Aktion in Zust¨anden
Eine Alternative zur logischen Komposition ist in Band 1 erkl¨art und charakterisiert eine abschnittsweise Gultigkeit ¨ der Bedingungen, die zum Beispiel bei Transitionsschleifen notwendig ist. Erg¨anzungen Zustandsinterne Transitionen sind eigenst¨andige Transitionen, bei denen die entry- oder exit-Aktion des Zustands nicht durchgefuhrt ¨ werden. Abbildung 2.43 zeigt, wie eine zustandsinterne Transition verstanden werden kann.
Abbildung 2.43. Zustandsinterne Transitionen
Wenn ein Zustand eine Situation des Objekts repr¨asentiert, in der eine Aktivit¨at herrscht, zum Beispiel eine Warnmeldung blinkt, kann sie durch
44
¨ 2 Kompakte Ubersicht zur UML/P
eine do-Aktivit¨at beschrieben werden. Abbildung 2.44 charakterisiert deren Interpretation durch einen Timer.
Abbildung 2.44. do-Aktivit¨at als zeitgesteuerte Wiederholung einer Aktion
Die in den beiden Abbildungen 2.43 und 2.44 eingefuhrten ¨ Konzepte werden durch Transformation auf bereits vorhandene Konzepte der Statecharts erkl¨art. In Band 1 ist ein aus 19 Regeln bestehendes Transformationssystem angegeben, das Statecharts vollst¨andig auf flache Mealy-Automaten reduziert und so fur ¨ eine weitere Bearbeitung zug¨anglich macht. Damit besitzen Statecharts einen Transformations-Kalkul, ¨ der fur ¨ die semantikerhaltende Verfeinerung geeignet ist.
2.5 Sequenzdiagramme Sequenzdiagramme werden zur Modellierung von Interaktionen zwischen Objekten eingesetzt. Ein Sequenzdiagramm stellt einen exemplarischen Ausschnitt aus dem Ablauf eines Softwaresystems dar. Es modelliert die dabei auftretenden Interaktionen und Aktivit¨aten und kann um OCL-Bedingungen erweitert werden. Ein Sequenzdiagramm beschreibt, in welcher Reihenfolge Methodenauf¨ rufe durchgefuhrt ¨ und beendet werden. Ahnlich wie bei Statecharts werden daher Verhaltensaspekte modelliert. Es ergeben sich jedoch einige wesentliche Unterschiede: • •
•
Ein Sequenzdiagramm stellt die Interaktion zwischen Objekten in den Vordergrund. Der innere Zustand eines Objekts wird dagegen nicht dargestellt. Ein Sequenzdiagramm ist grunds¨atzlich exemplarisch. Genau wie beim Objektdiagramm kann daher die dargestellte Information w¨ahrend des Ablaufs eines Systems beliebig h¨aufig, mehrfach parallel oder auch gar nicht auftreten. Aufgrund der Exemplarizit¨at sind Sequenzdiagramme nicht zur vollst¨andigen Modellierung von Verhalten geeignet und werden vor allem w¨ahrend der Anforderungsdefinition und, wie in diesem Band gezeigt, zur Modellierung von Testf¨allen eingesetzt.
2.5 Sequenzdiagramme
45
Abbildung 2.45 beinhaltet ein typisches Sequenzdiagramm. Die wesentlichen im Sequenzdiagramm auftretenden Konzepte sind in Abbildung 2.46 kurz erl¨autert.
Abbildung 2.45. Sequenzdiagramm zur Annahme eines Gebots
Objekt im Sequenzdiagramm hat dieselbe Bedeutung wie im Objektdiagramm (siehe Abbildung 2.21), wird allerdings nur mit Namen und Typ dargestellt. Mehrere Objekte desselben Typs sind erlaubt. Name oder Typ sind optional. Zeitlinie. Im Sequenzdiagramm werden zeitlich aufeinanderfolgende Ereignisse von oben nach unten dargestellt. Jedes Objekt hat eine Zeitlinie, die das Voranschreiten der Zeit dieses Objekts darstellt. Die Zeit ist nicht maßstabsgetreu und muss auch nicht in allen Objekten gleich sein. Die Zeitlinie dient daher nur zur Darstellung zeitlicher Reihenfolgen von Interaktionen. Interaktion. Eine Interaktion zwischen zwei Objekten kann durch eine von mehreren Arten von Stimuli ausgelost ¨ sein. Dazu gehoren ¨ Methodenaufrufe, Returns, Exceptions und asynchrone Nachrichten. Im Sequenzdiagramm konnen ¨ die Parameter von Interaktionen angegeben oder ausgelassen werden. Aktivit¨atsbalken. Zur Bearbeitung eines Methodenaufrufs ist ein Objekt eine gewisse Zeit aktiv. Zur Darstellung dieser Aktivit¨at dient der Aktivit¨atsbalken, der bei Rekursion auch geschachtelt auftreten kann. Bedingung. Zur detaillierten Beschreibung von Eigenschaften w¨ahrend eines Ablaufs konnen ¨ OCL-Bedingungen (engl. Guards“) eingesetzt werden. ” Abbildung 2.46. Begriffsdefinitionen fur ¨ Sequenzdiagramme
46
¨ 2 Kompakte Ubersicht zur UML/P
Im Sequenzdiagramm werden Objekte in einer Reihe nebeneinander dargestellt und mit einer nach unten gerichteten Zeitlinie versehen. Es wird vereinfachend angenommen, dass eine gleichzeitige oder uberkreuzende ¨ Versendung von Nachrichten nicht auftritt. Diese Vereinfachung ist motiviert aus dem Einsatz von Sequenzdiagrammen fur ¨ Testzwecke, in denen Nebenl¨aufigkeit explizit kontrolliert wird, um determinierte Testergebnisse zu erhalten. Dadurch wird die Modellierung mit Sequenzdiagrammen wesentlich vereinfacht, es sind aber unter Umst¨anden bestimmte, in einer Implementierung mogliche ¨ Sachverhalte nicht durch Sequenzdiagramme darstellbar. Die Arten der moglichen ¨ Interaktionen sind in Abbildung 2.47 dargestellt. Jede Interaktion mit Ausnahme des rekursiven Aufrufs wird durch einen waagrechten Pfeil dargestellt, der symbolisiert, dass die dabei verbrauchte Zeit vernachl¨assigt wird. An den Pfeilen werden Methodenaufrufe, Returns oder Exceptions mit oder ohne Argumente in Java angegeben.
Abbildung 2.47. Interaktionsformen im Sequenzdiagramm
Wenn eine Methode statisch ist, so wird sie wie auch im Klassendiagramm unterstrichen. In diesem Fall endet der Pfeil an einer Klasse, die damit analog zu einem Objekt eingesetzt wird. Wird ein Objekt w¨ahrend der Interaktion neu erzeugt, wird dies dargestellt, indem die Zeitlinie des Objekts sp¨ater beginnt. Abbildung 2.48 beinhaltet zwei typische Formen von Objekterzeugung. Wie Abbildung 2.45 zeigt, konnen ¨ in einem Sequenzdiagramm zus¨atzlich zu bestimmten Zeitpunkten des Ablaufs geltende Bedingungen angegeben werden. In diesen OCL-Bedingungen werden die im Sequenzdiagramm auftretenden Objekte und Werte in Beziehung gesetzt. Dazu ist der Zugriff auf benannte Objekte sowie Variablen moglich, ¨ die als Argumente in Methodenaufrufen eingesetzt sind. Eine OCL-Bedingung kann die auftretenden Objekte und Parameter sowie den Effekt einer Nachricht auf einem Objekt genauer beschreiben, wobei sich der Kontext aus dem Sequenzdiagramm ergibt. Ein Zugriff auf Attribute ist durch die Qualifizierung mit dem Objekt moglich. ¨ Ist ein OCL-Ausdruck
2.5 Sequenzdiagramme
47
Abbildung 2.48. Erzeugung eines neuen Objekts
allerdings ausschließlich uber ¨ einer Zeitlinie, so ist er diesem Objekt zugeordnet und auf die Attribute dieses Objekts kann direkt zugegriffen werden. Die Gultigkeit ¨ einer OCL-Bedingung bezieht sich auf den Zeitpunkt unmittelbar nach der letzten aufgetretenen Interaktion beziehungsweise dem letzten aufgetretenen Aktivit¨atsbalken. Das bedeutet, die OCL-Bedingung ist nur unmittelbar nach dieser Interaktion einzuhalten. Soll in der Bedingung auf einen fruheren ¨ Wert eines Attributs zugegriffen werden, so ist dieser explizit in einer Zwischenvariable abzulegen. Wie Abbildung 2.49 zeigt, wird dazu ein an OCL angelehntes let-Konstrukt verwendet, das neue Variablen einfuhrt, ¨ die nur innerhalb des Sequenzdiagramms zugreifbar sind.
Abbildung 2.49. Hilfsvariablen speichern Werte
Semantik eines Sequenzdiagramms Auf den exemplarischen Charakter eines Sequenzdiagramms wurde bereits hingewiesen. Fur ¨ den methodischen Einsatz von Sequenzdiagrammen sind mehrere Formen der Exemplarizit¨at zu unterscheiden. Die Exemplarizit¨at eines Sequenzdiagramms basiert analog zum Objektdiagramm auf der Beschreibung einer Menge von Objekten, die in dieser Form in beliebiger
48
¨ 2 Kompakte Ubersicht zur UML/P
H¨aufigkeit oder auch gar nicht, in einem System auftreten kann. Außerdem ist der Ablauf an sich exemplarisch, der in dieser Form sogar beliebig h¨aufig nebeneinander und verschachtelt, aber auch gar nicht auftreten kann. Zu diesen Formen der Exemplarizit¨at kommt hinzu, dass ein Sequenzdiagramm eine Abstraktion eines Ablaufs darstellt, da es weder alle Objekte des Systems, noch alle auftretenden Interaktionen beinhalten kann. Bei einem Sequenzdiagramm konnen ¨ auch zwischendurch Interaktionen fehlen. Findet ein Methodenaufruf im System w¨ahrend des Beobachtungszeitraums mehrfach statt, so entsteht zus¨atzlich eine mehrdeutige Situation, welcher tats¨achliche Methodenaufruf mit der im Diagramm dargestellten Interaktion korrespondiert. Mithilfe geeigneter Stereotypen kann jedoch die Beobachtungsform gec “ zeigt bei Objeknauer festgelegt werden. Der Repr¨asentationsindikator ” ten im Sequenzdiagramm die Vollst¨andigkeit der gezeigten Interaktionen an. Der alternative Repr¨asentationsindikator . . .“ fur ¨ Unvollst¨andigkeit gilt per ” Default. Bei einem in diesem Sinn vollst¨andig beobachteten Objekt ergibt sich notwendigerweise, dass zu jedem Aufruf ein passender Return angegeben sein muss. Dabei sind alle Objekte anzugeben, die direkt mit dem beobachteten Objekt interagieren. Dies kann zu erheblich detaillierteren Diagrammen fuhren, ¨ als eigentlich gewunscht ¨ wird. Deshalb werden in Band 1 weitere Stereotypen eingefuhrt, ¨ die zus¨atzliche Varianten der Semantikdefinition erlauben. Der Stereotyp match:visible ist zum Beispiel geeignet fur ¨ die Definition von Sequenzdiagrammen fur ¨ Tests: match:visible verbietet das Auslassen von Interaktionen mit anderen im Sequenzdiagramm angegebenen Objekten, erlaubt jedoch das Fehlen von Interaktionen mit Objekten, die im Diagramm nicht angegeben sind. Die Beobachtung dieses Objekts ist also vollst¨andig in Bezug auf die im Diagramm sichtbaren Objekte.
3 Prinzipien der Codegenerierung
Der Worte sind genug gewechselt, lasst mich auch endlich Taten sehen. Faust, Johann Wolfgang von Goethe
Codegenerierung ist ein wesentlicher Erfolgsfaktor fur ¨ den Einsatz von Modellen im Softwareentwicklungsprozess. Aus vielen Modellen kann Code fur ¨ das Produktionssystem oder fur ¨ Testtreiber effizient generiert und damit die Konsistenz zwischen Modell und Implementierung verbessert sowie Ressourcen eingespart werden. Dieses Kapitel beschreibt grundlegende Konzepte, Techniken und Probleme der Codegenerierung und skizziert eine Darstellungsform fur ¨ Regeln zur Codegenerierung in Form von Transformationsregeln.
3.1 3.2 3.3 3.4
Konzepte der Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . Techniken der Codegenerierung . . . . . . . . . . . . . . . . . . . . . . Semantik der Codegenerierung . . . . . . . . . . . . . . . . . . . . . . . Parametrisierung eines Werkzeugs . . . . . . . . . . . . . . . . . . . .
52 60 68 71
50
3 Prinzipien der Codegenerierung
Die Moglichkeit, ¨ aus einem Modell ablauff¨ahigen Code zu erzeugen, bietet interessante Perspektiven bei der Softwareentwicklung und ist teilweise sogar eine wesentliche Voraussetzung fur ¨ • •
• • •
die Steigerung der Effizienz der Entwickler, die Trennung von Anwendungsmodellierung und technischem Code, die die Wartbarkeit und die Weiterentwicklung der Funktionalit¨at sowie die Portierung auf neue Hardware und Betriebssystemversionen besser unterstutzt, ¨ Rapid Prototyping mit Hilfe von Modellen, die eine kompaktere Beschreibung des Systems erlauben, als es durch eine rein textuelle Programmiersprache wie Java moglich ¨ w¨are, schnelles Feedback durch Demonstrationen und Testl¨aufe und einen wesentlichen Aspekt der Qualit¨atssicherung: der Generierung von automatisierten Tests.
Eine der St¨arken der Generierung ist die Moglichkeit ¨ zur Konzentration redundanter, das heißt sich fur ¨ viele Komponenten wiederholender Codefragmente (oder Aspekte“, [LOO01, KLM+ 97]) an einer Stelle und damit ” die Reduktion der Große ¨ der manuell zu erstellenden Dokumente. Dies wiederum fuhrt ¨ zu weniger Programmierfehlern, großerer ¨ Konformit¨at des generierten Codes zu Codierungsstandards1 sowie zur schnelleren Erstellung und flexibleren Anpassung des Systems auf Basis von Modellen. Probleme heutiger Werkzeuge Die Erzeugung von ablauff¨ahigem Code aus einem Modell ist daher derzeit zurecht eine der wesentlichen Anstrengungen der Hersteller von Modellierungswerkzeugen. Dies gilt nicht nur fur ¨ UML-basierte Werkzeuge, sondern auch fur ¨ Werkzeuge mit anderen Notationen, wie Autofocus [HSSS96], der in der Telekommunikation verwendeten SDL [IT99c, IT99b] oder den in Statemate und Rhapsody umgesetzten Statecharts [HN96]. Aufgrund dieser vielf¨altigen Anstrengungen ist davon auszugehen, dass sich die Situation bei der Codegenerierung in den n¨achsten Jahren weiter verbessern wird. Viele der heute existierenden Werkzeuge bieten bereits die Generierung von Code oder Coderahmen aus Teilen der UML an. Jedoch gibt es hier eine Reihe grunds¨atzlicher Probleme, die auch konzeptioneller Verbesserungen bedurfen. ¨ 1. Die Erzeugung von Coderahmen aus Klassendiagrammen ist mittlerweile Stand der Technik. Dabei werden Hulsen ¨ fur ¨ die Klassen erzeugt, die Attributdefinitionen und Zugriffsfunktionen beinhalten. Die Rumpfe ¨ generierter Methoden sind manuell einzutragen. Da detaillierte Modelle ¨ im Projekt meist einer hohen Anderungsrate unterworfen sind, mussen ¨ 1
Das ist zum Beispiel bei einer Zertifizierung des Systems wichtig.
3 Prinzipien der Codegenerierung
2.
3.
4.
5.
51
die manuell eingesetzten Coderumpfe ¨ nach jeder Generierung neu nachgetragen werden oder gehen verloren. Als Ausweg wird deshalb das Roundtrip-Engineering“ verwendet. ” Roundtrip-Engineering erlaubt die wechselseitige Transformation von Code in Klassendiagramme und umgekehrt. Wesentlich ist dabei, dass ¨ beide Sichten manuell a¨ nderbar sind, ohne dass die Anderungen in der jeweils anderen Sicht verloren gehen. Insbesondere bleiben Methodenrumpfe ¨ in der Code-Sicht erhalten. Wenn aber eine moglichst ¨ kompakte Darstellung des Systems gewunscht ¨ ist, dann ist das eine Sackgasse. Sinnvoller ist vielmehr nur eine Darstellung anzubieten, die graphische Klassendiagramme und Coderumpfe ¨ integriert. Die wesentlichen Hindernisse dafur ¨ sind das derzeit noch zu geringe Zutrauen des Entwicklers in den generierten Code, so dass ein manueller Eingriff in den generierten Code noch gewunscht ¨ wird, und die nicht zufriedenstellend gekl¨arte Frage, wie Coderumpfe ¨ abgelegt werden, so dass sie vom Entwickler effizient bearbeitet werden konnen. ¨ Mit den ersten Compilern war die Situation jedoch a¨ hnlich. Es wurde Assembler-Quellcode erzeugt, der manuell a¨ nderbar sein sollte. Es kann davon ausgegangen werden, dass mit zunehmender Reife der Generierungstechnologie die Ebene des Java-Quellcodes unwichtiger wird und Bytecode direkt erzeugt werden kann. Werden Coderumpfe ¨ wie beim Roundtrip-Engineering direkt in den generierten Code eingesetzt, so ist in diesen Coderumpfen ¨ keine Abstraktion von der konkreten Realisierung von Attributen, Assoziationen, etc. moglich. ¨ Stattdessen muss der Entwickler die Form der Umsetzung und die daraus resultierenden Zugriffsfunktionen kennen. Werden Coderumpfe ¨ stattdessen ebenfalls aus abstrakten“ Coderumpfen ¨ gene” riert, so konnen ¨ zum Beispiel Attributzugriffe durch entsprechende getund set-Methoden ersetzt werden. Auch ist dann die Instrumentierung des Codes fur ¨ Tests besser moglich. ¨ Leider stimmt die dokumentierte oder den Analyse- und RefactoringTechniken zugrunde liegende Semantik mit dem bei der Codegenerierung entstandenen Verhalten gelegentlich nicht uberein. ¨ Dies ist ein generelles Problem, das einer sorgf¨altigen Festlegung von Codegenerierung, Analysen, Refactoring-Techniken und der dokumentierten Semantik bedarf. Denn sonst ist es moglich, ¨ dass ein bezuglich ¨ der offiziellen Semantik korrektes Refactoring doch zu einem ver¨anderten Systemverhalten fuhrt. ¨ Einfache Werkzeuge generieren oft eine starre Form von Code, ohne auf die spezifischen Bedurfnisse ¨ des Projekts einzugehen. Eine Parametrisierung der Codegenerierung ist wunschenswert ¨ und an vielen Stellen auch notwendig, um zum Beispiel plattformspezifische Anpassungen in den Code einzubauen, die Nutzung von Frameworks, Speicher- und Kommunikationstechniken zu erlauben oder die Optimierung der Umsetzung von Modellelementen zu ermoglichen. ¨
52
3 Prinzipien der Codegenerierung
Die moglichen ¨ Formen der Codegenerierung sind vielf¨altig und konnen ¨ kaum direkt antizipiert werden. Deshalb ist einerseits eine flexible Skriptsprache2 notwendig, andererseits dennoch zu sichern, dass die essenti” elle Semantik“ der Diagramme durch die Codegenerierung nicht verloren geht. In diesem Kapitel werden grundlegenden Konzepte zur Codegenerierung diskutiert und ausschnittsweise anhand der UML/P-Notation erl¨autert. Fur ¨ ein weitergehendes Studium von Gererierungstechniken wird [CE00] empfohlen. Der Abschnitt 3.1 erortert ¨ Konzepte von Codegeneratoren, die auch die im Kapitel 6 diskutierte Verwendung der UML zur Modellierung von Testf¨allen behandelt. In Abschnitt 3.2 werden Anforderungen wie Flexibilit¨at, Plattformunabh¨angigkeit und Steuerbarkeit eines Codegenerators diskutiert. Abschnitt 3.3 behandelt die Beziehung zwischen Codegenerator und Semantikdefinition der Sprache. Abschnitt 3.4 beschreibt, wie ein flexibler Codegenerator aussehen kann.
3.1 Konzepte der Codegenerierung In Vorwegnahme der nachfolgend diskutierten konzeptionellen Grundlagen werden in Abbildung 3.1 die wesentlichen Begriffe einfuhrend ¨ definiert. Der Einsatz eines Codegenerators hat einige Vorteile gegenuber ¨ konventioneller Programmierung. Die Verst¨andlichkeit der benutzten Modellierungsbeziehungsweise Programmiersprache wird erhoht, ¨ indem die Sprache kompakter und durch graphische Elemente ubersichtlicher ¨ wird. Die Effizienz der Softwareentwicklung wird erhoht. ¨ Allein dadurch, dass weniger Code manuell zu schreiben, prufen ¨ und testen ist, konnen ¨ Entwickler ihre Effizienz steigern. Zus¨atzliche Aspekte, wie die bessere Wiederverwendbarkeit von abstrakten Modellen aus einer Modellbibliothek, steigern die Entwicklereffizienz weiter. Dies fuhrt ¨ zu einer Reduktion des Gesamtaufwands fur ¨ die Softwareentwicklung. Dadurch wird weniger Projektmanagement notwendig, wodurch weitere Effizienzsteigerungen moglich ¨ werden. Die Wiederverwendbarkeit ist dabei auf mehreren Ebenen moglich. ¨ Ein Modell kann in angepasster Form in einem a¨ hnlichen Projekt wiederverwendet werden. Idealerweise kann durch wiederholte Anpassung ein ModellFramework entstehen, das fur ¨ gleichartige Projekte verwendbar ist und sogar einen geeignet angepassten Codegenerator besitzt. Das im Codegenerator eingebettete technische Wissen beispielsweise zur Erzeugung von Schnitt¨ stellen oder sicherer und effizienter Ubertragungsmechanismen kann unabh¨angig davon wiederverwendet werden. Eine dritte Moglichkeit ¨ zur Wiederverwendung von Modellen ergibt sich innerhalb eines Projekts. Ein Objektdiagramm kann zum Beispiel sowohl als Pr¨adikat als auch konstruk2
Assistenten“ oder Wizards“ bieten zus¨atzlich Werkzeugunterstutzung ¨ bei der ” ” Skripterstellung.
3.1 Konzepte der Codegenerierung
53
Konstruktives Modell ist die Spezifikation eines Systems, die mithilfe eines automatischen Generators zur Codegenerierung eingesetzt wird. Die Ver¨anderung eines konstruktiven Modells hat die direkte Ver¨anderung des Produktionssystems zur Folge. Die Spezifikationssprache wird auch als High-LevelProgrammiersprache angesehen, da sie grunds¨atzlich ausfuhrbar ¨ ist. Deskriptives Modell ist eine Spezifikation, die zur Beschreibung des Systems verwendet wird, ohne konstruktiv bei der Implementierung eingesetzt zu werden. Das sind typischerweise abstrakte und unvollst¨andige Beschreibungen, also insbesondere Modelle, die als Vorlage fur ¨ eine manuelle Implementierung dienen. Testmodell ist eine Spezifikation, die fur ¨ die manuelle oder automatische Ableitung von Tests geeignet ist. Das Testmodell wird in ausfuhrbaren ¨ Code ubersetzt, ¨ der zum Aufbau von Testdatens¨atzen, als Testtreiber oder als Test-Sollergebnis eingesetzt wird. Konstruktives Testmodell ist ein Testmodell, das durch den Codegenerator in ausfuhrbare ¨ Tests ubersetzt ¨ wird. Demgegenuber ¨ werden deskriptive Testmodelle manuell in Tests ubersetzt. ¨ Codegenerierung ist der Vorgang zur Erzeugung von Code aus konstruktiven Modellen. Codegenerator ist ein Programm, das ein konstruktives Modell einer h¨oheren Programmiersprache in eine Implementierung transformiert (nach [CE00]). Der generierte Code kann zum Produktionssystem oder zum Testcode geh¨oren. Skript beinhaltet die konstruktive Steuerung der Codegenerierung. Skripte parametrisieren den Codegenerator und erlauben damit plattform- und aufgabenspezifische Codegenerierung. Template ist eine spezielle Form eines Skripts. Ein Template beschreibt Codemuster, in die bei der Generierung konkrete Elemente des Modells eingesetzt werden. Dabei wird typischerweise ein Makro-Ersetzungsmechanismus verwendet. Abbildung 3.1. Begriffsdefinitionen zur Codegenerierung
tiv zur Erzeugung einer Objektstruktur eingesetzt werden. Beide Formen konnen ¨ außerdem im Produktionssystem oder bei der Testfalldefinition eingesetzt werden. Der dafur ¨ generierte Code ist, wie in Abschnitt 4.2 ersichtlich, sehr unterschiedlich und daher manuell viel aufw¨andiger zu erstellen. In sehr eingeschr¨anktem Maße sind Codegeneratoren heute auch bereits in der Lage, effizienteren Code zu erstellen, als dies in vertretbarem Aufwand durch manuelle Optimierungen moglich ¨ w¨are. Dies gilt vor allem fur ¨ ausgereifte Compiler normaler Programmiersprachen, die eine Reihe von Optimierungstechniken einsetzen. Fur ¨ ausfuhrbare ¨ Modellierungssprachen wie die UML/P ist davon auszugehen, dass die Steigerung der Effizienz der Entwickler derzeit durch eine weniger effiziente Implementierung erkauft werden muss. Entsprechend ist der Einsatz von Generatoren fur ¨ Modellierungssprachen vor allem bei Individualsoftware und erst in zweiter Linie bei eingebetteter, massenhaft in Billigger¨aten vertriebener Systemsoftware sinnvoll. Die flexible Generierung von Code aus fachlichen Modellen erlaubt letztendlich auch die Behandlung von technischen und teilweise fachlichen Va-
54
3 Prinzipien der Codegenerierung
riabilit¨aten im Sinne von [HP02]. Dabei werden offene Aspekte des Modells durch einen jeweils projektspezifisch angepassten Generator geeignet ausgefullt. ¨ 3.1.1 Konstruktive Interpretation von Modellen Wie bereits in Band 1 beschrieben, ist ein Modell seinem Wesen nach eine in Maßstab, Detailliertheit oder Funktionalit¨at verkurzte ¨ beziehungsweise abstrahierte Darstellung des originalen Systems [Sta73]. Modelle werden immer dort eingesetzt, wo das tats¨achliche System so komplex ist, dass es sich zun¨achst lohnt, bestimmte Eigenschaften des Systems am Modell zu analysieren oder dem Kunden zu erkl¨aren. Dazu gehoren ¨ Architekturmodelle von Geb¨auden ebenso wie technische Modelle komplexer Maschinen. Bei manchen Modellen, wie zum Beispiel Baupl¨anen oder Schaltzeichnungen, steht hier der Wunsch nach einer Beschreibung des Aufbaus (Architektur) im Vordergrund, bei anderen die Simulation von Funktionalit¨at und anderer verhaltensorientierter Eigenschaften.3 Generell gilt aber, dass diese Modelle und Bauzeichnungen als Hilfsmittel fur ¨ die sp¨atere Erstellung des Artefakts dienen. Wird das Modell erstellt, um danach das eigentliche Artefakt zu bilden, so hat das Modell eine vorschreibende (pr¨askriptive) Wirkung. Im Gegensatz dazu wird ein Modell beschreibend (deskriptiv) eingesetzt, wenn das Original vor dem Modell existiert. Beispiele hierzu sind etwa eine Modelleisenbahn oder Fotografien [Lud02]. Aufgrund der Immaterialit¨at von Software entfalten Modelle in der Softwareentwicklung zus¨atzlich zur deskriptiven Wirkung auch eine konstruktive Wirkung. Wenn fur ¨ die Generierung lauff¨ahiger Software aus einem immateriellen, im Computer gespeicherten Modell nur ein Knopfdruck notwendig ist, dann wirkt das Modell als konstruktiver Mechanismus. Der Quellcode ¨ einer Programmiersprache kann aufgrund der automatisierten Ubersetzung als zu dem erzeugten Objektcode a¨ quivalent angesehen werden. Streng genommen sind Quellcode und Objectcode ebenfalls Modelle des Systems. Fur ¨ praktische Belange werden sie jedoch – und mit Recht – mit dem System selbst identifiziert. Dasselbe kann auch fur ¨ ausfuhrbare ¨ UML/P-Modelle gefordert werden. Die konstruktive Verwendung der Modelle hat konkrete Auswirkungen, die bei einem nicht-konstruktiven Einsatz nicht auftreten. Zum Beispiel ver¨andert die Hinzunahme oder das Weglassen von Elementen des Modells sofort das modellierte Artefakt. Ein Beispiel ist die Verwendung mehrerer Statecharts zur Modellierung des Verhaltens einer Klasse auf verschiedenen Abstraktionsstufen. Ein Generator, der mit dieser Situation umgehen kann, simuliert diese parallel und realisiert damit ein mehrdimensionales 3
Fur ¨ eine tiefergehende Diskussion des Modellbegriffs eignen sich zum Beispiel [Sta73] oder [Lud02].
3.1 Konzepte der Codegenerierung
55
Zustandskonzept fur ¨ eine Klasse.4 Wird nun ein weiteres Statechart als Modell hinzugenommen, das ausschließlich bereits vorhandene Information in abstrakterer Form darstellt, so wird das Zustandskonzept weiter aufgebl¨aht. Das a¨ ndert zwar nicht das funktionale Gesamtverhalten, wohl aber die interne Struktur und das Zeitverhalten des Systems. Modelle werden also in der Softwareentwicklung in verschiedenen Rollen eingesetzt. Dazu gehoren ¨ die automatisierte Generierung von Produktionscode, aber auch von Tests. Die manuelle Umsetzung eines Modells ist ebenso moglich, ¨ wie die Erstellung von Modellen, nachdem das Artefakt bereits existiert. Abbildung 3.2 charakterisiert die drei Dimensionen zur Unterscheidung des Einsatzes von Modellen.
Abbildung 3.2. Varianten des Einsatzes von Modellen
Streng genommen ist die Erstellung eines konstruktiv verwendeten Modells nach dem Artefakt moglich ¨ und zum Beispiel im Reverse Engineering sinnvoll. Jedoch wird dieses Modell nicht zur Erstellung des bereits vorhandenen Originals, sondern fur ¨ die n¨achste Version verwendet. Der Unterschied zwischen der konstruktiven und der deskriptiven Interpretation von Modellen ist verwandt zu einem a¨ hnlichen Ph¨anomen, das bei algebraischen Spezifikationssprachen detailliert diskutiert wurde. Einem deskriptiv eingesetzten Modell sollte eine lose Semantik [BFG+ 93] zugeordnet sein. Das heißt, dass verschiedene Implementierungen existieren, die 4
Dabei wird davon ausgegangen, dass Werkzeuge nicht automatisch in der Lage sind zu erkennen, dass ein Statechart eine Abstraktion eines anderen ist. Dies kann zum Beispiel aufgrund von verwendeten OCL-Bedingungen der Fall sein.
56
3 Prinzipien der Codegenerierung
das Modell erfullen. ¨ Viele dieser Implementierungen enthalten beispielsweise weitere Zustandskomponenten, Funktionalit¨at oder Schnittstellen, die im Modell nicht explizit erw¨ahnt sind. Ein Klassendiagramm beschreibt dann einen Ausschnitt eines Systems, da noch weitere, ungenannte Klassen besitzen kann. Ein deskriptives Modell kann daher unvollst¨andig sein. In Band 1 wurde eine entsprechende lose Semantik“ fur ¨ Sequenzdiagramme beschrie” ben. Demgegenuber ¨ stellt ein konstruktiv eingesetztes Modell eine vollst¨andige Beschreibung des Softwaresystems dar, da allein aus dem Modell das gesamte lauff¨ahige System generiert wird. Dies entspricht bei algebraischen Spezifikationen einer initialen Semantik, die einem Modell genau eine Implementierung zuordnet.5,6 3.1.2 Tests versus Implementierung Die UML/P erlaubt die Erstellung von Modellen, die sich sowohl zur Testgenerierung als auch zur Generierung fur ¨ das Produktionssystem eignen. Dazu z¨ahlen Objektdiagramme, die, wie in Abschnitt 5.4, Band 1 besprochen, als Vorbedingungen konstruktiv eingesetzt werden, um die Ausgangssituation eines Tests herzustellen, und als Nachbedingungen eingesetzt werden, um zu beschreiben, welche Situation nach Anwendung der Funktion fur ¨ einen Testerfolg erfullt ¨ sein muss. Aus bestimmten Teilen eines Modells l¨asst sich auch kein konstruktiver Code, sondern nur Testcode generieren. Beispielsweise sind OCL-Bedingungen im Allgemeinen ausfuhrbar. ¨ Wie in Abschnitt 4.3.10, Band 1 beschrieben, gilt dies meist auch fur ¨ Quantoren, da mit Ausnahme der Quantoren uber ¨ Grunddatentypen wie int und mengen- beziehungsweise listenwertigen Typen hoheren ¨ Grades alle Quantoren endlich und damit auswertbar sind. Dennoch ist es ein wesentlicher Unterschied, ob eine in OCL formulierte Nachbedingung getestet oder konstruktiv erzwungen werden kann. Nahezu alle praktisch interessanten OCL-Bedingungen fallen in die erste Kategorie. Die Kategorie der konstruktiven OCL-Bedingungen ist allerdings deutlich kleiner. Das demonstrieren die folgenden zwei Beispiele. Sortieren Die Methode sort soll ein Array von Zahlen (int) sortieren. Eine ofter ¨ zu findende Beschreibung in Form einer Vor-/Nachbedingung ist die Folgende: 5
6
In der Theorie algebraischer Spezifikationen werden Modelle als Spezifikationen“ ” und Elemente des semantischen Domains beziehungsweise Implementierungen als Modelle“ bezeichnet. ” Um eine initiale Semantik zu besitzen, mussen ¨ bestimmte Einschr¨ankungen an die Form der Spezifikationen gestellt werden. Siehe dazu auch [EM85].
3.1 Konzepte der Codegenerierung context int[] sort(int a[]) pre: true post: forall int i in {1..result.length-1}: result[i-1] <= result[i]
57 OCL
Diese Spezifikation kann sehr einfach und in linearer Zeit getestet werden. Als konstruktive Beschreibung ist sie allerdings nicht geeignet, weil ein Generator daraus keinen Sortieralgorithmus erzeugen kann. Daruber ¨ hinaus ist sie in wesentlichen Eigenschaften unvollst¨andig, da sie nicht sichert, dass die Ausgangselemente der Reihung a[] in der Ergebnisreihung result[] wieder vorkommen mussen. ¨ Tats¨achlich w¨are daher eine Implementierung der Form result=new int[0] ebenfalls korrekt. Die konstruktive Beschreibung eines Sortieralgorithmus ist zwar im Prinzip moglich, ¨ aber genauso komplex wie eine direkte Implementierung. Gerade bei komplexen Algorithmen zeigt sich der wesentliche Vorteil deskriptiver Beschreibungen, da sie keine Implementierungsform vorwegnehmen. Sie sind daher insbesondere gegenuber ¨ effizienten Implementierungen sehr viel leichter verst¨andlich. Gleichungen als Zuweisungen Zuweisungsmethoden haben im Allgemeinen die einzige Aufgabe, das mog¨ licherweise gekapselte Attribut zu setzen: context void setAttr(Type val) pre: true post: attr==val
OCL
Diese Spezifikation ist sowohl fur ¨ Tests der Methode setAttr als auch fur ¨ eine konstruktive Umsetzung in eine Implementierung geeignet. Wird n¨amlich der Gleichheitsoperator == durch den Java-Zuweisungsoperator = ersetzt, so kann die Nachbedingung als Implementierung verwendet werden. Diese Implementierung ist allerdings nur dann wirklich korrekt, wenn keine weiteren Invarianten existieren, die eine zus¨atzliche Ver¨anderung anderer Attribute erforderlich machen. Leider ist die konstruktive Umsetzung von Nachbedingungen nur unter bestimmten, sehr eng umrissenen Rahmenbedingungen moglich. ¨ Typischerweise darf eine Nachbedingung nur aus einer Konjunktion von Zuweisungen an lokale Variablen bestehen und sp¨atere Zuweisungen durfen ¨ die fruheren ¨ nicht wieder invalidieren. Beispielsweise ist val==attr zur obigen Nachbedingung a¨ quivalent, kann aber in dieser Form nicht in Code umgesetzt werden.7 Auch die nachfolgende Bedingung ist fur ¨ eine Codegenerierung ungeeignet, da sie zyklische Abh¨angigkeiten enth¨alt: 7
Genau genommen ist val=attr in Java erlaubt, hat jedoch einen anderen Effekt als gewunscht. ¨
58
3 Prinzipien der Codegenerierung
context void method(Type val) pre: true post: a==b+1 && b==2*a-val
OCL
Fur ¨ ihre konstruktive Umsetzung ist zun¨achst das lineare Gleichungssystem zu losen ¨ und es kann konstruktiv formuliert werden: context void method(Type val) pre: true post: a==val-1 && b==val-2
OCL
Naturlich ¨ gibt es eine Reihe trickreicher Verfahren zur konstruktiven Interpretation von Bedingungen, die fur ¨ verschiedene Hochsprachen entwickelt wurden. Von denen seien insbesondere die Horn-Klausel-Logik von Prolog [Llo87], die Auswertung von in Gleichungslogik formulierter algebraischer Spezifikationen [EM85] und deren Erweiterung um Konditionale erw¨ahnt. Beispielsweise kann auch die folgende Spezifikation konstruktiv umgesetzt werden: context int abs(int val) pre: true post: if (val>=0) then result==val else result==-val
OCL
Arten der Generierung Da die Notationen der UML/P zur Modellierung von exemplarischen und vollst¨andigen Strukturen und Verhalten eingesetzt werden, eignen sie sich in unterschiedlicher Weise zur Generierung von Code. Abbildung 3.3 zeigt, welche Diagrammart haupts¨achlich (dicker Pfeil) und nebenbei (dunner ¨ Pfeil) fur ¨ welche Form der Code- beziehungsweise Testgenerierung eingesetzt wird. Es ist aber festzuhalten, dass sich nicht alle Konzepte der UML/PDokumente zur Codegenerierung eignen. UML/P erlaubt grunds¨atzlich die Abstraktion von Details, zum Beispiel durch Auslassen von Typinformation bei Attributen oder durch Unterspezifikation bei Methoden und Transitionen, so dass die F¨ahigkeit zur Code- und Testgenerierung aus einem UML/P-Artefakt unter anderem von dessen Vollst¨andigkeit abh¨angt. Intelligente Generierungsalgorithmen konnen ¨ naturlich ¨ auch unvollst¨andige Artefakte zur Generierung nutzen, indem sie die offenen Aspekte durch Defaults ausfullen. ¨ So kann zum Beispiel bei einem unvollst¨andigen Statechart ein standardm¨aßiges Fehlerverhalten hinzugefugt ¨ werden und bei Attributen ohne Typinformation versucht werden, durch Typinferenz an den Stellen der Attributnutzung den benotigten ¨ Typ auszurechnen. Der generierte Produktionscode kann dabei je nach Verwendungszweck mit zus¨atzlichem Testcode instrumentiert sein. So konnen ¨ fur ¨ Testzwecke Inspektionsmethoden, interaktive Haltepunkte, Funktionen zum Zugriff auf private Attribute oder die Prufung ¨ von Invarianten in den Produktionscode integriert sein, die bei der Erzeugung des Produktionscode fur ¨ den Einsatz
3.1 Konzepte der Codegenerierung
59
Abbildung 3.3. Generierung von Code und Tests aus UML/P
als fertiges Produkt weggelassen werden. Diese Form der Instrumentierung birgt Probleme, wenn der optimale Code Seiteneffekte beinhaltet, die das Verhalten des instrumentierten Produktionscodes ver¨andern. Es ist deshalb wichtig, dass eine solche Instrumentierung nicht manuell, sondern von Codegeneratoren durchgefuhrt ¨ wird, so dass verhaltensver¨andernde Seiteneffekte ausgeschlossen werden konnen. ¨ Die durch die Instrumentierung entstandene Ver¨anderung des zeitlichen Verhaltens muss in nebenl¨aufigen Systemen unter gesonderten Gesichtspunkten betrachtet werden. 3.1.3 Tests und Implementierung aus dem gleichen Modell Wie im vorherigen Abschnitt diskutiert, lassen sich aus manchen Modellen einerseits Tests, andererseits aber auch konstruktiver Code generieren. Die Generierung beider Codearten aus demselben Modell kann jedoch kein zus¨atzliches Zutrauen in die Richtigkeit des erstellten Systems erzeugen. Werden aus einem falschen Modell sowohl fehlerhafter Implementierungscode erzeugt als auch die Tests fur ¨ diese Implementierung abgeleitet, so sind die Tests in gleicher Weise falsch. Dies zeigt das folgende einfache Beispiel, das den Absolutwert einer Zahl berechnen soll: context int abs(int val) pre: true post: result==-val
OCL
Die Codegenerierung kann damit folgenden Java-Code erstellen: int abs(int val) { return -val; }
Java
Eine typische Sammlung von Tests benotigt ¨ mehrere Eingabewerte, auf denen getestet wird. Als gute Standardwerte haben sich fur ¨ den Datentyp
60
3 Prinzipien der Codegenerierung
int Sammlungen von Zahlen der Form −n, −2, −1, 0, 1, 2, n fur ¨ einige große n herausgestellt.8 Normalerweise werden diese vom Entwickler vorgegeben. Die erwarteten Ergebnisse mussen ¨ nicht separiert ausgerechnet werden, da mit der Nachbedingung eine Moglichkeit ¨ zur Prufung ¨ der Korrektheit des Ergebnisses existiert. Folgender Testcode wurde ¨ erzeugt werden konnen: ¨ int val[] = new int[] {-1234567,-2,-1,0,1,2,3675675}; for(int i = 0; i
Java/P
Da der Testcode genauso falsch ist wie die Implementierung, wurde ¨ der Fehler damit nicht erkannt werden. In so einer Situation wird eigentlich nicht der implementierte Code getestet, sondern es wird getestet, ob der Codegenerator korrekt funktioniert. Denn wenn in dieser Situation ein Fehler gemeldet werden wurde, ¨ dann wurde ¨ der nur auf eine Inkonsistenz zwischen dem generierten Code und dem ebenfalls generierten Testtreiber hinweisen. Ein solches Vorgehen ist genau dann interessant, wenn die Parametrisierung des Generators getestet werden soll. Als Konsequenz dieser Beobachtung ergibt sich, dass das konstruktive, zur Codegenerierung verwendete Modell und das Testmodell getrennt modelliert werden mussen. ¨ Dabei durfen ¨ Fragmente des Test- und des konstruktiven Modells in denselben Diagrammen dargestellt sein. Es ist jedoch klar zu trennen, welche Konzepte wofur ¨ verwendet werden. Beispielsweise werden Statecharts im Wesentlichen konstruktiv eingesetzt. Die in den Statecharts verwendbaren Zustandsinvarianten werden jedoch bis auf Ausnahmen nur zur Prufung ¨ in Tests eingesetzt.
3.2 Techniken der Codegenerierung 3.2.1 Plattformabh¨angige Codegenerierung Obwohl mit der Festlegung der UML/P auf die Programmiersprache Java bereits eine wesentliche Entwurfsentscheidung getroffen wurde, ist die Form des generierten Codes nicht eindeutig festgelegt. Es gibt mehrere Dimensionen von Variationen, die bei der Codegenerierung zu beachten sind. Dazu z¨ahlt zum einen die hier besprochene Plattformabh¨angigkeit, die besonders bei eingebetteten Systemen eine wesentliche Rolle spielt. Je nach Zielplattform stehen unterschiedliche Mechanismen zur Verfu¨ gung, um zum Beispiel Kommunikation im verteilten System mit den gesteuerten Anlagen, Nachbarsystemen oder Nutzern sowie Speicherung und 8
Der Auswahl der Zahlen liegt eine einfache Klassifikation fur ¨ den Wertebereich int und die Verwendung einzelner Vertreter und Grenzwerte aus den gebildeten Klassen zugrunde.
3.2 Techniken der Codegenerierung
61
Fehlerbehandlung durchzufuhren ¨ oder die Einbruchssicherheit, Datenauthentizit¨at und -integrit¨at sicher zu stellen. Diese Mechanismen konnen ¨ abh¨angig sein von der Hardware, in der die Software eingebettet ist, oder an die zur Verfugung ¨ stehenden Klassenbibliotheken beziehungsweise API’s anzupassen sein. Die dabei in generierten Code einzusetzenden Codestucke ¨ sind vom Codegenerator nicht vorauszusehen, da beispielsweise durch neue Plattformen, neue Steuerger¨ate oder neue Versionen von Klassenbibliotheken ein stetiger und schneller Wandel stattfinden kann. Deshalb ist es wesentlich, dass die Codegenerierung flexibel an die jeweiligen Rahmenbedingungen angepasst werden kann. Dazu gibt es zwei wesentliche Ans¨atze: Generierung fur ¨ abstrakte Schnittstellen, wie in Abbildung 3.4 illustriert, und die Parametrisierung der Codegenerierung, wie in Abbildung 3.5 gezeigt.
Abbildung 3.4. Generierung von Code gegen eine abstrakte Schnittstelle
Fur ¨ die Trennung von plattformspezifischem und hardwareunabh¨angigem Code ist die Bildung einer abstrakten Schnittstelle und damit die Schichtentrennung ein ideales Werkzeug, das die Portabilit¨at von Software verbessert. Viele der Java-API’s sind genau fur ¨ diesen Zweck definiert und zum Standard erhoben worden. In [SD00] wurde diese strikte Trennung von Code in Anwendungscode ( A-Code“) und plattformspezifischen, techni” schen Code ( T-Code“) detaillierter untersucht sowie notwendige Mischfor” men identifiziert. Eines der Ergebnisse dieser durch die Praxis untermauerten Untersuchungen ist dabei, dass eine standardisierte T-Architektur“, also ” der technische Code fur ¨ die Speicherung, Anzeige, Fehlerbearbeitung und a¨ hnliche standardisierbare Funktionalit¨aten, ein hohes Potential zur Wiederverwendung hat. Dieses Potential kann bei der Codegenerierung durch die flexible Kombination von T-Architektur-Anteilen mit den vom Entwickler vorgegebenen Applikations(A)-Modellen ausgeschopft ¨ werden.
62
3 Prinzipien der Codegenerierung
Abbildung 3.5. Parametrisierte Codegenerierung
Die strikte Trennung der beiden Codearten kann aber wie immer bei der Einfuhrung ¨ von Schichten und Adaptern zu Ineffizienzen fuhren. ¨ Beispielsweise basiert das Auktionssystem auf asynchroner Kommunikation von Nachrichten. Wenn die abstrakte Schnittstelle aber nur einen RPC-Mechanismus zur Verfugung ¨ stellt, so muss unter anderem die Pufferung der Nachrichten selbst codiert werden. Auf unterster Ebene wird aber wieder asynchron uber ¨ das Internet kommuniziert, wo Puffermechanismen bereits eingebaut sind. Eine Effizienzsteigerung um einen deutlichen Faktor kann zum Beispiel durch Aufgabe der konzeptuell vorhandenen Schichtenbildung und durch ein Verweben“ hoherer ¨ und niederer Schichten erreicht werden. 9 ” Alternativ kann die angebotene abstrakte Schnittstelle auch breit angelegt sein und im Beispiel sowohl synchronen RPC als auch asynchrone Kommunikation anbieten. Das fuhrt ¨ aber zu erheblichem Mehraufwand bei der Realisierung und Weiterentwicklung und zahlt sich nur aus, wenn ausreichend oft eine Wiederverwendung in anderen Projekten stattfindet. Wird die Generierung des Zielcodes daruber ¨ hinaus als korrekt angenommen und auf eine manuelle Nachbearbeitung oder Inspektion verzichtet, so ist die Schichtenbildung im generierten Code nicht essentiell. Stattdessen kann bei der Generierung mehr auf Effizienz geachtet werden und a¨ hnlich zu Optimierungstechniken der Compiler ein Verweben des plattformunabh¨angigen und -spezifischen Codes erfolgen. Dadurch entsteht nach [SD00] ein sehr schwer wartbarer AT-Code, der sowohl Anwendungs- als auch technisches Wissen beinhaltet. Auch deshalb ist es wesentlich, dass der 9
Tats¨achlich entstehen diese Schichten auch hier, jedoch ist die Schichtenarchitektur nicht horizontal (also Schicht fur ¨ Schicht) entwickelt, sondern vertikal, so dass die Bedurfnisse ¨ h¨oherer Schichten direkt beim Entwurf niederer Schichten berucksich¨ tigt werden konnten. Der Wechsel von RPC zu der selbst realisierten asynchronen Kommunikationsform wurde ubrigens ¨ mit Refactoring-Techniken [Fow99] effizient umgesetzt.
3.2 Techniken der Codegenerierung
63
generierte Code nicht manuell weiterbearbeitet wird, sondern nur die Ausgangsmodelle und die Generatorskripte. In der Praxis ist davon auszugehen, dass eine Mischform aus beiden Generierungsmechanismen zu den besten Ergebnissen fuhren ¨ wird. Daruber ¨ hinaus wird ein System eine weitere Komponente besitzen, die eine Laufzeitumgebung fur ¨ bestimmte Funktionalit¨aten zur Verfugung ¨ stellt, die weder in Java-Klassenbibliotheken noch in Java-Sprachkonzepte abgebildet werden konnen. ¨ Dazu gehoren ¨ zum Beispiel erweiterte Funktionalit¨aten zur Behandlung der in OCL verfugbaren ¨ Mengen und Listen ebenso wie die Bearbeitung von explizit im Code abgelegten Zustandsmodellen. Abbildung 3.6 beschreibt daher die prinzipielle Struktur eines Codegenerators.
Abbildung 3.6. Struktur eines Codegenerators
Der unter anderem in [BBWL01, RFBLO01] gepr¨agte Begriff der UML ” Virtual Machine“ entspricht dabei dem rechten Teil des Bildes 3.6, bestehend aus dem UML-Laufzeitsystem und einer plattformspezifischen Implementierung der festgelegten Schnittstellen. Angelehnt an die “Java Virtual Machine“, dem Interpreter des Java-Bytecodes, entspricht der Codegenerator dem Java-Compiler. Die “UML Virtual Machine“ stellt eine Art operationeller Semantik des ausfuhrbaren ¨ Teils der UML/P dar. 3.2.2 Funktionalit¨at und Flexibilit¨at Die im letzten Abschnitt angesprochene Parametrisierung der Codegenerierung kann nicht nur zur Anpassung an plattformspezifische Merkmale verwendet werden, sondern auch dafur, ¨ den erzeugten Code um zus¨atzliche Funktionalit¨at zu erweitern. Im Prinzip sind der dabei entstehenden Flexibilit¨at kaum Grenzen gesetzt. Nachfolgend wird dies an dem einfachen und
64
3 Prinzipien der Codegenerierung
weitgehend bekannten Beispiel der Codegenerierung fur ¨ Attribute im Klassendiagramm diskutiert. Ein in einer Klasse des Klassendiagramms definiertes Attribut besitzt die in Abbildung 3.7 demonstrierte naturliche“ ¨ Umsetzung als Attribut im ge” nerierten Java-Code. Mit Ausnahme der Merkmale fur ¨ abgeleitete und fur ¨ nur lesbare Attribute (/ und readonly) konnen ¨ alle Merkmale, Typen und initialen Zuweisungen an das Attribut direkt umgesetzt werden. Diese direkte Umsetzung birgt jedoch einige Nachteile, wie zum Beispiel den nicht gekapselten und nicht synchronisierten Zugriff durch andere Objekte.
Abbildung 3.7. Direkte Umsetzung von Attributen
Deshalb ist es heute nicht ublich, ¨ Attribute aus Analyse- und Entwurfsmodellen direkt in Attribute der Implementierung umzusetzen, sondern stattdessen eine Infrastruktur in Form von so genannten get- und setMethoden zur Verfugung ¨ zu stellen. Abbildung 3.8 zeigt die so entstehende Codestruktur. Dabei wird oft jedem Attributnamen ein geeigneter Pr¨afix (hier zum Beispiel der Unterstrich _“) vorangestellt. Die Verwendung von ” Zugriffsfunktionen erhoht ¨ die Flexibilit¨at. Sie erlaubt zum Beispiel die mogli¨ cherweise notwendige Synchronisation von Threads oder die Realisierung des Zugriffsrechts readonly durch zwei get/set-Methoden mit unterschiedlichen Sichtbarkeitsangaben. Die in den beiden Abbildungen 3.7 und 3.8 skizzierten Umsetzungen sind heute relativ verbreitet, aber keineswegs die einzigen. Es gibt weitere Varianten, die zum Beispiel persistente Attribute, eine Ablage der Attribute in Enterprise JavaBeans [MH00], Propagierung von Attribut¨anderungen und dergleichen mehr erlauben. Um die verschiedenen und auch nicht generell vorhersehbaren Varianten der Codegenerierung dennoch flexibel zu ermoglichen, ¨ ist es grunds¨atzlich notwendig, die Umsetzung von Konzepten der UML/P stark zu parametrisieren.
3.2 Techniken der Codegenerierung
65
Abbildung 3.8. Umsetzung von Attributen mittels Zugriffsfunktionen
In gewisser Weise sind fur ¨ die Konzepte der UML/P API’s“ 10 identifi” zierbar, die jeweils umzusetzen sind. Fur ¨ das Konzept Attribut“ der Klas” sendiagramme l¨asst sich beispielsweise folgendes API identifizieren: • • •
Setzen eines Attributs, Auslesen eines Attributs und Initialisierung eines Attributs mit einem Defaultwert. Erweiterungen dieses API’s konnen ¨ zum Beispiel fur ¨
• • •
Serialisierung, Laden aus und Speichern in einer Datenbank, Bildschirmausgabe und Einlesen aus einer Bildschirmmaske
oder fur ¨ typspezifische Funktionalit¨aten definiert werden. Dazu gehoren ¨ zum Beispiel die Inkrementierung von Zahlenwerten, das Anh¨angen von Strings (analog dem Java-Operator +=) oder die Behandlung einzelner Elemente in Containerstrukturen. Die fur ¨ eine Codegenerierung wunschenswerte ¨ Flexibilit¨at besteht also nicht nur in der Form der Umsetzung von UML/P-Konzepten, sondern auch in der damit angebotenen Funktionalit¨at. Die angebotene Funktionalit¨at muss nicht nur generiert werden, sondern auch in einer Form zur Verfugung ¨ stehen, die es dem Entwickler erlaubt, an anderer Stelle darauf zuzugreifen. Dabei gibt es zwei generelle Verfahren: 10
API im Sinne eines abstract programming interface.
66
3 Prinzipien der Codegenerierung
1. Die Umsetzung des API’s wird offen gelegt, indem zum Beispiel aus dem Namen und dem Typ eines Attributs eindeutig die Namen und Signaturen der jeweils verwendbaren Funktionen abgeleitet werden konnen. ¨ Ist also beispielsweise das Attribut title im Klassendiagramm definiert, so kann in Java mit getTitle() und setTitle(...) darauf zugegriffen werden. 2. Es wird das API selbst offengelegt, die Umsetzung aber bleibt verborgen. Der manuell geschriebene Java-Code nutzt daher direkt das API und muss bei der Codegenerierung ebenfalls transformiert werden. Im Beispiel wird dann auch im Java-Code das Attribut title benutzt. Dies wird je nach Anwendungsform bei der Codegenerierung durch eine get- oder set-Methode ersetzt. W¨ahrend der erste Ansatz zu einfacheren Codegeneratoren fuhrt ¨ und keine Behandlung des Java-Codes erfordert, ist beim zweiten Ansatz mehr Flexibilit¨at und Codierungssicherheit gegeben. Durch das Verbergen der tats¨achlichen Implementierung kann diese relativ einfach ersetzt oder erg¨anzt werden. Außerdem ist die Verwendung der API abstrakter und fuhrt ¨ zu kompakterem Code. Jedoch wird es in diesem Ansatz notwendig, bei der Codegenerierung auch direkt in Java formulierte Codeteile zu transformieren. Wie das relativ einfache Beispiel zur Realisierung von Attributen zeigt, lassen sich bereits daran viele der auftretenden Effekte studieren. Deshalb wird im folgenden Abschnitt 3.4 zun¨achst eine lesbare Form der Darstellung von Codegenerierungen beschrieben und deren Einsatzf¨ahigkeit anhand der Transformation von Attributen demonstriert. Die bei der Umsetzung von UML/P-Konzepten in die Implementierung zur Verfugung ¨ stehenden Formen der Codegenerierung haben unter Umst¨anden Auswirkungen auf die Semantik der Konzepte. Dies ist nicht unkritisch, aber auch eine Chance fur ¨ Anwender der UML, die semantischen Freiheitsgrade, oft auch als variation points“ bezeichnet, zu nutzen, um pro” jektspezifische oder zus¨atzliche Funktionalit¨aten und F¨ahigkeiten zu integrieren. Diese Freiheitsgrade pr¨azise zu beschreiben ist innerhalb der UML nicht moglich, ¨ einerseits weil die UML selbst keine Mechanismen fur ¨ ihre Semantikdefinition zur Verfugung ¨ stellt, andererseits weil diese Freiheitsgrade keine allgemeine Gultigkeit ¨ besitzen, sondern abh¨angig von potentiellen Zielplattformen und gewunschten ¨ Funktionalit¨aten sind.11 Es bleibt daher in der Praxis oft nur die Moglichkeit, ¨ jeweils einzelne Formen der Codegenerierung und die damit intendierte semantische Interpretation in konstruktiver 11
Es ist allenfalls moglich, ¨ durch die Verwendung von Stereotypen auf eine spezielle Bedeutung eines Konstrukts hinzuweisen. Die pr¨azise Definition des Stereotyps und der intendierten Semantik ist in der UML nicht moglich. ¨ Stattdessen kann aber eine informelle Beschreibung, wie die in Abschnitt 3.17, Band 1 dargestellte, verwendet werden.
3.2 Techniken der Codegenerierung
67
Form anzugeben. Eine umfassende Beschreibung der Menge mo¨ glicher Varianten in ihrer gesamten Bandbreite erscheint nicht moglich. ¨ Die bei der Codegenerierung auftretende Transformation von UML/PKonzepten in die Konzepte der Programmiersprachen besitzen einige Charakteristiken, die aus anderen Bereichen der Informatik bekannt sind. So kann eine in der Modellierung eingesetzte Datenstruktur in der Implementierung durch eine andere, vielleicht effektivere oder um zus¨atzliche Funktionalit¨at erweiterte Datenstruktur ersetzt werden und damit ein Datenstrukturwechsel stattfinden. Die Trennung der Funktionalit¨at und der plattformabh¨angigen Codeteile wurden im Bereich des Aspect-Oriented-Programming (AOP) [KLM+ 97, LOO01] beziehungsweise der damit eng verwandten generativen Programmierung [CE00] bereits eingehend diskutiert. Dabei werden Techniken vorgestellt, die eine noch weitergehende Trennung einzelner Programmaspekte erlauben. Spezielle Verfahren (so genanntes Weaving“) erlauben die Kom” bination zun¨achst unabh¨angig voneinander formulierter und meist nur uber ¨ eine abstrakte Nahtstelle (Programmierschnittstelle) verbundener Codeteile. Diese Vorgehensweise wird in eingeschr¨ankter Form auch bei der hier diskutierten Codegenerierung verwendet. Auch die in [Pre97, Pre00, KPR97] diskutierte Komposition von Klassen aus Features und deren Interaktionen kann durch einen generativen Ansatz realisiert werden. Welche Klasse bei der Generierung welche (zus¨atzlichen) Features erh¨alt, kann durch Stereotypen und Merkmale in geeigneter Form gesteuert werden. 3.2.3 Steuerung der Codegenerierung Um die bereits mehrfach erw¨ahnte, notwendige Flexibilit¨at in der Codegene¨ rierung in der vollen Bandbreite zu nutzen, muss die Ubersetzung sinnvoll gesteuert werden konnen. ¨ So ist es normalerweise sinnvoll, dasselbe Konzept innerhalb eines Projekts, ja sogar innerhalb eines Modells unterschiedlich zu realisieren. Die bereits mehrfach diskutierten Moglichkeiten ¨ zur Umsetzung von Attributen konnen ¨ zum Beispiel abh¨angig sein von • • •
der Klasse, die das Attribut beinhaltet, weil diese Klasse Aufgaben wie Datenhaltung oder Applikationssteuerung haben kann oder als Schnittstelle zu anderen Systemteilen wirkt und daher zu synchronisieren ist, der Aufgabe des Attributs innerhalb der Klasse, weil beispielsweise die Klasse persistent ist, das Attribut aber berechnet werden kann oder nur tempor¨are Daten beinhaltet oder dem Typ des Attributs, weil dieser beispielsweise in UML/P, nicht aber in Java existiert.
68
3 Prinzipien der Codegenerierung
Diese statisch, also zur Zeit der Codegenerierung festgelegten Abh¨angigkeiten konnen ¨ durch dynamische Abh¨angigkeiten erg¨anzt werden, wenn zum Beispiel ein Flag benutzt wird, um festzulegen, ob ein Objekt persistent sein soll. Projektspezifische F¨alle wie diese sollten typischerweise nicht mehr durch eine vorgegebene Semantikdefinition, sondern durch selbstdefinierten Code realisiert werden, der durch den parametrisierten Codegenerator systematisch hinzugefugt ¨ wird. Die Steuerung der Implementierungsform jedes UML/P-Konzepts kann grunds¨atzlich durch Stereotypen und Merkmale erfolgen. Jedoch ist die ausschließliche Verwendung von Stereotypen und Merkmalen in der Praxis nicht ausreichend. Stattdessen reduziert sich die Markierung der UMLDiagramme im Wesentlichen auf den Stereotyp-Namen, optional erg¨anzt um zus¨atzliche Parameterwerte. Die Umsetzung eines mit einem Stereotyp markierten UML/P-Elements in eine Implementierung wird durch zus¨atzliche Skripte vorgenommen, die durch den Codegenerator ausgefuhrt ¨ werden und diesen, wie in Abbildung 3.6 gezeigt, sehr flexibel parametrisieren.
3.3 Semantik der Codegenerierung Eine Codegenerierung ist vor allem eine Transformation einer Sprache in eine andere Sprache. Auch eine Semantikdefinition ist im Wesentlichen eine Abbildung einer als unbekannt betrachteten Sprache (hier also UML/P) in eine als bekannt und als verstanden angesehene Zielsprache. Ist die Zielsprache daruber ¨ hinaus formal und die Abbildung pr¨azise formuliert, so wird auch von einer formalen Semantik gesprochen. In diesem Sinn kann eine durch ein Programm implementierte Abbildung der UML/P in die Programmiersprache Java selbst als eine formale Semantik verstanden werden. Dabei gibt es allerdings mehrere Punkte, die bei dieser Argumentation zu beachten sind: 1. Es ist fur ¨ das Verst¨andnis einer Sprache wie UML/P hilfreich, mehr als nur eine Bedeutungserkl¨arung (Semantik) zur Verfugung ¨ zu haben. Durch Verwendung mehrerer Herangehensweisen zur Definition einer Semantik einer Sprache werden unterschiedliche Probleme erkannt und konnen ¨ so in die Sprachdefinition und deren Benutzung (Analyse, Tests, Refactoring) zuruckfließen. ¨ 2. Der generierte Code ist typischerweise nicht unbedingt gut lesbar, denn er enth¨alt im Allgemeinen eine Vielzahl von technischen Details. Es ist daher nicht allgemein zumutbar, zum Verst¨andnis der Bedeutung einer Modellierungssprache den generierten Code inspizieren zu mussen. ¨ Allerdings gibt es eine Reihe von Sprachbeschreibungen, die darauf beruhen, das Prinzip der Codegenerierung allgemein zu diskutieren. Sie sprechen damit ein breites Publikum an, da heute Programmiersprachen wie Java die am weitesten verbreiteten formalen Sprachen“ sind. ”
3.3 Semantik der Codegenerierung
69
3. Bestimmte Aspekte einer Sprache konnen ¨ bei der Umsetzung in ablauff¨ahigen Code nicht oder nur mit großem Aufwand umgesetzt werden. Dazu gehoren ¨ zum Beispiel Konzepte, die Unterspezifikation erlauben und die in der UML/P an mehreren Stellen eingebaut sind. So konnen ¨ initialisierende Werte fur ¨ Attribute fehlen oder im Statechart mehrere alternative Transitionen gleichzeitig schaltbereit sein. Der dabei entstehende Nichtdeterminismus wird bei einer Codegenerierung im Normalfall durch die Auswahl einer hoher ¨ priorisierten Transition aufgelost ¨ (siehe Abschnitt 6.4.4, Band 1). Das durch den generierten Code beschriebene System ist daher im Allgemeinen nicht identisch zum Ausgangsmodell, sondern stellt eine von mehreren mo¨ glichen Spezialisierungen dar. 4. Die UML/P ist teilweise auf Ausfuhrbarkeit ¨ ausgelegt, erlaubt jedoch an vielen Stellen die Verwendung von nicht ausfuhrbaren ¨ Konzepten. Dazu gehoren ¨ neben den bereits erw¨ahnten Moglichkeiten, ¨ Aspekte wegzulassen, unter anderem die Spezifikation von Bedingungen mit unendlichen Quantoren. Auch die nachfolgend noch genauer diskutierte Frage der Umsetzung von OCL-Nachbedingungen gehort ¨ zu diesem Problemkreis. Daher ist eine vollst¨andige Abbildung von UML/P nach Java nicht moglich. ¨ Als Alternative zu diesen sehr impliziten Semantikdefinitionen lassen sich Techniken der formalen Methoden einsetzen, um eine formale Semantik fur ¨ die Quellsprache, unabh¨angig von der Codegenerierung, zu definieren. Typischerweise sind solche Semantikdefinitionen ebenfalls Abbildungen mit einer UML-Variante als Quelle. Als Zielsprachen werden jedoch mathematisch formale Kalkule ¨ verwendet und die Abbildungen in kompakter Form dargestellt, so dass sie einer Analyse leichter zug¨anglich werden.12 Wie in [HR00] und [Rum98] argumentiert, kann die Existenz zweier Abbildungen fur ¨ eine Quellsprache verwendet werden, um das Zutrauen in die Korrektheit beider Abbildungen und damit insbesondere in die Codegenerierung zu erhohen. ¨ Ist die Codegenerierung in der hier vorgeschlagenen Form parametrisiert und haben die benutzten Skripte Einfluss auf Verhalten und Struktur des generierten Codes, so kann dies auf zwei Arten in eine formale Semantikdefinition einbezogen werden. Abbildung 3.9 formalisiert eine Variante zur Semantikdefinition, in der die Semantikabbildung unabh¨angig vom benutzten Skript ist. Die in Abbildung 3.9 dargestellte Formalisierung nutzt die mengenwertige Semantikabbildung, um damit die Variabilit¨at des parametrisierten Codegenerators darzustellen. Die Formalisierung h¨angt sehr stark von den da12
Eine sich davon dezidiert unterschiedene Form, zum Beispiel eine axiomatische Semantikdefinition fur ¨ die UML oder einzelnen Teilen, ist bisher nur in [EHHS00] und [EH00] zu finden und basiert auf einem transformationellen Ansatz dem Graphgrammatiken zugrunde liegen.
70
3 Prinzipien der Codegenerierung
Zur Formalisierung einer Sprache und der Codegenerierung werden folgende Definitionen benotigt: ¨ •
die Quellsprache (UML/P) als eine Menge UML von syntaktisch wohlgeformten Ausdrucken, ¨
•
eine geeignete formale Zielsprache mit dem Sprachschatz Z,
•
die Skriptsprache des Codegenerators mit dem Sprachschatz S und
•
die Menge J aller Java-Programme.
Ein Codegenerator ist eine unter Umst¨anden partielle Abbildung Gen : UML → J. Eine formale Semantik ist demgegenuber ¨ eine Abbildung Sem : UML → P(Z). Damit wird einem einzelnen typischerweise unterspezifizierten und abstrakten Modell aus der Quellsprache eine Menge von mo¨ glichen Implementierungen zugewiesen, die mit eben diesem Modell gemeint sind. Dies stellt eine Form der losen Semantik dar. Fur ¨ einen Vergleich beider Abbildungen Sem und Gen ist eine Semantik fur ¨ JavaProgramme in der Form SemJ ava : J → Z notwendig. Dann muss fur ¨ jedes UMLDokument u ∈ UML, fur ¨ das Code generiert werden kann, gelten: ∀u ∈ UML : SemJ ava (Gen(u)) ∈ Sem(u) Das heißt, im Allgemeinen w¨ahlt der Codegenerator eine von mehreren mo¨ glichen Implementierungen aus, indem er etwa offene Aspekte durch Defaults ausfullt. ¨ Nur wenn Sem(u) ein einziges Element darstellt, war die Spezifikation offensichtlich vollst¨andig und eindeutig. Ein parametrisierter Codegenerator wird um die Parameter, also die Skriptsprache S, erweitert: Genp : UML × S → J. Es muss nun gelten: ∀u ∈ UML, s ∈ S : SemJ ava (Genp (u, s)) ∈ Sem(u) Das heißt, im Rahmen der Vorgabe durch Sem(u) darf das Skript s eine mogliche ¨ Implementierung fur ¨ u ausw¨ahlen. Abbildung 3.9. Semantik des parametrisierten Codegenerators
durch beobachteten Aspekten einer Sprache ab. Wird zum Beispiel nur das extern sichtbare Verhalten formalisiert, so sind in Bezug auf Umsetzung von Attributen, Assoziationen und anderen Strukturelementen Freiheiten gegeben. Tats¨achlich ist es fur ¨ eine so umfangreiche Sprache wie die UML kaum praktikabel, eine vollst¨andige Formalisierung vorzunehmen, obwohl dies in ¨ [Ove00] bemerkenswert vollst¨andig, aber nicht sehr elegant gelungen ist. Stattdessen ist es sinnvoll, einzelne, kritische Aspekte genauer zu beleuchten und damit Ruckkopplung ¨ in den Standardisierungsprozess zu geben. In einer Reihe von Publikationen wurden auch die prinzipiellen Vorteile und Probleme einer Standardisierung diskutiert [BHH+ 97, FELR98b, FELR98a], die unter anderem in den Proceedings der UML-Konferenzen [BM98, FR99, EKS00, GK01] zu finden sind.
3.4 Parametrisierung eines Werkzeugs
71
Eine alternative Sichtweise zu der in Abbildung 3.9 dargestellten Form ist die Einbeziehung der Skriptsprache S in die Semantikdefinition. Quellsprache und Skriptsprache stellen dann in gewisser Weise die gemeinsame Pro” grammiersprache“ dar. Eine Semantikdefinition kann dies in Form einer Funktion Semp : UML × S → Z widerspiegeln, die die Auswahl genau eines Elements der Zielsprache Z vornimmt.
3.4 Parametrisierung eines Werkzeugs In diesem Abschnitt wird zun¨achst die Frage nach einer geeigneten Skriptsprache diskutiert und dann eine kompakte Form der Darstellung der Effekte solcher Skripte eingefuhrt. ¨ Im Kapitel 4 werden die Eigenschaften dieser Skripte anhand einer exemplarischen Umsetzung der UML/P-Konstrukte demonstriert. Die hier diskutierten Konzepte stellen for allem Bezug zu einem Codegenerator her, sind aber auf andere Formen von Analysewerkzeugen und Transformatoren ebenfalls anwendbar. Zum Beispiel konnen ¨ damit auch Datenstrukturwechsel oder Refactoring-Techniken beschrieben werden. 3.4.1 Implementierung von Werkzeugen In Abschnitt 3.2.3 wird die Verwendung von Skripten zur flexiblen Parametrisierung eines Codegenerators, aber auch fur ¨ Analyse- und Testwerkzeuge diskutiert. Tats¨achlich erfordert eine plattformspezifische und eine auf verschiedene Implementierungsmoglichkeiten ¨ ausgerichtete Codegenerierung einen flexiblen Mechanismus zur Steuerung der Codegenerierung. Die Generierung kann im Prinzip durch Stereotypen und Merkmale gesteuert werden. Die Details des zu erzeugenden Codes konnen ¨ mit Stereotypen aber nicht oder zumindest nicht komfortabel festgehalten werden. Im Allgemeinen konnen ¨ die zu erzeugenden Codestrukturen sehr komplex sein und unterschiedlichste Formen haben. Meist sind Symboltabellen notwendig und Nebenbedingungen zu prufen, ¨ die die Anwendbarkeit einer Codegenerierung oder mogliche ¨ Optimierungen beschreiben. Aufgrund der erforderlichen Flexibilit¨at kommt nur eine in ihrer Beschreibungsm¨achtigkeit (weitgehend) vollst¨andige Programmiersprache in Betracht, die eine kompakte Formulierung von Bedingungen und Transformationen erlaubt. Pro” gramme“ dieser Sprache werden vom Codegenerator zur Ausfuhrung ¨ gebracht, um den Implementierungscode zu erzeugen. Sie sind selbst zur Laufzeit nicht vorhanden. Abbildung 3.10 beschreibt die daraus resultierende innere Struktur eines Generators. Fur ¨ Skriptsprachen wurden in der Werkzeugentwicklung unterschiedliche Vorschl¨age gemacht. So sind interpretierte Derivate der Sprache C ebenso in Verwendung wie Visual Basic, Skriptsprachen wie Pearl, Tcl/Tk oder sogar Eigenentwicklungen. Funktionale Programmiersprachen wie ML
72
3 Prinzipien der Codegenerierung
Abbildung 3.10. Innere Struktur eines Codegenerators
[Pau94, MTHM97] bieten daruber ¨ hinaus eine sehr kompakte Form, solche Werkzeuge zu parametrisieren. Zum Beispiel ist das Verifikationswerkzeug Isabelle [NPW02] in ML geschrieben und bietet damit gute, ebenfalls in ML zu formulierende Erweiterungsmoglichkeiten. ¨ Eine weitere Alternative ist die zur Zeit immer popul¨arer werdende XML-Technologie [McL01, W3C00] und die Verwendung von XML/XSLTbasierten Werkzeugen zur Transformation in den Zielcode. Allerdings besitzt XML durch die explizite Einbettung der Tags (Nichtterminale) einerseits ein sehr schlechtes Verh¨altnis zwischen Struktur- und Nutzinformation und andererseits sind die heutigen Parse- und Transformationswerkzeuge fur ¨ die XML noch nicht so leistungsf¨ahig, wie die im Bereich des Compilerbau l¨angst bekannten Yacc/Lex und deren Derivate. Mit ihrer Vielzahl an Bibliotheken, Frameworks und Werkzeugen sowie der Moglichkeit ¨ des dynamischen Ladens von Klassenbibliotheken ist auch Java ein guter Kandidat, einerseits, um damit einen Codegenerator zu realisieren und andererseits, um damit die Parametrisierung uber ¨ einen Plugin-Mechanismus zu ermoglichen. ¨ Beispielsweise nutzt das Werkzeug Poseidon [BS01a, BBWL01] diesen Mechanismus. Eine aktive Skriptsprache besitzt jedoch Defizite bei der Darstellung der Form der Zielsprache. Deshalb bietet sich zus¨atzlich die Verwendung eines Makro-Ersetzungsmechanismus an, der Templates13 verarbeitet und daraus Code erzeugt. Ein Template ist als passive Skriptsprache zu sehen, die Makros beinhaltet, die durch den Ersetzungsalgorithmus zum Beispiel durch echte Namen oder Ausdrucke ¨ ersetzt werden. Ein Template kann aktive Ele13
Der englische Begriff Template“ hat sich in diesem Kontext eingeburgert, ¨ so dass ” er auch in diesem Buch gegenuber ¨ der deutschen Version Codeschablone“ bevor” zugt wird.
3.4 Parametrisierung eines Werkzeugs
73
mente beinhalten, um damit Kontrollstrukturen, wie Alternative, Wiederholung oder Einbindung anderer Templates zu ermoglichen, ¨ aber auch um Berechnungen in beschr¨anktem Maß durchzufuhren. ¨ Verwendbar sind hierbei XML auf der Transformationssprache XSLT basierende Werkzeuge oder ein mit Java-Code kombinierter Template-Mechanismus a¨ hnlich der JSP-Seiten [FK00], der zum Beispiel von [SvVB02] vorgestellt wird. Aus der notwendigen Programmierbarkeit des Generators folgt, dass Entwickler nicht nur in der Entwicklungsprogrammiersprache, sondern in eingeschr¨ankter Form auch in der Skriptprogrammiersprache entwickeln mussen. ¨ Aufgrund der o¨ fter notwendigen Anpassungen muss dies parallel zur eigentlichen Entwicklung geschehen. Es ist daher fur ¨ das Erlernen von Vorteil, wenn sich Skript- und Programmiersprache a¨ hnlich sind, oder sogar ubereinstimmen. ¨ Generell kann davon ausgegangen werden, dass ein Framework allgemein verwendbarer Skripte und Templates zur Verfugung ¨ stehen wird, das fur ¨ Projekte individuell anzupassen ist. Eine Wiederverwendung der Templates vollig ¨ ohne Anpassung ist zum Beispiel im Hinblick auf die Generierung von graphischen Oberfl¨achen unwahrscheinlich. Ist eine Entscheidung zugunsten einer Skriptsprache und einer TemplateSprache gefallen, so stellt sich die Frage, ob sich die Effekte der Skripte fur ¨ den Anwender des Generators in kompakter, verst¨andlicher, aber gegebenenfalls informeller Form darstellen lassen, die nicht auf der doch mit vielen Implementierungsdetails behafteten Skriptsprache beruhen. Dieser Wunsch wird noch verst¨andlicher, wenn die XML-basierte und damit sehr verbose Transformationssprache XSLT verwendet werden soll. 3.4.2 Darstellung von Skripttransformationen Der Effekt eines fur ¨ die Codegenerierung verwendeten Skripts beruht darauf, UML/P-Konzepte in die Zielprogrammiersprache Java zu transformieren. Da ein solches Skript auch Rand- und Sonderf¨alle behandelt, Rahmenbedingungen pruft ¨ und dazu eine Reihe von Hilfsfunktionen nutzt, ist es zweckm¨aßig den Effekt eines solchen Skripts in kompakter Form darzustellen, dabei Sonderf¨alle nur informell zu diskutieren und so fur ¨ den Anwender verst¨andlich zu machen. Dazu wird die Schablone in Tabelle 3.11 vorgeschlagen, in der neben der eigentlichen Transformation Kontextbedingungen eine allgemeine Beschreibung angegeben und potentielle Alternativen diskutiert werden konnen. ¨ Diese Beschreibung ist weder als vollst¨andig noch als formal anzusehen, obwohl sie einigen nachfolgend beschriebenen Einschr¨ankungen unterliegt. Diese Schablone stellt eher eine Form von Pseudocode fur ¨ die Beschreibung von Transformationen dar.
74
3 Prinzipien der Codegenerierung
Name der Transformation Erkl¨arung Transformationsregel
Motivation und Zweck der Transformation. Meist gibt es eine prim¨are Transformationsregel, die in folgender Form dargestellt wird: Ursprung ⇓ Ziel • Ursprung und Ziel konnen ¨ dabei jeweils textuell oder durch ein Diagramm dargestellt werden. • Der Ursprung fuhrt ¨ die Elemente der Syntaxklassendiagramme und EBNF-Produktionen ein, die transformiert werden. Der Kontext wird ebenfalls aufgelistet. Dafur ¨ werden schablonenartige Diagramme und Textstucke ¨ verwendet. • Die kursiven Namen stellen Schemavariablen dar, die bei der Anwendung der Transformation mit echten Sprachelementen belegt werden. • Erkl¨arende Texte beschreiben die Arten der Schemavariablen, welche Teile optional sind oder mehrfach auftreten konnen, ¨ etc. • Die Inhalte der Schemavariablen konnen ¨ selbst gewissen Transformationen unterliegen. So kann die Schemavariable attr dazu genutzt werden, einen Methodennamen setAttr zu konstruieren, der aus dem konstanten Teil set und der kapitalisierten Form des Attributnamens zusammengesetzt ist.
weitere Aus der Haupttransformation ergeben sich meist zus¨atzlich Transforma- notwendige Transformationen, die in analoger Form dargetionen stellt werden. Diese Transformationen beziehen sich zum Beispiel auf Elemente der in Abschnitt 3.2.2 diskutierten API, die ebenfalls zu transformieren sind. Beachtens- Dieser Abschnitt rundet durch zus¨atzliche Betrachtungen, wert Hinweise und die Diskussion potentieller Problemstellungen die Beschreibung ab. Tabelle 3.11.: Name der Transformation Der verwendete Regelmechanismus ist angelehnt an formale Regelkalkule ¨ der Form
3.4 Parametrisierung eines Werkzeugs
75
Ursprung Rahmen⇓ bedingungen, Ziel die neben Ursprung und Ziel eine pr¨azise Angabe der Rahmenbedingungen in einer formalen Notation enthalten. Der Ursprung enth¨alt Schemavariablen (Platzhalter, [BBB+ 85]), die bei der Anwendung der Transformation mit echten Sprachelementen belegt werden. Jede Schemavariable ist einem bestimmten Nichtterminal zugeordnet, ist also damit typisiert. Nachfolgend ein Anwendungsbeispiel fur ¨ eine Standardtransformation, die die in Abbildung 3.8 exemplarisch gezeigte Codegenerierung vornimmt.14 Wie bereits in Abschnitt 3.2.2 besprochen, stellt die hier dargestellte Umsetzung nur eine von mehreren Moglichkeiten ¨ dar, die zum Beispiel durch die Verwendung geeigneter Stereotypen und Merkmale am Attribut, der Klasse oder dem Klassendiagramm, aber auch durch Angabe bestimmter Skripte ausgew¨ahlt werden konnen. ¨ Attribut1: Standardtransformation von Attributen Erkl¨arung
¨ Die Standardform zur Ubersetzung von Attributen mit Kapselung und Zugriff durch explizite Zugriffsfunktionen. Typ- oder projektspezifische Funktionalit¨aten sind nicht enthalten.
Attributdefinition
⇓
class Class { ... Java private Type attr = value; tags’ synchronized Type getAttr() { return attr; } tags” synchronized Type setAttr(Type a) { return attr=a; } } (Fortsetzung auf n¨achster Seite)
14
Allerdings unter Verzicht auf die in manchen Codierungsstandards propagierte Verwendung von fuhrenden ¨ Unterstrichen fur ¨ Attribute.
76
3 Prinzipien der Codegenerierung (Fortsetzung von Tabelle 3.12.: Attribut1: Standardtransformation von Attributen)
• Die optionale Besetzung mit =value wird nur verwendet, wenn sie im Diagramm angegeben ist. • Die in tags angegebene Sichtbarkeit wird mit Ausnahme von readonly ubernommen. ¨ readonly wird bei getAttr (also in tags’) in public umgesetzt und bei tags” in protected. • return ist bei setAttr sinnvoll, denn die Zuweisung mit = hat denselben Wert. • Die Kardinalit¨at des ubersetzten ¨ Attributs ist entweder nicht angegeben und damit 1“ oder 0..1“. ” ” Attributzugriff
attr ⇓ getAttr()
quali.attr ⇓ quali.getAttr()
• Typ von Ausdruck quali ist konform zur Klasse Class. Attributbesetzung
attr=expr ⇓ setAttr(expr)
quali.attr=expr ⇓ quali.setAttr(expr)
• Typ von Ausdruck quali ist konform zur Klasse Class. • Typ von Ausdruck expr ist konform zum Attributtyp Type. Beachtenswert
Typspezifische Konstrukte wie attr++ konnen ¨ entweder durch zus¨atzliche Funktionalit¨at effizient umgesetzt oder in setAttr(getAttr()+1) transformiert werden. Merkmale und Stereotypen werden in dieser Standardumsetzung nicht berucksichtigt. ¨ In UML ist es moglich, ¨ Attributen eine Kardinalit¨at zuzuordnen. Im Fall *“ ist eine Umsetzung a¨ hnlich der Assoziation ” vorzunehmen.
Tabelle 3.12.: Attribut1: Standardtransformation von Attributen
4 Transformationen fur ¨ die Codegenerierung
Wer klare Begriffe hat, kann befehlen. Johann Wolfgang von Goethe
¨ Dieses Kapitel erg¨anzt die prinzipiellen Uberlegungen zur Codegenerierung aus Kapitel 3 um konkrete Techniken. Dabei wird die Vorgehensweise zur Umsetzung von Klassendiagrammen, Objektdiagrammen, der Codegenerierung aus der OCL, der Ausfuhrung ¨ von Statecharts und der Testgenerierung aus Sequenzdiagrammen in jeweils einem der Abschnitte 4.1 bis 4.5 erkl¨art. Bei den Klassendiagrammen werden dabei eher bereits bekannte Konzepte in kompakter Form als Transformationen aufbereitet und Alternativen diskutiert. Insbesondere der Abschnitt 4.1 uber ¨ Klassendiagramme dient dabei zur Demonstration der systematisierten Darstellung von Transformationsregeln fur ¨ die Codegenerierung. Fur ¨ Statecharts werden Generierungsalternativen intensiv diskutiert. Fur ¨ alle anderen Notationen werden vor allem grundlegende Prinzipien der ¨ Ubersetzung behandelt, weil so Nutzern unter anderem die Moglichkeit ¨ ge¨ geben wird, auf die Zielsprache bzw. Zielumgebung zugeschnittene Ubersetzungsformen selbst zu entwickeln bzw. in Abwesenheit eines geeigneten Werkzeugs diese manuell durchzufuhren. ¨
4.1 4.2 4.3 4.4 4.5 4.6
¨ Ubersetzung von Klassendiagrammen . . . . . . . . . . . . . . . . ¨ Ubersetzung von Objektdiagrammen . . . . . . . . . . . . . . . . . Codegenerierung aus OCL . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausfuhrung ¨ von Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . ¨ Ubersetzung von Sequenzdiagrammen . . . . . . . . . . . . . . . Zusammenfassung zur Codegenerierung . . . . . . . . . . . . . .
78 102 112 125 136 140
78
4 Transformationen fur ¨ die Codegenerierung
¨ 4.1 Ubersetzung von Klassendiagrammen In diesem Abschnitt wird die Transformation von Konzepten des Klassendiagramms und einiger damit zusammenh¨angender Elemente in Java als komplexeres Beispiel durch eine Sammlung von Transformationsregeln beschrieben, die auch die Moglichkeiten ¨ zur Beschreibung von Alternativen ¨ und von Komposition der Regeln zeigen. Bei dieser Ubersetzung werden weder Java-Frameworks oder Infrastrukturkonzepte wie JavaBeans oder Middleware-Komponenten noch Datenbank-Anbindungen berucksichtigt. ¨ Dafur ¨ sind jeweils spezielle Generatoren notwendig, die in der hier angestrebten allgemeinen Form nicht diskutiert werden konnen. ¨ 4.1.1 Attribute Fur ¨ normale und statische Attribute von Klassen wurde mit der Transformationsregel Attribut1 von Seite 75 bereits eine Regel zur Umsetzung angegeben. Diese kann im Prinzip auch fur ¨ abgeleitete Attribute verwendet werden, ignoriert jedoch ein wesentliches Merkmal dieser Art von Attributen. Im Normalfall existiert fur ¨ ein abgeleitetes Attribut eine als Invariante formulierte Berechnungsvorschrift. Alternativ dazu kann auch eine bereits in der Zielsprache Java formulierte Methode existieren, die die Berechnung des Attributs vornimmt. Nach unserer Konvention heißt eine solche Methode calcAttr. Attribut2eager : Abgeleitete Attribute - Eager Version Erkl¨arung
Fur ¨ ein abgeleitetes Attribut /attr existiert eine Berechnungsvorschrift als OCL-Invariante der Form attr=expr oder eine Methode calcAttr. ¨ Anderungen der zur Berechnung verwendeten Attribute treten gegenuber ¨ der Abfrage des abgeleiteten Attributs selten auf. Deshalb wird das abgeleitete Attribut sofort neu berechnet und gespeichert. (Fortsetzung auf n¨achster Seite)
¨ 4.1 Ubersetzung von Klassendiagrammen
79
(Fortsetzung von Tabelle 4.1.: Attribut2 eager : Abgeleitete Attribute - Eager Version)
Attributdefinition
⇓
class Class { ... Java private Type attr; tags’ synchronized Type getAttr() { return attr; } private synchronized void calcAttr() { attr = expr’; } }
• Attributdefinition und getAttr werden wie bei der Standardregel Attribut1 transformiert. Eine setAttr Methode existiert jedoch nicht. • Ist die Berechnungsvorschrift als nicht-rekursive OCLBedingung in der Form attr=expr angegeben, so wird diese in Java-Code expr’ transformiert und in die Methode calcAttr eingebettet. Alternativ kann diese Methode bereits existieren oder durch eine Nachbedingung in der angegebenen Form spezifiziert sein. Attributzugriff
attr ⇓ getAttr()
quali.attr ⇓ quali.getAttr()
• Wie in Transformationsregel Attribut1. Attributbesetzung Besetzung eines Ausgangsattributs
ist nicht moglich. ¨ durch Analyse des OCL-Ausdrucks beziehungsweise der vorhandenen calcAttr-Implementierung konnen ¨ die Ausgangsattribute ermittelt werden, von denen attr abgeleitet ist. Fur ¨ jedes Ausgangsattribut source wird die set-Methode erweitert: (Fortsetzung auf n¨achster Seite)
80
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.1.: Attribut2 eager : Abgeleitete Attribute - Eager Version)
⇓ Java
class Class { ... tags’ synchronized Type’ setSource(Type’ a) { source=a; calcAttr(); return a; } }
• Dies zeigt nur den (einfachen) Fall, dass Ausgangs- und abgeleitetes Attribut in demselben Objekt lokalisiert sind. Ist das nicht der Fall, muss eine bidirektionale Verbindung zwischen beiden Objekten bestehen, die durch die aktuellen Ver¨anderungen nicht beeintr¨achtigt sein sollte.1 Beachtenswert
Die Ver¨anderung eines Attributs hat die automatische Ver¨anderung aller davon abgeleiteten Attribute zur Folge. Dies kann zu kaskadenartigen Neuberechnungen abgeleiteter ¨ Attribute fuhren ¨ und ineffizient sein, wenn mehr Anderungen als Abfragen auftreten. Zirkul¨are Abh¨angigkeiten fuhren ¨ daruber ¨ hinaus zu nichtterminierenden Neuberechnungen und sind daher verboten.
Tabelle 4.1.: Attribut2eager : Abgeleitete Attribute - Eager Version Der oben formulierten eager“ Version der Umsetzung kann eine lazy“ ” ” Version entgegengesetzt werden, die den Attributwert nur bei Bedarf berech¨ net. Aufgrund der Ahnlichkeiten zur vorherigen Transformationsregel wird diese verkurzt ¨ wiedergegeben:
1
Die Bidirektionalit¨at ist notwendig, weil die Berechnung nicht dem Kontrollfluss folgt, sondern eine Form der Change-Propagation (Publisher-Subscriber-Pattern) und damit eine Umkehrung der Berechnungsabh¨angigkeiten realisiert wird. Eine Datenflussanalyse kann prufen, ¨ ob dieses Pattern mit all seiner Infrastruktur eingebaut werden muss oder eine bidirektionale Assoziation vorhanden ist. Bei einem geeigneten Generator kann auch durch den Anwender eingegriffen werden, um eine optimale Umsetzung zu sichern.
¨ 4.1 Ubersetzung von Klassendiagrammen
81
Attribut2lazy : Abgeleitete Attribute - Lazy Version Erkl¨arung
Fur ¨ ein abgeleitetes Attribut /attr existiert eine Berechnungsvorschrift als OCL-Invariante der Form attr=expr oder eine Methode calcAttr. ¨ Die H¨aufigkeit der Anderungen der zur Berechnung verwendeten Attribute liegt in einer a¨ hnlichen Großenordnung ¨ wie die Abfrage des abgeleiteten Attributs. Deshalb wird das abgeleitete Attribut erst bei Bedarf berechnet und nicht gespeichert.
Attributdefinition
⇓
class Class { ... tags’ synchronized Type getAttr() { return calcAttr(); } private synchronized Type calcAttr() { return expr’; } }
Java
• Siehe Anmerkungen zu Attribut2eager . Attribute
Beachtenswert
• Attributzugriff erfolgt wie bei Attribut2eager . • Die direkte Attributbesetzung ist wie bei Attribut2eager nicht moglich. ¨ • Die Besetzung eines Ausgangsattributs (von dem dieses abh¨angt) muss nicht angepasst werden. Vorteil gegenuber ¨ der Attribut2eager Version ist, dass der Kontrollfluss nicht invertiert wurde und damit keine bidirektionale Assoziationen oder eine andere Infrastruktur notwendig sind. Ineffizienz kann aber durch wiederholt durchgefuhrte ¨ Berechnung des Attributs entstehen. Zirkul¨are Abh¨angigkeiten fuhren ¨ auch hier zu nichtterminierenden Neuberechnungen und sind daher verboten.
Tabelle 4.2.: Attribut2lazy : Abgeleitete Attribute - Lazy Version Eine im Bereich der graphischen Oberfl¨achen gelegentlich verwendete Form des Model-View-Controller-Pattern nutzt Vorteile beider Ans¨atze, in-
82
4 Transformationen fur ¨ die Codegenerierung
dem die change propagation nur in einer booleschen Statusvariable vermerkt wird, aber eine Neuberechnung erst bei Bedarf erfolgt. 4.1.2 Methoden Fur ¨ die Implementierung von Methoden stehen mehrere Strategien zur Verfugung, ¨ die von der Ausgangsituation abh¨angig sind: 1. Der Methodenrumpf ist bereits formuliert und muss nur in die Methode eingesetzt werden. Dabei werden auch die notwendigen Transformationen beispielsweise von Attributzugriffen vorgenommen. 2. Die Methode ist durch ein Vor-/Nachbedingungspaar beschrieben, wobei die Nachbedingung, wie in Abschnitt 3.1 diskutiert, algorithmisch formuliert ist und direkt in Code umgesetzt werden kann. 3. Die Methode ist durch ein Vor-/Nachbedingungspaar beschrieben, das aber nicht algorithmisch umsetzbar und deshalb nur fur ¨ Tests geeignet ist. 4. Ein Methoden-Statechart (siehe Abschnitt 6.4.3, Band 1) beschreibt den Ablauf der Methode. 5. Fur ¨ diese Methode gibt es noch keine Implementierung oder Spezifikation. Fur ¨ jeden dieser F¨alle ist eine eigenst¨andige Vorgehensweise sinnvoll. Der erste Fall benotigt ¨ nur die Integration des Methodenrumpfs mit der Signatur sowie die Umsetzung zum Beispiel der Attributzugriffe im Methodenrumpf. Methode1impl : Methoden mit gegebener Implementierung Erkl¨arung
Eine Methode meth mit gegebenem Methodenrumpf code wird in Java umgesetzt.
Methodendefinition
(Fortsetzung auf n¨achster Seite)
¨ 4.1 Ubersetzung von Klassendiagrammen
83
(Fortsetzung von Tabelle 4.3.: Methode1impl : Methoden mit gegebener Implementierung)
• Der Methodenrumpf code besteht aus einer Sequenz von Anweisungen. Diese wird entsprechend der gultigen ¨ Transformation fur ¨ Anweisungen in code’ transformiert, um zum Beispiel Attributzugriff und -besetzung oder die Umsetzung von Zusicherungen (assertions) zu behandeln. • Sind Sichtbarkeitsangaben, Parameternamen, -typen und Ergebnistyp teilweise im Text und im Diagramm gegeben, so durfen ¨ sie sich erg¨anzen, aber nicht widersprechen. Tabelle 4.3.: Methode1impl : Methoden mit gegebener Implementierung Fur ¨ die Beschaffung des Methodenrumpfs gibt es in den heute verfugba¨ ren Werkzeugen mehrere Ans¨atze. Eine Moglichkeit ¨ ist, den Rumpf als Textstuck, ¨ zum Beispiel als Kommentar, der Methodensignatur im Diagramm zu hinterlegen und durch Anw¨ahlen zug¨anglich zu machen. Dies ist allerdings fur ¨ große Systeme mit vielen Methoden nicht praktikabel. Die Technik des Round Trip Engineering“ liest die Methodenrumpfe ¨ direkt aus dem Quell” code, um sie dorthin zuruck ¨ zu schreiben.2 Hier wird eine weitere Alternative vorgeschlagen, die einige Vorteile des Aspect Oriented Programming mit einer kompakten Aufschreibung vereint. Grundidee ist es, Methodenimplementierungen nicht notwendigerweise nur nach der Klassenzugeho¨ rigkeit, sondern auch nach funktionalen Kriterien zusammenzufassen. So konnen ¨ beispielsweise die protocol-Methoden aller Klassen, die Methoden zum Durchlauf einer Teilehierarchie oder die Serialisierungsmethoden mehrerer Klassen jeweils in eine Datei zusammengefasst werden, die dann diesen Aspekt des Programms beinhaltet. Neben oder statt Java-Implementierungen konnen ¨ auch OCL-Spezifikationen von Methoden in der Form von Vor- und Nachbedingungspaaren verwendet werden. In Abschnitt 4.4.3, Band 1 ist die Integration mehrerer solcher Methodenspezifikationen behandelt worden. Deshalb kann hier von einem einzelnen Paar ausgegangen werden. Ist die Spezifikation algorithmisch in der in Abschnitt 3.1.2 diskutierten Form, so kann daraus direkt Code erzeugt werden.3 2
3
Round Trip Engineering“ erlaubt es, sowohl in der abstrakten Diagrammsicht ” als auch in der Implementierung Ver¨anderungen vorzunehmen, die dann in der jeweils anderen Sicht nachgezogen werden. Diese Vorgehensweise ist jedoch fragil, da nach heutigem Stand der Technik nur Kommentare verwendet werden, um die Verbindung zwischen beiden Sichten aufrecht zu erhalten. Die Ausfuhrbarkeit ¨ besteht im Wesentlichen darin, dass die Nachbedingungen aus einer Konjunktion von Gleichungen besteht, deren linke Seiten ver¨anderbare Attribute beziehungsweise das Ergebnis darstellen und die rechten Seiten diese Attribute in nur eingeschr¨ankter Form benutzen. Zum Beispiel durfen ¨ keine zirkul¨aren
84
4 Transformationen fur ¨ die Codegenerierung
Weil die Umsetzung einer derartig spezifizierten Methode im Wesentlichen auf der in Abschnitt 4.3 diskutierten Umsetzung von OCL in Java-Code beruht, soll hier auf eine explizite Formulierung der Transformationsregel verzichtet werden. Im dritten oben genannten Fall existiert sowohl eine Implementierung als auch eine Spezifikation. Damit ist es sinnvoll, die Spezifikation zur Prufung ¨ w¨ahrend der Laufzeit einzusetzen. Der Generator weiß, ob er effizienten Produktionscode oder mit diesen Prufungen ¨ instrumentierten Code erzeugen ¨ soll. Zum Beispiel bieten Eiffel- und Java 1.4-Ubersetzer die Moglichkeit, ¨ Zusicherungen optional zu ubersetzen. ¨ Im Prinzip ist nur die Vorbedingung vor Start der Methode und die Nachbedingung nach deren Ende zu testen. Dabei sind jedoch unter Umst¨anden mit dem let-Konstrukt lokal definierte Variable und eventuell in der Nachbedingung genutzte Anfangszust¨ande von Attributen zu sichern. Diese Sicherung kann komplex sein, wenn die benutzten Attribute in anderen Objekten liegen und die Zugangspfade ihrerseits ver¨andert worden sein konnen. ¨ Eine uber ¨ den Abschnitt 4.4.3, Band 1 hinausgehende ausfuhrliche ¨ Diskussion dieser Problematik ist zum Beispiel in [RG02] zu finden. Auch die in Abschnitt 6.4.3, Band 1 diskutierten Methoden-Statecharts konnen ¨ zur Implementierung einer Methode verwendet und dabei wie oben beschrieben mit einer Vor-/Nachbedingung kombiniert werden. Die wesentlichen Strategien zur Umsetzung von Statecharts werden in Abschnitt 4.4 behandelt und basieren auf der in Abschnitt 6.6.2, Band 1 beschriebenen vereinfachenden Transformation fur ¨ Statecharts. Dabei entsteht ein Coderumpf, der dann, wie in der Transformation Methode1impl (Seite 82) beschrieben, eingesetzt werden kann. Als letzte Variante soll hier noch der Fall kurz diskutiert werden, in dem es weder eine Implementierung noch eine algorithmisch ausfuhrbare ¨ Spezifikation fur ¨ eine Methode gibt. Dann kann die Methode nicht automatisiert implementiert werden. Fur ¨ Simulationen und Tests, die diese Methode vielleicht nur marginal beruhren, ¨ sind jedoch Strategien moglich ¨ und sinnvoll, gute Implementierungen zu generieren. • • •
Spielt die Methode bei den durchzufuhrenden ¨ Tests keine Rolle, so kann ein Fehleraufruf oder die Ruckgabe ¨ eines Default-Werts in die Methode generiert werden. Ist die Methode noch nicht realisiert, so kann ein interaktives Eingabefeld w¨ahrend Simulationsl¨aufen dazu benutzt werden, dass der Nutzer auf Basis der aktuellen Parameter jeweils selbst das Ergebnis bestimmt. Fur ¨ eine, gezwungenermaßen endliche Menge von Eingaben konnen ¨ in einer Tabelle Ergebnisse abgelegt sein. Diese Ergebnisse konnen ¨ sogar aus fruheren ¨ interaktiven Simulationsl¨aufen mitprotokolliert worden sein. Abh¨angigkeiten zwischen Attributen bestehen, um eine sequentielle Berechnung zu ermoglichen. ¨ Jede Gleichung darf zus¨atzlich mit einer Bedingung versehen sein.
¨ 4.1 Ubersetzung von Klassendiagrammen
85
Einerseits ist eine interaktive Eingabe von Ergebnissen einzelner Methoden fur ¨ automatisierte Testl¨aufe nicht sinnvoll, andererseits konnen ¨ damit w¨ahrend der Vorfuhrung ¨ eines Prototypen sofort Anwenderentscheidungen in das System zuruckgef ¨ uhrt ¨ werden. Diese konnen ¨ protokolliert und sp¨ater zum Beispiel als Testdaten genutzt werden. Diese interaktive Form des Erkenntnisgewinns ist sicherlich beschr¨ankt, kann aber unter Umst¨anden zu effektiverer Kommunikation mit Anwendern fuhren. ¨ Es ist generell nicht ublich, ¨ Hilfsmethoden wie getAttr in Klassendiagrammen explizit zu vermerken. Dadurch bleibt das Modell kompakter und ubersichtlicher. ¨ Auch mussen ¨ diese Funktionen in Coderumpfen, ¨ die bei der Generierung ubersetzt ¨ werden, nicht explizit verwendet werden. Es reicht aus, den Attributzugriff und die Attributbesetzung in Form von Zuweisungen einzusetzen. Ein Codegenerator ubersetzt ¨ diese wie in den Transformationsregeln beschrieben in Methodenaufrufe. Es sollte jedoch erlaubt sein, diese Methoden direkt zu verwenden. Außerdem ist unter Umst¨anden sinnvoll, die Generierung einer solchen Methode vorwegzunehmen, indem eine manuelle Implementierung angegeben wird. Dadurch lassen sich eventuell Optimierungen vornehmen oder zus¨atzliche Funktionalit¨aten realisieren. 4.1.3 Assoziationen Eine unidirektionale Assoziation wird standardm¨aßig durch ein Attribut umgesetzt. Der Rollenname wird dabei als Attributname verwendet. Fehlt der notwendige Rollenname, so wird wie bei den in Abschnitt 4.3.8, Band 1 angegebenen Navigationsregeln ein Attributname aus dem Assoziationsnamen oder dem Namen der gegenuberliegenden ¨ Klasse gebildet. Kardinalit¨aten werden entsprechend berucksichtigt: ¨ 0..1“ fuhrt ¨ zu ei” nem einfachen Attribut, das den Wert null annehmen darf, 1“ fuhrt ¨ zu ” einem einfachen Attribut, das immer besetzt ist, und eine Assoziation mit Kardinalit¨at *“ wird mengenwertig. Abh¨angig von zus¨atzlichen Merkma” len wie {ordered} stehen Mengen- oder Listen-Implementierungen zur Auswahl. Fur ¨ qualifizierte Assoziationen wird entsprechend eine Abbildung (Map) zur Verfugung ¨ gestellt. Bidirektionale Assoziationen werden durch Attribute auf beiden Seiten realisiert, die durch ein geeignetes Methodenprotokoll konsistent gehalten werden. Ist keine Navigationsrichtung angegeben, so wird eine geeignete Navigationsrichtung aus dem Kontext ermittelt und gegebenenfalls werden beide Richtungen realisiert. Um die oben genannte Konsistenz bidirektionaler Assoziationen zu sichern, werden alle Zugriffe auf die Assoziation uber ¨ generierte Methoden gefuhrt. ¨ Die Form dieser generierten Methoden, also das fur ¨ eine Assoziation verwendbare API, h¨angt von den Eigenschaften und Merkmalen der Assoziation ab. So werden bei den Merkmalen {addOnly} und {frozen} entsprechende Funktionen zur Modifikation eingeschr¨ankt. Abgeleitete Assoziationen wer-
86
4 Transformationen fur ¨ die Codegenerierung
den mit denselben Prinzipien behandelt, wie abgeleitete Attribute. Das heißt, es werden nur Abfragemethoden zur Verfugung ¨ gestellt und diese durch Berechnungen implementiert. Nachfolgende Transformation ist exemplarisch fur ¨ bidirektionale, in beiden Richtungen mit Kardinalit¨at *“ versehene Assoziationen. ” Assoziation∗,∗,bidir : Bidirektionale Assoziation Erkl¨arung
Assoziationen werden in den Zustandsraum zumindest einer der beteiligten Klassen transformiert, indem entsprechende Attribute und Zugriffsfunktionen generiert werden. Diese Transformationsregel ist fur ¨ bidirektionale Assoziationen mit Kardinalit¨at *“ in beiden Richtungen geeignet. Die ” Assoziation ist nicht abgeleitet und keine Komposition.
Definition der Assoziation
• Nachfolgende Ausfuhrungen ¨ gelten fur ¨ ClassB entsprechend, da die Situation symmetrisch ist. • Zugriffe auf die Assoziation werden durch Zugriffe auf das Attribut roleB modelliert, das die in Abschnitt 4.3.5, Band 1 eingefuhrte ¨ Signatur von Collection(ClassB) besitzt. • Der Attributname roleB extrahiert sich aus dem Rollennamen, dem Namen der Assoziation (assocname) oder wenn beide fehlen, dem Namen der gegenuberliegenden ¨ Klasse (classB). Allerdings muss die Eindeutigkeit des Namens gew¨ahrleistet sein (siehe Abschnitt 4.3.8, Band 1). • Die Umsetzung der zur Modellierung verwendeten Konstrukte in den Implementierungscode erfolgt relativ schematisch, jedoch werden ver¨andernde Operationen wie addRoleB oder removeRoleB entsprechend angepasst, um damit die Konsistenz der bidirektionalen Assoziation sicherzustellen. (Fortsetzung auf n¨achster Seite)
¨ 4.1 Ubersetzung von Klassendiagrammen
87
(Fortsetzung von Tabelle 4.4.: Assoziation∗,∗,bidir : Bidirektionale Assoziation)
Zugriffsfunktionen
roleB.isEmpty() ⇓ roleB.isEmpty() roleB.size ⇓ roleB.size()
roleB.contains(obj) ⇓ roleB.contains(obj) etc.
• Lesende Zugriffe bleiben weitgehend erhalten. Die Umsetzung entspricht der Standardumsetzung des OCLCollection-Interface nach Java. Modifikation
roleB.add(obj) ⇓ addRoleB(obj)
quali.roleB.add(obj) ⇓ quali.addRoleB(obj)
roleB.remove(obj) ⇓ removeRoleB(obj)
quali.roleB.remove(obj) ⇓ quali.removeRoleB(obj)
etc. • Modifizierende Zugriffe werden auf speziell generierte Methoden abgebildet. • Weitere modifizierende Zugriffe, wie zum Beispiel roleB.clear(), werden ebenfalls entsprechend abgebildet. OCLNavigation
roleB ⇓ getRoleB()
quali.roleB ⇓ quali.getRoleB()
• getRoleB liefert als Ergebnis eine unver¨anderbare Menge.4 (Fortsetzung auf n¨achster Seite)
4
Die Methode unmodifiableSet der Klasse java.util.Collections produziert aus einer beliebigen eine unver¨anderbare Menge, ohne allerdings die Signatur zu ver¨andern.
88
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.4.: Assoziation∗,∗,bidir : Bidirektionale Assoziation)
Zus¨atzliche Methoden
⇓ Java
class ClassA { ... public synchronized Set(ClassB) getRoleB() { return Collections.unmodifiableSet(roleB); } public synchronized void addRoleB(ClassB b) { roleB.add(b); b.addLocalRoleA(this); } public synchronized void addLocalRoleB(ClassB b) { roleB.add(b); } }
• Hilfsfunktionen wie addLocalRoleB oder removeLocalRoleB durfen ¨ außerhalb dieses Protokolls nicht benutzt werden, obwohl sie als public generiert werden. Sie stehen deshalb dem Entwickler nicht zur 5 Verfugung. ¨ • Weitere Modifikatoren wie removeRoleB oder clearRoleB werden in a¨ hnlicher Form generiert. Allerdings erfordert zum Beispiel clearRoleB in bidirektionalen Assoziationen die Abmeldung jedes Links auf der gegenuberliegenden ¨ Seite, hat also lineare Komplexit¨at. • Ist das Merkmal {addOnly} angegeben, so stehen remove-Operationen nicht zur Verfugung. ¨ • Ist das Merkmal {ordered} angegeben, so wird eine Listen-Implementierung gew¨ahlt und die entsprechende Funktionalit¨at zus¨atzlich angeboten. Beachtenswert
Die durch ein Protokoll gesicherte Konsistenz zwischen beiden Enden einer bidirektionalen Assoziation besitzt im Normalfall nur konstanten Zusatzaufwand, ist also vertretbar. Ist eine Assoziation nur unidirektional, so kann dieser Aufwand dennoch wegfallen.
Tabelle 4.4.: Assoziation∗,∗,bidir : Bidirektionale Assoziation
5
Dies kann einerseits durch geeignete Kontextprufungen ¨ bei den Coderumpfen ¨ gesichert, andererseits aber auch durch Verwendung außerhalb des Generators nicht bekannter Methodennamen erreicht werden.
¨ 4.1 Ubersetzung von Klassendiagrammen
89
Die Umsetzung von Assoziationen in Java-Code zeigt, wie groß die Variationsmoglichkeiten ¨ bei der Codegenerierung sind. Variabel abh¨angig von den Eigenschaften der Assoziation ist nicht nur das API einer Assoziation (also welche Funktionen in UML/P zum Zugriff und zur Manipulation zur Verfugung ¨ stehen), sondern auch die intern genutzte Datenstruktur. Da die Wahl der Datenstruktur zumindest Auswirkungen auf das Laufzeitverhalten der Implementierung hat, wird sinnvollerweise durch geeignete Steuerungsmechanismen wie etwa dem Merkmal {Vector} oder durch geeignete Anpassung der Skripte die Auswahl der Implementierung ermoglicht. ¨ Fur ¨ Assoziationen mit beschr¨ankten Kardinalit¨aten ist außerdem zu kl¨aren, wie der Versuch einer Verletzung der Kardinalit¨at behandelt wird. Dafur ¨ gibt es zum Beispiel die Varianten, dies robust zuzulassen, aber gegebenenfalls eine Warnung zu protokollieren, bis hin zur Erzeugung einer Exception, die dann vom aufrufenden Objekt zu behandeln ist. Neben der oben vorgeschlagenen Form der Implementierung einer Assoziation gibt es Vorschl¨age, die Links durch eigenst¨andige Objekte zu realisieren oder durch eine global verwaltete Datenstruktur zu ersetzen. All diese Erweiterungen haben als Ziel, zus¨atzliche Funktionalit¨at anzubieten, die durch das API der Modellierung zug¨anglich werden, oder Verhaltens- beziehungsweise Sicherheitseigenschaften zu optimieren. Eine globale Datenstruktur in Form einer Abbildung von Quell- zu Zielobjekt ist zum Beispiel von Interesse, wenn die Assoziation dunn ¨ besetzt ist und der Speicherplatz dadurch effizienter genutzt wird. Dies sollte dem Nutzer der API verborgen bleiben, da es sich um Realisierungsdetails handelt. Die notwendige Umsetzung von Java-Code zur Sicherung der Konsistenz der Assoziation zeigt, dass es wichtig ist, dass der Codegenerator die vollst¨andige Kontrolle uber ¨ alle Teile des generierten Codes, also auch uber ¨ Methodenrumpfe ¨ hat. Dadurch wird beispielsweise die fur ¨ bidirektionale Assoziationen gultige ¨ Konsistenzbedingung gesichert: context ClassA a, ClassB b inv: a.roleB.contains(b) <=> b.roleA.contains(a)
OCL
Das bereits diskutierte Verfahren des Roundtrip-Engineering kann dies nicht leisten, da es dem Entwickler die Moglichkeit ¨ gibt, beliebig in generierte Datenstrukturen einzugreifen. Dort musste ¨ also diese Konsistenzbedingung zur Laufzeit gepruft ¨ werden. Bei einer Transformation der Methodenrumpfe ¨ durch den Codegenerator konnen ¨ die Zugriffe und Modifikationen fur ¨ die Assoziation uberpr ¨ uft ¨ beziehungsweise transformiert und damit verhindert werden, dass dem Entwickler die Methode addLocalRoleB zur Programmierung zur Verfugung ¨ steht.6 6
Meistens wird den generierten Methoden ein anderer, nur intern bekannter Name gegeben und damit auch eine zuf¨allige Namensubereinstimmung ¨ verhindert. Dadurch kann der Entwickler eine beliebige Methode addLocalRoleB definieren, die mit der generierten Methode nicht in Zusammenhang steht.
90
4 Transformationen fur ¨ die Codegenerierung
4.1.4 Qualifizierte Assoziation Die qualifizierte Assoziation bietet gegenuber ¨ der normalen Assoziation ein angepasstes API, das die qualifizierte Selektion und Manipulation erlaubt, aber auch einige Operationen zur Modifikation unqualifizierter Assoziationen verbietet. Deshalb wird fur ¨ die qualifizierte Assoziation eine eigene Transformationsliste angegeben, die auch das API beschreibt. Assoziationquali : Qualifizierte Assoziation Erkl¨arung
Eine qualifizierte Assoziation wird a¨ hnlich der normalen Assoziation umgesetzt, bietet aber angepasste Funktionalit¨at fur ¨ qualifizierten Zugriff. Diese Transformationsregel ist geeignet fur ¨ unidirektionale qualifizierte Assoziationen mit Kardinalit¨at 1“.7 Die Assozia” tion ist weder abgeleitet noch eine Komposition. Der angegebene Qualifikator qualifier ist ein Attribut der gegenuberliegenden ¨ Klasse. Der Qualifikatorwert ist deshalb identisch zu dem Attributwert.
Definiten der Assoziation
• Zugriffe auf die Assoziation werden durch Zugriffe auf das Attribut roleB modelliert, das eine Signatur der Form Map(QualiType,ClassB) besitzt. • Zus¨atzliche Methoden der unqualifizierten Assoziationen, wie das nachfolgend definierte addRoleB, sind moglich, ¨ weil der Qualifikator im Zielobjekt enthalten ist. (Fortsetzung auf n¨achster Seite)
7
Wie in Abschnitt 3.3.7, Band 1 beschrieben, bedeutet die Kardinalit¨at 1“, dass der ” qualifizierte Zugriff genau ein Objekt liefert.
¨ 4.1 Ubersetzung von Klassendiagrammen
91
(Fortsetzung von Tabelle 4.5.: Assoziationquali : Qualifizierte Assoziation)
Zugriffsfunktionen
roleB.get(key) ⇓ roleB.get(key)
roleB.isEmpty ⇓ roleB.isEmpty()
roleB.containsValue(obj) ⇓ roleB.containsValue(obj)
roleB.keySet() ⇓ roleB.keySet()
roleB.containsKey(obj) ⇓ roleB.containsKey(obj)
roleB.values() ⇓ roleB.values()
roleB.size ⇓ roleB.size() • Lesende Zugriffe bleiben weitgehend erhalten. Modifikation
roleB.clear() ⇓ roleB.clear() roleB.removeValue(obj) ⇓ roleB.remove(obj.qualifier)
roleB.put(key,obj) ⇓ putRoleB(key,obj) roleB.removeKey(obj) ⇓ roleB.remove(obj)
roleB.add(obj) ⇓ roleB.put(obj.qualifier,obj)
etc.
• Weitere modifizierende Zugriffe, wie zum Beispiel roleB.putAll, werden entsprechend abgebildet. • Die Methode remove(obj) fur ¨ unqualifizierte Assoziationen wird fur ¨ diese Form der qualifizierten Assoziationen nicht angeboten, weil eine gleichnamige Methode fur ¨ Maps eine andere Funktionalit¨at erfullt ¨ (sie entfernt Schlussel¨ werte). Stattdessen werden zwei Operationen mit jeweils eigenem Namen angeboten. • Bei gesetztem Merkmal {addOnly} stehen die removeOperationen nicht zur Verfugung. ¨ OCLNavigation
roleB ⇓ roleB.values()
roleB[key] ⇓ roleB.get(key)
• Qualifizierte und unqualifizierte Navigation sind moglich. ¨ (Fortsetzung auf n¨achster Seite)
92
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.5.: Assoziationquali : Qualifizierte Assoziation)
Zus¨atzliche Methoden
⇓ Java
class ClassA { ... public synchronized Collection(ClassB) getRoleB() { return Collections.unmodifiableCollection( roleB.values()); } public synchronized void putRoleB (QualiType q, ClassB b) { if(q==b.qualifier) { // Objekttypen nutzen equals() roleB.put(q,b); } else { // Exception, Warnung oder // robuste Implementierung } } }
• Die Methode putRoleB wird verwendet, um sicherzustellen, dass der Qualifikatorwert (Schlussel) ¨ und der Wert des Attributs qualifier identisch sind. Beachtenswert
Abh¨angig vom Zweck des Codes (Test, Simulation, Produktion) werden verschiedene Strategien fur ¨ die Behandlung des Fehlerfalls von einer Fehlermeldung uber ¨ eine Mitteilung in einem Protokoll bis hin zur robusten Implementierung eingesetzt. Der Zugriff auf das hier verwendete Attribut roleB ist entsprechend der fur ¨ dieses Attribut gultigen ¨ Transformation ebenfalls umzusetzen. Tabelle 4.5.: Assoziationquali : Qualifizierte Assoziation
Die Transformation der qualifizierten Assoziation nutzt die Komponierbarkeit von Transformationsregeln, da hier zun¨achst eine Assoziation in ein Attribut transformiert wird, das durch eine weitere Transformation durch Zugriffsmethoden gekapselt wird. Bei dieser Kapselung durch Zugriffsmethoden ist allerdings zu beachten, dass die Methode getroleB zwei unterschiedliche Aufgaben zu erfullen ¨ hat. Bei qualifizierten Assoziationen ist zwischen (1) der Menge aller durch die Links erreichbaren Objekte und dem (2) Attributinhalt zu unterscheiden. Nur bei normalen Assoziationen sind beide Bedeutungsvarianten identisch. Die Methode getroleB realisiert Vari-
¨ 4.1 Ubersetzung von Klassendiagrammen
93
ante (1). Fur ¨ die Variante (2) wird bei Bedarf eine Methode mit dem Namen getroleBAttribute eingefuhrt, ¨ die hier ein Map-Objekt zuruckgibt. ¨ Durch die zahlreichen qualifizierten Zugriffsmoglichkeiten ¨ sollte jedoch der Zugriff auf die realisierende Map-Datenstruktur durch den Modellierer nicht notwendig sein. 4.1.5 Komposition Wie bereits in Abschnitt 3.3.4, Band 1 diskutiert, besteht zwischen den Lebenszyklen des Kompositums und den davon abh¨angigen Objekten eine zeitliche Beziehung. Diese ist jedoch durch erhebliche Interpretationsunterschiede gekennzeichnet. Die Komposition wird strukturell wie eine normale Assoziation behandelt, das Anlegen beziehungsweise Entfernen von Links aus einer Komposition unterliegt aber der jeweiligen Interpretation. Entsprechend werden einige Operationen des Assoziations-API nicht angeboten oder unterliegen Restriktionen. Eine Interpretation des Kompositums, die relativ verbreitet ist und im Auktionsprojekt als einzige verwendet wurde, wird nachfolgend dargestellt. Kompositionf rozen : Fixierte Komposition Erkl¨arung
Kompositionsdefinition
Die fixierte Form der Komposition wird genutzt, wenn das abh¨angige Objekt dieselbe Lebensspanne wie das Kompositum hat, w¨ahrend der Initialisierungsphase des Kompositums erzeugt wird und der Link zwischen beiden Objekten unver¨anderbar ist. Diese Transformationsregel ist geeignet fur ¨ die unidirektionale Kompositionen mit Kardinalit¨at 1“. ”
• Die Struktur entspricht einer Assoziation mit derselben Kardinalit¨at. • Zugriffe auf die Assoziation werden durch Zugriffe auf das Attribut roleB modelliert, das einen einfachen Objekttyp hat. (Fortsetzung auf n¨achster Seite)
94
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.6.: Kompositionf rozen : Fixierte Komposition)
• Der Attributname roleB extrahiert sich aus dem Rollennamen, dem Namen der Assoziation (assocname) oder wenn beide fehlen (was bei Kompositionen h¨aufig der Fall ist), dem Namen der gegenuberliegenden ¨ Klasse (classB). Allerdings muss die Eindeutigkeit des Namens gew¨ahrleistet sein (siehe Abschnitt 4.3.8, Band 1). Zugriffsfunktion
roleB ⇓ roleB • Lesende Zugriffe auf das Attribut werden durch eine nachgeschaltete Transformationsregel (normalerweise in getRoleB()) umgesetzt.
Modifikation
Die Besetzung des Attributs roleB darf ausschließlich im Konstruktor, also der Initialisierungsphase erfolgen.8 Dafur ¨ wird entweder eine Factory oder ein new-Kommando eingesetzt: roleB=factory.newClassB(arguments) roleB=new ClassB(arguments). Eine statische Analyse sichert, dass die einmalige Besetzung des Attributs in jedem Fall stattfindet. • Im Fall einer bidirektionalen Komposition wird in dem abh¨angigen Objekt durch einem in der Transformation Assoziation∗,∗,bidir beschriebenen Verfahren der entsprechende Link ebenfalls gesetzt. Dazu wird die oben gezeigte Besetzung mit roleB= durch einen Methodenaufruf setRoleB ersetzt.
OCLNavigation Beachtenswert
wie in vorangegangenen Transformationsregeln Die Restriktion, dass das abh¨angige Objekt erst im Konstruktor des Kompositums erzeugt wird, stellt sicher, dass abh¨angige Objekte nicht mehrfach verwendet werden.9 Eine weniger strikte Umsetzung wurde ¨ zum Beispiel erlauben, das abh¨angige Objekt bereits als Parameter an den Konstruktor zu uberge¨ ben. Dann kann jedoch nicht mehr sicher festgestellt werden, ob das Objekt neu erzeugt wurde und damit der Kompositionsbeziehung genugt. ¨ Tabelle 4.6.: Kompositionf rozen : Fixierte Komposition
8 9
Bei bestimmten Klassen, zum Beispiel Applets, ist die Initialisierung in eine Methode init() ausgelagert, die dann zur Initialisierungsphase z¨ahlt. Dabei wird angenommen, dass eine gegebenenfalls verwendete Factory tats¨achlich neue Objekte erzeugt.
¨ 4.1 Ubersetzung von Klassendiagrammen
95
4.1.6 Klassen ¨ Die Ubersetzung einer Klasse mit ihren Attributen, Methoden, Assoziationen, Kompositionen und den bislang noch nicht besprochenen Vererbungsbeziehungen ist relativ schematisch, da die kanonische Vorgehensweise die direkte Abbildung der UML-Klasse in die Java-Klasse ist. Die Umsetzung von Klassen ist jedoch stark getrieben durch Stereotypen und Merkmale, die steuern, welche zus¨atzliche Funktionalit¨at und welche Varianten der Transformation von Attributen vorgenommen werden. In dieser Grundtransformation werden keine Stereotypen berucksichtigt. ¨ Klassen: Umsetzung einer Klasse Erkl¨arung
Eine Klasse wird direkt ubernommen. ¨ In Abh¨angigkeit der ihr beigefugten ¨ Stereotypen und Merkmale sowie genereller ¨ Ubersetzungsvorgaben wird fur ¨ die Klasse zus¨atzliche Funktionalit¨at generiert, die dem Entwickler bei der Benutzung der Klasse zur Verfugung ¨ steht.
Klassendefinition
• Vererbung und Interface-Implementierung werden uber¨ nommen. • Attribute, Assoziationen werden entsprechend der jeweils gultigen ¨ Regeln zu Code transformiert. • Stereotypen und Merkmale steuern sowohl die Umsetzung der genannten Modellierungselemente als auch die Generierung zus¨atzlicher Funktionalit¨at. Vergleichsfunktion
⇓ class Class { ... public boolean equals(Object obj) { // Vergleich der Attribute } }
Java
(Fortsetzung auf n¨achster Seite)
96
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.7.: Klassen: Umsetzung einer Klasse)
• Die Methode equals vergleicht die einzelnen, neu definierten Attribute und verwendet die gleichnamige Methode der Oberklasse. • Assoziationen und abgeleitete Attribute werden im Normalfall zum Vergleich nicht berucksichtigt, ¨ jedoch aber Kompositionen, bei denen die gerade bearbeitete Klasse das Kompositum darstellt. • Das Merkmal {Equals=Liste} erlaubt die explizite Auflistung, welche Attribute und Assoziationen in den Vergleich einbezogen werden. Abkurzend ¨ kann mit dem Merkmal Equals+ eine Liste zus¨atzlicher Assoziationen oder mit Equals- eine Negativliste auszunehmender Attribute spezifiziert werden. • Ist fur ¨ die Klasse eine equals-Methode bereits explizit angegeben, so wird diese ubernommen ¨ anstatt sie zu generieren. Hashfunktion
⇓
class Class { ... Java public int hashCode() { // geeignete Berechnung aus den Attributen } }
• Die Hash-Funktion wird geeignet implementiert. • Mit den Merkmalen {Hash=Liste}, {Hash+} und {Hash-} kann analog zur Vergleichsfunktion gesteuert werden, welche Attribute dafur ¨ herangezogen werden. • Ist fur ¨ die Klasse eine hash-Methode bereits explizit angegeben, so wird diese ubernommen. ¨ Stringumwandlung
⇓
class Class { ... public String toString() { // Umsetzung der Attribute, Assoziationen } }
Java
(Fortsetzung auf n¨achster Seite)
¨ 4.1 Ubersetzung von Klassendiagrammen
97
(Fortsetzung von Tabelle 4.7.: Klassen: Umsetzung einer Klasse)
• Die Methode toString liefert eine einfache Umsetzung in einen String, der die Inhalte der beteiligten Attribute wiedergibt. Diese Form der Ausgabe dient vor allem fur ¨ Tests und Simulationen und sollte im Produktionssystem normalerweise nicht eingesetzt werden. • Assoziationen, die im Zustandsraum der Klasse abgelegt sind, abgeleitete Attribute und Kompositionen werden miteinbezogen. • Die Merkmale {ToString=Liste}, {ToString+} und {ToString-} erlauben die Kontrolle daruber, ¨ welche Klassenelemente ausgegeben werden. • Das Merkmal {ToStringVerbosity=Nummer} erlaubt die Steuerung der Verbosit¨at. 0: Keine Ausgabe, 1: Klassenname, 2: Attributinhalte sehr kompakt (ohne erreichbare und abh¨angige Objekte) und 6: verbose Ausgabe jedes Attributs und jeder Assoziation in der Form Attributname=Attributwert, die alle erreichbaren Objekte einschließt. • Ist fur ¨ die Klasse eine toString-Methode bereits explizit angegeben, so wird diese ubernommen. ¨ Konstruktoren
⇓
class Class { ... Java public Class() { // geeignete Besetzung der Attribute mit Defaults } public Class(Attributlist) { setAttribute(attribute); ... } }
• Konstruktoren werden gem¨aß der Generierungsstrategie erzeugt. • Wenn nicht explizit ausgeschlossen, dann ist standardm¨aßig der leere Konstruktor und ein Konstruktor zur Besetzung aller Attribute dabei. • Weil das Merkmal {new(Attributlist)} mehfrach anwendbar ist, konnen ¨ beliebig viele Konstruktoren erzeugt werden. Alternativ ist es auch moglich, ¨ Konstruktoren direkt anzugeben, weil so zus¨atzliche Funktionalit¨at im Konstruktor realisiert werden kann. (Fortsetzung auf n¨achster Seite)
98
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.7.: Klassen: Umsetzung einer Klasse)
Protokollausgabe
⇓ Java
class Class { ... public String stringForProtocol() { // Umsetzung der Attribute, Teile der Assoziationen } }
• Diese Methode arbeitet a¨ hnlich wie toString, wird aber fur ¨ die Ausgabe in Protokollen verwendet. Sie kann analog parametrisiert beziehungsweise manuell implementiert werden. Beachtenswert
Neben stringForProtocol gibt es eine Reihe weiterer Funktionen, die in entsprechender Form realisiert werden, aber hier nicht erw¨ahnt wurden. Einige sind aus der von allen Objekten abgeleiteten Klasse Object (beispielsweise clone), andere folgen aus Interfaces, die zu implementieren sind (beispielsweise compareTo aus dem Interface Comparable) und wieder andere sind bedingt durch Implementierungsvorgaben fur ¨ den Codegenerator. Dazu gehoren ¨ Funktionalit¨aten fur ¨ die Protokollausgabe wie oben beschrieben, Speicherung, Fehlerbehandlung und zus¨atzliche Funktionen, die zur Bearbeitung von Tests hilfreich sind. Tabelle 4.7.: Klassen: Umsetzung einer Klasse
Gerade fur ¨ den Einsatz in Testumgebungen sind unter Umst¨anden eine Reihe weiterer Methoden und Datenstrukturen fur ¨ eine Klasse zu generieren. Bei der Generierung solcher uniformen Methoden fur ¨ Implementierung und Tests kann ein Codegenerator wertvolle Dienste leisten. Eine der wenigen und eher selten gew¨ahlten Alternativen zu der hier beschriebenen Abbildung sei dennoch erw¨ahnt. Sie verzichtet darauf, das Typsystem der Zielsprache Java zu nutzen und legt stattdessen Attribute als Abbildung des Attributnamens auf den Wert mit dem Hashtable (String, Object) ab. Es ist dann im Prinzip ausreichend, eine einzige Java-Klasse in der in Abbildung 4.8 dargestellten Form zu realisieren, die zwar einiges an zus¨atzlicher Flexibilit¨at mit sich bringt, aber ineffizienter ist. Eine a¨ hnliche Form wird zum Beispiel zur Ressourcen-Verwaltung von Parametern verwendet. 4.1.7 Objekterzeugung Ein letzter interessanter Punkt im Kontext der Codeerzeugung fur ¨ Klassen ist das Management ihrer Objekte. Dazu gehort ¨ beispielsweise die Erzeugung
¨ 4.1 Ubersetzung von Klassendiagrammen class Chameleon { ... // Tr¨ager aller Attribute Hashtable(String,Object) attributes;
99 Java
public Object get(String attributeName) { return attributes.get(attributeName); }
}
// Typprufung: ¨ feststellen, ob bestimmte Attribute vorhanden sind public boolean isInstanceOf(Set(String) attributeNames) { return attributes.keySet().containsAll(attributeNames); }
Abbildung 4.8. Dynamische Verwaltung von Attributen
von Objekten, die Verwaltung und der effiziente Zugriff auf einzelne Objekte oder das Speichern und Laden von Datenbanken. Verwaltungst¨atigkeiten werden oft so genannten Management-Objekten“ auferlegt, die neben einer ” Sammlung der im Speicher befindlichen Objekte die transaktionsgesteuerte Abbildung auf die Datenbank und den effizienten Zugriff geladener Objekte erlauben. Von all diesen T¨atigkeiten soll nachfolgend nur die Objekterzeugung in Java diskutiert werden, da sie unter anderem fur ¨ Tests instrumentierbar sein muss. Die in den Coderumpfen ¨ verwendete Form des new Class(...) kann bei der Codeerzeugung durch den Aufruf geeigneter Factory-Methoden umgesetzt werden. Dies erhoht ¨ die Flexibilit¨at bei der Codeerzeugung betr¨achtlich, da so Unterklassen verwendet oder in automatisierten Tests Dummies 10 eingesetzt werden konnen. ¨ Dieses Verfahren basiert auf dem Entwurfsmuster Abstract Factory aus [GHJV94]. Objekterzeugung: Objekte mit einer Factory erzeugen Erkl¨arung
Im Quellcode wird die Objekterzeugung mit dem newKonstrukt vorgenommen. Der generierte Code enth¨alt stattdessen Factory-Aufrufe. Eine Standard-Factory wird generiert und kann durch Bildung von Unterklassen auf spezifische Situationen angepasst werden. (Fortsetzung auf n¨achster Seite)
10
Dummies simulieren ein Objekt, ohne die Funktionalit¨at tats¨achlich zu implementieren. Dies ist fur ¨ die Simulation einer Interaktion mit der Systemumgebung genauso geeignet wie fur ¨ Komponentengrenzen.
100
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.9.: Objekterzeugung: Objekte mit einer Factory erzeugen)
Objekterzeugung
new Class(Arguments) ⇓ Factory.f.newClass(Arguments) • Die generierte Klasse Factory besitzt ein statisches Attribut f vom Typ Factory, auf das die Methode newClass angewandt wird. Diese Methode erzeugt das neue Objekt. • Das Attribut f wird bei der Systeminitialisierung standardm¨aßig belegt, darf aber uberschrieben ¨ werden.
Klasse Factory
⇓
public class Factory { ... public static Factory f;
Java
public static initFactory() { f = new Factory(); }
}
// fur ¨ jede Klasse Class public Class newClass(Arguments) { return new Class(Arguments); }
• Entsprechende Factory-Methoden werden fur ¨ jede Klasse des Systems generiert. • Mehrere Factory-Methoden fur ¨ dieselbe Klasse mit unterschiedlichen Parameters¨atzen werden erzeugt, wenn es entsprechende Konstruktoren gibt. • Das Attribut f kann durch eine entsprechende Transformation durch Zugriffsmethoden geschutzt ¨ werden. • Eine Aufteilung der Factory in mehrere Klassen, zum Beispiel entsprechend einer Subsystem-Struktur, kann vorgenommen werden, muss dann aber vom Generator-Skript gesteuert werden. Alternative
Es ist moglich, ¨ statt einem einzelnen Attribut f fur ¨ mehrere Gruppen von zu erzeugenden Klassen beziehungsweise sogar fur ¨ jede Klasse ein eigenes Attribut einzusetzen, so dass die Generierung von Objekten individuell angepasst werden kann: (Fortsetzung auf n¨achster Seite)
¨ 4.1 Ubersetzung von Klassendiagrammen
101
(Fortsetzung von Tabelle 4.9.: Objekterzeugung: Objekte mit einer Factory erzeugen) Java
public class Factory { ... public static Factory fClass; ... // fur ¨ jede Klasse public static initFactory() { fClass = new Factory(); ... // fur ¨ jede Klasse }
}
// fur ¨ jede Klasse Class public Class newClass(Arguments) { return new Class(Arguments); }
wobei Aufrufe transformiert werden mittels: new Class(Arguments) ⇓ Factory.fClass.newClass(Arguments) Tabelle 4.9.: Objekterzeugung: Objekte mit einer Factory erzeugen
¨ Mehrstufige Ubersetzung Die exemplarisch diskutierten Varianten zur Umsetzung von Klassendiagrammen zeigen die hohe Bandbreite an moglichen ¨ Generierungsformen. Wie bereits diskutiert folgt daraus, dass die Codegenerierung eine grosse Flexibilit¨at benotigt, ¨ um die jeweils notwendigen Aufgaben zu erfullen. ¨ Ein Weg, die Flexibilit¨at zu steigern, ist die Moglichkeit, ¨ aus mehreren Templates oder Skripten auszuw¨ahlen. Daruber ¨ hinaus nutzen sich die Templates gegenseitig, indem zum Beispiel Assoziationen zun¨achst in Attribute transformiert und diese dann durch Zugriffsmethoden gekapselt werden. Die in diesem Abschnitt gezeigten Regeln zur Transformation von Konzepten der Klassendiagramme in Java-Code sind daher nicht unabh¨angig voneinander. Abbildung 4.10 zeigt die Abh¨angigkeiten der Transformationsregeln. Dabei sind nur die explizit definierten Regeln beschrieben, aber es sollte fur ¨ ein geeignetes Framework weitere Transformationsregeln geben, die durch weitere Templates festgelegt werden. Die Auswahl der Alternativen ist manchmal durch den Kontext oder Eigenschaften des ubersetzen ¨ Konzepts vorgegeben (wie hier zum Beispiel bei den Assoziationen) oder kann durch Einstellungen des Generators gesteuert werden (wie zum Beispiel bei den abgeleiteten Attributen).
102
4 Transformationen fur ¨ die Codegenerierung
Abbildung 4.10. Abh¨angigkeiten der behandelten Transformationsregeln zur Codegenerierung aus Klassendiagrammen
¨ 4.2 Ubersetzung von Objektdiagrammen Die vollst¨andige Beschreibung zur Generierung von Code aus der UML/P wurde ¨ den Rahmen dieses Buchs sprengen. Deshalb werden in den folgenden Abschnitten einige der interessantesten Aspekte der Transformation weiterer Diagramm- und Textarten in Java-Code erl¨autert, ohne alle Details zu diskutieren. Objektdiagramme konnen ¨ auf zwei Arten verwendet werden. Zum einen konnen ¨ Objektdiagramme konstruktiv eingesetzt werden, um damit Objektstrukturen zu erzeugen. Diese Funktionalit¨at kann sowohl im Produktionssystem als auch zur Darstellung von Objektstrukturen, auf denen automatisierte Tests stattfinden sollen, verwendet werden. Zum anderen werden Objektdiagramme als Pr¨adikate eingesetzt, um zu prufen, ¨ ob eine bestimmte Objektstruktur vorhanden ist. In Abschnitt 5.4, Band 1 wurden diese Verwendungsarten bereits vom methodischen Standpunkt aus beleuchtet. Dieser Abschnitt wird deshalb Aspekte der Codegenerierung aus Objektdiagrammen diskutieren. Dabei sei zun¨achst die in Kapitel 5, Band 1 vorgenommene Integration von Objektdiagrammen und OCL vernachl¨assigt. 4.2.1 Konstruktiv eingesetzte Objektdiagramme Die Transformation eines Objektdiagramms in Funktionalit¨at zum konstruktiven Aufbau von Objektstrukturen erfolgt gesteuert durch ein Skript beziehungsweise durch Merkmale, die dem Objektdiagramm angefugt ¨ werden.
¨ 4.2 Ubersetzung von Objektdiagrammen
103
Dabei sind einige Parameter festzulegen, die bei der Codegenerierung wichtig sind: 1. Die Klasse, der die Methode zur Erzeugung der Objektstruktur zugeordnet wird. Ist im Diagramm ein Objekt eindeutig ausgezeichnet, so kann dies entfallen. 2. Der Name der zu erzeugenden Methode. Als Default wird setupDiagramName verwendet, wenn dieser Name eindeutig ist. 3. Die Objekte des Diagramms, die bereits existieren und als Parameter ubergeben ¨ werden. In der Regel ist dies keines oder eines, das als uber¨ geordnetes Objekt bereits erzeugt wurde. 4. Treten im Objektdiagramm freie Variablen auf, so werden sie ebenfalls als Parameter fur ¨ die generierte Methode interpretiert. Da die Ausgangssituation und die damit jeweils bereits existenten Objekte unterschiedlich sein konnen, ¨ kann es sinnvoll sein, mehrere Methoden aus einem Objektdiagramm zu generieren. Diese konnen ¨ sich durch die Signatur oder, falls dies nicht eindeutig ist, auch durch die Methodennamen unterscheiden. Die Verwendung freier Variablen und unbesetzter Attribute als Parameter der generierten Funktion erlaubt es, Objektdiagramme wie in Abschnitt 5.2.2, Band 1 diskutiert als Muster mit prototypischen Objekten zu interpretieren, die eine mehrfache Instanziierung mit unterschiedlichen Inhalten erlauben. Bei der Generierung der setup-Methoden gibt es mehrere Aspekte, die zu beachten sind. Fur ¨ die Erzeugung eines Objekts ist die Verwendung eines Konstruktors notwendig. Idealerweise sollte ein Konstruktor ohne Parameter zur Verfugung ¨ stehen, der nur das leere Objekt erzeugt. Steht ein solcher Konstruktor nicht zur Verfugung, ¨ so kann im Testsystem ein solcher generiert werden. Im Produktionssystem muss allerdings auf einen existenten Konstruktor zuruckgegriffen ¨ werden, der entsprechend ausgezeichnet wurde. Ein weiteres Problem ist die Besetzung der Attribute entsprechend der im Objektdiagramm vorgegebenen Werte. Dafur ¨ sollten geeignete Hilfsfunktionen zur Verfugung ¨ stehen oder direkter Zugriff auf die Attribute erfolgen. Die in Abschnitt 4.1.1 diskutierten set-Methoden sind dafur ¨ nur partiell geeignet, da sie unter Umst¨anden zus¨atzliche Funktionalit¨at beinhalten. Im Prinzip konnen ¨ dafur ¨ auch Konstruktoren mit Parametern weiterhelfen, wenn die Zuordnung zwischen dem Parameter und dem zu besetzenden Attribut aus der Konstruktordefinition eindeutig hervorgeht.11 Ein Objektdiagramm kann grunds¨atzlich unvollst¨andig sein, indem etwa die Klasse eines Objekts nicht angegeben ist oder manchen Attributen kein Wert zugewiesen wurde. Zum einen konnen ¨ unbesetzte Attribute als freie 11
Das ist zum Beispiel dann der Fall, wenn der Konstruktor wie in Abschnitt 4.1 beschrieben ebenfalls generiert wurde.
104
4 Transformationen fur ¨ die Codegenerierung
Variable verstanden werden und als Parameter in die generierte Methode aufgenommen werden. Ist dies nicht gewunscht, ¨ so sind abh¨angig von der Art des Einsatzes im Testsystem, zur Simulation oder im Produktionssystem verschiedene Strategien zur Behandlung unbesetzter Attribute mo¨ glich. Im Testsystem wird eine Failure-Strategie genutzt: Unbesetzte Attributwerte sollten fur ¨ den getesteten Systemablauf keine Rolle spielen und der Zugriff darauf mit einem sofortigen Scheitern des Tests reagieren. Entsprechendes Verhalten kann in die get-Funktionen integriert werden. Bei der Simulation ist die bereits in Abschnitt 4.1.2 diskutierte Vorgehensweise sinnvoll, fehlende Attributwerte w¨ahrend des Simulationslaufs interaktiv zu erfragen oder mit Default-Werten zu arbeiten. Bei der Codegenerierung fur ¨ das Produktionssystem ist schließlich eine vollst¨andige Definition der Objekte im Objektdiagramm Voraussetzung. Dies verhindert Unachtsamkeiten bei der Definition von Objektdiagrammen und gibt so dem Entwickler Zutrauen in die Zuverl¨assigkeit des modellierten Systems. Nicht alle im Objektdiagramm formulierbaren Angaben werden bei konstruktiver oder auch der sp¨ater diskutierten pr¨adikativen Codegenerierung verwendet. Sichtbarkeiten, die Information uber ¨ Kompositionalit¨at eines Links, Merkmale wie {frozen} und a¨ hnliches mehr bedurfen ¨ keiner Umsetzung in den hier diskutierten Code, sondern werden mit den in Klassendiagrammen vorhandenen Informationen abgeglichen oder in Tests eingesetzt. Ein alternativer, mit Methodenspezifikationen verbundener Ansatz zum konstruktiven Einsatz von Objektdiagrammen ist bereits in Abschnitt 5.4.7, Band 1 diskutiert. Er nutzt ein Objektdiagramm in der Nachbedingung eines Konstruktors beziehungsweise einer Initialisierungsmethode, das nach denselben Prinzipien wie den hier gezeigten in konstruktiven Code umgesetzt werden kann. 4.2.2 Beispiel einer konstruktiven Codegenerierung Statt nun die Transformationsregeln fur ¨ jedes Modellelement des Objektdiagramms zu diskutieren sollen diese anhand des in Abbildung 4.11 gezeigten Objektdiagramms beispielhaft erl¨autert werden. Dieses Diagramm beschreibt einen Ausschnitt der initialen Objektstruktur des Applets im Auktionssystem und ist eingebettet in eine OCL-Methodenspezifikation fur ¨ die Initialisierungsfunktion init(). Der generierte Code ist in Abbildung 4.12 dargestellt, wobei hier und in den folgenden Beispielen vereinfachend angenommen wird, dass der direkte Zugriff auf die Attribute erst noch transformiert wird: Aus dem zugehorigen ¨ (hier nicht wiedergegebenen) Klassendiagramm kann das System ableiten, dass die Links durch die Attribute loginPanel und httpServerProxy in der Klasse WebBidding realisiert werden. Fur ¨ die Klassen HttpServerProxy und LoginPanel wird ein geeignet parametrisierter Konstruktor verwendet, der die Besetzung der angegebenen
¨ 4.2 Ubersetzung von Objektdiagrammen
context WebBidding.init() let String language = getParameter("language"); String login = getParameter("login") post: OD.WBinit
105
OCL
Abbildung 4.11. Objektdiagramm zur Initialisierung einer Struktur class WebBidding { ... Java public void init() { // aus dem let-Konstrukt String language = getParameter("language"); String login = getParameter("login"); // aus Objekt this status = AppStatus.INITIAL; person = null; auction = null; auctionChooserPanel = null; multibiddingPanel = null; appletLanguage = language==null ? "German" : language; // aus Objekt :LoginPanel setLoginPanel(new LoginPanel(login==null ? "":login,"")); // aus Objekt :HttpServerProxy setHttpServerProxy( new HttpServerProxy( "https://"+getCodeBase().getHost()+":443/")); }
}
Abbildung 4.12. Aus dem Objektdiagramm generierte init()-Funktion
Attribute vornimmt.12 Die Links werden durch set-Methoden besetzt, die auch die korrekte Besetzung der Ruckrichtung ¨ sichern. 12
Wenn kein geeigneter Konstruktor existiert, dann kann ein Codegenerator einen solchen Konstruktor generieren.
106
4 Transformationen fur ¨ die Codegenerierung
4.2.3 Als Pr¨adikate eingesetzte Objektdiagramme Der Einsatz eines Objektdiagramms als Pr¨adikat, das pruft, ¨ ob eine bestimmte Objektstruktur vorliegt und die im Objektdiagramm angegebenen Werte ¨ ubereinstimmen, ¨ wird in eine boolesche Methode transformiert. Ahnlich wie bei der konstruktiven Variante steuern mehrere Parameter den zu erzeugenden Code: 1. Die Klasse, der die boolesche Methode zugeordnet wird. Ist im Diagramm ein Objekt eindeutig ausgezeichnet, zum Beispiel durch den Namen this:Classname, so kann dies entfallen. Alternativ kann die Methode als statisch definiert und/oder einer Testklasse zugeordnet werden. 2. Der Name der zu erzeugenden Methode. Ist der Name nicht gegeben, so wird als Default isStructuredAsDiagramName verwendet. 3. Die Objekte des Diagramms, die als Ausgangsobjekte bereits identifiziert sind und deshalb als Parameter ubergeben ¨ werden. In der Regel ist dies ein einzelnes Objekt, das als eine Art Master fur ¨ die Objektstruktur gilt. 4. Treten im Objektdiagramm freie Variablen auf, so werden sie im Normalfall nicht weiter beachtet. Sollen diese Variablen aber bestimmte Werte annehmen, so werden die Variablen ebenfalls als Parameter fur ¨ die generierte Methode interpretiert. Im Gegensatz zur konstruktiven Variante eines Objektdiagramms kann ein pr¨adikativ eingesetztes Diagramm in mehrerer Hinsicht unvollst¨andig sein. Attribute und Attributwerte durfen ¨ ebenso weggelassen werden wie die Klassen der dargestellen Objekte. Auch Eigenschaften mit einer zeitlichen Implikation, wie etwa das Merkmal {frozen} fur ¨ Links, konnen ¨ nicht in einem Pr¨adikat uber ¨ einen Zustand gepruft ¨ werden. Dazu ist zus¨atzliche Infrastruktur notig, ¨ die dies entweder konstruktiv sichert, indem keine Methoden zur Modifikation eines Links angeboten werden, oder zur Laufzeit pruft, ¨ indem der ursprungliche ¨ Zustand des Links in einer Kopie aufgehoben wird. In Abschnitt 5.3, Band 1 wurde die Bedeutung eines Objektdiagramms als Pr¨adikat im Kontext der Integration mit OCL-Bedingungen bereits ausfuhr¨ lich diskutiert. Dabei wurde festgestellt, dass ein Objektdiagramm grunds¨atzlich als OCL-Bedingung dargestellt werden kann. In genau dieser Bedeutung werden pr¨adikative Objektdiagramme in entsprechende boolesche Methoden, die mit dem in Abschnitt 4.4.1, Band 1 eingefuhrten ¨ Stereotyp query markiert sind, ubersetzt. ¨ Die so generierten Methoden konnen ¨ genau wie die Referenz auf das Objektdiagramm bei Invarianten, Vor- und Nachbedingungen von Methoden und von Transitionen in Statecharts, aber auch in Java-Rumpfen ¨ des Produktionscodes eingesetzt werden. Fur ¨ die Verwendung im Produktionscode ist jedoch auf die Effizienz der Umsetzung zu achten. Wie in Abschnitt 5.3, Band 1 diskutiert, wirken anonyme Objekte des Objektdiagramms als existenzquantifiziert. In derselben
¨ 4.2 Ubersetzung von Objektdiagrammen
107
Weise werden benannte Objekte behandelt, die jedoch nicht bereits als Parameter an das boolesche Pr¨adikat ubergeben ¨ werden. Diese Objekte werden vom Pr¨adikat selbst gesucht, indem die entsprechenden Assoziationen gepruft ¨ werden. Die Belegung der freien Objekte erfolgt in einer dem StrukturMatching der Graph Grammatiken [Roz99, EEKR99] analogen Form. Bei mengenwertigen Assoziationen kann eine derartige Suche von linearer Komplexit¨at sein und sollte deshalb vermieden werden. Moglichkeiten ¨ zur Verbesserung der Situation bieten der Einsatz eines Qualifikators bei der ¨ Assoziation oder die explizite Ubergabe gesuchter Objekte als Parameter, wenn diese aus dem Kontext effizient ermittelt werden konnen. ¨
Abbildung 4.13. Fur ¨ ein Pr¨adikat bestimmtes Objektdiagramm
Anhand des aus dem Auktionssystem stammenden und in Abbildung 4.13 dargestellten Objektdiagramms wird die Transformation in ein Pr¨adikat illustriert. Der fur ¨ den Einsatz im Testsystem generierte Code ist in Abbildung 4.14 dargestellt. Nach einer Phase der Belegung von Objektnamen werden alle Attribute gepruft. ¨ Dabei durfen ¨ Attributwerte aufeinander Bezug nehmen. Werden statt der im (hier nicht dargestellten) Klassendiagramm angegebenen Typen echte Unterklassen angegeben, so wird gepruft, ¨ ob das entsprechende Objekt tats¨achlich zu dieser Unterklasse gehort. ¨ Anhand der L¨ange des in Abbildung 4.14 dargestellten Codes ist ersichtlich, dass eine Darstellung im Objektdiagramm kompakter und ubersichtli¨ cher ist, also dem Modellierer insbesondere bei der schnellen Erfassung eines ¨ Uberblicks und der Suche einzelner Werte Vorteile bringt.
108
4 Transformationen fur ¨ die Codegenerierung Java
class Auction { ... public static boolean isStructedAsKupfer(Auction kupfer912, int status, boolean isInExtension) { // Namen festlegen (optimierbar) BiddingPolicy bidPol = kupfer912.bidPol; TimingPolicy timePol = kupfer912.timePol; Money min = bidPol.min; Money max = bidPol.max; Time start = timePol.start; Time finish = timePol.finish; return // Hauptobjekt kupfer912.auctionIdent == 912 && kupfer912.title.equals("420t Kupfer") && kupfer912.numberOfBids == 0 && // BiddingPolicy bidPol instanceof DownwardBiddingPolicy && bidPol.kind == BiddingPolicy.DOWNWARD && bidPol.bidCountMax == BiddingPolicy.UNLIMITED && // Money Objekte min.amount == 52290000 && min.decimalplaces == 2 && min.currency.equals("$US") && min.full.equals("522.900,00 $US") && max.full.equals("720.000,00 $US") && // TimingPolicy timePol instanceof ConstantTimingPolicy && timePol.status == status && timePol.isInExtension == isInExtension && timePol.extensionTimeSecs == 180 && // Times start.timeSec == 953640000 && start.time.equals("13:00:00") && start.date.equals("21. Februar 2000") && finish.timeSec == start.timeSec + 2*60*60 && finish.time.equals("15:00:00") && finish.date.equals("21. Februar 2000") ; }
}
Abbildung 4.14. Aus Objektdiagramm generiertes Pr¨adikat
¨ 4.2 Ubersetzung von Objektdiagrammen
109
Als Erweiterung beziehungsweise Alternative bei der Codegenerierung kann eine weitere Methode mit dem Namen isExactlyStructuredAsDiagramName generiert werden. Diese Methode kann zus¨atzlich sichern, dass die Objektstruktur nur die im Diagramm angegebenen Objekte enth¨alt. Das ist besonders bei mehrwertigen und optionalen Assoziationen von Interesse und kann zum Beispiel durch einen geeigneten Stereotyp complete fur ¨ Objektdiagramme gesteuert werden. Tabelle 4.15 gibt eine knappe Einfuhrung ¨ zu diesem Stereotyp. Stereotyp complete Modellelement Motivation
Rahmenbedingung Wirkung
Objektdiagramm. Die Bedeutung eines Objektdiagramms als Pr¨adikat ist normalerweise so festgelegt, dass die explizit angegebenen Eigenschaften erfullt ¨ sein mussen. ¨ Weitere, im Diagramm nicht angegebene Objekte konnen ¨ existieren. In einem mit complete markierten Objektdiagramm mussen ¨ alle Attribute und Links angegeben sein. Geordnete Assoziationen sind vollst¨andig darzustellen. Der Stereotyp complete fordert, dass keine weiteren Objekte in der angegebenen Objektstruktur existieren. Das angegebene Objektdiagramm ist also eine vollst¨andige Darstellung der Objektstruktur. Damit kann die Methode isExactlyStructuredAsDiagramName generiert werden, die diese Vollst¨andigkeit zus¨atzlich zur Erfullung ¨ der angegebenen Eigenschaften des Objektdiagramms pruft. ¨ Tabelle 4.15.: Stereotyp complete
4.2.4 Objektdiagramm beschreibt Strukturmodifikation Eine weitere interessante Form des Einsatzes von Objektdiagrammen ergibt sich aus der Kombination beider Einsatzformen. Dabei wird eine existente Objektstruktur auf das Vorhandensein der im Objektdiagramm beschriebenen Objekte gepruft, ¨ die fehlenden Objekte generiert und die falsch besetzten Attribute modifiziert. Derartige Methoden erhalten als Namen adaptToDiagramName. Mit diesen Methoden kann eine bereits vorhandene Objektstruktur in Abh¨angigkeit des aktuell gewunschten ¨ Zustands umgebaut werden. Damit lassen sich Objektdiagramme zum Beispiel als Zustandsinvarianten im Statechart oder zur Adaption der jeweiligen Objektstruktur in der Entry-Aktion von Zust¨anden einsetzen. Wird beispielsweise das in Abbildung 4.11 gegebene Objektdiagramm (ohne Einbettung in die
110
4 Transformationen fur ¨ die Codegenerierung
dort stehende OCL-Bedingung) in dieser Form eingesetzt, so wird die in Abbildung 4.16 dargestellte Methode erzeugt. class WebBidding {
Java
public void adaptToWBinit(String language, String login) { // aus Objekt this status = AppStatus.INITIAL; person = null; auction = null; auctionChooserPanel = null; multibiddingPanel = null; appletLanguage = language==null ? "German" : language; // aus Objekt :LoginPanel if(loginPanel != null && loginPanel instanceof LoginPanel) { loginPanel.loginField = (login==null) ? "" : login; loginPanel.passwordField = ""; } else { setLoginPanel(new LoginPanel( login==null ? "" : login, "")); }
}}
// aus Objekt :HttpServerProxy if(httpServerProxy != null && httpServerProxy instanceof HttpServerProxy) { httpServerProxy.secureURL = "https://"+getCodeBase().getHost()+":443/"; httpServerProxy.connectStatus = HttpServerProxy.NOT CONNECTED; httpServerProxy.lastConnectionTime = Time.now(); } else { setHttpServerProxy( new HttpServerProxy( "https://"+getCodeBase().getHost()+":443/")); }
Abbildung 4.16. Aus Objektdiagramm generierte Adaptionsmethode
Fehlt ein Objekt oder hat es den falschen Typ, so wird es neu erzeugt. Ist das Objekt bereits vorhanden, so werden seine Attribute in der gewunschten ¨ Form modifiziert. Deshalb werden sowohl Konstruktoren, die in diesem Beispiel bereits Attributwerte als Parameter enthalten, als auch set-Methoden zur Attributbesetzung verwendet.
¨ 4.2 Ubersetzung von Objektdiagrammen
111
Die Eindeutigkeit des entlang eines Links zu identifizierenden Objekts wie zum Beispiel dem anonymen Objekt :LoginPanel ist nur bei Assoziationen der Kardinalit¨at 1“ oder 0..1“ gegeben. Bei mengenwertigen As” ” soziationen ist ein Vergleich mit allen vorhandenen Objekten durchzufuhren, ¨ der auch dessen Attribute einbezieht. Findet sich kein Objekt in der Objektstruktur, das dem im Diagramm angegebenen prototypischen Objekt entspricht, so wird in diesem Fall keines der vorhandenen Objekte angepasst, sondern ein neues Objekt erzeugt. Dadurch vergroßert ¨ sich die Menge der Links entsprechend. Der Nachteil dieser Methode ist allerdings, dass die Loschung ¨ unerwunschter ¨ Objekte nicht dargestellt werden kann. Außerdem kann diese Methode ineffizient werden, wenn zum Beispiel mit dem in Abbildung 2.28 gegebenen Objektdiagramm (ohne die OCL-Bedingung) und der daraus generierten und in Abbildung 4.17 dargestellten Methode hundert Personen einzeln angelegt werden. class Auction { ...
Java/P
public void adaptToNPersons(Auction test32, int x) { // setze Objekt test32 test32.auctionIdent = 32; test32.title = "Testauktion";
}
}
// gibt es p:Person? Person p = null; for(Iterator(Person) ip = participants.iterator(); ip.hasNext() && p==null; ) { Person pit = ip.next(); if(pit.personIdent == 1000+x && pit.login == "log" +x && pit.name == "Tester " +x && pit.isActive == (x%2 == 0)) { p = pit; } } // p:Person anlegen if(p == null) { p = new Person(); // Default-Konstruktor p.personIdent = 1000+x; p.login = "log" +x; p.name = "Tester " +x; p.isActive = (x }
Abbildung 4.17. Adaptionsmethode mit Suche in *-Assoziation
112
4 Transformationen fur ¨ die Codegenerierung
4.2.5 Objektdiagramme und OCL Ein wesentliches Mittel zur Steigerung der Ausdrucksm¨achtigkeit von Objektdiagrammen ist die ab Abschnitt 5.3, Band 1 durchgefuhrte ¨ Integration mit der OCL. Die Transformation eines um OCL-Bedingungen erweiterten Objektdiagramms in konstruktiven Code beziehungsweise in ein Pr¨adikat h¨angt daher von der Umsetzbarkeit der OCL-Bedingungen ab. Die Umsetzung und die Ausfuhrbarkeit ¨ von OCL-Bedingungen wurde bereits in Abschnitt 3.1.2 diskutiert. Zu beachten ist aber auch, dass die in der vorangegangenen Diskussion erw¨ahnte lineare Suchkomplexit¨at fur ¨ existenzquantifizierte Objekte polynomial ansteigen kann, wenn uber ¨ mehrere mengenwertige Assoziationen navigiert wird und die so erreichten Objekte uber ¨ eine OCL-Bedingung verknupft ¨ sind.
4.3 Codegenerierung aus OCL Auf Basis der bisher beschriebenen Moglichkeiten, ¨ die OCL als Spezifikationssprache fur ¨ UML/P-Programme zu nutzen, gibt es zum Beispiel die Moglichkeit, ¨ die OCL zur Modellierung von Invarianten in Datenbanksystemen einzusetzen [DH99] oder mit der OCL die Gesch¨aftslogik zu beschreiben [DHL01]. Die dabei zugrunde liegende Semantik der OCL ist in ihrer Essenz mit der bisher diskutierten Bedeutung der OCL identisch, jedoch ist die Einsatzform und damit die Abbildung der OCL in ausfuhrbaren ¨ Code wesentlich anders. Statt Klassen und Objekten stehen in relationalen Datenbanken Entit¨aten und Zeilen zur Verfugung, ¨ uber ¨ denen OCL-Ausdrucke ¨ interpretiert werden. Mehrere, teils prototypische Werkzeuge bieten bereits Realisierungen fur ¨ OCL-Codegeneratoren an. In [HDF00] wird eine modulare Architektur fur ¨ die OCL vorgestellt, die als Grundlage fur ¨ eine Integration mit anderen UML-Werkzeugen gut geeignet ist [BS01a, BBWL01]. Eine der OCL verwandte Sprache, genannt Java Interface Specification Language“ (JISL), ” wird in [MMPH99] zur Methodenspezifikation vorgestellt und diskutiert, wie die Ausfuhrbarkeit ¨ der Spezifikation durch eine Transformation nach Java vorgenommen werden kann. Weil zwischen OCL/P und Java aufgrund der syntaktischen und semantischen N¨ahe relativ viele Konzepte eindeutig nach Java umgesetzt werden, sollen nachfolgend vor allem die interessanten OCL-Konstrukte diskutiert werden. Dazu gehoren ¨ unter anderem die Umsetzung der Kontextdefinition, der OCL-Logik, der Komprehension, der Navigation, der Quantoren und der Spezialoperatoren. Viele, jedoch nicht alle der nachfolgend beschriebenen Codestucke ¨ lassen sich in wiederverwendbare Methoden kapseln. Es wird hier auf die Darstellung der Kapselung verzichtet, da dies nur den dargestellten Code ver-
4.3 Codegenerierung aus OCL
113
großert. ¨ Bei einer manuellen Verwendung dieser Codestucke ¨ (als eine Art von Entwurfsmuster) ist allerdings eine Kapselung zu empfehlen. 4.3.1 OCL-Aussage als Pr¨adikat Obwohl OCL-Bedingungen, wie in Abschnitt 3.1.2 beschrieben, in sehr eingeschr¨ankter Form in Implementierungscode umgesetzt werden konnen, ¨ hat eine OCL-Aussage eine naturliche ¨ Bedeutung als Pr¨adikat. Deshalb werden OCL-Aussagen, wie etwa context Auction a inv Bidders1: a.activeParticipants <= a.bidder.size
OCL
in boolesche Methoden ubersetzt. ¨ Dabei gibt es zwei Varianten. Die Interpretation als Invariante produziert: public static boolean invariantBidders1() { boolean res = true; Set(Auction) auctions = Auction.getAllInstances(); for(Iterator(Auction) ia = auctions.iterator(); ia.hasNext() && res; ) { Auction a = ia.next(); res &= a.activeParticipants <= a.bidder.size; } return res; }
Java/P
Die generierte Methode pruft ¨ die Invariante Bidders1 fur ¨ alle Objekte der Klasse Auction. Um dies zu ermoglichen, ¨ ist eine Infrastruktur zur Verwaltung der Menge aller Auktionsobjekte erforderlich, die zum Beispiel durch die Methode getAllInstances zur Verfugung ¨ gestellt wird. Tats¨achlich ist fur ¨ praktische Anwendungen ein Einsatz der OCL-Bedingung in der oben beschriebenen Form ungunstig. ¨ Im laufenden System werden seit der letzten Prufung ¨ der Invariante normalerweise nur relativ wenige Auktionsobjekte modifiziert worden sein. Deshalb ist es unter Umst¨anden gunstig, ¨ zus¨atzliche Infrastruktur zu investieren, um die Invariantenprufung ¨ effizienter zu gestalten. Dies kann erreicht werden, indem die Prufung ¨ der Invariante explizit an bestimmten Stellen gefordert wird. Dann sind aber nicht alle, sondern nur bestimmte Auktionsobjekte zu testen. Die dafur ¨ geeignete Methode ist public static boolean checkBidders1(Auction a) { return a.activeParticipants <= a.bidder.size(); }
und ihre der Klasse Auction zugeordnete Variante
Java
114
4 Transformationen fur ¨ die Codegenerierung
class Auction ... { public boolean checkBidders1() { return a.activeParticipants <= a.bidder.size(); }
Java
Alle diese Formen eignen sich zum Einsatz in Tests, in denen die Prufung ¨ von Invarianten explizit gefordert werden kann. Zu beachten ist, dass nach Konvention die vollst¨andige Invariante das Pr¨afix invariant besitzt, w¨ah¨ rend check fur ¨ die Einzelfall-Prufung ¨ verwendet wird. Die explizite Ubergabe des Kontexts einer OCL-Aussage in Form der zu prufenden ¨ Objekte wird normalerweise durch das Schlusselwort ¨ import vorgenommen. Deshalb werden die letzten beiden Methoden auch aus folgender Bedingung erzeugt: import Auction a inv Bidders1: a.activeParticipants <= a.bidder.size
OCL
Da in der Praxis ein mit dem Schlusselwort ¨ context geschlossener Kontext oft auch auf einzelne Objekte angewandt werden soll, werden daraus sowohl eine geschlossene als auch zus¨atzlich parametrisierte Methoden erzeugt. Besteht die Kontextangabe aus mehreren Objekten, so werden alle als Parameter verwendet. Folgende Tabelle 4.18 beschreibt die Transformation: OCL-Kontext: Umsetzung des Kontexts einer Bedingung Erkl¨arung
Kontext
Der Kontext wird in OCL explizit in Form von Objekten angegeben. Diese wirken als Parameter bei den daraus generierten booleschen Methoden. context Classcontext inv Name: Body
OCL
⇓
Java/P
public static boolean invariantName() { boolean res = true; // Iteration nur einmal dargestellt Set(Class) s = Class.getAllInstances(); for(Iterator(Class) it = s.iterator(); it.hasNext() && res; ) { Class ob = it.next(); res &= Body’; } return res; } public static boolean checkName(Classcontext) { Body’ } (Fortsetzung auf n¨achster Seite)
4.3 Codegenerierung aus OCL
115
(Fortsetzung von Tabelle 4.18.: OCL-Kontext: Umsetzung des Kontexts einer Bedingung)
• Die Iteration wird fur ¨ jedes Element Class ob des Kontexts geschachtelt wiederholt. Entsprechend ergeben sich polynomiale Komplexit¨aten bei der Prufung ¨ der vollst¨andigen Invariante. • Der generierte Code ist in einer statischen Methode abgelegt und wird einer geeigneten Klasse zugeordnet. Besteht der Kontext aus nur einem Objekt, so kann zus¨atzlich eine nicht-statische Methode in der Klasse des Objekts generiert werden. • Die Methode getAllInstances wird nachfolgend beschrieben. • Das alternative Schlusselwort ¨ import generiert nur die parametrisierten Formen. • Der Rumpf der Bedingung Body wird entsprechend der nachfolgenden Regeln in Body’ umgesetzt. Tabelle 4.18.: OCL-Kontext: Umsetzung des Kontexts einer Bedingung
4.3.2 OCL-Logik Wie in Abschnitt 4.2.2, Band 1 besprochen ist die OCL-Logik zweiwertig und nutzt einen impliziten Liftingoperator, um undefinierte Ergebnisse als false zu interpretieren. Der mit ↑ bezeichnete Liftingoperator ist wegen seiner Eigenschaft ↑undef==false nicht (vollst¨andig) implementierbar. Jedoch existiert eine bereits in Abschnitt 4.2.2, Band 1 diskutierte Umsetzung dieses Operators in Java, die Exceptions abf¨angt und so nur nichtterminierende Berechnungen nicht erkennen kann. Der Liftingoperator ist implizit bei allen booleschen Operationen der OCL zu finden, so dass die Umsetzung der booleschen Operationen &&, ||, !, implies und <=> jeweils der Verwendung des Liftings bedarf. OCL-Logik: Logikoperatoren Erkl¨arung
Die zweiwertige Logik wird mittels Lifting des undefinierten Pseudowerts undef auf false umgesetzt. (Fortsetzung auf n¨achster Seite)
116
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.19.: OCL-Logik: Logikoperatoren)
Negation
... !a ... ⇓ boolean res; try { res = a; } catch(Exception e) { res = false; } ... !res ...
Java
• Die Einbettung der Auswertung des booleschen Ausdrucks a in eine catch-Anweisung macht eine Zerschlagung des Kontexts von a notwendig. Der Teilausdruck a wird vorab berechnet und in der Ergebnisvariable res zwischengespeichert. Die Umordnung der Berechnung ist erlaubt, da Seiteneffekte in OCL-Ausdrucken ¨ nicht auftreten. • Eine Nichtterminierung der Auswertung von a wird nicht erkannt. ¨ Aquivalenz
... a <=> b ... ⇓ boolean resa, resb; try { resa = a; } catch(Exception e) { resa = false; } try { resb = b; } catch(Exception e) { resb = false; } ... (resa==resb) ...
Java
Weitere werden in analoger Form umgesetzt. Implikation a implies Operatoren b wird auf !a || b, Konjunktion und Disjunktion werden auf die jeweiligen Java-Operatoren abgebildet. Tabelle 4.19.: OCL-Logik: Logikoperatoren Zur Umsetzung der OCL-Kontrollstrukturen konnen ¨ die entsprechenden Java-Kontrollanweisungen eingesetzt werden. Das let-Konstrukt fuhrt ¨ lokale Variablen ein und f¨angt bei der Berechnung der Variablenwerte auftretende Exceptions ab.
4.3 Codegenerierung aus OCL
117
4.3.3 OCL-Typen Die OCL/P-Grunddatentypen stimmen mit denen aus Java uberein, ¨ so dass eine Umsetzung nicht erforderlich ist. Auch der Vergleich der Grunddatentypen == sowie die arithmetischen Operationen werden unver¨andert uber¨ nommen. Die Container Set(X), List(X) und Collection(X), die die OCL anbietet, stellen eine Teilmenge der Container aus Java dar, wobei eine explizite Typisierung der Argumente im Sinne der Java-Generics vorgenommen wurde. Deshalb konnen ¨ die OCL-Container nahezu unver¨andert nach Java ubernommen ¨ werden. Die explizite Aufz¨ahlung wird im Wesentlichen auf einen Konstruktor und elementweises Hinzufugen ¨ abgebildet. Dabei wird vom Generator festgelegt, welche konkrete Mengen- oder Listenimplementierung verwendet wird. Die Verwendung von Containern uber ¨ Grunddatentypen bedarf einer besonderen Behandlung. Java ist im Gegensatz zur OCL nicht in der Lage, zum Beispiel int-Werte direkt in Containerstrukturen abzulegen. Stattdessen ist die objektifizierte Form Integer“ zu verwenden. Der Typ Set(int) wird ” also nach Set(Integer) uberf ¨ uhrt ¨ und alle Anwendungsstellen werden entsprechend transformiert. Da fur ¨ Container nicht die Objektidentit¨at, sondern der inhaltliche Vergleich von Interesse ist, wird der Vergleich == in eine eigens generierte Methode ubersetzt, ¨ die Gleichheit auf Containern wie in Abschnitt 4.3.3, Band 1 beschrieben interpretiert. Die Umsetzung der in der OCL/P eingefuhrten ¨ Komprehensionskonstrukte wird in der folgenden Tabelle 4.20 erkl¨art. OCL-Komprehension: Umformung der Komprehension Erkl¨arung Aufz¨ahlung
Die fur ¨ Mengen und Listen verwendeten Komprehensionsformen werden in Java durch zus¨atzliche Infrastruktur erg¨anzt. Set{ a..b }
OCL
⇓
Set(Integer) res = ... for(int i = a; i <= b; i++) res.add(new Integer(i)); } // Weiterverwendung von res
Java/P
(Fortsetzung auf n¨achster Seite)
118
4 Transformationen fur ¨ die Codegenerierung (Fortsetzung von Tabelle 4.20.: OCL-Komprehension: Umformung der Komprehension)
• Die Aufz¨ahlung wird durch eine Schleife realisiert, da Java keine Kompaktform anbietet. • Die Variable res erlaubt die Weiterverwendung fur ¨ den Aufbau einer Menge aus mehreren Aufz¨ahlungen, zum Beispiel der Form Set{a,b..c,d..e}. • res ist zum Beispiel mit new HashSet() zu besetzen. Komprehension mit Generator
Set{ expr | var in expr2 }
OCL
⇓
Java/P
Set(Class) res = new HashSet(Class); for(Iterator(Class2) it = expr2’.iterator(); it.hasNext(); ) { Class2 var = it.next(); res.add(expr’); } // Weiterverwendung von res
• Die Berechnung des OCL-Ausdrucks wird in mehrere Anweisungen mit Zwischenergebnis res aufgebrochen. Die Umordnung der Berechnung ist erlaubt, da OCL und auch seine Umsetzung seiteneffektfrei ist. • Die Ausdrucke ¨ expr und expr2 werden ebenfalls transformiert. • expr hat den Typ Class. • expr2 hat den Typ Set(Class2). Komprehension mit Filter
Set{ expr | var in expr2, boolexpr }
OCL
⇓
Java/P
Set(Class) res = new HashSet(Class); for(Iterator(Class2) it = expr2’.iterator(); it.hasNext(); ) { Class2 var = it.next(); if(boolexpr) res.add(expr’); } // Weiterverwendung von res (Fortsetzung auf n¨achster Seite)
4.3 Codegenerierung aus OCL
119
(Fortsetzung von Tabelle 4.20.: OCL-Komprehension: Umformung der Komprehension)
• Wie in Abschnitt 4.3.2, Band 1 beschrieben, entfernt ein Filter boolexpr Elemente aus der Menge. • Der Filter gewinnt seine M¨achtigkeit in Kombination mit dem bereits formulierten Generator. Deshalb wird die Transformation mit angefugtem ¨ Generator dargestellt.
Lokale Variablen
Set{ expr | var in expr2, Type x = expr3 }
OCL
⇓
Java/P
Set(Class) res = new HashSet(Class); for(Iterator(Class2) it = expr2’.iterator(); it.hasNext(); ) { Class2 var = it.next(); Type x = expr3; res.add(expr’); } // Weiterverwendung von res
• Eine lokale Variablendefinition wirkt wie ein letKonstrukt. Kombination
von Generatoren, lokalen Variablendefinitionen und Filtern innerhalb von Komprehensionen ist beliebig mo¨ glich. Bei mehreren Generatoren steigt jedoch die Komplexit¨at schnell an.
Tabelle 4.20.: OCL-Komprehension: Umformung der Komprehension Die Komprehension ist grunds¨atzlich verwandt mit SQL-Statements, ¨ wenn auch ausdrucksst¨arker. Eine Ubersetzung von OCL in eine Datenbankanfrage kann zumindest teilweise die Effizienz der Datenbankanfragen nutzen. 4.3.4 Typen als Extension Die OCL erlaubt die Verwendung von Typausdrucken ¨ wie Auction als Extension, die damit gleichzeitig die Menge der aktuell existierenden Objekte dieses Typs beschreibt. So kann zum Beispiel formuliert werden: forall a in Auction: a.person.size < 500
OCL
¨ Um eine Uberpr ufung ¨ dieser Bedingung zur Laufzeit zu erlauben, ist der Zugriff auf alle zu einem Zeitpunkt existenten Objekte des Typs Auction erforderlich. Deshalb ist eine entsprechende Infrastruktur zur Verwaltung der
120
4 Transformationen fur ¨ die Codegenerierung
Extensionen der Objekte zur Verfugung ¨ zu stellen. Diese kann auf Basis von WeakHashMaps realisiert sein, die mit der Garbage Collection zusammenarbeiten. Eine boolesche Variable OCL.oclmode beschreibt, ob das System gerade eine OCL-Bedingung auswertet und daher eventuell neu angelegte Objekte nicht zur Extension hinzuzurechnen sind13 . In der Verwaltung sind zus¨atzliche Maßnahmen zu treffen, um die in den nachfolgend besprochenen Methodenspezifikationen moglichen ¨ Konstrukte Class@pre und new(.) zur Charakterisierung neuer Objekte zu evaluieren. Da Methodenaufrufe geschachtelt sein konnen, ¨ wird der Aufrufkeller bei der Speicherung der Instanzmengen nachgebildet, um so jeder Nachbedingung den Zugriff auf den jeweiligen Zustand zur Vorbedingung zu ermoglichen. ¨ Wesentlich ist hier die effiziente Verwaltung und Optimierung in Abh¨angigkeit tats¨achlich benotigter ¨ Extensionen. 4.3.5 Navigation und Flattening Der Flatten-Operator flatten wird fur ¨ die verschiedenen Varianten der Collections in der in 4.3.6, Band 1 beschriebenen Form realisiert und bei dem Flachdrucken ¨ von Navigationsergebnissen angewandt. Die Navigation der OCL ist mengenwertig. Da Java eine Navigation ausgehend von mengen- und listenwertigen Strukturen nicht kennt, sind diese entsprechend zu transformieren. Tabelle 4.21 zeigt eine effiziente Umsetzung der Navigation. Navigation: Mengen- und listenwertige Navigation Erkl¨arung einfache Navigation
Navigation ausgehend von Containerstrukturen ist in Java durch Iteratoren zu realisieren. a.role ⇓ a.role • Ausdruck a beschreibt ein einzelnes Objekt. Die Navigation entlang der gewunschten ¨ Assoziation ist durch role moglich. ¨ • Eine weitere Umsetzung durch Regeln fur ¨ Assoziationen ist zu beachten. Zum Beispiel kann eine Kapselung von a durch Zugriffsmethoden vorgenommen werden. • Die Navigation kann ein mengenwertiges Ergebnis haben oder qualifiziert sein. Dann ist gegebenenfalls eine Anpassung der Container-Form notwendig. (Fortsetzung auf n¨achster Seite)
13
Siehe Abschnitt 4.4.1, Band 1 uber ¨ die Objekterzeugung in Queries.
4.3 Codegenerierung aus OCL
121
(Fortsetzung von Tabelle 4.21.: Navigation: Mengen- und listenwertige Navigation)
Navigation aus einer Menge
sa.role ⇓ Java/P
Set(Class) res = new HashSet(Class); for(Iterator(Class2) it = sa’.iterator(); it.hasNext(); ) { Class2 a = it.next(); if(a.role != null) res.add(a.role); } // Weiterverwendung von res
• Ausdruck sa beschreibt eine Menge von Objekten. Die Navigation entlang der gewunschten ¨ Assoziation ist durch role moglich. ¨ Die Kardinalit¨at ist 1“ oder 0..1“. ” ” • Siehe auch Aussagen zur einfachen Navigation. • Class ist der Typ der durch die Assoziation erreichten Klasse. • Class2 ist der Typ der Ausgangsklasse. Mengenwertige Navigation aus einer Menge
sa.role ⇓ Java/P
Set(Class) res = new HashSet(Class); for(Iterator(Class2) it = sa’.iterator(); it.hasNext(); ) { Class2 a = it.next(); res.addAll(a.role); } // Weiterverwendung von res
• Im Unterschied zum obigen Fall ist die Kardinalit¨at nun *“, also a.role mengenwertig. ” • Ansonsten gelten Aussagen der vorherigen Transformation. Tabelle 4.21.: Navigation: Mengen- und listenwertige Navigation
4.3.6 Quantoren und Spezialoperatoren Unendliche Quantoren, also Quantoren uber ¨ den Grunddatentypen wie int und uber ¨ Containerstrukturen wie List(Auction) werden nicht uber¨ setzt. Alle objektwertigen Quantoren jedoch sind endlich und damit in Java ubersetzbar. ¨ Eine entsprechende Realisierung in Form eines Iterators ist
122
4 Transformationen fur ¨ die Codegenerierung
bereits im Eingangsbeispiel dieses Abschnitts gezeigt worden. Operatoren wie any oder iterate konnen ¨ in a¨ hnlicher Form umgesetzt werden. Auch die Umsetzung von Referenzen auf Objektdiagramme in der OCL sind einfach. Ein Objektdiagramm wirkt als Pr¨adikat, kann daher selbst in eine boolesche Methode transformiert und der Aufruf in den generierten Code eingesetzt werden. Das von der OCL/P zur Verfugung ¨ gestellte typeifKonstrukt zur typsicheren Konversion von Objekten wird durch eine Abfrage mit instanceof und eine Typkonversion realisiert. 4.3.7 Methodenspezifikation Die folgende Methodenspezifikation beschreibt fur ¨ einen eingeschr¨ankten Bereich, wie nach Eingang eines neuen Gebots eine neue Zeit fur ¨ das Auktionsende festgelegt wird. Dabei sind einige zeitliche Abh¨angigkeiten zu sichern: context Time ConstantTimingPolicy.newCurrentClosingTime( OCL Auction a, Bid b) let long old = a.closingTime.timeSec; long now = b.time.timeSec pre: status==RUNNING && isInExtension && now <= old post: result.timeSec == min(now + extensionTimeSecs,a.finishTime.timeSec)
Die Vorbedingung wirkt wie ein zu Beginn der Methode formuliertes ocl-Statement und die mit dem in der OCL zur Verfugung ¨ stehenden letKonstrukt definierten Variablen werden als lokale Variable verstanden. Die ¨ Ubersetzung dieser Spezifikation in erweitertes Java erfolgt in Abbildung 4.22 unter der Annahme, dass der eigentliche Methodenrumpf gegeben ist. Diese Methode aus Abbildung 4.22 kann, wie in in Anhang B, Band 1 beschrieben, in normales Java ubersetzt ¨ werden. Gem¨aß der in Abschnitt 4.4.3, Band 1 beschriebenen Bedeutung von Methodenspezifikationen muss die Nachbedingung nur erfullt ¨ sein, wenn die Vorbedingung gilt. Deshalb wird die Vorbedingung evaluiert und ihr Ergebnis in der Variable precond gespeichert, um in der Nachbedingung uberpr ¨ uft ¨ zu werden. Mit dieser Form der Transformation ist es moglich, ¨ mehrere Methodenspezifikationen, die unabh¨angig voneinander definiert oder geerbt wurden, gleichzeitig zu testen. Eine wie in Abschnitt 4.4.3, Band 1 beschriebene Integration ist daher fur ¨ diesen Zweck nicht notwendig. Je nach Generatoreinstellung werden ocl- und let-Anweisungen nicht ubersetzt, ¨ im Fehlerfall eine Exception erzeugt, eine Warnung im Protokoll vermerkt und nach Bedarf ein Auszug des aktuellen Objekts ausgegeben. Das let-Konstrukt zur Definition von Hilfsvariablen in Java, die nicht in den Produktionscode gehoren, ¨ wird wie erwartet durch lokale Variablendefinitionen realisiert. Es entsteht der in Abbildung 4.23 dargestellte Code,
4.3 Codegenerierung aus OCL
123
Java/P
class ConstantTimingPolicy { public Time newCurrentClosingTime(Auction a, Bid b) { let long old = a.closingTime.timeSec; let long now = b.time.timeSec; let boolean precond = (status==RUNNING && isInExtension && now <= old); // Rumpf der Java Funktion (ohne return-Anweisung) Time result = // Ausdruck der Return-Anweisung ocl !precond || (result.timeSec == OCL.min(now + extensionTimeSecs, a.finishTime.timeSec)); return result; }
}
Abbildung 4.22. OCL transformiert in erweitertes Java class ConstantTimingPolicy { Java public Time newCurrentClosingTime(Auction a, Bid b) { long old = a.closingTime.timeSec; long now = b.time.timeSec; boolean precond = (status==RUNNING && isInExtension && now <= old); // Rumpf der Java Funktion (ohne return-Anweisung) Time result = // Ausdruck der Return-Anweisung if(precond && !(result.timeSec == OCL.min(now + extensionTimeSecs, a.finishTime.timeSec)) alert(...); return result; } } Abbildung 4.23. OCL transformiert in Java
bei dem allerdings vereinfachend das Abfangen von Exceptions weggelassen wurde. Dieser relativ einfachen Umsetzung steht eine substantielle Komplexit¨at der Umsetzung von Methodenspezifikationen gegenuber, ¨ wenn die Nachbedingung auf die Ursprungswerte der Variablen zu Beginn des Methoden¨ aufrufs zugreift. Fur ¨ die Ubersetzung solcher Zugriffe ist eine Infrastruktur notwendig, die effizient feststellt, welche fruheren ¨ Werte gegebenenfalls benotigt ¨ werden und diese geeignet zwischenspeichert. Dabei konnen ¨ wie nachfolgend beschrieben deutliche Aufw¨ande entstehen. Dazu wird das folgende aus Abschnitt 4.4.3, Band 1 adaptierte Beispiel betrachtet, das den Ef-
124
4 Transformationen fur ¨ die Codegenerierung
fekt eines Unternehmenswechsels einer Person unter der Annahme, dass das Unternehmen bereits erfasst ist, beschreibt: context Person.changeCompany(String name) OCL pre: exists Company co: co.name == name == name && post: company.name company.employees == company.employees@pre +1 &&
[email protected] ==
[email protected]@pre -1
In der Nachbedingung wird unter anderem mit company@pre auf das vorherige Unternehmen der wechselnden Person und mit company.employees@pre auf die Anzahl der vorherigen Mitarbeiter des neuen Unternehmens zugegriffen. Der Ausdruck company@pre ist fur ¨ den Codegenerator leicht zu identifizieren und sein Inhalt zu speichern. Dies wirkt, als ob dabei eine interne Hilfsvariable angelegt wird. Gleiches gilt fur ¨ den Ausdruck
[email protected]@pre: context Person.changeCompany(String name) OCL let Company companyPre = company; int employeesPre = company.employees pre: exists Company co: co.name == name post: company.name == name && company.employees == company.employees@pre +1 && companyPre.employees == employeesPre -1
Schwierigkeiten bereitet jedoch company.employees@pre, weil hier auf den fruheren ¨ Zustand des neuen Unternehmens zugegriffen wird. Fur ¨ einen Codegenerator ist es faktisch unmoglich ¨ Code zu generieren, der zu Beginn einer Methodenausfuhrung ¨ err¨at, welches das neue company-Objekt sein wird und dann dessen alten Zustand speichert. Dieses Problem wird noch deutlicher am Ausdruck ad.auction@pre[id] sichtbar, der in der Klasse AllData zur Selektion einer Auktion mit dem Identifikator id verwendet werden kann. Da id erst nach Methodenausfuhrung ¨ evaluiert wird, ist unklar, welche Auktion des Ursprungszustands von ad.auction selektiert werden wird. Deshalb gibt es drei Strategien damit umzugehen: 1. Es werden die alten Zust¨ande aller company-Objekte gespeichert. Dies erfordert allerdings bereits fur ¨ kleine Testdatenstrukturen einen deutlichen Effizienzverlust und ist im Testeinsatz des Produktionssystems mit großen Datenbest¨anden nicht umzusetzen. 2. Fur ¨ Tests wird eine Infrastruktur generiert, die eine interne Protokollierung aller Attribut¨anderungen einschließlich der ursprunglichen ¨ Werte durchfuhrt. ¨ Dies erfordert ebenfalls eine geeignete Infrastruktur, hat aber ¨ unter Umst¨anden den Vorteil, dass die Anderungshistorie eines gescheiterten Tests analysiert werden kann. 3. Der Codegenerator warnt vor der Ineffizienz der Spezifikation oder weist sie sogar zuruck ¨ mit dem Hinweis, eine effizientere Formulierung
4.4 Ausfuhrung ¨ von Statecharts
125
zu finden. Der Entwickler hat meist eine gute Vorstellung, welche Objektzust¨ande tats¨achlich zu speichern sind und legt diese explizit in letAnweisungen ab. Wird eine Effizienzsteigerung durch manuelle Umsetzung gewunscht, ¨ so kann im Beispiel etwa aus der Vorbedingung erraten“ werden, welches ” company-Objekt relevant ist und damit die Vorbedingung vereinfacht werden kann: context Person.changeCompany(String name) OCL let Company newCo = AllData.ad.company[name] pre: newCo != null == name && post: company.name company.employees == newCo.employees@pre +1 &&
[email protected] ==
[email protected]@pre -1
Um sicherzugehen, dass das neue Unternehmen tats¨achlich mit dem in der let-Anweisung definierten Unternehmen ubereinstimmt, ¨ kann zus¨atzlich company==newCo in die Nachbedingung aufgenommen werden. Diese Form benotigt ¨ nun nur minimalen zus¨atzlichen Speicherplatz, wird allerdings bereits so detailliert und implementierungsnah, dass dann eine direkte Implementierung unter Umst¨anden vorzuziehen ist. 4.3.8 Vererbung von Methodenspezifikationen Ob eine Methodenspezifikation fur ¨ die Implementierungen der Unterklassen gilt, wird gem¨aß Abschnitt 4.4.3, Band 1 durch den Stereotyp notinherited bestimmt. Im Allgemeinen sind deshalb die geerbten Vor- oder Nachbedingungen zus¨atzlich zu testen. Fur ¨ eine redundanzfreie Implementierung werden die zu prufenden ¨ Bedingungen aber nicht direkt als Zusicherungen formuliert, sondern in eigenst¨andige Methoden ausgelagert, die in Unterklassen in geeigneter Form zur Verfugung ¨ stehen. Dieses Verfahren wird Percolation“ genannt [Bin99] und fuhrt ¨ zu der in Abbildung 4.24 dar” gestellten Implementierung. Ein Vorteil dieses Verfahrens ist, dass ein Testtreiber zus¨atzlich prufen ¨ kann, ob die Vorbedingung der Methodenspezifikation auch erfullt ¨ ist, indem er diese vorab auf den gegebenen Testdaten pruft. ¨ Dadurch wird verhindert, dass ein Test als Erfolg gewertet wird, weil die Testdaten f¨alschlicherweise so aufgebaut wurden, dass sie die Vorbedingung nicht erfullen. ¨ Das in Abbildung 4.24 dargestellte Verfahren kann analog fur ¨ Invarianten verwendet werden, so dass auch diese in den Unterklassen fur ¨ Tests zur Verfugung ¨ stehen.
4.4 Ausfuhrung ¨ von Statecharts Wie David Harel, dem die Erfindung der Statecharts zugeschrieben wird, gerne bemerkt, Buildings are there to be, but Software is there to do“. Da ”
126
4 Transformationen fur ¨ die Codegenerierung
class ConstantTimingPolicy { Java // Vorbedingung ohne/mit vorbelegten Parametern boolean preNewCurrentClosingTime(Auction a, Bid b) { long old = a.closingTime.timeSec; long now = b.time.timeSec; return preNewCurrentClosingTime(a,b,old,now); } boolean preNewCurrentClosingTime(Auction a, Bid b, long old, long now){ return status==RUNNING && isInExtension && now <= old; } // Nachbedingung boolean postNewCurrentClosingTime(Time result, Auction a, Bid b, long old, long now) { return result.timeSec == OCL.min(now + extensionTimeSecs,a.finishTime.timeSec); }
}
// Eigentliche Funktion public Time newCurrentClosingTime(Auction a, Bid b) { long old = a.closingTime.timeSec; long now = b.time.timeSec; boolean precond = preNewCurrentClosingTime(a,b,old,now); // Rumpf der Java Funktion (ohne return-Anweisung) Time result = // Ausdruck der Return-Anweisung if(precond && !postNewCurrentClosingTime(result,a,b,old,now)) alert(...); return result; }
Abbildung 4.24. OCL-Bedingungen transformiert in Java-Pr¨adikate
Statecharts in naturlicher ¨ Weise zur vollst¨andigen Beschreibung von Verhalten genutzt werden konnen, ¨ ist der Aspekt der Codegenerierung, also der Ausfuhrung ¨ von Statecharts, von besonderem Interesse. Deshalb wird in diesem Abschnitt auf Realisierungsstrategien fur ¨ Statecharts eingegangen, ohne ¨ jedoch vollst¨andige Ubersetzungsalgorithmen zu beschreiben. Das Ziel dieses Abschnitts ist es auch, dem Leser durch die Beschreibung der Beziehung zwischen syntaktischen Konzepten der Statecharts und Java-Codeelementen zum einen die Moglichkeit ¨ zu geben, manuell ein Statechart in Code zu ubersetzen. ¨ Genauso wichtig ist die dadurch entstehende Hilfestellung zum Verst¨andnis von Statecharts und ihrer Verwendung bei der Codegenerie¨ rung. Die Ubersetzung der Statecharts in Java dient sowohl als Semantikde-
4.4 Ausfuhrung ¨ von Statecharts
127
finition, als auch zur Verbesserung des intuitiven Zugangs den Statecharts. Die nachfolgend diskutierten Realisierungsstrategien zeigen alternative Umsetzungsmoglichkeiten ¨ von Statecharts in Java, die jedoch semantisch a¨ quivalent sind. Die Auswahl der fur ¨ ein Projekt geeigneten Umsetzungsstrategie h¨angt daher von der gewunschten ¨ Flexibilit¨at und Effizienzuberlegungen ¨ ab. Die Codegenerierung aus Statecharts ist noch keineswegs so verbreitet wie die auf Basis von Klassendiagrammen, jedoch nimmt die Verwendung von Statecharts fur ¨ die Implementierung stetig zu. Die eingebetteten Systeme sind hier der Wegbereiter: ... automatically generated code can and ” is being used today in a variety of hard real-time and embedded systems.“ [Dou99, S. 156]. Die nachfolgend diskutierten Varianten zur Umsetzung sind keineswegs vollst¨andig, sondern stellen nur einen Ausschnitt der Umsetzungsmoglich¨ keiten dar. Die bereits eingefuhrten ¨ Stereotypen statedefining, completion:ignore und weitere konnen ¨ zur Auswahl zwischen diesen Varianten verwendet werden. Nicht alles l¨asst sich jedoch bereits mit Stereotypen beschreiben, weshalb die Verwendung von Templates und Skripten fur ¨ die Codegenerierung von Statecharts sehr hilfreich ist. 4.4.1 Methoden-Statecharts Die Zust¨ande eines Statecharts konnen ¨ auf verschiedene Arten interpretiert werden. In einem Methoden-Statechart, wie dem in Abbildung 2.38, repr¨asentieren die Diagrammzust¨ande Zwischenzust¨ande innerhalb des Ablaufs einer Methode. Die Diagrammzust¨ande werden deshalb durch den Programmz¨ahler unterschieden. Diese Diagrammzust¨ande konnen ¨ noch einmal in zwei Klassen unterschieden werden. Zum einen gibt es Diagrammzust¨ande, die Stellen zwischen Anweisungen entsprechen und deren Fortfuhrung ¨ durch spontane ε-Transitionen erfolgt. Zum anderen gibt es Zust¨ande, in denen auf das Ergebnis eines Methodenaufrufs gewartet wird, der in einer ankommenden Transition gestartet wurde. W¨ahrend die Quellzust¨ande spontaner Transitionen vor allem dazu genutzt werden, den Kontrollfluss, wie zum Beispiel Verzweigungen, zu modellieren, stellen Quellzust¨ande von Transitionen, die mit dem Stimulus return markiert sind, echte Unterbrechungen der Ausfuhrungen ¨ einer Methode dar. Die Umsetzung eines Methoden-Statecharts erfolgt kanonisch durch die Generierung der beschriebenen Methode und ihres Rumpfs. Von besonderem Interesse ist die Behandlung von ε-Schleifen innerhalb eines Statecharts. Die Existenz einer ε-Schleife bedeutet, dass der Kontrollfluss innerhalb eines Methoden-Statecharts eine Schleife besitzt. Das Statechart selbst ist daher nicht mehr vollst¨andig in der Lage, das Verhalten der Methode zu beschreiben. Die Initialisierung, der Rumpf und die Abbruchbedingung der Schleife sind jeweils durch Aktionen zu beschreiben. Dabei ist nicht gesichert, dass die Schleife definitiv abbricht. Dennoch kann davon ausgegangen werden, dass modelliertes Verhalten grunds¨atzlich terminiert.
128
4 Transformationen fur ¨ die Codegenerierung
Diese Annahme ist aus pragmatischen Grunden ¨ sinnvoll, weil in der vorgeschlagenen Entwicklungsmethode Schleifen grunds¨atzlich mit Tests so uber¨ pruft ¨ werden, dass eine nichtterminierende Schleife entdeckt werden wurde. ¨ 4.4.2 Umsetzung der Zust¨ande Die Statecharts, die nicht zur Darstellung eines Methodenablaufs dienen, beinhalten keine Kontrollzust¨ande und daher auch keine spontanen Transitionen. Die Zust¨ande eines solchen Statechart entsprechen daher Datenzust¨anden des Objekts. Das heißt, der Diagrammzustand des Statecharts muss aus dem Datenzustand des Objekts rekonstruierbar sein. Dies wurde bereits bei der Transformation in vereinfachte Statecharts in Abschnitt 6.6.3, Band 1 diskutiert und ein Verfahren zur Darstellung von Diagrammzust¨anden gezeigt. Hier werden weitere Verfahren besprochen. Die Statecharts, die nicht zur Darstellung eines Methodenablaufs dienen, beinhalten keine Kontrollzust¨ande und daher auch keine spontanen Transitionen. Die Zust¨ande eines solchen Statechart entsprechen daher Datenzust¨anden des Objekts. Das heißt, der Diagrammzustand des Statecharts muss aus dem Datenzustand des Objekts rekonstruierbar sein. Dies wurde bereits bei der Transformation in vereinfachte Statecharts in Abschnitt 6.6.3, Band 1 diskutiert und ein Verfahren zur Darstellung von Diagrammzust¨anden gezeigt. Hier werden weitere Verfahren besprochen. Leichtgewicht: Nutzung der Zustandsinvarianten Als besonders einfach anzusehen ist die Strategie, die paarweise disjunkten Zustandsinvarianten des vereinfachten Statecharts zu nehmen, um aus einem Objekt den Diagrammzustand jeweils zu berechnen. Abbildung 4.25 zeigt schematisch eine solche Umsetzung. Bei dieser Transformation wird der linken Transition der Vorrang vor der mittleren Transition gegeben, da ¨ ihre Vorbedingung zuerst evaluiert wird. Uberlappen die beiden Vorbedingungen, also vorb1&&vorb2 ist nicht a¨ quivalent zu false, dann wurde damit eine Entwurfsentscheidung getroffen, die eine spezielle Implementierung aus der Menge der moglichen ¨ Implementierungen ausw¨ahlt. Der Vorteil dieser Umsetzung ist, dass kein zus¨atzliches Attribut notwendig ist, um den Diagrammzustand im Objekt zu speichern. Umgekehrt mussen ¨ im ungunstigsten ¨ Fall jedoch die Zustandsinvarianten aller Zust¨ande evaluiert werden, wodurch ein deutlicher Effizienzverlust auftreten kann. Deshalb ist diese Umsetzung nur fur ¨ effizient evaluierbare Zustandsinvarianten sinnvoll. Meist bestehen eine Reihe von Optimierungsmoglichkeiten, ¨ weil Zustandsinvarianten verwandter“ Zust¨ande oft gemeinsame Teilbe” ¨ dingungen besitzen, deren Evaluierung nur einmal notwendig ist. Ahnliches gilt fur ¨ die Evaluierung der Vorbedingungen von Transitionen mit demselben Quellzustand. Gilt zum Beispiel vorb1<=>!vorb2, so l¨asst sich die innere if-Abfrage in Abbildung 4.25 deutlich vereinfachen.
4.4 Ausfuhrung ¨ von Statecharts
129
¨ Abbildung 4.25. Ubersetzung unter Ausnutzung von Zustandsinvarianten
Optimierungen bei der Codegenerierung, wie die oben besprochenen, sind im Allgemeinen nicht automatisiert erkennbar. Jedoch besteht die berechtigte Hoffnung, dass die Werkzeuge zur Generierung von Code aus Modellen in der n¨achsten Zukunft a¨ hnliche Optimierungsalgorithmen einbauen werden, wie dies fur ¨ Compiler textueller Programmiersprachen heute bereits der Fall ist. Bis dahin ist damit zu rechnen, dass die gesteigerte Entwicklereffektivit¨at bei der Verwendung abstrakter Modelle zu gewissen Effizienznachteilen beim realisierten Code fuhrt. ¨ Unter Umst¨anden ist dann, wie etwa bei den Herstellern eingebetteter Systeme, durchaus zu beobachten, dass nach Fertigstellung der Modelle zu Simulations- und Validierungszwecken ein zus¨atzlicher manueller Schritt zur Optimierung des Ergebnisses sinnvoll ist. Dafur ¨ lassen sich Refactoring-Techniken auf Ebene der Zielsprache ebenso verwenden, wie die Verwendung zus¨atzlicher Steuerungsmechanismen bei der Generierung des Codes. Beispielsweise konnen ¨ durch die Vergabe von Priorit¨aten und die Zusicherung der Disjunktheit von Vorbedingungen dem Codegenerator Optimierungen vorgeschlagen werden. Zust¨ande als Pr¨adikate Eine Variation der gezeigten Umsetzung ist in 4.26 zu sehen, in der die Auswertung einzelner Zustandsinvarianten und Aktionen in eigene Methoden ausgelagert wurde. Die Hilfsmethoden zur Umsetzung von Aktionen lassen sich dann gegebenenfalls wiederverwenden, wenn verschiedene Transitionen dieselbe Aktion besitzen. Die Auslagerung der Evaluierung von Zustandsinvarianten in eigene Pr¨adikate, die den Zustandsnamen als Pr¨adikatnamen tragen, hat daruber ¨ hinaus den Vorteil, dass auf diese Weise der Diagrammzustand des Statecharts an beliebigen Stellen im Code festgestellt
130
4 Transformationen fur ¨ die Codegenerierung
werden kann. Dies ist besonders fur ¨ Methoden hilfreich, deren Verhalten nicht im Statechart spezifiziert ist, aber dennoch von dem im Statechart modellierten Zustandskonzept abh¨angt.
Abbildung 4.26. Pr¨adikate evaluieren Zustandsinvarianten
Aufz¨ahlungsattribut als Speicher fur ¨ den Zustand Ist die Evaluierung der Zustandsinvarianten zu ineffizient, so eignet sich der heute meistens genutzte Standardweg, den Diagrammzustand in Form eines Attributs, das eine Aufz¨ahlung beinhaltet, explizit im Zustandsraum des Objekts abzulegen. Zur Feststellung des Diagrammzustands reicht es nun, das status-Attribut zu prufen. ¨ Abbildung 4.27 zeigt die verwendbare Codegenerierung. Vollst¨andiges Statechart Die Verwendung eines Attributs zur Speicherung des Diagrammzustands fuhrt ¨ zu einer besseren Laufzeiteffizienz, verhindert jedoch nicht, dass weiterhin die Vorbedingungen von alternativen Transitionen gepruft ¨ werden mussen. ¨ Allerdings kann die jeweils letzte Vorbedingung sowie auch die Zustandsinvariante in einer assert-Anweisung eingesetzt werden, um statt im konstruktiven Anteil der Implementierung zu Testzwecken verwendet zu werden.14 Abbildung 4.28 zeigt eine entsprechend modifizierte Umsetzung. 14
Um aussagekr¨aftige Testresultate zu erhalten, empfiehlt es sich, anstatt der von Java 1.4 zu Verfugung ¨ gestellten assert-Anweisung das in Abschnitt 5.2.3 diskutierte Test-Framework zu verwenden.
4.4 Ausfuhrung ¨ von Statecharts
131
Abbildung 4.27. Diagrammzustand wird in Attribut gespeichert
Abbildung 4.28. Verwendung von Zustandsinvarianten als Zusicherungen
Wurde das Statechart nicht, wie in Abschnitt 6.6.3, Band 1 beschrieben, explizit vervollst¨andigt, so kann in Abh¨angigkeit der gew¨ahlten Semantik diese Vervollst¨andigung auch w¨ahrend der Codegenerierung durchgefuhrt ¨ werden. Dazu dient zum Beispiel die default-Anweisung, die alle nicht durch explizite Transitionen abgedeckten Situationen abfangen kann. Auch kann zum Beispiel ein weiterer Wert in der Aufz¨ahlung der Zust¨ande in der
132
4 Transformationen fur ¨ die Codegenerierung
Form ERROR==-1 eingefuhrt ¨ werden, der in solchen Situationen angenommen wird. Fur ¨ das Abfangen von Exceptions aufgrund des Stereotyps excepti on kann eine umfassende try-catch-Anweisung verwendet werden. Dabei mussen ¨ allerdings je nach gew¨ahlter Umsetzungsstrategie mehrere Anweisungen in verschiedenen Methoden oder case-Alternativen eingesetzt werden. Schwergewicht: State-Entwurfsmuster Eine weitere Moglichkeit ¨ zur Umsetzung des Zustandskonzepts ist die Verwendung des State-Entwurfsmusters, zum Beispiel beschrieben in [GHJV94]. Dieses Entwurfsmuster wird als Schwergewicht bezeichnet, da es zu einer Reihe von zus¨atzlichen Klassen fuhrt. ¨ Der Zustand des eigentlich modellierten Objekts wird ausgelagert in eine eigene Zustandsklasse. Diese besitzt mehrere Unterklassen, die je einem Zustand des eigentlich modellierten Objekts entsprechen. Der aktuelle Zustand des Objekts wird durch Referenz auf eines dieser Zustandsobjekte gespeichert. Das Verhalten wird nicht direkt in der modellierten Methode realisiert, sondern an dieses Zustandsobjekt delegiert. Dadurch wird das im Statechart auf den Transitionen verteilte Verhalten nicht innerhalb einer Methode zusammengefasst, sondern entsprechend der Zust¨ande gruppiert. Dies bietet zum Beispiel die Flexibilit¨at, das Verhalten an einem Quellzustand durch Unterklassenbildung zu modifizieren. Der notationelle und operative Aufwand fur ¨ das State-Entwurfsmuster ist allerdings immens. So muss ein Management der Zustandsobjekte realisiert werden, das entweder dynamisch solche Objekte erzeugt oder in einem Pool von Objekten speichert. Abbildung 4.29 zeigt daher nur einen kleinen Ausschnitt der Umsetzung in einem State-Entwurfsmuster. Die Codeteile invarianteA’ und aktion1’ sind dabei entsprechend anzupassen, da die darin enthaltenen Attribute und Methodenaufrufe auf das Ursprungsobjekt k statt self zugreifen mussen. ¨ Die Verwendung des State-Entwurfsmusters ist nur zu empfehlen, wenn die dadurch entstehende Flexibilit¨at den Zusatzaufwand bei der Generierung rechtfertigen kann. Da die Umsetzung eines Statecharts in Java-Code normalerweise automatisiert erfolgt und ein manueller Eingriff in den generierten Code im Allgemeinen nicht sinnvoll ist, sollte dementsprechend auch das State-Entwurfsmuster nur selten ad¨aquat sein. 4.4.3 Umsetzung der Transitionen Stimuli Wie Abbildung 2.37 zeigt, werden drei Arten von Stimuli unterschieden. Spontane Transitionen und Empfang von return-Ergebnissen treten nur in¨ nerhalb von Methoden-Statecharts auf. Die Ubertragung asynchroner Nachrichten und der Methodenaufruf werden im Statechart nicht unterschieden.
4.4 Ausfuhrung ¨ von Statecharts
133
Abbildung 4.29. Schwergewicht: State-Entwurfsmuster
Erst bei der Umsetzung in Code ist die Unterscheidung zwischen einem Methodenaufruf und der Verwendung von Nachrichtenobjekten zur asyn¨ chronen Ubertragung relevant. Handelt es sich um einen Methodenaufruf, so wird dieser, wie bereits im letzten Abschnitt beschrieben, durch eine entsprechende Methode implementiert. Wird eine Objektifizierung der Stimuli vorgenommen, so werden Events typischerweise in Form einer Klassenhierarchie mit einer abstrakten Oberklasse Event umgesetzt, deren Unterklassen jeweils einzelnen Nachrichtentypen entsprechen. Nachrichten werden durch Aufruf des entsprechen¨ den Konstruktors erzeugt und durch einen geeigneten Ubertragungsund Scheduling-Mechanismus beim Zielobjekt zur Ausfuhrung ¨ gebracht. Fur ¨ den Transport dieser Nachrichten stehen eine Reihe von Frameworks und Middleware-Komponenten, wie zum Beispiel Corba [OH98], zur Verfugung. ¨ Weitere Varianten sind die Serialisierung der Nachrichtenobjekte zum Bei¨ spiel mit XML [W3C00] oder die Ubertragung durch ein selbstdefiniertes, typischerweise effizienteres Protokoll. Aufgrund der hohen Auswahl an zur Verfugung ¨ stehenden Losungen ¨ soll dieser technische Aspekt der Kommunikation hier nicht weiter vertieft werden. Fur ¨ die Umsetzung des Statecharts ist letztendlich nur wesentlich, dass das Nachrichtenobjekt an das zu verarbeitende Objekt ubergeben ¨ wird, indem eine geeignete Methode aufgerufen wird. Abbildung 4.30 skizziert eine mogliche ¨ Realisierung auf Basis einer doppelten switch-Anweisung. Durch Definition von Methoden, die jeweils ein spezifisches Nachrichtenobjekt verarbeiten oder durch Auslagerung der Verarbeitung einer Methode auf abh¨angige Objekte konnen ¨ auch in
134
4 Transformationen fur ¨ die Codegenerierung
diesem Fall die bereits fruher ¨ diskutierten Varianten zur Codegenerierung angewandt werden.
Abbildung 4.30. Verwendung von Event-Objekten als Stimuli
Ein bereits fruher ¨ diskutierter Vorteil der Verwendung asynchron kommunizierter Nachrichten ist die Vermeidung von rekursiven Objektaufrufen. Die Verarbeitung eines Nachrichtenobjekts findet immer unter exklusivem Zugriff auf den Objektzustand statt. Eine Parallelverarbeitung mehrerer Nachrichten ist ausgeschlossen. Um das sicherzustellen, werden geeignete Synchronisationsmechanismen der Programmiersprache Java eingesetzt. In einem Statechart konnen ¨ auch die Verarbeitung von Nachrichten und Methodenaufrufe gemischt vorkommen. Fur ¨ das Statechart ist letztendlich unerheblich, ob der Stimulus durch die spezielle Methode receive zur Nachrichtenverarbeitung oder durch einen normalen Methodenaufruf bearbeitet wird. Zu beachten ist nur, dass innerhalb einer Aktion des Statecharts keine weiteren Methodenaufrufe an dasselbe Objekt stattfinden, die bezuglich ¨ des Statecharts eine zustandsver¨andernde Wirkung besitzen. Diese, als Rekursionsfreiheit bezeichnete Bedingung, wurde bereits in den Abschnitt 6.1, Band 1 ausfuhrlich ¨ diskutiert. Aktionen Die Aktionsbeschreibungen eines Statecharts bestehen aus zwei moglichen ¨ Komponenten. Die prozedural formulierten Aktionen konnen ¨ nach Anpassung von Attibutzugriffen, etc. gem¨aß Abschnitt 4.1 in den generierten Code ubernommen ¨ werden. Wurde ein zus¨atzliches Attribut zur Speicherung des
4.4 Ausfuhrung ¨ von Statecharts
135
Diagrammzustands eingefuhrt, ¨ so ist zum Ende der Aktion eine zus¨atzliche Zuweisung des neuen Zustands notwendig. Die Umsetzung in Abbildung 4.27 illustriert dies. Wurden bei den Aktionen zus¨atzlich oder ausschließlich OCL-Nachbedingungen verwendet, so kann daraus im Allgemeinen kein operationeller Code generiert werden. Der Einsatz eines solchen Statecharts zur Programmierung ist daher nicht moglich. ¨ Solche Nachbedingungen werden deshalb typischerweise nur zur abstrakten Spezifikation von Verhalten verwendet, das zum einem bei einer Implementierung manuell in ablauff¨ahigen Code umgesetzt oder zum anderen bei Tests eingesetzt wird. Die Nachbedingungen konnen ¨ deshalb nur mithilfe von ocl-Anweisungen in den Code umgesetzt werden. Existente Codegenerierungen fur ¨ Statecharts Naturlich ¨ ist die oben beschriebene Form nicht die erste Form der Umsetzung von Statecharts in Code. Mehrere Werkzeuge, wie zum Beispiel Statemate oder Rhapsody [HN96] sowie einige der neueren UML-basierten Werkzeuge und Ans¨atze wie [SZ01, BS01a, BLP01] ubersetzen ¨ ihre Version der Statecharts in produktiven Code. Dabei werden teilweise auch Konzepte wie parallele Zust¨ande, ein History-Mechanismus, Pseudozust¨ande oder echte Parallelit¨at innerhalb eines Zustands umgesetzt, die hier nicht eingefuhrt ¨ wurden. Dies liegt unter anderem an der bisher dominierenden Verwendung von Statecharts zur Modellierung verteilter und eingebetteter Systeme, die meist einen hoheren ¨ Kontrollanteil besitzen als die datenlastigen Gesch¨aftssysteme [Dou98, Dou99]. Statecharts wurden bereits in [Har87] eingefuhrt ¨ und in [vdB94] wurde die bis dahin entstandene Vielzahl von Semantiken verglichen sowie in [vdB01] die UML-Semantik der Statechart in den Vergleich einbezogen. Ein interessantes Merkmal dieser Semantiken ist, dass sie teilweise unterschiedliches Verhalten beschreiben und damit zu unterschiedlichen Implementierungen der Statecharts fuhren, ¨ teilweise aber auch nur verschiedene Mechanismen nutzen, um den Statecharts die essentiell gleiche Semantik zuzuweisen. Von besonderer Aufmerksamkeit ist die Behandlung der Priorisierung und Unterbrechbarkeit von Transitionen verschiedener Hierarchieebenen und die damit eng verbundene Run to Completion“-Problematik fur ¨ State” charts. W¨ahrend fur ¨ eingebettete Systeme die a¨ ußeren Transitionen (bezogen auf den Quellzustand) bevorzugt werden, wird zum Beispiel in [HG97] fur ¨ objektorientierte Statecharts diese Priorisierung umgekehrt und inneren Transitionen der Vorzug gegeben. Der hier verfolgte Ansatz, dies uber ¨ Stereotypen dem Modellierer selbst entscheiden zu lassen, fuhrt ¨ zu mehr Flexibilit¨at. Run to Completion“ spielt bei den UML/P-Statecharts keine Rolle, ” denn wegen dem zugrunde liegenden, auf Java basierendem Maschinenmodell wird davon ausgegangen, dass die Exceptions, die als Stimuli auftreten,
136
4 Transformationen fur ¨ die Codegenerierung
durch das Statechart selbst oder eine aus einer Aktion des Statecharts heraus aufgerufenen Methode verursacht sind und daher eine Weiterfuhrung ¨ der Transition zu einem naturlichen ¨ Ende nur beschr¨ankt sinnvoll ist. Die Moglichkeit ¨ paralleler Verarbeitung von Transitionen in demselben Objekt wird durch die grunds¨atzlich verwendete Synchronisation auf zustandsbehafteten Objekten ausgeschlossen.
¨ 4.5 Ubersetzung von Sequenzdiagrammen Ein Sequenzdiagramm ist in seiner Natur exemplarisch. Es zeigt einen einzelnen moglichen ¨ Ablauf eines Systems, das typischerweise abh¨angig von der aktuellen Objektstruktur, den Inhalten der Attribute und der Systemumgebung alternative Abl¨aufe erlaubt. Die Beschreibung eines exemplarischen Ablaufs ist fur ¨ die konstruktive Codegenerierung nur schlecht geeignet. Die einzige Moglichkeit, ¨ einen exemplarischen Ablauf konstruktiv einzusetzen, ergibt sich, wenn eine damit modellierte Methode nur einen Ablauf besitzt. Sie darf dann keine Verzweigungen des Kontrollflusses oder Iterationen beinhalten. Eine derartig einfache Struktur haben typischerweise meist nur Testtreiber. Da Sequenzdiagramme jedoch vor allem fur ¨ die Modellierung von Tests eingesetzt werden, ist eine Codegenerierung fur ¨ die Prufung ¨ von Testabl¨aufen notwendig. 4.5.1 Sequenzdiagramm als Testtreiber Abbildung 4.31 enth¨alt ein typisches Sequenzdiagramm, von dem fur ¨ das Objekt t eine Methode generiert werden soll, die die mit dem Stereotyp trigger markierten Aufrufe durchfuhrt. ¨
Abbildung 4.31. Sequenzdiagramm mit Treiber
Die zu generierende Methode benotigt ¨ einen Namen, der aus dem Diagrammnamen extrahiert werden kann. In Kombination mit einem standard-
¨ 4.5 Ubersetzung von Sequenzdiagrammen
137
m¨aßig festgelegten Pr¨afix runSD ergibt sich damit der in der Klasse Class generierte Methodenname runSDTreiber in der Klasse. Die Signatur dieser Methode ist wie bei der konstruktiven Umsetzung von Objektdiagrammen gepr¨agt durch die freien Variablen des Sequenzdiagramms. Im Beispiel werden daher zumindest die beiden anderen Objekte a und b als Parameter eingesetzt. Freie Variablen, die zuerst als Argumente eines trigger-Aufrufs auftreten, werden ebenfalls als Parameter aufgenommen. Demgegenuber ¨ werden freie Variablen, die in Returns das erste Mal verwendet werden, durch diesen Return-Wert belegt. Aufrufe und Returns zwischen anderen Objekten des Sequenzdiagramms sowie Aufrufe an Treiber-Objekte werden bei der Codegenerierung fur ¨ die angegebene Methode ignoriert. Als Ergebnis entsteht der in Abbildung 4.32 angegebene Code. class Class { ... public void runSDTreiber(A a, B b, Type2 args2) { Type value = a.m1(); a.m2(args2); b.m3(); } }
Java
Abbildung 4.32. Aus einem Sequenzdiagramm generierter Treiber
Die Typen der im Sequenzdiagramm angegebenen Variablen konnen, ¨ soweit nicht aus dem Sequenzdiagramm ersichtlich, aus dem Kontext, also zum Beispiel der Signatur der Klassen A und B, bestimmt werden. Als Erweiterung konnte ¨ es von Interesse sein, zwischendurch und am Ende zus¨atzliche Aktionen in Form von Java-Code anzugeben, die zum Beispiel ein explizites Return-Ergebnis der Methode berechnen. Fur ¨ den Einsatz als Testtreiber ist allerdings die angegebene Form der Codegenerierung ausreichend. Eine Vereinfachung entsteht zum Beispiel, wenn die Variable a bereits als Attribut der Klasse Class deklariert ist. Dann wird auf die Verwendung des Parameters a verzichtet. Das Verfahren zur konstruktiven Umsetzung von Teilen eines Sequenzdiagramms kann auch eingesetzt werden, wenn der Methodenaufruf der generierten Methode im Sequenzdiagramm selbst angegeben ist. Abbildung 4.33 beschreibt ein Dummy-Objekt, das eine einfache Implementierung der Methode foo() benotigt. ¨ Diese wird nach demselben Verfahren generiert und steht damit fur ¨ den in Abbildung 4.33 angegebenen Test zur Verfugung. ¨ Das Zielobjekt b der durch foo() aufzurufenden Methode ist als Attribut in der Klasse Dummy festgelegt. Die Besetzung der Objektstruktur wird typischerweise durch ein Objektdiagramm vorgenommen.
138
4 Transformationen fur ¨ die Codegenerierung
Abbildung 4.33. Sequenzdiagramm mit Treiber und Dummy
4.5.2 Sequenzdiagramm als Pr¨adikat Ein Sequenzdiagramm beschreibt genau wie ein Objektdiagramm eine exemplarische Eigenschaft des Systems. Im Gegensatz zu einem Objektdiagramm kann diese aber nicht an einem Snapshot eines Systems, sondern muss w¨ahrend eines Systemablaufs gepruft ¨ werden. Um die w¨ahrend eines Ablaufs auftretenden Interaktionen prufen ¨ zu konnen, ¨ ist eine entsprechende Instrumentierung des Codes notwendig. Dabei muss zu Beginn und am Ende jeder beobachteten Methode eine Mitteilung uber ¨ deren Aufruf beziehungsweise Terminierung erfolgen. Dabei ist auch die anormale Terminierung durch eine Exception zu protokollieren. Dies kann innerhalb der instrumentierten Methode, aber auch durch das Adapter-Entwurfsmuster [GHJV94] erfolgen. Ein solcher Adapter kann zum Beispiel durch Redefinition der Methode in einer Unterklasse gebildet werden. Das Prinzip fur ¨ die Klasse A aus Abbildung 4.33 ist in Abbildung 4.34 dargestellt. Es ist allerdings in der Praxis sinnvoll, diese Informationen statt an ein globales Objekt SDlog zu ubergeben, ¨ aus dem Aufrufkeller uber ¨ die Virtual Machine auszulesen. Der Algorithmus zur Erkennung, ob ein Sequenzdiagramm in der angegebenen Form abgearbeitet wurde, basiert auf der in Abbildung 7.14, Band 1 angegebenen Form zur Interpretation eines Sequenzdiagramms als regul¨arer Ausdruck. Der regul¨are Ausdruck wird zun¨achst in einem nichtdeterministischen endlichen Automaten15 realisiert, der es erlaubt, diesen regul¨aren Ausdruck zu erkennen. Abbildung 4.35 demonstriert dies an einem Beispiel. Im Sequenzdiagramm konnen ¨ prototypische Objekte auftreten, fur ¨ die zun¨achst keine Zuordnung zu echten Objekten existiert. Diese Zuordnung findet bei der jeweils ersten Interaktion mit einem passenden Objekt statt. Deshalb werden die Zust¨ande des nichtdeterministischen Automaten um die Konfiguration dieser Objekte sowie weiterer freier Variablen erweitert. 15
Der Automat dient zur Erkennung von Eingabesequenzen und hat mit den in Kapitel 6, Band 1 beschriebenen Statecharts formal nichts zu tun.
¨ 4.5 Ubersetzung von Sequenzdiagrammen
139
class Ainstrumented extends A { ... Java public Type method() { // Protokolliere Methodenaufruf (Objekt, Methode, leere Argumentliste) SDlog.call(this, "method", new Object[] {}); Type result; try{ // Eigentlicher Aufruf result = super.method(); catch (Exception ex) { // Protokolliere Exception SDlog.exceptionReturn(this, "method", ex); throw ex; } // Protokolliere Return + Ergebnis SDlog.normalReturn(this, "method", result); return result; } } Abbildung 4.34. Adapter zur Codeinstrumentierung
Deshalb kann ein Automat mehrfach denselben Zustand mit verschiedenen Objektkonfigurationen einnehmen. Nach jeder Interaktion werden eventuell im Sequenzdiagramm nachfolgende OCL-Bedingungen gepruft ¨ und die Konfigurationen, welche die Bedingung nicht erfullen, ¨ entfernt. Dies kann im Automaten durch eine zus¨atzliche Transition dargestellt werden, die keine Interaktion verarbeitet, aber eine Vorbedingung besitzt. Beginnend mit einer Konfiguration im Startzustand wird unter Umst¨anden eine leere Menge von gultigen ¨ Konfigurationen erreicht. Ein Sequenzdiagramm gilt als erfullt, ¨ wenn nach der Testdurchfuhrung ¨ der Endzustand in den erreichten Konfigurationen enthalten ist. Als Nebenerzeugnis der Prufung ¨ entsteht dabei auch die Belegung der freien Variablen und der prototypischen Objekte. Die verschiedenen durch den Stereotyp match w¨ahlbaren Semantiken fur ¨ Sequenzdiagramme werden durch die in Abbildung 7.14, Band 1 beschriebene Beschr¨ankung von Interaktionen, die ignoriert werden durfen, ¨ umgesetzt. Der beschriebene Algorithmus kann anhand des in Abbildung 4.35 beschriebenen Sequenzdiagramms illustriert werden. Dabei wird von konkreten Werten der Parameter abstrahiert. Diese konnen ¨ genauso behandelt werden, wie die OCL-Bedingungen, also zus¨atzlich gepruft ¨ werden. Abbildung 4.35 beinhaltet auch den Automaten, der zur Erkennung des Sequenzdiagramms verwendet wird.
140
4 Transformationen fur ¨ die Codegenerierung
Abbildung 4.35. Erkennender Automat aus einem Sequenzdiagramm
Die ubliche ¨ Konstruktion, um den Automaten deterministisch zu machen und zu minimieren [HU90], kann aufgrund der Konfigurationen, die Auswirkungen auf die Schaltbereitschaft weiterer Transitionen und die Evaluierung der OCL-Bedingungen haben, im Allgemeinen nicht durchgefuhrt ¨ werden. Dies w¨are zum Beispiel moglich, ¨ wenn die im Sequenzdiagramm dargestellten Objekte im Voraus auf echte Objekte zugeordnet werden konnten. ¨ Andererseits hat zum Beispiel ein Automat, der aus einem mit dem Stereotyp match:complete markierten Sequenzdiagramm entsteht, keine Schleifen und ist damit bereits deterministisch. Ein Sequenzdiagramm, das bereits teilweise zur konstruktiven Generierung von Treibern benutzt wurde, kann zus¨atzlich zu einer Prufung ¨ verwendet werden. Dass dabei auch der Testtreiber instrumentiert wird, erzeugt nur wenig Zusatzaufwand, ist aber notwendig, um die Reihenfolge der auftretenden Nachrichten und die OCL-Bedingungen prufen ¨ zu konnen. ¨
4.6 Zusammenfassung zur Codegenerierung Kapitel 3 und dieses Kapitel diskutieren zun¨achst grunds¨atzliche Fragestellungen zur Codegenerierung, beginnend mit der Ausdrucksm¨achtigkeit der UML/P bis hin zu einer sinnvollen Architektur eines Codegenerators, der die notwendige Flexibilit¨at und Parametrisierbarkeit besitzt, um verschiedenen Zielplattformen und unterschiedlichen Einsatzgebieten der UML/P gerecht zu werden. Statt einer tats¨achlichen Darstellung der Skripte und Templates wurde eine abstrakte, an den Darstellungen von Regelkalkulen ¨ orientierte
4.6 Zusammenfassung zur Codegenerierung
141
Form gew¨ahlt, um Transformationen darzustellen. Dabei wurde der pragmatische Ansatz unvollst¨andiger und verkurzender ¨ Transformationsregeln in Kombination mit erkl¨arendem Text gew¨ahlt. Die Umsetzung der einzelnen UML/P-Notationen in Code werden auf Basis dieser Transformationsregeln in diesem Kapitel beschrieben. Ausblick zur Codegenerierung Die Ans¨atze der ersten Generation von CASE-Werkzeugen zeigen, dass bei der Verwendung von Codegenerierung neben den in diesem Kapitel beschriebenen Vorteilen auch einige Nachteile in Kauf zu nehmen sind. So ist die Wartbarkeit eines mit Codegenerierung entwickelten Systems davon abh¨angig, ob der Codegenerator in der verwendeten oder einer aufw¨artskompatiblen Form w¨ahrend des Einsatzzeitraums des Produktionssystems zur Verfugung ¨ steht. Ist das nicht der Fall, ist der einzige Ausweg, den generierten Code manuell weiterzubearbeiten. Wird zum Beispiel die technische Plattform migriert, so ist es daruber ¨ hinaus notwendig, dass Entwickler mit F¨ahigkeiten zur Anpassung ( Programmierung“) des Generators zur ” Verfugung ¨ stehen. Wird ein Codegenerator daher im eigenen Projekt entwickelt, dann sollte er sehr einfach sein geschrieben und seine Verwendung wenn bei Wartung und Evolution des Systems notwendig wieder rekonstruierbar sein. Die Komplexit¨at der heutigen Quellsprachen wie UML steht aber im Widerspruch zu diesem Wunsch. Als vernunftige ¨ Alternative steht daher ein extern entwickelter, produktreifer Codegenerator zur Diskussion, der eine ausreichende Stabilit¨at aufweist, um auch in der Wartungsphase noch zur Verfugung ¨ zu stehen. Um Inkompatibilit¨aten bei Versionswechseln zu verhindern oder wenigstens zu minimieren, ist hier eine Standardisierung a¨ hnlich der Standardisierung von Compilern und der Semantik der zugrunde liegenden Sprachen geboten. Eine fur ¨ diese Zwecke ausreichend detaillierte Standardisierung ist in der UML-Standardisierungsdiskussion derzeit nicht zu finden. Dennoch konnte ¨ die Interoperabilit¨at heutiger UML-Werkzeuge einen gewissen Standardisierungseffekt fur ¨ die Codegenerierung bewirken. Die Stabilit¨at von Generierungswerkzeugen und das Vorhandensein geeigneter Experten ist unter Umst¨anden bei Open-Source-Werkzeugen wie ARGO/UML [RVR+ 01] und seinem Nachfolger Poseidon [BBWL01, BS01a] großer ¨ als bei kommerziellen Werkzeugen, da die quasi demokratisch organisierte Weiterentwicklung des Werkzeugs plotzliche ¨ Inkompatibilit¨aten verhindert und einen großeren ¨ Entwicklerstamm schafft. Auch vermeidet die Offenlegung des Quellcodes in Open-Source-Projekten Situationen, in denen der Hersteller kommerzieller Werkzeuge seine T¨atigkeit einstellt und ein Upgrade damit grunds¨atzlich nicht mehr moglich ¨ ist.
5 Grundlagen des Testens
Qualit¨at ist kein Zufall; sie ist das Ergebnis angestrengten Denkens. John Ruskin
Die Durchfuhrung ¨ von Tests fur ¨ alle Teile des Produktionssystems ist eine wesentlichen Maßnahme zur Softwarequalit¨atssicherung. Das Testen der Implementierung auf Robustheit, Konformit¨at zur Spezifikation, Korrektheit gegenuber ¨ den Anforderungen muss daher essentieller Bestandteil jeder qualit¨atsorientierten Softwareentwicklungsmethode sein. In diesem Kapitel werden die dazu notwendigen Grundlagen gekl¨art.
5.1 5.2
Einfuhrung ¨ in die Testproblematik . . . . . . . . . . . . . . . . . . . 144 Definition von Testf¨allen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
144
5 Grundlagen des Testens
Ein System, das den Anwender erfolgreich in seiner T¨atigkeit unterstutzen ¨ soll, muss moglichst ¨ frei von Fehlern sein. Da Softwaresysteme eine große Komplexit¨at besitzen, ist davon auszugehen, dass praktisch relevante Systeme nie vollst¨andig fehlerfrei sein werden. Wesentliche Aufgabe der Entwicklung ist es daher, die Fehlerrate und ihre Auswirkungen unter den vorhandenen zeitlichen und personellen Ressourcen so weit wie mo¨ glich zu minimieren und die fur ¨ die Anwendungsdom¨ane notwendige Qualit¨at zu erreichen. Das Testen des Softwaresystems ist dabei ein wesentliches Hilfsmittel zur Erkennung von Fehlern. Allgemein anerkannt ist, dass der Testaufwand im Durchschnitt ca. 40-50% des Gesamtaufwands der Softwareentwicklung betragen kann [Kru00a, Mye00]. Tests wurden sowohl in traditionellen als auch agilen Vorgehensweisen als wesentliches Kriterium zur Qualit¨atssicherung erkannt. Deshalb sind die in diesem Kapitel diskutierten Techniken nicht auf agile Methoden beschr¨ankt, sondern werden in der einen oder anderen Form seit l¨angerem eingesetzt. Durch die intensive Integration der Testentwicklung in die Methodik, dem Wunsch nach moglichst ¨ einfacher und effektiver Definition von Tests und dem durchg¨angigen Einsatz der UML fur ¨ Test- und Codegenerierung sind aber Anpassungen und Erweiterungen vorhandener Vorgehensweisen zur Testdefinition sinnvoll. Die große Anzahl verfugbarer ¨ Literatur zum Thema Testen allgemein und objektorientierter Systeme insbesondere zeigt, dass es sehr viele unterschiedliche Herangehensweisen und Techniken zur Testentwicklung gibt. Es w¨are jenseits der Kapazit¨at dieses Kapitels, das darin entwickelte Wissen zur Entwicklung und zum Management von Tests wiedergeben zu wollen. Stattdessen wird an den jeweils relevanten Stellen auf geeignete weiterfuhrende ¨ Literatur verwiesen. Der Inhalt dieses Kapitels richtet sich in der Terminologie weitgehend nach [Bin99], [Mye00], [Bal98] beziehungsweise [Lig90]. Der Inhalt dieses Kapitels ist deshalb keine allgemeine Einfuhrung ¨ in Testverfahren und -technologien, sondern nimmt eine kompakte Begriffsbestimmung fur ¨ Tests und eine Diskussion technischer Grundlagen vor. Im folgenden Kapitel 6 werden diese Grundlagen dann mit der UML/P umgesetzt. Fur ¨ erg¨anzende Vorgehensweisen zur Testentwicklung seien das eher pragmatische Buch [LF02] zum Thema Test-First“, die ausfuhrliche ¨ Samm” lung an Testtechniken in [Bei90, Bei95], das objektorientierte, ebenfalls um¨ fassende Analogon [Bin99] und die kompakte Ubersicht zur Testtheorie [Bal98] empfohlen. In seinen Grundzugen ¨ nach wie vor gultig ¨ ist [Mye79] (aktuelle deutsche Fassung [Mye00]).
5.1 Einfuhrung ¨ in die Testproblematik Zur Sicherung oder Verbesserung der Qualit¨at von Software gibt es eine Reihe von Verfahren, die w¨ahrend eines Softwareentwicklungsprojekts oder
5.1 Einfuhrung ¨ in die Testproblematik
145
projektubergreifend ¨ zum Einsatz kommen konnen. ¨ Qualit¨at“ ist ein viel ge” brauchter Begriff, der jedoch nur schwer zu definieren ist. In [Lig90] werden eine Reihe von Qualit¨atseigenschaften genannt, von denen unter anderem die zur Produktnutzung wichtigen Eigenschaften funktionale Korrektheit und Robustheit durch Tests uberpr ¨ uft ¨ werden konnen. ¨ Weitere Qualit¨atseigenschaften wie Laufzeit- und Speichereffizienz lassen sich durch statistische Tests zumindest in eingeschr¨ankter Form prufen. ¨ 5.1.1 Testbegriffe Abbildung 5.1 enth¨alt einige Definitionen des Begriffs Test“ aus der Litera” tur. In der Literatur sind unterschiedliche Definitionen fur ¨ den vielfach gebrauchten Begriff Test und die T¨atigkeit Testen zu finden (teilweise ins Deutsche ubersetzt): ¨ •
Testen ist der Prozess, ein Programm mit der Absicht auszufuhren, ¨ Fehler zu ” finden.“ [Mye00, S. 4]
•
Testen von Software ist die Ausfuhrung ¨ der Softwareimplementierung auf Testdaten und die Untersuchung der Ergebnisse und des operationellen Verhaltens, um zu prufen, ¨ dass die Software sich wie gefordert verh¨alt. [Som01, S. 420]
•
Die Anwendung von Test-, Analyse- und Verifikationsverfahren dient im we” ¨ sentlichen (sic) zur Uberpr ufung ¨ der Qualit¨atseigenschaften funktionale Korrektheit und Robustheit.“ [Lig90, S. 17]
•
Ein Test ist der Entwurf und die Implementierung einer speziellen Form eines Softwaresystems. Es pruft ¨ ein anderes Softwaresystem mit dem Ziel, Fehler zu finden. Tests werden entworfen, um das zu testende System zu analysieren und zu entscheiden, wie fehlerhaft es wahrscheinlich ist. Testentwurfe ¨ stellen Anforderungen an das automatisierte Testsystem, das die Tests automatisiert anwendet und evaluiert. Das Testsystem muss so entworfen werden, dass es mit den physischen Schnittstellen, der Struktur und der Laufzeitumgebung des zu testenden Systems zusammenarbeitet. Naturlich ¨ spielen manuelle Tests immer noch eine Rolle, aber Testen bedeutet haupts¨achlich die Entwicklung eines automatisierten Systems, das anwendungsspezifische Tests implementiert. [Bin99, S. 41]
•
Es gibt zwei Arten von Tests: (1) Unit-Tests und (2) Akzeptanztests. Entwickler schreiben Unit-Tests gemeinsam mit dem Code. Anwender schreiben Akzeptanztests nachdem die Anwendungsf¨alle definiert sind. [AM02, S. 6]
•
Unter Testen versteht man den Prozeß (sic) des Planens, der Vorbereitung und ” der Messung, mit dem Ziel, die Merkmale eines IT-Systems festzustellen und den Unterschied zwischen dem aktuellen und dem erforderlichen Zustand nachzuweisen.“ [PKS02, S. 528] Abbildung 5.1. Begriffsdefinitionen fur ¨ Tests“ ”
146
5 Grundlagen des Testens
Typisch fur ¨ den Extreme Programming-Ansatz ist das Fehlen einer eigenschaftsorientierten Definition des Begriffs Test“. Stattdessen wird eine ” operative Beschreibung angegeben, die gleichzeitig festlegt, wer Tests entwickelt, und sich dabei auf zwei einfache Testarten einschr¨ankt [AM02]. Besonders die verbose Definition von [Bin99] ist als Grundlage fur ¨ dieses Buch geeignet. Daraus lassen sich die in Abbildung 5.2 zusammengestellten Charakteristika fur ¨ Tests ableiten, die in diesem Buch als Grundlage dienen. Es gibt allerdings eine Reihe von Ausnahmen dieser Charakterisierung, die ebenfalls als Tests bezeichnet werden. Dazu gehoren ¨ manuelle, interaktive Tests, die zum Beispiel durch den Anwender beim Abnahmetest durchgefuhrt ¨ werden. Eine weitere Alternative ist das symbolische Testen, bei dem symbolische Berechnungen stattfinden, damit also das zu testende System nicht wirklich abl¨auft“. Lasttests in real verteilten Systemen produzieren ” typischerweise nicht immer dieselben Ergebnisse, sondern ermitteln durch wiederholte Ausfuhrung ¨ Durchschnittswerte. All diese Testarten sind nicht Gegenstand dieses Kapitels. Die Definition vollst¨andig automatisierter Tests gewinnt in den letzten Jahren immer mehr an Gewicht. So enth¨alt [FG99] eine ausfuhrliche ¨ Motivation und eine detaillierte Abgrenzung zu semi-automatischen oder manuellen Vorgehensweisen. 1. Ein Test l¨asst – im Gegensatz zu einer statischen Analyse – das zu testende System ablaufen. 2. Tests sind automatisiert. Da bei großen Systemen manuelle Tests sehr zeitraubend sind, wurde ¨ sonst die Qualit¨at der Tests leiden oder die Projektbeteiligten nur noch Tests durchfuhren. ¨ 3. Ein automatisierter Test fuhrt ¨ den Aufbau der Testdaten, den Test und die Prufung ¨ des Testergebnisses selbst¨andig durch. Der Erfolgsfall beziehungsweise das Scheitern werden durch den Testlauf erkannt und gemeldet. 4. Eine Sammlung von Tests bildet selbst ein Softwaresystem, das gemeinsam mit dem zu prufenden ¨ System abl¨auft. 5. Ein Test ist exemplarisch. Er arbeitet auf einem Satz von Eingabedaten, den Testdaten. 6. Ein Test ist wiederholbar und determiniert. Er produziert fur ¨ dasselbe zu testende System immer dieselben Ergebnisse. 7. Ein Test ist zielorientiert. Entweder er demonstriert die Anwesenheit und Auswirkungen eines Fehlers oder er zeigt, dass das System fur ¨ den Testfall die geforderte Funktionalit¨at hat und bezuglich ¨ der Testdaten robust ist. 8. Ein Test kann bei einem modifizierten System exemplarisch die Verhaltensgleichheit mit dem Ursprungssystem nachweisen und so bei der Vermeidung von Fehlern w¨ahrend der Weiterentwicklung helfen. Abbildung 5.2. Charakterisierung von Tests
5.1 Einfuhrung ¨ in die Testproblematik
147
Wichtig ist auch die Unterscheidung zwischen der Aktivit¨at des Testens mit dem Ziel der Fehlererkennung und der Fehlerbehebung, die nach dem Testen folgt. Weitere Maßnahmen zur Qualit¨atssicherung sind die CodeInspektion, in der der Quellcode von Entwicklern auf mogliche ¨ Fehler untersucht wird, und die Verifikation, die fur ¨ industrierelevante Systeme praktisch nicht durchfuhrbar ¨ ist, aber im Gegensatz zum Test die vollst¨andige Korrektheit einer Implementierung gegenuber ¨ einer Spezifikation nachweisen konn¨ te.
Tabelle 5.3. Tests auf verschiedenen Ebenen im System
Tabelle 5.3 ordnet Testbegriffe nach der Art des zu testenden Systemelements und charakterisiert, wer normalerweise diese Tests entwerfen und durchfuhren ¨ sollte. Je nach verwendetem Entwicklungsprozess wird die Gewichtung der Testentwicklung mehr auf einem eigenst¨andigen Testteam oder bei den Entwicklern selbst liegen. 5.1.2 Ziele der Testaktivit¨at Im Portfolio der Qualit¨atssicherungsmaßnahmen spielen Tests eine wesentliche Rolle, da eine systematische, zielgerichtete Erstellung und Durchfuhrung ¨ automatisierter Tests mit vertretbarem Aufwand durchgefuhrt ¨ werden kann. Testcode kann durchaus die Großenordnung ¨ von dem zu testenden Code erreichen oder uberschreiten, ¨ jedoch ist er meistens einfacher strukturiert und im Gegensatz zum Produktionscode ist seine Eleganz und Redundanzfreiheit deutlich weniger wichtig. So ist es akzeptabel, dass Tests im Copy-undPaste-Verfahren entwickelt werden [Den91, Fow99], obwohl es sich auch bei Tests lohnt, wiederverwendbare Abstraktionen in eigene Methoden auszulagern. Da das Auktionssystem mit hohen Geldbetr¨agen arbeitet und strengen
148
5 Grundlagen des Testens
zeitlichen Rahmenbedingungen unterliegt, wurde dort die notwendige Qualit¨at in Bezug auf Fehlerfreiheit der Software besonders hoch eingesch¨atzt. Dementsprechend ist 63% des insgesamt entwickelten Codes Teil des Testsystems.1 Dennoch wurden nur zirka 20% des Gesamtaufwands und damit vergleichsweise wenig in die Entwicklung und Wartung der Testf¨alle gesteckt. Die zus¨atzlichen Kosten fur ¨ die Entwicklung und Wartung einer automatisierten Testsammlung sind dementsprechend vertretbar. Eine Untersuchung in [KFN93] sch¨atzt ab, dass der Break-Even zwischen Kosten und Nutzen automatisierter Tests gegenuber ¨ etwa zehn manuellen Testdurchg¨angen erreicht wird. Bei sich dynamisch weiterentwickelnden Systemen, bei denen ¨ auch nach einer Installation noch Anderungen vorgenommen werden sind zehn manuelle Testdurchg¨ange schnell erreicht. Durch die mittlerweile zur Verfugung ¨ stehenden Werkzeuge und Frameworks wie JUnit [JUn02, BG98, BG99] steigt außerdem die Effizienz der Testfalldefinition, so dass der BreakEven weiter gesenkt werden durfte. ¨ Auch im Auktionsprojekt zeigten sich enorme Vorteile der automatisierten Tests, die in anderen Projekten in a¨ hnlicher Form zu erwarten sind: • • •
•
•
•
• 1
Fehler in der Programmlogik, bei Randwertbetrachtungen und falschen Codierungen werden fruhzeitig ¨ und fast immer bereits mit der Entstehung ausgemerzt. Das Zutrauen der Entwickler in den eigenen Code sowie den Code von Kollegen ist aufgrund der vorhandenen Tests signifikant hoher ¨ als ublich. ¨ Das Selbstvertrauen eines Entwicklers, auch den nicht von ihm selbst entwickelten Code auf ge¨anderte Anforderungen anzupassen, steigt aufgrund automatisiert wiederholbarer Tests und dem darin eingebetteten Wissen uber ¨ die Systemfunktionalit¨at. Eine ausfuhrliche ¨ Sammlung von Testf¨allen l¨asst sich neben der eigentlichen Systemspezifikation als zweites Modell fur ¨ das System verstehen. Dieses Modell enth¨alt zwar nur exemplarische Beschreibungen des Systems und diese sind sehr implizit im Testfall verborgen. Es hat jedoch den unsch¨atzbaren Vorteil der Ausfuhrbarkeit. ¨ Ein gescheiterter Test kann als Fehlerbeschreibung verstanden werden, der das Fehlersymptom dokumentiert. So konnen ¨ Nutzer einer Schnittstelle gegenuber ¨ den Implementierern Fehler nachweisen und die Behebung des Fehlers sehr einfach prufen. ¨ Automatisierte Tests sind fur ¨ die wiederholte Prufung ¨ des Systemverhaltens bei einem sich weiterentwickelnden System unverzichtbar. Interaktive Regressionstests wurden ¨ den wiederholten Testaufwand so vergroßern, ¨ dass dafur ¨ im Verlauf des Projekts sehr viel personelle Ressourcen gebunden w¨aren. Nach [Fow99] sowie eigener Erfahrung ist es hilfreich, den Einstieg in fremden und insbesondere ungetesteten Code dadurch vorzunehmen, Gemessen in LOC auf Java-Basis = Lines-Of-Code inklusive Kommentare.
5.1 Einfuhrung ¨ in die Testproblematik
•
149
dass auf Basis eines Codereviews das erwartete, aber gegebenenfalls in Details unklare Verhalten des Systems in Form von neu definierten Testf¨allen gepruft ¨ wird. Das dadurch entwickelte Verst¨andnis fur ¨ den Code ist intensiver und als Nebeneffekt entstehen (weitere) Tests. Letztendlich ist eine ausfuhrliche ¨ Testsammlung auch eine Dokumentation dem Kunden gegenuber, ¨ der zwar normalerweise nicht uber ¨ die Kapazit¨at und das Wissen verfugt, ¨ die Testf¨alle, wohl aber die Erfolgsmeldungen der Tests zu verstehen. Kunden sind dadurch auch leichter in der Lage, sp¨atere Verbesserungen und Erweiterungen am System durch andere Entwickler vornehmen zu lassen.
Tats¨achlich gab es bis jetzt im Auktionssystem keinen einzigen auf Programmierfehler des getesteten Softwaresystems zuruckzuf ¨ uhrenden ¨ Fehler. Alle Auktionen wurden spezifikationsgem¨aß und erfolgreich aufgesetzt und durchgefuhrt. ¨ Die Entwicklung von Tests ist zielorientiert. W¨ahrend Tests fur ¨ einzelne Methoden, Klassen und kleine Subsysteme vor allem dazu dienen, Fehler zu entdecken und fur ¨ eine Behebung herauszuarbeiten und damit zu dokumentieren, dienen Integrations- und Systemtests vor allem dazu, zu demonstrieren, dass Fehler (weitgehend) abwesend sind und dass das implementierte System sich den vorgegebenen Beschreibungen/Spezifikationen gem¨aß verh¨alt. 5.1.3 Fehlerkategorien In einem Softwaresystem konnen ¨ mehrere Fehlerkategorien unterschieden werden. Abbildung 5.4 enth¨alt eine Begriffsbestimmung fur ¨ die wichtigsten Fehlerkategorien. Versagen (engl.: failure) ist die Unf¨ahigkeit eines Systems oder einer Komponente eine geforderte Funktionalit¨at in den spezifizierten Grenzen zu erbringen. Versagen manifestiert sich durch falsche Ausgaben, fehlerhafte Terminierung oder nicht eingehaltene Zeit- und Speicher-Rahmenbedingungen. Mangel (engl.: fault) ist ein fehlender oder falscher Code. Fehler (engl.: error) ist eine Aktion des Anwenders oder eines Systems der Umgebung, das ein Versagen herbeifuhrt. ¨ Auslassung (engl.: omission) ist das Fehlen von geforderter Funktionalit¨at. ¨ Uberraschung (engl.: surprise) ist Code, der keine geforderte Funktionalit¨at unterstutzt ¨ und daher nutzlos ist. Abbildung 5.4. Begriffsdefinitionen fur ¨ Fehler nach [Bin99, S. 48]
Ein Mangel in der Software druckt ¨ sich dadurch aus, dass er bei Ausfuhrung ¨ des mangelhaften Codes zu einem Versagen des Softwaresystems fuhren ¨ kann. Mit einem Test l¨asst sich das Versagen von Software in der
150
5 Grundlagen des Testens
Testsituation erkennen und darauf zuruckschließen, ¨ dass die Software einen Mangel besitzt. Ein Versagen kann auf eine Kombination von M¨angeln zuruckzuf ¨ uhren ¨ sein. Umgekehrt kann derselbe Mangel zu unterschiedlichen Formen des Versagens fuhren, ¨ so dass mitunter detektivische Arbeit notwendig ist, um den Ort eines Mangels einzugrenzen. Zum einen kann dies durch Debugging mit manueller Verfolgung von Einzelschritten im System erfolgen. Besser ist es jedoch, Tests auf jeder Ebene des Systems zu besitzen, so dass ein Mangel bereits bei der kleinsten moglichen ¨ Systemkonfiguration erkannt wird. Durch die Definition weiterer Tests kann ein Mangel gezielt lokalisiert werden. Oft wird auch der Begriff Bug als Oberbegriff fur ¨ Versagen und Fehler definiert2,3 . In diesem Buch wird jedoch der Begriff Fehler als Synonym fur ¨ Bug verwendet und darunter generell das Versagen des Systems verstanden, das durch eine Anwenderaktion, eine Interaktion mit der Systemumgebung oder einen Test herbeigefuhrt ¨ wurde. ¨ Auslassungen und Uberraschungen lassen sich nicht durch automatisierte Tests feststellen. Fur ¨ die Erkennung von Auslassungen sind formale oder informelle Beschreibungen der geforderten Funktionalit¨at notwendig, die fur ¨ eine statische Analyse oder einen vergleichenden Review verwendet werden. Beispielsweise werden Auslassungen durch nicht ubersetzbare ¨ Pro¨ gramme und durch Abnahmetests entdeckt. Uberraschungen sind demgegenuber ¨ weniger problematisch. Sie fuhren ¨ zwar bei der Systementwicklung zu unnotigem ¨ Mehraufwand, storen ¨ aber die wesentliche Funktionalit¨at nicht. Statische Analysen konnen ¨ zum Beispiel unerreichbaren Code in einer Methode erkennen. 5.1.4 Begriffsbestimmung fur ¨ Testverfahren Im Kontext von Tests gibt es eine Reihe weiterer Begriffe, die teilweise auch in diesem Kapitel bereits verwendet wurden und noch einer Kl¨arung bedurfen. ¨ Abbildung 5.5 beschreibt die wesentlichsten in Kurzform. Der Begriff Testling“ ist zwar ein Kunstwort, trifft aber die Bedeutung ” des zu testenden Systems beziehungsweise der Systemkomponente, weshalb in diesem Buch dieser Begriff von [Den91] ubernommen ¨ wird.4 In [Bin99] wird der Begriff test point“ fur ¨ einen Testdatensatz verwendet, der ” sehr schon ¨ die exemplarische, punktuelle Natur eines Tests herausstellt. Der TTCN-Standard [ISO92] unterscheidet weitere Testurteile. Zus¨atzlich zum 2
3
4
Der englische Begriff bug stammt von [Hop81], in dem beschrieben wird, wie eine Motte sich in einem Relay fing und daraufhin die Suche von Fehlern mit der Suche von K¨afern identifiziert wurde. Bug findet sich mittlerweile auch im deutschen Sprachgebrauch, hat sich aber nur in der Umgangssprache durchsetzen konnen. ¨ In diesem Buch wird deshalb der Begriff Fehler verwendet. Das zum Beispiel in [PKS02] verwendete Testobjekt“ kann auch ein Teilsystem ” sein und aus mehreren Objekten bestehen. Es ist daher irrefuhrend. ¨
5.1 Einfuhrung ¨ in die Testproblematik
151
Validierung dient zur Prufung, ¨ ob das System die vom Anwender geforderten Anforderungen erfullt ¨ (nach [Boe81]). Dies geschieht zum Beispiel durch Prototyping w¨ahrend des Projekts und Abnahmetests an dessen Ende. Verifikation dient zum Nachweis, dass das implementierte System die formale Spezifikation erfullt, ¨ also korrekt ist (nach [Boe81]). System im Test wird auch das zu testende System“, Testling [Den91, Bal98], Prufling ¨ ” [Lig90] und Testobjekt [PKS02] genannt. Testverfahren ist eine Vorgehensweise zur Erstellung und Durchfuhrung ¨ von Tests. Die Testtheorie kennt eine Reihe von Verfahren, die speziell die Entwicklung von Testdaten behandeln. Testdaten (engl.: test point, [Bin99]). Die Testdaten bestehen aus einem konkreten Satz von Werten fur ¨ die Eingabe eines Tests, die auch die Objektstruktur mit den zu testenden Objekten beinhaltet. Test-Sollergebnis ist das erwartete Ergebnis eines Tests. Dieses kann explizit durch einen Datensatz oder implizit durch ein Prufpr¨ ¨ adikat zum Beispiel als Vergleich mit dem Ergebnis eines Testorakels gegeben sein. Testfall (engl.: test case) besteht aus einer Beschreibung des Zustands des zu testenden Systems und der Umgebung vor dem Test, den Testdaten und dem TestSollergebnis. Testsammlung (engl.: test suite) ist eine Menge von Testf¨allen. Testlauf (oder auch Testablauf, engl.: test run) ist die Durchfuhrung ¨ eines Tests einschließlich der tats¨achlichen Ergebnisse (Test-Istergebnisse). Ein Testtreiber organisiert die Durchfuhrung ¨ vom Aufbau der Testdaten bis zur Prufung ¨ des Testerfolgs. Testerfolg ist genau dann eingetreten, wenn das Istergebnis und das Sollergebnis konform sind. Ansonsten ist der Test gescheitert. Testurteil ist die bin¨are Aussage, ob der Test erfolgreich war oder gescheitert ist. Abbildung 5.5. Begriffsdefinitionen fur ¨ Tests
Testerfolg ( pass“) gibt es zwei Arten des Scheiterns ( failure“, error“) und ” ” ” die Alternativen inconclusive“ und none“, in denen das Testziel nicht ge” ” pruft ¨ werden konnte. Das Testurteil wird dort auch als Verdikt“ bezeichnet. ” Das Testurteil error“ kann in Java zum Beispiel als Terminierung des Test” lings durch eine Exception interpretiert werden. Besonderer Beachtung bedarf, dass das Scheitern eines Tests bedeutet, dass Test und Implementierung nicht konform sind oder w¨ahrend des Tests eine unerwartete Exception aufgetreten ist. Damit ist der Test oder die Implementierung mangelhaft.5 5
In [Mye79] wird darauf hingewiesen, dass ein in diesem Sinn gescheiterter Test seinen Testzweck, n¨amlich die Fehlerfindung erfullt ¨ hat und somit als Erfolg fur ¨ den Test gewertet werden kann.
152
5 Grundlagen des Testens
5.1.5 Suche geeigneter Testdaten Ein wesentlicher Problemkreis beim Testen ist die effiziente Entwicklung einer systematischen und alle wesentlichen“ F¨alle uberdeckenden ¨ Sammlung ” von Testf¨allen. Sind fur ¨ einen Testfall die Testdaten gegeben, so ist das Sollergebnis meist aus der Spezifikation abgeleitet oder vom Entwickler beziehungsweise Anwender festgelegt worden. Die Vorgabe eines Sollergebnisses kann aufw¨andig werden und ist fehleranf¨allig. Jedoch testen sich das Produktionssystem und die Testsammlung gegenseitig, so dass auch Fehler in Testf¨allen erkannt und behoben werden konnen. ¨ Als wesentliche Schwierigkeit bleibt daher die Identifikation geeigneter Testdaten und die Festlegung, wieviele Testf¨alle fur ¨ eine ad¨aquate Sammlung von Tests ausreichend sind. Bereits in [Mye79] wurden Heuristiken und Verfahren zur Entwicklung von Testdatens¨atzen beschrieben, die in [Lig90] und [Bei90] in verfeinerter Form diskutiert werden. Dazu gehoren ¨ Verfahren, die sich am Kontrollfluss der Implementierung orientieren, indem sie alle Anweisungen, Verzweigungen, Bedingungsvariationen oder Pfade innerhalb einer Methode nach bestimmten Kriterien uberdecken. ¨ Andere Verfahren identifizieren zus¨atzlich ¨ Aquivalenzklassen von Testdaten und Grenzwertbereiche. Datenflussorientierte Testverfahren nutzen Attribut- und Variablenzugriffe, um Testdaten zu entwickeln. Den kontrollfluss- und datenflussorientierten Verfahren ist gemeinsam, dass sie die Implementierung des Systems als bekannt voraussetzen und die Erstellung der Testf¨alle basierend auf der Analyse der Implementierung beruht. Demgegenuber ¨ steht die Klasse der funktionalen oder spezifikationsbasierten Tests. Sie basieren nicht auf der Implementierung, sondern einer Spezifikation und prufen ¨ die funktionalen Eigenschaften eines Systems. Sie erkennen also Konformit¨atsfehler des Systems, beziehungsweise demonstrieren ¨ die Ubereinstimmung zu der spezifizierten Funktionalit¨at. Solche Tests werden auch Konformit¨atstests genannt. Zur Bestimmung der Qualit¨at einer Sammlung von Tests werden Metriken verwendet, die eine Testuberdeckung ¨ nach verschiedenen Kriterien messen. Jedoch ist auch eine vollst¨andige Testuberdeckung ¨ nach diesen Metriken keine Garantie fur ¨ ein korrektes System. Deshalb wird in der Praxis von der eher dogmatischen Testtheorie verst¨arkt zu einer erfahrungsgetriebenen, zum Beispiel durch Testmuster [Bin99], Checklisten [PKS02] und Best Practi” ces“ beschriebenen Vorgehensweise ubergegangen. ¨ Es ist den pragmatischen Testmustern anzumerken, dass Elemente der Testtheorie Eingang gefunden haben, ohne jedoch dogmatisch deren Erfullung ¨ zu 100% zu fordern. So kann zum Beispiel aus dem Extreme Programming-Ansatz gefolgert werden, dass die Anweisungsuberdeckung ¨ als Minimalziel gefordert und nach Moglich¨ keit eine minimale Pfaduberdeckung ¨ gewunscht ¨ ist.
5.1 Einfuhrung ¨ in die Testproblematik
153
5.1.6 Sprachspezifische Fehlerquellen Eine zu obigen Punkten orthogonale Fehlerkategorisierung ergibt sich aus der Frage, ob ein System zum einen robust und zum anderen konform zur Spezifikation ist. Die Robustheit einer Implementierung kann durch anormale Absturze ¨ (Exceptions) aufgrund von nicht initialisierten Attributen, Referenzen auf nicht existierende Objekte6 und a¨ hnlichen Problemen gestort ¨ werden. Typisch fur ¨ M¨angel in der Robustheit ist, dass nicht gegen eine Spezifikation getestet wird, sondern sprachspezifische Fehlerquellen zu eliminieren sind. C++ ist ein Paradebeispiel fur ¨ außerordentlich viele Fehlerquellen, die aus der hohen Anzahl von ungesicherten C++-Konstrukten resultieren. Die dem Entwickler uberlassene ¨ Speicherverwaltung, die Zeigerarithmetik und ungeprufte ¨ Zugriffe auf Felder sind nur einige der moglichen ¨ Fehlerquellen. Java ist, obwohl syntaktisch der Sprache C++ a¨ hnlich, in Bezug auf derartige Fehlerquellen wesentlich robuster. Viele Fehlerquellen werden in Java durch restriktive Kontextbedingungen in der Sprache und damit bereits durch statische Analysen eliminierbar. Zum Beispiel wird durch eine ausgefeilte Datenflussanalyse [GJSB00] gepruft, ¨ ob Variablen besetzt wurden, bevor sie benutzt werden. Die ESC/Java-Erweiterung [RLNS00] um Zusicherungen erlaubt eine noch weitergehende statische Analyse, erfordert jedoch eine detaillierte Beschreibung von Zusicherungen im Java-Code. Dennoch kann dadurch die Anzahl der Fehlerquellen weiter reduziert werden. Weitere Java-Fehlerquellen werden durch Laufzeituberpr ¨ ufungen ¨ entdeckt und durch Exceptions dem Programm gemeldet. Dazu gehoren ¨ zum ¨ Beispiel das Uberschreiten von Arraygrenzen, die Division durch 0 oder illegale Typkonversionen. Damit l¨asst sich ein Programm relativ leicht robust gestalten. Jedoch sollte die dafur ¨ notwendige Verwendung von Exceptions soweit wie moglich ¨ begrenzt werden, da die Verarbeitung von Exceptions Charakteristika der goto-Anweisung aufweist und leicht zu unubersichtli¨ chem Code fuhrt. ¨ Als generelles Prinzip sollten nur externe Fehlerquellen, wie eine nicht vorhandene Datei, eine nicht erreichbare Datenbank oder eine abgebrochene Internet-Verbindung, mit Exceptions behandelt werden und intern zu verantwortende Fehlerquellen, wie Division durch 0 oder falsche Arraygrenzen durch explizite Abfragen abgesichert werden. Unabh¨angig von der Art des Abfangens solcher Fehler ist eine robuste Behandlung des Fehlers und dementsprechender Tests notwendig, die demonstrieren, dass der Fehler korrekt behandelt wird. Auch dafur ¨ ist es sinnvoll, moglichst ¨ wenig Exceptions durch die Aufrufhierarchie verfolgen zu mussen. ¨ Obwohl Java gegenuber ¨ C++ sehr viel sicherer entworfen wurde, gibt es auch in Java eine Reihe von Fehlerquellen. Von diesen Fehlerquellen konnen ¨ viele durch restriktive Programmierung verhindert werden. So sollte in Java 6
Ein typisches C++-Problem.
154
5 Grundlagen des Testens
ein Attribut der Oberklasse nicht verschattet werden, indem in der Unterklasse ein gleichnamiges Attribut definiert wird. Objektorientierte Programme haben mit ihrer hohen Dynamik, der Vererbungshierarchie und des dynamischen Bindens von Methoden eine deutlich hohere ¨ Komplexit¨at, als dies noch bei prozeduralen Sprachen der Fall war. Objektorientierte Methoden sind meist sehr viel kleiner als das Prozeduren waren und interagieren st¨arker mit anderen Methoden. Dadurch entsteht zum Beispiel die in Frameworks so wichtige Flexibilit¨at durch Adaption von Methoden in Unterklassen [FPR01, FSJ99]. Jedoch erfordert diese Moglich¨ keit zur Redefinition zus¨atzlichen Aufwand beim Test. Insbesondere reicht es nicht mehr, nur innerhalb einer Methode alle moglichen ¨ Kontrollflusse ¨ zu testen, sondern es mussen ¨ alle potentiellen Konstellationen der Zusammenarbeit von Methoden in allen Unterklassen gepruft ¨ werden. Die Aufgabe potenziert sich, wenn mehrere Objekte kollaborieren, von denen jedes aus einer von mehreren Unterklassen stammen kann. Es ist daher oft nicht mehr durchfuhrbar, ¨ Tests, die alle Kombinationen von Methodenaufrufen und Objektstrukturen uberdecken, ¨ zu entwickeln. Da UML/P als Zielsprache Java nutzt, sind die in Java vorhandenen Probleme weitgehend in der UML/P wiederzufinden. Eine Ausnahme ist zum Beispiel die erw¨ahnte Verschattung von Attributen, die in UML aufgrund entsprechender Kontextbedingungen nicht dargestellt werden kann und daher bei einer Codegenerierung nach Java nicht entsteht. Bei einer konstruktiven Nutzung von UML-Diagrammen zur Codegenerierung entstehen fur ¨ das resultierende Programm eine Reihe von Moglich¨ keiten, die vorgegebene Spezifikation zu verletzen. Je nach Form der Generierung und der dabei umgesetzten Konzepte, werden bestimmte Fehler der zugrunde liegenden Sprache Java vermieden oder neue Probleme eingefuhrt. ¨ Beispielsweise werden gem¨aß der in Abschnitt 4.1.3 angegebenen Transformation einschr¨ankende Kardinalit¨aten von Assoziationen normalerweise nicht konstruktiv umgesetzt. Dadurch kann eine Verletzung der Invarianten auftreten, die nur dadurch verhindert werden kann, dass die Umgebung der Assoziation die Invariante beachtet. Dies ist in Tests zu prufen. ¨ ¨ Ahnlich werden Zustands- oder Nachbedingungen in Statecharts nicht notwendig konstruktiv sichergestellt und sind daher zu testen. Andererseits kann, wie in Abschnitt 4.1.3 gezeigt, durch Generierung geeigneter Funktionalit¨at beispielsweise sichergestellt werden, dass eine bidirektionale Assoziation immer konsistent ist, so dass dadurch Tests entfallen konnen. ¨ Die Definition von sprachspezifischen Tests, die Robustheitsfehler erkennen konnen, ¨ h¨angt daher wesentlich von der Form des generierten Codes ab. Da der Codegenerator in wesentlichen Elementen parametrisierbar sein muss, ist damit eine Vorhersage, welche sprachspezifischen Tests fur ¨ UML/P notwendig sind, schwer durchfuhrbar. ¨ Es ist daher hilfreich, fur ¨ Stellen, an denen ein Generator-Skript es nicht verhindert, Invarianten zu verletzen, geeignete Testf¨alle zu deren Prufung ¨ zu definieren.
5.1 Einfuhrung ¨ in die Testproblematik
155
5.1.7 UML/P als Test- und Implementierungssprache Wie in Abschnitt 3.1 diskutiert, kann die UML/P im Softwareentwicklungsprozess mehrere Rollen einnehmen. Sie ist gleichzeitig als Implementierungssprache und als Sprache zur Definition von Tests geeignet. Damit uber¨ nimmt sie a¨ hnliche Aufgabenstellungen wie die jeweils benutzte Programmiersprache in Extreme Programming-Projekten. Dort werden Tests und Implementierung ebenfalls in derselben Sprache formuliert. Erfahrungen mit der UML als Sprache zur Testmodellierung zeigen außerdem, dass die Effizienz der Entwickler verbessert wird [BMJ01, JB04]. Jedoch ist es wichtig, die UML in einer testbaren Form einzusetzen [BL01, Rum03]. Unter Testbarkeit wird generell die F¨ahigkeit verstanden, aus dem Modell Tests abzuleiten oder – idealerweise – automatisch zu generieren. Wird die UML/P als Implementierungssprache verwendet, so ist fur ¨ eine systematische Testentwicklung die Kenntnis notwendig, welche sprachspezifischen und typisch objektorientierten Probleme die UML/P behebt, aber auch mit sich bringt. Ein typisches Problem der Umsetzung von bidirektionalen Assoziationen ist die Wahrung der Konsistenz zwischen beiden Attributen, die die Assoziation speichern. Bei der Generierung von Code aus einem Klassendiagramm, der nicht mehr manuell ver¨andert werden darf, kann diese Konsistenz durch den in Abschnitt 4.1.3 beschriebenen Code konstruktiv gesichert werden. Sie muss also nicht mehr durch Tests gepruft ¨ werden. Andererseits fuhrt ¨ die Verwendung eines Attributs des referenzierten Objekts als Qualifikator in einer qualifizierten Assoziation Redundanz ein, ¨ die bei Anderung des Attributwerts zur Inkonsistenz fuhren ¨ kann. Eine sta¨ tische Analyse des Codes kann feststellen, ob eine Anderung dieses Attributs uberhaupt ¨ stattfindet. Ist das jedoch der Fall, so sind dynamische Tests notwendig, um festzustellen, ob dadurch die Konsistenz fur ¨ eine qualifizierte Assoziation verletzt wird. Um also in der UML/P formulierte Implementierungen auf ihre Robustheit zu prufen, ¨ ist es notwendig die Notationen der UML/P und ihre Umsetzung selbst einer kritischen Analyse auf mogliche ¨ Fehlerquellen zu untersuchen. Dabei ist zu beachten, dass die UML/P nicht nur aus mehreren Diagrammarten und der OCL besteht, sondern auch Java-Code explizit als Methodenrumpfe ¨ und als prozedurale Aktionen in Statecharts erlaubt. Dadurch bleiben wie bereits erw¨ahnt viele der fur ¨ Java typischen Fehlerquellen erhalten. Die potentiellen Fehlerquellen der Implementierungssprache UML/P h¨angen aber weitgehend von der konkreten Umsetzung durch den parametrisierten Codegenerator ab und konnen ¨ daher nicht allgemein diskutiert werden. Die Untersuchung nach UML/P-Fehlerquellen beinhaltet wie im obigen Beispiel der Assoziationen auch die Frage, wie diese Fehlerquellen zu behandeln sind. Dazu gibt es funf ¨ wesentliche Strategien:
156
5 Grundlagen des Testens
1. Eine statische Analyse der UML/P-Modelle kann kl¨aren, ob an einem generierten Element auf unzul¨assige Weise Manipulationen vorgenommen werden konnen. ¨ Das kann entweder in Form einer Kontextbedingung verboten oder durch Warnungen mitgeteilt werden. In der UML/P gehoren ¨ dazu zum Beispiel Manipulationen auf mit frozen gekennzeichneten Attributen oder Assoziationen. 2. Der Codegenerator fugt ¨ eine Laufzeitprufung ¨ hinzu, die zwar den Versuch zur verbotenen Manipulation nicht verhindert, aber diese zum Beispiel durch die Ausgabe einer Exception anzeigt. Java macht dies beispielsweise bei illegalen Array-Zugriffen. UML/P kann dieses Konzept zum Beispiel bei qualifizierten Assoziationen ubernehmen. ¨ Jedoch mussen ¨ diese Exceptions als Teil des in Abschnitt 3.2.2 diskutierten API dem Entwickler bekannt gegeben werden. 3. Der Codegenerator entwirft nicht nur den Code zur Umsetzung eines UML/P-Konstrukts, sondern auch Testcode, der zur Laufzeit pruft, ¨ ob eine Eigenschaft eingehalten wird. Beispielsweise kann die eingeschr¨ankte Kardinalit¨at einer Assoziation durch die Prufung ¨ einer Invariante gesichert werden. Diese Form der Prufung ¨ der Invariante a¨ hnelt der oben beschriebenen Laufzeitprufung. ¨ Sie unterscheidet sich aber einerseits darin, dass sie nur im instrumentierten Produktionscode existiert. Zum anderen wirft sie keine vom Produktionssystem zu verarbeitende Exception, sondern meldet das Scheitern eines Tests. 4. Der Codegenerator nutzt ein Modell als Spezifikation des erwarteten ¨ Verhaltens und extrahiert daraus Testf¨alle nach einem Uberdeckungskriterium. Beispielsweise sind bestimmte Statecharts fur ¨ eine Generierung von Zustands-, Transitions- oder Pfad-uberdeckenden ¨ Testf¨allen geeignet. 5. Der Entwickler entwirft selbst weitere Tests, um Eigenschaften des aus einem UML/P-Konstrukt generierten Codes zu uberpr ¨ ufen. ¨ Der letzte genannte Punkt ist eigentlich nicht notwendig, wenn der Codegenerator korrekt funktioniert. Jedoch ist ein Codegenerator, wie in Kapitel 3 beschrieben, parametrisiert. Das heißt, Skripte konnen ¨ die Codegenerierung in einer flexiblen Weise steuern. Moglicherweise ¨ wird dadurch Code generiert, der beispielsweise die Konsistenz der bidirektionalen Assoziation nicht durch geeignete Maßnahmen sicherstellt oder sogar selbst verletzt. Das bedeutet, dass Tests fur ¨ solche Zwecke typischerweise den Codegenerator beziehungsweise seine Skripte prufen ¨ und damit ihre Berechtigung haben. Es ist daher fur ¨ jedes Projekt einzeln zu entscheiden, welche Teile des Systems wie intensiv getestet werden. Dies h¨angt naturlich ¨ auch von den Projektzielen, der zu erreichenden Qualit¨at und der Einsatzform des Produkts ab. Bereits Abschnitt 3.1.2 diskutiert vom Standpunkt der Codegenerierung, welche UML/P-Teile sich konstruktiv oder fur ¨ Tests einsetzen lassen. Nicht direkt in konstruktiven Code ubersetzbare ¨ Objektdiagramme, OCL-Bedin-
5.1 Einfuhrung ¨ in die Testproblematik
157
gungen und Statecharts bilden dennoch nicht notwendigerweise Testmodelle, die vom Codegenerator zur Generierung von Tests verwendet wer7 den konnen. ¨ Werden die genannten Diagramme im Entwicklungsprozess zun¨achst nur zur Kommunikation der Entwickler untereinander oder zur abstrahierten Darstellung verwendet, so mussen ¨ diese normalerweise umgeformt und detailliert werden, um daraus Testmodelle zu erhalten, die zur Generierung von Tests geeignet sind. In den Kapiteln 3 und 4 wurde die Umsetzung einzelner Diagramme und OCL-Bedingungen in Code fur ¨ einen Einsatz in Tests bereits ausfuhrlich ¨ diskutiert. Dieses Kapitel wird daher den methodischen Einsatz durch Kombination dieser UML/P-Artefakte zu Testf¨allen fur ¨ Konformit¨atstests besprechen. Abbildung 3.3 skizziert den typischen Einsatz von UML/P-Diagrammen fur ¨ Tests und Implementierung. W¨ahrend der Produktionscode ein vollst¨andiges System darstellt, ist der Testcode nur in Kombination mit dem Produktionscode lauff¨ahig. Der Produktionscode wird außerdem fur ¨ den Einsatz im Test instrumentiert. Das heißt, er wird durch zus¨atzliche Codestucke ¨ erweitert, damit der Testcode auch w¨ahrend des Testablaufs auf alle notwendigen Informationen zugreifen kann. Dazu gehoren ¨ zum Beispiel spezielle Funktionen zum Besetzen und Auslesen gekapselter Attribute, wenn die entsprechenden get- und set-Funktionen nicht standardm¨aßig zur Verfugung ¨ stehen oder zus¨atzliche Effekte haben, die zum Beispiel zur Erhaltung der Konsistenz dienen. So wird Code eingefugt, ¨ der Invarianten und OCL-Methodenspezifikationen zur Laufzeit pruft ¨ sowie die Protokollierung von Methodenaufrufen zum Vergleich mit vorgegebenen Aufrufreihenfolgen ermoglicht. ¨ Bei konventioneller Programmierung mit Java mussen ¨ fur ¨ solche Aufgaben Testmonitore oder Adapter entwickelt werden [Wil01], die jedoch nur einen begrenzten Zugang zu den Testlingen haben. Die Instrumentierung darf das funktionale Verhalten des Produktionscodes nicht ver¨andern, weshalb es verboten ist, in OCL-Bedingungen modifizierende Methoden einzusetzen. Nur so ist gew¨ahrleistet, dass der fur ¨ die Freigabe bestimmte, nicht instrumentierte Produktionscode dasselbe Verhalten besitzt wie der getestete Code. Es kann notwendig sein, denselben Produktionscode fur ¨ verschiedene Tests unterschiedlich zu instrumentieren. Zum Beispiel sind nur fur ¨ manche Tests Aufrufreihenfolgen irrelevant. Auch kann es sehr ineffizient werden, wenn alle OCL-Invarianten in allen Tests gepruft ¨ werden. Dauert die Ausfuhrung ¨ von Tests zu lange, so werden diese unpraktikabel. Die Instrumentierung ist daher entweder abh¨angig vom gerade ausgefuhrten ¨ Test zu individualisieren oder durch boolesche Flags zur Laufzeit parametrisieren. 7
Der Begriff Testmodell“ wurde bereits in Abbildung 3.1 eingefuhrt. ¨ Aufgrund des ” Generierungsaspekts aus UML-Modellen wird er dort etwas enger definiert, als zum Beispiel im Rational Unified Process [Kru00a], in dem eine manuelle Umsetzung in Skripte und Testtreiber vorgeschlagen wird, die ebenfalls zum Testmodell gez¨ahlt werden.
158
5 Grundlagen des Testens
Letzteres hat den Nachteil, dass dadurch der instrumentierte Code groß werden kann, aber den Vorteil, dass keine wiederholte Codegenerierung ¨ und Ubersetzung des generierten Codes notwendig ist. Die Besetzung der Flags kann durch den Testtreiber direkt oder durch Stereotypen in der Testbeschreibung festgelegt werden. Aufgrund der stetig leistungsf¨ahiger werdenden Rechner und der Effizienz guter Compiler kann davon ausgegangen werden, dass die Codeinstrumentierung fur ¨ Testzwecke und die damit mogliche ¨ Simulation von Umgebung und Verteilung sowie die dynamische Prufung ¨ von Invarianten, Vor- und Nachbedingungen im Produktionscode praktikabel sind oder werden. Der instrumentierte Produktionscode und der Testcode testen sich gegenseitig. Ist ein Testfall gescheitert, so kann der Testling, aber auch der Testfall selbst fehlerhaft sein. In der auch im Auktionsprojekt beobachteten Praxis sind deutlich h¨aufiger die Testf¨alle selbst fehlerhaft, zum Beispiel ¨ weil vergessen wurde, die Anderung in der Funktionalit¨at einer Methode im Testfall ad¨aquat nachzuziehen. Unangenehm wird es wenn beide, der Testling und der Testfall, konsistent falsch sind und f¨alschlicherweise ein Testerfolg gemeldet wird. Dieses Problem tritt besonders dann auf, wenn ein Entwickler sowohl den Produktionscode als auch den Test entwirft und dabei einen Denkfehler wiederholt. Dem wird zum Beispiel im Extreme Programming-Ansatz dadurch entgegnet, dass zwei Entwickler gleichzeitig und gemeinsam am Code arbeiten. Zus¨atzlich sollten Tests der daruber ¨ liegenden Schichten in der Lage sein, einen solchen Fehler dennoch zu entdecken. 5.1.8 Eine Notation fur ¨ die Testfalldefinition Fur ¨ bestimmte Zwecke werden auch eigenst¨andige Testnotationen verwendet. Die Telekommunikationsindustrie nutzt zum Beispiel vorrangig TTCN [ISO92, GS02] in Kombination mit MSCs [IT99a, Kru00b] ¨ und SDL [IT99c]. Die Verwendung einer speziellen Testnotation wird als Vorteil gegenuber ¨ der Benutzung einer Programmiersprache zur Definition von Testtreibern empfunden, weil sie abstrakter und damit ubersichtlicher ¨ ist und Ver¨anderungen der Funktionalit¨at damit leichter in den Tests nachgezogen werden konnen. ¨ Dies ist allerdings nur bedingt richtig, wenn die in einer Programmiersprache wie Java implementierten Tests von einer gut entworfenen Bibliothek von Hilfsfunktionen unterstutzt ¨ werden. Bei der Verwendung einer abstrakten Testnotation ist viel Aufwand in die Entwicklung des Testwerkzeugs zur Interpretation der Testnotation und in die Ansteuerung von Softund Hardwarekomponenten sowie die Schulung der Entwickler zu investieren. Bei der Verwendung der Programmiersprache Java zur Testfalldefinition spiegelt sich dieser Aufwand in der Benutzung des Frameworks wider, das die implementierten Hilfsfunktionen zur Verfugung ¨ stellt. Damit haben beide Vorgehensweisen einiges gemeinsam. Jedoch hat die Verwendung der-
5.1 Einfuhrung ¨ in die Testproblematik
159
selben Programmiersprache fur ¨ Tests und Implementierung einige Vorteile gegenuber ¨ der Verwendung einer eigenen Testnotation: • •
•
Der Aufwand, eine Testnotation zu erlernen, ist nicht zu vernachl¨assigen und entf¨allt bei der Nutzung einer Sprache, die auch als Realisierungssprache verwendet wird. Testnotationen sind in ihrer Beschreibungsm¨achtigkeit typischerweise eingeschr¨ankt. Das fuhrt ¨ dazu, dass entweder bestimmte Tests nicht formuliert werden konnen, ¨ oder die Testnotation ad hoc erweitert wird. Letzteres ist beispielsweise dann nicht moglich, ¨ wenn das genutzte Werkzeug nicht im Quellcode zur Verfugung ¨ steht. Falls es moglich ¨ ist, bedeutet es einen enormen Zusatzaufwand, der bei der Erweiterung eines entsprechenden Frameworks normalerweise deutlich geringer ausf¨allt. Die Integration zwischen Testnotation und Implementierungssprache ist am besten, wenn beide identisch sind. Ansonsten sind die Konzepte der Implementierungssprache (zum Beispiel Attribute oder Methodenaufrufe) in der Testnotation in geeigneter Weise verfugbar ¨ zu machen, um in Tests auf sie zugreifen zu konnen. ¨
Aus diesen Grunden ¨ ist es nicht verwunderlich, dass in vielen und insbesondere agilen Softwareentwicklungsprojekten fur ¨ die Realisierung von Tests dieselbe Notation verwendet wird, wie zur Programmierung. In Projekten, die UML/P, also eine Kombination aus Modellierungstechniken der UML und Java nutzen, ist es deshalb von Vorteil, fur ¨ die Modellierung von Tests ebenfalls UML/P einzusetzen: • • •
• • •
Fur ¨ die Modellierung von Tests steht mit der UML/P eine abstrakte Notation zur Verfugung. ¨ Die Kombination der UML mit Java erlaubt es, nicht direkt in UML formulierbare Sonderf¨alle von Tests dennoch innerhalb eines integrierten Rahmens zu beschreiben. Der bereits genannte Aufwand zur Einarbeitung in eine neue Testnotation entf¨allt, beziehungsweise reduziert sich darauf, zu verstehen, wie die bereits zur Systementwicklung eingesetzte UML/P auch zur Testmodellierung eingesetzt wird. Die mentale Hurde ¨ zur Entwicklung von Tests in einer neuen Notation entf¨allt. Ein konzeptioneller Bruch zwischen Testnotation und Modellierungs- beziehungsweise Implementierungssprache existiert nicht. Es sind keine zus¨atzlichen notationellen oder technischen Kenntnisse zur Modellierung von Tests notwendig, so dass Entwickler grunds¨atzlich in der Lage sind, selbst Tests zu definieren.
Die UML/P vereinigt also die Vorteile einer abstrakten Notation fur ¨ Testf¨alle mit der guten Integration von Test- und Implementierungssprache. Durch diese integrierte Verwendung der UML/P ist es tats¨achlich realistisch,
160
5 Grundlagen des Testens
in kurzer Zeit parallel zur Entwicklung eine ausreichende Testsammlung zu erstellen, die die Qualit¨at der erstellten Software demonstriert. Deshalb ist es nicht uberraschend, ¨ dass nicht nur bei der Entwicklung von Gesch¨aftssoftware, sondern auch bei der Entwicklung eingebetteter Systeme, wie etwa bei Telekommunikationssystemen, verst¨arkt Java parallel zu Testnotationen, wie etwa MSC, zum Einsatz kommen.8 Da ein Test meistens aus mehreren UML-Diagrammen besteht, ist dennoch ein Stuck ¨ zus¨atzlicher Syntax notwendig, um Tests in kompakter Form zu definieren. Der hier dargestellte Vorschlag beschr¨ankt sich auf die Referenzierung von UML-Diagrammen, OCL-Bedingungen und der Einbindung von Java-Code, um Teile des Tests zu formulieren. Abbildung 5.6 zeigt das vollst¨andige Schema fur ¨ die Definition von Tests. Jeweils ungenutzte Anteile konnen ¨ entfallen. Test
test Testling, z.B. Methode oder Klasse z.B. Auction.bid { name: Generierungsziel fur ¨ den Test, z.B. AuctionTest.testBid testdata: Objektdiagramme bereiten den Testdatensatz vor tune: Java-Code erlaubt individuelle, zus¨atzliche Anpassung der Testdaten driver: Java-Methodenaufruf(e) | Sequenzdiagramm methodspec: OCL-Methodenspezifikationen werden bei einem Methodenaufruf gepruft ¨ interaction: Sequenzdiagramme werden als Ablaufbeschreibungen gepruft ¨ oracle: Java-Methodenaufruf | Statechart produziert vergleichbare Orakelergebnisse comparator: Java-Code | OCL-Code vergleicht Testergebnis mit Orakelergebnis ¨ Default ist Ubereinstimmung von Struktur und Attributinhalten statechart: Statechart for Objektname from Anfangszustand to { Zielzust¨ande } Der Test bewirkt Transitionsuberg¨ ¨ ange im genannten Objekt vom Anfangszustand in einen der Zielzust¨ande assert: Objektdiagramme | OCL-Bedingungen | Java-Prufcode ¨ Boolesche Bedingungen uber ¨ das Testergebnis cleanup: Java-Code r¨aumt benutzte Ressourcen auf } Abbildung 5.6. Schablone fur ¨ die Definition eines Tests
Die einzelnen Bestandteile werden im weiteren Verlauf dieses Kapitels diskutiert. Dabei wird auch eine tabellenartige Variante dieses Schemas verwendet, die fur ¨ die ubersichtliche ¨ Definition mehrerer Tests geeignet ist. 8
Beispielsweise nutzt Ericsson Java zur Modellierung von Funktionstests im Mobilfunkbereich und TTCN [ISO92] nur fur ¨ die Protokollvalidierung.
5.2 Definition von Testf¨allen
161
5.2 Definition von Testf¨allen 5.2.1 Operative Umsetzung eines Testfalls Ein Testfall besteht laut Definition aus einem Satz von Testdaten, einer Beschreibung des Ausgangszustands des Testlings und seiner Umgebung und dem Sollergebnis des Tests. Um einen Testfall operativ umzusetzen, ist ein ausfuhr¨ barer Testtreiber zu realisieren. Ein Testtreiber hat die Aufgabe den Testling inklusive der Testdaten aufzubauen und in eine Testumgebung einzusetzen, damit der Testling so ablaufen kann, als w¨are er im Produktionssystem. Zugleich werden die fur ¨ den Vergleich mit dem Sollergebnis relevanten Daten des Istergebnisses gespeichert. Dabei nutzt der Testtreiber eine Instrumentierung des Testlings, die ihm den notwendigen Zugriff auf die Daten des Testlings erlaubt, und die in Abschnitt 7.1 beschriebenen Dummy-Objekte, um die Umgebung des Testlings zu simulieren. Seiteneffekte wie Datenbankzugriff, Bildschirmausgabe oder Internet-Kommunikation werden dabei in Dummies abgefangen und gegebenenfalls protokolliert. Abbildung 5.7 demonstriert die fur ¨ einfache Testtreiber typische Vorgehensweise. In Abschnitt 6.4 wird diese verfeinert, indem statt nur einer Methode eine Methodensequenz ausgefuhrt ¨ und die Reihenfolge der internen Abl¨aufe dabei beobachtet wird.
Abbildung 5.7. Struktur eines einfachen Testtreibers
In Abbildung 5.7 ist eine Objektstruktur aus vier Objekten dargestellt, die als Testdatensatz an die aufgerufene Methode ubergeben ¨ wird. Die aufgerufene Methode ist normalerweise selbst einem dieser Objekte (zum Beispiel o1) zugeordnet. Die weiteren Objekte o2 bis o4 sind notwendig, da die aufgerufene Methode beziehungsweise davon abh¨angige Methoden auf diese Objekte zugreifen und diese gegebenenfalls ver¨andern. Weitere Objekte u1,
162
5 Grundlagen des Testens
u2 und u3 sind notwendig, um den Zugriff auf die Umgebung zu erlauben. Die Umgebung wird dabei durch eine Simulation mit den in Abschnitt 7.1 diskutierten Dummies ersetzt. Je nach Testziel wird eine unterschiedliche Abgrenzung zwischen dem Testling und der simulierten Testumgebung vorgenommen. Bei einem Methodentest besteht der Testling tats¨achlich nur aus der aufgerufenen Methode. Deshalb ist es hier meist sinnvoll, bereits die direkte Umgebung durch Dummies zu ersetzen und gegebenenfalls sogar andere Methoden derselben Klasse zu simulieren. Auch bei Klassentests werden typischerweise die Objekte der Umgebung durch Dummies ersetzt. Fur ¨ Integrations- und Systemtests wird stattdessen moglichst ¨ wenig, also oft nur die echte Systemgrenze ersetzt. Fur ¨ die operative Umsetzung eines Testfalls wird ein Testtreiber benotigt, ¨ der letztendlich in einer Testmethode realisiert wird. Ein Testtreiber besteht aus drei wesentlichen Phasen: 1. Die fur ¨ den Test notwendige Objektstruktur einschließlich der Dummies fur ¨ die Umgebung wird aufgebaut. 2. Der Testling wird zur Ausfuhrung ¨ gebracht. Meist wird dabei eine einzelne Methode aufgerufen. 3. Das erhaltene Istergebnis wird mit dem erwarteten Sollergebnis verglichen. Dabei besteht das erhaltene Istergebnis, wie in Abbildung 5.7 illustriert, aus der resultierenden Objektstruktur, den gegebenenfalls in den Dummies zu findenden Zugriffsprotokollen auf die simulierte Umgebung und zus¨atzlich aus einem return-Wert der getesteten Methode. ¨ Ist fur ¨ einen Testfall das Offnen einer Datei oder einer Internet-Verbindung, die Anpassung einer Datenbank oder a¨ hnliches mehr notwendig, so mussen ¨ diese am Ende des Tests aufger¨aumt werden. In C++ gehort ¨ dazu zum Beispiel auch die Freigabe angelegter Objektstrukturen. Das mittlerweile fur ¨ viele Programmiersprachen verfugbare ¨ Java-Framework JUnit [JUn02, BG98, BG99, HL02] bietet eine hervorragende Unterstut¨ zung zur Definition von Tests. Eine angepasste Fassung des Frameworks wurde deshalb auch im Auktionsprojekt erfolgreich eingesetzt. JUnit bietet eine Infrastruktur und methodische Richtlinien zur Definition von Testdaten und Durchfuhrung ¨ von Tests in der in Abbildung 5.7 gezeigten Form. Der Vergleich des erhaltenen Istergebnisses mit dem erwarteten Sollergebnis kann auf viele Weisen durchgefuhrt ¨ werden und h¨angt davon ab, in welcher Form und wie detailliert das Sollergebnis vorhanden ist. Im Auktionsprojekt wurde dazu eine zus¨atzliche Komponente VUnit“ entwickelt, ” die fur ¨ eine bestimmte Form von Vergleichen geeignet ist. Weitere Verfahren zur effizienten Darstellung von Vergleichen sind im nachfolgenden Abschnitt 5.2.2 beschrieben. Neben internen Testtreibern gibt es Ans¨atze, Testtreiber als Skripte außerhalb des eigentlichen Systems zu definieren. Die Testdaten und -ergebnisse sind dann typischerweise in Dateien abgelegt. Der Vergleich von Soll- und Istergebnis reduziert sich dann auf einen Dateivergleich. Die Nutzung externer
5.2 Definition von Testf¨allen
163
Testdaten und -treiber stellt nach heutiger Technik vor allem fur ¨ dateiorientierte Systeme wie etwa Compiler, Generatoren oder XML-Transformatoren eine gute Erg¨anzung fur ¨ interne Testf¨alle dar, ist aber nur fur ¨ Systemtests wirklich geeignet. In [Den91] wird eine weitere Variante fur ¨ ein Testsystem vorgeschlagen, das nur die Ergebnisse extern in Dateien oder einer Datenbank ablegt und dort vergleicht, w¨ahrend die Testtreiber in der Programmiersprache des Systems erstellt werden. 5.2.2 Vergleich der Testergebnisse Vergleichsmuster Neben der Erstellung von Testdaten ist der Vergleich von Soll- und Istergebnis von besonderer Komplexit¨at. Die ad¨aquate Unterstutzung ¨ des Entwicklers zur Analyse des Istergebnisses eines Tests durch Vergleich mit ei¨ nem expliziten Sollergebnis, Uberpr ufung ¨ einer Invariante oder a¨ hnlichen Techniken hat wesentliche Auswirkungen auf die Effizienz bei der Testentwicklung. Dabei ist einerseits zu beachten, dass dieser Vergleich moglichst ¨ schnell und kompakt formuliert werden kann. Wichtig ist aber genauso, dass ¨ er eine gewisse Widerstandsf¨ahigkeit gegenuber ¨ Anderungen von Elementen des Systems hat, die im Test zwar genutzt werden, aber eigentlich nicht ¨ Teil des Tests sind. Dafur ¨ bieten sich Aquivalenzvergleiche oder Abstraktionen an. Deshalb sind Vergleichsmuster hilfreich, die jeweils richtige Form des Vergleichs zu w¨ahlen. Zun¨achst wird in Abbildung 5.8 mit mathematischen Mitteln eine Klassifizierung moglicher ¨ Vergleiche vorgenommen. Diese in der Theorie a¨ hnlichen Techniken haben bei der anschließend besprochenen praktischen Umsetzung sehr unterschiedliche Auswirkungen. Vorteile und Nachteile der Vergleichsmuster Der Differenzvergleich P (x, f (x)) kann gewunschte ¨ Ver¨anderungen prufen ¨ und unerwunschte ¨ Ver¨anderungen erkennen. Aufgrund der Parametrisierung von P kann der Vergleich auf verschiedene Ausgangsdatens¨atze x angewandt werden, auf denen f ein uniformes Verhalten besitzen soll. Beispiel ist etwa die Prufung, ¨ ob ein Z¨ahler erhoht ¨ wurde. Solche Vergleiche konnen ¨ durch OCL-Methodenspezifikationen geeignet beschrieben werden. Die Eigenschaftsprufung ¨ ermittelt zum Beispiel, ob Invarianten eingehalten wurden, oder ob bestimmte Attribute mit bestimmten Werten belegt sind. Meistens wird das Istergebnis nur selektiv gepruft ¨ und damit eine Ab¨ straktion vorgenommen. Dies hat den Vorteil, dass bei einer Anderung des Systems an einer, fur ¨ den Testfall nicht relevanten Stelle, der Testfall immer noch erfolgreich ist. Als Spezialfall der Eigenschaftsprufung ¨ benutzt ¨ der Aquivalenzvergleich die explizite Darstellung y des Sollergebnisses. Beim ¨ Vergleich kann durch die Verwendung der Aquivalenz zum Beispiel auf die Prufung ¨ irrelevanter Attribute oder Reihenfolgen in Containern verzichtet
164
5 Grundlagen des Testens
Seien A und B die Mengen behandelter Objektstrukturen (eine Objektstruktur beinhaltet im Allgemeinen mehrere Objekte). Eine transformierende Methode f wird als Funktionen f : A → B verstanden. P stellt ein Pr¨adikat dar. Vereinfachend seien Parameter und Ergebnis in A beziehungsweise B enthalten. Es lassen sich folgende Vergleichsmuster identifizieren und damit das Sollergebnis explizit oder implizit darstellen, wobei diese Muster teilweise ihre Vorg¨anger spezialisieren. Es sei x ∈ A: Differenzvergleich P (x, f (x)). Der Vor-/Nachvergleich beschreibt den Vergleich zwischen Ausgangsstruktur x und Istergebnis f (x) auf Basis eines Vergleichspr¨adikats P . Eigenschaftsprufung ¨ P (f (x)). Mit der Eigenschaftsprufung ¨ wird eine von der Ausgangsstruktur nicht direkt abh¨angige Eigenschaft gepruft. ¨ Dazu geh¨oren Invarianten, aber auch spezifische Tests uber ¨ einzelne Attribute und Attributkombinationen des Istergebnisses. ¨ ¨ Aquivalenzvergleich f (x) ≡ y mit explizit vorgegebenem y ∈ B und einer Aqui¨ valenz ≡, die nur relevante Aspekte vergleicht. Eine einfach realisierbare Aquivalenz ist die strukturelle und wertem¨aßige Gleichheit. Vergleich nach Abstraktion Ab(f (x)) = Ab(y) beziehungsweise Ab(f (x)) = z ist ¨ eine Variante des Aquivalenztests mit einer explizit dargestellten Abstraktion Ab : B → Z und z ∈ Z. Ein einfaches Beispiel fur ¨ eine Abstraktion ist der selektive Vergleich von Attributen eines Objekts, der zum Beispiel in der Methode equal gelegentlich umgesetzt wird. Identit¨atsprufung ¨ f −1 (f (x)) = x, wobei f invertierbar sein muss, um den Ursprungszustand wieder herstellen zu konnen. ¨ Prufung ¨ mit Orakelfunktion g(x) = f (x), indem eine zweite, von f unabh¨angige Realisierung g existiert, die dieselbe Funktionalit¨at implementiert. Diese Vergleichstechniken lassen sich kombinieren. Wenn zum Beispiel eine Orakelfunktion nur ein a¨ quivalentes, aber kein identisches Ergebnis produziert, ist ein ¨ Vergleich der Form g(x) ≡ f (x) mit einer geeigneten Aquivalenz sinnvoll. Abbildung 5.8. Vergleichsmuster fur ¨ Testf¨alle
werden. Mathematisch a¨ quivalent, aber in seiner Umsetzung unterschiedlich ist die Verwendung einer Abstraktion.9 Die Identit¨atsprufung ¨ ist nur unter eng begrenzten Umst¨anden moglich, ¨ wenn (a) eine Invertierbarkeit der Funktion vorliegt und (b) die Umkehrfunktion f −1 mit vernunftigem ¨ Aufwand realisierbar ist. Idealerweise ist die Umkehrfunktion f −1 ebenfalls in dem zu testenden System vorhanden und bereits durch andere Testf¨alle gepruft ¨ worden. Die Verwendung einer alternativen Implementierung, auch Orakelfunktion genannt, bietet sich zum Beispiel dann an, wenn eine bereits vorhandene Implementierung in einem Refactoring durch eine verbesserte Fassung ersetzt werden soll. Auf diese Weise kann zum Beispiel ein Sortieralgorith9
¨ Eine Abstraktion Ab definiert eine Aquivalenz durch x ≡ y ⇔ Ab(x) = Ab(y).
5.2 Definition von Testf¨allen
165
mus ausgetauscht werden. Eine andere Moglichkeit ¨ bietet die Verwendung einer ausfuhrbaren ¨ Spezifikation als Orakel, wenn diese nicht bereits fur ¨ die Implementierung verwendet wurde. Statecharts sind dafur ¨ beispielsweise geeignet. Wenn das durch die Orakelfunktion erhaltene Ergebnis vom Testergebnis in bestimmten Details abweicht, so kann eine explizite Vergleichs¨ funktion angegeben werden, die statt der Gleichheit eine Aquivalenz pruft. ¨ Anforderungen fur ¨ eine operative Umsetzung Aus der mathematischen Charakterisierung der Vergleichsmuster der Abbildung 5.8 l¨asst sich ableiten, welche Funktionalit¨at ein Framework zur Verfugung ¨ stellen sollte, das deren operative Umsetzung unterstutzt. ¨ Die Framework-Funktionalit¨at wird als Teil der UML-Laufzeitumgebung nach Abbildung 3.6 verstanden. Beim Differenzvergleich P (x, f (x)) sind nach Testdurchfuhrung ¨ die Ausgangsstruktur x und das Istergebnis f (x) gleichzeitig zur Verfugung ¨ zu stellen. Vor Ausfuhrung ¨ von f ist daher eine Kopie (engl.: clone) der Ausgangsdaten anzulegen. Wenn ein Vergleich scheitert, so ist eine ausfuhrliche, ¨ aber ubersichtliche ¨ Ausgabe des Istergebnisses f (x) zur Analyse des Fehlers notwendig. Sowohl die Kopierfunktionalit¨at als auch die Ausgabe mussen ¨ das zugrunde liegende Objektgeflecht bearbeiten. ¨ Der Aquivalenzvergleich f (x) ≡ y benotigt ¨ eine Implementierung des Vergleichsoperators. Die von Java standardm¨aßig zur Verfugung ¨ gestellten Vergleichsoperatoren == und equals() sind dafur ¨ nur bedingt nutzbar. Der Operator == vergleicht fur ¨ Objekte nur Objektidentit¨at und ist daher fur ¨ inhaltliche Vergleiche nicht nutzbar. equals() ist vom Entwickler fur ¨ jede Klasse neu zu definieren, wird aber bereits im Produktionssystem benutzt. Zum Beispiel benutzen einige der Containerstrukturen den equals()-Operator. Deshalb kann die im Produktionssystem gewunschte ¨ Funktionalit¨at dieses Operators inkompatibel zum Verhalten bei einem Testvergleich sein. Daruber ¨ hinaus konnen ¨ fur ¨ verschiedene Tests jeweils individuelle Vergleiche ≡ notwendig sein. Beispielsweise konnen ¨ Vector-Objekte als Realisierungen von Sequenzen oder Mengen, also mit oder ohne Respektierung der Reihenfolge, zu vergleichen sein oder fur ¨ den Test einer Anwendungsklasse ist nur eine Teilmenge der Attribute relevant. Es ist deshalb eine Unterstutzung ¨ zur flexiblen Definition von Vergleichen fur ¨ Objektstrukturen wunschenswert, ¨ die auch rekursive oder mit Zyklen behaftete Objektstrukturen vergleichen kann. Scheitert der Test, so sollte hier die rekursive Ausgabe des Istergebnisses f (x) zus¨atzlich mit Markierungen der Unterschiede zum Sollergebnis y versehen sein. Abstraktionen der Form Ab(f (x)) = Ab(y) beziehungsweise Ab(f (x)) = z vor einem Vergleich sind zum Beispiel die Selektion einzelner Attribute, eines einzelnen Objekts oder einer Teilstruktur. Auch die oben beschriebene Definition einer Vergleichsoperation fuhrt ¨ implizit eine Abstraktion durch, ohne die abstrahierte Datenstruktur explizit zu berechnen und ist damit ein
166
5 Grundlagen des Testens
effizienter Weg, eine fur ¨ einen Vergleich vorgesehene Abstraktion zu codieren. In manchen F¨allen ist es sinnvoll, stattdessen nicht zu vergleichende Attributwerte und Links zu loschen ¨ oder eine Umformung in eine Normalform zu berechnen. So konnen ¨ zum Beispiel zwei Reihungen, die als Mengen zu interpretieren sind, zun¨achst sortiert und dann elementweise verglichen werden. Die beiden letzten Varianten, die Identit¨atsprufung ¨ f −1 (f (x)) = x und die Prufung ¨ mithilfe einer Orakelfunktion g(x) = f (x), basieren auf dem Vorhandensein entsprechender Funktionalit¨at. Die Funktion g stellt eine Form des Orakels dar. Orakel werden in [Bin99] genauer diskutiert. Dort wird richtigerweise angemerkt, dass es perfekte Orakel nicht geben kann. Gleichzeitig werden dort aber 15 Muster angegeben, um Orakel zu realisieren. 5.2.3 Werkzeuge JUnit und VUnit Aus der bisherigen Diskussion dieses Abschnitts l¨asst sich erkennen, dass die Unterstutzung ¨ der Definition von Tests durch geeignete Funktionalit¨at wichtig ist. Dabei kann diese Funktionalit¨at durch Werkzeuge wie Generatoren oder Analysatoren oder durch ein Framework erbracht werden. Idealerweise wird sogar beides kombiniert, indem ein Generator Testcode generiert, der mit dem Framework zusammenarbeitet. Das Framework kann dann als Bestandteil der UML-Laufzeitumgebung angesehen werden. Die beiden nachfolgend beschriebenen Frameworks JUnit [JUn02, BG98, BG99, HL02] und das im Auktionsprojekt entstandene, JUnit erg¨anzende VUnit liefern gemeinsam die geforderte Funktionalit¨at. Nachfolgend werden die Moglich¨ keiten beider Frameworks kurz vorgestellt, um daraus eine Verwendung fur ¨ UML/P abzuleiten. Testframework JUnit In Abbildung 5.9 ist ein Ausschnitt eines in Java formulierten und mit JUnit durchgefuhrten ¨ Tests der Methode bid der Klasse Auction zu sehen. In einer Aufbauphase (1) werden alle Objekte erzeugt, die fur ¨ den Test notwendig sind. Dies konnen ¨ wie in den F¨allen Auktion und Person komplexere Objektstrukturen sein, die auch noch untereinander verbunden sind. Deshalb wird diese erste Phase oft in eine eigene Methode mit dem Namen setUp ausgelagert. Die Anzahl der in diesem Beispiel notwendigen Objekte ist bereits relativ hoch. Um eine mo¨ glichst große Effizienz des Testablaufs zu erreichen, ist es nach [LF02] sinnvoll moglichst ¨ kleine Objektstrukturen zu nutzen. Die Durchfuhrung ¨ des Tests (2) besteht aus einem einfachen Methodenaufruf. Dann werden die entstandenen Ergebnisse verglichen (3), wobei die Methoden assertX zur Festlegung und zur Meldung eines Erfolgs oder
5.2 Definition von Testf¨allen
167
public class AuctionTest extends TestCase { Java public void testBidSimple() { // (1) Aufbau der Objektstruktur Auction auction = new Auction(...); Person person = new Person(...); Time time = new Time("14:42:22", "Feb 21 2000"); Money money = new Money("552000", "$US", 2); // weitere Objekte sind notwendig, um die Struktur zu komplettieren // (2) Durchfuhrung ¨ des Tests boolean result = auction.bid(person,time,money); // (3) Prufen ¨ der Ergebnisse // result==true: Gebot war erfolgreich assertTrue(result); // das abgegebene Gebot ist derzeit das Beste assertEquals(money, auction.getBestBid()); // Das Auktionsende wurde auf time + extensiontime festgelegt Time expectedClosingTime = new Time("14:45:22", "Feb 21 2000"); assertEquals(expectedClosingTime, auction.getCurrentClosingTime());
}
} ... // z.B. Konstruktor
Abbildung 5.9. Testtreiber fur ¨ Methode bid in JUnit
Scheiterns10 des Tests verwendet werden konnen. ¨ Diese Methoden werden aus der Klasse TestCase geerbt. St¨arken besitzt JUnit im Management und der Kombination von Testsammlungen, indem es einfache Mechanismen zur Erstellung einer Sammlung von Tests (engl.: test suite) und zur individuellen Ausfuhrung ¨ solcher Sammlungen zur Verfugung ¨ stellt. JUnit ist ein trickreich implementiertes Framework, das mit wenig Code und sehr wenigen, vom Anwender zu verstehenden Klassen durch geschickte Bildung von Unterklassen und Interface-Implementierung effektiv zur Definition von Testf¨allen genutzt werden kann. Die Handhabung von JUnit ist bereits deshalb einfach, weil die Implementierungssprache und die Sprache zur Testdefinition identisch sind. Aus diesem Grund konnen ¨ auch dieselben Werkzeuge, wie etwa Versionsverwaltung und Entwicklungsumgebung, verwendet werden. JUnit stellt ein gelungenes Beispiel fur ¨ ein Entwicklungswerkzeug dar, das als 10
JUnit verwendet den Begriff des Failure“ fur ¨ ein Scheitern des Tests und Error“ ” ” fur ¨ eine anormale Terminierung der Testdurchfuhrung ¨ durch Exception.
168
5 Grundlagen des Testens
Framework in der Implementierungssprache selbst realisiert ist und damit auf einfache und elegante Weise eine enge Verbindung zwischen Werkzeug und Implementierung schafft. Vergleich, Kopieren und Ausgabe mit VUnit In der Einfachheit von JUnit liegen einige St¨arken, aber auch Erweiterungspotential. VUnit ist eine solche Erweiterung und wurde im Auktionsprojekt entwickelt, um das Vergleichen, das Kopieren sowie die Ausgabe von Objektstrukturen effizienter zu gestalten. VUnit basiert im Wesentlichen auf der Nutzung von reflektiven Mechanismen zur Bearbeitung und Inspektion von Objektstrukturen. Eine flexible Methode zum Kopieren von Objektstrukturen ermoglicht ¨ das so genannte deep cloning“, das die Behandlung von Zyklen einschließt ” und per Angabe durch Attribut oder Parameter erlaubt, bestimmte Objekte vom Kopieren auszunehmen. Um volle Flexibilit¨at zu ermoglichen ¨ kann eine eigene Clone-Methode fur ¨ die Objekte einer Klasse angegeben werden. 11 Die Aufbereitung einer Objektstruktur zur Ausgabe wird mit a¨ hnlichen Mechanismen und analoger Flexibilit¨at zur Verfugung ¨ gestellt. Sie dient dazu, Testdatens¨atze von Objektstrukturen in textuell lesbarer Form darzustellen. Sinnvoll kann hier aber auch das Reverse Engineering einer Objektstruktur in ein Objektdiagramm sein. JUnit bietet in der Version 3.7 insgesamt 28 verschiedene Varianten von assertX-Methoden, darunter assertEqual-Varianten, die die Vergleichsmethode equals der verglichenen Objektstrukturen verwenden. VUnit bietet eine zus¨atzliche Vergleichsmethode, die ebenfalls flexibel parametrisiert ¨ ist. So kann ein Aquivalenzvergleich durchgefuhrt ¨ werden, bei dem von einzelnen Attributinhalten, Links oder Containerstrukturen abstrahiert wird. Das Ergebnis eines Vergleichs ist die Liste der Unterschiede zweier Objektstrukturen. Es ist also nicht das gesamte erwartete Ergebnis, sondern nur die Menge der Unterschiede zu beschreiben. Der in Abbildung 5.9 beschriebene Testfall kann unter zus¨atzlicher Verwendung von VUnit damit wie in Abbildung 5.10 dargestellt werden. Dieser Testtreiber pruft ¨ aber zus¨atzlich, dass neben den erwarteten Ver¨anderungen keine weiteren Ver¨anderungen vorgenom¨ ¨ men wurden. Die Verwendung einer Anderungsliste erlaubt es, alle Anderungen in einem von JUnit gemeldeten Fehler abzulegen. Die direkte Verwendung von VUnit ist vor allem von Interesse, wenn nur die Testdaten, wie nachfolgend beschrieben, nicht aber das Sollergebnis mit einem Objektdiagramm modelliert werden. 11
Diese Clone-Methode ist nicht mit dem aus Java bekannten clone identisch, um Verflechtungen mit einer Nutzung derselben Methode im Produktionssystem zu vermeiden.
5.2 Definition von Testf¨allen public class AuctionTest extends VUnitTestCase { public void testBidSimple() { // (1) Aufbau der Objektstruktur ... (wie oben)
169 Java
// (1b) Kopie der Objektstruktur anlegen Auction auctionOld = (Auction)vclone(auction); // (2) Durchfuhrung ¨ des Tests ... (wie oben) // (3) Prufen ¨ der Ergebnisse // result==true: Gebot war erfolgreich assertTrue(result);
}
}
// Liste der Unterschiede in der Form: objekt.attributkette = wert // (Reihenfolge egal, assertExact ist eine Funktion aus VUnit) Time time = new Time("14:45:22", "Feb 21 2000"); assertExact(auctionOld, auction, new String[] { "bidPol.min.amount=55200000", "bidPol.min.currency=$US", "bidPol.min.decimalplaces=2", "timePol.currentClosingTime.timeSec="+time.timeSec });
Abbildung 5.10. Test fur ¨ Methode bid in JUnit und VUnit
6 Modellbasierte Tests
Testen ist eine extrem kreative und intellektuell herausfordernde Aufgabe. Glenford J. Myers, [Mye00]
Auf Basis des vorangegangenen Kapitels 5 stehen in diesem Kapitel praktische Fragestellungen zur Umsetzung von Tests mit der UML im Vordergrund. Dabei wird demonstriert, wie unter Nutzung der UML/P effizient Testf¨alle definiert werden und welche Diagrammarten sich dafur ¨ eignen.
6.1 6.2 6.3 6.4 6.5 6.6
Testdaten und Sollergebnis mit Objektdiagrammen . . . Invarianten als Codeinstrumentierungen . . . . . . . . . . . . . . Methodenspezifikationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sequenzdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statecharts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung und offene Punkte beim Testen . . . .
172 175 177 182 189 201
172
6 Modellbasierte Tests
Nach der Einfuhrung ¨ der beim Testen eingesetzten Terminologie und der Diskussion der Probleme und ihrer Losungsans¨ ¨ atze unter Nutzung von Modellen wird in diesem Kapitel auf die konkreten Ans¨atze zum Einsatz der Notationen der UML/P zur Definition und Entwicklung von Testf¨allen eingegangen. Die Definition von Testf¨allen mit der UML/P basiert auf der Umsetzung der Diagramme und der OCL in Codeteile in Kapitel 4, die zu Testf¨allen und Konsistenzprufungen ¨ zusammengesetzt werden. Darauf aufbauend wird die Testfallmodellierung mit Objektdiagrammen in Abschnitt 6.1, mit OCLInvarianten in Abschnitt 6.2, mit Methodenspezifikationen in Abschnitt 6.3, mit Sequenzdiagrammen in Abschnitt 6.4 und mit Statecharts in Abschnitt 6.5 anhand von Beispielen demonstriert.
6.1 Testdaten und Sollergebnis mit Objektdiagrammen Abbildung 6.1 zeigt den sich aus der in Abbildung 5.7 ergebenden Einsatz von zwei Objektdiagrammen zur Darstellung der Testdaten und des Sollergebnisses. Damit ein Objektdiagramm fur ¨ mehrere Testf¨alle verwendet werden kann, sind gegebenenfalls kleinere Anpassungen fur ¨ einen spezifischen Testfall notwendig. Deshalb ist es moglich, ¨ nach Aufbau der entsprechenden Objektstruktur zus¨atzlich Java-Code einzubauen.
Abbildung 6.1. Standardform des Tests mit Objektdiagrammen, OCL und JavaTesttreiber
Nach Ausfuhrung ¨ des Testlings steht die Istdatenstruktur zur Verfugung ¨ und kann mit der durch das zweite Objektdiagramm vorgegebenen Solldatenstruktur verglichen werden. Zus¨atzliche Eigenschaften konnen ¨ durch OCL-Bedingungen gepruft ¨ werden. Dies konnen ¨ allgemein gultige ¨ OCLInvarianten sein, die immer gepruft ¨ werden sollten, aber auch fur ¨ den Test spezifische Bedingungen. Einer der typischen im Auktionssystem verwendeten Testdatens¨atze besteht aus einem Objekt der Klasse AllData, mehreren Auktionen mit allen
6.1 Testdaten und Sollergebnis mit Objektdiagrammen
173
Abbildung 6.2. Testdatensatz als Objektdiagramm
davon abh¨angigen Objekten, mehreren Personen, die in unterschiedlichen Situationen angemeldet sind, und einer Reihe von Nachrichten, die einigen offenen und abgelaufenen Auktionen zugeordnet sind. Von diesen Grundstrukturen sind nur relativ wenige Datens¨atze notwendig, denn durch Anpassung mit zus¨atzlichem Java-Code lassen sich diese fur ¨ viele Testf¨alle wiederverwenden. Auf dem in Abbildung 6.2 dargestellten Testdatensatz wird der Effekt der Methode zum Eroffnen ¨ einer Auktion start() durch das Sollergebnis (ausschnittsweise) in Abbildung 6.3 beschrieben.
context Auction a inv NoBidYet: { m in a1213.message | m instanceof BidMessage } == Set{}
OCL
Abbildung 6.3. Sollergebnis als Objektdiagramm
Neben der als NoBidYet angegebenen OCL-Bedingung, die besagt, dass nach der Eroffnung ¨ noch kein Gebot abgegeben ist, existieren auch einzuhaltende Invarianten. Beispielsweise ist Bidders1 fur ¨ Auktionen allgemeingultig: ¨ context Auction a inv Bidders1: a.activeParticipants <= a.bidder.size
OCL
und muss naturlich ¨ auch nach der Eroffnung ¨ einer Auktion gelten. Wird eine Codegenerierung fur ¨ die beschriebenen Diagramme nach Kapitel 4 zu-
174
6 Modellbasierte Tests
grunde gelegt, so kann ein Test mit der in Anhang B, Band 1 beschriebenen Java/P-Erweiterung wie folgt formuliert werden: testStart() { Auktion a1213 = setupYetClosed(); a1213.start(); ocl isStructuredAsRunning(a1213); ocl checkNoBidYet(a1213); ocl checkBidders1(a1213); ocl checkTime1(a1213); }
Java/P
// Testdaten erzeugen // Test ausfuhren ¨ // Sollergebnis erfullt? ¨ // Eigenschaft NoBidYet // Invariante Bidders1 // weitere Invariante Time1
Die UML/P bietet eine eigenst¨andige Dokumentart an, mit der diese Tests kompakter formuliert werden konnen: ¨ test Auction.start() { Test testdata: OD YetClosed; driver: a1213.start(); assert: OD Running; inv NoBidYet; inv Bidders1; inv Time1; }
Um die F¨ahigkeiten der beiden oben beschriebenen Frameworks JUnit und VUnit richtig zu nutzen, ist allerdings eine angepasste Codegenerierung fur ¨ die pr¨adikative Abfrage isStructuredAs auf Basis von Objektdiagrammen und check fur ¨ OCL-Bedingungen sinnvoll. Dabei entsteht nicht nur eine boolesche Aussage, sondern im Fehlerfall eine bei JUnit ubliche ¨ Beschreibung des Istergebnisses, die idealerweise den Namen der verletzten OCL-Bedingung beziehungsweise Namen und Inhalt des vom Sollergebnis abweichenden Objekts und Attributs beschreibt. In Abbildung 6.3 wurde das Sollergebnis ebenso detailliert dargestellt wie die Testdaten. Dies muss im Allgemeinen nicht der Fall sein. W¨ahrend das Objektdiagramm fur ¨ die Testdaten eine gewisse Vollst¨andigkeit benotigt, ¨ um damit konstruktiv Objektstrukturen bilden zu konnen, ¨ kann das Sollergebnis abstrakt sein und nur den Teil der Objektstruktur darstellen, der fur ¨ den behandelten Testfall von Interesse ist. Dabei konnen ¨ einzelne Attribute ausgelassen oder auch abgeleitete Attribute eingesetzt werden. Fehlt im Sollergebnis eine Teilstruktur der Objekte, so bedeutet das entsprechend der Semantik eines Objektdiagramms nicht, dass diese im Istergebnis zu loschen ¨ war, sondern dass ihre tats¨achliche Form nicht von Interesse ist. Durch ein Hinzufugen ¨ des Stereotyps complete aus Tabelle 4.15 kann gefordert werden, dass das Objektdiagramm als vollst¨andige Beschreibung einer Objektstruktur inklusive aller Links verstanden wird. Der Vergleich ist dann, wie in Abschnitt 4.2 beschrieben, wesentlich restriktiver. Allerdings mussen ¨ in diesem Objektdiagramm auch alle Attributwerte angegeben werden. Die in Abschnitt 5.3, Band 1 diskutierte Kombinierbarkeit von Objektdiagrammen, die durch OCL-Logikoperatoren gesteuert wird, kann bei der
6.2 Invarianten als Codeinstrumentierungen
175
Modellierung von Testdaten und von Sollergebnissen hilfreich zur Modularisierung eingesetzt werden. Nach dem in Abschnitt 4.2 diskutierten Verfahren konnen ¨ Objektdiagramme kombiniert und damit die Gesamtstruktur der Testdaten aus mehreren Einzeldiagrammen zusammengesetzt werden. Bei der Definition des Sollergebnisses kann die in Kapitel 5, Band 1 diskutierte, sehr flexible Kombinierbarkeit von Objektdiagrammen mithilfe der OCLOperatoren genutzt werden. Dabei kann zum Beispiel mit negierten Objektdiagrammen gepruft ¨ werden, dass eine bestimmte Situation nicht auftritt. Der hier vorgeschlagene Weg zur Modellierung von Testf¨allen mit der UML findet sich in ersten Ans¨atzen auch in [CCD01] wieder, wo ebenfalls Objektdiagramme zur Modellierung von Testdaten eingesetzt werden. Dort werden mehrere Stereotypen vergeben, um im Objektdiagramm direkt zu markieren, wofur ¨ es eingesetzt werden soll, wodurch jedoch die Wiederverwendbarkeit fur ¨ verschiedene Zwecke sinkt. Die Verwendung von Objektdiagrammen wird in Kapitel 7 anhand von Testmustern detailliert demonstriert.
6.2 Invarianten als Codeinstrumentierungen Einer der Nachteile einer Testfallmodellierung im Stil der Abbildung 5.7 ist die fehlende Moglichkeit, ¨ w¨ahrend des Testablaufs Invarianten zu prufen. ¨ Wird zum Beispiel ein komplexer Algorithmus bearbeitet, so kann es sinnvoll sein, statt nur das Ergebnis zu prufen ¨ und damit auf interne Zwischenzust¨ande ruckschließen ¨ zu mussen, ¨ direkt die zwischendurch geltenden Eigenschaften zu formulieren und zu prufen. ¨ Zu diesem Zweck wurde in Anhang B, Band 1 die bereits mehrfach genutzte ocl-Anweisung mit einer OCL-Bedingung als zu prufende ¨ Zusicherung eingefuhrt. ¨ Erg¨anzt wird diese durch eine let-Anweisung zur Definition lokaler Variablen, die in sp¨ateren OCL-Invarianten verwendet werden konnen, ¨ um damit auf fruhere ¨ 1 Zust¨ande zuruckzugreifen. ¨ Das Beispiel in Abbildung 6.4 demonstriert die Verwendung von oclund let-Anweisungen anhand einer Methode der Klasse Auction zur Versendung von Nachrichten. Eine ocl-Anweisung ist, wie in Abbildung 6.4 gezeigt, mit einer OCLBedingung oder einer Referenz auf eine benannte OCL-Invariante versehen. Im Beispiel wurde auf die folgende OCL-Invariante Bezug genommen, die eine allgemeine Beziehung zwischen einer Auktion und der eine Nachricht empfangenden Person beschreibt: import Auction a, Person p inv MessagesDelivered: p in a.bidder implies 1
OCL
Java 1.4 bietet mit der assert-Anweisung eine a¨ hnliche Form fur ¨ Zusicherungen. Jedoch fehlt eine Analogie fur ¨ die let-Anweisung, die es erlaubt lokale Variablen nur zu Testzwecken einzufuhren. ¨
176
6 Modellbasierte Tests
class Auction { addMessage(Message m) { ocl !this.message.contains(m);
Java/P
let oldMessageSize = message.size; message.add(m); ocl message.size == oldMessageSize +1;
}}
for(Iterator(Person) ip = bidder.iterator(); ip.hasNext(); ) { Person p = ip.next(); p.addMessage(m); ocl MessagesDelivered(this,p); }
Abbildung 6.4. Zusicherungen als ocl-Anweisungen forall m in a.message: m in p.message
Da Objektdiagramme sich nach Abschnitt 5.4, Band 1 hervorragend als pr¨adikative Beschreibung eines Zustands eignen, lassen sich damit naturlich ¨ auch Objektdiagramme sowie die in Abschnitt 5.3, Band 1 diskutierte Kombination von Objektdiagrammen und der OCL als Zusicherungen einsetzen. Es ist auch moglich, ¨ lokale Variablen innerhalb von Schleifen zu definieren. Die mit let eingefuhrten ¨ Variablen sind zwar unver¨anderbar, haben aber denselben Sichtbarkeitsbereich wie normale lokale Java-Variablen und werden daher bei jedem Schleifendurchlauf neu belegt. So l¨asst sich zum Beispiel die Terminierung der in Abbildung 6.5 gegebenen (nicht ganz trivialen) Schleife prufen. ¨ int n = ...; while( n>0 ) { let nOld = n; if( n % 1 == 0 ) n = n/2; else n = n-1; // Verwendung von n . . . ocl nOld > n; // absteigende Werte ocl nOld > 0; // Begrenzung durch 0 } ocl n<=0; // Nach der Schleife gilt
Java/P
Abbildung 6.5. Zusicherungen, die die Terminierung zeigen
Zur Terminierung der Schleife in Abbildung 6.5 wird die stetig absteigende und nach unten durch 0 begrenzte Variable n verwendet, deren alter Wert in nOld zwischengespeichert wird.
6.3 Methodenspezifikationen
177
Die Erweiterung von Java um die beiden Anweisungen ist relativ stark angelehnt an die Zusicherungslogik, die sich derselben Techniken bedient, um Aussagen uber ¨ Programme zu beweisen. Tats¨achlich konnen ¨ die beschriebenen Hilfsmittel weit mehr als nur exemplarische Tests unterstutzen. ¨ Ein geeigneter Beweiskalkul ¨ fur ¨ Java, wie er zum Beispiel in [vO01, RWH01] diskutiert wird, kann damit verifizieren, dass Invarianten immer gelten. Allerdings ist in einem objektorientierten System die Entwicklung einer ausreichend vollst¨andigen Menge von Zusicherungen im Code, so dass ein Verifikationswerkzeug die Korrektheit prufen ¨ kann, sehr aufw¨andig. Ein solches Vorgehen kann aber vor allem fur ¨ spezifische Aufgabenstellungen, wie komplexe Algorithmen, Protokolle oder Kernelemente der Sicherheitsarchitektur von Interesse sein. Die Instrumentierung des Produktionscodes mit Prufungen ¨ fur ¨ Invarianten muss vom Compiler flexibel gehandhabt werden konnen. ¨ Im laufenden Betrieb sollte die Instrumentierung unterbleiben, im Probebetrieb eine fur ¨ den Anwender unsichtbare Instrumentierung mit Ausgabe im Hintergrund (Protokoll) moglich ¨ sein und im Testsystem eine fur ¨ Testf¨alle geeignete Instrumentierung durch Ausgabe und Fehleranzeige mittels JUnit erfolgen.
6.3 Methodenspezifikationen Eines der wesentlichen Anwendungsgebiete der OCL ist die Beschreibung des Verhaltens einzelner Methoden auf abstrakte Weise, indem fur ¨ die Methode eine Vorbedingung und eine Nachbedingung angegeben werden. Eine Methodenspezifikation kann als Codeinstrumentierung, als Teil eines Testfalls und als Ausgangspunkt zur Ableitung von Testdatens¨atzen dienen. 6.3.1 Methodenspezifikationen als Codeinstrumentierung Das Paar CC2pre/CC2post ist ein typisches Beispiel fur ¨ eine Methodenspezifikation. Es wurde aus Abschnitt 4.4.3, Band 1 ubernommen. ¨ Dessen Kontext ist bereits dort beschrieben: context Person.changeCompany(String name) OCL pre CC2pre: company.name != name && exists Company co: co.name == name post CC2post: company.name == name && company.employees == company.employees@pre +1 &&
[email protected] ==
[email protected]@pre -1
Eine typische Umsetzung dieser Methodenspezifikation ist die Instrumentierung des Produktionscodes analog der in Abschnitt 6.2 beschriebenen Verwendung von Zusicherungen. Abbildung 6.6 zeigt einen Methodenrumpf der Methode changeCompany fur ¨ einen der drei nachfolgend noch diskutierten F¨alle, der um ocl-Anweisungen angereichert wurde.
178
6 Modellbasierte Tests
class Person { changeCompany(String name) { // pre CC2pre: ocl company.name != name && exists Company co: co.name == name;
Java/P
// Methodenimplementierung Company oldCo = company; Company newCo = AllData.instance().getCompany(name); if(newCo==null) ... // Company existiert nicht company = newCo; newCo.employees++; oldCo.employees--; // post CC2post: ocl company.name == name && company.employees == company.employees@pre +1 &&
[email protected] ==
[email protected]@pre-1; }} Abbildung 6.6. Instrumentierung mit Vor-/Nachbedingung
Die ocl-Anweisungen und ihre OCL-Argumente werden, wie bereits in Abschnitt 6.2 diskutiert, in JUnit-f¨ahige Laufzeitprufungen ¨ umgesetzt. Die fur ¨ die Laufzeit problematische Existenzquantifizierung in der OCLBedingung kann effizienter gestaltet werden, indem die Methodenspezifikation so umgestaltet wird, dass statt der Existenzquantifizierung mittels letKonstrukt direkt das infrage kommende Company-Objekt festgelegt wird. 6.3.2 Methodenspezifikationen zur Testfallbestimmung Mit der obigen Umsetzung ist zwar die Methodenspezifikation zur Instrumentierung des Produktionscodes verwendet worden, ein Testfall beziehungsweise dessen operative Umsetzung in einen Testtreiber ist aber damit nicht entstanden. Die Entwicklung von Tests aus einem Vor-/Nachbedingungspaar ist generell nicht einfach. Jedoch lassen sich fur ¨ bestimmte Formen von Methoden aus der Struktur der Spezifikation geeignete Testdatens¨atze ableiten. Ausgehend von der disjunktiven Normalform der Vorbedingung kann eine Partitionierung vorgenommen werden, die es erfordert, pro erfullba¨ rer Klausel der Normalform einen Testfall zu definieren. In [BW02a, BW02b] wurde dies an einem Beispiel durch Transformation nach Isabelle/HOL [NPW02] vorgenommen um mit einem Verifikationswerkzeug zumindest teilweise automatisiert nicht erfullbare ¨ Anteile zu erkennen und zu eliminieren. Das Beispiel changeCompany mit seinen drei Spezifikationsteilen (siehe
6.3 Methodenspezifikationen
179
Abschnitt 4.4.3, Band 1) kann ebenfalls als Disjunktion verstanden werden. Diese Methode ist durch drei Vor-/Nachbedingungspaare beschrieben, die ¨ drei Aquivalenzklassen CC1pre, CC2pre und CC3pre von Eingaben festlegen: // Liste von Vorbedingungen als OCL-Teile context Person.changeCompany(String name) pre
CC1pre:
!exists Company co: co.name == name
pre
CC2pre:
company.name != name && exists Company co: co.name == name
pre
CC3pre:
company.name == name
OCL
¨ Diese Aquivalenzklassen sind paarweise disjunkt und partitionieren den gesamten moglichen ¨ Eingabebereich. Die Disjunktion der drei Bedingungen ergibt: CC1pre || CC2pre || CC3pre
<=>
true
OCL
¨ Zumindest ein Testfall sollte daher fur ¨ jede dieser drei Aquivalenzklassen ¨ zur Verfugung ¨ gestellt werden. Die Identifikation von Aquivalenzklassen fur ¨ Testdaten ist ein wesentlicher Schritt zur systematischen Entwicklung von Tests. Auf Basis einer manuellen oder werkzeuggestutzten ¨ Analyse der Spezifikation konnen ¨ interessante Testf¨alle ermittelt werden. Leider ist davon auszugehen, dass die Generierung von Testdatens¨atzen, in diesem Beispiel also Objektstrukturen, in denen jeweils eine der angegebenen Bedingungen gilt, nicht ohne weiteres automatisierbar ist. Beispielsweise ist die konstruktive Umsetzung des Existenzquantors exists x: P, also die Generierung von Code, der ein Objekt x erzeugt, das die Bedingung P erfullt, ¨ nur fur ¨ Spezialf¨alle von P losbar. ¨ Dennoch ist es hilfreich, bei der Analyse einer gegebenen Testsammlung einen Hinweis zu erhalten, wenn eine der angegebenen ¨ Aquivalenzklassen durch Tests nicht abgedeckt wird. Wie in [Mye00, Lig90, Bal98] beschrieben, kann die Partitionierung des Testdatenraums durch eine Analyse der Nachbedingungen verfeinert werden. Dazu wird folgende Spezifikation betrachtet: context int abs(int val) pre: true post: if (val>=0) then result==val else result==-val
OCL
Da die Vorbedingung true ist, kann sie fur ¨ keine Partitionierung des Testdatenraums genutzt werden. In diesem Fall kann aber eine Analyse der ¨ Nachbedingung helfen, die sofort zwei Aquivalenzklassen erkennen l¨asst: val>=0 und val<0. Eine weitere Moglichkeit ¨ zur Testfalldefinition bilden die standardm¨aßi¨ ge Aquivalenzklassenbildung und die Betrachtung von Grenzwertf¨allen.
180
6 Modellbasierte Tests
Dies eignet sich besonders fur ¨ die von der Programmiersprache angebotenen Datentypen. Zum Beispiel bietet es sich fur ¨ ganze Zahlen an, Testdaten aus Set{-n,-10,-2,-1,0,1,2,3,4,10,11,n+1} (fur ¨ ein großes n des Wertebereichs) zu verwenden, da meist kritische Sonderf¨alle im Bereich um die 0 zu finden sind. Fur ¨ Container-Datenstrukturen, boolesche Werte und Fließkommazahlen lassen sich a¨ hnliche Standards festlegen. Diese Standardf¨alle leiten sich aus der Erkenntnis her, dass in diesen Datentypen ausgezeichnete Werte existieren, bei denen die Implementierung einen anderen Pfad nimmt, als bei benachbarten Werten. Das Verfahren der Grenzwertanalyse [Mye00, Bal98] grenzt explizit solche Wertebereiche ein und deckt sie durch Testdatens¨atze auf jeder Seite der Grenze ab. Im Fall der Absolutfunktion ist die 0 eine solche Grenze und erfordert -1, 0, +1 als Testdaten. Meist ist jedoch die Feststellung der Grenzwerte komplexer, da Fallunterscheidungen die Parameter in Beziehung setzen konnen. ¨ Neben der Entwicklung von Black-Box-Tests aus der Spezifikation darf aber auch die Entwicklung von White-Box-Tests aus einer gegebenen Implementierung nicht vernachl¨assigt werden. Erst durch eine Analyse der Implementierung, in diesem Fall also vor allem der Java-Coderumpfe ¨ und Transitions-Aktionen, werden zus¨atzliche F¨alle offensichtlich, die in der Spezifikation eventuell nicht erkennbar waren. Hier sind klassische Techniken zur Testuberdeckung ¨ einzusetzen [Bei95, Bal98]. In manchen F¨allen l¨asst sich die Menge und Art notwendiger Testf¨alle aus einer Spezifikation nicht vorhersagen, weil es eine Reihe unterschiedlicher Implementierungen gibt. Dazu gehoren ¨ zum Beispiel Sortierverfahren, wie Mergesort, Quicksort oder Bubblesort, die jeweils sehr unterschiedlich funktionieren und deshalb unterschiedliche Testdatens¨atze erfordern. Besonders aufw¨andig werden Tests fur ¨ Kombinationen, indem etwa Bubblesort fur ¨ das Vorsortieren kleiner Reihungen vor der Anwendung von Mergesort verwendet wird. Allgemein ist es aber wichtig, dass Testf¨alle sowohl auf Basis einer Codeanalyse als auch aus der Spezifikation entwickelt werden. Denn codebasierte Tests sind vor allem zur Sicherung der Robustheit geeignet, w¨ahrend ¨ spezifikationsbasierte Tests die Ubereinstimmung des implementierten und des spezifizierten Verhaltens, also der Spezifikationskonformit¨at, prufen. ¨ Ist zum Beispiel bei der Implementierung ein Spezifikationsfall vergessen worden (eine Auslassung), so kann dies durch codebasierte Testf¨alle nicht entdeckt werden. Jedoch ist auch bei der schematischen Verwendung von Metriken zur Testfalluberdeckung ¨ Vorsicht geboten, da diese dazu fuhren ¨ konnen, ¨ die Metriken zu schematisch zu erfullen ¨ und einerseits doch wichtige F¨alle zu uber¨ sehen, andererseits aber viel, eventuell unnotigen ¨ Zusatzaufwand erzeugen. Generell ist daher eine an die Komplexit¨at des Testlings und die notwendige Qualit¨at des Systems angepasste Kombination aus verschiedenen Vorgehensweisen zur Testfalldefinition als optimal anzusehen.
6.3 Methodenspezifikationen
181
6.3.3 Testfalldefinition mit Methodenspezifikationen Eine Methodenspezifikation stellt noch keinen vollst¨andigen Testfall dar, sondern benotigt ¨ zus¨atzlich einen Testdatensatz. Abbildung 6.7 zeigt eine tabellarische Darstellung einer Testfallsammlung, bestehend aus funf ¨ Testf¨allen, aus der automatisch eine Testsuite fur ¨ JUnit generiert werden kann.
context Person.changeCompany(String name) OCL pre CCpre: true post CCpost: company.name == name && (
[email protected] != name implies
[email protected] ==
[email protected]@pre -1 && (company.employees == company.employees@pre +1 || (isnew(company) && company.employees == 1))) Abbildung 6.7. Definition einer Testfallsammlung
Dabei kann fur ¨ die ersten drei F¨alle dasselbe Objektdiagramm als Testdatensatz verwendet werden, denn es konnen ¨ alleine durch den AufrufParameter alle drei F¨alle variiert werden. Fur ¨ die F¨alle (4) und (5) wird zus¨atzlicher Java-Code verwendet, der nach Aufbau des Objektdiagramms, aber vor dem Test selbst ausgefuhrt ¨ wird. Beide F¨alle stellen Varianten bereits ¨ vorhandener F¨alle dar. Sie prufen ¨ vor allem die korrekte Anderung der Zahl der Angestellten. Dies erscheint notwendig, denn sonst h¨atte eine Implemen-
182
6 Modellbasierte Tests
tierung die Angestelltenzahl immer auf denselben Wert setzen konnen, ¨ ohne dass dies entdeckt worden w¨are. Durch die Moglichkeit, ¨ zus¨atzliche OCL-Bedingungen oder Invarianten (per Namen) anzugeben, konnen ¨ weitere Eigenschaften gepruft ¨ werden. So wird in Fall (3) gefordert, dass die neue Firma auch im Singleton ad angemeldet ist, und im Fall (5) beschrieben, dass die unbeteiligte Firma KPLV“ die ” Anzahl ihrer am System angemeldeten Angestellten beibeh¨alt. Beides h¨atte auch jeweils durch Objektdiagramme ausgedruckt ¨ werden konnen. ¨ Das angegebene Objektdiagramm BeforeChange wird konstruktiv eingesetzt, denn daraus werden die Testdaten generiert. Das Objektdiagramm gibt hier also sogar die komplette Umgebung an, es existieren keine weiteren Person- und Company-Objekte. Deshalb verletzt der im Attribut employee angegebene Wert Invarianten, die deshalb bei diesem Test nicht gepruft ¨ werden durfen. ¨
6.4 Sequenzdiagramme Unter einer Testsequenz wird eine Folge von Eingaben an den Testling verstanden, die dieser w¨ahrend eines Tests bearbeitet. In den 90er Jahren wurden einige Anstrengungen unternommen, um zu verstehen, wie Testsequenzen zu entwerfen sind, um bestimmte Situationen herzustellen und das Verhalten des Systems in solchen Situationen zu testen. Demgegenuber ¨ hat sich gemeinsam mit der Objektorientierung verst¨arkt eine Vorgehensweise durchgesetzt, nur die Testdaten vor der Testausfuhrung ¨ zu erstellen, statt einen Pfad von einem Startzustand bis zu diesem Datensatz anzugeben. Dies hat mehrere Grunde. ¨ Zum einen sind aufgrund ge¨anderter Codierungsstandards im objektorientierten Ansatz Methoden tendenziell kleiner als die Prozeduren der imperativen Welt. Zum zweiten kann mit dynamischer Bindung und Bildung von Unterklassen der Testling besser kontrolliert werden, indem ihm an kritischen Stellen Dummies statt der echten Implementierung angeboten werden. Zum dritten hat sich die Erkenntnis, dass der Code durchaus so gestaltet oder umgestaltet werden sollte, dass er gut testbar ist, nicht erst mit Test-First-Ans¨atzen durchgesetzt. Zum Beispiel wird in dem in Kapitel 7 vorgestellten Ansatz die Erzeugung neuer Objekte in eine Factory ausgelagert, um in einem Factory-Dummy auch diese Erzeugung kontrollieren zu konnen. ¨ Durch diese Maßnahmen ist keine Sequenz von Aufrufen mehr notwendig, um eine bestimmte Situation im System herzustellen. Diese kann direkt als Testdatensatz konstruiert werden. Deshalb kann ein Großteil der Testf¨alle durch einen einfachen Methodenaufruf gesteuert werden, nachdem der ad¨aquate Testdatensatz erstellt wurde. Diese Vorgehensweise birgt allerdings das Risiko, dass der benutzte Testdatensatz im realen System gar nicht auftreten kann. Durch die Verkleinerung der objektorientierten Methoden entstehen jedoch tiefere Aufrufhierarchien, denn viele Methoden nutzen weitere Me-
6.4 Sequenzdiagramme
183
thoden der aufgerufenen oder anderer Objekte. Die Objekte bilden damit komplexere Interaktionsmuster, die durch die Vor- und Nachbedingung einer Methode oft nicht ad¨aquat erfasst werden. Fur ¨ das Verst¨andnis des Zusammenspiels zwischen verschiedenen Objekten oder Systemteilen ist es daher sinnvoll, diese Interaktionsmuster in Form von Sequenzdiagrammen zu modellieren. Ein Sequenzdiagramm ist eine exemplarische Darstellung eines moglichen ¨ Ablaufs. Deshalb eignet sich ein Sequenzdiagramm in idealer Weise, den internen Ablauf eines Tests zu beschreiben. Beispielsweise besch¨aftigt sich [PJH+ 01] mit der Verwendung eines an die UML angelehnten MSC-Dialekts fur ¨ die Konformit¨atstests von Telekommunikationsprotokollen. Im Folgenden wird anhand einiger Beispiele demonstriert Sequenzdiagramme zur Modellierung von Testf¨allen verwendet werden konnen. ¨ 6.4.1 Trigger
Abbildung 6.8. Sequenzdiagramm als Testfallbeschreibung
Das in Abbildung 6.8 gezeigte Beispiel nutzt den in Abschnitt 7.1, Band 1 definierten Stereotyp trigger, um den ersten Methodenaufruf als Ausloser ¨ zu kennzeichnen. Damit mussen ¨ notwendigerweise die weiteren angegebenen Methodenaufrufe und Returns stattfinden, damit der beschriebene Test erfolgreich ist. Zus¨atzliche OCL-Bedingungen konnen ¨ innerhalb des Sequenzdiagramms angegeben werden, um Zwischenzust¨ande zu prufen ¨
184
6 Modellbasierte Tests
sowie eine finale Prufung ¨ des Ergebnisses vorzunehmen. Das Sequenzdiagramm zeigt nicht alle stattfindenden Methodenaufrufe. Es abstrahiert zum Beispiel von Interaktionen zwischen dem Auktionsobjekt und den anderen teilnehmenden Personen, die ebenfalls eine Mitteilung erhalten. Dieser Aspekt wird also mit dem angegebenen Sequenzdiagramm nicht getestet. Um den Test zu vervollst¨andigen, ist fur ¨ den Ablauf neben dem Sequenzdiagramm ein Testdatensatz notwendig, der zum Beispiel durch ein Objektdiagramm beschrieben werden kann. Durch die Verwendung eines Objektdiagramms werden die im Sequenzdiagramm angegebenen Objekte in eine strukturelle Beziehung durch Links gesetzt, die im Sequenzdiagramm nicht dargestellt werden kann. Aufgrund der Komplexit¨at der konstruktiv und damit vollst¨andig zu modellierenden Struktur, empfiehlt es sich, diese Struktur im Beispiel durch zwei Objektdiagramme zu beschreiben. Dabei wird angenommen, dass eine vervollst¨andigte und damit fur ¨ konstruktive Testdatengenerierung verwendbare Fassung des Objektdiagramms aus Abbildung 4.13 unter dem Namen Kupfer912 zur Verfugung ¨ steht. Abbildung 6.9 beinhaltet damit die vollst¨andige Beschreibung des Tests auf Basis des Sequenzdiagramms HandleBid.
Abbildung 6.9. Test nutzt Sequenzdiagramm HandleBid
Da es ein Sequenzdiagramm erlaubt, OCL-Bedingungen zu verschiedenen Punkten im Ablauf des Systems anzugeben, ist die Verwendung einer zus¨atzlichen Methodenspezifikation nicht notwendig, obwohl eine solche im Test zus¨atzlich angegeben werden kann. Fur ¨ die Prufung ¨ der angegebenen Interaktionen sowie der OCL-Bedingungen w¨ahrend des Ablaufs eines Sequenzdiagramms ist es notwendig, den Code geeignet zu instrumentieren. So muss, wie in Abschnitt 4.5 beschrieben, jeder Methodenaufruf und jede return-Anweisung protokolliert werden. Zus¨atzlich mussen ¨ die OCL-Bedingungen gepruft ¨ werden. Die Prufung, ¨ der Aufbau der Testdaten und die Ausfuhrung ¨ des Tests werden im
6.4 Sequenzdiagramme
185
Testtreiber lokalisiert, der als von JUnit aufrufbare Methode in der Klasse AuctionTest abgelegt wird. 6.4.2 Vollst¨andigkeit und Matching Gem¨aß Abschnitt 7.3, Band 1 gibt es mehrere Interpretationsmoglichkei¨ ten fur ¨ ein Sequenzdiagramm. So kann durch Awendung des Stereotyps match:complete festgelegt werden, dass alle Interaktionen des Objekts im dargestellten Zeitraum im Sequenzdiagramm gezeigt werden. Diese fur ¨ Spezifikationen meist unpraktikabel starke Aussage hat im Test seine Berechtigung, da die getesteten Objekte ausschließlich zum Zweck des Tests erzeugt wurden und weder davor, noch danach benutzt werden. Der Stereotyp match:visible wirkt etwas schw¨acher, indem er nur fordert, dass alle Interaktionen zwischen den im Diagramm angegebenen Objekten gezeigt werden, aber weitere Methodenaufrufe an andere Objekte moglich ¨ sind. Damit ist match:visible ebenfalls eine nutzliche ¨ Interpretation von Sequenzdiagrammen fur ¨ Tests. Die Verwendung von match:initial erlaubt den getesteten Klassen weitere Freiheiten, da das Sequenzdiagramm nach Auslo¨ sung des Triggers jeweils nur die angegebenen Interaktionen fordert, aber weitere zul¨asst. Damit gehen aber einige Kontrollmoglichkeiten ¨ verloren, die gerade fur ¨ Tests von Interesse sind. Die Interpretation mit dem Stereotyp match:free ist daher fur ¨ Tests kaum mehr geeignet, wenn der Stereotyp auf das komplette Sequenzdiagramm angewandt wird. Eine interessante Technik bietet allerdings die kombinierte Verwendung von Stereotypen, wie in Abbildung 6.10 gezeigt.
Abbildung 6.10. Kombinierte Verwendung von Stereotypen
Bei Anmeldung einer neuen Person in einer Auktion werden dem Personenobjekt durch die Methode sendMessage nacheinander alle aufgetretenen Nachrichten zugestellt. Dabei wird sendMessage mehrfach aufgerufen.
186
6 Modellbasierte Tests
Das Sequenzdiagramm fordert nun, dass ein Aufruf dabei ist, der die angegebenen Eigenschaften erfullt, ¨ bevor die ubergeordnete ¨ login-Methode abgearbeitet ist. 6.4.3 Nicht-kausale Sequenzdiagramme Wie in Abschnitt 7.4, Band 1 beschrieben, kann ein Sequenzdiagramm unvollst¨andig und damit nicht-kausal sein. Beispielsweise kann gewollt sein, dass ein an der Ausfuhrung ¨ beteiligtes Objekt oder einzelne Methodenaufrufe nicht beobachtet werden sollen, diese aber weitere beobachtete Methodenaufrufe zur Folge haben. Derartige Sequenzdiagramme sind fur ¨ Tests ebenfalls geeignet, wie das Beispiel in Abbildung 6.11 zeigt.
Abbildung 6.11. Nicht-kausales Sequenzdiagramm als Testablaufbeschreibung
Die zeitliche Folge der Methodenaufrufe wird alleine durch deren Reihenfolge entlang der Zeitachse festgelegt und kann somit uberpr ¨ uft ¨ werden. Die Kausalit¨at kann allerdings nicht beliebig aufgehoben werden. Naturlich ¨ konnen ¨ return-Pfeile nur angegeben werden, wenn der zugehorige ¨ Methodenaufruf angegeben ist. 6.4.4 Mehrere Sequenzdiagramme in einem Test Da ein Sequenzdiagramm eine Beobachtung beschreibt, ist es mo¨ glich, mehrere Sequenzdiagramme fur ¨ verschiedene Teilabl¨aufe innerhalb eines Tests einzusetzen. So erg¨anzen sich die beiden Diagramme aus den Abbildungen 6.8 und 6.11. Dabei werden die Sequenzdiagramme unabh¨angig voneinander gepruft, ¨ also jeweils so, als wenn kein anderes Sequenzdiagramm vorhanden w¨are. Die in beiden Sequenzdiagrammen gemeinsam auftretenden
6.4 Sequenzdiagramme
187
Methodenaufrufe, im Beispiel also handleBid, werden daher im Testablauf durch denselben Methodenaufruf erfullt. ¨ Eine sequentielle oder alternative Komposition oder Iteration von Sequenzdiagrammen, wie sie MSC’s [IT99a], der UML-Standard [OMG03] oder auch YAMS [Kru00b] ¨ erlauben, w¨are eine mogliche ¨ Erweiterung, ist jedoch, wie in Kapitel 7, Band 1 begrundet, ¨ in UML/P nicht vorgesehen. Stattdessen unterliegt der Interpretation mehrerer Sequenzdiagramme eine lose Form der Verschmelzung“, die gleiche Metho” denaufrufe identifizieren kann und so die Sequenzdiagramme unabh¨angig voneinander einsetzt. 6.4.5 Mehrere Trigger im Sequenzdiagramm Testklassen werden entsprechend mit den in JUnit ublichen ¨ Codierungsstandards mit dem Suffix Test“ versehen oder, wie in Abschnitt 3.5.2, Band 1 ” vorgeschlagen, durch einen Stereotyp wie Testclass markiert. Weil die Methodenaufrufe zum Start des Tests ausschließlich vom Testtreiber vorgenommen werden konnen, ¨ umgekehrt aber auch jeder Methodenaufruf, ausgehend von der Testklasse als Trigger zu verstehen ist, ist bereits einer der Stereotypen trigger oder Testclass bei der Definition von Testf¨allen ausreichend. Wie Abbildung 6.12 zeigt, kann ein Sequenzdiagramm auch mehrere mit dem Stereotyp trigger markierte Methodenaufrufe beinhalten. Ein solches Sequenzdiagramm beschreibt einen Testtreiber, der nacheinander mehrere Methodenaufrufe durchfuhrt. ¨
Abbildung 6.12. Sequenzdiagramm mit aufeinanderfolgenden Triggern
188
6 Modellbasierte Tests
6.4.6 Interaktionsmuster Die Verwendung eines Sequenzdiagramms zur Definition eines Testfalls hat den wesentlichen Vorteil, dass durch das Sequenzdiagramm das geprufte ¨ Interaktionsmuster explizit dargestellt ist. Durch Abgleich mit einer vorhandenen Implementierung l¨asst sich relativ leicht analysieren, ob und wie gut eine gegebene Menge von solchen Testf¨allen die verschiedenen moglichen ¨ Abl¨aufe im Testling abdeckt. ¨ Eine vollst¨andige Uberdeckung aller moglichen ¨ Abl¨aufe eines Testlings ist aber meist nicht moglich, ¨ denn ganz a¨ hnlich zum Pfaduberdeckungstest ¨ einer Prozedur [Bal98, Mye00] fuhren ¨ Schleifen und Rekursion zu einer unbeschr¨ankten Anzahl von Varianten moglicher ¨ Abl¨aufe. Fur ¨ praktische Belange ist es deshalb notwendig, eine endliche, moglichst ¨ repr¨asentative Menge von Abl¨aufen festzulegen, die in Testf¨allen umgesetzt wird. Im Auktionsprojekt sind dies zum Beispiel Auktionen mit verschiedenen Mengen an Geboten (0,1,2,3,5 und viele), die in Testf¨allen gepruft ¨ worden sind. Solche Tests, die den Ablauf einer Teilphase oder sogar der gesamten Auktion prufen, ¨ gehen uber ¨ den einzelnen Methodentest hinaus und bilden damit einen ersten Schritt zur Integration verschiedener Systemteile. Sequenzdiagramme sind daher auch geeignet, Integrationstests zu modellieren. Dabei ist es sinnvoll, bei Integrationstests vor allem die Schnittstellen zwischen den Systemteilen zu modellieren und die Kapselung der Systemteile selbst zu respektieren. Dafur ¨ sind Sequenzdiagramme mit Stereotypen wie match:free geeignet. ¨ Weitere Probleme, eine Uberdeckung der Interaktionsmuster zu erzielen, ergeben sich aus der explodierenden Anzahl von moglichen ¨ Objektstrukturen, die als Grundlage fur ¨ die Abl¨aufe dienen: •
•
•
Mengenwertige Assoziationen erlauben grunds¨atzlich eine unendliche Anzahl verschiedener Objektstrukturen, die bei einer Bearbeitung ebenfalls Schleifen benotigen ¨ und daher wieder zu unendlich vielen mogli¨ chen Ablaufstrukturen fuhren. ¨ Die sich aus der Vererbung ergebende dynamische Bindung von Methoden erfordert es, fur ¨ ein gegebenes Objekt einer Klasse auch alle Unterklassen zu testen. Dies fuhrt ¨ bei mehreren Objekten im Testdatensatz zu einer Explosion der Testfallanzahl. Die dynamische Ver¨anderbarkeit von Objektstrukturen macht das Ablaufverhalten eines Testlings auch von den Parametern abh¨angig. Beispielsweise kann ein Parameter bestimmen, wieviele Objekte in einer Datenstruktur erzeugt oder wie diese durch Links verbunden werden.
Deshalb ist es fur ¨ praktische Belange unrealistisch, eine vollst¨andige ¨ Uberdeckung moglicher ¨ Abl¨aufe durch Testf¨alle auf Basis von Sequenzdiagrammen zu erwarten. Wie das folgende Beispiel zeigt, ist damit außerdem auch nicht gesichert, dass eine Anweisungsuberdeckung ¨ der getesteten Methoden stattfindet:
6.5 Statecharts void methode(int i) { if(i >= 1) foo(i); else { attribut = i; foo(i+1); } }
189 Java
Alle Sequenzdiagramme zum Test von method zeigen dieselbe Ablaufstruktur. Es ist daher falsch, anzunehmen, dass zwei Sequenzdiagramme mit derselben Ablaufstruktur dieselben Abl¨aufe der Implementierung testen. Deshalb ist die Definition von Integrationstests mit Sequenzdiagrammen nur eines von mehreren Instrumenten zur Testfallmodellierung, das gemeinsam mit den bereits besprochenen Testf¨allen aus Methodenspezifikationen und der Prufung ¨ von Invarianten einzusetzen ist.
6.5 Statecharts Die in Kapitel 6, Band 1 definierten Statecharts haben folgende grunds¨atzlichen Anwendungsgebiete: 1. Ausfuhrbare ¨ Statecharts werden konstruktiv in Code ubersetzt ¨ und finden im Produktionssystem oder als Orakelfunktion Verwendung. ¨ 2. Fur ¨ Tests einsetzbare Statecharts werden zur Uberpr ufung ¨ des korrekten Ablaufs eines Tests genutzt. 3. Statecharts dienen als allgemein gultige ¨ Verhaltensspezifikationen und werden zur manuellen oder automatisierten Ableitung von Testf¨allen eingesetzt. 4. Abstrakte Statecharts dienen als Dokumentation, sind aber fur ¨ Tests und Codegenerierung zu abstrakt oder zu informell.2 Ein abstraktes, informelles Statechart kann durch Detaillierung und Pr¨azisierung der darin enthaltenen Information zu einem fur ¨ Tests geeigneten oder ausfuhrbaren ¨ Statechart transformiert werden. Da in einem Statechart gleichzeitig konstruktive Elemente wie Aktionen und deskriptive Elemente wie OCL-Nachbedingungen verwendet werden konnen, ¨ ist auch ein kombinierter Einsatz sinnvoll. So kann ein konstruktiver Anteil eines Statecharts zur Generierung von Methoden eingesetzt werden, w¨ahrend zum Beispiel Zustands- und Nachbedingungen parallel dazu verwendet werden, Tests zu unterstutzen ¨ und Invarianten zu generieren. 2
In [Wil01] werden diese als Skizzen ( Sketches“) und nicht als echte Modelle be” zeichnet.
190
6 Modellbasierte Tests
6.5.1 Ausfuhrbare ¨ Statecharts Ein ausfuhrbares ¨ Statechart wird als konstruktives Modell genutzt und, wie in Abschnitt 4.4 beschrieben, direkt in Code umgesetzt. Ausfuhrbare ¨ Statecharts besitzen typischerweise nur prozedurale Aktionen in Form von Java-Code. Der bei der Umsetzung entstehende Java-Code kann durch im Statechart eingetragene Zustandsinvarianten erweitert sein, die w¨ahrend der Testphase des Systems im Code zur Laufzeit gepruft ¨ werden. Konstruktive Statecharts eignen sich jedoch nicht als Testbeschreibung, da der Effekt einer prozeduralen Aktion nicht getestet, sondern die Aktion nur ausgefuhrt ¨ werden kann. Im Prinzip konnen ¨ Statecharts und insbesondere Methoden-Statecharts als Testtreiber eingesetzt werden. Es ist jedoch ein Prinzip bei der Definition von Testtreibern, diese moglichst ¨ einfach und damit im Wesentlichen ohne verzweigte Kontrollstruktur zu entwickeln. Die lineare Struktur eines Testtreibers aber l¨asst ein Statechart zu einer linearen Form entarten, die auch durch ein Sequenzdiagramm dargestellt werden kann. Interessant ist der Einsatz eines konstruktiven Statecharts als Orakelfunktion fur ¨ das Testergebnis. Dies empfiehlt sich zum Beispiel, wenn der durch ein Statechart generierte Code g fur ¨ das Produktionssystem zu langsam ist und deshalb eine alternative Implementierung f realisiert wurde. Im Test wird der Testdatensatz x mit VUnit.clone (siehe Abschnitt 5.2.3) kopiert, auf beiden Testdatens¨atzen die jeweilige Funktion angewandt und das Ergebnis mit VUnit.compare verglichen. Die Vergleichsform kann, wie in Abbildung 5.6 gezeigt, durch die Angabe eines comparator-Eintrags auch explizit definiert werden. Ein Black-Box-Vergleich der Ergebnisse f(x)=g(x) hat den Vorteil, dass die interne Realisierung einer Methode wesentlich von der Beschreibung durch das Statechart abweichen und damit einem Refactoring unterworfen werden kann, ohne das Statechart in seiner Funktion als Orakel zu beeintr¨achtigen. Bei dieser Vorgehensweise werden aber die im Statechart angegebenen OCL-Bedingungen w¨ahrend des Ablaufs nicht gepruft. ¨ Um das zu erreichen ist eine gleichzeitige Verwendung der konstruktiven Anteile des Statecharts fur ¨ die Generierung von Produktionscode und der deskriptiven Anteile fur ¨ die Generierung von Prufungen ¨ in Tests notwendig. Sowohl bei der Verwendung eines Statecharts als Testtreiber als auch als Orakel ist Nichtdeterminismus im Statechart kritisch. Nichtdeterministische Statecharts, also solche mit uberlappenden ¨ Schaltbereichen, sind nur sinnvoll, wenn das Statechart als Ablaufbeschreibung im Test eingesetzt wird. Der Nichtdeterminismus wirkt dann als Unterspezifikation und wird durch die tats¨achliche Implementierung aufgelost. ¨ In Abbildung 6.13 sind drei der Strategien des Auktionssystems skizziert, die gem¨aß der Beschreibung in Abschnitt D.2, Band 1 bei Gebotsabgabe unterschiedliche Verl¨angerungen (delta) in Abh¨angigkeit des Zeitpunkts des jeweils aktuellen Gebots bewirken.
6.5 Statecharts
191
Abbildung 6.13. Verl¨angerungsstrategien bei Geboten in der Auktion
Die historisch gewachsene Berechnungsstruktur fur ¨ das Auktionsende nach Abgabe eines Gebots ist durch das Methoden-Statechart in Abbildung 6.14 beschrieben.3 Es zeigt die Berechnung fur ¨ eine konstante Verl¨angerung, keine Verl¨angerung sowie eine linear absteigende Verl¨angerung mit minimaler Untergrenze.4
Abbildung 6.14. Statechart berechnet neues Auktionsende
Die Berechnung ist im tats¨achlichen Auktionssystem in verschiedene Methoden auf mehrere Unterklassen von TimingPolicy verteilt worden. Die 3 4
Alle Time-Objekte wurden in diesem Beispiel vereinfachend durch long-Werte ersetzt. Ein so entwickeltes Statechart dient typischerweise als erster Entwurf und kann wie auch hier meistens vereinfacht werden. In diesem Fall ist auch eine tabellarische Darstellung statt eines Diagramms sinnvoll.
192
6 Modellbasierte Tests
in Abbildung 6.14 gezeigte ursprungliche ¨ Modellierung der Funktionalit¨at ist deshalb als konstruktive Implementierung nicht geeignet, kann aber aufgrund ihrer Ausfuhrbarkeit ¨ und der inhaltlichen Korrektheit als Orakelfunktion eingesetzt werden.5 Daher konnen ¨ Tests unter Nutzung der modellierten Funktionalit¨at als Orakel, wie in Abbildung 6.15, formuliert werden.
Abbildung 6.15. Test mit Methoden-Statechart als Orakelfunktion
6.5.2 Statechart als Ablaufbeschreibung Ein Statechart, das einen Lebenszyklus deskriptiv modelliert, kann a¨ hnlich wie ein Sequenzdiagramm zur Prufung ¨ der Korrektheit eines Systemablaufs eingesetzt werden. Das Statechart wird also als Pr¨adikat fur ¨ einen Testfall verstanden, der in einem vorgegebenen Zustand beginnt, und dessen Interaktionen durch das Statechart uberwacht ¨ werden. Damit kann ein Statechart in einem Test zus¨atzlich als einzuhaltendes Pr¨adikat eingesetzt werden. Das Statechart selbst stellt allerdings keinen kompletten Testfall dar, da sowohl die Testdaten als auch der Testtreiber fehlen. Ein zugehoriger ¨ Testablauf wird typischerweise durch ein Sequenzdiagramm beschrieben. Die beteiligten Objekte werden in der ublichen ¨ Form durch eine Objektstruktur modelliert. Einem oder mehreren dieser Objekte kann nun jeweils ein Statechart zugeordnet sein, dessen Gultigkeit ¨ zu prufen ¨ ist. Die Objekte mussen ¨ sich dabei nicht in einem durch das Statechart vorgegebenen Startzustand befinden oder w¨ahrend des Testablaufs einen Endzustand erreichen. Deshalb werden fur ¨ jedes Objekt zus¨atzlich der zu Beginn des Tests relevante Zustand und die am Ende des Tests erlaubten Zust¨ande angegeben. Abh¨angig von der in Abschnitt 4.4 beschriebenen Umsetzung 5
Parallel zum Refactoring der Funktionalit¨at von newCurrentClosingTime mit der Verschiebung auf die TimingPolicy-Klassen sind Anpassungen der verwendeten Attribute notwendig. Auf diese wird im Beispiel aber verzichtet.
6.5 Statecharts
193
der Zust¨ande durch ein Aufz¨ahlungsattribut oder Pr¨adikate werden diese Zust¨ande eingestellt oder gepruft. ¨ Da ein Statechart verschiedene Pfade darstellt ist eine Wiederverwendung eines Statecharts fur ¨ mehrere Testf¨alle sinnvoll. Abbildung 6.16 zeigt ein geegnetes Statechart und einen Testtreiber, der alternativ auch als Sequenzdiagramm dargestellt sein konnte. ¨ Weitere Testf¨alle konnen ¨ in anderen Zust¨anden starten oder andere Pfade nehmen, indem sie zum Beispiel Gebote (bid-Aufrufe) berucksichtigen. ¨
Abbildung 6.16. Statechart als Lebenszyklusbeschreibung im Test
Fur ¨ den Vergleich des Testablaufs mit den Transitionen und Zust¨anden des Statecharts ist eine Instrumentierung des Testlings in a¨ hnlicher Form wie beim Sequenzdiagramm notwendig. Das heißt, zu Beginn und am Ende jeder Methode wird eine geeignete Instrumentierung eingebaut, um unter anderem die Zust¨ande, Zustandsinvarianten und die Schaltbereitschaft prufen ¨ zu konnen. ¨ Nicht alle im Statechart formulierbaren Eigenschaften ko¨ nnen allerdings zur Prufung ¨ verwendet werden. Zum Beispiel sind prozedurale Aktionen als Prufvorgaben ¨ ungeeignet und mussen ¨ daher (falls angegeben) ignoriert werden.6 Außerdem konnen ¨ bei Methoden-Statecharts beziehungsweise generell die Kontrollzust¨ande (Stereotyp controlstate) eines Statecharts nicht gepruft ¨ werden, da deren automatische Zuordnung zu Programmstellen im Code nicht moglich ¨ ist. Nur wenn die Implementierung aus dem prozeduralen Teil eines Methoden-Statecharts konstruktiv erzeugt wird, ist es moglich, ¨ 6
Es ist moglich, ¨ prozedurale Aktionen als Orakelfunktion g fur ¨ Tests der Form f (x) = g(x) des Effekts jeder einzelnen Transition zu verwenden. Das ist allerdings wegen des fur ¨ jede Transition notwendigen Kopierens der jeweils aktuellen Datenstruktur nur bedingt effizient.
194
6 Modellbasierte Tests
den deskriptiven Teil gleichzeitig als Zusicherungen in die generierte Methode einzusetzen. Auch fur ¨ die in Abschnitt 6.5.2, Band 1 diskutierten, abschnittsweise komponierten Aktionen (Stereotyp action:sequential) ist eine Prufung ¨ der zwischendurch gultigen ¨ Bedingungen nur bei einer konstruktiven Umsetzung des Statecharts moglich. ¨ 6.5.3 Testverfahren fur ¨ Statecharts Da Statecharts nicht einen einzelnen Ablauf, sondern eine unendliche Menge von potentiellen Abl¨aufen fur ¨ ein Objekt beschreiben, ist ein Statechart eine hervorragende Ausgangsbasis fur ¨ die Entwicklung von Tests und fur ¨ ¨ die Messung der Uberdeckung der im Statechart modellierten Verhaltensteile. Im Gegensatz zu den bisher diskutierten Formen des Einsatzes der UML zur Testfallmodellierung, wird hier also nicht ein Diagramm fur ¨ einen Testfall eingesetzt, sondern es werden aus einem Diagramm viele Testf¨alle abgeleitet. Wird ein Statechart konstruktiv eingesetzt, so konnen ¨ die aus dem pr¨adikativen Anteil (OCL-Bedingungen) generierten Zusicherungen als WhiteBox-Tests verstanden werden, da das Statechart die Implementierung darstellt. Existiert jedoch eine unabh¨angig entstandene Implementierung, so wird das Statechart als Spezifikation mit der Implementierung verglichen. Die Tests sind dann also Black-Box-Tests. Es ist heute Gegenstand der Forschung aus gegebenen Statechart-Spezifikationen moglichst ¨ kompakte, aber doch vollst¨andige Sammlungen von ¨ Testdatens¨atzen zu generieren, die eine Uberdeckung nach einer vorgegebe¨ nen Uberdeckungsmetrik erreichen. Fur ¨ verschiedene Varianten flacher Automaten wurden bereits Verfahren entwickelt [PYvB96, RDT95]. In Abschnitt 6.5.6 werden weiterfuhrende ¨ Ans¨atze diskutiert. Wie bereits beschrieben, ist es generell unentscheidbar, ob eine OCLBedingung erfullbar ¨ ist. Wurde eine unerfullbare ¨ OCL-Bedingung als Vorbedingung einer Transition eingesetzt, so kann kein Pfad gefunden werden, ¨ der die zugehorige ¨ Transition beinhaltet. Eine Uberdeckung aller Transitionen ist daher unmoglich. ¨ Wegen diesen Unentscheidbarkeitsproblemen gibt es fur ¨ die in der UML/P definierten Statecharts wie fur ¨ viele andere Varianten von Automaten kein automatisches Verfahren, das fur ¨ alle Formen eine nach einem bestimmten Kriterium vollst¨andige Testfallsammlung generiert. Ein weiterer Problemkreis entsteht durch den moglichen ¨ Nichtdeterminismus im deskriptiven Statechart zum Beispiel durch uberlappende ¨ Schaltbereiche. Hat ein Statechart zwei Transitionen mit identischen Schaltbereichen, aber unterschiedlichen Zielzust¨anden, wie zum Beispiel in Abbildung 2.39(a) dargestellt, so ist es moglich, ¨ dass eine Implementierung immer nur eine Alternative ausw¨ahlt. Die zweite Transition wird daher nicht gew¨ahlt ¨ und kann durch Tests nicht uberdeckt ¨ werden. Auch die Uberlappung von
6.5 Statecharts
195
Schaltbereichen und die Bevorzugung einer Transition in einer Implementierung kann im Allgemeinen nicht automatisiert erkannt werden. Es ist daher bereits eine sinnvolle Unterstutzung, ¨ wenn ein Werkzeug auf Basis vorhandener Tests misst, zu welchem Grad ein Statechart uberdeckt ¨ wurde und gegebenenfalls auf Defizite hinweist. Beiden Formen von Statecharts ist gemeinsam, dass die damit beschriebenen Transitionsfolgen eine gewisse Abstraktion der vollst¨andigen Implementierung darstellen. So konnen ¨ • • •
in den Aktionen komplexe Anweisungsfolgen einschließlich Schleifen und Verzweigungen eingebettet sein, ein einzelner Diagrammzustand einer Menge von Objektzust¨anden entsprechen und OCL-Bedingungen aus mehreren in eine Disjunktion zusammengefassten alternativ erfullbaren ¨ Klauseln bestehen.
Fur ¨ detaillierte Tests sollte dies berucksichtigt ¨ werden. Dies kann gesche¨ hen, indem a¨ hnlich zu [GH99] Uberdeckungsmetriken fur ¨ diese Elemente mit den nachfolgend genannten Verfahren fur ¨ Statecharts kombiniert werden. Das heißt also, dass nach der Entwicklung der Tests auf Basis der Statechart-Struktur diese so verfeinert werden, dass die einzelnen Bestandteile von Bedingungen und Aktionen im Statechart ebenfalls uberdeckt ¨ wer¨ den. Eine Alternative ist es, die Uberdeckung nicht auf Basis des Statecharts, sondern des daraus generierten Codes zu messen. Ein weiterer Aspekt ist die Einbeziehung der Abl¨aufe in aufgerufenen Methoden desselben oder anderer Objekte. Insbesondere, wenn eine Kompositionsbeziehung zu anderen Objekten besteht, wie im Auktionsbeispiel zwischen dem Auction- und seinen Policy-Objekten, dann wird das Verhalten der abh¨angigen Objekte in die Tests des Kompositums einbezogen. Dadurch w¨achst aber die Zahl der notwendigen Tests stark an, da verschiedene Konfigurationen von Objektstrukturen und in jeder Konfiguration jeweils die Zustandsmodelle aller Objekte des Kompositums betrachtet wer¨ den konnen. ¨ Eine Absch¨atzung zeigt schnell, ob ein Versuch einer Uberdeckung praktisch durchfuhrbar ¨ ist. Die ad¨aquate Wahl des zu testenden ¨ Teilsystems, des gewunschten ¨ Uberdeckungskriteriums und die intelligente, aber dadurch leider manuelle Auswahl von Testf¨allen wird daher notwendig. Bereits in [Mye79] wird vermerkt, dass erfahrene Tester auch ohne explizit angewandte Systematik durch Error Guessing“, also dem Erraten von ” potentiellen Fehlerquellen, gute Ergebnisse erzielen. [PKS02] aber fordern fur ¨ viele Anwendungen oder zumindest kritische Systemteile, dass Error ” Guessing“ vor allem als zus¨atzliche Technik zu mehr systematischen Vorgehensweisen zu sehen ist.
196
6 Modellbasierte Tests
¨ 6.5.4 Uberdeckungsmetriken In Vernachl¨assigung der diskutierten Komplexit¨aten bei Aktionen und OCL¨ Bedingungen lassen sich folgende Metriken fur ¨ die Uberdeckung eines Statecharts durch eine Testsammlung identifizieren. Diese sind als Kontrollflussbasierte Metriken bereits in [FHNS02, Bal98] genannt: Zustandsuberdeckung ¨ erfordert einen Testfall fur ¨ jeden erreichbaren Zustand im Diagramm. Ein Testfall legt dabei eine Sequenz von Eingaben fest, die von einem Startzustand in diesen Zustand fuhrt. ¨ Transitionsuberdeckung ¨ erfordert einen Testfall fur ¨ jede schaltbereite Transition. Die Sequenz von Eingaben beschreibt dabei den Pfad von einem Ausgangszustand bis zur Ausfuhrung ¨ der Transition. Pfaduberdeckung ¨ erfordert einen Test fur ¨ jeden moglichen ¨ Pfad von einem ¨ Start- in einen Endzustand. Diese Form der Uberdeckung umfasst die beiden vorhergehenden Formen. Wenn Schleifen im Automat existieren, ist die Menge der Pfade jedoch unendlich und damit eine Pfaduberde¨ ckung praktisch undurchfuhrbar. ¨ Minimale Schleifenuberdeckung ¨ ist deshalb eine reduzierte Form der Pfaduberdeckung. ¨ Dabei wird bei Schleifen darauf verzichtet, diese mehrmals zu durchlaufen. Jede im Statechart vorkommende Schleife uss mjedoch mindestens einmal durchlaufen werden. Nach [Bal98] hat die Pfaduberdeckung ¨ keine praktische Relevanz, da sie bei Schleifen sofort zu einer unendlichen und damit nicht mehr durchfuhr¨ baren Aufgabe wird. Demgegenuber ¨ ist die minimale Schleifenuberdeckung ¨ ein starkes und dennoch praktikables Kriterium, das fur ¨ qualitativ hochwertige Software eingesetzt werden sollte. Bei Statecharts ohne Schleifen sind jedoch beide Metriken a¨ quivalent. Ein Problem beider Metriken ist die Frage, ob ein im Statechart erkennbarer Pfad in einer Implementierung tats¨achlich durchgefuhrt ¨ werden kann. Wird die Vorbedingung einer Transition durch Invarianten und dem bisherigen Verlauf eines Teilpfades nie erfullbar, ¨ so l¨asst sich diese Transition nicht ausfuhren. ¨ Dies gilt um so mehr, wenn das durch ein Statechart modellierte Objekt in einen Testling aus mehreren Objekten eingebettet und daher nicht direkt zug¨anglich ist. Die Umgebung kann dann auch verhindern, dass eine erforderliche Eingabesequenz auftritt. Deshalb ist die Testumgebung jeweils geeignet zu w¨ahlen. ¨ Um die Uberdeckungsmetriken bezuglich ¨ einer Testsammlung zu ermitteln, werden die Abl¨aufe der Tests auf Basis einer Instrumentierung des Codes protokolliert. ¨ Die ursprunglich ¨ fur ¨ flache Automaten entwickelten Uberdeckungsmetriken konnen ¨ in dieser Form auch auf Statecharts mit hierarchischem Zustandskonzept angewandt werden. Durch die in Abschnitt 6.6.2, Band 1 diskutierten Transformationen auf Statecharts ist es allerdings auch moglich, ¨ die Tests auf Basis der expandierten Hierarchie durchzufuhren. ¨ Dadurch wird die notwendige Testuberdeckung ¨ verfeinert, da zum Beispiel beim Auflosen ¨
6.5 Statecharts
197
einer Transition in einen hierarchischen Zustand eine Vervielfachung dieser Transition stattfindet. Die Expansion kann also genutzt werden, um eine feinere Testuberdeckung ¨ zu erhalten. Anhand des Statecharts aus Abbildung 6.14 werden die vier genannten Metriken in Abbildung 6.17 illustriert. Dabei werden strukturell identische, ikonisierte Formen des Ausgangsstatecharts verwendet, die den Durchlauf des Tests durch das Statechart illustrieren. Die drei Pfade der Zustandsuber¨ deckung reichen fur ¨ die Transitionsuberdeckung ¨ nicht aus: Es muss ein vierter Ablauf hinzu genommen und zwei der vorhandenen Abl¨aufe so modifiziert werden, dass auch die am Ende stattfindenden Transitionen uberdeckt ¨ sind.
Abbildung 6.17. Testfallpfade zur Erfullung ¨ der Metriken
Die Pfaduberdeckung ¨ erfordert 18 Testf¨alle. Da es sich bei dem zu testenden Statechart um ein Methoden-Statechart handelt, besteht die Eingabe aus nur einem Methodenaufruf. Gegebenenfalls werden weitere Interaktionen mit der Umgebung vorgenommen, indem Methoden der Umgebung aufgerufen werden und durch return-Werte eine weitere Steuerung des Transitionsverlaufs moglich ¨ ist. In diesem Beispiel aber h¨angt der Verlauf des Testfalls ausschließlich von der initialen Eingabe und der Wertebelegung in der initialen Objektstruktur ab. Um eine 100%ige Pfaduberdeckung ¨ zu erreichen, sind also 18 verschiedene Belegungen fur ¨ die im Statechart benutzten Variablen zu finden. Die Invariante
198
6 Modellbasierte Tests
context Auction a inv: MIN DELTA <= a.extensionTime
OCL
fordert zum Beispiel, dass die extensionTime einer Auktion immer uber ¨ der minimalen Erweiterungszeit von MIN DELTA (5 Sekunden) liegt. Deshalb sind die markierten drei Pfade nicht durchfuhrbar. ¨ Fur ¨ die anderen 15 Pfade lassen sich geeignete Testdaten finden. Zus¨atzlich ist es sinnvoll, Randf¨alle, wie die Ankunft eines Gebots exakt zum Zeitpunkt des Auktionsendes oder minimal danach, zu behandeln. Weitere Randf¨alle lassen sich durch die Analyse der Schaltbedingungen von Transitionen identifizieren und so weitere Tests ableiten. Als zweites Beispiel werden die Metriken anhand des in Abbildung 6.16 dargestellen Statecharts demonstriert. Dort ist jede Transition durch einen Methodenaufruf zu steuern, die Eingabe ist also eine mehrteilige Sequenz.
Abbildung 6.18. Testf¨alle zur Erfullung ¨ der Metriken
Die Moglichkeit, ¨ Statecharts durch Verwendung verschiedener Stereotypen eine unterschiedliche Semantik in Bezug auf Vervollst¨andigung und Fehlersituationen zu geben, erfordert bei der Definition von Tests die Beruck¨ sichtigung dieser Stereotypen. Folgende Situationen und Strategien sind dabei moglich: ¨ 1. Das Statechart ist vollst¨andig, indem zum Beispiel ein Fehlerzustand eingefuhrt ¨ wurde. Dadurch existieren implizite Transitionen, die in den Fehlerzustand fuhren ¨ und deren Schaltbereiche alles abdecken, was nicht durch explizite Transitionen abgedeckt ist. Diese Transitionen konnen ¨ ¨ in die Uberdeckung einbezogen werden. Da Statecharts einen zweiten Fehlerzustand fur ¨ die Behandlung aufgetretener Exceptions besitzen konnen, ¨ ist die hier auftretende Situation analog. Es ist wieder zu ent-
6.5 Statecharts
199
scheiden, ob und wie detailliert die Verarbeitung der Exceptions getestet werden soll. 2. Das Statechart wurde mit completion:ignore vervollst¨andigt. Dadurch entstehen Transitionsschleifen, die bei Transitions- und Pfaduberdeckung ¨ getestet werden konnen. ¨ 3. Ein mit completion:chaos markiertes Statechart basiert auf der Annahme, dass das Statechart nur einen Teil des Verhaltens festlegt. Es beschreibt das Verhalten des Objekts bis zu dem ersten Auftreten einer Situation, in der das Statechart nicht schaltbereit ist und der Testling beliebiges Verhalten annehmen kann. Derartige Abl¨aufe mussen ¨ nicht getestet werden. Bei einer Vervollst¨andigung mittels completion:ignore wird besonders offensichtlich, dass ein Test dieser zus¨atzlichen Transitionen fur ¨ das Verhalten des mit dem Statechart beschriebenen Objekts nicht sehr wesentlich ist, da diese Transitionen in uniformer Weise generiert sind. Wesentlich interessanter ist es daher meistens, Objekte der Umgebung darauf zu testen, ob deren Umgang mit dem beschriebenen Objekt soweit korrekt ist, dass sie auf das Ignorieren eines Methodenaufrufs oder das Betreten eines Fehlerzustands ihrerseits robust reagieren. Bei der Annahme, dass ein Statechart durch die Unvollst¨andigkeit alle erlaubten Abl¨aufe eines Objekts beschreibt, entstehen dadurch sogar wesentliche Einschr¨ankungen an die Umgebung, die zumindest in Form von Tests zu prufen ¨ sind. Das kann zum Beispiel durch eine Vervollst¨andigung mit Fehlerzustand umgesetzt werden, bei der im Fehlerzustand ein Scheitern des Tests gemeldet wird. 6.5.5 Transitionstests statt Testsequenzen Bei den bisher diskutierten Testsequenzen wird grunds¨atzlich davon ausgegangen, dass als Testdatensatz das beschriebene Objekt in einem Initialzustand vorliegt. Um daher eine Transition zu testen, ist zun¨achst ein Pfad zu finden, der zum Quellzustand dieser Transition fuhrt. ¨ Die mit Abbildung 5.7 charakterisierte Grundstruktur von Testf¨allen zeigt, dass ein Testfall von einem beliebigen Testdatensatz ausgehen kann. Dies bedeutet, dass eine Transition eines Statecharts auch dadurch getestet werden kann, dass ein Testdatensatz gefunden wird, der dem Quellzustand der Transition entspricht und diese schaltbereit macht. Dies geht vor allem bei dem Lebenszyklus eines Objekts, bei dem ein Methodenaufruf einer einzelnen Transition entspricht und somit isoliert ausgefuhrt ¨ werden kann. Statt also eine Testsequenz beginnend mit einem Initialzustand zu erfordern, reicht es fur ¨ einen Transitionstest aus, einen einfachen Methodenaufruf durchzufuhren. ¨ Bei einem Methoden-Statechart, wie dem in Abbildung 6.14, ist dies allerdings nicht moglich. ¨ Um die Verwendbarkeit fur ¨ Tests zu verbessern ist daher eine Restrukturierung des Statecharts sinnvoll. Im Beispiel hat sich eine
200
6 Modellbasierte Tests
Aufspaltung in zwei Methoden angeboten, wobei zus¨atzlich die Berechnung des Werts delta durch drei Varianten derselben Methode in verschiedenen Unterklassen stattfindet. Die Zerlegung in mehrere kleine Methoden erlaubt es dann auch hier, einen Zwischenzustand als Objektzustand zu erstellen von dem aus ein Test gestartet wird. Bei einer Automatisierung der Generierung von Tests wird durch die Definition von Testdaten, die einem Quellzustand einer Transition entsprechen, das bereits in Abschnitt 6.4 diskutierte Problem umgangen, dass eine Testsequenz zu finden ist, die zu einem bestimmten Zustand beziehungsweise einer Transition fuhrt. ¨ Stattdessen ist eine Objektstruktur zu finden, in der der Testling einem Statechart-Zustand entspricht. Es ist deshalb notwendig Diagrammzust¨ande mit OCL-Bedingungen ausreichend genau zu charakterisieren. Sonst kann es bei diesem Ansatz vorkommen, dass ein Objektzustand fur ¨ die Ausgangsdaten festgelegt wird, der im Produktionssystem nicht erreichbar ist. Bei der Suche von Objektzust¨anden, die einem Diagrammzustand entsprechen, sind außerdem auch die allgemein gultigen ¨ Invarianten zu beachten. Damit ist die Suche nach Testdaten fur ¨ Transitionen aus Statecharts a¨ quivalent mit der bereits in Abschnitt 6.3.2 diskutierten Suche nach Testdaten, die die Vorbedingung einer OCL-Methodenspezifikation erfullen. ¨ Wird ein Verfahren zur Identifikation von Testdaten, die eine OCL-Vorbedingung erfullen, ¨ verwendet, so kann dieses Verfahren aufgrund der in Abschnitt 6.6.2, Band 1 vorgestellten Transformation von Statecharts in OCL auch fur ¨ Transitionen in Lebenszyklen-Statecharts verwendet werden. Ein Vorteil des hier skizzierten transitionsbasierten Ansatzes ist die einfache Prufbarkeit ¨ jeder Transition, die auch in effizienter ablaufenden Tests mundet. ¨ Nachteil ist, dass nicht erkannt wird, ob ein Zustand uberhaupt ¨ erreichbar ist. Von solchen Zust¨anden ausgehende Transitionen mussen ¨ nicht getestet werden, da sie keinen Beitrag zum Systemverhalten leisten. Der skizzierte Ansatz ist a¨ quivalent zur Transitionsuberdeckung, ¨ ignoriert aber die Pfade vom Startzustand bis zum Quellzustand der eigentlich durchzufuhrenden ¨ Transition. 6.5.6 Weiterfuhrende ¨ Ans¨atze Wie bereits erw¨ahnt, ist es derzeit ein Forschungsgebiet, aus automatenartigen Beschreibungstechniken mit verschiedenen Verfahren Testf¨alle zu ermitteln. Exemplarisch werden die folgenden Ans¨atze kurz skizziert, die den Fortschritt in diesem Bereich demonstrieren. Fur ¨ einfachere Varianten von zustandsbasierten Systemen ist zum Beispiel in [GH99] ein Verfahren angegeben, mit dem aus einem ausfuhrbaren ¨ Automaten mithilfe von Model-Checking automatisierte Tests generiert werden konnen. ¨ Diese Tests prufen ¨ eine Implementierung gegen den als Orakelfunktion verwendeten Automaten. Diese Automaten haben aber keine Zustandshierarchie, keinen Nichtdeterminismus und eine im Vergleich zu den
6.6 Zusammenfassung und offene Punkte beim Testen
201
hier pr¨asentierten Statecharts eingeschr¨ankte Form von Transitionsmarkierungen. In dem Verfahren wird eine Transitionsuberdeckung ¨ des Automaten erreicht. Model-Checking wird dabei vor allem verwendet, um Aufrufsequenzen fur ¨ den Automaten zu finden, die Vorbedingungen der jeweils zu testenden Transition erfullen. ¨ Dieses Verfahren wird außerdem auf zwei Arten verfeinert. Zum einen werden aus Disjunktionen bestehende Vorbedingungen zerlegt und damit implizit die Transitionen geteilt, um so eine ¨ Uberdeckung aller Klauseln einer Disjunktion zu erreichen. Zum anderen wird eine einfache Grenzwertanalyse gemacht, indem Randvergleiche wie etwa a>=b in zwei F¨alle a>b und a==b zerlegt werden. In [FHNS02] wird ein Ansatz beschrieben, aus einem Zustandsautomaten durch Projektion eine Verringerung des Zustandsraums zu erhalten. ¨ Dadurch wird eine Uberdeckung des als Projektion erhaltenen Automaten durch Tests nach den Kriterien Zustands- und Transitionsuberdeckung ¨ moglich. ¨ Allerdings h¨angt es von der Wahl der Projektion ab, welche Pfade durch das System wirklich getestet werden und welche Fehler nicht entdeckt werden, weil sie uber ¨ mehrere der projezierten Automaten verteilt sind. Dieses Verfahren ist hilfreich, wenn man ein Objekt mit vielen Zust¨anden hat. In objektorientierten Systemen ist es in diesem Fall ratsam (wenn auch nicht immer moglich), ¨ durch Auslagerung von Teilfunktionalit¨at diese Projektion bereits beim Entwurf vorzunehmen. Als Nebeneffekt entstehen mehrere Objekte, deren Zustandsraum jeweils einer Projektion entsprechen kann. Das Projektionsverfahren aus [FHNS02] bietet aber zus¨atzliche Flexibilit¨at, da es uberlappende ¨ Projektionen erlaubt und so uberlappende ¨ Sichten fur ¨ Tests zul¨asst. Der Ansatz in [CCD01] diskutiert a¨ hnlich wie hier den Einsatz einer restringierten Form der UML-Statecharts fur ¨ die Gewinnung von Tests. Fur ¨ die Testdaten werden ebenfalls Objektdiagramme eingesetzt. Jedoch erkl¨aren die Autoren der noch in Weiterentwicklung befindlichen Arbeit mehr die Architektur einer auf XML basierenden Werkzeugkopplung, als die Vorgehensweise aus den hierarchischen Statecharts Testf¨alle zu generieren.
6.6 Zusammenfassung und offene Punkte beim Testen Zusammenfassung Qualit¨atssicherung ist ohne systematisches und diszipliniertes Entwickeln von Tests nicht moglich. ¨ Die Definition von Testf¨allen ist aber aufw¨andig, ¨ insbesondere da fur ¨ gesch¨aftskritische Systeme eine gute Uberdeckung durch Testf¨alle notwendig ist. Die effiziente Entwicklung und Darstellung von Testf¨allen spielt daher eine wesentliche Rolle. Die Techniken der UML/P erlauben eine kompakte und ubersichtliche ¨ Darstellung von Tests durch die Kombination verschiedener Diagramme zur Darstellung der Testdaten, des
202
6 Modellbasierte Tests
Testtreibers und des Sollergebnisses sowie von Invarianten des Systems. Diese Diagramme und Spezifikationen konnen ¨ unabh¨angig voneinander entwickelt werden, sind kompakter und leichter verst¨andlich als Testcode und erleichtern daher die Wiederverwendung. In einer Test-First-Vorgehensweise konnen ¨ mittels Sequenzdiagrammen zun¨achst Verhaltensmuster spezifiziert werden, die gegebenenfalls mit Erg¨anzungen als Testfallbeschreibungen einsetzbar sind, bevor eine Implementierung vorgenommen wird. Damit ist dieser Ansatz eine Erweiterung des klassischen Vorgehens Sequenzdiagramme bei der fruhen ¨ Anforderungserhebung einzusetzen. Nach einer Implementierung ist die Definition weiterer Testf¨alle sinnvoll, um Rand- und Sonderf¨alle zu prufen. ¨ Wie intensiv getestet wird, h¨angt von der gewunschten ¨ Qualit¨at und der zugrunde liegenden Methodik ab. In einem agilen Ansatz werden vor allem an besonders kritischen und komplexen Stellen Metriken zur Analyse der Testqualit¨at eingesetzt und sonst auf die Erfahrung der Testentwickler vertraut. Ein UML/P-Modell kann Gegenstand des Tests sein, wenn das Modell konstruktiv zur Beschreibung des Systems verwendet wird. Ein Modell kann aber auch als testbare Spezifikation eingesetzt werden, wenn bereits eine Implementierung gegeben ist. Wie bereits die Verfahren zur Codegenerierung in Kapitel 3 und Kapitel 4 gezeigt haben, konnen ¨ einzelne Konzepte des Modells auch unterschiedliche Rollen ubernehmen ¨ und je nach Ansatz der Generierung konstruktiv oder als Testcode eingesetzt werden. Nach einer Einfuhrung ¨ in die Begriffswelt der Testverfahren und einer kurzen Beschreibung zweier wesentlicher Testwerkzeuge wurde in diesem Kapitel demonstriert, wie UML/P-Diagramme zur Modellierung von Tests eingesetzt werden. Trotz vorhandener Literatur zum Thema Testen von Software gibt es gerade beim Konformit¨atstest von Code, der aus graphisch notierten Implementierungsmodellen generiert wurde, eine Reihe von Verbesserungsmoglich¨ keiten und offenen Enden, von denen einige nachfolgend diskutiert werden. In diesem Buch sind außerdem Lasttests, Qualit¨atssicherungsmaßnahmen wie Inspektionen und Modellreviews oder die Vorgehensweisen fur ¨ interaktive Tests unter Nutzerpartizipation zum Zweck der Abnahme nicht behandelt. Entwicklungspotential bei der Testfallgenerierung Die Anwendung von UML/P-Diagrammen und insbesondere OCL-Spezifikationen zur Generierung automatisierter Testf¨alle bietet noch großes Entwicklungspotential. Wie in Abschnitt 6.3 beschrieben, w¨are eine moglichst ¨ effektive Generierung einer den Code ausreichend uberdeckenden, ¨ aber mog¨ lichst kleinen Menge von Testf¨allen aus einer OCL-Methodenspezifikation hilfreich, um in noch effizienterer Form Testf¨alle zu entwickeln. Das gleiche gilt fur ¨ die in Abschnitt 6.5 diskutierten Statecharts.
6.6 Zusammenfassung und offene Punkte beim Testen
203
Im Kontext der UML und der OCL sind noch relativ wenig Arbeiten zum Thema Testen bekannt. Das liegt teilweise daran, dass lange davon ausgegangen wurde, dass die Testverfahren, die im Wesentlichen alle bereits fur ¨ prozedurale Programmierung entwickelt wurden [Bei90, Lig90], nur auf Objektorientierung zu ubertragen ¨ w¨aren. Durch die Vererbung und die dynamische Bindung von Methoden entstehen aber grunds¨atzlich neue Problemstellungen, die erst seit kurzem aufgearbeitet werden [Bin99]. Als weitere Problemquelle wird die Entkopplung der Sprache UML und der vielen, teilweise sehr unterschiedlichen Vorgehensmodelle auf Basis der UML genannt [BL02], die sehr unterschiedlich detaillierte und damit unterschiedlich testbare Modelle einsetzen. Derzeit ist dennoch eine steigende Anzahl von Arbeiten zu erkennen, die verschiedene Testverfahren auf die UML ubertragen, ¨ ohne allerdings das Potential fur ¨ Code- und Testgenerierung bereits auszuschopfen. ¨ [PJH+ 01] ¨ besch¨aftigen sich etwa mit der Ubertragung der aus TTCN [ISO92] bekannten Verwendung von Sequenzdiagrammen zur Testfallmodellierung. [BL02] beschreiben die systematische und teilweise automatisierte Entwicklung von Testf¨allen aus UML-Analysedokumenten wie Use Case Diagrammen, Sequenz-, Kommunikations- und das Dom¨anenmodell darstellenden Klassendiagrammen. In [BB00] wird ein a¨ hnlicher Ansatz verfolgt, der ebenfalls eine Form von Sequenzdiagrammen zur Beschreibung von Interaktionen zwischen Objekten verwendet, und dies mit einem bereits bekannten Verfahren zur Kategorie-Partitionierung [OB88] kombiniert. In [PLP01] wird beschrieben, wie fur ¨ eine fur ¨ verteilte Systeme geeignete, allerdings nicht an der UML angelehnte, graphische Modellierungssprache Techniken des Constraint Reasoning angewandt werden, um aus einer abstrakten Testfallspezifikation eine Sammlung von Testf¨allen (dort als Testse” quenzen“) bezeichnet, zu generieren. Allerdings ist die Form der betrachteten ¨ Systeme dort statisch und deshalb die Ubertragung der Algorithmen auf dynamische, objektorientierte Systeme nicht kanonisch. Bereits in den Publikationen [DN84, HT90] wurde beschrieben, dass die zufallsbasierte Generierung von Testdaten fur ¨ Entwicklung von Zutrauen in die Korrektheit der Implementierung a¨ hnlich gute Ergebnisse aufweist wie partitionsbasierte Testverfahren. Wenn diese Ergebnisse sich auch bei den heutigen Sprachen und Testverfahren bewahrheiten, dann bedeutet dies, dass die zufallsbasierte Generierung von Tests ein wesentliches Hilfsmittel fur ¨ Testverfahren sein kann. Insbesondere Statecharts und OCLNachbedingungen sind dann als Orakel fur ¨ solchermaßen generierte Tests hilfreich. Ist es erwunscht, ¨ die generierten Tests zu speichern, so konnen ¨ dafur ¨ Objekt- und Sequenzdiagramme verwendet werden. Wie bereits in [HT90] vermerkt, muss bei den Testverfahren eine weitere Verlagerung von der arbeitsaufw¨andigen manuellen Erstellung zur automatisierten Testgenerierung stattfinden. Dieses Kapitel konnte nur einen Teil der insgesamt zum Thema Testen existierenden Konzepte, Techniken und Vorgehensweisen diskutieren. An
204
6 Modellbasierte Tests
verschiedenen Stellen wurde deshalb auf entsprechende Spezialliteratur verwiesen. Generell ist festzustellen, dass fur ¨ eine effektive Unterstutzung ¨ der Testfallentwicklung nicht nur fur ¨ die UML gute und parametrisierte Generatoren existieren mussen, ¨ sondern auch Unterstutzung ¨ fur ¨ Testmuster sowie fur ¨ einzubindende Frameworks und Komponenten anzubieten sind. In objektorientierten und insbesondere agilen Vorgehensmodellen ist der Trend zu beobachten, dass das Produktionssystem explizit so strukturiert wird, dass es gut testbar ist. Dadurch ist es nicht mehr notwendig eine Testsequenz zu definieren, die ausgehend von einem Initialzustand eine bestimmte Transition, Anweisung oder Methode pruft. ¨ Stattdessen kann eine Objektstruktur angegeben werden, die direkt zu dessen Prufung ¨ fuhrt. ¨ Objektstrukturen sind viel leichter zu finden, der Test wird effektiver in der Ausfuhrung ¨ und besser verst¨andlich. Symbolisches Testen Ein Beispiel fur ¨ die potentielle Weiterentwicklung der Testverfahren basiert auf der Interpretation von symbolischen statt echten Werten. So kann die Verwendung von abstrakten, durch Symbole repr¨asentierten Elementen in Objektdiagrammen, wie in Abschnitt 5.2.2, Band 1 diskutiert, als Grundlage fur ¨ eine interessante Erweiterung des Einsatzes dieser Diagramme dienen, die hier als Ausblick skizziert wird. Durch die Nutzung symbolischer Werte konnen ¨ Testf¨alle verallgemeinert werden. Dabei wird in einer Vorbedingung ein symbolischer Wert angegeben, der innerhalb der getesteten Methode ver¨andert oder fur ¨ die Berechnung anderer Attribute genutzt wird. Dabei wird der Ausdruck nicht ausgewertet, sondern unausgewertet als Term abgelegt. In der Nachbedingung kann dann statt eines konkreten Werts gepruft ¨ werden, ob der zur Berechnung verwendete Term der gewunschten ¨ Intention entspricht. Die einfachste Form des Vergleichs ist die syntaktische Gleichheit, jedoch kann durch geeignete algebraische Umformung auch ein semantisch a¨ quivalenter Term angegeben werden. Ein Beispiel ist die folgende Berechnung, die auf umst¨andliche Weise das Maximum beider Argumente ergibt: int foo(x,y) { int a = x; int b = y; a = a-b; if(x < y) b = b-a; return a+b; }
Java
Dabei wird statt mit konkreten Werten mit den symbolischen Werten x und y gerechnet. Unter der zus¨atzlichen Annahme x
6.6 Zusammenfassung und offene Punkte beim Testen int foo(x,y) { int a = x; int b = y; a = a-b; if(x < y) b = b-a; return a+b; }
// Wert a= b= // x // x y // x-y y // x-y y-(x-y) // Ergebnis: (x-y)+(y-(x-y))
205 Java
Tats¨achlich ist das Ergebnis (x-y)+(y-(x-y)) a¨ quivalent zu y. In analoger Form kann die Negation der Bedingung !(x
206
6 Modellbasierte Tests
mit welcher Wahrscheinlichkeit ankommen. Auf dieser Gewichtung basiert dann die Auswahl der Testpfade fur ¨ das Statechart. Alternativ, allerdings in Cleanroom selbst nicht propagiert, w¨are auch die Gewichtung der Zust¨ande nach der erwarteten H¨aufigkeit des Auftretens von Interesse. Außerdem l¨asst sich dasselbe Verfahren moglicherweise ¨ auch bei Klassendiagrammen gewinnbringend einsetzen, um eine Gewichtung der Objektstrukturen zu erreichen, die als Testdatens¨atze einzusetzen sind. Die Verwendung statistischer Verfahren erlaubt in Cleanroom die Vorhersage von durchschnittlichen Fehlerh¨aufigkeiten in Abh¨angigkeit der Anzahl durchgefuhrter ¨ Tests und der dabei gefundenen Fehler. Insbesondere kann so ein pr¨azises Kriterium fur ¨ das Testende in Abh¨angigkeit der gewunsch¨ ten Qualit¨at (Fehlerh¨aufigkeit) entwickelt werden. Eine der wesentlichen Grundvoraussetzungen dafur ¨ ist allerdings die Erhebung einer aussagekr¨aftigen Menge an Daten, wie gut die so entwickelten Tests tats¨achlich Fehler entdecken. Solche Daten h¨angen von der verwendeten Programmiersprache ab und konnen ¨ nur in geeigneten Feldversuchen erhoben werden.
7 Testmuster im Einsatz
Die zwei großten ¨ Tyrannen der Erde: der Zufall und die Zeit. Johann Gottfried von Herder
Erg¨anzend zur Beschreibung der Theorie und der allgemeinen Vorgehensweise bei der Entwicklung von Tests wird in diesem Kapitel der Einsatz der UML/P anhand von Testmustern demonstriert. Mit diesen Mustern wird unter anderem die Definition von Dummies und von funktionalen Tests fur ¨ nebenl¨aufige und verteilte Systeme skizziert.
7.1 7.2 7.3 7.4 7.5 7.6
Dummies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testbare Programme gestalten . . . . . . . . . . . . . . . . . . . . . . . . Behandlung der Zeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Nebenl¨aufigkeit mit Threads . . . . . . . . . . . . . . . . . . . . . . . . . Verteilung und Kommunikation . . . . . . . . . . . . . . . . . . . . . . Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
210 215 222 229 237 245
208
7 Testmuster im Einsatz
Die Sammlung von Entwurfsmustern ( Design Patterns“) [GHJV94] ist ” eine der ersten Arbeiten, die gezeigt hat, dass in Softwareentwicklungsprojekten erprobte Strukturen und Techniken herausgearbeitet, dokumentiert und dadurch wiederverwendet werden konnen. ¨ Auch fur ¨ Testzwecke lassen sich allgemein gultige ¨ und themenspezifische Muster identifizieren. In diesem Abschnitt werden einige Testmuster exemplarisch beschrieben, um damit zu zeigen, wie die UML/P zur Umsetzung solcher Testmuster verwendet werden kann. Die hier behandelten Testmuster konzentrieren sich auf die Themen: • • • • •
Dummies1 , Gestaltung testbarer Programme in Bezug auf die Verwendung statischer Variablen, Objekterzeugung und die Verwendung vorgefertigter Frameworks, Simulation von Zeit, Nebenl¨aufigkeit, Verteilung und Kommunikation.
Diese Testmuster dienen vor allem der Pr¨aparation des Testlings und dessen Umgebung, so dass damit effektiv Tests der Funktionalit¨at durchgefuhrt ¨ werden konnen. ¨ Dummies dienen zum Beispiel zur Simulation der Umgebung des Testlings und fangen Seiteneffekte ab oder stellen vorgefertigte Aufrufergebnisse bereit. Die in den Testmustern behandelten Konzepte konnen ¨ auch nachtr¨aglich in bereits vorhandene Software eingebracht werden. Dazu eignet sich die in Kapitel 9 diskutierte Einbettung dieser Testmuster in entsprechende Regeln fur ¨ das Refactoring von Modellen fur ¨ den Einbau der Testmuster. Eine empfehlenswerte Sammlung an Testmustern, die Tests von einzelnen Methoden bis zu vollst¨andigen Systemen diskutiert, ist in [Bin99] ent¨ halten. Darin wurden zum Beispiel Teststrategien fur ¨ die Uberdeckung von Kontrollflussen, ¨ Integrations- oder Regressionstests diskutiert. Erg¨anzend dazu behandelt [LF02] eher technologiespezifische Themen wie Persistenz mithilfe von Datenbanken, Kommunikation unter Zuhilfenahme von CORBA [OH98], die Verwendung von Frameworks wie Enterprise JavaBeans [MH00] oder Technologien wie Java Server Pages [FK00]. Der Verbesserung des Testprozesses widmet sich [PKS02]. Dabei werden Checklisten genutzt, um beispielsweise eine projektspezifische Optimierung des Testprozesses zu erreichen. Ein Testmuster ist in Analogie zu den Entwurfsmustern in [GHJV94], eine generische Beschreibung eines wiederkehrenden Entwurfsproblems, das speziell die Testbarkeit des Systems unterstutzt. ¨ Dabei wird auf die Vorgehensweise zur Durchfuhrung ¨ von Tests, auf die dabei notwendigen strukturellen Ver¨anderungen des zu testenden Codes und die Strategie der Testdurchfuhrung ¨ eingegangen. Weil jedoch diese Strategie im Sinne einer agilen 1
Fur ¨ Dummy“ ist auch der Begriff Mock“ gebr¨auchlich. ” ”
7 Testmuster im Einsatz
209
Methodik vor allem durch den Test-First-Ansatz (siehe Abschnitt 2.3.1, Band 1 und [Rum04a]) getrieben wird, liegt bei diesen Testmustern im Gegensatz zu [Bin99] der Fokus vor allem auf der Unterstutzung ¨ der Durchfuhrung ¨ effektiver Tests. Dabei wird konsequent eine Systemarchitektur gefordert, die sich fur ¨ Tests eignet, und diskutiert, wie diese auch bei vorgefertigten Frameworks und Komponenten umgesetzt werden kann. Dass ein System so entworfen werden sollte, dass es fur ¨ Tests kontrollierbar und beobachtbar ist, setzt sich erst langsam durch [Bin94, HBG01]. Ein Teil dieses Kapitels ist dem funktionalen Test verteilter Systeme gewidmet, die intern und mit der Umgebung asynchron kommunizieren oder mehrere nebenl¨aufige Threads besitzen. Es erg¨anzt damit die bereits genannten Testmustersammlungen. Verteilte Systeme sind inh¨arent nichtdeterministisch. Zum Beispiel wird die Verarbeitung von Nutzereingaben aus der Oberfl¨ache aus Grunden ¨ der besseren Reaktivit¨at durch verschiedene Threads gehandhabt. Ebenso werden Internet-Anfragen nebenl¨aufig bearbeitet, so dass heute nahezu in jedem System Nebenl¨aufigkeit auftritt. Auch das Auktionssystem ist durch das Internet auf viele Clients und Server verteilt, besitzt aber auch innerhalb jedes dieser Systemteile mehrere Threads. Fur ¨ automatisierte Tests sind nichtdeterministische Abl¨aufe grunds¨atzlich kritisch, da sie die Bewertbarkeit des Ergebnisses erschweren. Durch Ver¨ wendung von OCL-Bedingungen zur Uberpr ufung ¨ des Testerfolgs, die eine gewisse Bandbreite zulassen, ist es prinzipiell moglich, ¨ mit nichtdeterministischen Ergebnissen bei Testabl¨aufen umzugehen. Jedoch geht ein wesentliches Kriterium fur ¨ automatisierte Tests, die Wiederholbarkeit, verloren. Ein nur sporadisches Scheitern eines Tests kann vom Entwickler oft nur schwer nachvollzogen werden und macht die Behebung des Fehlers schwierig. Fur ¨ funktionale Tests ist es deshalb sinnvoll, nichtdeterministische Effekte vollst¨andig zu unterbinden. Dies kann durch geschickte Verwendung von Dummies sowie durch die explizite Festlegung an sich paralleler Abl¨aufe durch ein kontrolliertes Scheduling erfolgen und wird in den nachfolgenden Abschnitten erortert. ¨ Die Systematik zur Vorstellung von Mustern, wie sie in [GHJV94] eingefuhrt ¨ und seitdem fur ¨ viele Musterarten fortgefuhrt ¨ wurde, kann auch fur ¨ Testmuster eingesetzt werden und wird in diesem Abschnitt vereinzelt angewandt. Dabei wird nur eine Teilmenge des Schemas aus [Bin99] verwendet, da diese Technik vor allem zusammenfassend am Ende einer Diskussion eingesetzt wird. Der Abschnitt 7.1 diskutiert die Einfuhrung ¨ von Dummies zur Definition der Umgebung eines Testlings. Abschnitt 7.2 beschreibt generelle, sich aus der zugrunde liegenden Sprache ergebende Probleme beim Testen objektorientierter Systeme und gibt Richtlinien zur Strukturierung des Produktionssystems, so dass dieses fur ¨ Tests besser zug¨anglich ist. Dazu gehoren ¨ zum Beispiel die Kapselung statischer Variablen und die Dynamisierung der Objekterzeugung durch das Factory-Entwurfsmuster. In Abschnitt 7.3 wird die Simulation von Zeit in einem funktionalen, wiederholbaren und damit
210
7 Testmuster im Einsatz
auf Determiniertheit ausgerichteten Test diskutiert. Die Abschnitte 7.4 und 7.5 behandeln Nebenl¨aufigkeit, Verteilung und Kommunikationsaspekte, die die Definition funktionaler Tests uber ¨ die Grenzen einzelner Prozesse hinaus erschweren. Es werden mehrere Testmuster eingefuhrt, ¨ die die Testbarkeit funktionaler Eigenschaften solcher Systeme signifikant verbessern beziehungsweise erst ermoglichen. ¨
7.1 Dummies Ein Dummy ist ein Objekt, das einen Teil der Umgebung des Testlings simuliert und dem Testling damit eine Umgebung zur Verfugung ¨ stellt, in der sein Verhalten gepruft ¨ werden kann. Die Verwendung von Dummies hat sich durch die objektorientierte Programmierung wesentlich vereinfacht, weil sich durch Vererbung und dynamisches Redefinieren von Methoden und durch die Implementierung von Interfaces relativ leicht Dummy-Objekte ins System einbringen lassen, ohne den Produktionscode davon durch syntaktische Beziehungen (beispielsweise import) abh¨angig zu machen. Abbildung 7.1 zeigt, wie typischerweise Dummy-Klassen definiert werden. Diese Klassen erhalten per Konvention den Suffix Dummy“, lassen sich aber auch ” durch einen geeigneten Stereotyp Dummy kennzeichnen und stehen nur im Testsystem zur Verfugung. ¨
Abbildung 7.1. Dummy-Klassen redefinieren Methoden
Damit konnen ¨ beispielsweise Testf¨alle fur ¨ die Klasse Auction aufgebaut werden. Abbildung 7.2 zeigt einen Ausschnitt eines Objektdiagramms, das fur ¨ den Aufbau solcher Testf¨alle verwendet wird. In den Dummy-Klassen konnen ¨ Methoden geeignet uberschrieben ¨ werden, die w¨ahrend des Tests verwendet werden. Eine einfache Dummy-Methode gibt ein vorgegebenes, konstantes Ergebnis zuruck ¨ oder liest Ergebnisse der Reihe nach aus einer Liste. Beispielsweise wurde im Auktionsprojekt das auf einen Test spezialisierte Dummy aus Abbildung 7.3 verwendet, um Zeitabfragen zu simulieren.
7.1 Dummies
211
Abbildung 7.2. Testsituation fur ¨ ein Auction-Objekt class TimingPolicyDummy implements TimingPolicy { Time[] timelist = new Time[] { new Time("14:42:22", "Feb 21 2000"), new Time("14:42:23", "Feb 21 2000"), new Time("14:44:18", "Feb 21 2000"), new Time("14:59:59", "Feb 21 2000") }; int count = 0;
Java/P
public Time newCurrentClosingTime(Auction a, Bid b) { ocl count < timelist.length; return timelist[count++]; }} Abbildung 7.3. Dummy mit Ergebnissen fur ¨ vier Aufrufe
Fur ¨ die Tests einfacher Klassen mit wenig Funktionalit¨at sind meist keine Dummies notwendig. Fur ¨ andere Klassen konnen ¨ abh¨angig vom Testzweck mehrere verschiedene Dummies notwendig sein. Entsprechend werden mehrere Klassen oder eine parametrisierte Dummy-Klasse realisiert. Zum Beispiel kann TimingPolicyDummy einen Konstruktor mit der Liste der auszugebenden Time-Objekte als Parameter anbieten oder mehrere Listen besitzen, von denen eine ausgew¨ahlt wird. 7.1.1 Dummies fur ¨ Schichten der Architektur Abh¨angig vom Testziel konnen ¨ statt einzelnen Objekten auch Gruppen von Objekten getestet werden. Zum Beispiel l¨asst sich die Funktionalit¨at des Servers dadurch testen, dass sowohl die Policy- als auch die Personen-Objekte jeweils aus dem Produktionssystem sind und nur deren Umgebung wie Persistenzmechanismen, Protokollierung und Internet-Anschluss durch Dum-
212
7 Testmuster im Einsatz
mies ersetzt werden. Generell lassen sich fur ¨ Tests verschiedene Umgebungen gruppieren. So schl¨agt [Bin99] Tests fur ¨ einzelne Schichten einer Architektur nach dem in Abbildung 7.4 gegebenen Muster vor. Dabei wird jeweils eine Schicht getestet und die darunterliegende Schicht durch Dummies ersetzt (a), (b). Bei weitergehenden Integrationstests (c) konnen ¨ auch Schichten gekoppelt werden. Da eine Schicht selbst aus einer Gruppe von Objekten besteht, kann diese umgekehrt auch zerlegt werden und durch Klassen- und Methodentests, wie bereits gezeigt, einzelne Objekte getestet werden.
Abbildung 7.4. Testumgebungen in der Schichtenarchitektur
Ruft die zu testende Methode andere Methoden desselben Objekts auf, so kann es sinnvoll sein, von der Klasse mit dem Testling selbst eine Unterklasse zu bilden und darin die aufgerufenen Methoden durch Dummy-Methoden zu ersetzen. Testling und Dummy sind dann im gleichen Objekt. 7.1.2 Dummies mit Ged¨achtnis Eine Verfeinerung der Testumgebung kann dadurch vorgenommen werden, dass diese mitprotokolliert, in welcher Form sie vom Testling genutzt wird und gegebenenfalls dieses Ged¨achtnis nutzt, um die Return-Ergebnisse daraus zu berechnen. Im Umfeld des Extreme Programming-Ansatzes [MFC01] werden solche Dummies auch Mock-Objekte und in Telekommunikationsnahen Ans¨atzen Stubs genannt. Ein einfaches Beispiel ist bereits in Abbildung 7.3 gegeben. Mit dem Dummy-Objekt kann nach Ende des Tests gepruft ¨ werden, wie oft die Methode newCurrentClosingTime aufgerufen wurde, indem der Z¨ahlerstand ausgelesen wird. Ein weiteres Beispiel fur ¨ einen solchen Dummy mit nachtr¨aglicher Abfragefunktion bietet ein Protokollobjekt, das zur Protokollierung von wesentlichen Ereignissen im Auktionssystem dient. Dazu gehort ¨ zum Beispiel auch die Mitteilung uber ¨ die Abgabe eines neuen Gebots. Deshalb wird neben den eigentlichen Testdaten ein Dummy-Objekt der Klasse ProtocolDummy realisiert. Abbildung 7.5 zeigt das Klassendiagramm fur ¨ eine vereinfachte Form der Protokollierung. Mit der Klasse ProtocolDummy konnen ¨ nun Tests um
7.1 Dummies
213
das Protokoll erweitert werden. Dabei ko¨ nnen im Testdatensatz das Objektdiagramm ProtBefore und im Sollergebnis zum Beispiel ProtAfter aus Abbildung 7.6 zum Einsatz kommen.2
class ProtocolDummy { writeToLog(String text) { logCount++; logLastLine = text; } }
Java/P
Abbildung 7.5. Protokoll-Dummy erlaubt das Abfangen von Seiteneffekten
Der hier dargestellte Dummy speichert nur die letzte Zeile sowie die Gesamtzahl aufgetretener Meldungen. Manchmal ist auch die Speicherung aller Meldungen in einer Liste oder die selektive Speicherung nach bestimmten Mustern im String sinnvoll.
Abbildung 7.6. Objekt des Protokoll-Dummy beschreibt das Sollergebnis
2
Ein Ausbau zur Unterscheidung zwischen Warnungen, Fehlern und Mitteilungen sowie der Einstellbarkeit der Ausgabeverbosit¨at und des Debug-Levels ist in der Praxis sinnvoll.
214
7 Testmuster im Einsatz
7.1.3 Sequenzdiagramm statt Ged¨achtnis Im ProtocolDummy ist ein Ged¨achtnis notwendig, damit auf Basis der gespeicherten Daten Ruckschl ¨ usse ¨ auf die w¨ahrend des Testablaufs durchgefuhrten ¨ Interaktionen vorgenommen werden konnen. ¨ Da Sequenzdiagramme geeignet sind, Interaktionen zu protokollieren, konnen ¨ sie als Ersatz fur ¨ speichernde Dummies eingesetzt werden. Abbildung 7.7 beschreibt den Protokoll-Aspekt des Testablaufs aus Abbildung 6.12, indem es angibt, wie oft und mit welchen Argumenten die Methode writeToLog aufgerufen wird.
Abbildung 7.7. Sequenzdiagramm pruft ¨ Protokollverhalten
Die Methode writeToLog muss im Dummy der Klasse Protocol kein Ged¨achtnis besitzen. Stattdessen reicht es, zum Beispiel durch einen leeren Rumpf sicherzustellen, dass keine Seiteneffekte auftreten. Dafur ¨ wird die automatisch generierte Klasse ProtocolSimpleDummy eingesetzt. ¨ Wegen der h¨aufigen Anderungen bei der Ausgabe von Texten ist es oft besser, statt einer vollst¨andigen Angabe des Protokolltexts nur bestimmte Eigenschaften zu fordern. So kann der Methodenaufruf mit writeToLog(s) modelliert werden und der String s auf das Enthaltensein von Angaben gepruft ¨ werden: s.indexOf("552.000,00 $US")>=0. Das Beispiel in Abbildung 7.7 enth¨alt neben dem Treiberobjekt auch einen Dummy. Dies demonstriert, dass Dummies in Sequenzdiagrammen wie normale Objekte eingesetzt werden konnen, ¨ so dass die gesamte Interaktion im Testablauf modelliert werden kann. Allerdings bedeutet dies, dass auch Dummies vom Generator zu instrumentieren sind, um eine Beobachtung zu ermoglichen. ¨
7.2 Testbare Programme gestalten
215
7.1.4 Abfangen von Seiteneffekten Das im vorherigen Abschnitt angewandte Verfahren demonstriert, wie ein Testling in eine Testumgebung von oben“ durch den Testtreiber und von ” unten“ durch Dummies in eine Testumgebung eingebettet werden kann ” und damit auf allen Seiten die fur ¨ die Prufung ¨ des Testerfolgs relevanten Daten und Interaktionen gesichert werden konnen. ¨ Insbesondere konnen ¨ auf diese Weise alle Effekte des Testlings, die die Systemumgebung verlassen, verhindert werden. Die Verhinderung von Seiteneffekten ist eine wesentliche Voraussetzung fur ¨ schnelle und effiziente Tests. Allerdings muss auch die Protokoll-Klasse selbst getestet werden. Generell ist fur ¨ Tests der letzten Schicht vor einem Dateisystem, einer Datenbank oder einer Kommunikationsschicht Aufwand zu betreiben, um eine definierte Ausgangssituation der Umgebung sicherzustellen, nach Ende des Tests auf die Testergebnisse zugreifen zu konnen ¨ und aufzur¨aumen. Beispielsweise mussen ¨ Protokoll-Dateien oder die Datenbank entsprechend eingerichtet sein. [LF02] enth¨alt dazu detaillierte Beschreibungen. Abschnitt 7.5 dieses Buchs behandelt nur die Simulation verteilter Kommunikation ohne die Verteilung tats¨achlich durchzufuhren. ¨
7.2 Testbare Programme gestalten Einer der Vorteile objektorientierter Systeme ist die verbesserte Testbarkeit, die darauf beruht, Unterklassen der Testumgebung zu bilden und dabei dynamisch Methoden zu redefinieren. Dennoch gibt es bei objektorientierten Systemen einige Problemstellungen, die die Definition von Testumgebungen, die Durchfuhrung ¨ des Tests oder die Feststellung des Testerfolgs erschweren oder sogar verhindern. Diese resultieren grunds¨atzlich aus statischen Attributen und Methoden, der Objekterzeugung und der Verwendung vordefinierter Frameworks oder Komponenten. Diese Probleme und ihre Behebung werden im Folgenden diskutiert. Dabei werden weitere Testmuster unter Verwendung von UML-Diagrammen vorgestellt. Interessanterweise sind Polymorphie und dynamische Bindung zwar Komplexit¨atstreiber fur ¨ Tests, aber aufgrund der dadurch entstehenden Flexibilit¨at auch wesentliche Elemente zur Testfalldefinition. Um ein System testbar zu machen, werden im Folgenden eine Reihe von Strukturver¨anderungen vorgeschlagen, die die Simulation einer Systemumgebung erleichtern oder erst ermoglichen. ¨ Zus¨atzlich wird der Testling fur ¨ die Testdurchfuhrung ¨ instrumentiert. Beide Arten von Ver¨anderungen greifen in das System ein. Die vorgeschlagenen Strukturver¨anderungen sind permanent und erfordern daher die Akzeptanz durch die Entwickler. Objektorientierte Methoden unterstutzen ¨ dieses Vorgehen tendenziell deutlich eher als fruhere ¨ Paradigmen. Außerdem ist es bei einer agilen Vorgehensweise
216
7 Testmuster im Einsatz
von Vorteil, dass die Tests nicht erst nach Fertigstellung des Systems entwickelt werden und damit a priori Einfluß auf die Systemstruktur nehmen konnen. ¨ Die Instrumentierung des Testlings ist demgegenuber ¨ nicht permanent und erfordert damit großte ¨ Vorsicht, da sie die Funktionsf¨ahigkeit des Testlings nicht ver¨andern darf. Vom Codegenerator korrekt durchgefuhrte ¨ Instrumentierungen fuhren ¨ zumindest zu einer geringfugigen ¨ Ver¨anderung der Laufzeiten und verf¨alschen damit Laufzeitmessungen, aber nicht funktionale Tests. 7.2.1 Statische Variablen und Methoden Eine immer wiederkehrende Problemstellung ist die Verwendung von statischen Variablen und Methoden. Soweit wie moglich ¨ sollte auf die Verwendung derartiger statischer Elemente verzichtet werden. Wo dies nicht moglich ¨ oder sinnvoll ist, sollte aber zum Beispiel statt der Variablen selbst ein Objekt verwendet werden, das die eigentliche Variable kapselt. Im Auktionsprojekt wurde zum Beispiel das in Abbildung 2.7 dargestellte SingletonObjekt AllData verwendet, um damit Zugang auf alle Auktionen, Personen und weitere Datenstrukturen zu erhalten. Dieses Objekt ist im UML-Modell durch das statische und mit dem Zugriffsrecht readonly versehene Attribut AllData.ad zug¨anglich. Bei der Codegenerierung wird daraus eine geeig¨ nete Zugriffsmethode erstellt, allerdings keine Methode zur Anderung des Attributwerts selbst. Das heißt, das Singleton ist von außen zugreifbar, aber gegen Ersetzung geschutzt. ¨ Dennoch wird nachfolgend ein Muster diskutiert, das statische Variablen vollst¨andig kapselt und statische Methoden fur ¨ Tests besser zug¨anglich macht. Das Singleton fur ¨ Protokolle wird im Attribut prot der Klasse Protocol abgelegt. Wie Abbildung 7.8 zeigt, wird zus¨atzlich eine Kapselung des Attributs in einer statischen Methode vorgenommen. Eine Meldung im Protokoll kann dann mit Protocol.writeToLog("Meldung")
Java
vorgenommen werden. Um die initiale Besetzung des Attributs prot sicherzustellen, pruft ¨ die Methode -das statische Attribut und besetzt es bei Bedarf, fuhrt ¨ dann aber ausschließlich eine Delegation an dieses Objekt durch. Die Ersetzung des Protocol-Objekts durch ein Dummy ist nun einfach. Die Methode doWriteToLog wird im Dummy uberschrieben ¨ und das Dummy-Objekt macht sich durch den Aufruf von setAsProtocol() selbst zum Protokollempf¨anger. Dieses Verfahren l¨asst sich in Tabelle 7.9 als Muster zusammenfassen. In Abschnitt 9.1.5 ist daruber ¨ hinaus eine Refactoring-Regel angegeben, die dieses Testmuster in ein bestehendes Modell der Struktur eines Systems einfuhrt. ¨
7.2 Testbare Programme gestalten
class Protocol { static public writeToLog(String text) { // Sicherstellung, dass das Objekt existiert if(prot==null) { prot = new Protocol(); // Alternative w¨are: Factory.getProtocolObject(); } // Delegation prot.doWriteToLog(text); } protected doWriteToLog(String text) { // Hier wird die eigentliche Ausgabe vorgenommen . . . } } class ProtocolDummy { setAsProtocol() { prot = this; } public doWriteToLog(String text) { // Redefinition logList.add(text); } }
217
Java/P
Abbildung 7.8. Globales Protokoll-Objekt mit Ersetzungsmo¨ glichkeit
Muster: Singleton hinter statischen Methoden Intention
Das Muster ermoglicht ¨ einerseits kompakten Zugriff auf ein Singleton-Objekt, das fur ¨ Tests durch ein Dummy ersetzt werden kann, vermeidet aber andererseits eine offentlich ¨ zug¨angliche statische Variable fur ¨ dieses Objekt. (Fortsetzung auf n¨achster Seite)
218
7 Testmuster im Einsatz (Fortsetzung von Tabelle 7.9.: Muster: Singleton hinter statischen Methoden)
Motivation
Siehe vorherige Diskussion zur Testbarkeit von Code mit statischen Variablen. Beispiele sind Objekte, die Protokoll-Aufgaben, Abfragen der Zeit oder einen FactoryMechanismus realisieren. Anwendung Eine Anwendung dieses Musters ist sinnvoll, wenn • von einer Klasse nur ein Singleton, dieses aber an vielen Stellen benutzt wird, • ein kompakter Zugriff der Form Singleton.method() gewunscht ¨ ist, • die Speichervariable fur ¨ das Singleton verborgen bleiben soll und • das Singleton in Tests durch ein Dummy ersetzbar sein soll. Struktur
Implementierung Singleton
class Singleton { Java/P static initialize() { initialize(new Singleton( ... )); } static initialize(Singleton s) { singleton=s; } static method(Arguments) { // eigene Initialisierung wenn notwendig if(singleton==null) initialize(); // Delegation: return singleton.doMethod(Arguments); }
}
doMethod(Arguments) { // hier wird gearbeitet . . . }
(Fortsetzung auf n¨achster Seite)
7.2 Testbare Programme gestalten
219
(Fortsetzung von Tabelle 7.9.: Muster: Singleton hinter statischen Methoden)
Implementierung Dummy
class SingletonDummy { setAsSingleton() { initialize(this); } doMethod(Arguments) { // hier wird Arbeit simuliert . . . } }
Zugriff
Der Zugriff auf das Singleton erfolgt mit dem Ausdruck Singleton.method(Arguments). Eine vorherige Initialisierung ist nicht notwendig. Das Problem der unvollst¨andigen Initialisierung wird behoben, indem als Default ein Objekt der Klasse selbst erzeugt wird. Eine restriktivere Form konnte ¨ hier eine Fehlermeldung erzeugen, da erfahrungsgem¨aß gerade bei Tests die ad¨aquate Besetzung des Singletons gerne ubersehen ¨ wird.
Beachtenswert
Java/P
Tabelle 7.9.: Muster: Singleton hinter statischen Methoden
7.2.2 Seiteneffekte in Konstruktoren Eines der wesentlichen Probleme bei dem gezeigten Verfahren ist die durch Java vorgegebene Notwendigkeit, dass Objekte aus Unterklassen einen Konstruktor der Oberklasse aufrufen. Wenn dieser Konstruktor Seiteneffekte verursacht, also im Beispiel die Protokolldatei offnet, ¨ so ist die Definition von Dummies ohne Seiteneffekte fur ¨ diese Klasse nicht mehr moglich. ¨ Aus diesem Grund sollten Konstruktoren relativ wenig Funktionalit¨at beinhalten und gegebenenfalls zus¨atzliche Funktionen angeboten werden, die solche Initialisierungen vornehmen. Applets bieten mit der redefinierbaren Funktion init() eine solche Trennung an. 7.2.3 Objekterzeugung Ein a¨ hnlich gelagertes Problem ist die Erzeugung neuer Objekte. Ein Kommando der Form new Klasse() im Testling legt die Form des dabei entstehenden Objekts genau fest. Es ist hier nicht moglich, ¨ in Testl¨aufen statt dem angegebenen Objekt ein geeignetes Dummy einzusetzen. Dieses Problem l¨asst sich beheben, indem im Produktionscode eine Factory [GHJV94] eingesetzt wird. Wie in Abschnitt 4.1.7 beschrieben, kann diese Factory aus dem gegebenen UML-Modell erzeugt werden, indem fur ¨ alle auftretenden Konstruktoren entsprechende Methoden der Factory erzeugt werden. In analoger Weise werden durch den Generator alle Konstruktoraufrufe in den
220
7 Testmuster im Einsatz
Java-Coderumpfen ¨ durch Factory-Aufrufe ersetzt. Die Factory ist selbst ein Singleton, das typischerweise in einer statischen Variable abgelegt ist. Es kann daher mit dem bereits fur ¨ Protokolle angewandten Muster dynamisiert und durch ein FactoryDummy-Objekt fur ¨ Tests vorbereitet werden. Abbildung 7.10 zeigt einen noch weitergehenden Ansatz, der ein DummyObjekt als Factory angibt, das mehrere vorbereitete Dummy-Objekte fur ¨ den tats¨achlichen Testablauf bereitstellt.
class FactoryDummy { Class getNewClass(Arguments) { ocl indexClass < class.size; // zus¨atzlich ist es moglich, ¨ eine ocl-Zusicherung fur ¨ Argumente // anzugeben oder diese Argumente zu verwenden, um Attribute // der ubergebenen ¨ Objekte zu setzen return class[indexClass++]; } }
Java/P
Abbildung 7.10. Factory bereitet zu erzeugende Objekte des Testlaufs vor
Die im Testablauf benotigten ¨ Objekte werden also nicht mehr w¨ahrend des Tests generiert, sondern bereits vorab erstellt und dann nur noch uberge¨ ben. Deshalb kann die Factory zum Beispiel durch ein Objektdiagramm wie in Abbildung 7.11 initialisiert werden. Dabei kann exakt bestimmt werden, welche Klassen genutzt und, falls es sinnvoll ist, wie die Attribute vorbelegt werden sollen. 7.2.4 Vorgefertigte Frameworks und Komponenten Die in den letzten Abschnitten beschriebenen Umformungen sind jedoch nicht durchfuhrbar, ¨ wenn vorgegebene Frameworks, Klassenbibliotheken oder Komponenten verwendet werden sollen. Ziel eines Tests sind dabei nicht die vorgegebenen Frameworks, sondern die selbst entwickelten Klassen, deren Funktionalit¨at darauf aufbaut und deshalb Teile des Frameworks in der Testumgebung benotigt. ¨ Nach [LF02] konnen ¨ zum Beispiel Enterprise
7.2 Testbare Programme gestalten
221
Abbildung 7.11. Factory-Objekt bereitet zu erzeugende Objekte des Testlaufs vor
JavaBeans (EJB) [MH00] nur sehr schlecht in Tests einbezogen werden. Das hat im Allgemeinen mehrere Grunde: ¨ • • • • •
•
Die Ablauflogik kann durch ein Framework festgelegt sein. Das in Frameworks ubliche ¨ Don’t call us, we call you“-Prinzip [FPR01] erlaubt es in ” Tests nur mit Aufwand, die Kontrolle zu ubernehmen. ¨ Die Erzeugung neuer Objekte ist im Framework bereits fixiert. Ein Eingreifen mit einer Factory ist nicht moglich. ¨ Statische Variablen, insbesondere wenn sie gekapselt sind, konnen ¨ im Test nicht ausreichend kontrolliert und nicht geeignet besetzt werden. Gekapselte Objektzust¨ande erlauben den Zugriff fur ¨ die Bewertung des Testerfolgs nicht. Von den zur Verfugung ¨ stehenden Klassen konnen ¨ keine Unterklassen und damit keine Dummies gebildet werden, weil (1) die Klasse oder eine darin enthaltene Methode als final deklariert ist, (2) kein offentlicher ¨ Konstruktor existiert, (3) Konstruktoren unerwunschte ¨ Seiteneffekte haben oder (4) die interne Ablauflogik unbekannt ist. Die Instrumentierung der Klassen ist nicht moglich, ¨ so dass zum Beispiel die fur ¨ die Prufung ¨ von Invarianten und Sequenzdiagrammen notwendige Information nicht zug¨anglich ist.
Um Software dennoch testen zu konnen, ¨ ist daher eine Separation der Applikationslogik von derartigen Frameworks oder Komponenten notwendig. Dazu kann generell das Adapter-Entwurfsmuster [GHJV94] verwendet werden. [SD00] beschreibt diese Trennung als wichtig fur ¨ die unabh¨angige Wiederverwendbarkeit der Applikationslogik und des technischen Codes, aber auch fur ¨ die Verbesserung der Wartbarkeit. Ein weiterer positiver Effekt dieser Trennung ist die bessere Testbarkeit. In Abbildung 7.12 ist ein Adapter fur ¨ Java Server Pages (JSP) dargestellt. Das Klassendiagramm beschreibt eine Trennung der Verarbeitung von ubers ¨ Web eingegebenen Datens¨atzen und der tats¨achlichen Speicherung in HttpServletRequest-Objekten, die von den JSP [FK00] zur Verfugung ¨ gestellt werden. Diese Request-Objekte beinhalten die vom Anwender uber ¨ ein Formular eingegebenen Daten und
222
7 Testmuster im Einsatz
konnen ¨ unter anderem uber ¨ die Liste der Parameter getParameterNames und das Auslesen einzelner Parameterwerte getParameter erfragt werden. Weitere Methoden wie getSession liefern zum Beispiel den Kontext der Session, zu der das Formular gehort. ¨ Der Interaktionsmechanismus, den JSP fordert, um zum Beispiel die Eingabedaten auszulesen, macht die komplexe Adaption notwendig. Dabei sind gegebenenfalls Parameter und Ergebnisse jeweils wieder zu ver- und entpacken. Im Auktionssystem wurde dieser Mechanismus verwendet, um die JSP-Oberfl¨ache vom Applikationskern zu trennen.3 Fur ¨ die Separation des entwickelten Codes von benutzten Frameworks und Komponenten gibt es prim¨ar zwei Varianten. Zum einen kann eine vollst¨andige Sammlung von Adaptern fur ¨ alle Klassen des Frameworks angeboten werden. Zum anderen kann eine Minimalversion der gerade benotigten ¨ Klassen und der davon verwendeten Methoden erstellt werden. Die Minimalversion entspricht der Idee, dass moglichst ¨ wenig Aufwand in solche technischen Definitionen gesteckt werden sollte, hat aber den Nachteil, dass mit der Notwendigkeit fur ¨ weitere Methoden die Adapter-Schicht iterativ ausgebaut werden muss. Andererseits hat diese Beschr¨ankung auch den Vorteil, dass eine Anpassung an eine neue Version des Frameworks sowie eine Migration zu einem anderen Framework leichter moglich ¨ ist. Demgegenuber ¨ hat eine vollst¨andige Adapter-Schicht den Vorteil, dass sie eine großere ¨ Wiederverwendbarkeit besitzt. Leider l¨asst sich, wie die Methode getSession zeigt, diese Adapter-Schicht nicht vollautomatisch generieren. Es ist deshalb von Vorteil, wenn das Framework selbst solche Adapter bereits besitzt oder durch Interfaces und Factory-Objekte so gekapselt ist, dass die direkte Verwendung von Dummy-Objekten moglich ¨ wird. Idealerweise bringt das Framework in einem zus¨atzlichen Paket sogar eine Reihe von Dummy-Klassen mit, die fur ¨ verschiedene Testzwecke verwendet werden konnen. ¨ Dies wurde ¨ die Testentwicklung in Framework-abh¨angigen Projekten stark vereinfachen. Umgekehrt ist es aber auch sinnvoll, bei der Veroffentlichung ¨ einer Komponente oder eines Frameworks eine Sammlung von Tests mit herauszugeben, die demonstriert, dass die Komponente beziehungsweise das Framework sich entsprechend einer gegebenen Spezifikation verh¨alt. Dies dient gleichzeitig dazu, das Zutrauen der Komponentennutzer zu erhohen ¨ und den Nutzern durch Beispiele die Anwendung zu erkl¨aren.
7.3 Behandlung der Zeit In verteilten Echtzeitsystemen spielt die kontinuierlich voranschreitende Zeit eine wesentliche Rolle. Im Auktionssystem wird zum Beispiel anhand der 3
Dabei wurde zus¨atzlich gespeichert, ob einem Session-Objekt bereits ein OwnSession-Objekt zugeordnet wurde. Das ist notwendig, um die Eindeutigkeit einer Session in der Applikation sicherzustellen.
7.3 Behandlung der Zeit
223
class OwnServletRequest { Java/P OwnServletRequest() { // nichts zu erledigen httpServletRequest = null; } OwnServletRequest(HttpServletRequest hsr) { httpServletRequest = hsr; } getParameterNames() { ocl httpServletRequest != null; httpServletRequest.getParameterNames(); } OwnSession getSession() { ocl httpServletRequest != null; return new OwnSession(httpServletRequest.getSession()); } ... } class OwnServletRequestDummy { OwnServletRequestDummy(Map(String,String) p) { super(); // Aufruf des leeren Konstruktors parameter = p; } Enumeration getParameterNames() { return parameter.keys(); } OwnSession getSession() { return new OwnSessionDummy( ... ); } ... } Abbildung 7.12. Adapter fur ¨ Request-Objekte
aktuellen Zeit entschieden, in welchen Zustand eine Auktion ubergehen ¨ soll,
224
7 Testmuster im Einsatz
ob ein Gebot angenommen oder abgelehnt wird oder ob Mitteilungen an die Bieter versandt werden. In Java kann mit System.currentTimeMillis() die aktuelle Zeit in Form von Millisekunden ermittelt werden. Je nach Laufzeit eines Tests werden so auch innerhalb eines Tests unterschiedliche Zeiten und Zeitdifferenzen gemessen, die das Testergebnis beeinflussen. Beispielsweise differiert dann bereits die Protokollausgabe in Abh¨angigkeit des Zeitraums, in dem ein Test ausgefuhrt ¨ wird. Durch die Einbeziehung der aktuellen Zeit geht die in Kapitel 5 geforderte Determiniertheit des Testablaufs verloren. Deshalb ist es notwendig, die w¨ahrend des Testablaufs herrschende Zeit durch den Testtreiber zu kontrollieren. Als Nebeneffekt konnen ¨ damit auch Tests, die sich in der echten Zeitrechnung uber ¨ mehrere Stunden hinziehen, wie etwa eine gesamte Auktion, effektiv in Sekundenbruchteilen durchgefuhrt ¨ werden. 7.3.1 Simulation der Zeit im Dummy ¨ Aufgrund der genannten Uberlegungen wird die Abfrage nach der Zeit in einer eigenen Klasse konzentriert und durch ein voreinstellbares DummyObjekt kontrollierbar gemacht. Abbildung 7.13 beschreibt diese Konstruktion mit Ausschnitten der Dummy-Klasse. Im Auktionsprojekt wurde zus¨atzlich das Muster fur ¨ Singletons hinter statischen Methoden aus Tabelle 7.9 angewandt. Die Funktion setTime erlaubt es Testtreibern, die Zeit beliebig zu besetzen. Allerdings bleibt w¨ahrend der Durchfuhrung ¨ eines Tests die Zeit konstant. Dies entspricht einer idealisierenden Annahme, dass w¨ahrend der Berechnung einer Reaktion keine tats¨achliche Zeit vergeht, die zum Beispiel in Sprachen fur ¨ eingebettete Systeme wie Esterel [Hal93] angewandt wird. Tats¨achlich fordert das vorgeschlagene Testmuster Einschr¨ankungen bezuglich ¨ der Verwendung der angeforderten Zeit. Idealerweise sollte nur eine Zeitanfrage stattfinden, die dann als Referenzzeit w¨ahrend der Berechnung genutzt wird. Sollte es notwendig sein, dass innerhalb des Rumpfs einer Methode die Zeit voranschreitet, so kann durch Aufruf von incTime() bei jeder Anfrage von now() ein Voranschreiten simuliert werden. 7.3.2 Variable Zeiteinstellung im Sequenzdiagramm Parallel zum Einsatz der Zeitsimulation kann ein Sequenzdiagramm genutzt werden, um die Uhr fur ¨ jeden neuen Aufruf des Testlings umzustellen. Beispielsweise kann das Sequenzdiagramm aus Abbildung 6.12 so erweitert werden, dass die Zeit jeweils explizit gesetzt wird. Es entsteht das Sequenzdiagramm aus Abbildung 7.14. Durch die Verwendung von Merkmalen fur ¨ Methodenaufrufe lassen sich diese Zeitangaben kompakter modellieren. Im Testsystem werden diese zeitlichen Merkmale konstruktiv interpretiert, indem sie nicht gemessen, son-
7.3 Behandlung der Zeit
class Time ... { Time() { ... } long now() { // Holt die aktuelle Systemzeit return System.currentTimeMillis(); } }
225
Java
class TimeDummy extends Time { TimeDummy(long time) { storedTime = time; } long now() { return storedTime; // Liefert die gespeicherte statt der echten Zeit } void setTime(long time) { storedTime = time; // Erlaubt es, die Zeit zu ver¨andern } void incTime() { storedTime++; // Erhohung ¨ um 1msek } } Abbildung 7.13. Simulation der Zeit durch TimeDummy
dern als Vorgabe fur ¨ die jeweils gultige ¨ Systemzeit verwendet werden. Damit l¨asst sich eine mit Systemzeiten annotierte Form eines Auktionsablaufs in Abbildung 7.15 darstellen. Die Instrumentierung des Testlings fur ¨ die Beobachtung von Methodenaufrufen und Returns erlaubt damit gleichzeitig die Anpassung der jeweils aktuellen Zeit. Tabelle 7.16 beschreibt die Anwendung des Merkmals {time} und seiner additiven Form {time+}.
226
7 Testmuster im Einsatz
Abbildung 7.14. Besetzung der Zeit fur ¨ jeden Aufruf des Testlings
Abbildung 7.15. Annotation als Zeitvorgabe
Merkmal {time} Modellelement
Interaktionen im Sequenzdiagramm.
(Fortsetzung auf n¨achster Seite)
7.3 Behandlung der Zeit
227
(Fortsetzung von Tabelle 7.16.: Merkmal {time})
Motivation
Rahmenbedingung
Wirkung
Beispiel
Das Merkmal {time} gibt in einem Sequenzdiagramm vor, welche Zeit ab Aufruf einer Methode beziehungsweise eines Returns gilt. Damit kann die Systemzeit simuliert werden. Um die Vorgabe der Zeit umzusetzen, muss das Sequenzdiagramm als Testtreiber in einer Umgebung mit simulierter Zeit eingesetzt werden. Die Uhrzeiten mussen ¨ mit der Zeitlinie nach unten großer ¨ werden. Die Vorgabe von Zeiten ist im konstruktiven Sequenzdiagramm in der Interpretation mit dem Stereotyp match:free verboten. Der instrumentierte Produktionscode setzt die jeweils angegebene Zeit, wenn der entsprechende Aufruf beziehungsweise das Return erfolgt. Wie nachfolgend in dem in Tabelle 7.17 beschriebenen Muster zur Simulation der Zeit festgelegt, bleibt diese Zeit bis zum n¨achsten Merkmal konstant. Das Merkmal {time} erlaubt als Argument verschiedene Datums- und Zeitformate, die zur Beschreibung der zu setzenden Zeit gelten. Fehlt das Datum, so wird der bereits gulti¨ ge Tag beibehalten. Es ist moglich, ¨ nicht nur konstante Werte anzugeben, sondern in Berechnungen auch Variablen- und Attributwerte einzubeziehen. Die additive Variante {time+} erlaubt es, Zeitdifferenzen zu addieren, die ebenfalls in verschiedenen Formaten angegeben sein konnen. ¨ Abbildung 7.15 nutzt diese Merkmale. Tabelle 7.16.: Merkmal {time}
7.3.3 Muster zur Simulation von Zeit Das diskutierte Muster zur Behandlung der Zeitproblematik wird in Tabelle 7.17 zusammengefasst. Muster: Simulation von Zeit Intention
Motivation
Um deterministische Ergebnisse bei Tests zeitabh¨angigen Verhaltens sicherzustellen, wird die Zeit simuliert. W¨ahrend des Ablaufs des Testlings kann die Zeit vom Treiber ge¨andert werden. Zeitabh¨angiges Verhalten kann damit simuliert werden und wird wiederholbar deterministisch. (Fortsetzung auf n¨achster Seite)
228
7 Testmuster im Einsatz (Fortsetzung von Tabelle 7.17.: Muster: Simulation von Zeit)
Anwendung Eine Anwendung dieses Musters ist sinnvoll, wenn das Systemverhalten von der aktuellen Zeit abh¨angt, wie zum Beispiel der Protokollierung von Vorg¨angen, oder auch fur ¨ Tests von Interaktionsmustern, die Timeouts besitzen. Struktur Siehe Abbildung 7.13. Zur Anwendung des Musters stehen zum Beispiel Sequenzdiagramme wie in Abbildung 7.15 zur Verfugung, ¨ die durch Merkmale die jeweils aktuelle Zeit bestimmen. Implemen- Siehe Abbildung 7.13. Typischerweise wird dies kombiniert tierung mit dem in Tabelle 7.9 beschriebenen Muster zur Kapselung eines Singletons hinter statischen Methoden, so dass Time.now() als Aufruf verwendet werden kann. Beachtens- Eine Implementierung darf nicht davon ausgehen, dass die wert Zeit w¨ahrend ihrer Aktivit¨at voranschreitet. Insbesondere durfen ¨ keine Warteschleifen der Form t=Time.now(); while(Time.now()
7.4 Nebenl¨aufigkeit mit Threads
229
7.4 Nebenl¨aufigkeit mit Threads Nebenl¨aufigkeit tritt immer dann auf, wenn unabh¨angig voneinander agierende Systemteile gleichzeitig Aktivit¨aten durchfuhren ¨ konnen ¨ [Bro98]. Zum Beispiel konnen ¨ mehrere Threads innerhalb eines Systems verschiedene Aufgaben erledigen. Typischerweise werden externe Ereignisse, wie Nutzereingaben, TCP/IP-Kommunikation, Druck- und Ladevorg¨ange, in eigenst¨andigen Threads behandelt. So findet die Bearbeitung von Events im AWT in einem eigenen Thread statt. Das erhoht ¨ zwar normalerweise die Gesamtgeschwindigkeit des Systems nicht, nutzt aber Wartezeiten, die durch die Umgebung verursacht werden, effizient aus und verbessert das Reaktionsverhalten der graphischen Nutzeroberfl¨ache. Threads sind leichtgewichtige Prozesse, die alle im gleichen Speicherraum ablaufen. Demgegenuber ¨ gibt es schwergewichtige Prozesse, die vom Betriebssystem verwaltet werden und keinen gemeinsamen Speicher nutzen. Als drittes konnen ¨ Prozesse auf verschiedenen Prozessoren zum Beispiel uber ¨ das Internet verteilt sein. In allen diesen F¨allen tritt Nebenl¨aufigkeit auf, die zu nichtdeterministischen Ergebnissen und damit auch zu sporadischen Fehlern fuhren ¨ kann. Es ist deshalb fur ¨ Konformit¨atstests wichtig, diese Form des Nichtdeterminismus zu unterbinden oder zumindest einzuschr¨anken. Deshalb wird nachfolgend ein Konzept diskutiert, das es erlaubt, den durch Threads innerhalb eines Prozessraums auftretenden Nichtdeterminismus zu kontrollieren. Das hinter diesem Ansatz zum Test verteilter Systeme stehende Prinzip l¨asst sich wie folgt charakterisieren. Anhand eines deterministischen Ablaufs wird zun¨achst die grunds¨atzliche Funktionalit¨at gepruft ¨ und damit sichergestellt, dass die Methoden korrekt zusammenarbeiten konnen. ¨ Darin werden alle fur ¨ den Test notwendigen Aktivit¨aten in eine deterministische und fur ¨ den Test geeignete Reihenfolge gebracht. Dies ist zun¨achst eine Art Existenzbeweis, dass es korrekte Abl¨aufe gibt. Dieser erlaubt naturlich ¨ keine allquantifizierte Aussage uber ¨ alle Abl¨aufe. Deshalb werden in weiteren Tests alternative Reihenfolgen gepruft. ¨ Durch dieses Interleaving auf Methodenebene entsteht weitere Sicherheit uber ¨ die Korrektheit des nebenl¨aufigen Zusammenspiels. Dass keine unerwunschten ¨ Interaktionen zwischen Threads auf gemeinsamen Daten auftreten, wird durch diese Tests nicht uberpr ¨ uft, ¨ sondern vor allem durch die ad¨aquate Verwendung von Synchronisationsmechanismen sichergestellt. Diese in Java auch als Threadsicherheit“ bekannte Technik ” wird zum Beispiel mit Reviews oder bereits bei der paarweisen Entwicklung gesichert. Durch die intensive Verwendung von Synchronisation ist in Hochsprachen wie Java das Problem der Nebenl¨aufigkeit deutlich geringer als zum Beispiel in Hardware-nahen, eingebetteten Systemen, in denen Interleaving auf Maschineninstruktionsebene auftritt. Der zum Beispiel in [LF02] vorgeschlagene Weg, nichtdeterministische Testf¨alle mehrfach laufen zu lassen um so moglicherweise ¨ verschiedene Varianten des Ablaufs zu testen, kann dazu als Erg¨anzung gesehen werden. Auch [Bin99] bietet zur Integration verteilter Systeme ein Testmuster an, das
230
7 Testmuster im Einsatz
aber keine funktionale Simulation innerhalb eines Prozessraums beinhaltet und deshalb ebenfalls als Erg¨anzung zu sehen ist. 7.4.1 Eigenes Scheduling Determinierte Testergebnisse lassen sich meist nur dadurch erreichen, dass die Nebenl¨aufigkeit vollst¨andig durch den Testtreiber kontrolliert wird. Dazu muss ein Testtreiber entweder in den Scheduler der Java Virtual Machine eingreifen konnen ¨ oder dessen Scheduling aufheben. Da sowohl der Produktionscode als auch der Testcode auf mehreren Plattformen oder zumindest mit verschiedenen Versionen von Java-Implementierungen funktionieren sollen, ist eine Adaption der Java Virtual Machine nur bedingt sinnvoll. Eine relativ elegante und stabile Losung ¨ ist es aber, das Scheduling in einfacher Form im Testtreiber zu modellieren. Da ein Testtreiber nur fur ¨ einen Testlauf formuliert wird, ist er unter folgenden Annahmen relativ einfach zu beschreiben: 1. Jeder Thread besteht aus einer oder mehreren regelm¨aßig wiederkehrenden T¨atigkeiten, die jede fur ¨ sich relativ wenig Zeit erfordert. 2. Die parallele Ausfuhrung ¨ einzelner T¨atigkeiten hat keine Interferenzen, die fur ¨ die Programmausfuhrung ¨ erforderlich sind.4 3. Threadsicherheit und Freiheit von Deadlocks werden mit anderen Testund Inspektionsverfahren gepruft. ¨ Ein Thread hat dann nach 1. in etwa die in Abbildung 7.18 dargestellte Form. Auch die Verwendung anderer Mechanismen, wie etwa dem ab Java 1.3 zur Verfugung ¨ stehenden TimerTask und der damit verbundenen Moglich¨ keit fur ¨ Methodenaufrufe ein explizites Scheduling festzulegen, ver¨andert dieses Prinzip nicht. Im Gegenteil fordert ¨ die Verwendung dieses Mechanismus sogar die gewollte Trennung von Thread-Management und Applikationsfunktionalit¨at. Falls notwendig muss der Produktionscode einem geeigneten Refactoring unterzogen werden, indem zum Beispiel die in der Methode run verborgene Funktionalit¨at in eine eigene Methode ausgelagert wird. Ist die Berechnung einer Abbruchbedingung fur ¨ die while-Schleife oder der sleepPeriod komplexer, so werden auch diese beiden Berechnungen in eigene Methoden ausgelagert. Dadurch werden diese einzelnen Funktionalit¨aten testbar, w¨ahrend die Grundfunktionalit¨at des Thread einfach und damit uberschaubar ¨ wird. Auch wenn der eigentliche Thread in einer fremden Komponente oder im Framework verborgen ist und nur Callbacks, also Aufrufe aus dem Framework heraus auf den selbst entwickelten Code stattfinden, ist dieses Prinzip erreicht. Unter Umst¨anden sind aber die dabei ubergebenen ¨ Argumente in 4
Problematisch w¨are zum Beispiel eine Synchronisation von Threads uber ¨ Variablen und Busy-Wait-Schleifen.
7.4 Nebenl¨aufigkeit mit Threads class OwnThread extends Thread { protected Workingclass client; public OwnThread(Workingclass client) { this.client = client; // Starten des Threads mit start() erfolgt nicht eigenst¨andig // bereits bei dessen Erzeugung sondern danach durch den Erzeuger! } public void run() { // St¨andige Wiederholung: while (true) { client.workingmethod(arguments); try { sleep(sleepPeriod); } catch (InterruptedException e) { } } } }
231 Java
Abbildung 7.18. Typisches Aussehen eines testbaren Threads
Adaptern zu verpacken, um keine Abh¨angigkeiten zwischen Applikationscode und Framework zu erhalten, die ein Einbetten in ein Testsystem verhindern. Dies ist anhand des Beispiels der Request-Klassen aus JSP bereits in Abschnitt 7.2 demonstriert worden. Eine weitere Problemklasse besteht darin, dass es Threads gibt, die zwar in die in Abbildung 7.18 beschriebene Struktur gebracht werden konnen, ¨ die regelm¨aßig aufgerufene Methode aber selbst blockierende Aufrufe durchfuh¨ ren muss. Zum Beispiel ist die Kommunikation uber ¨ Socket-Klassen in Java 1.3 dadurch geregelt, dass ankommende Daten mittels einem blockierenden read()-Aufruf abzuholen sind. Um diese Blockade zu umgehen, kann ein Socket-Dummy eingesetzt werden, das bereits einen Satz von Eingabedaten besitzt. 7.4.2 Sequenzdiagramm als Scheduling-Modell Auf Basis der nach Abbildung 7.18 strukturierten Threads kann mit Sequenzdiagrammen ein Scheduling einfach modelliert werden. Abbildung 7.19 beschreibt einen Testtreiber, der mehrere Methoden ausfuhrt, ¨ deren Aufrufe im Auktionssystem in verschiedenen Threads stattfinden. Die drei Objekte der Klassen ClockDisplay, WebBidding und BiddingPanel sind drei unterschiedlichen Threads zugeordnet. Das erste ist fur ¨ die Aktualisierung der Zeitanzeige im Applet verantwortlich, das zweite fur ¨ die regelm¨aßige Nachfrage nach neuen Informationen zu den aktuell beobachteten Auktionen beim Server und das dritte ist eines von einer Reihe von Elementen der graphischen Oberfl¨ache, das auf Aktionen des Nutzers wartet. Das Objekt :BiddingPanel wird also durch Callbacks aus
232
7 Testmuster im Einsatz
Abbildung 7.19. Scheduling auf der Ebene einzelner Funktionen
dem AWT-Framework heraus angesprochen. Das Beispiel testet einen Großteil des Client-Systems, da es nicht nur den Applikationskern, sondern auch die Bearbeitung der graphischen Oberfl¨ache und die Kommunikation mit dem Server einbezieht. Dadurch sind an mehreren Stellen Dummies notwendig, um die Effekte der benutzten Umgebung zu simulieren. Insbesondere ist ¨ die Verwendung von AWT-Klassen zur Ubergabe von Events aus den in Abschnitt 7.2 beschriebenen Grunden ¨ kritisch. Deshalb empfiehlt es sich, eine Schicht tiefer zu testen oder eine Adapter-Schicht zwischen AWT und Applikationscode zu legen. Im Beispiel wird das Drucken ¨ der Return-Taste im Eingabefeld fur ¨ Gebote als Gebotsabgabe interpretiert und fuhrt ¨ zum Aufruf von bid(String eingabetext). Diese Methode kann auch direkt und damit unabh¨angig vom AWT-Framework aufgerufen werden. Abbildung 7.20 zeigt eines von mehreren im Auktionssystem genutzten Szenarien zum Test des Applikationskerns im Applet, wobei wieder auf die vollst¨andige Definition der zugrunde liegenden Objektstruktur verzichtet wird. 7.4.3 Behandlung von Threads Die bisherige Modellierung von Nebenl¨aufigkeit geht davon aus, dass die jeweiligen Threads bereits existieren und unabh¨angig voneinander agieren. Es gibt jedoch mehrere Aspekte, bei denen sich Threads gegenseitig beeinflussen. Beispielsweise kann jederzeit ein neuer Thread erzeugt werden. Da das Scheduling der Threads komplett vom Testtreiber zu ubernehmen ¨ ist, muss durch Anwendung einer Factory zur Erzeugung neuer Objekte und einem Dummy-Thread, der nicht zum echten Start eines Threads fuhrt, ¨ die Erzeugung eines neuen Threads simuliert werden. Die Simulation einer erzwungenen Beendigung oder Unterbrechung eines anderen Threads ist
7.4 Nebenl¨aufigkeit mit Threads
233
Abbildung 7.20. Scheduling nur im Applikationskern
dann jedoch unproblematisch. In einer Dummy-Klasse fur ¨ Threads konnen ¨ daher Methoden wie start(), interrupt() oder destroy() durch leere Methoden ersetzt werden. Wird die Methode join() verwendet, so ist allerdings das verwendete Schedulingverfahren zu erweitern, indem etwa durch das join() solange Methoden anderer Threads aufgerufen werden, bis die dortigen Threads als beendet gelten konnen. ¨ Da neben anderen Methoden auch die Methode join() in der Klasse Thread nicht redefiniert werden kann, ist in diesem Fall ein Adapter notwendig. Eine Klasse OwnThread kann entsprechend dem Muster in Abschnitt 7.2.4 gebildet werden. ThreadDummy ist davon die fur ¨ den Test verwendete Unterklasse.
Abbildung 7.21. Thread-Dummy ubernimmt ¨ Scheduling-Aufgaben
Mit einem geeigneten Codegenerator l¨asst sich auch aus dem Sequenzdiagramm in Abbildung 7.21 ein Testfall generieren, der die Funktionalit¨at des Testtreibers teilweise in die Klasse ThreadDummy integriert. Weitere Aufmerksamkeit ist fur ¨ Methoden wie sleep() notwendig, die im Thread-Dummy hochstens ¨ die simulierte Zeit redefinieren, aber nicht zu tats¨achlicher Verzogerung ¨ fuhren. ¨
234
7 Testmuster im Einsatz
7.4.4 Muster fur ¨ die Behandlung von Threads Die Diskussion zur Behandlung von Threads l¨asst sich mit dem in Tabelle 7.22 dargestellten Muster zusammenfassen. Muster: Behandlung von Threads Intention
Threads konnen ¨ grunds¨atzlich nichtdeterministische Abl¨aufe bedingen und sind deshalb fur ¨ Tests geeignet anzupassen. Motivation Die Bearbeitung von Threads und damit von Nebenl¨aufigkeit innerhalb eines Prozesses ist durch einen eigenen Scheduler zu simulieren, der zu determinierten Testergebnissen fuhrt. ¨ Anwendung Eine Anwendung dieses Musters ist sinnvoll, wenn • mehrere Threads parallel innerhalb eines Prozesses laufen sollen, • Interaktionen zwischen den Threads bestehen, die getestet werden sollen, und • die Threads nach der in Abbildung 7.18 beschriebenen Struktur definiert sind oder entsprechend umgebaut werden konnen. ¨ Das beinhaltet, dass ein Thread regelm¨aßig wiederkehrende, relativ kurz dauernde T¨atigkeiten durchfuhrt. ¨ Struktur
Das Muster besteht aus zwei Teilen. Ein ThreadDummy wird verwendet, wenn im Testling explizit Threads kontrolliert werden sollen. ThreadDummy ist die Unterklasse eines Adapters fur ¨ die Klasse Thread. Im Produktionssystem wird der Adapter eingesetzt. Der Test von parallel ablaufenden Threads, von denen angenommen wird, dass sie bereits initialisiert sind, erfolgt durch einen fur ¨ den Testablauf formulierten Scheduler. Dieser Scheduler ruft die einzelnen T¨atigkeiten der verschiedenen Threads der Reihe nach auf und l¨asst jede dieser T¨atigkeiten vollst¨andig abarbeiten, bevor die n¨achste T¨atigkeit ausgefuhrt ¨ wird. Es wird also eine Art kooperatives Multitasking umgesetzt. Umsetzung Ein solcher Scheduler kann durch ein Sequenzdiagramm wie dem in Abbildung 7.19 modelliert werden. Wird dieses Muster in Kombination mit der Simulation von Zeit aus Tabelle 7.17 verwendet, so kann das Merkmal {time} verwendet werden, um die voranschreitende Zeit zu modellieren. Beachtens- Verwendet eine durch das Scheduling aufgerufene Methode wert spezielle Funktionen, wie yield(), sleep(), etc., so sind diese im ThreadDummy geeignet zu redefinieren. Tabelle 7.22.: Muster: Behandlung von Threads
7.4 Nebenl¨aufigkeit mit Threads
235
Dieses Muster beschreibt einen zu generierenden und einen strukturellen Anteil. Der letztere gehort ¨ zur Laufzeitumgebung der UML/P, die standardm¨aßig den Adapter OwnThread und die Unterklasse ThreadDummy zur Verfugung ¨ stellt, die wie beschrieben alle Methodenaufrufe ignoriert. Von dieser Klasse konnen ¨ eigene Unterklassen definiert werden, die zum Beispiel das in Abbildung 7.21 beschriebene Scheduling durchfuhren. ¨ Ein fur ¨ das Thread-Scheduling geeigneter Generator kann nicht nur Testtreiber aus Sequenzdiagrammen generieren, die das Scheduling uberneh¨ men, sondern auch diese Unterklassen von ThreadDummy erzeugen. 7.4.5 Probleme der erzwungenen Sequentialisierung Abschließend soll noch einmal daran erinnert werden, dass das hier beschriebene Verfahren zum Scheduling von Tests nicht die Nebenl¨aufigkeit testet, sondern im Gegenteil diese explizit ausschließt, um Konformit¨atstests durchzufuhren. ¨ Die Ergebnisse der Tests lassen sich daher nur begrenzt auf das echt nebenl¨aufige Produktionssystem ubertragen. ¨ Der im Test durchgefuhrte ¨ Ablauf entspricht nur einem von mehreren tats¨achlich moglichen ¨ Abl¨aufen. Wird allerdings, wie in Java-Codierungsstandards gefordert, jede kritische Methode synchronisiert, so kann zumindest feingranulare Nebenl¨aufigkeit verhindert werden. Dadurch wird das System besser testbar und die Konformit¨atstests aussagekr¨aftiger. Das folgende Codestuck ¨ gehort ¨ zu einem schlecht testbaren Programm, das Race Conditions“ erlaubt: ” class X { int a = 0; int loopa() { for(int i=0; i<100000; i++) a = (a+1) % 99; } int setAndReada() { a = 0; return a; } }
Java
Bei einem Scheduling, das wie vorgeschlagen, die Methoden in beliebiger Reihenfolge, aber nacheinander aufruft, wird die OCL-Bedingung context X.setAndReada() true pre: result==0 post SetAndReadA:
OCL
in Tests immer erfolgreich gepruft ¨ werden, da setAndReada() unter Ausschluss paralleler Funktionen abl¨auft. Wenn im Produktionssystem allerdings echte Nebenl¨aufigkeit auftritt, so ist das Ergebnis von setAndReada() nicht determiniert. Es gibt zwei Moglichkeiten ¨ dies zu adressieren: (1) Die
236
7 Testmuster im Einsatz
Bedingung SetAndReadA ist zu eng definiert. Es ist stattdessen 0<=result && result<99 zu verwenden. (2) Die Methoden sind falsch implementiert. ¨ Sie sollten durch Synchronisation vor derartigen Uberraschungen geschutzt ¨ werden. Eine Simulation der ungeschutzten ¨ Funktionen mit dem vorgeschlagenen Verfahren macht eine Reorganisation der parallelen Funktionen notwendig, um ein feineres Scheduling zu erlauben. Beispielsweise kann die Methode setAndReada() in zwei Teile (eine modifizierende Methode und eine Query) geteilt und der Rumpf von loopa() in eine eigene Methode ausgelagert werden. Als Ergebnis entstehen so die Methoden, die mit einem Testtreiber der Form seta(); loopaBody(); reada(); sofort eine erkennbare Verletzung der Invariante hervorrufen: class X { int a = 0; int loopa() { for(int i=0; i<100000; i++) loopaBody(); } int loopaBody() { a = (a+1) % 99; } void seta() { a = 0; } int reada() { return a; } }
Java
Ein alternativer Ansatz konnte ¨ ein Scheduling nutzen, in der sich laufende Funktionen mit einem Methodenaufruf selbst unterbrechen.5 Das erfordert allerdings gesteigerten Aufwand beim Management des Scheduling. Als großtes ¨ Problem aber bleibt bestehen, dass die Anzahl der quasiparallelen Abl¨aufe mit feingranularem Scheduling stark ansteigt. Entsprechend mussen ¨ die potentiellen Gefahrenquellen durch die Entwickler antizipiert werden, um durch konkrete Tests realisiert zu werden. Andererseits konnen ¨ unklare Situationen, von denen Entwickler vermuten, sie ko¨ nnten zu Fehlern fuhren, ¨ auf diese Weise gepruft ¨ werden. In der Literatur zum Thema Testen werden verschiedene Vorschl¨age gemacht, nebenl¨aufige Programme zu testen. Neben der generellen Richtlinie, Programme threadsicher“ zu machen und eine Art Pseudodetermi” ” nismus“ [LF02] zu etablieren, indem nebenl¨aufige Programmteile moglichst ¨ unabh¨angig gestaltet werden, wird vorgeschlagen, moglichst ¨ viele automatisierte Testabl¨aufe mit den echten Threads durchzufuhren, ¨ um damit zumindest sporadisch auftretende Fehler zu erhalten [LF02]. Das Zutrauen in die Robustheit des Systems bleibt aber auch mit diesem Verfahren beschr¨ankt, insbesondere wenn das Produktionssystem sich vom Testsystem in Hard¨ ware, Betriebssystem, Compiler-Version, Systemlast oder Ahnlichem unterscheidet. 5
Die ersten Implementierungen der Java Virtual Machine haben dies zum Beispiel durch Aufrufe von yield() erfordert, da sie nur kooperatives Multitasking realisiert haben. Hier w¨are ein a¨ hnliches Prinzip anzuwenden.
7.5 Verteilung und Kommunikation
237
7.5 Verteilung und Kommunikation Echt verteilte Programme unterscheiden sich von rein nebenl¨aufigen Programmen durch die r¨aumliche oder konzeptionelle Verteilung der Teilsysteme. Daraus resultieren getrennte Speicher, die ein gemeinsames Arbeiten auf denselben Objekten verhindern und explizite Kommunikation notwendig machen [Bro98, Bog99]. Systeme wie das Auktionssystem, die im Internet verteilt sind, konnen ¨ auf verschiedene Arten kommunizieren. Mehrere Frameworks und Technologien, wie RMI oder CORBA, bieten unterschiedlich gute Unterstutzung ¨ und erlauben teilweise sogar die an sich asynchrone Kommunikation ubers ¨ Internet durch einen simuliert synchronen Methodenaufruf zu ersetzen. 7.5.1 Simulation der Verteilung Verteilte Systeme sind ebenfalls nebenl¨aufige Systeme und daher inh¨arent nichtdeterministisch. Dazu kommt, dass anders als bei normaler ThreadProgrammierung ein Ausfall eines Kommunikationspartners relativ h¨aufig vorkommt, weil zum Beispiel die Internet-Leitung unterbrochen, der Rechner ausgeschaltet oder das Client-Applet terminiert wurde. Zustandsbasierte Kommunikation, bei der also auf wenigstens einer Seite der Zustand des Kommunikationspartners gespeichert wird, muss daher spontane Zustandsuberg¨ ¨ ange des Partners in Tests miteinbeziehen. Das fur ¨ Tests verteilter Systeme allgemein verwendbare Prinzip ist auf dem Scheduling nebenl¨aufiger Threads aufgebaut. Das Muster fur ¨ die Simulation der Nebenl¨aufigkeit aus Tabelle 7.22 ist daher grunds¨atzlich auch fur ¨ die Simulation verteilter Systeme geeignet. Es mussen ¨ jedoch einige zus¨atzliche Probleme beachtet werden. So konnen ¨ Threads, die fur ¨ unterschiedliche Prozessr¨aume konzipiert sind, im Test innerhalb eines Prozessraums auf ungewollte Weise interagieren. So teilen sich im Testablauf alle Threads dieselben statischen Variablen. Dies kann dadurch umgangen werden, dass bei jedem Wechsel des Threadkontexts die statischen Variablen geeignet umgesetzt werden oder in einer redefinierbaren Schnittstelle gekapselt sind. Dazu eignet sich in hervorragender Weise das Singleton-Muster aus Tabelle 7.9, bei der die Realisierung der do. . . -Methode durch Festlegung des jeweils aktiven Kontexts das jeweils richtige Objekt verwendet. Dieses Konzept ist a¨ hnlich zu der Verwendung der Session-Objekte in JSP [FK00], die den Kontext des gerade aktiven Threads definieren. Jedoch ist in dem hier vorgestellten Ansatz durch die zus¨atzliche Kapselung in einer statischen Methode die Verwendung fur ¨ den Nutzer nicht sichtbar. Abbildung 7.23 zeigt einen einfachen Scheduler, der eine Nachricht am Server ablegt und dann zwei Clients zum Abholen der Nachricht veranlasst. Die eigentliche Kommunikation ist in diesem Sequenzdiagramm nicht enthalten. Sie wird erst nachfolgend behandelt. Die am Test beteiligten Ob-
238
7 Testmuster im Einsatz
Abbildung 7.23. Simulation eines verteilten Systems
jekte sind mit dem in Tabelle 7.24 eingefuhrten ¨ Merkmal {location} parametrisiert, dessen Wert den jeweiligen Prozessraum beschreibt. Vor der Ausfuhrung ¨ des jeweiligen Methodenaufrufs wird der Prozesskontext umgestellt, so dass jede angestoßene T¨atigkeit ihre naturliche ¨ Umgebung vorfindet. Merkmal {location} Modellelement
Objekte im Objektdiagramm und im Sequenzdiagramm.
Motivation
Beschreibt die physische oder konzeptionelle Verteilung der damit markierten Objekte in verschiedenen Prozessr¨aumen. Die mit dem Merkmal beschriebene Ortsangabe heißt Lokation6 . Da Prozessr¨aume physisch oder konzeptionell verteilt sind, sind normalerweise Links und Aufrufe zwischen Objekten verschiedener Prozessr¨aume nicht moglich. ¨ Eine Ausnahme wird dafur ¨ allerdings fur ¨ Testtreiber und Kommunikationselemente gemacht. Objekte, die aufgrund eines Sequenzdiagramms erzeugt werden, gehoren ¨ zum gleichen Prozessraum, wie das erzeugende Objekt.
Glossar Rahmenbedingung
(Fortsetzung auf n¨achster Seite)
6
In Anlehnung an die im Englischen gebr¨auchliche Form der location“ wird Lo” ” kation“ dem Begriff Ortsangabe“ vorgezogen. ”
7.5 Verteilung und Kommunikation
239
(Fortsetzung von Tabelle 7.24.: Merkmal {location})
Wirkung
Beispiel(e)
Die physische Verteilung kann in einem Testfall simuliert werden. Jeder Lokation steht eine eigene Umgebung zur Verfugung, ¨ die zum Beispiel verhindert, dass die Nutzung statischer Variablen zu unerwunschten ¨ Interaktionen fuhrt. ¨ Das Merkmal {location} hat als Argument einen Identifikator vom Typ String, der den Namen des Prozessraums darstellt. Das Sequenzdiagramm in Abbildung 7.23 nutzt diese Merkmale. Tabelle 7.24.: Merkmal {location}
Die hier verwendete Umsetzung von Ortsangaben ist eine sehr pragmatische Form der Verwendung von Lokationen zur Modellierung von Systemen. Auch im Ambient-Calculus [CG98] werden Lokationen eingesetzt, um damit dynamische Systeme zu modellieren. Im UML-Standard konnen ¨ in a¨ hnlicher Form auch die in diesem Buch nicht weiter vertieften DeploymentDiagramme verwendet werden. 7.5.2 Simulation von Singletons Die Simulation eines verteilten Echtzeitsystems erfordert meist die Verwendung unterschiedlicher Systemzeiten fur ¨ jede einzelne Lokation. Dafur ¨ kann das Muster in Tabelle 7.17 zur Zeitsimulation genauso ausgebaut werden, wie auch andere Singletons. Das Muster in Tabelle 7.25 beschreibt, wie ein Singleton pro Lokation verwaltet werden kann. Muster: Individuelles Singleton fur ¨ jede Lokation Intention
Wird im Testsystem Verteilung simuliert, so wird mit diesem Muster ermoglicht, ¨ jeder Lokation ein eigenst¨andiges Singleton zuzuweisen. Das Muster baut auf dem Singleton-Muster aus Tabelle 7.9 auf. Anwendung Durch die Anwendung dieses Musters bleibt dem Produktionscode verborgen, dass eine Verteilung nur simuliert ist. Virtuell hat jede Lokation eigene statische Variablen. (Fortsetzung auf n¨achster Seite)
240
7 Testmuster im Einsatz (Fortsetzung von Tabelle 7.25.: Muster: Individuelles Singleton f u¨ r jede Lokation)
Implementierung Singleton
class Singleton { static String location = ""; static Map(String,Singleton) singletonMap = new Hashtable();
Java/P
static public void setLocation(String l) { location = l; } static public void initialize(Singleton s) { singletonMap.put(location,s); } static public void initialize() { initialize(new Singleton(...)); }
}
Beachtenswert
static method(Arguments) { // Wahl des Singletons Singleton singleton = singletonMap.get(location); // eigene Initialisierung wenn notwendig if(singleton==null) { initialize(); singleton = singletonMap.get(location); } // Delegation: return singleton.doMethod(Arguments); } doMethod(Arguments) { // hier wird gearbeitet . . . }
Fur ¨ die Simulation wird lediglich die Klasse Singleton angepasst. Die Unterklassen der Form SingletonDummy werden genauso definiert, wie in Tabelle 7.9 beschrieben. • Durch die Verwendung mehrerer Singletons“ im gleichen ” Prozessraum des Tests kann nur noch von virtuellen Singletons gesprochen werden. Tats¨achlich existieren mehrere Instanzen gleichzeitig. • Wichtig ist deshalb fur ¨ die Simulation, dass auch das durch diese Technik verkapselte Singleton nur uber ¨ den hier vorgestellten Mechanismus auf seine statischen Attribute zugreift.
Tabelle 7.25.: Muster: Individuelles Singleton fur ¨ jede Lokation
7.5 Verteilung und Kommunikation
241
7.5.3 OCL-Bedingungen uber ¨ mehrere Lokationen Durch die im Test verwendete Zusammenlegung mehrerer virtueller Prozessr¨aume gilt die Einzigartigkeit des Singletons nur noch bezuglich ¨ seiner Lokation. Dadurch ist fur ¨ Auktionen uber ¨ mehrere Lokationen die Eindeutigkeit des Auktionsidentifikators auctionIdent nicht gegeben. Deshalb sind bei simulierter Verteilung OCL-Invarianten entsprechend anders zu interpretieren als einfuhrend ¨ in Kapitel 4, Band 1 beschrieben. Grunds¨atzlich gelten Aussagen wie context AllData ad inv AllDataIsSingleton: AllData == {ad}
OCL
oder context Auction a, Auction b inv AuctionIdentIsUnique: a.auctionIdent == b.auctionIdent implies a == b
OCL
fur ¨ jede Lokation, nicht jedoch fur ¨ den gesamten Testfall, da sie implizit Quantoren uber ¨ der Menge aller vorhandenen Objekte nutzen.7 Deshalb sind die in diesen Testf¨allen verwendeten OCL-Bedingungen lokal uber ¨ der Extension einer Klasse zu interpretieren. Dies erfordert zum einen fur ¨ die durch die Codegenerierung erzeugte Verwaltung der Extension einer Klasse eine zus¨atzliche Differenzierung nach den Lokationen, denen diese Objekte zugeordnet werden. Zum anderen aber ist es, wie die OCL-Bedingungen in Abbildung 7.23 zeigen, von Interesse, bei der Simulation verteilter Prozesse Bedingungen auszuwerten, die uber ¨ Prozessgrenzen hinweg formuliert sind. Damit kon¨ nen globale Eigenschaften formuliert werden, wie etwa, dass nach dem Holen der am Server anliegenden Nachrichten der Wert des letzten Gebots b als niedrigstes Gebot beim Client vorliegt. Die dabei verglichenen MoneyObjekte gehoren ¨ zu unterschiedlichen Lokationen. Fur ¨ den Fall, dass es nicht wie im Beispiel offensichtlich ist, dass eine OCL-Bedingung global zu interpretieren ist, wird die Verwendung eines Merkmals der Form {global} fur ¨ OCL-Invarianten vorgeschlagen. Analog kann die Gultigkeit ¨ einer OCL-Invariante auf jede der Lokationen einzeln mit {local} und eine bestimmte Lokation eingeschr¨ankt werden, indem diese explizit angegeben wird: {location=server} context Auction a, Auction b inv AuctionIdentIsUnique: a.auctionIdent == b.auctionIdent implies a == b 7
OCL
context Auction a ist eine Allquantifizierung uber ¨ alle Objekte der Klasse Auction.
242
7 Testmuster im Einsatz
7.5.4 Kommunikation simuliert verteilter Prozesse Fur ¨ das Produktionssystem stehen verteilten Prozessen mehrere Kommunikationsmechanismen zur Verfugung. ¨ In einem Testlauf mit simulierter Verteilung ist eine echte Kommunikation uber ¨ ein Netz nicht notwendig. Die zur Kommunikation verwendeten Objekte sind daher durch Dummies zu ersetzen, die die zu ubermittelnde ¨ Information auf andere Weise ubertragen. ¨ Im Auktionssystem ist die Kommunikation durch mehrere selbst entworfene Layer auf Basis direkter HTTP-Anfragen realisiert. Dadurch ist ein sehr effizientes und konfigurierbares System entstanden, das Verschlusselung, ¨ Verwaltung von Kommunikationszust¨anden in Sessions“ einfach ein- und ” ausschalten sowie mit Firewalls und Cache-Systemen verschiedener Arten umgehen kann. Die Grundstruktur der Kommunikation ist unter Abstraktion von Details wie Sessions und Verschlusselung ¨ in Abbildung 7.26 dargestellt und beschreibt nur den Anteil, der fur ¨ die Gebotsabgabe genutzt wird.
Abbildung 7.26. Kommunikationsschichten im Auktionssystem
Beim Client entspricht die Aufrufhierarchie der Schichtung, also asp ruft mhp auf, der eine vorhandene Verbindung verwendet oder eine neue erstellt, die Daten in einen verschlusselten ¨ String transformiert und ubertr¨ ¨ agt. Beim Server geht die Aktivit¨at von einer nicht dargestellten Menge vorhandener Threads aus, die am Socket prufen, ¨ ob eine Anfrage vorliegt und diese dann in jeweils eigenen Instanzen der Klasse HttpConnection bearbeiten lassen. Daher geht beim Server die Aktivit¨at von der Klasse HttpConnection aus. Wie in Abschnitt 7.1 besprochen, lassen sich auch in verteilten Systemen jeweils einzelne oder Kombinationen von Schichten testen. Dazu ist jeweils eine geeignete Konfiguration unter Nutzung von Dummies und Factories zu entwerfen. Die Konfiguration simulierter verteilter Systeme l¨asst sich
7.5 Verteilung und Kommunikation
243
durch ein Objektdiagramm darstellen. Beispielsweise ist eine sehr einfache Konfiguration, die jegliche Kommunikationsaspekte ausblendet, durch das Objektdiagramm in Abbildung 7.27 erreichbar. Dabei wird ein Dummy zur Delegation eingesetzt, damit eine Kopie der ubertragenen ¨ Objekte angelegt werden kann und keine gemeinsamen Objekte in verschiedenen Lokationen genutzt werden.
Abbildung 7.27. Verbindung in der obersten Schicht
Durch die Konfiguration in Abbildung 7.28 l¨asst sich zus¨atzlich die korrekte Umwandlung der ubertragenen ¨ Gebote in Strings ( Marshalling“) prufen. ¨ ” Die Schichten wurden jeweils so konstruiert, dass das Original anstelle des Proxy eingesetzt werden kann. In der gezeigten Konfiguration kann sogar ¨ auf die Bildung von Kopien bei der Ubergabe zwischen den Lokationen verzichtet werden, da Strings unver¨anderbare Objekte sind.
Abbildung 7.28. Verbindung der Handler in der zweiten Schicht
Weitere Konfigurationen sind moglich, ¨ indem etwa die Socket- und URLConnection-Objekte durch Dummies oder wie in Abbildung 7.29 Reader und Writer durch eine Pipe ersetzt werden. Testtreiber sind fur ¨ die verschiedenen Konfigurationen teilweise wiederverwendbar, mussen ¨ aber unter Umst¨anden angepasst werden. So ist in der Konfiguration aus Abbildung 7.29 sicherzustellen, dass die in der Pipe abgelegten Daten wieder ausgelesen werden. Das heißt, HttpConnection ist entsprechend oft zu aktivieren. In der in Abbildung 7.27 dargestellten Konfiguration entf¨allt dieser Zusatzaufwand fur ¨ den Treiber.
244
7 Testmuster im Einsatz
Abbildung 7.29. Verbindung der Handler in der dritten Schicht
7.5.5 Muster fur ¨ Verteilung und Kommunikation Die Simulation von Verteilung und der sich daraus ergebende Aufwand fur ¨ statische Variablen und Kommunikation kann in dem Muster aus Tabelle 7.30 zusammengefasst werden. Muster: Verteilung und Kommunikation Intention
Echte Verteilung ist sehr schwer zu testen und verteilte Testl¨aufe sind zeitaufw¨andig. Aus Effizienzgrunden ¨ ist es deshalb sinnvoll, Verteilung in einem Prozessraum zu simulieren. Anwendung Eine Anwendung dieses Musters ist sinnvoll, wenn • verteilte Systeme getestet werden sollen und • die Prufung ¨ globaler Systemzust¨ande uber ¨ verteilte Subsysteme hinweg notwendig ist oder • die Kommunikation beziehungsweise Teilaspekte wie Verschlusselung ¨ oder Kommunikationszust¨ande gepruft ¨ werden sollen. Struktur
Es werden Lokationen (Tabelle 7.24) benutzt, um die physische Verteilung zu modellieren. Statische Variablen und Factories werden mit dem Muster 7.25 fur ¨ die Lokationen individualisiert und in der Implementierung wird verborgen, welche Lokation gerade aktiv ist. Die Kommunikation wird entsprechend der nachfolgenden Klassenstruktur aufgebaut. Verschiedene Konfigurationen erlauben den flexiblen Einsatz im Produktionssystem und in Testf¨allen. (Fortsetzung auf n¨achster Seite)
7.6 Zusammenfassung
245
(Fortsetzung von Tabelle 7.30.: Muster: Verteilung und Kommunikation)
Implementierung Dummy
Beachtenswert
Im Wesentlichen wird vom Dummy direkt zum Server delegiert, wobei unter Umst¨anden als Argumente angegebene Objektstrukturen zu kopieren beziehungsweise die in der jeweiligen Lokation bereits vorhandenen Duplikate zu verwenden sind. Der Dummy wirkt als Schnittstelle zwischen verschiedenen Lokationen und ist daher verantwortlich fur ¨ den Wechsel des Prozesskontexts bei Aufruf des Servers und dessen Return. Der Server muss nicht notwendigerweise dasselbe Interface wie das Proxy realisieren. Dann ist der Umsetzungsaufwand im Dummy entsprechend gro¨ ßer.
Tabelle 7.30.: Muster: Verteilung und Kommunikation
7.6 Zusammenfassung Dieses Kapitel diskutiert einerseits, wie UML/P-Modelle praktisch zur Testfallmodellierung eingesetzt werden und beschreibt andererseits Testmuster, die fur ¨ die Prufung ¨ funktionaler Eigenschaften eines verteilten oder nebenl¨aufigen Systems geeignet sind. Damit lassen sich Testling und Testumgebung fur ¨ funktionale, automatisierte Tests vorbereiten. W¨ahrend die Verwendung von Dummies in der Literatur zum Thema Testen standardm¨aßig vorgeschlagen wird [Bin99, LF02], sind die Muster zur Simulation von Zeit, Kommunikation und Verteilung fur ¨ funktionale Tests in dieser Konsequenz neu. Auch die Diskussion der Probleme bei der Verwendung von Frameworks und Komponenten sind mit Ausnahme von [SD00] sonst kaum zu finden und werden sonst nirgendwo in dieser Systematik durch Simulationen mit Adaptern ersetzt. Eines der Ergebnisse dieses Kapitels ist die klare Forderung, die Testbarkeit des Systems bereits beim Entwurf zu berucksich¨ tigen, auch wenn dadurch Indirektionsstufen zum Beispiel durch Adapter entstehen.
8 Refactoring als Modelltransformation
Nichts auf der Welt ist so kraftvoll wie eine Idee deren Zeit gekommen ist. Victor Hugo
Refactoring bedeutet Anwendung systematischer und beherrschbarer Transformationsregeln zur Verbesserung des Systementwurfs unter Beibehaltung des extern beobachtbaren Verhaltens. In diesem Kapitel werden zun¨achst die Methodik zur Anwendung von Refactoring-Regeln und die allgemeinere Theorie der Modelltransformationen diskutiert. Auf dieser Grundlage wird ein praktisch verwendbarer Beobachtungsbegriff entwickelt, der auf UML/P-Testf¨allen beruht. Konkrete Refactoring-Regeln fur ¨ die UML/P werden drauf aufbauend im n¨achsten Kapitel besprochen.
8.1 8.2 8.3
Einfuhrende ¨ Beispiele fur ¨ Transformationen . . . . . . . . . . 248 Methodik des Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 Theorie der Modelltransformationen . . . . . . . . . . . . . . . . . 258
248
8 Refactoring als Modelltransformation
Die Anforderungen an ein im Einsatz befindliches Softwaresystem a¨ ndern sich mit dem Gesch¨aftsmodell genauso wie mit den steigenden Anspruchen ¨ der Anwender an die Softwarefunktionalit¨at. Die technologische Basis einer Anwendung, wie das zugrundeliegende Betriebssystem, benutzte Frameworks oder Nachbarsysteme, unterliegt einer st¨andigen Evolution. W¨ahrend eines Projekts entstehen neue Anforderungen der Anwender, die flexibel in das Projekt einzubringen sind. Zus¨atzlich dazu ist die Komplexit¨at heutiger Software meistens so hoch, dass eine allen Anforderungen gewachsene Architektur des Systems nicht von Anfang an entwickelt werden kann. Aus diesen Grunden ¨ ist es notwendig, Techniken zu beherrschen, mit denen Software weiterentwickelt sowie den ge¨anderten Anforderungen und einem neuen technischen Umfeld angepasst werden kann. Eine der dafur ¨ verwendbaren Techniken ist das Refactoring vorhandener Software [Fow99, Opd92], das aus einer Sammlung zielgerichteter und systematisch anwendbarer Transformationen besteht, mit denen eine konsequente Verbesserung einer Systemarchitektur vorgenommen werden kann. Diese Verbesserung dient anschließend als Grundlage fur ¨ Erweiterungen des Systems. Ziel dieses Kapitels ist zun¨achst die Demonstration der methodischen Vorgehensweise anhand von Beispielen. Darauf aufbauend steht die Einbettung von Refactoring-Techniken in die Theorie der Modelltransformationen im Vordergrund. Zun¨achst werden in Abschnitt 8.1 einige Beispiele verschiedener Arten von Refactoring-Anwendungen vorgestellt. Der Abschnitt 8.2 enth¨alt eine methodische Einordnung der Refactoring-Techniken in den Softwareentwicklungsprozess. In Abschnitt 8.3 werden die Konzepte der Modelltransformationen, von denen Refactoring einen Spezialfall darstellt, erl¨autert. Dabei wird die Semantik von Transformationsregeln pr¨azisiert und ein fur ¨ die in [Rum04c] vorgestellte Vorgehensweise geeigneter Beobachtungsbegriff definiert.
8.1 Einfuhrende ¨ Beispiele fur ¨ Transformationen Dieser Abschnitt demonstriert anhand einiger kleiner Beispiele welche Arten von Refactoring-Schritten es gibt. Wie bereits in [Fow99] diskutiert, kann anhand kleinster Beispiele zwar das Prinzip, nicht aber die Motivation fur ¨ Refactoring demonstriert werden. [Fow99] nutzt deshalb ein gro¨ ßeres Beispiel von 50 Seiten, um zu zeigen, dass mit Refactoring-Techniken Komplexit¨at beherrschbar wird. Mit den nachfolgenden kleinen Beispielen wird deshalb weniger die Notwendigkeit des Einsatzes von Refactoring motiviert, sondern die Einsatzmoglichkeiten ¨ beschrieben.
8.1 Einfuhrende ¨ Beispiele fur ¨ Transformationen
249
In Abbildung 8.1 ist eine fur ¨ die nachfolgenden Beispiele ausreichende Begriffsdefinition aus der Literatur angegeben.1 In Abbildung 9.2 wird darauf aufbauend eine detaillierte Begriffsdefinition vorgenommen. Definitionen des Begriffs Refactoring“ aus der Literatur: ” • Refactoring“ l¨asst sich als Operation zur Restrukturierung eines Programms cha” rakterisieren, die den Entwurf, die Evolution und die Wiederverwendung objektorientierter Frameworks unterstutzt. ¨ [Opd92, S. iii] Das zum Thema Refactoring am meisten zitierte Werk [Fow99] bietet in der deut¨ schen Ubersetzung [Fow00] folgende Definitionen: •
¨ Refaktorisierung (Substantiv): Eine Anderung an der internen Struktur einer ” Software, um sie leichter verst¨andlich zu machen und einfacher zu ver¨andern, ohne ihr beobachtetes Verhalten zu a¨ ndern.“ [Fow00, S. 41]
•
Refaktorisieren (Verb): Eine Software umstrukturieren, ohne ihr beobachtbares ” Verhalten zu a¨ ndern, indem man eine Reihe von Refaktorisierungen anwendet.“ [Fow00, S. 41] Abbildung 8.1. Begriffsdefinitionen fur ¨ das Refactoring“ ”
In [Opd92] werden Refactoring-Schritte auch als Pl¨ane zur Reorganisation von Software bezeichnet, die Ver¨anderungen auf einem mittleren Level erlauben. Als Low-Level“ werden dort Ver¨anderungen einzelner Code” Zeilen und als High-Level“ die Anpassung ganzer, fur ¨ den Anwender sicht” barer Funktionalit¨aten bezeichnet. [Opd92] sieht Refactoring vor allem als Technik zur Weiterentwicklung von Frameworks. Ein Einsatz zur Evolution der Architektur innerhalb eines Projekts wird erst in [Fow99] vorgeschlagen und zum Beispiel im Extreme Programming-Ansatz (siehe Abschnitt 2.2, Band 1) eingesetzt. In Erweiterung der gegebenen Definitionen wird in diesem Buch jegliche Transformation als Refactoring bezeichnet, die ein gegebenes Modell und die darin enthaltenen Codeteile durch Anwendung eines oder mehrerer Schritte in ein neues, nach einem geeigneten Beobachtungsbegriff a¨ quivalentes Modell transformiert. Algebraische Umformungen Die einfachste Form des Refactorings ist die algebraische Umformung eines Ausdrucks. Dabei werden die mathematischen Gleichungen herangezogen, die zum Beispiel zur Vereinfachung von Ausdrucken ¨ eingesetzt werden konnen. ¨ Eine solche Regel ist beispielsweise: 1
In [Fow00] wird Refactoring mit Refaktorisierung“ ubersetzt ¨ und fur ¨ die Anwen” dung einer Regel das Wort refaktorisieren“ verwendet. Dieses Buch nutzt, wie ” auch andere deutsche Publikationen [LRW02], die englischen Originalbegriffe.
250
8 Refactoring als Modelltransformation
Diese Regel kann sowohl auf Java-Ausdrucke ¨ als auch auf die OCL angewandt werden. Die Regel nutzt die bereits in Kapitel 3 eingefuhrten ¨ Schemavariablen als Platzhalter fur ¨ andere Ausdrucke. ¨ So einfach diese Regel aussieht, so hat sie bei einem Einsatz in Java doch Kontextbedingungen. So darf der fur ¨ a eingesetzte Ausdruck keine Seiteneffekte haben. Zum Beispiel wurde ¨ nach der Anwendung der Transformation auf (i++)+(i++) die Variable i einen anderen Inhalt haben und ein anderes Ergebnis entstehen. Außerdem muss a deterministisch sein. Eine Abfrage der Zeit mit der statischen Query Time.now() fur ¨ a w¨are zum Beispiel nicht geeignet. Dies wurde ¨ insbesondere dann auffallen, wenn die Regel:
angewandt werden wurde. ¨ Bei algebraischen Umformungen konnen ¨ weitere Effekte auftreten, die zu beachten sind. So kann der Tausch in der Reihenfolge einer Addition gem¨aß Regel
¨ zu einem Uberlauf fuhren, ¨ der nur in der neuen Berechnung auftritt, wenn a und c sehr groß und b negativ sind. Umgekehrt kann mit einer algebraischen Umformung das System robuster gegen arithmetische Exceptions gemacht werden. Durch geeignete Umformung numerischer Berechnungen kann das Ergebnis außerdem in seiner Genauigkeit beeinflusst werden. Algebraische Umformungen sind jedoch keineswegs auf numerische Berechnungen beschr¨ankt, sondern konnen ¨ auch auf andere Grunddatentypen und Container angewandt werden. Dazu z¨ahlen Vereinfachungen boolescher Berechnungen, Umformungen bei Strings, Ausnutzung von Kommutativit¨ats- und anderen Gesetzen bei Mengen, etc. Allerdings ist dabei die Seiteneffektfreiheit und die vorhandene Identit¨at zum Beispiel bei Containern zu beachten.
Die ebenfalls aus der Mathematik bekannte Substitution von Gleichem erfordert eine mathematisch formulierbare Kontextbedingung:
8.1 Einfuhrende ¨ Beispiele fur ¨ Transformationen
251
Diese Regel kann zum Beispiel angewandt werden, um einen Methodenaufruf durch einen anderen, allgemeineren zu ersetzen:
Damit l¨asst sich eine neue, allgemeinere Methode bar einfuhren ¨ und die alte Methode foo eliminieren. Diese Regel fordert in ihren Kontextbedingungen auch die Gultigkeit ¨ einer OCL-Invariante. Algebraische Umformungen sind aus der Mathematik und den algebraischen Spezifikationstechniken [BFG+ 93, EM85] bekannt, werden aber nicht als Kern der fur ¨ objektorientierte Sprachen entwickelten RefactoringTechniken angesehen. Sie sind aber eine Grundlage, um die oft notwendigen Umformungen von Coderumpfen ¨ und Invarianten durchzufuhren. ¨ Expansion einer Methode Ein weiteres Beispiel ist die Expansion einer Methode:
Diese Regelanwendung nutzt dasselbe Prinzip, das beim Inlining“ von ” Methoden, zum Beispiel von optimierenden Compilern durchgefuhrt ¨ wird. Allerdings wird bei der Verwendung einer solchen Regel im Refactoring ein anderes Ziel verfolgt. So kann durch die Expansion oft eine nachfolgende algebraische Vereinfachung vorgenommen werden oder das entstandene Codestuck ¨ durch einen anderen Methodenaufruf wieder faktorisiert werden. Tats¨achlich l¨asst sich die beschriebene Expansionsregel auch in umgekehrter Richtung als Extraktionsregel2 einsetzen, um Teilfunktionalit¨at zu faktorisieren. Die Teilfunktionalit¨at kann damit besser wiederverwendet oder eigenst¨andig getestet werden und l¨asst sich in Unterklassen besser redefinieren. Die Anwendung der Expansionsregel hat ebenfalls Kontextbedingungen und erfordert meistens auch zus¨atzliche Maßnahmen zur Anpassung des expandierten Codes. Ist die expandierte Methode von einer anderen Klasse, so ist zum Beispiel als Kontextbedingung zu fordern, dass kein Attribut dieser Klasse verwendet wird. Dies kann durch eine vorbereitende Maßnahme erreicht werden, indem alle verwendeten Attribute durch Methodenaufrufe gekapselt werden. 2
Die Regel zur Extraktion ist in anderen Transformationsans¨atzen als Folding“ be” kannt.
252
8 Refactoring als Modelltransformation
Durch die Expansion geht die dynamische Bindung der Methode verloren. Deshalb darf die expandierte Methode in keiner Unterklasse redefiniert worden sein. Die Kontextbedingungen fur ¨ das Faktorisieren eines Codestucks ¨ in eine Teilmethode sind noch komplexer. Allerdings sind viele dieser Kontextbedingungen durch syntaktische Analysen uberpr ¨ ufbar, ¨ so dass die Korrektheit einer solchen Regelanwendung wie auch im Fall der Expansion automatisch gepruft ¨ werden kann. Restrukturierung der Klassenhierarchie Weitere Refactoring-Techniken bearbeiten die durch Klassendiagramme vorgegebene Struktur des Systems. So konnen ¨ Abstraktionen als neue, gemeinsame Oberklassen faktorisiert werden oder Attribute und Methoden entlang der Klassenhierarchie verschoben werden. Die Abbildung 8.2 demonstriert die Einfuhrung ¨ einer neuen Klasse inmitten der Klassenhierarchie. Diese Klasse erh¨alt die aus der Unterklasse extrahierte gemeinsame Implementierung der Methode validateBid(), die an diese Stelle verschoben wird. Damit dies moglich ¨ wird, werden klassenspezifische Unterschiede in eigenst¨andige Methoden faktorisiert. Zum Beispiel enth¨alt die neue Methode setNewBestBid() den fur ¨ die Unterklassen spezifischen Vergleich, ob sich ein Gebot als neues Bestgebot qualifiziert.
Abbildung 8.2. Einfuhrung ¨ einer neuen Oberklasse
8.2 Methodik des Refactoring
253
Das dargestellte Refactoring ist bereits auf die zu bearbeitende Applikation spezialisiert und zeigt mehrere Schritte gleichzeitig. Unter Benutzung der in Abschnitt 3.4.2 eingefuhrten ¨ Schemavariablen l¨asst sich die Einfuhrung ¨ der neuen Klasse auch allgemeiner darstellen, indem statt konkreten Klassen- und Methodennamen ausfullbare ¨ Platzhalter eingesetzt werden.3 Attribute und Methoden konnen ¨ entlang von Assoziationen verschoben werden, wenn die entsprechende Assoziation und die daran beteiligten Klassen bestimmte Bedingungen erfullen. ¨ So darf sich ein etablierter Link einer solchen Assoziation normalerweise nicht mehr ver¨andern, um den Zugriff auf ein verlagertes Attribut zu erlauben. Diese zeitlichen Kontextbedingungen konnen ¨ aber durch eine statische Analyse normalerweise nicht mehr erkannt werden sondern erfordern zus¨atzliche Maßnahmen, wie zum Beispiel den Einsatz geeigneter Invarianten und Tests. Signaturver¨anderung Refactoring-Schritte konnen ¨ interne Ver¨anderungen bewirken oder Ver¨anderungen der Signatur von Systemteilen hervorrufen. Die Entfernung einer nicht mehr benotigten ¨ lokalen Variable ist zum Beispiel unkritisch. Die Entfernung einer Methode ist dann problematisch, wenn diese in der Signatur der Klasse oder des Subsystems publiziert wurde und es moglich ¨ ist, dass andere Entwickler die geloschte ¨ Methode verwenden. Deshalb ist es notwendig, Refactoring nicht auf lokal begrenzte, offene Teilsysteme anzuwenden, sondern das System moglichst ¨ in seiner Ganzheit zu bearbeiten. Die Ver¨anderung von Signaturen zeigt auch, dass hier der Beobachtungs¨ begriff eine wesentliche Rolle spielt. Uber die in Abbildung 8.1 geforderte Verhaltens¨aquivalenz des Gesamtsystems hinaus bieten Schnittstellen innerhalb des Systems und zu anderen Systemteilen zus¨atzliche Beobachtungspunkte, die bei einer Anwendung von Refactoring-Techniken zu beachten sind.
8.2 Methodik des Refactoring 8.2.1 Technische und methodische Voraussetzungen fur ¨ Refactoring Wie viele andere Elemente im Portfolio einer agilen Methodik ist Refactoring besonders erfolgreich in Kombination mit weiteren Techniken und Konzepten: 3
Schemavariablen konnen ¨ auch als Platzhalter fur ¨ Listen von Attributen oder Unterklassen eingesetzt werden, allerdings ist dafur ¨ eine intuitive graphische Darstellung notwendig, die in diesem Buch nicht vertieft wird.
254
• • • • •
8 Refactoring als Modelltransformation
Objektorientierung Automatisierte Tests Gemeinsamer Modell- und Codebesitz Keine oder kaum zus¨atzliche Dokumentation Modellierungs- beziehungsweise Codierungsstandards
Bereits in [Mey97] werden objektorientierte Konzepte als hilfreich fur ¨ die Wiederverwendung von Softwarekomponenten angesehen. Insbesondere die Bildung von Unterklassen, die dynamische Konfigurierbarkeit von Objektstrukturen, die dynamische Bindung von Methoden und die daraus resultierende Moglichkeit ¨ zur partiellen Redefinition von Verhalten werden als Faktoren zur besseren Wiederverwendung erkannt. Die in Kapitel 7 vorgestellten Testmuster zeigen, dass objektorientierte Konzepte auch zur Definition von Tests hilfreich eingesetzt werden konnen. ¨ Automatisierte Tests aber bilden einen wesentlichen Eckpfeiler fur ¨ den Erfolg der Refactoring-Technik. Automatisierte Tests erlauben die effiziente ¨ Uberpr ufung, ¨ ob bei der Durchfuhrung ¨ eines Refactorings die nicht direkt betroffene Funktionalit¨at noch ihre Aufgaben erfullt. ¨ Fehlen Tests fur ¨ Systemteile die einem Refactoring unterworfen werden sollen, so ist zu empfehlen, zun¨achst geeignete Tests zu entwickeln. Wie in Abschnitt 8.3.3 noch besprochen wird, legen außerdem die vom Anwender vorgegebenen Akzeptanztests einen Beobachtungsbegriff fur ¨ Refactoring-Schritte fest. Ist jedes Artefakt einem Besitzer zugeordnet, der dieses Artefakt kontrolliert und als einziger modifizieren darf, dann ist ein Refactoring des Systems zum Scheitern verurteilt. Der Abstimmungsaufwand, der zwischen den Besitzern notwendig ist, an mehreren Artefakten parallel (und aufgrund der ¨ Mikro-Iterationen nahezu zeitgleich) Anderungen vorzunehmen, ist praktisch nicht sinnvoll. Hinzu kommt, dass Besitzer eines Artefakts die zus¨atzliche Arbeitsbelastung ablehnen, wenn ein Refactoring nicht fur ¨ sie, sondern nur fur ¨ das Nachbarsystem von Vorteil ist. In solchen F¨allen wird oft nicht die fur ¨ die Architektur beste Losung, ¨ sondern die fur ¨ den Diskussionssieger am wenigsten arbeitsintensive Losung ¨ gew¨ahlt. Der gemeinsame Modellbesitz erlaubt einzelnen Entwicklern, Refactorings auch uber ¨ Schnittstellen und Artefakte hinweg effizient durchzufuhren ¨ und erst das modifizierte und wieder komplett lauff¨ahige System in das Repository einzuchecken. Durch die Existenz der automatisierten Tests verlagert sich der Abstimmungsaufwand zwischen den Entwicklern zu einem Abstimmungs” aufwand“ zwischen dem Entwickler (oder Entwickler-Paar) und den automatisch durchfuhrbaren ¨ Tests. Refactoring ver¨andert die Struktur eines Systems. Ist die Struktur nicht nur durch die fur ¨ die Codegenerierung verwendeten Modelle, sondern zus¨atzlich durch weitere Dokumentation beschrieben, so entsteht der Aufwand, diese Dokumente ebenfalls zu aktualisieren. Dieser Aufwand kann aber leicht dieselbe Großenordnung ¨ wie das Refactoring selbst erreichen und ubertreffen. ¨ Nicht zu untersch¨atzen ist dabei der Aufwand, dass in einem
8.2 Methodik des Refactoring
255
Dokument die zu a¨ ndernden Stellen zun¨achst zu identifizieren sind. Die Si¨ cherung der Qualit¨at eines Dokuments, also insbesondere der Ubereinstimmung mit der Implementierung ist hier besonders kritisch. Werden dafur ¨ keine geeigneten Maßnahmen getroffen, so ist das Vertrauen der Entwickler in die Aktualit¨at der Dokumente nicht gegeben und die Dokumente sind relativ wertlos. Refactoring kann also am effektivsten eingesetzt werden, wenn keine detaillierte Dokumentation existiert. Entsprechend sind aber pr¨azise Modellierungsstandards einzuhalten, damit den Entwicklern der Zugang zu den Modellen erleichtert wird. Auch Ans¨atze wie Literate Programming“ ” mit intensiver Verzahnung von Modell und Dokumentation sind wenig geeignet, da die informelle Dokumentation nicht automatisch angepasst werden kann. 8.2.2 Qualit¨at des Designs Wie in Abbildung 8.3 skizziert, ist Refactoring orthogonal zur Entwicklung neuer Funktionalit¨at. W¨ahrend in der normalen Weiterentwicklung neue Funktionalit¨at hinzugefugt ¨ und dabei in Kauf genommen wird, die Qualit¨at des Designs zu verschlechtern, wird beim Refactoring die Funktionalit¨at beibehalten und die Qualit¨at des Designs normalerweise verbessert.
Abbildung 8.3. Refactoring und Weiterentwicklung erg¨anzen sich
Fur ¨ die Funktionalit¨at eines Systems existieren Maße, wie beispielsweise die Function Point Methode [AG83] oder deren objektorientierte Anpassung [Sne96]. Im einfachsten Fall kann auch der Anteil der bereits umgesetzten Anforderungen als Maß fur ¨ die implementierte Funktionalit¨at verwendet werden. Demgegenuber ¨ gibt es kein allgemein anerkanntes Verfahren, um die Qualit¨at eines Designs objektiv zu messen. Kriterien, die fur ¨ Programmiersprachen entwickelt wurden, um die Optimalit¨at“ eines Designs zu messen, ” sind abh¨angig von der Programmiersprache und der Erfahrenheit der Entwickler. Zum Beispiel werden fur ¨ objektorientierte Sprachen andere Kriterien vorgeschlagen, als vor 10-20 Jahren fur ¨ prozedurale Sprachen diskutiert
256
8 Refactoring als Modelltransformation
wurden. Wesentlich sind zum Beispiel, die Kopplung von Klassen moglichst ¨ gering zu halten, einzelne Klassen nicht zu groß oder klein zu halten, die Tiefe einer Vererbungshierarchie zu beschr¨anken, etc. Defizite werden unter anderem in [Fow99] als Bad Smells“ bezeichnet und 22 davon aufgez¨ahlt. ” Jedoch sind nicht nur Refactoring-Schritte sinnvoll, die das Design bezuglich ¨ einer gegebenen Metrik verbessern. Refactoring sollte vor allem auch eingesetzt werden, um in einem nachfolgenden Schritt neue Funktionalit¨at besser hinzufugen ¨ zu konnen. ¨ Das zielgerichtete Refactoring kann daher zun¨achst ein Design verschlechtern, um neue Funktionalit¨at hinzuzufugen ¨ oder um weitere Refactoring-Schritte vorzunehmen. Ein typisches Beispiel ist etwa die Teilung einer Klasse in zwei, durch eine 1-zu-1-Assoziation verbundene Klassen und die nachfolgende Verallgemeinerung der Assoziation in die Form 1-zu-*. Hilfreich ist dabei zum Beispiel die Identifikation von Defiziten mit Metriken sowie der Vorschlag zu deren Behebung durch geeignete RefactoringSchritte. Die Entscheidung, ob ein Refactoring-Vorschlag durchgefuhrt ¨ wird, muss allerdings beim Anwender liegen, der das Design und die zugrunde liegende Motivation kennt. Eventuelle Metrik-Werkzeuge sind allerdings fur ¨ die UML/P erst geeignet zu adaptieren, da es sich bei der UML/P um eine eigenst¨andige Modellierungs- und Programmiersprache handelt. Einige durch diese Metriken typischerweise gemessenen Elemente, wie die Vererbungshierarchie oder die Kopplung von Klassen konnen ¨ weitgehend unver¨andert ubernom¨ men werden. Andererseits erlaubt die großere ¨ Kompaktheit der UML gegenuber ¨ Java eine großere ¨ Dichte von Funktionalit¨at innerhalb einer Klasse und damit die Reduktion der Anzahl notwendiger Klassen. Immerhin wird ein Teil der Standardfunktionalit¨at, wie get- und set-Methoden fur ¨ Attribute, technische Methoden zur Speicherung, etc. durch den Codegenerator hinzugefugt ¨ und sind deshalb im Modell fur ¨ den Entwickler nicht mehr sichtbar. Parallel dazu konnen ¨ fur ¨ die Notationen der UML/P weitere Kriterien guten Entwurfs angegeben werden. Ein Objektdiagramm, das sehr viele Objekte beinhaltet sollte zum Beispiel in mehrere Objektdiagramme geteilt und durch die in Abschnitt 5.3, Band 1 skizzierte Logik fur ¨ Objektdiagramme kombiniert werden. Auch die Große ¨ und Form von Statecharts, Sequenzund Klassendiagrammen sowie von OCL-Bedingungen konnen ¨ geeigneten Metriken unterliegen, die aber erst auf Basis empirischer Untersuchungen erstellt werden mussen. ¨ Zum Beispiel kann dabei die aus anderen Bereichen bereits bekannte Regel eingesetzt werden, dass ein Betrachter nur bis zu 5±2Elemente gleichzeitig erfassen kann. 8.2.3 Refactoring, Evolution und Wiederverwendung Das Refactoring eines Systems fuhrt ¨ zun¨achst zu keiner fur ¨ den Anwender und Kunden sichtbaren Verbesserung der Systemfunktionalit¨at. Entspre-
8.2 Methodik des Refactoring
257
chend ist die Motivation ein Refactoring durchzufuhren ¨ gut zu begrunden. ¨ Idealerweise sollte auch dafur ¨ statistisch untermauertes Zahlenmaterial zur Verfugung ¨ stehen, das heute aufgrund der noch begrenzten Verwendung von Refactoring und der noch stark im Aufbau befindlichen Werkzeugunterstutzung ¨ noch nicht vorhanden sein kann. Unter okonomischen ¨ Gesichtspunkten betrachtet ist ein Refactoring nur dann sinnvoll, wenn es auf ein Ziel ausgerichtet ist, das den Arbeitsaufwand rechtfertigt. Wie in Abbildung 8.3 gezeigt, muss das entstehende System kein optimales Design besitzen. Das Design muss jedoch w¨ahrend des gesamten Entwicklungsprozesses gut genug sein, um Weiterentwicklungen zu erlauben. Um auch zukunftige ¨ Weiterentwicklungen zu ermoglichen, ¨ sollte das Design auch gegen Ende des Projekts eine gewisse Qualit¨at besitzen. Deshalb ist der Nutzen eines Refactorings immer gegen den notwendigen Aufwand abzusch¨atzen. Insbesondere fur ¨ große Modifikationen, die die technische oder fachliche Architektur des Systems ver¨andern, indem sie zum Beispiel die Kommunikationsinfrastruktur ( Middleware“) austau” schen oder Kernelemente der Datenstruktur modifizieren, sollten vorher in ihren Aufw¨anden abgesch¨atzt werden. Dazu liegt derzeit kein Vergleichszahlenmaterial vor. Es kann jedoch aufgrund bisheriger praktischer Erfahrungen festgehalten werden, dass bei konsequenter Anwendung von Refactoring-Schritten sogar durch die relativ einfache Unterstutzung ¨ durch Suchund Ersetzungs-Funktionalit¨at einer Entwicklungsumgebung und einer vorhandenen Testsammlung der Fortschritt deutlich schneller ist, als zun¨achst gesch¨atzt. Refactoring-Schritte konnen ¨ nicht nur innerhalb eines Softwareentwicklungsprojekts, sondern auch fur ¨ den ursprunglich ¨ in [Opd92] vorgesehenen Zweck, der Evolution und Wiederverwendung von Frameworks in unterschiedlichen Projekten, eingesetzt werden. Ein Framework wird normalerweise als eigenst¨andiges Ziel weiterentwickelt, indem in jeder Applikation neue Funktionalit¨at zum Framework hinzugefugt ¨ wird und notwendige Restrukturierungen vorgenommen werden. Dabei wird aber besonders darauf geachtet, die Kompatibilit¨at des Frameworks mit den ursprunglichen ¨ Applikationen zu wahren, um auch diese weiterentwickeln zu konnen. ¨ In einem agilen Projekt spielen bei An¨ wendung des Prinzips der Einfachheit diese Uberlegungen keine Rolle. Deshalb ist Extreme Programming ohne Anpassungen nicht geeignet, um damit Frameworks zu entwickeln. Um die Wiederverwendung von Frameworks zu erhohen, ¨ sind zus¨atzliche methodische Vorkehrungen zu treffen, die eine Abschirmung des Frameworks gegen beliebige Modifikationen erlauben. In diesem Fall sind agile und Framework-basierte Methoden zu kombinieren, wie dies zum Beispiel in [FPR01] skizziert ist.
258
8 Refactoring als Modelltransformation
8.3 Theorie der Modelltransformationen In Abbildung 8.1 wurde eine Definition des Refactorings angegeben, dessen Konzepte in diesem Abschnitt genauer analysiert werden. Dabei werden die Natur von Modelltransformationen, ein Beobachtungsbegriff, ein Transformationskalkul ¨ und das Zusammenspiel mit der Semantik der Modellierungssprache diskutiert. 8.3.1 Modelltransformationen In diesem Abschnitt werden die prinzipiellen Konzepte fur ¨ Modelltransformationen eingefuhrt ¨ und deren Wirkungen und Effekte diskutiert. Dazu wird auf der in Abschnitt 3.3 diskutierten formalen Semantik der Codegenerierung aufgebaut. Eine Modelltransformation besitzt auf dieser Abstraktionsstufe keine grunds¨atzlichen Unterschiede zu einer Codegenerierung. Grunds¨atzlich ist auch eine Modelltransformation eine Abbildung ausgehend von einem syntaktisch wohlgeformten Ausdruck der Quellsprache UML/P. Als Zielsprache wird jedoch nicht Java, sondern wieder die Quellsprache UML/P eingesetzt. Grunds¨atzlich bleibt aber auch die Vorgehensweise zur Betrachtung der Bedeutung, also der Semantik einer Modelltransformation dieselbe. Abbildung 8.4 beschreibt aufbauend auf Abbildung 3.9 das Grundmuster einer parametrisierbaren Modelltransformation. Zur kompakten Formalisierung des Modelltransformations-Konzepts werden folgende Definitionen benotigt: ¨ •
die Modellsprache (UML/P) als eine Menge UML von syntaktisch wohlgeformten Ausdrucken ¨ und
•
eine Skriptsprache zur Beschreibung der Transformation mit dem Sprachschatz S.
Eine Modelltransformation ist demnach eine Abbildung T : UML → UML. Wird die Modelltransformation mit einem Skript parametrisiert, so entspricht dies einer Abbildung Tp : S × UML → UML. ¨ Modelltransformationen. Tp ist damit eine Art Interpreter der Skriptsprache S fur Auf Basis der gelifteten, aber mathematisch a¨ quivalenten Fassung Tp : S → UML → UML ist fur ¨ ein Skript s ∈ S jede Transformation von der Form Tp (s). Abbildung 8.4. Prinzip einer Modelltransformation
Da Modelltransformationen Abbildungen sind, konnen ¨ ihre diesbezugli¨ chen Eigenschaften untersucht werden. Eine Abbildung kann partiell definiert sein, weil die Voraussetzungen fur ¨ die Anwendbarkeit der Transformation nicht gegeben sind oder das entstehende neue Modell nicht wohldefiniert ist. Beispielsweise kann eine Unterklasse nur eingefugt ¨ werden, wenn
8.3 Theorie der Modelltransformationen
259
die Oberklasse existiert. Soll andererseits eine Klasse eingefugt ¨ werden, die bereits existiert, so entsteht ein nicht wohlgeformtes Modell. Wie in diesen beiden Beispielen konnen ¨ viele dieser Voraussetzungen automatisch gepruft ¨ werden. Dies ist aber nicht mit allen Voraussetzungen moglich. ¨ Eine Modelltransformation kann injektiv sein. Interessant ist aber vor allem der Fall, wenn diese nicht injektiv ist, da dann offensichtlich zun¨achst unterschiedliche Modelle auf dasselbe neue Modell abgebildet werden und informationstragende Details verloren gehen. Solche Transformationen haben typischerweise Abstraktionscharakter und sind nicht notwendigerweise in allen Details semantikerhaltend. Ein Beispiel ist die Entfernung aller Attribute aus einem Klassendiagramm. So entsteht eine abstraktere Darstellung mit weniger Detailinformation. Die Surjektivit¨at einer Modelltransformation ist normalerweise nicht gegeben. Untersuchenswert ist aber bei einem Kalkul, ¨ ob die Gesamtheit der zur Verfugung ¨ stehenden Modelltransformationen und deren Kombination surjektiv ist. Damit w¨are es moglich, ¨ aus einem generischen Start, zum Beispiel einem leeren“ Modell, jedes Modell durch Transformationsschritte zu ” entwickeln. 8.3.2 Semantik einer Modelltransformation Prinzip der Semantik fur ¨ eine Transformation Die Verwendung einer Abbildung statt einer Relation zur Erkl¨arung von Modelltransformationen weist auf den konstruktiven Charakter der Transformation hin. Eine Relation, wie zum Beispiel eine Abstraktions-4 oder Verfeinerungsrelation, kann auf der Seite der Semantik verwendet werden. In Abbildung 8.5 wird in Erweiterung der Abbildung 8.4 das Prinzip zur Semantikdefinition einer Modelltransformation diskutiert. Das in Abbildung 8.5 beschriebene Prinzip der Semantik einer Modelltransformation kann so verstanden werden, dass fur ¨ die Korrektheit einer Transformation die graphisch dargestellte Kommutativit¨at nachgewiesen wird. Jedoch ist dies in der Praxis insbesondere bei großeren ¨ Transformationen nicht ohne weiteres durchfuhrbar. ¨ Stattdessen wird dort durch explizite Diskussion von zu beachtenden Sonderf¨allen und durch Nutzung von Testsammlungen mit Invarianten eine informelle Rechtfertigung fur ¨ die Transformation gegeben, der Nachweis der Korrektheit aber dem Ausfuhren¨ den uberlassen. ¨ Bei der in Abschnitt 9.1.2 diskutierten Granularit¨at zum Beispiel der Refactoring-Regeln aus [Fow99] w¨are fur ¨ eine Formalisierung 4
Eine abstrakte Oberklasse stellt eine Abstraktion innerhalb des Modells dar. Davon zu unterscheiden ist eine Modelltransformation, die eine Abstraktion zwischen Modellen durchfuhrt. ¨ Die Einfuhrung ¨ einer neuen Oberklasse in ein Klassendiagramm ist in diesem Sinn keine Modellabstraktion, sondern im Gegenteil fugt ¨ neue Information zum Modell hinzu. Diese ist allerdings im Allgemeinen extern nicht beobachtbar und deshalb ein Refactoring.
260
8 Refactoring als Modelltransformation
Zur Darstellung der Semantik einer Modelltransformation werden aufbauend auf Abbildung 8.4 folgende Definitionen benotigt: ¨ •
eine geeignete formale Zielsprache mit dem Sprachschatz Z,
•
eine formale Semantik Sem : UML → P(Z) und
•
eine oder mehrere Relationen R ⊆ P(Z) × P(Z), die Beziehungen wie Abstraktion, Signaturwechsel, Verfeinerungen, etc. zwischen Elementen der Zielsprache darstellen.
Eine Modelltransformation genugt ¨ einer Relation R in Bezug auf ein Modell u ∈ UML, wenn fur ¨ das transformierte Modell u gilt: (Sem(u), Sem(T (u))) ∈ R Diese Bedingung l¨asst sich graphisch durch ein kommutierendes Diagramm illustrieren:
Wenn diese Beziehung fur ¨ alle Modelle u ∈ UML gilt, fur ¨ die das transformierte Modell wohlgeformt ist, das heißt u genugt ¨ den Kontextbedingungen der Transformation T und T (u) ∈ UML, dann genugt ¨ die Modelltransformation der Relation R beziehungsweise ist eine operative Umsetzung von R. Es kann viele verschiedene Modelltransformationen geben, die derselben Relation genugen. ¨ Wird die Transformation durch ein Skript s ∈ S beschrieben, so genugt ¨ ein Skript ¨ einer Relation R, wenn die Transformation Tp (s) der Relation R genugt. Abbildung 8.5. Modelltransformation und Modell-Semantik
zun¨achst noch eine deutliche Detaillierung der Kontextbedingungen und der Sonderf¨alle vorzunehmen. Diese sind zum Beispiel fur ¨ eine Umsetzung der Refactoring-Regeln in Werkzeuge notwendig. Auf der Basis einer auf diese Weise pr¨azisierten Beschreibung l¨asst sich dann uber ¨ die Durchfuhrung ¨ von Beweisen zur Korrektheit von Refactoring-Regeln nachdenken. Beispiele fur ¨ Semantiken und Relationen Ein wesentliches Element der Betrachtung der Bedeutung einer Modelltransformation in Abbildung 8.5 ist das Vorhandensein von Relationen R auf der als Semantik verwendeten Zielsprache Z. Je nachdem wie gut die Theorie
8.3 Theorie der Modelltransformationen
261
ausgebaut ist, die zu einer als Semantik geeigneten Zielsprache gehort, ¨ stehen unterschiedlich m¨achtige Relationen zur Verfugung. ¨ Ein Beispiel fur ¨ eine ausgereifte Theorie stellen die Strome ¨ in Focus [BS01b] dar, die Abstraktionsund Verfeinerungsrelationen unterschiedlichster Art, von Signatur- bis hin zur Interaktions- und Zeitverfeinerung fur ¨ verteilte Systeme anbieten. Ein weiteres Beispiel bilden die in Abschnitt 6.2, Band 1 eingefuhrten ¨ erkennenden Automaten mit einer Semantik in Form von Mengen erkannter Worter ¨ des Eingabealphabets. Wird die erkannte Menge von Wortern ¨ als Beobachtung angenommen, so sind zum Beispiel die aus der Automatentheorie [HU90, Bra84] bekannten Transformationen zur Berechnung einer deterministischen Automatenvariante oder zur Minimierung als Refactorings verstehbar. Auch die meisten Transformationsregeln fur ¨ Statecharts aus Abschnitt 6.6.2, Band 1 ver¨andern nur die Struktur eines Statecharts, wirken sich aber nicht auf das von außen beobachtbare Verhalten aus. Bei den Automaten entspricht die Ersetzung eines Eingabezeichens einer Transformation zur Umbenennung der Signatur. Eine Abstraktion auf Signaturebene ist zum Beispiel, wenn eine Gruppe von Eingabezeichen durch ein Einzelnes ersetzt wird. Abbildung 8.6 zeigt den Effekt einer Abstraktion auf einen erkennenden Automaten. Bei echten Abstraktionen geht Information des detaillierten Ausgangsmodells verloren. Es ist daher ein Merkmal von Abstraktionen, dass mehrere Ausgangsmodelle auf dasselbe Zielmodell abgebildet werden. Abstraktionen sind also normalerweise nicht surjektiv. Eine Abstraktion eignet sich daher auch nicht zur Weiterentwicklung eines Modells, sondern zur Analyse oder zur Definition von Tests, die von dem abstrakteren Modell besser abgeleitet werden konnen. ¨ Beachtenswert ist auch, dass Abstraktion auf der syntaktischen Ebene bedeutet, dass Elemente oder Detailinformation aus dem Modell entfernt werden und das Modell in diesem Sinne kleiner“ wird. Demgegenuber ¨ wird ” wegen der Form der Semantikdefinition Sem, die, wie in Abschnitt 3.1.1 diskutiert, auch als lose Semantik bezeichnet wird, die Menge von moglichen ¨ Implementierungen großer. ¨ Die Abstraktionsrelation stellt sich damit als Mengeninklusion dar (formal: R = {(x, y)|x ⊆ y}). Die umgekehrte Relation wird Verfeinerung genannt. Sie erlaubt es, aus einem abstrakten Modell durch das Hinzufugen ¨ von Information zu einem konkreteren, detaillierteren und damit auch vollst¨andigeren Modell zu gelangen. Wie nachfolgend diskutiert, dienen die Refactoring-Techniken der Semantikerhaltung. Sie bilden damit weder Abstraktionen noch Verfeinerungen der Ausgangsmodelle, sondern stellen lediglich eine nach außen nicht beobachtbare Umstrukturierung dar. Die zugehorige ¨ Relation wird daher ei¨ ne Aquivalenz sein, die auf Basis des sp¨ater eingefuhrten ¨ Beobachtungsbegriffs als Identit¨at wirkt. Die hier gezeigten Beispiele stammen aus der relativ einfachen und gut verstandenen Theorie endlicher Automaten, weil sich dadurch das Prin-
262
8 Refactoring als Modelltransformation
Sei L(Ai ) die Menge der von dem Automat Ai erkannten Worter. ¨ Automat A1 beschreibt ein Zahlenformat, das keine 0 als letzte Nachkommastelle zul¨asst. In einer Abstraktion werden die einzelnen Ziffern durch ein Zeichen # ersetzt. Auf dem Alphabet lautet die Abstraktionsrelation φ = {(n, ’#’) | n ∈ [’0’, . . . , ’9’]} Diese wird punktweise auf Worter ¨ ausgedehnt. Beispiel: (’17.33’, ’##.##’) ∈ φ. Eine Ersetzung der Ziffern im Automat A1 gem¨aß φ fuhrt ¨ zu Automat A2 . Es gilt fur ¨ jedes Wort w1 , das von A1 erkannt wird, dass die Abstraktion w2 entsprechend von A2 erkannt wird: ∀w1 , w2 : w1 ∈ L(A1 ) ∧ (w1 , w2 ) ∈ φ ⇒ w2 ∈ L(A2 ) Die Umkehrung gilt jedoch nicht, wie das Beispiel ’17.30’ zeigt. Wie bei Abstraktionen h¨aufig der Fall, geht dabei Information verloren. Zum Beispiel konnen ¨ die nur mit der Ziffer 0 markierten Transitionen nicht mehr detailliert genug dargestellt werden. Es entsteht eine gegenuber ¨ dem ursprunglichen ¨ Automat unterspezifizierte Darstellung, die immer noch wesentliche Informationen enth¨alt, aber nicht mehr so detailliert ist. Abbildung 8.6. Abstraktion am Beispiel eines erkennenden Automaten
zip am besten erkl¨aren l¨asst. Bei den syntaktisch wesentlich reichhaltigeren Statecharts gilt dasselbe Prinzip, aber es sind die zu beachtenden Rahmenbedingungen komplexer. Wesentlich ist bei den Statecharts auch, dass diese eine Ausgabe besitzen, die zus¨atzlich zur Eingabe beobachtet werden kann. Die sich daraus ergebenden semantikerhaltenden Transformationen fur ¨ Statecharts wurden in Abschnitt 6.6.2, Band 1 bereits diskutiert. Kategorien von Semantikrelationen In den gezeigten Beispielen werden drei semantische Relationen vorgestellt. Eine Kategorisierung zeigt, dass aufgrund der gew¨ahlten Semantik auch nur wenige Relationen notwendig sind. Als Kategorien sind identifizierbar: 1. Die Abstraktion, die von Details abstrahiert. Das sich ergebende Modell hat als Semantik eine Obermenge des Ausgangsmodells.
8.3 Theorie der Modelltransformationen
263
2. Die Verfeinerung, bei der Details hinzugefugt ¨ werden. Verfeinerung ist damit die Umkehrung der Abstraktion. 3. Das Refactoring als bezuglich ¨ einer vorgegebenen Beobachtung semantikerhaltende Transformation. Refactoring kann gleichermaßen als Spezialfall von Verfeinerung und Abstraktion (bezuglich ¨ der Beobachtung) gesehen werden. 4. Signaturwechsel durch Umbenennung von syntaktischen Elementen, die in Schnittstellen sichtbar sind. Die Kategorisierung der semantischen Relationen l¨asst sich direkt in eine Kategorisierung der Transformationsregeln ubertragen. ¨ Es gibt also Transformationsregeln fur ¨ Abstraktion, Verfeinerung, Refactoring und Signaturwechsel, wobei diese Kategorien nicht disjunkt sind. Beispielsweise ist ein Signaturwechsel als Refactoring bezuglich ¨ einer Beobachtung auffassbar, die die Signatur nicht beachtet. Die Ersetzung der Ziffern durch das Zeichen ’#’ in dem Beispiel in Abbildung 8.6 ist eine Abstraktion. Andere Formen von Abstraktionen bestehen darin, Klassen aus einem Klassendiagramm, Objekte aus einem Objektdiagramm, etc. zu entfernen. Das entstehende Modell ist jeweils weniger aussagekr¨aftig. Umgekehrt konnen ¨ in einer Verfeinerung neue Klassen hinzugefugt ¨ werden. Ob es sich allerdings um eine echte Verfeinerung handelt, h¨angt vom nachfolgend diskutierten Beobachtungsbegriff ab. Transformationen auf Mengen von Artefakten In Abschnitt 2.5.4, Band 1 wurde festgelegt, dass in diesem Buch unter dem Begriff Modell“ sowohl ein einzelnes Artefakt, also zum Beispiel ein Klas” sendiagramm oder ein Statechart, als auch eine Sammlung dieser Artefakte zur Beschreibung mehrerer Aspekte verstanden wird. Deshalb werden Modelltransformationen in Abbildung 8.7 auf Mengen von bearbeiteten Artefakten ausgeweitet (eine mit Skripten parametrisierte Form kann analog festgelegt werden). Damit kann zum Beispiel die Umbenennung einer Methode an allen auftretenden Stellen konsistent erfolgen. Die Motivation fur ¨ die in Abbildung 8.7 verwendete Definition der Semantik ist, dass ein Modell u1 als zu erfullende ¨ Bedingung an ein System gesehen werden kann. Es gibt also eine Menge Sem(u1 ) von Systemen, die die gestellte Bedingung erfullen. ¨ Wird ein zweites Modell u2 als zus¨atzliche Bedingung aufgestellt, so ergibt sich die Menge von nun als Realisierung infrage kommender Systeme als Sem({u1, u2 }) = Sem(u1 ) ∩ Sem(u2 ), also genau die Systeme, die beide Bedingungen erfullen. ¨ Offene und geschlossene Systeme Die heute ubliche ¨ Form der Anwendung von Modelltransformationen sowohl bei CASE-Werkzeugen fur ¨ SDL oder UML-Modelle, als auch bei Ent-
264
8 Refactoring als Modelltransformation
Die Ausdehnung der Modelltransformationen auf Mengen wird aufbauend auf Abbildung 8.5 festgelegt. Eine Modelltransformation wird erweitert zu einer Abbildung T : P(UML) → P(UML) auf Mengen, indem jedes Artefakt der Menge M ⊆ UML einzeln transformiert wird: T (M ) = {T (u)|u ∈ M } Die Semantikdefinition wird erweitert zu Sem : P(UML) → P(Z), indem fur ¨ Mengen von Modellen M ⊆ UML, wie bei losen Semantiken ublich, ¨ festgelegt wird: \ Sem(M ) = Sem(u) u∈M
Abbildung 8.7. Modelltransformation auf Mengen von Artefakten
wicklungsumgebungen mit Refactoring-Unterstutzung ¨ ist die Annahme eines weitgehend geschlossenen Systems. Ein Modell heißt offen, wenn es ein offenes System modelliert, also explizite Schnittstellen an die Systemumgebung hat, deren Signatur nicht ge¨andert werden kann und uber ¨ deren Verhalten die Systemumgebung Annahmen macht. Dazu gehoren ¨ Nachbarsysteme, die zum Beispiel vom Kollegen bearbeitet werden, vorgegebene Frameworks, das Betriebssystem oder Middleware-Komponenten. Ein Modell heißt geschlossen, wenn solche Schnittstellen nicht existieren. Ein geschlossenes Modell entsteht typischerweise dann, wenn die Umgebung des Systems explizit in dem Modell enthalten ist. Geschlossene Modelle sind leichter zu modifizieren und anzupassen, als offene Modelle, weil in einem offenen Modell die Zusicherungen des Systems gegenuber ¨ der Umgebung beibehalten werden mussen. ¨ Schnittstellen und Interaktionsmuster durfen ¨ gegenuber ¨ der Umgebung nicht ver¨andert werden. Obwohl in heutigen Systemen zum Beispiel mit Frameworks nahezu immer Schnittstellen zur Umgebung vorhanden sind, wird allgemein versucht, moglichst ¨ so zu arbeiten, als wurde ¨ ein geschlossenes System vorliegen. In dem in [SD00] propagierten und auch in Kapitel 7 verwendeten Ansatz zur Separation des Applikationskerns von externen Komponenten durch Adapter entsteht eine geschlossene Form als Nebeneffekt, indem Schnittstellen zur Umgebung gekapselt werden. Die Adapter werden dann als Teil des Systems behandelt und es entsteht der Effekt eines geschlossenen und damit kontrollier- und manipulierbaren Systems. Der aus dem methodischen Ansatz des Buchs stammende und in Abschnitt 8.2 diskutierte gemeinsame Modellbesitz fur ¨ Entwickler hat einen analogen Effekt. So werden Grenzen zwischen den von einem Entwickler selbst erstellten Artefakten und denen der Teamkollegen aufgehoben. Die Umgebung des von ihm prim¨ar bearbeiteten Bereichs ist damit fur ¨ einen Entwickler zug¨anglich und ver¨anderbar, wie in einem geschlossenen System.
8.3 Theorie der Modelltransformationen
265
Systemmodell als semantischer Domain In den Abbildung 8.4 und 8.5 wurden die abstrakten Mengen UML und Z fur ¨ die Syntax und den semantischen Domain eingefuhrt. ¨ W¨ahrend die syntaktische Form der UML/P in Band 1 ausfuhrlich ¨ diskutiert wurde und im Anhang C, Band 1 durch EBNF-Produktionen und Syntaxklassendiagramme dargestellt ist, wurde der Domain fur ¨ die Formalisierung der Semantik noch nicht weiter charakterisiert. Fur ¨ semantische Domains gibt es eine Reihe von Vorschl¨agen, die zur Formalisierung von Teilen der UML eingesetzt wurden. [HR00] enth¨alt da¨ zu eine Ubersicht. Zum Einsatz kommen vor allem verschiedene Logiken und mathematische Formalismen, die meistens um spezifische Konstrukte erweitert wurden. Dazu gehort ¨ zum Beispiel [BHH+ 97], in dem eine Formalisierung großerer ¨ Teile der UML auf Basis eines verteilten, asynchron kommunizierenden Formalismus diskutiert wurde, oder [FELR98b], in dem eine Transformation in die formale Sprache Z [Spi88] vorgenommen wurde. Die formale Definition eines semantischen Domains und eine darauf basierende Semantikabbildung ist nicht Teil dieses auf methodische Anwendung der UML ausgelegten Buchs. Dennoch wird in diesem Abschnitt das grunds¨atzliche Aussehen eines solchen semantischen Domains diskutiert, um damit den Beobachtungsbegriff im n¨achsten Abschnitt pr¨azisieren zu konnen. ¨ Um einer Menge von unterschiedlichen UML-Diagrammen eine pr¨azise Semantik geben zu konnen, ¨ mussen ¨ in der Semantik alle wesentlichen Aspekte eines Systems dargestellt werden. Es ist nicht ausreichend, einzelne Aspekte, wie das Ein-/Ausgabeverhalten oder die Inhalte einzelner Objekte im semantischen Domain zu modellieren. Stattdessen ist es sinnvoll, eine geschlossene Form eines Systems zu w¨ahlen und dessen Struktur und zeitliches Verhalten darzustellen. Wesentlichen Einfluss auf den semantischen Domain haben auch Fragestellungen, ob und in welcher Form Verteilung, Nebenl¨aufigkeit und asynchrone Kommunikation repr¨asentiert werden sollen. In der in diesem Buch diskutierten Vorgehensweise werden solche Aspekte nur als Randerscheinungen behandelt. Auch die in Kapitel 7 definierten Tests fur ¨ verteilte Systeme und parallele Threads ersetzen echte Nebenl¨aufigkeit durch simuliertes Scheduling und damit einem sequentiellen, deterministischen Ablauf. Bereits [Huß97, Rum96] beschreiben die grundlegende Struktur solcher semantischen Domains, die auch als so genanntes Systemmodell, also einer abstrakten Darstellung der Struktur und des Verhaltens von Systemen, bezeichnet wird. Grunds¨atzlich ist eine solche mathematisch formale Darstellung inhaltlich sehr a¨ hnlich zur Definition einer virtuellen Maschine, wie sie in Abschnitt 3.2 fur ¨ die UML diskutiert wurde. Fur ¨ die Definition einer virtuellen Maschine wird jedoch eine konstruktiv, operationelle Beschreibung angegeben, w¨ahrend ein Systemmodell als denotationelle Semantik im Allgemeinen kompakter definiert werden kann.
266
8 Refactoring als Modelltransformation
Ein fur ¨ die UML/P ad¨aquates Systemmodell beschreibt ein System uber ¨ eine Menge von Systemabl¨aufen, die ihrerseits Interaktionen, Snapshots und die darin enthaltenen Objekte charakterisieren. Abbildung 8.8 charakterisiert wesentliche Elemente, wobei vereinfachend auf unendliche Abl¨aufe verzichtet wird und die eigentlich den Threads zugeordneten Stacks auf die Objekte verteilt werden. Das Systemmodell beschreibt eine Menge von Abl¨aufen eines Systems. Dazu werden folgende Definitionen eingefuhrt, ¨ wobei grundlegende Elemente wie die Menge der Attributnamen VarName oder der Werte Value nicht weiter detailliert werden. •
Ein Objekt o = (ident, attr, stack) besitzt einen Identifikator ident, zu jedem Zeitpunkt eine Belegung attr : VarName → Value und dem ihm zugeordneten Anteil aus dem Stack. Obj sei die Menge der Objekte.
•
Ein Snapshot sn ⊆ Obj ist eine Sammlung von Objekten, die zu einem Zeitpunkt existieren. Die Links zwischen Objekten werden durch die im Snapshot eindeutigen Identifikatoren dargestellt. SN sei die Menge der Snapshots.
•
Die Menge der Aktionen Act beschreibt Methodenaufrufe, Returns, Attributzuweisungen, etc.
•
Aus einem Snapshot l¨asst sich erkennen, welches Objekt gerade aktiv ist und damit welche Aktion als n¨achstes durchgefuhrt ¨ wird. Der Programmz¨ahler ist also in den stack der Objekte integriert. Damit ist die n¨achste stattfindende Aktion eines Snapshots festgelegt: act : SN → Act.
•
Ein Ablauf run ∈ SN ∗ ist eine Reihe von zeitlich aufeinander folgenden Snapshots.
•
Das Systemmodell SM ⊆ SN ∗ besteht aus einer Menge von Abl¨aufen.
Eine Reihe von zus¨atzlichen Bedingungen sind notwendig, um SM auf die Abl¨aufe einzuschr¨anken, die tats¨achlich auftreten konnen. ¨ Beispielsweise ist der Kontrollfluss zu wahren und nur Attribute im Objekt zu belegen, die dort auch existieren. In Abbildung 8.5 wurde als abstrakte Zielsprache fur ¨ eine Semantikdefinition Z eingefuhrt. ¨ Konkret kann Z = SM gesetzt werden. Jedes UML-Artefakt u ∈ UML erh¨alt dann seine Semantik in Form einer Teilmenge von Systemabl¨aufen aus SM , die die beschriebenen Eigenschaften erfullen. ¨ Abbildung 8.8. Prinzip eines Systemmodells als semantischer Domain
8.3.3 Beobachtungsbegriff Nach der Charakterisierung der Transformation einer Refactoring-Regel ist die zu erhaltende Beobachtung des Verhaltens wesentlich. In den beiden am meisten beachteten Werken zum Thema Refactoring [Fow99, Opd92] wird aber der Beobachtungsbegriff nicht pr¨azisiert. In [Opd92, S. 28] wird der Beobachtungsbegriff auf die Relation zwischen Ein- und Ausgabe reduziert,
8.3 Theorie der Modelltransformationen
267
ohne Interaktionen zu berucksichtigen. ¨ In [Fow99] wird an die intuitive Vorstellung der Beobachtung durch den Anwender appelliert. Als Beobachtung wird in XP-Projekten vor allem das an der Anwenderschnittstelle beobachtbare Verhalten verstanden. Die Schnittstellen zu anderen Systemen, Datenbanken, Frameworks, etc. gehoren ¨ aber unter Umst¨anden ebenfalls zum ex” tern beobachtbaren Verhalten“. Obwohl der Beobachtungsbegriff im XP-Ansatz nicht pr¨azisiert wurde, gibt es eine Moglichkeit, ¨ Beobachtungen zu definieren, um damit das Refactoring eines Systems zu prufen. ¨ Der XP-Ansatz nutzt dazu Testfalldefinitionen, die in Java formuliert werden und sich weitgehend auf die Prufung ¨ des Ergebnisses in Form einer Nachbedingung beschr¨anken. Die in den Kapiteln 5 und 7 diskutierten Tests und insbesondere ihre deskriptiven Bestandteile, wie Orakelfunktionen, Invarianten, Interaktionsmuster und Nachbedingungen, sind ein ideales Mittel zur Darstellung von Beobachtungen. Es wird daher festgelegt, dass die Beobachtungen bei Refactorings durch in UML/P formulierte Testf¨alle modelliert werden. Anhand der aufgez¨ahlten Liste von Darstellungsformen ist ersichtlich, dass sich eine mit der UML/P formulierte Beobachtung nicht auf Schnittstellen beschr¨anken muss, sondern auch interne Interaktionsmuster, Zwischenzust¨ande und den Endzustand des Testlings beobachten“ kann. Abbildung ” 8.9 illustriert dies auf Basis eines Systemablaufs bestehend aus einer einer Sequenz von Snapshots.
Abbildung 8.9. Test als Beobachtung eines Systemablaufs
Die Moglichkeit, ¨ im Test jegliche Kapselung eines Testlings zu durchbrechen, birgt wesentliche Vorteile bei der Modellierung von Tests, da so nicht erst aufgrund des Ausgabeverhaltens Ruckschl ¨ usse ¨ auf den Zustand des Testlings geschlossen werden mussen, ¨ sondern der Zustand direkt zug¨anglich ist. Auch die Modellierung von Interaktionsmustern innerhalb einer Teil-
268
8 Refactoring als Modelltransformation
komponente, die aus mehreren Objekten besteht, bricht die Kapselung der Komponente auf. Der Nachteil solcher Tests besteht darin, dass diese auch ¨ kleine Anderungen des Testlings registrieren, indem sie entweder nicht mehr kompilierbar sind (zum Beispiel bei Signatur¨anderung) oder ein Scheitern ¨ des Testlaufs anzeigen (zum Beispiel bei Anderung des Interaktionsmusters). Dies bedeutet zus¨atzlichen Aufwand beim Refactoring, da diese Tests ebenfalls angepasst werden mussen. ¨ Es ist daher bei der Definition von Tests sorgf¨altig abzuw¨agen, ob die Interna des Testlings oder eine abstraktere Programmierschnittstelle genutzt werden. In der Praxis sind zwei grobe Klassen von Tests identifizierbar. Die Unit-Tests fur ¨ einzelne Methoden und Klassen werden normalerweise auf Interna zugreifen und mussen ¨ gemeinsam mit dem Code modifiziert werden. Vom Anwender formulierte und mithilfe von Entwicklern realisierte, automatisierte Tests sind hingegen Teil der Beobachtung durch einen Anwender. Derartige Tests sollten durch Refactorings nicht beeintr¨achtigt werden. Dies bedeutet aber, dass ein solcher Test moglichst ¨ gegen eine abstrakte Schnittstelle testet, die auch dann beibehalten wird, wenn Interna des Systems modifiziert werden. Im Detail bedeutet dies, dass es fur ¨ Akzeptanztests besser ist, nach Moglichkeit ¨ • • • •
Query-Methoden statt direkte Attributzugriffe zu verwenden, OCL-Eigenschaften zu verwenden, die erlauben gewisse Freiheiten zu formulieren, statt Attributwerte pr¨azise vorzugeben, im Sollergebnis unwichtige Objekte und Attribute zu ignorieren und sich auf die wesentlichen Ergebnisse zu konzentrieren sowie nur wesentliche Interaktionen zu beobachten.
Auf diese Weise enth¨alt die durch den Test definierte Beobachtung eine Abstraktion und erlaubt damit einer Reihe von unterschiedlichen Abl¨aufen, den Test zu erfullen. ¨ Das System beziehungsweise sein Modell kann damit in gewissen Grenzen modifiziert werden, ohne dass sich dadurch die abstrakte Beobachtung a¨ ndert. Dies kann wie in Abbildung 8.10 illustriert werden.
Abbildung 8.10. Refactoring l¨asst Systemabl¨aufe unter Beobachtung invariant
8.3 Theorie der Modelltransformationen
269
Ein Nebeneffekt der Nutzung von Abstraktion bei der Beobachtung eines Testablaufs ist allerdings, dass ein Test nicht alle Aspekte des Testlings pruft. ¨ So kann ein Codestuck ¨ zwar ausgefuhrt ¨ worden sein, sein Effekt aber durch den Test vernachl¨assigt werden. Als Konsequenz daraus ist die Aussa¨ gekraft von Uberdeckungsmetriken fur ¨ Tests, die auf Prufung ¨ durchlaufenen Codes basieren, moglicherweise ¨ beschr¨ankt. Hilfreich sind hier Mutations¨ tests [Voa95, KCM00, Moo01], die prufen, ¨ ob eine Anderung des Codes zu einer Erkennung ver¨anderten Verhaltens durch die vorhandene Testsammlung fuhrt. ¨ Damit wird gemessen, inwieweit die grunds¨atzlich abstrahierenden Beobachtungen einer Testsammlung in ihrer Gesamtheit das beobachtbare Verhalten erfassen. Nach diesen aus der Praxis heraus motivierten und graphisch illustrier¨ ten Uberlegungen zur Darstellung von Beobachtungen wird nun die im vorherigen Abschnitt begonnene Pr¨azisierung von Modelltransformationen in Abbildung 8.11 um einen Beobachtungsbegriff erweitert. In dieser Abbildung wird außerdem ein Kontext fur ¨ Transformationen eingefuhrt. ¨ Der Kontext besteht aus Modellen, auf die das transformierte Modell aufbaut, die selbst aber nicht ver¨andert werden. Ein Beispiel ist ein Klassendiagramm, das einem zu transformierenden Objektdiagramm zugrunde liegt. Der dritte Punkt der Definition in Tabelle 8.11 druckt ¨ aus, dass eine Beobachtung in Form eines Tests den Testling zu bestimmten Systemabl¨aufen zwingt und nur dort pruft. ¨ Sem(u) ∩ Sem(b) stellt entsprechend diese von beiden Modellen durchfuhrbaren ¨ Abl¨aufe dar. Entsprechend ist die Beobachtungsinvarianz nur bezuglich ¨ der durch Sem(b) beschriebenen Abl¨aufe notwendig. Dies gibt der Transformation die Freiheit, unbeobachtete, also typischerweise interne Modifikationen vorzunehmen. Alternativ kann die Semantik von Beobachtungen auch durch die Einfuhrung ¨ eines semantischen Domains B und einer Abstraktionsfunktion β : Z → B beschrieben werden. Eine Beobachtung wird dann durch ¨ b ∈ B repr¨asentiert und charakterisiert eine Aquivalenzklasse von Abl¨aufen {z ∈ Z|β(z) = b}. Ver¨andert also ein Refactoring einen Ablauf z nach z , so muss der beobachtbare Anteil jedoch gleich bleiben: β(z ) = β(z). Ein Nachteil dieses Ansatzes sind die Notwendigkeit zur Definition einer ad¨aquaten Menge von Beobachtungen B. Wie der letzte Punkt in Abbildung 8.11 zeigt, existiert formal kein Unterschied zwischen dem Kontext und den Beobachtungen. Ein solcher Kontext kann zum Beispiel aus unver¨anderbaren Schnittstellen zur GUI, Datenbanken, etc. bestehen. Er kann aber auch die von der aktuellen Bearbeitung nicht betroffenen Modelle beinhalten. Ein Beispiel fur ¨ eine Beobachtung ist etwa eine in OCL formulierte Methodenspezifikation. Ihre Semantik ist ein Pr¨adikat uber ¨ den Beschreibungen von Abl¨aufen SM aus Abbildung 8.8. Ein Ablauf erfullt ¨ die Methodenspezi-
270
8 Refactoring als Modelltransformation
Eine Beobachtung betrachtet nur einzelne Aspekte eines Systemablaufs. Eine Beobachtung ist damit eine Abstraktion: •
Eine Beobachtung besteht aus einem oder mehreren Artefakten der UML/P.
•
Die Semantik einer Beobachtung ist die Teilmenge der durch die Beobachtung erlaubten Abl¨aufe des Systems Z und damit ebenfalls durch Sem : UML → P(Z) festgelegt.
•
Sei u ∈ UML ein Modell, das zum Beispiel zur konstruktiven Codegenerierung verwendet und durch b ∈ UML beobachtet wird. Die Beobachtung von u bezieht sich dann nur auf die gemeinsamen Abl¨aufe, dargestellt durch Sem(u)∩Sem(b).
•
Eine Modelltransformation T (u) auf dem Modell u ∈ UML ist beobachtungsinvariant bezuglich ¨ einer Beobachtung b ∈ UML, wenn der beobachtete Ausschnitt identisch ist: Sem(u) ∩ Sem(b) = Sem(T (u)) ∩ Sem(b)
•
Wird die Modelltransformation auf mehrere Artefakte A ⊆ UML angewandt und im Kontext anderer Modelle K ⊆ UML betrachtet, so gilt die Beobachtungsinvarianz bezuglich ¨ einer Menge von Beobachtungen B ∈ UML, wenn gilt: Sem(A) ∩ Sem(K) ∩ Sem(B) = Sem(T (A)) ∩ Sem(K) ∩ Sem(B)
In derselben Weise kann die in Abschnitt 8.3.1 diskutierte Verfeinerung in einen Kontext und relativ zu einer Beobachtung pr¨azisiert werden: Sem(A) ∩ Sem(K) ∩ Sem(B) ⊇ Sem(T (A)) ∩ Sem(K) ∩ Sem(B) Bei einer Abstraktion wird entsprechend die umgekehrte Relation ⊆ vorausgesetzt. Abbildung 8.11. Prinzip eines Beobachtungsbegriffs
fikation genau dann, wenn die Nachbedingung zum Ende jedes Methodenaufrufs gilt, bei dessen Beginn die Vorbedingung erfullt ¨ war.5 Ein vollst¨andiger Test, bestehend aus Testdatensatz, Testtreiber, etc., stellt ebenfalls ein Pr¨adikat uber ¨ einen Systemablauf dar. Ein Systemablauf erfullt ¨ einen Test genau dann, wenn der Test erfolgreich durchgefuhrt ¨ wurde. Das heißt, es gibt in dem Ablauf einen Snapshot, der dem initialen Testdatensatz entspricht, der Testling wird ausgefuhrt ¨ und die geforderten Bedingungen und Interaktionen sind w¨ahrend beziehungsweise nach dem Test erfullt. ¨ 8.3.4 Transformationsregeln In der Theorie ist eine Transformationsregel als Abbildung UML → UML erkl¨art, die bei Bedarf auf Mengen von UML-Artefakten zur simultanen Anwendung erweitert werden kann. In der Praxis konnen ¨ jedoch unterschied5
Fur ¨ die genaue Beschreibung der Semantik einer Methodenspezifikation siehe Abschnitt 4.4.3, Band 1.
8.3 Theorie der Modelltransformationen
271
lichste Auspr¨agungen dieser Abbildung existieren. Die Anzahl der mogli¨ chen Regeln h¨angt von der syntaktischen Reichhaltigkeit der zugrunde liegenden Sprache ab. Die UML/P besitzt zwar deutlich weniger Sprachkonzepte als der UML-Standard [OMG03], ist aber immer noch eine eher große Modellierungssprache. Ausgehend von Erfahrungen mit anderen Transformationskalkulen ¨ muss damit gerechnet werden, dass die Anzahl moglicher ¨ und sinnvoller Regeln mehr als linear mit der Große ¨ der Sprache w¨achst. Ein Grund dafur ¨ ist, dass viele Regeln das Zusammenspiel mehrerer Sprachkonzepte behandeln. Es ist daher praktisch unmoglich, ¨ fur ¨ die UML/P einen vollst¨andigen Regelkalkul ¨ zu identifizieren. Dies ist aber auch fur ¨ den praktischen Einsatz nicht notwendig. Wesentlich ist vielmehr, eine anwendbare Sammlung kompakter und einfacher Regeln zur Verfugung ¨ zu haben. Die Anzahl und konkrete Form von Transformationsregeln h¨angt sehr oft von der Auspr¨agung der zugrunde liegenden Sprache ab. Aber es gibt auch allgemeine Prinzipien fur ¨ Transformationsregeln, die weitgehend auf alle Sprachen angewandt werden konnen. ¨ Dazu gehoren ¨ zum Beispiel die Expansion von Methoden oder die Migration von Attributen, die auch bei prozeduralen Sprachen wie C auf Funktionen und struct-Eintr¨age angewandt werden konnen. ¨ Die Regelanwendungen unterscheiden sich jedoch in ihren technischen Details. Bei der Methodenexpansion sind in Java Sonderf¨alle fur ¨ abstrakte Methoden, dynamische Bindung, statische Methoden oder Konstruktoren zu berucksichtigen ¨ oder die Exceptions sind speziell zu behandeln. Die anwendbaren Regeln sollten moglichst ¨ einfach und generalisiert sein, um so maximale Anwendbarkeit sicherzustellen. Die M¨achtigkeit eines Regelkalkuls ¨ besteht zum Großteil aus der Komponierbarkeit der Regeln, indem diese etwa hintereinander angewandt werden. Mithilfe von zielgerichteten Taktiken konnen ¨ aus einfachen Regeln komplexe Transformationen komponiert werden. Damit l¨asst sich zum Beispiel die Einfuhrung ¨ eines Entwurfsmusters aus [GHJV94] als Serie einfacher Transformationsregeln erkl¨aren [TB01]. Wesentlich ist dabei, dass fur ¨ die grundlegenden Transformationsregeln Werkzeugunterstutzung ¨ zur Verfugung ¨ steht, die auch die Kontextbedingungen pruft ¨ und die Wohlgeformtheit des Ergebnisses sichert. Zielgerichtete Taktiken konnen ¨ als Skripte analog zu der in Abschnitt 3.2.3 bereits fur ¨ die Codegenerierung diskutierten Form realisiert werden. Eine Reihe von Transformationsregeln sind außerdem nur in Anwesenheit bestimmter Stereotypen anzuwenden. In Band 1 ist beispielsweise demonstriert, wie Transitionen mit uberlappenden ¨ Schaltbereichen unterschiedlich priorisiert sein konnen ¨ und dementsprechend verschiedene Regeln anwendbar sind. Weil aber die in UML/P verfugbaren ¨ Stereotypen mit dem in Abschnitt 3.5.3, Band 1 beschriebenen Verfahren frei erweitert werden konnen, ¨ ist es notwendig, gemeinsam mit der Einfuhrung ¨ eines neuen Stereotyps entsprechende Transformationsregeln und Skripte zu definieren, die einen spezialisierten Umgang mit Modellen mit diesem Stereotyp erlauben.
272
8 Refactoring als Modelltransformation
8.3.5 Korrektheit von Transformationsregeln Wie bereits in den vorherigen Abschnitten anhand von Beispielen illustriert, besitzt eine Transformationsregel meistens Kontextbedingungen, die erfullt ¨ sein mussen, ¨ um die Korrektheit einer Transformation zu sichern. Diese Kontextbedingungen konnen ¨ verschiedener Bauart sein, weshalb eine Klassifizierung von Transformationsregeln anhand der Art der Kontextbedingungen sinnvoll ist. Die Kontextbedingungen reichen von einfachen und meistens syntaktisch prufbaren ¨ Einschr¨ankungen bis hin zu komplexen, nicht mehr automatisiert entscheidbaren Invarianten. 1. Im einfachsten Fall besitzt eine Transformation keine Kontextbedingungen. 2. Einfache syntaktische Bedingungen wie zum Beispiel, dass ein zu ersetzender Ausdruck eine Variable nicht verwendet, konnen ¨ durch entsprechende Prufungen ¨ automatisiert werden. Diese Form der Kontextbedingungen tritt h¨aufig auf und kann durch eine gute Werkzeugunterstutzung ¨ anhand des Syntaxbaums effizient gepruft ¨ werden. 3. Komplexere Kontextbedingungen wie etwa die Typkorrektheit lassen sich ebenfalls anhand der Syntax entscheiden, erfordern aber erheblich mehr Aufwand. Weitere Beispiele fur ¨ solche Bedingungen sind Kontrollflussanalysen, zum Beispiel um die Unerreichbarkeit von Zust¨anden im Statechart oder von Anweisungen im Methodenrumpf zu identifizieren, oder Datenflussanalysen, um sicherzustellen, dass Variablen vor ihrer Nutzung belegt werden, wie sie beispielsweise in modernen Compilern integriert sind. 4. Nicht automatisiert uberpr ¨ ufbare ¨ Bedingungen sind meist komplexere Beziehungen zwischen Elementen des Systems. Beispielsweise konnen ¨ mehrere Attribute einer Klasse in einem inneren Zusammenhang stehen, der durch eine OCL-Bedingung formulierbar ist. Auf Basis dieses Zusammenhangs kann ein Attribut gegebenenfalls durch ein anderes ersetzt werden. Die Korrektheit von OCL-Bedingungen ist aber im Normalfall nicht automatisch prufbar, ¨ sondern erfordert punktuelle Tests oder interaktive Verifikation. Die Kategorie der syntaktisch prufbaren ¨ Kontextbedingungen wird im Compilerbau typischerweise in die Unterkategorien syntaktische und semantische Analyse getrennt. Beide Formen werden aber auf Basis der Syntax vorgenommen und werden automatisiert durchgefuhrt. ¨ Fur ¨ manche Fragestellungen existiert nur ein semi-entscheidbares Verfahren, bei dem unter Umst¨anden die Gultigkeit ¨ der Kontextbedingung nicht entschieden werden kann. In diesem Fall wird die Kontextbedingung bereits zuruckgewiesen, ¨ wenn sie nicht positiv entschieden werden konnte. Dabei spielt fast immer die bereits bei der Entwicklung von Testf¨allen in Kapitel
8.3 Theorie der Modelltransformationen
273
6 diskutierte Problematik der Unentscheidbarkeit der Erfullbarkeit ¨ boolescher Aussagen oder eine exponentielle Komplexit¨at bei der Entscheidungsfindung eine Rolle. Mehrere oft auftretende Rahmenbedingungen sind bereits aus anderen Ans¨atzen zur transformationellen Softwareentwicklung bekannt: Terminierung. Eine Kontextbedingung kann fordern, dass die Berechnung eines Ausdrucks immer terminiert. Auch eine Exception ist eine Terminierung. Wie bereits in Kapitel 4, Band 1 diskutiert, ist die Terminierung durch Tests insbesondere mit gegebenen Schleifen-Invarianten und Terminierungsbedingungen relativ einfach prufbar, ¨ obwohl sie im Prinzip unentscheidbar ist. Fur ¨ praktisch relevante Transformationen kann aber davon ausgegangen werden, dass jeder Ausdruck terminiert oder die zur Verfugung ¨ stehenden Tests eine Nichtterminierung entdecken. Definiertheit. Eine Berechnung ist definiert, wenn sie immer terminiert und dabei ein normales Ergebnis, also keine Exception erzeugt. Determiniertheit. Eine Berechnung ist determiniert, wenn sie immer terminiert und dabei ein eindeutiges Ergebnis produziert. Dies kann durchaus eine Exception sein. Wesentlich ist, dass bei der Berechnung kein Zufallselement auftritt oder sich zumindest nicht auf das Ergebnis auswirkt. Diese Bedingung kann zum Beispiel durch Einbeziehung von Zeitabfragen verletzt werden. Wie in Abschnitt 4.3.4, Band 1 an der Konversion von Mengen in Listen mit dem OCL-Operator asList diskutiert, ist das Ergebnis dieses Operators zwar eindeutig, aber dem Entwickler nicht a priori bekannt. Der Vorteil dieser Festlegung ist, dass einerseits ein Ausdruck der Form set.asList determiniert ist, andererseits aber einem OCL-Interpreter die konkrete Umsetzung uberlassen ¨ bleibt. Seiteneffektfreiheit. Ein Seiteneffekt einer Berechnung ist eine permanente Zustands¨anderung der vorhandenen Objektstruktur, indem zum Beispiel eine lokale Variable, ein Attribut oder ein Link ver¨andert wurde. Die Erzeugung neuer Objekte ist, wie in Abschnitt 4.4.1, Band 1 besprochen, nur dann ein Seiteneffekt, wenn dieses Objekt von der ursprungli¨ chen Objektstruktur aus zug¨anglich gemacht wird. ¨ Kontextbedingungen, die semantische Aquivalenzen beinhalten, sind praktisch unentscheidbar. Zum Beispiel ist bei der Ersetzung eines Ausdrucks durch einen anderen, wie in der entsprechenden Beispielregel in Abschnitt 8.1, deren Gleichheit nicht automatisiert prufbar. ¨ Die Darstellung einer solchen Eigenschaft erfolgt beispielsweise durch OCL-Bedingungen. Diese Bedingungen konnen ¨ daher nur fur ¨ Tests oder fur ¨ die Verifikation eingesetzt werden. W¨ahrend Tests keine Gewissheit uber ¨ die Korrektheit einer OCL-Bedingung geben, sind sie doch effizienter zu bewerkstelligen als die tats¨achliche Verifikation. Fur ¨ die Verifikation einer solchen Invariante ist es meistens notwendig, den Code mit weiteren Invarianten zu versehen und a¨ hnlich der HoareLogik alle Einzelschritte zu verifizieren (eine Java-Variante ist zum Beispiel
274
8 Refactoring als Modelltransformation
in [vO01] zu finden). Fur ¨ Systeme hochster ¨ Qualit¨at oder besonders kritische Bereiche, kann dies insbesondere in Kombination mit Tests sinnvoll sein. Durch Tests werden zun¨achst vorhandene Fehler erkannt und eliminiert. Mit Verifikationstechniken wird dann die vollst¨andige Korrektheit bewiesen und jedes Restrisiko einer fehlerhaften Invariante ausgeschaltet. Durch die Vorschaltung der Tests kann Verifikationsaufwand fur ¨ viele der fehlerhaften Aussagen eingespart und so effizienter vorgegangen werden. Fur ¨ viele Systeme wird aber die Verwendung von Tests ausreichend sein. Wie in Abschnitt 9.2 anhand einer Vorgehensweise fur ¨ Datenstrukturwechsel gezeigt, konnen ¨ Tests nicht nur als Indikator fur ¨ die Korrektheit eines Systems, sondern auch fur ¨ die Korrektheit einer Transformation eingesetzt werden. 8.3.6 Ans¨atze der transformationellen Softwareentwicklung Einige Ans¨atze transformationeller Softwareentwicklung sowie eine interessante Diskussion uber ¨ dessen Auswirkungen sind in [Pep84, Kap. 5] enthalten. Darin werden unter anderem die Fragen nach der Semantik, der Nutzlichkeit ¨ von Top-Down-Darstellungen einer Softwareentwicklung und der Sprachunabh¨angigkeit von Transformationstechniken diskutiert. Transformationelle Softwareentwicklung Insbesondere in der theoretischen Informatik wurden bereits eine Reihe von transformationellen und auf Verfeinerungskonzepten basierende Ans¨atze vorgestellt. Dazu gehoren ¨ zum Beispiel Arbeiten von Dijkstra [Dij76], Wirth [Wir71], Bauer [BW82], Back [BvW98] und Hoare [HHJ+ 87]. CIP-L [BBB+ 85] ist zum Beispiel eine Sprache, die einen algebraischen Spezifikationsstil sowie funktionale, algorithmische und prozedurale Programmierstile in sich vereint und durch zahlreiche transformationelle Schritte von einem in den n¨achsten Sprachstil uberleiten ¨ kann. Als Methodik wurde diesem Top-DownTransformationsansatz zugrunde gelegt, dass zun¨achst in einer abstrakten Spezifikationssprache modelliert, dann in eine funktionale Sprache transformiert und letztendlich mit dem Ziel einer prozeduralen Implementierungssprache optimiert wird. Elemente dieses Ansatzes transformationeller Softwareentwicklung sind auch im heutigen, an der inkrementellen Vorgehensweise orientierten Refactoring wieder zu finden. Dazu gehort ¨ zum Beispiel das Konzept, Optimierungen am Code erst moglichst ¨ sp¨at durchzufuhren, ¨ wenn die Korrektheit gekl¨art und die Stabilit¨at der Funktionalit¨at gesichert sind. In [BBB+ 85] wurde wie auch in verwandten Ans¨atzen die Verifikation als Mechanismus zur Sicherung der Korrektheit einer Transformation verwendet. Diese wird im hier vorgeschlagenen Ansatz durch die weniger aufw¨andigen Tests abgelost. ¨
8.3 Theorie der Modelltransformationen
275
Der Ansatz zur Spezifikation und Transformation von Programmen in [Par90] enth¨alt ebenfalls eine detaillierte Sammlung von Regeln zur transformationellen Softwareentwicklung fur ¨ abstrakte, durch algebraische Gesetze definierte Datentypen, funktionale und imperative Programme sowie von Datenstrukturen. Dort werden zum Beispiel die Entfernung uberfl ¨ ussi¨ ger Zuweisungen und Variablen, die Umordnung von Anweisungen, die Behandlung von Kontrollstrukturen oder verschiedene Varianten der Komposition von Funktionen diskutiert, die teilweise eine direkte Entsprechung in [Fow99] finden. Daruber ¨ hinaus bietet [Par90] wie auch [BBB+ 85] Techniken zur inkrementellen Verfeinerung einer Spezifikation in Richtung auf eine operationelle Implementierung. Algebraische Spezifikation In den algebraischen Spezifikationstechniken wurde als erstes die Definition expliziter Beobachtungen eingefuhrt. ¨ Mithilfe des Versteckens von Sorten hat zum Beispiel OBJ [FGJM85] einen Mechanismus angeboten, Details eines algebraisch spezifizierten abstrakten Datentypen zu Verstecken und eine explizite nutzbare und beobachtbare Schnittstelle zur Verfugung ¨ zu stellen. Weitere algebraische Ans¨atze [ST87, BHW95, GR99, BFG+ 93] demonstrieren, dass es moglich ¨ ist, explizit extern sichtbares Verhalten zu definieren und darauf rigorose Beweistechniken anzusetzen. [BBK91] enth¨alt eine generelle ¨ Ubersicht uber ¨ Ans¨atze zur Definition von Beobachtbarkeit in algebraischen Spezifikationen. Wie bereits diskutiert, ist es ein gewisser Nachteil des hier verwendeten, aber dafur ¨ sehr flexiblen Testansatzes, dass die Beobachtung und damit auch die beobachtete Schnittstelle durch die Tests nur implizit und fur ¨ jeden Test anders definiert ist. Es erfordert daher die Disziplin des Testentwicklers insbesondere bei Akzeptanztests moglichst ¨ auf stabile und wie bei den algebraischen Spezifikationen explizit publizierte“ Schnittstellen zuzugreifen. ” Refactoring und Verifikation In [Sou01] werden Refactoring und Verifikation in Beziehung gesetzt. In diesem Sinne wird zun¨achst die Verifikation mittels einer unter Umst¨anden interaktiv erstellten Sammlung von Beweisen als wesentliches Element zur Sicherstellung der Korrektheit eines Systems eingesetzt. Die Weiterentwicklung des Systems mithilfe kleiner, systematischer Schritte fuhrt ¨ dann dazu, auch die Beweise entsprechend zu adaptieren und deren Korrektheit durch automatische Wiederholung sicherzustellen. Durch ad¨aquate Werkzeugunterstutzung, ¨ wie sie zum Beispiel Isabelle [NPW02] mit den teilweise sehr m¨achtigen Beweistaktiken bietet, kann so eine evolution¨are Weiterentwicklung unter Wiederverwendung von Verifikationsanteilen vorgenommen werden.
276
8 Refactoring als Modelltransformation
Transformation graphischer Spezifikationen Eine Reihe von Arbeiten zeigt, dass fur ¨ graphische Spezifikationen, unabh¨angig davon, ob sie Struktur-, Verhaltens- oder Interaktionssichten eines Systems darstellen, ebenfalls transformationelle Techniken entwickelt werden konnen. ¨ Der in [BS01b] beschriebene Ansatz Focus demonstriert die Kombinierbarkeit formaler, textbasierter Spezifikationstechniken mit einer graphischen Repr¨asentation der verteilten Interaktion von Komponenten. Darin stehen pr¨azise Techniken zur Dekomposition von Komponenten und Kan¨alen und zur Verfeinerung von Verhalten und Schnittstellen bereit, deren Auswirkungen an graphischen Modellen geplant und studiert werden konnen. ¨ In diesem Kontext wurde in [PR97, PR99] eine Technik zur Glas-BoxTransformation der inneren Struktur eines verteilten Systems vorgestellt, dessen an der Schnittstelle beobachtbares Verhalten bei der Transformation a¨ quivalent bleibt oder verfeinert wird. Ein Teil dieser Transformationstechnik basiert auf der Verhaltensverfeinerung innerer Komponenten, die zum Beispiel mit Zustandsautomaten beschrieben werden konnen ¨ [Rum96]. Dass diese Transformationsformen nicht nur fur ¨ massiv verteilte oder Hardware-nahe Systeme geeignet sind, zeigt [RT98] in der Gesch¨aftsprozesse durch strukturelle Transformationen optimiert werden. Zusammenfassend l¨asst sich feststellen, dass das semantikerhaltende Refactoring auf einem durch eine Testsammlung basierenden Beobachtungsbegriff basiert. Zwei Verallgemeinerungen, der Transformation von Modellen, die Abstraktion und insbesondere die Verfeinerung konnten ¨ nicht nur zur Restrukturierung der vorhandenen Systembeschreibung, sondern auch zur transformationellen Weiterentwicklung dienen. Ans¨atze wie die in Abschnitt 8.3.6 beschriebenen [BBB+ 85, Par90] haben dies gezeigt. Praktisch anwendbar werden transformationelle Entwicklungsschritte aber vor allem durch die Existenz automatisierter Tests.
9 Refactoring von Modellen
Die Handlungen der Menschen leben fort in den Wirkungen. Gottfried Wilhelm von Leibniz
Auf Basis der Grundlagen fur ¨ Modelltransformationen und ihrer Spezialisierung als Refactorings-Regeln beinhaltet dieses Kapitel eine Diskussion von ¨ moglichen ¨ Formen von Refactoring-Regeln fur ¨ die UML/P, die Ubertragung von Regeln anderer Sprachen auf die UML/P und eine additive Vorgehensweise fur ¨ die Durchfuhrung ¨ großerer ¨ Datenstrukturwechsel.
9.1 9.2 9.3
Quellen fur ¨ UML/P-Refactoring-Regeln . . . . . . . . . . . . . . . 278 Additive Methode fur ¨ Datenstrukturwechsel . . . . . . . . . . 299 Zusammenfassung der Refactoring-Techniken . . . . . . . . . 314
278
9 Refactoring von Modellen
Es ist davon auszugehen, dass die Anzahl der Refactoring-Regeln a¨ hnlich den Theoremen der Mathematik im Prinzip unbeschr¨ankt ist. Deshalb ist es erstrebenswert, neben einem grundlegenden Satz von Regeln und einem ausgew¨ahlten Portfolio an komplexen Regeln fur ¨ spezifische Problem¨ stellungen, Mechanismen zu kennen, die die Ubertragung von vorhandenen Refactoring-Regeln anderer Sprachen sowie die Spezialisierung und die Kombination von Regeln erlaubt. In diesem Kapitel wird deshalb zun¨achst untersucht, wie RefactoringRegeln anderer Sprachen ubertragen ¨ werden konnen. ¨ Die darauf aufbauende exemplarische Diskussion komplexer Refactorings bietet wie auch bei den Testmustern gegenuber ¨ einer detaillierten Liste von Regeln den Vorteil der selbst¨andigen Adaptierbarkeit. Abschnitt 9.1 diskutiert Mechanismen zur Erstellung von Refactorings ¨ und zur Ubertragung vorhandener Refactorings auf die UML/P. Als Quellen eignen sich dafur ¨ zum Beispiel die der UML/P zugrunde liegende Programmiersprache Java, die Automatentheorie fur ¨ Statecharts und Verifikationstechniken fur ¨ die OCL. In Abschnitt 9.2 wird darauf aufbauend ein auf OCL-Invarianten basierendes Verfahren zum Wechsel von Datenstrukturen vorgestellt, die mit Klassendiagrammen modelliert sind. Dieses Verfahren erlaubt es, individuelle und großere ¨ Refactoring-Schritte zu entwickeln und durchzufuhren. ¨ Es nutzt dabei Anleihen aus der Verifikation, indem es pr¨adikative Beziehungen zwischen den auszutauschenden Datenstrukturen herstellt, pruft ¨ aber deren Korrektheit durch automatisierte Tests.
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln ¨ Nach der allgemeinen und durch theoretische Uberlegungen fundierten Betrachtung von Modelltransformationen im letzten Abschnitt, werden diese ¨ Uberlegungen nun praktisch umgesetzt. Dabei werden vor allem Mechanismen und Quellen diskutiert, Refactoring-Regeln aus bekannten Ans¨atzen auf die UML/P zu transferieren. Refactoring-Schritte auf der UML/P tangieren meistens mehrere Notationen. So ist bei der Verschiebung einer Methode im Klassendiagramm auch immer ein Teil des Java/P-Codes, der Sequenzdiagramme und sogar Statecharts betroffen. Eine Diskussion von Refactoring-Techniken fur ¨ UML/P kann daher nicht vollst¨andig isoliert fur ¨ einzelne Notationen erfolgen. Die Granularit¨at eines Refactorings ist so zu w¨ahlen, dass die Transformationsschritte beherrschbar bleiben und die automatisierten Tests regelm¨aßig ausgefuhrt ¨ werden konnen. ¨ Zu große oder zu wenig durch Tests unterstutzte ¨ Schritte fuhren ¨ zum Big-Bang-Syndrom mit viel Aufwand fur ¨ die Fehlersuche. Das bedeutet, dass große Refactorings soweit wie notwendig in kleine Schritte zerlegt und ein Plan zur Umsetzung erstellt werden
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
279
sollte. Dennoch gibt es gr¨oßere Refactorings, die ganz bestimmte, teilweise spezialisierte Problemstellungen behandeln. Die UML/P ist im Vergleich zu Java-Programmen relativ kompakt, indem sie eine Trennung zwischen technischem und applikationsspezifischem Code erlaubt, Hilfsmethoden automatisch generiert und durch die Trennung ¨ verschiedener Sichten einen besseren Uberblick ermoglicht. ¨ Diese Kompaktheit der UML/P erlaubt die Beherrschung noch großerer ¨ Refactoring-Schritte als dies mit Java der Fall ist. So unterstutzt ¨ ein Klassendiagramm die Planung des Refactorings und die Verwendung von OCL-Invarianten erlaubt die Modellierung und Prufung ¨ von Annahmen, die fur ¨ ein Refactoring getroffen ¨ werden. Diese Uberlegung fuhrt ¨ zu der in Abschnitt 9.2 diskutierten Vorgehensweise fur ¨ die Durchfuhrung ¨ von großeren ¨ Datenstrukturwechseln mithilfe von Refactoring bei denen mehrere UML/P-Notationen eine wesentliche Rolle spielen. W¨ahrend fur ¨ objektorientierte Programmiersprachen wie Java oder Smalltalk unter anderem mit [Fow99] bereits Sammlungen kleiner und mittlerer Refactorings vorliegen, existieren fur ¨ eine Modellierungssprache wie UML/P solche Refactoring-Schritte nur marginal. In [Ast02] wird zum Beispiel ein Ansatz beschrieben, der UML-Klassendiagramme als Unterstutzung ¨ fur ¨ das Refactoring von Java-Programmen nutzt. Dabei werden aus dem existierenden Code Klassendiagramme extrahiert, um damit Codedefizite zu identifizieren. Ein auf Graphgrammatiken basierender Transformationsansatz fur ¨ die graphische Modellierungssprache UML ist in [EH00, EHHS00] beschrieben. In [EHHS00] werden diese Transformationen sogar zur Beschreibung von dynamischen Systemabl¨aufen uber ¨ die Refaktorisierung der Systemstruktur hinaus eingesetzt. UML-Diagramme werden in der aktuellen Literatur noch wenig als Prim¨arziel fur ¨ Refactorings verstanden. Dabei ist die Anwendung insbesondere auf die konstruktiven Beschreibungstechniken, wie Klassendiagramme, Statecharts und die OCL, interessant. Die Anpassung exemplarischer Beschreibungen, wie Objekt- und Sequenzdiagramme ist demgegenuber ¨ vergleichsweise einfach. Da letztere vor allem zur Definition von Tests herangezogen werden, ist deren Anpassung immer dann notwendig, wenn ein Test nach einem Refactoring scheitert. Dabei kann es durchaus sinnvoll sein, aus einem gescheiterten Test mehrere neue zu entwickeln, wenn zum Beispiel ein einzelner Methodenaufruf durch ein Protokoll mit einer Serie von zusammenh¨angenden Methodenaufrufen ersetzt wurde und verschiedene Reihenfolgen und Abbruchsmoglichkeiten ¨ im Protokoll getestet werden sollen. Die das beobachtbare Verhalten erhaltenden Transformationen von Statecharts wurden bereits in Abschnitt 6.6.2, Band 1 ausfuhrlich ¨ diskutiert. Dabei wurde eine Sammlung von zielorientierten Regeln vorgestellt, die es erlauben, Statecharts zu vereinfachen, indem zum Beispiel hierarchische Zust¨ande flach gedruckt ¨ werden. Viele dieser Regeln konnen ¨ auch in umge-
280
9 Refactoring von Modellen
kehrter Richtung angewandt werden. Manche Regeln jedoch, wie zum Beispiel die Reduktion von Nichtdeterminismus im Statechart sind eine echte Verfeinerung in dem in Abschnitt 8.3.2 definierten Sinn. Aus den in Abschnitt 8.2 beschriebenen okonomischen ¨ Gesichtspunkten, wird aus der heutigen Sicht ein Refactoring von Statecharts vor allem fur ¨ Systeme oder Systemteile mit komplexen Zustandsr¨aumen und hoher Kritikalit¨at als sinnvoll erachtet. Dazu gehoren ¨ beispielsweise Avioniksysteme oder komplexe Transaktionslogiken in Banksystemen. Ziel dieses Abschnitts ist es, zu diskutieren, wie Regels¨atze fur ¨ die Modellierungssprache UML/P aus anderen Refactoring-Ans¨atzen ubernommen ¨ werden konnen. ¨ Dabei soll kein vollst¨andiger Katalog entwickelt werden. Stattdessen wird anhand ausgesuchter Beispiele demonstriert, wie Refactoring-Regeln fur ¨ die UML/P definiert werden und welche Effekte damit erzielt werden konnen. ¨ Damit wird also nicht ein Katalog an Refactorings, sondern eine Technik zur eigenst¨andigen Entwicklung von Refactoring-Regeln zur Verfugung ¨ gestellt. Dazu gehort ¨ insbesondere auch die im n¨achsten Abschnitt diskutierte Vorgehensweise zum Refactoring von Datenstrukturen, die auch dazu genutzt werden kann, neue Refactoring-Regeln aus konkreten Anwendungen heraus zu extrahieren. 9.1.1 Definition und Darstellung von Refactoring-Regeln Ein Refactoring kann a¨ hnlich wie eine Transformation fur ¨ die Codegenerierung in zwei Formen beschrieben werden. Eine technische, detaillierte und pr¨azise Beschreibung eignet sich vor allem fur ¨ die Umsetzung in Werkzeugen. Sie benotigt ¨ als Grundlage die abstrakte Syntax sowie Kontextbedingungen, die pr¨azise auf dieser abstrakten Syntax definiert sind. Eine zweite Form der Darstellung ist fur ¨ den Anwender geeignet. Das Prinzip wird motiviert und anhand eines relativ allgemeinen, aber oft nicht alle F¨alle abdeckenden Beispiels erkl¨art. Kontextbedingungen werden eher informell, aber doch pr¨azise diskutiert und Sonderf¨alle erl¨autert. Wie bei Mustern ublich, ¨ werden konkrete Beispiele angegeben. Eine Diskussion der Konsequenzen, Vor- und Nachteile ist insbesondere bei großeren ¨ Refactorings sinnvoll. Als letztes werden Verweise auf verwandte Refactorings sowie auf die Umkehrung des Refactorings angegeben. Das Format zur Darstellung eines Refactorings lehnt sich also an das Format fur ¨ Codegenerierung aus Tabelle 3.11 an und ist in Tabelle 9.1 als Schablone dargestellt. In [Fow99] werden Refactoring-Regeln durchg¨angig als Tripel Motivation, Mechanik und Beispiel dargestellt. Der Abschnitt Mechanik beschreibt dabei genau wie hier eine operative Liste von Einzelschritten, die zur Durchfuhrung ¨ des Refactorings sinnvoll sind. Dabei werden Sonderf¨alle ebenfalls behandelt.
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
281
Schablone fur ¨ die Darstellung von Refactoring-Regeln Problem
Welches Problem besteht und soll durch diese Regel behoben werden? Ziel Soweit nicht durch Motivation und Problembeschreibung bereits klar, werden hier die Ziele des Refactorings noch einmal beschrieben. Motivation Warum und wann wird die Refactoring-Regel eingesetzt? Welche Verbesserungen ergeben sich dadurch fur ¨ die Softwareentwicklung? Refactoring Meist gibt es eine prim¨are Transformationsregel fur ¨ das Refactoring, die in folgender Form dargestellt wird: Ursprung ⇓ Ziel • Die Darstellung einer Refactoring-Transformation ist analog zur Darstellung von generierenden Transformationen, wie in Tabelle 3.11 beschrieben. Das Aussehen von Ursprung und Ziel sind dort erkl¨art. • Erkl¨arende Texte beschreiben Kontextbedingungen, Sonderf¨alle und den Umgang damit. • Ist das Refactoring in mehrere Einzelschritte aufzuteilen, so werden diese hier beschrieben. Diese Einzelschritte werden auch als Mechanik“ bezeichnet. ” Weitere Begleitend zur prim¨aren Transformation ergeben sich meist Refactorings zus¨atzlich notwendige Transformationen, die in analoger Form dargestellt werden. Implemen- Technische Details wie zum Beispiel die Modifikation von Metierung thodenrumpfen ¨ werden hier dargestellt und diskutiert. Beispiele Beachtenswert
Beispiele konnen ¨ zur Erl¨auterung des Prinzips und zur Diskussion von Sonderf¨allen genutzt werden. Dieser Abschnitt rundet durch zus¨atzliche Betrachtungen, Hinweise und der Diskussion potentieller Problemstellungen die Beschreibung ab. Insbesondere wird darauf hingewiesen, welche Konsequenzen, Vor- und Nachteile das beschriebene Refactoring hat.
Tabelle 9.1.: Schablone fur ¨ die Darstellung von Refactoring-Regeln Nachdem die Motivation und die Hintergrunde ¨ fur ¨ Refactoring und das Aussehen von Refactoring-Regeln gekl¨art wurden, wird in Abbildung 9.2
282
9 Refactoring von Modellen
eine fur ¨ dieses Buch gultige ¨ Begriffsbestimmung auf Basis der Abbildung 8.1 vorgenommen. Die diesem Buch zugrunde liegenden Definitionen im Kontext des Refactoring: Refactoring ist eine Technik zur regelbasierten Transformation von Modellen unter Erhalt des extern beobachtbaren Verhaltens. Ein Refactoring kann in eine Serie von einzelnen Schritten aufgeteilt sein. Refactoring-Regel ist eine zielgerichtete Vorschrift zur Durchfuhrung ¨ eines Refactorings. Sie beinhaltet eine Serie von mit Schemavariablen formulierten, beobachtungsinvarianten Modelltransformationen, die auf das Ausgangsmodell anzuwenden sind, eine Motivation fur ¨ deren Anwendung und eine Diskussion der Auswirkungen. Refactoring-Schritt ist die Anwendung einer Refactoring-Regel an einer konkreten Stelle. Beobachtung ist ein Test, bestehend aus mehreren Modellen, die vom System geforderte Eigenschaften beschreiben. Externe Beobachtung ist eine Beobachtung, die nicht ver¨andert werden darf, ohne projektexterne Konsultationen zu erfordern. Externe Beobachtungen werden durch Akzeptanztests und Tests uber ¨ Schnittstellen zu Nachbarsystemen, fixierte Frameworks, etc. festgelegt. Abbildung 9.2. Begriffsdefinition im Kontext des Refactoring
9.1.2 Refactoring in Java/P Die in der UML eingebettete und in Anhang B, Band 1 definierte Programmiersprache Java/P unterscheidet sich in ihrer Syntax kaum von der JavaStandardversion. Wesentlichster Unterschied ist, dass gem¨aß der in Kapitel 3 und Kapitel 4 beschriebenen Codegenerierung die damit beschriebenen Coderumpfe ¨ einer Umsetzung unterliegen. Dabei werden beispielsweise Attributzugriffe in get- und set-Methoden umgewandelt. Attribute sind damit immer gekapselt und einige der in [Fow99] definierten Refactorings fur ¨ Java/P unnotig. ¨ So wird zum Beispiel die Refactoring-Regel Encapsulate ” Field“ [Fow99, S. 206] durch den Codegenerator automatisch durchgefuhrt. ¨ Dies hat den Vorteil, dass die Kapselung zwar sichergestellt ist, dem Entwickler aber in der Modellierung die Kapselungsmethoden verborgen bleiben. Andere Refactoring-Regeln aus [Opd92] und [Fow99] lassen sich uber¨ nehmen. In [Opd92] sind 26 Low-Level-Refactorings fur ¨ C++ angegeben. Davon sind jeweils drei zur Erzeugung und Loschung ¨ von Programmelementen (Klassen, Funktionen, Attributen), 15 fur ¨ die Anpassung vorhandener Programmelemente und zwei fur ¨ die Verschiebung vorgesehen. Drei Refactorings sind Kompositionen vorheriger Transformationen. Drei zus¨atzliche Refactorings dienen der Generalisierung und Spezialisierung der Klas-
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
283
senhierarchie und der Behandlung von Aggregation und wirken damit uber ¨ einzelne Klassen hinaus. [Fow99] enth¨alt entsprechende Analogien fur ¨ Java. ¨ Deshalb wird nachfolgend die Ubertragbarkeit der einzelnen RefactoringRegeln aus [Fow99] auf Java/P beziehungsweise Diagramme der UML/P diskutiert. Refactorings in [Fow99] In [Fow99] sind 72 Refactoring-Regeln enthalten, die dort als initialer und unvollst¨andiger Refactoring-Katalog (S. 103) bezeichnet werden, der weiter ausgebaut werden kann. Dies findet derzeit in mehreren Diskussionsforen1 statt, durch die die Anzahl elementarer, zusammengesetzter und komplexer Refactorings auf Java-Basis weiter anwachsen wird. Nachfolgend werden 68 der in [Fow99] publizierten Refactoring-Regeln in kompakter Form analysiert. Dabei wird angenommen, dass [Fow99] bekannt ist. Die vier nicht dargestellten Regeln werden als Big Refactorings“ ” bezeichnet, die sich zum Beispiel mit der Separation komplexer Vererbungshierarchien oder dem Umbau prozeduralen Codes in Objektstrukturen besch¨aftigen. Ziel dieser Analyse ist eine Klassifikation der Regeln nach zwei Kriterien: (1) Auf welche Elemente wirkt die Regel und (2) welche Auswirkungen hat sie darauf? Die sechs Spalten entsprechen den Java-Sprachelementen Coderumpfe, ¨ Methoden einschließlich Konstruktoren, Attribute, Klassensignaturen einschließlich Interfaces, Vererbungsbeziehungen und Assoziationen. Als Auswir¨ kungen sind Verschieben (m), neu Einfuhren ¨ (∗), L¨oschen (†) und Andern (ch) in die entsprechenden Spalten eingefugt. ¨ Der originale englische Name der in Tabelle 9.3 in alphabetischer Reihenfolge aufgelisteten Regeln wurde beibehalten. Die Tabelle spiegelt die Ver¨anderungen der prim¨aren Refactoring¨ Regel wider. Weitere Anderungen anderer Sprachelemente konnen ¨ sich in Sonderf¨allen ergeben.
Assoz.
Vererb.
Name der Refactoring-Regel Add Parameter ch ch Change Bidirectional Association to Unidirectional ch †
Kl.Sig.
Attrib.
Methd.
Code
Klassifikation der Refactoring-Regeln aus [Fow99]
ch ch
(wird in UML/P durch Generator vereinfacht)
Change Reference to Value
ch
ch Fortsetzung auf n¨achster Seite
1
¨ http://www.refactoring.com/ enth¨alt eine Ubersicht aktuell existenter Refactoring-Regeln, Links auf weitere Quellen und insbesondere aktuelle Refactoring-f¨ahige Werkzeuge (Stand: Aug. 2002).
284
9 Refactoring von Modellen
∗
Assoz.
Vererb.
Kl.Sig.
Attrib.
Name der Refactoring-Regel Change Unidirectional Association to Bidirectional ch
Methd.
Code
(Fortsetzung von Tabelle 9.3.: Klassifikation der Refactoring-Regeln aus [Fow99])
ch
(wird in UML/P durch Generator vereinfacht)
Change Value to Reference Collapse Hierarchy Consolidate Conditional Expression Consolidate Duplicate Conditional Fragments Decompose Conditional Duplicate Observed Data Encapsulate Collection Encapsulate Downcast Encapsulate Field
ch
(unn¨otig, wenn das dem Generator aufgetragen wird)
Extract Class Extract Interface Extract Method Extract Subclass Extract Superclass Form Template Method Hide Delegate Hide Method Inline Class Inline Method Inline Temp Introduce Assertion
†
ch m
∗
∗
m m ∗ ∗ ∗ ∗ m m ∗ ∗ m m ∗ ∗ ∗ ch ∗ ch ch ch m m † †
∗
m m † ch ∗ ch ch ∗ ch ∗ ∗ ∗ ch ∗ ch ch ch ∗ ch
m
ch ch m ch ch
† †
(in UML/P gibt es dazu erweiterte M¨oglichkeiten)
Introduce Explaining Variable Introduce Foreign Method Introduce Local Extension
ch ch ∗ ∗
(nutzt Adapter oder Unterklasse)
Introduce Null Object Introduce Parameter Object Move Field Move Method Parameterize Method Preserve Whole Object Pull Up Constructor Body Pull Up Field Pull Up Method Push Down Field Push Down Method
ch ch ch ch ch ch m
∗ ch m ch ch ∗ m m
m
∗
∗
∗ ∗
∗
∗
ch ch ch m m Fortsetzung auf n¨achster Seite
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
285
ch ch ch ch ch ch ch ch ch ch
† ch ∗ ∗
Replace Type Code with Subclasses Self Encapsulate Field (unn¨otig, wenn das dem Generator aufgetragen wird)
Separate Query from Modifier Split Temporary Variable Substitute Algorithm
ch †
∗
∗
∗
∗ ∗
ch ch ∗
ch ch m ∗ ch † ch †
†
†
∗ ∗
∗
†
∗
∗
ch ch ∗ ch ∗ ch ch
Assoz.
Vererb.
∗ †
∗ ch
(sinnvoll ist auch die Nutzung eines Statecharts)
∗
ch ∗ ch ∗
(fur ¨ UML/P uninteressant)
Replace Subclass with Fields Replace Temp with Query Replace Type Code with Class Replace Type Code with State/Strategy
Kl.Sig.
ch ch ch † ch ch ch ch † ch ch ch ch ∗ ch ∗ ∗ ch ∗ ch
(unn¨otig, wenn das dem Generator aufgetragen wird)
Replace Data Value with Object Replace Delegation with Inheritance Replace Error Code with Exception Replace Exception with Test Replace Inheritance with Delegation Replace Magic Number with Symbolic Constant Replace Method with Method Object Replace Nested Conditional with Guard Clauses Replace Parameter with Explicit Methods Replace Parameter with Method Replace Record with Data Class
Attrib.
Methd.
Name der Refactoring-Regel Remove Assignments to Parameters Remove Control Flag Remove Middle Man Remove Parameter Remove Setting Method Rename Method Replace Array with Object Replace Conditional with Polymorphism Replace Constructor with Factory Method
Code
(Fortsetzung von Tabelle 9.3.: Klassifikation der Refactoring-Regeln aus [Fow99])
∗
ch
Tabelle 9.3.: Klassifikation der Refactoring-Regeln aus [Fow99] Weil in fast allen Refactoring-Regeln die Klasse, die die modifizierten, eingefuhrten ¨ oder entfernten Elemente enth¨alt, betroffen ist, wird die entsprechende Spalte nur markiert, wenn der extern bekannte (public) Anteil der Methoden einer Modifikation unterliegt.
286
9 Refactoring von Modellen
Im Gegensatz zu allen anderen Regeln beschreibt das Refactoring Encap” sulate Collection“ den Umgang mit Container-Klassen und ist damit abh¨angig von der Java-Klassenbibliothek. Dieses Beispiel zeigt, dass nicht nur auf der Sprache, sondern auch auf den Klassenbibliotheken operierende RefactoringRegeln sinnvoll sind. Wie sich aus der Tabelle erkennen l¨asst, werden bei vielen Refactorings aus [Fow99] mehrere Schritte zusammengefasst. So wird bei der Expansion einer Methode mit Inline Method“ gleichzeitig vorgeschlagen, diese zu ” loschen. ¨ Dies hat zur Nebenbedingung, dass die expandierte Methode sonst nirgendwo verwendet wird, und h¨atte auch in zwei unabh¨angige Refactorings zerlegt werden konnen. ¨ Die Granularit¨at vieler Regeln, wie Form Template Method“ zur Extrak” tion einer Methode in eine Oberklasse, die in mehreren Unterklassen a¨ hnlich realisiert ist, ist so gew¨ahlt, dass sie zielgerichtete Vorgehensweisen zur Verbesserung der Aufrufstruktur von Methoden beinhalten. Derartige Regeln nutzen oft andere Regeln, wie hier zum Beispiel die Verschiebung von Methoden in der Klassenhierarchie. Die Refactoring-Regeln in [Fow99] sind oft nicht minimal, sondern kombinieren einzelne Transformationen zu einer zielgerichteten Vorgehensweise. Die bei der Behandlung des Codes oft notwendigen algebraischen Umformungen werden dabei nicht explizit diskutiert, sondern stillschweigend deren Beherrschung vorausgesetzt. ¨ Ubertragung der Refactorings auf UML/P Fur ¨ die Erstellung von Transformationsregeln lassen sich mehrere Ans¨atze identifizieren. Beispielsweise lassen sich Transformationsregeln aus der in Abbildung 8.5 dargestellten Theorie motivieren. Dabei wird die vorhandene Sprache untersucht und a¨ quivalente beziehungsweise in Verfeinerung stehende Darstellungen eines Sachverhalts identifiziert. Demgegenuber ¨ konnen ¨ Transformationsregeln fur ¨ eine neue Sprache durch eine Adaption von Regeln einer bereits bekannten Sprache entstehen. Dazu kann die in Abbildung 9.4 dargestellte Vorgehensweise angewandt werden.
Abbildung 9.4. Refactoring-Regeln lassen sich ubertragen ¨
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
287
Die in [Fow99] enthaltenen Refactorings fur ¨ Java konnen ¨ dadurch auf UML/P ubertragen ¨ werden. Grundlage dazu ist die in Kapitel 3 diskutierte Codegenerierung als Verbindung zwischen UML/P und Java. Die auf Java existierenden Refactoring-Regeln konnen ¨ mithilfe dieser Codegenerierung auf UML/P ubersetzt ¨ werden. Dabei konnen ¨ allerdings manche Regeln obsolet werden, wie bereits in der Tabelle 9.3 vermerkt. Andere Regeln finden mehrere Auspr¨agungen. Diese Ruckrechnung ¨ der Transformationsregeln auf UML/P wird umso komplexer, je großer ¨ der konzeptuelle Unterschied zwischen beiden Sprachen ist. Bei Klassendiagrammen und Java ist dieser sehr gering und die Ruckrechnung ¨ daher weitgehend kanonisch. Statecharts und Java haben jedoch einen so großen konzeptuellen Abstand, dass fur ¨ Statecharts ganz eigenst¨andige Transformationsregeln sinnvoll sind. Da OCL und Java viele Sprachkonzepte gemeinsam haben, lassen sich eine Reihe der in [Fow99] beschriebenen Refactoring-Regeln sinngem¨aß auf die OCL ubertragen. ¨ In der OCL werden zum Beispiel tempor¨are Variablen mit dem let-Konstrukt definiert. Diese konnen ¨ ebenfalls expandiert oder umgeformt werden. Die Extraktion von neuen Methoden wird in der OCL mithilfe der Definition von Queries im zugrunde liegenden Klassendiagramm durchgefuhrt. ¨ Zum Beispiel kann die Regel Inline Temp“, die eine tempor¨ar ” benutzte Variable expandiert, durch folgende Regel beschrieben werden:
Dabei wird jedes Vorkommen der Variable temp im Ausdruck expr2 durch den Teilausdruck expr substituiert. In der OCL sind wie zum Beispiel hier aufgrund der Seiteneffektfreiheit und Determiniertheit viele Kontextbedingungen von vornherein erfullt. ¨ Andere Regeln modifizieren die Struktur oder die Signatur von Klassen oder fugen ¨ neue Klassen ein. Diese Regeln konnen ¨ daher in ad¨aquater Weise ebenfalls auf Klassendiagrammen (und den davon abh¨angigen Java/PCoderumpfen) ¨ eingesetzt werden. Allerdings ist der Regelsatz auch hier keineswegs vollst¨andig. So existiert mit Hide Method“ eine Regel, eine offentli¨ ” che Methode in eine private umzuwandeln, aber die Umkehrung ist so einfach, dass diese nicht durch eine Regel abgedeckt wurde. Weitere Regeln konnen ¨ zum Beispiel die Umwandlung einer Klasse in ein Interface oder die Modifikation beigefugter ¨ Stereotypen behandeln. Regeln, die die Signatur einer Methode oder eines Attributs ver¨andern, haben auch Auswirkungen auf die Stellen in Tests, Statecharts oder Sequenzdiagrammen, in denen diese Elemente genutzt werden. Ein fur ¨ einen Test eingesetztes Sequenzdiagramm ist zum Beispiel anzupassen, wenn sich die innere Aufrufstruktur a¨ ndert und Stereotypen wie match:complete dies in einem Sequenzdiagramm erfordern (siehe Abschnitt 7.3, Band 1).
288
9 Refactoring von Modellen
9.1.3 Refactoring von Klassendiagrammen Im vorangegangenen Abschnitt 9.1.2 wurden im Kontext von Java/P bereits eine Reihe von Refactoring-Regeln identifiziert, die Klassen modifizieren und damit auch auf Klassendiagramme Auswirkungen haben. Es gibt jedoch weitere Refactoring-Regeln fur ¨ Klassendiagramme. Die Regeln fur ¨ die Transformation von Klassendiagrammen lassen sich in folgende Kategorien einteilen: Kleine Refactorings dienen der Bearbeitung eines einzelnen Elements im Klassendiagramm. Dazu gehorten ¨ zum Beispiel das Loschen ¨ oder Einfu¨ gen eines Attributs. Zielgerichtete, mittelgroße Refactorings sind Transformationsschritte, die eine Motivation und ein Ziel beinhalten. Diese Form der RefactoringSchritte wurde bereits in Abschnitt 9.1.2 fur ¨ Java diskutiert und tangiert meistens mehrere Elemente auch außerhalb eines einzelnen Modells. Diese Regeln sind aber in ihren Auswirkungen lokal begrenzt. Verbesserung der Repr¨asentation dient zur besseren Darstellung eines ¨ Sachverhalts, ohne dabei eine inhaltliche Anderung vorzunehmen. Dazu gehort ¨ zum Beispiel die Reorganisation von Klassendiagrammen. Abstrakte Klassendiagramme konnen ¨ als Beschreibungen von Schnittstellen und Komponenten eingesetzt werden. Ein solches Klassendiagramm enth¨alt nur den Teil der Datenstruktur und Methoden, die explizit publiziert wurden und damit gesichert zur Verfugung ¨ stehen. Ein solches Klassendiagramm wird durch Abstraktionsschritte aus der internen Struktur gewonnen. Diese Kategorien von Transformationstechniken werden nun im Einzelnen diskutiert. Kleine Refactorings Viele Transformationsregeln bestehen nur aus der Transformation eines einzelnen syntaktischen Elements. Die Syntax von Klassendiagrammen wird in Anhang C, Band 1 beschrieben und besteht aus 19 Nichtterminalen. Fur ¨ jedes Nichtterminal kann ein neues Element eingefuhrt, ¨ ein Bestehendes geloscht ¨ oder dessen Bestandteile modifiziert werden. Dazu gehoren ¨ zum Beispiel Umbenennung eines Attributs, einer Assoziation oder Rolle, Versch¨arfung einer Kardinalit¨at, Modifikation einer Sichtbarkeitsangabe, Ver¨anderung einer Navigationsrichtung, Einfuhrung ¨ oder Elimination eines Qualifikators und Ersetzung des Typs einer Variable. Weil diese auf ein syntaktisches Element fokussierenden Modifikationen aber relativ klein und kanonisch anwendbar sind, wird hier auf eine Auflistung dieser Refactorings verzichtet. Allerdings besitzen einige dieser Modifikationen Kontextbedingungen oder erfordern weitere Aktivit¨aten an den benutzenden Stellen.
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
289
Mittlere und große Refactorings sind zielgerichtet und aus der Erfahrung praktischer Anwendung definiert. Sie modifizieren meistens mehrere Elemente und unter Umst¨anden sogar einen signifikanten Teil der Applikation. Zielgerichtete Refactorings Wie die Tabelle 9.3 zeigt, tangieren die in [Fow99] beschrieben Regeln meistens mehrere Elemente und kombinieren zielgerichtete Strategien zur Verbesserung des Designs. Zum Beispiel konnte ¨ die Migration eines Attributs im Prinzip in die Einzelschritte (1) neues Attribut einfuhren, ¨ (2) die Modifikation der verwendenden Stellen und (3) altes Attribut loschen, ¨ zerlegt werden. Erst durch die Kombination dieser Einzelschritte entsteht aber eine zielgerichtete Strategie. Dennoch besitzt die Regel zur Migration von Attributen in [Fow99] ein Defizit in ihrer Umsetzung, die zum Beispiel durch die Verwendung von Invarianten behoben werden kann. Dieses Defizit besteht darin, dass sichergestellt werden muss, dass ein eindeutiger Navigationspfad zwischen der alten und der neuen Klasse des Attributs existieren muss, der keinen zeitlichen Ver¨anderungen unterliegt. Die nachfolgend demonstrierte Migration eines Attributs zwischen zwei Klassen ist ein einfaches Beispiel fur ¨ das Zusammenspiel mehrerer UML/PNotationen, das uber ¨ ein Klassendiagramm koordiniert werden kann. Einfache Sonderf¨alle dieser Migration sind, dass die neue Klasse eine Oberklasse darstellt oder das Attribut statisch ist und damit nur einmal existiert. Im allgemeinen Fall jedoch gibt es zwei Klassen, deren Objekte uber ¨ einen moglicherweise ¨ komplexen Navigationspfad miteinander verbunden sind. Ausgehend von der in Abbildung 9.5(a) dargestellten Situation soll das angegebene Attribut von A nach B verschoben werden.
context A a inv Connect: a.connection == a.exp context A a1, A a2 inv Unique: a1 != a2 implies a1.connection != a2.connection Abbildung 9.5. Verschiebung eines Attributs zwischen Klassen
OCL
OCL
290
9 Refactoring von Modellen
Dabei ist einerseits zu beachten, dass zwischen den Objekten beider Klassen eine geeignete Beziehung herrscht. Dies kann meist dadurch repr¨asentiert werden, dass es einen Ausdruck a.exp fur ¨ jedes Objekt a:A gibt, der eindeutig zu Objekten der Klasse B fuhrt. ¨ Dieser Ausdruck kann komplexe Navigationspfade und Methodenaufrufe enthalten, besitzt aber keine Seiteneffekte. Durch die in Abbildung 9.5(b) dargestellte abgeleitete Assoziation connection und die OCL-Bedingung Connect wird dieser Ausdruck in die Struktur des Klassendiagramms ubernommen ¨ und so einer leichteren Bearbeitung zug¨anglich gemacht. Insbesondere reicht es nun aus, das Merkmal {frozen} auf die abgeleitete Assoziation anzuwenden, um sicherzustellen, dass die Objekte der Klasse B nicht ausgetauscht werden. Dies wurde ¨ sonst bedeuten, dass so das ausgelagerte Attribut implizit seinen Inhalt wechseln wurde. ¨ Zum anderen ist sicherzustellen, dass jedes A-Objekt weiterhin sein eigenes Attribut besitzt. OCL-Bedingung Unique fordert dementsprechend, dass jedem A-Objekt ein eigenes B-Objekt zugeordnet ist. Die erarbeiteten Bedingungen wirken als Kontextbedingungen fur ¨ die ¨ Ubertragung eines Attributs in eine andere Klasse. Das Ergebnis l¨asst sich in der Refactoring-Regel in Tabelle 9.6 zusammenfassen. Refactoring: Migration eines Attributs Problem
Ziel Motivation
Ein Attribut gehort ¨ inhaltlich zu einer anderen Klasse. Das Attribut wird mit jedem Objekt neu angelegt und die Klassen stehen nicht in Vererbungsbeziehung. Das Attribut wird zwischen zwei Klassen verschoben. Wenn das Attribut eher von der anderen Klasse genutzt wird, empfiehlt es sich das Attribut dorthin zu verschieben.
Refactoring
(Fortsetzung auf n¨achster Seite)
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
291
(Fortsetzung von Tabelle 9.6.: Refactoring: Migration eines Attributs)
• Zuerst wird der Navigationspfad a.exp identifiziert. • Dieser hat zwei Bedingungen zu erfullen, ¨ die durch die abgeleitete Assoziation und die Invarianten beschrieben sind und damit auch getestet werden konnen. ¨ • Ist das Attribut als private deklariert, so sind gegebenenfalls Zugriffsmethoden einzufuhren. ¨ • Wenn das Attribut verschoben wird, werden gleichzeitig alle Zugriffspfade angepasst. Vereinfachungen sind hier moglich. ¨ Sonderf¨alle Beachtenswert
Ist das Attribut statisch oder wird es entlang der Vererbungshierarchie nach oben verschoben, so entf¨allt die Assoziation. Es sind Verallgemeinerungen moglich, ¨ wenn sich zum Beispiel mehrere A-Objekte mit gleichen Attributinhalten ein gemeinsames B-Objekt teilen. Tabelle 9.6.: Refactoring: Migration eines Attributs
Wie in der Refactoring-Regel demonstriert, konnen ¨ Invarianten am Ende des Refactorings beziehungsweise in einer nachfolgenden Weiterentwicklung des Systems aufgegeben werden. Dies ist zum Beispiel notwendig, wenn es das Ziel dieses Refactorings ist, den Austausch des Attributinhalts attribute in Zukunft durch Umh¨angen von B-Objekten zu bewerkstelligen. Die zeitliche Fixierung des Navigationspfads von Klasse A zu Klasse B wird in der Literatur gerne ubergangen ¨ und l¨asst sich in Java selbst auch nicht ohne Aufwand darstellen. Die UML/P ist aufgrund ihrer syntaktischen Reichhaltigkeit dafur ¨ deutlich besser geeignet. Verbesserung der Pr¨asentationsform durch Refactoring Wie bereits in Abschnitt 3.4, Band 1 diskutiert, ist die Beziehung zwischen Modell und Implementierung vielschichtig. So konnen ¨ syntaktisch unter¨ schiedliche Modelle bei ihrer Ubersetzung in Code zum gleichen System fuhren. ¨ Die Unterschiede der Modelle beziehen sich daher nur auf ihre Darstellung, nicht aber auf die Implementierung. Ein Standardbeispiel dafur ¨ ist die in Abschnitt 3.4, Band 1 diskutierte Verschmelzung von Klassendiagrammen, die aus zwei oder mehr Teildiagrammen ein Gesamtmodell entwickelt, das dieselbe Information beinhaltet. Die Migration von Information aus einem Klassendiagramm in ein anderes oder die Spaltung von Klassendiagrammen sind dazu verwandte Schritte, die w¨ahrend der Entwicklung vorgenommen werden. Eine Spaltung bie-
292
9 Refactoring von Modellen
tet sich zum Beispiel an, wenn durch wiederholtes Hinzufugen ¨ von Funktionalit¨at und Struktur in Form neuer Klassen, Methoden und Attribute ein Klassendiagramm uberladen ¨ wurde. Ebenso ist eine Spaltung von Interesse, wenn sich der im Diagramm dargestellte Systemausschnitt in zwei relativ unabh¨angige Subsysteme gliedern l¨asst, die im weiteren Verlauf des Projekts von unabh¨angigen Entwicklerteams bearbeitet werden sollen. Auch die Migration von Klassen zwischen Diagrammen dient zur Verbesserung der Darstellungsform des Modells. Detailinformation zu einzelnen Klassen, wie zum Beispiel Attribute oder Methoden, konnen ¨ zwischen Diagrammen migriert werden, wenn die Diagramme uberlappende ¨ Anteile besitzen.
Abbildung 9.7. Migration von Detailinformation in Klassendiagrammen
Wichtig ist es hier, den Unterschied zwischen der Migration eines Attributs oder einer Methode von einer Klasse in eine andere und der Migration der Information zwischen Klassendiagrammen zu unterscheiden. Im Beispiel bleiben Attribute und Methoden in derselben Klasse und werden nur an anderer Stelle dargestellt. Eine weitere Form der Bearbeitung von Klassendiagrammen ist zum Beispiel die Expansion der Detailinformation von Klassen. So kann in einem Diagramm die aus anderen Diagrammen verfugbare ¨ Information zus¨atzlich dargestellt werden, ohne diese Information an anderer Stelle zu entfernen. Die diskutierten Beispiele demonstrieren, dass Refactoring nicht nur zur Verbesserung der Systemstruktur eingesetzt werden kann, sondern insbesondere auch, um die Pr¨asentationsform der Strukturen eines Systems besser darzustellen. Dieses Ph¨anomen ist in manchen der in [Fow99] diskutierten Refactorings ebenfalls zu beobachten, wenn zum Beispiel vorgeschlagen wird, den Namen einer Methode so zu a¨ ndern, dass er deren Aufgabe inhaltlich besser beschreibt. Refactorings haben dort aber oft gleichzeitig Auswirkungen auf Pr¨asentation und Struktur. So verbessert die Aufteilung einer Klasse deren Pr¨asentation dem Entwickler gegenuber, ¨ modifiziert aber auch die Struktur des Systems.
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
293
Die syntaktische Reichhaltigkeit der UML/P ist einer der Grunde ¨ fur ¨ den gesteigerten Bedarf an einer Verbesserung der Darstellung von Modellen. W¨ahrend sich in der Programmiersprache Java, die Variabilit¨at des Quellcodes auf die Reihenfolge der dargestellten Methoden und Attribute, Einruckungen, ¨ algebraisch a¨ quivalente Umformungen von Ausdrucken ¨ und dergleichen beschr¨ankt, lassen sich in der UML/P mehr Varianten fur ¨ die Darstellung desselben Sachverhalts finden. Das liegt zum Beispiel darin begrundet, ¨ dass die Definitionsstellen fur ¨ Attribute und Methoden nicht eindeutig festgelegt sind, sondern sich in verschiedenen Klassendiagrammen befinden konnen. ¨ Auch fur ¨ OCL-Bedingungen lassen sich im Allgemeinen eine Reihe semantisch a¨ quivalenter Darstellungen finden. Zum Beispiel konnen ¨ die Hierarchie, die Transitionen und die Zust¨ande in Statecharts durch die in Abschnitt 6.6.2, Band 1 eingefuhrten ¨ Regeln manipuliert werden. Einsatz abstrakter Klassendiagramme zur Schnittstellendefinition Die syntaktische Reichhaltigkeit der UML/P bietet einerseits den Vorteil, dass die fur ¨ jede Situation ad¨aquate, kompakte Darstellungsform gew¨ahlt werden kann, fuhrt ¨ aber andererseits zu dem hier sichtbaren Problem, dass dadurch zum Beispiel die Definitionsstelle fur ¨ ein Attribut schwerer auffindbar und daher gute Werkzeugunterstutzung ¨ notwendig ist. Dieses Problem muss durch einen geeigneten Modellierungsstandard geregelt werden. Beispielsweise hat es sich als hilfreich erwiesen, jeweils ein detailliertes Klassendiagramm fur ¨ jedes nicht weiter untergliederte Subsystem zu verwenden, in dem alle Attribute und Methoden mit ihren Signaturen aufgelistet werden. Weitere Klassendiagramme werden eingesetzt, um Zusammenh¨ange zwischen Subsystemen darzustellen. Diese enthalten nur eine Teilmenge der existenten Klassen und Assoziationen und vernachl¨assigen meistens Detailinformation. Ein Klassendiagramm kann auch, wie in [HRR98] diskutiert, als Schnittstelle fur ¨ die von außen zug¨anglichen Anteile einer Komponente eingesetzt werden. Ein solches Klassendiagramm stellt dann ebenfalls eine Abstraktion des tats¨achlichen Modells der Komponente dar, die fur ¨ einen Entwickler ausreicht, um die Komponente einzusetzen. Die verschiedenen Formen von Klassendiagrammen konnen ¨ durch systematische Manipulationen, die ebenfalls als Refactoring-Schritte bezeichnet werden konnen, ¨ voneinander hergeleitet werden. So lassen sich dafur ¨ Techniken zur Verschmelzung von Diagrammen, Migration oder Expansion von Detailinformation und in umgekehrter Richtung die Entfernung redundant vorhandener Information einsetzen. Wesentlich ist aber, dass insbesondere bei Klassendiagrammen die Einsatzform durch geeignete Stereotypen kenntlich gemacht wird. Hier eignen sich zum Beispiel die Repr¨asentationsindic “ und . . .“ zur Anzeige, ob die dargestellte Detailinformation katoren ” ” vollst¨andig oder unvollst¨andig ist.
294
9 Refactoring von Modellen
Aus pragmatischen Grunden ¨ sollte aber auch versucht werden, die Redundanz zwischen verschiedenen Repr¨asentationen desselben Sachverhalts moglichst ¨ gering zu halten. Redundanz fuhrt ¨ h¨aufig zu Inkonsistenzen, wenn ein Teil des Systems zum Beispiel durch Refactoring-Schritte modifiziert wird und kein Werkzeug eingesetzt werden kann, um diese Konsistenz ¨ automatisch zu sichern. Redundanz fuhrt ¨ dann zu erhohtem ¨ Anderungsaufwand. Andererseits ist geschickt eingesetzte Redundanz ein wesentliches Mittel zur Durchfuhrung ¨ von Konsistenztests. Dazu gehoren ¨ die Redundanz zwischen Testmodell und Implementierung, aber zum Beispiel auch ein als Schnittstelle einer Komponente bekannt gegebenes ( publiziertes‘’) Klassen” diagramm, das eine Abstraktion der Implementierung darstellt und mit dieser konsistent sein muss. 9.1.4 Refactoring in der OCL Weil die OCL seiteneffektfrei und determiniert ist, existiert ein breites Angebot an Umformungen, die auf OCL-Aussagen angewandt werden ko¨ nnen. Zu diesen Umformungen gehoren ¨ Beispiele aus Abschnitt 8.1 oder Gesetze im Umgang mit Containern, wie etwa:
Neben den algebraischen Umformungen sind vor allem die Gesetze der Logik wesentlich, um OCL-Aussagen zu modifizieren. Typisch sind etwa die Gesetze der booleschen Logik wie zum Beispiel die Kommutativit¨at:
Da die OCL in den Kontext der UML eingebettet ist, konnen ¨ viele Aussagen nur durch Bezug auf die zugrunde liegenden Modelle formuliert werden. Wir nehmen an, es sei festgelegt, dass es genau ein Objekt der Klasse AllData gibt, und dass dieses mit AllData.ad zug¨anglich ist. Deshalb ist unter dieser Kontextbedingung folgende Transformation moglich: ¨
Wie bereits in Abschnitt 8.3.6 diskutiert, hat die Mathematik eine lange Tradition in der Manipulation und korrekten Umformung von Aussagen. Durch Logik-Kalkule ¨ und algebraische Systeme wurden diese Transformationstechniken weiter verfeinert und pr¨azisiert. Heute existieren verschiede-
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
295
ne Werkzeuge, die eine pr¨azise Manipulation von Formeln erlauben. Dazu gehort ¨ zum Beispiel der auf HOL basierende Theorembeweiser [NPW02]. Eine Einbettung der OCL in HOL, wie dies in [BW02a, BW02b] diskutiert wurde, erlaubt die Transformation der OCL-Aussagen in HOL und macht den dort zur Verfugung ¨ stehenden Verifikationsapparat auf OCL anwendbar. Tats¨achlich bilden die Refactoring-Regeln auf der OCL einen Logik-Kalkul ¨ fur ¨ die OCL. Die fur ¨ eine Logik ubliche ¨ Pr¨azision der Kontextbedingungen ist dabei sehr hilfreich, wenn die Anwendung der Regeln mit automatisierten Werkzeugen unterstutzt ¨ werden soll. Auf der Syntax automatisiert prufbare ¨ Kontextbedingungen konnen ¨ entsprechend von einem Werkzeug ubernommen ¨ werden. Fur ¨ die nicht automatisiert prufbaren ¨ Kontextbedingungen konnen ¨ mehrere Strategien eingesetzt werden: •
•
•
Die Kontextbedingungen werden informell auf Plausibilit¨at gepruft. ¨ Die Korrektheit der Transformation ist damit nicht sichergestellt. Allerdings handelt es sich bei den Bedingungen zumeist um Invarianten, die in Tests eingesetzt werden konnen. ¨ Damit besteht die Chance, dass eine fehlerhaft transformierte Invariante durch einen fehlerhaften Test erkannt wird. Die Wahrscheinlichkeit, dass dies geschieht ist allerdings relativ gering, ¨ da eine Uberdeckung“ der verschiedenen Alternativen einer Invariante ” durch Tests im Allgemeinen nicht vorliegt. Dennoch ist diese pragmatische Vorgehensweise fur ¨ verschiedene Projekttypen ausreichend. Deutlich verbessert wird die Situation, wenn zus¨atzliche Tests eingesetzt werden, um die Korrektheit einer Kontextbedingung zu prufen. ¨ Das bedeutet, dass die in OCL formulierte Kontextbedingung einer Transformation fur ¨ die Anwendungsstelle selbst als Invariante angesehen und vorubergehend ¨ in den Code eingefugt ¨ wird. Durch das Vorhandensein einer guten Testsammlung beziehungsweise den Aufbau weiterer geeigneter Tests wird die Plausibilit¨at der Kontextbedingung deutlich besser gepruft. ¨ Da die Tests selbst automatisch ablaufen und daher effizient eingesetzt werden konnen, ¨ bedeutet dies nicht sehr viel Zusatzaufwand und ist daher zumindest fur ¨ kritische oder nicht ganz klare Kontextbedingungen sinnvoll. Die Verifikation der Kontextbedingungen bietet Sicherheit fur ¨ die Korrektheit der Transformation, ist aber ublicherweise ¨ nicht durchfuhrbar ¨ oder zu aufw¨andig.
Das vorgeschlagene Verfahren, Kontextbedingungen durch Tests zu pru¨ fen, wird in Abschnitt 9.2 ausgebaut, um damit Datenstrukturwechsel vorzunehmen. Die OCL ist als Spezifikationssprache im Kontext anderer UML-Diagramme konzipiert. Auch deshalb bietet die OCL nur wenig Unterstutzung ¨ fur ¨ Verifikationstechniken und es bietet sich an in der Praxis die beiden zuerst genannten Verfahren einzusetzen.
296
9 Refactoring von Modellen
9.1.5 Einfuhrung ¨ von Testmustern als Refactoring In Kapitel 7 wurden mehrere Muster beschrieben, die ein System besser fur ¨ die Definition von Tests zug¨anglich machen. Diese Muster wurden in Ergebnisform pr¨asentiert, indem die gewunschte ¨ Struktur dargestellt wurde. Oft ist das zu testende System jedoch bereits in anderer Form vorhanden und muss geeignet adaptiert werden, um Tests effektiv definieren zu konnen. ¨ Es bietet sich daher an, das System mit geeigneten Refactoring-Regeln so zu transformieren, dass es danach die vom Testmuster vorgeschlagene Struktur enth¨alt. Die folgende Refactoring-Regel fuhrt ¨ die in Tabelle 7.9 diskutierte Struktur ein, um eine statische Methode fur ¨ die Testumgebung adaptierbar zu machen. Dabei wird von einer gegebenen statischen Methode ausgegangen und diese in ein Singleton gekapselt. Refactoring: Statische Methoden fur ¨ Tests adaptierbar machen Problem Ziel
Motivation
Statische Methoden sind fur ¨ Tests unzug¨anglich, da sie nicht durch Dummies uberschrieben ¨ werden konnen. ¨ ist es, die Funktionalit¨at einer statischen Methode an ein Objekt zu delegieren, das fur ¨ Tests durch ein Dummy ersetzt werden kann, aber eine o¨ ffentlich zug¨angliche statische Variable zu vermeiden, die dieses Objekt beinhaltet. Siehe Tabelle 7.9 zur Beschreibung des Testmusters.
Refactoring Teil 1
• Die Klasse Singleton wird, wie im Diagramm beschrieben, eingefuhrt. ¨ • Die Initialisierung der Klasse Singleton wird sichergestellt. (Fortsetzung auf n¨achster Seite)
9.1 Quellen fur ¨ UML/P-Refactoring-Regeln
297
(Fortsetzung von Tabelle 9.8.: Refactoring: Statische Methoden f u¨ r Tests adaptierbar machen)
• doMethod erh¨alt dieselbe Funktionalit¨at, wie method aus OldOwner. Dabei ist gegebenenfalls auf genutzte Attribute zu achten (siehe auch Refactoring zur Migration einer Methode). • OldOwner.method delegiert nun an doMethod. Refactoring Teil 2
• Fur ¨ die Kapselung des Singleton ist es sinnvoll, die statische Methode method ins Singleton zu migrieren. • Vor dem Loschen ¨ der Methode in OldOwner sind alle Aufrufe dieser Methode anzupassen. • Die Methode getSingleton kann nun ebenfalls geloscht ¨ werden und das Singleton-Objekt ist nicht mehr o¨ ffentlich zug¨anglich. Implementierungen Beispiele
Beachtenswert
Die Implementierungen der einzelnen Methoden sind in Tabelle 7.9 zu finden. Diese Transformation wurde im Auktionssystem unter anderem verwendet, um den Datenbankanschluss und die Protokollierung zu kapseln (siehe auch Abbildung 7.8). Die Regel wurde in zwei Teile unterteilt, da der erste Teil eigenst¨andig eingesetzt werden kann. Dies empfiehlt sich, wenn das Singleton offentlich ¨ zug¨anglich bleiben soll. Wenn die Klasse OldOwner keine weiteren Aufgaben hat, so kann diese statt der neu eingefuhrten ¨ Singleton-Klasse direkt verwendet werden. Verwandt mit dieser Refactoring-Regel ist die Kapselung statischer Variablen und die Definition von Methoden zu deren Zugriff und Manipulation.
Tabelle 9.8.: Refactoring: Statische Methoden fur ¨ Tests adaptierbar machen Da dieses Muster und ein Anwendungsbeispiel bereits in Abschnitt 7.2.1 diskutiert wurden, werden statt einer vollst¨andigen Darstellung der Refactoring-Regel vereinzelt Verweise angegeben.
298
9 Refactoring von Modellen
Die anderen in Kapitel 7 definierten Testmuster lassen sich a¨ hnlich zu der obigen Refactoring-Regel darstellen. Exemplarisch wird dies in Tabelle 9.9 an der in Abschnitt 7.2.4 diskutierten Trennung der Applikation von Frameworks in Regelform vorgefuhrt. ¨ Dabei wird wieder auf die ausfuhrlichere ¨ Diskussion in Abschnitt 7.2.4 verwiesen. Refactoring: Applikation von Frameworks entkoppeln Problem, Ziel und Motivation
Wie in Abschnitt 7.2.4 beschrieben, ist ein Framework im Allgemeinen nicht fur ¨ Tests adaptierbar. Um die Applikation testbar zu machen, werden Applikation und Framework durch eine Adapter-Schicht entkoppelt. Weitere Vorteile dieser Technik sind in [SD00] zu finden.
Refactoring
• Fur ¨ jede der benutzten Framework-Klassen wird ein Adapter und ein entsprechendes Dummy eingefuhrt. ¨ Der Adapter delegiert seine Aufrufe, das Dummy wird fur ¨ Tests verwendet, hat aber keinen Kontakt zum Framework. • Die Erzeugung von Objekten des Frameworks wird in eine Factory verlagert. • Im Applikationskern werden alle Referenzen auf das Framework durch Referenzen auf die entsprechenden Adapter ersetzt. Dazu gehoren ¨ Objekterzeugung, Attribute sowie Argumente und Ergebnisse der Methodenaufrufe. Nur die Factory und die Adapter sind syntaktisch vom Framework abh¨angig. (Fortsetzung auf n¨achster Seite)
9.2 Additive Methode fur ¨ Datenstrukturwechsel
299
(Fortsetzung von Tabelle 9.9.: Refactoring: Applikation von Frameworks entkoppeln)
AdapterManagement
Die Migration der Signaturen von Adaptern kann, wie in Abbildung 7.12 gezeigt, zu Schwierigkeiten fuhren: ¨ • Wird ein Framework-Objekt als Ergebnis eines Methodenaufrufs zuruckgegeben, ¨ so ist dieses entsprechend in einen Adapter zu kapseln. • Kann dasselbe Objekt mehrfach zuruckgegeben ¨ werden, so muss fur ¨ die Kapselung derselbe Adapter verwendet werden. Eine Abbildung des Typs WeakHashMap kann diese Zuordnung speichern und erlaubt der Factory ein entsprechendes Management der Adapter.
Beispiele
Besonders bei Frameworks mit eigenem Kontrollfluss wie etwa JSP empfehlenswert. Abschnitt 7.2.4 diskutiert ein solches Beispiel. Siehe dazu die Diskussion in Abschnitt 7.2.4.
Beachtenswert
Tabelle 9.9.: Refactoring: Applikation von Frameworks entkoppeln
9.2 Additive Methode fur ¨ Datenstrukturwechsel Refactoring-Schritte sind relativ klein und systematisch, um sicherzustellen, dass die Regelanwendung beherrschbar ist und eventuell auftretende Fehler effizient erkannt und behoben werden konnen. ¨ In diesem Abschnitt wird eine Alternative diskutiert, die die Durchfuhrung ¨ komplexerer RefactoringSchritte ohne Zerlegung in Einzelschritte besser beherrschbar macht. Diese Technik eignet sich besonders fur ¨ den Wechsel von Datenstrukturen, die mit Klassendiagrammen modelliert werden. Sie basiert auf der im Auktionsprojekt entwickelten und mehrfach erfolgreich angewandten Idee, die alte und die neue Datenstruktur w¨ahrend der Umformung parallel zu nutzen und durch geeignete Invarianten miteinander in Beziehung zu setzen. Weil dabei zun¨achst die neue Datenstruktur hinzugefugt ¨ wird, ohne die alte zu entfernen, wird die Vorgehensweise als additiv bezeichnet. Nachfolgend wird zun¨achst die Vorgehensweise vorgestellt und dann anhand zweier Beispiele demonstriert und im Detail diskutiert. 9.2.1 Vorgehensweise fur ¨ den Datenstrukturwechsel Refactoring-Schritte sind dazu geeignet, einen Datenstrukturwechsel vorzunehmen und dabei die Korrektheit der Modifikation soweit wie moglich ¨ durch Tests sicherzustellen. In formaleren Ans¨atzen wie [BBB+ 85] sind gerade Datenstrukturwechsel eine gut verstandene Vorgehensweise, deren Kon-
300
9 Refactoring von Modellen
textbedingungen mit Verifikationstechniken sichergestellt werden. In Anlehnung an die dort entwickelten Konzepte, wird in Tabelle 9.10 eine pragmatische Vorgehensweise zum Wechsel einer Datenstruktur vorgeschlagen. Die Verwendung von Invarianten zur Festlegung der Beziehungen zwischen der alten und der neuen Datenstruktur ist dabei ein wesentlicher Baustein der korrekten Transformation. Additive Vorgehensweise zum Datenstrukturwechsel Problem, Ziel und Motivation
Das Zerlegen eines Datenstrukturwechsels in viele kleine Refactorings ist nicht immer einfach und birgt durch die großere ¨ Anzahl an Schritten das Risiko des Einbaus eines Fehlers. Ziel ist es, unter Verwendung von Invarianten, die die alte und die neue Datenstruktur in Beziehung setzen, großere ¨ Refactoring-Schritte beherrschbar zu machen und zus¨atzliche Sicherheit bei der Durchfuhrung ¨ zu erhalten.
Vorgehensweise
Die Durchfuhrung ¨ eines Datenstrukturwechsels mit Refactoring-Techniken besteht aus folgenden Schritten: 1. Identifikation der zu ersetzenden, alten Datenstruktur. 2. Entwicklung der neuen Datenstruktur, der zugehorigen ¨ Methoden und notwendigen Tests, die zur vorhandenen, alten Datenstruktur hinzugefugt ¨ werden. Das vorhandene System wird nicht ver¨andert, bleibt also in seiner Funktionalit¨at erhalten. Dies wird durch die Tests gepruft. ¨ 3. Definition von Invarianten, die beide Datenstrukturen in Beziehung setzen. 4. An allen Stellen, an denen die alte Datenstruktur besetzt oder ver¨andert wird, wird nun zus¨atzlich die neue Datenstruktur besetzt beziehungsweise modifiziert. Nach jeder dieser Modifikationsstellen werden die entsprechenden Invarianten zur Prufung ¨ eingefugt. ¨ Dadurch wird die neue Datenstruktur in den Systemablauf einbezogen, ohne bereits am Verhalten beteiligt zu sein. Dies wird durch die Tests gepruft. ¨ 5. Nun werden alle Stellen, die die alte Datenstruktur nutzen, auf die neue Datenstruktur umgestellt. Dies wird durch die Tests gepruft. ¨ 6. Durch den Umbau lassen sich normalerweise einige Teile algebraisch umformen und dadurch vereinfachen. Das Ergebnis wird durch die Tests gepruft. ¨ 7. Am Ende wird die nun nicht mehr benutzte, alte Datenstruktur entfernt. Das System l¨auft wie gewohnt. Dies wird durch die Tests gepruft. ¨ (Fortsetzung auf n¨achster Seite)
9.2 Additive Methode fur ¨ Datenstrukturwechsel
301
(Fortsetzung von Tabelle 9.10.: Additive Vorgehensweise zum Datenstrukturwechsel)
Beispiele
Beachtenswert
In den Abschnitten 9.2.2 und 9.2.3 wird die Vorgehensweise anhand zweier Beispiele aus dem Auktionssystem demonstriert. • Schritt 5 und 6 werden oft verschr¨ankt durchgefuhrt. ¨ • Sind einzelne Schritte komplex, dann empfiehlt sich die Durchfuhrung ¨ von Tests auch zwischendurch. • Tests sind oft von den Umstellungen mit betroffen und konnen ¨ in Schritt 5 ebenfalls umgeformt werden. Dabei konnen ¨ Tests auch obsolet werden oder die Notwendigkeit zur Definition zus¨atzlicher Tests entstehen.
Tabelle 9.10.: Additive Vorgehensweise zum Datenstrukturwechsel Der Trick bei dieser Vorgehensweise besteht darin, im Gegensatz zu einem Verifikationsansatz die Invarianten in Tests einzusetzen. Unter der Annahme, dass eine ausreichende Testsammlung fur ¨ das System existiert, kann die Korrektheit der Transformation so mit hoherer ¨ Wahrscheinlichkeit sichergestellt werden. Da die zur Prufung ¨ des Datenstrukturwechsels notwendigen Tests in der in diesem Buch vorgeschlagenen Vorgehensweise bereits existieren, ist der Datenstrukturwechsel effizient durchfuhrbar. ¨ Tats¨achlich wurde dieses Prinzip im Auktionsprojekt mehrfach mit außerordentlichem Erfolg eingesetzt und der fur ¨ komplexere Datenstrukturwechsel gesch¨atzte Aufwand drastisch unterschritten, weil systematisch vorgegangen wurde, dabei wenig Fehler entstanden sind und diese sehr schnell identifiziert, lokalisiert und behoben werden konnten. Ist das Zutrauen in die Korrektheit der Transformation immer noch nicht ausreichend, so kann wie bereits in Abschnitt 9.1.4 diskutiert nach Durchfuh¨ rung der Tests zus¨atzlich eine Verifikation zum Beispiel mit einer der HoareLogik nachempfundenen Vorgehensweise vorgenommen werden. Das in Tabelle 9.10 skizzierte Vorgehen zum Wechsel einer Datenstruktur ist in Abh¨angigkeit der tats¨achlichen Komplexit¨at und Form der Datenstruktur jeweils geeignet anzupassen. Werden zum Beispiel Elemente der alten Datenstruktur als Methodenparameter eingesetzt, so ist durch Erweiterung der Methodenparameter (Schritt 2) die neue Datenstruktur parallel hinzuzufugen. ¨ Dadurch entsteht die Moglichkeit, ¨ in den Vorbedingun¨ gen derartiger Methoden die Ubereinstimmung beider Datenstrukturen zu prufen ¨ (Schritt 3). In einem weiteren Refactoring-Schritt werden am Ende die nicht mehr benotigten ¨ Methodenparameter der alten Datenstruktur entfernt (Schritt 7). Neben der Entfernung der alten Datenstruktur in Schritt 7 zeigen sich bei der Vereinfachung der Berechnungen in Schritt 6 die Vorteile der neuen Datenstruktur. Naturlich ¨ lassen sich die Schritte 5 und 6 auch verschr¨ankt
302
9 Refactoring von Modellen
durchfuhren. ¨ Um die Beziehungen zwischen beiden Datenstrukturen effektiv beschreiben zu konnen, ¨ kann es sinnvoll sein, vorubergehend ¨ zus¨atzliche ¨ Methoden zur Ubersetzung zwischen den Datenstrukturen zu verwenden und diese am Ende ebenfalls zu entfernen. Obwohl sich diese Vorgehensweise bei nahezu allen Refactoring-Regeln, wie zum Beispiel dem Verschieben eines Attributs oder der Teilung einer Klasse, einsetzen l¨asst, bietet sich diese Technik vor allem fur ¨ großere ¨ Refactorings an. Nachfolgend wird anhand zweier Beispiele aus dem Auktionsprojekt ausschnittsweise gezeigt, wie diese Vorgehensweise angewandt werden kann. 9.2.2 Beispiel: Darstellung von Geldbetr¨agen In einer ersten Version des Auktionssystems wurden Geldbetr¨age durch eine Zahl des Datentyps long dargestellt. Fur ¨ die Internationalisierung war das Systems auf Money-Objekte umzustellen, die unterschiedliche W¨ahrungen innerhalb einer Auktion verarbeiten konnen. ¨ Nachfolgend wird eine vereinfachte Form der Datenstrukturen umgestellt.2 Schritt 1: Die Ausgangsdatenstruktur wird, wie in Abbildung 9.11 dargestellt, identifiziert.
Abbildung 9.11. Ursprungliche ¨ Datenstruktur fur ¨ die Darstellung von Geldbetr¨agen
Schritt 2: Die neue Datenstruktur ist in Abbildung 9.12 dargestellt. Fur ¨ die neue Klasse Money werden außerdem geeignete Tests entwickelt, die die von der Klasse angebotenen Funktionen ausreichend testen.
Abbildung 9.12. Erweiterte Datenstruktur fur ¨ die Darstellung von Gebotswerten 2
Tats¨achlich werden im Auktionssystem verschiedene W¨ahrungen, mengenbezogene Preise wie Euro/kg“, Nachkommastellen und variabel große Slots ebenfalls ” berucksichtigt. ¨
9.2 Additive Methode fur ¨ Datenstrukturwechsel
303
Schritt 3: Invarianten zwischen beiden Datenstrukturen sind leicht zu identifizieren. Mithilfe der zur Verfugung ¨ gestellten Query valueInCent kann als Invariante festgelegt werden: context Auction a inv BestBidEqualsCurrentBid: currentBidInCent == bestBid.valueInCent()
OCL
Entsprechend der in der UML/P verwendeten zweiwertigen Logik fur ¨ OCL und dem Umgang mit undefinierten Werten, ist die Invariante genau dann erfullt, ¨ wenn die Assoziation bestBid besetzt ist und das MoneyObjekt den entsprechenden Inhalt besitzt. Schritt 4: Die neue Datenstruktur ist zwar eingefuhrt, ¨ wird aber noch nicht eingesetzt. In diesem Schritt wird sie deshalb an allen Stellen besetzt beziehungsweise modifiziert, an denen dies auch mit der alten Datenstruktur geschieht. Ein Auszug aus der Methode, die ein Gebot annimmt und das neue Bestgebot berechnet, ist in Abbildung 9.13 angegeben.
Abbildung 9.13. Besetzung der neuen Datenstruktur
Da die gezeigte Methode das aktuelle Gebot als Argument erh¨alt, wird ein zweites Argument fur ¨ die neue Darstellung des Gebots eingefuhrt. ¨ Die erste OCL-Bedingung ist aus der Invariante BestBidEqualsCurrentBid abgeleitet und sichert die Korrektheit der Argumente. Die anderen beiden OCL-Bedingungen testen die Invariante zu Beginn der Methode und nach der Ver¨anderung des Money-Objekts. Schritt 5: Die neue Datenstruktur wird nun besetzt, aber es wird noch immer die alte benutzt. Deshalb werden jetzt alle Stellen ersetzt, die die alte Datenstruktur verwenden. Dazu konnen ¨ oft die Invarianten eingesetzt werden. In diesem Fall kann die unter Schritt 3 formulierte Invariante BestBidEqualsCurrentBid direkt als Ersetzungsanweisung verstanden werden. Die linke Seite der Gleichung currentBidInCent == bestBid.valueInCent()
OCL
304
9 Refactoring von Modellen
kann an allen benutzenden Stellen durch die rechte Seite der Gleichung ersetzt werden. Abbildung 9.14 zeigt dies an der Weiterentwicklung des Ergebnisses aus Schritt 4 (Abbildung 9.13).
Abbildung 9.14. Verwendung der neuen Datenstruktur in Java-Code
Diese Transformation ist auch fur ¨ die OCL-Bedingungen sinnvoll. Zum Beispiel kann die Spezifikation der Methode setNewBestBid entsprechend Abbildung 9.15(a) zun¨achst zu der in (b) dargestellten Form erweitert und dann zu der Fassung in (c) transformiert werden. Bei diesen Transformationen ist allerdings zu beachten, dass die Invarianten, die fur ¨ die Darstellung des Zusammenhangs der alten mit der neuen Datenstruktur eingesetzt werden, nicht ebenfalls ersetzt werden.
Abbildung 9.15. Verwendung der neuen Datenstruktur in OCL-Bedingungen
Dieses Beispiel zeigt auch, dass die Umsetzung nicht immer vollig ¨ schematisch ablaufen kann. In diesem Fall wird zum Beispiel ein Attribut durch einen Methodenaufruf ersetzt und der Operator @pre ist nicht mehr anwendbar. Deshalb wird der entsprechende Wert in einer let-Variable zwischengespeichert. Schritt 6: Die Vereinfachung der entstandenen Codestucke ¨ und insbesondere der Ausdrucke ¨ ist ein wesentlicher Schritt, um den entstandenen Code
9.2 Additive Methode fur ¨ Datenstrukturwechsel
305
lesbar und elegant zu halten. Tats¨achlich werden die Schritte 5 und 6 oft verschr¨ankt durchgefuhrt. ¨ Schritt 6 kann aber bei großen Datenstrukturwechseln auch in mehrere kleine Schritte aufgeteilt werden. Beispielsweise gilt die Ersetzung:
Ebenso konnen ¨ Berechnungen vereinfacht werden, wobei bei der nachfolgenden Transformation darauf zu achten ist, dass Seiteneffekte entstehen, wenn das alte bid1-Objekt noch an anderer Stelle bekannt ist:
Wesentlich ist hier, wie auch bei allen anderen Schritten, dass danach die automatisierten Tests durchgefuhrt ¨ werden. Sollten nicht ausreichend Tests vorhanden sein, so sind bei Bedarf zus¨atzliche Tests zu entwickeln. Schritt 7: Im letzten Schritt lassen sich nun die alte Datenstruktur sowie alle Invarianten und Bedingungen, die die alte und neue Datenstruktur in Beziehung setzen, entfernen. Es entsteht das in Abbildung 9.16 gezeigte Ergebnis.
Abbildung 9.16. Ergebnis des Datenstrukturwechsels
Fur ¨ das gezeigte einfache Beispiel ist die verwendete Methode relativ komplex und aufw¨andig. Die hier vorgeschlagene, sehr detaillierte Methode empfiehlt sich erst, wenn der Wechsel komplexer und damit fehleranf¨alliger wird. Dabei muss es sich nicht notwendigerweise um komplexe Datenstrukturen handeln. Es ist auch bereits hilfreich, diese Technik anzuwenden,
306
9 Refactoring von Modellen
wenn viele Attribute des Typs long durch Money-Objekte ersetzt werden sollen und die Komplexit¨at damit durch die Menge zu ersetzender Elemente entsteht. 9.2.3 Beispiel: Einfuhrung ¨ des Chairs im Auktionssystem Das additive Verfahren wurde im Auktionssystem in dieser Detailliertheit zum ersten Mal angewandt, als die Anforderungen auftraten, (1) dass ein Bieter an mehreren Auktionen gleichzeitig teilnehmen kann, und (2) dass ein Kollege, der eine Auktion beobachtet, nicht notwendigerweise vom gleichen Unternehmen wie der Bieter stammen muss. Anforderung (1) war eigentlich von Beginn an bekannt, wurde jedoch nicht sofort umgesetzt, da aufgrund der kurzen Laufzeit von Auktionen mehrere parallele Auktionen fur ¨ denselben Bieter zun¨achst unwahrscheinlich waren. Dies a¨ nderte sich, als zeitlich synchronisierte Auktionen a¨ hnlicher Guter ¨ zur Verbesserung der Konkurrenzsituation gewunscht ¨ wurden. Schritt 1: Identifikation der alten Datenstruktur Das Auktionssystem war von Anfang an dafur ¨ ausgelegt, neben den aktiven Bietern und dem Auktionator, den Kunden weitere, in Anhang D, Band 1 beschriebene Rollen fur ¨ Beobachter anzubieten. Dabei waren mehrere Varianten externer Beobachter zugelassen. So genannte Bieter-Kollegen erhalten alle Informationen des eigentlichen Bieters, haben aber nicht die Moglichkeit ¨ zur Gebotsabgabe. Die Erkennung von Kollegen wurde uber ¨ ein gemeinsames Company-Objekt realisiert. Anforderung (2) stammt aus der Erkenntnis, dass große Unternehmen unterschiedliche Standorte und Subunternehmen haben, externe Consultants als Bieter anstellen, etc. und deshalb eine Flexibilisierung notwendig war. Das beschriebene Beispiel wurde mit der in diesem Abschnitt skizzierten Vorgehensweise sehr effizient und fehlerfrei umgesetzt. Dies ist umso erstaunlicher, als aufgrund der zentralen Bedeutung der ge¨anderten Systemstruktur nicht nur der Applikationskern, sondern auch die Datenbank, die Ergebnissicherheit, das System zum Aufsetzen von Auktionen und die graphische Oberfl¨ache bis hin zum auf der Firmenzugehorigkeit ¨ basierenden Passwort-geschutzten ¨ Anmeldeverfahren anzupassen waren. Dies implizier¨ te aber auch die Anderung einer Reihe von Unit- und Akzeptanztests. In Abbildung 9.17 wird ein vereinfachter Ausschnitt der Ausgangssitua¨ tion zur Anderung des Applikationskerns dargestellt. Die letzte OCL-Bedingung SameInfos zeigt, dass mehrere identische Informationen, wie das aktuelle eigene Gebot, das zur Darstellung verwendete Symbol, etc. redundant gepeichert wurden.
9.2 Additive Methode fur ¨ Datenstrukturwechsel
// Nur ein Bieter pro Unternehmen context Person p1,p2 inv OneBidderOnly: p1.company==p2.company implies !p1.isBiddingAllowed || !p2.isBiddingAllowed // Personen desselben Unternehmens sind in der gleichen Auktion context Person p1,p2 inv SameAuction: p1.company==p2.company implies p1.auction==p2.auction // Personen desselben Unternehmens haben // gleiches Symbol und Eigengebot context Person p1,p2 inv SameInfos: p1.company==p2.company implies p1.graphSymbol==p2.graphSymbol && p1.ownBid==p2.ownBid
307
OCL
OCL
OCL
Abbildung 9.17. Ausgangssituation mit Invarianten
Schritt 2: Entwicklung der neuen Datenstruktur Als erwunschte ¨ Datenstruktur wurde die Situation in Abbildung 9.18 identifiziert, in der die Rollen nicht mehr durch ein Flag, sondern durch Unterklassen festgelegt werden. Außerdem wurde die Abstraktion Chair als Metapher fur ¨ den Stuhl einer Person in einer klassischen Auktion eingefuhrt. ¨ Die ursprunglichen ¨ Bedingungen OneBidderOnly und SameInfos fallen weg. SameAuction wird zu ChairSameAuction. Neu eingefuhrt ¨ wurden die Invarianten ChairAssoc1 und ChairAssoc2, die die Rolle der Klasse Chair in Bezug auf die Assoziation zwischen Person und Auction demonstrieren. Gemeinsam mit der Einfuhrung ¨ der Chair-Klassen wurden eine Reihe von neuen Tests fur ¨ die neue Datenstruktur entwickelt, die hier aber nicht dargestellt werden. Schritt 3: Festlegung der Invarianten Die beiden Klassendiagramme in den Abbildungen 9.17 und 9.18 zeigen jeweils nur Teile der Implementierung, die sich in den Klassen und einer Assoziation uberlappen. ¨ Die Klassendiagramme werden jetzt gemeinsam als Implementierung genutzt, indem, wie in Abschnitt 3.4, Band 1 beschrieben, eine Verschmelzung der Diagramme zur Codegenerierung eingesetzt wird. Darauf aufbauend lassen sich nun die notwendigen Invarianten zwischen der alten und der neuen Datenstruktur identifizieren. Dabei wird zun¨achst weiter davon ausgegangen, dass Personen nur an einer Auktion teilnehmen, denn die Tests sind fur ¨ die alte Datenstruktur ausgelegt:
308
9 Refactoring von Modellen
class BidderChair { Java/P isBiddingAllowed() {return true;} getSymbol() {return graphSymbol;} } class FellowChair { // Keine Gebotsabgabe, ansonsten Delegation isBiddingAllowed() {return false;} getSymbol() {return bidderChair.getSymbol();} } class Guest { isBiddingAllowed() {return false;} getSymbol() {return Symbol.GUEST WITHOUT OWN BIDS;} } // FellowChair und BidderChair sind in der gleichen Auktion OCL context FellowChair cc inv ChairSameAuction: cc.auction == cc.bidderChair.auction // Assoziation Auction - Person - Chair stimmt OCL context Auction a inv ChairAssoc1: forall p in a.person: p.chair[a].auction==a context Person p inv ChairAssoc2: forall a in p.chair.keySet(): p.chair[a].auction==a Abbildung 9.18. Zielstruktur mit Invarianten
context Person p inv: // zun¨achst nur ein Chair fur ¨ jede Person p.chair.size==1;
OCL
Dementsprechend ist any p.chair das eindeutige Chair-Objekt, das einer Person zugeordnet ist. Damit lassen sich einige Invarianten identifizieren, die den Transfer der Informationen vom Person- zum Chair-Objekt betreffen.
9.2 Additive Methode fur ¨ Datenstrukturwechsel
309
context Person p inv PersonChairInvs: OCL let Chair c = any p.chair in // Bieter hat BieterChair ( p.role==IS SUPPLIER && p.isBiddingAllowed <=> c instanceof BidderChair ) && // Bieter-Kollege hat FellowChair ( p.role==IS SUPPLIER && !p.isBiddingAllowed <=> c instanceof FellowChair ) && // isBiddingAllowed stimmt uberein ¨ p.isBiddingAllowed == c.isBiddingAllowed() && // Symbol stimmt uberein ¨ p.graphSymbol == c.getSymbol()
Folgende Eigenschaften gelten zus¨atzlich, sind aber separat dargestellt, um sie einzeln benennen zu konnen: ¨ context Person p inv BidderChairInv: let Chair c = any p.chair in // Gebot stimmt bei Bietenden uberein ¨ typeif c instanceof BidderChair then p.ownBid == c.ownBid else true context Person p inv FellowChairInv: let Chair c = any p.chair in // Gebot stimmt bei Bieter-Kollegen uberein ¨ typeif c instanceof FellowChair then p.ownBid == c.bidderChair.ownBid else true
OCL
OCL
Die Verbindung des Bieter-Kollegen zum zugehorigen ¨ Bieter wird uber ¨ einen Link organisiert. Wenn Person p1 bieten darf und Person p2 einen Bieter-Kollegen derselben Company darstellt, dann muss der Link entsprechend gesetzt sein: context Person p1, Person p2 inv: let BidderChair c1 = (BidderChair) any p1.chair; FellowChair c2 = (FellowChair) any p2.chair in defined(c1) && defined(c2) && p1.company==p2.company implies c2.bidderChair==c1
OCL
Die neue Datenstruktur ist genugend ¨ komplex, um die Fehlerfreiheit w¨ahrend der Entwicklung der neuen Datenstruktur und der Invarianten illusorisch zu machen. Aber durch den effektiven Einsatz von Syntaxprufun¨ gen und automatisierten Tests lassen sich die so entstandenen Modelle gegenseitig prufen. ¨ Fehler werden aufgrund der mehrfachen Redundanz der dargestellten Systemeigenschaften
310
1. 2. 3. 4.
9 Refactoring von Modellen
in den automatisierten Tests, in der alten als korrekt angenommenen Datenstruktur, in der neuen Datenstruktur sowie durch die Verbindung beider Datenstrukturen durch Invarianten erkannt.
Schritt 4: Besetzung der neuen Datenstruktur Im n¨achsten Schritt wird an allen Stellen der Code zur Besetzung der neuen Datenstruktur eingebaut und dabei werden diese Invarianten benutzt, um die Korrektheit des neuen Codes zu prufen. ¨ Da die Invarianten bereits vorhanden sind, konnen ¨ diese als anleitende Spezifikation eingesetzt werden, die beschreiben wie die Implementierung anzupassen ist. Zum Beispiel kann aus p.graphSymbol==c.getSymbol() die Implementierung ¨ fur ¨ getSymbol() extrahiert werden. Dadurch werden die Uberlegungen bei der Definition der Invarianten wiederverwendet und so die Effizienz der Entwicklung gesteigert. Wenn die Invarianten allerdings zur Ableitung der Implementierung eingesetzt werden, dann werden fehlerhafte Invarianten nicht erkannt, sondern wirken sich im Gegenteil auch durch eine fehlerhafte Implementierung aus. Es ist daher fallbasiert zu uberlegen, ¨ die Implementierung unabh¨angig von den Invarianten durchzufuhren, ¨ denn es existieren fur ¨ die alte Datenstruktur Tests, die sp¨ater auf die neue Datenstruktur umgesetzt werden und dann die Korrektheit der neuen Datenstruktur prufen. ¨ Die Besetzung der neuen Datenstruktur wird in Abbildung 9.19 exemplarisch an der Methode zur Speicherung eines Gebots bei der Person demonstriert. Die Form der hier benutzten Gebote ist bereits in Anhang D, Band 1 beschrieben. Schritte 5 und 6: Einbau der neuen Datenstruktur/Optimierung In Kombination mit Schritt 6 erlaubt der Schritt 5 einen stufenweisen Umbau des Systems und dessen Optimierung. Eine konservative Vorgehensweise ist es, zun¨achst alle bisherigen Methoden weiter anzubieten, um so der Umgebung der modifizierten Datenstruktur die bisherigen Schnittstellen weiterhin zur Verfugung ¨ zu stellen. Sinnvoll ist es aber oft auch, zu prufen, ¨ wo Optimierungen zum Beispiel durch Expansion von Methoden gunstig ¨ sind. Die konservative Vorgehensweise l¨asst sich an dem einfachen Beispiel der get- und set-Methoden demonstrieren. In den Abschnitten 3.2.2 und 4.1 ist beschrieben, wie aus einem Attribut des Klassendiagramms bei der Codegenerierung diese get/set-Methoden generiert werden. Wird das Attribut verschoben, so werden die zugehorigen ¨ Methoden nicht mehr generiert. Wurden diese Methoden jedoch anderweitig genutzt, so kann eine manuelle Definition dieser Methoden zur Verfugung ¨ gestellt werden. Beispielsweise bildet
9.2 Additive Methode fur ¨ Datenstrukturwechsel
311
Abbildung 9.19. Besetzung der neuen Struktur hinzugefugt ¨ class Person { Money getOwnBid(Auction a) { return this.chair.get(a).getOwnBid(); }}
Java/P
einen geeigneten Ersatz, der auf der neuen Datenstruktur basiert. Die explizite Definition dieser Methode kann auch dazu genutzt werden, die ansonsten standardm¨aßig generierte Methode zu uberschreiben. ¨ Deshalb kann mit solchen expliziten Definitionen von get/set-Methoden in sehr einfacher und eleganter Weise der Zugriff von Attributen der alten Datenstruktur auf die neue Datenstruktur umgelenkt werden. Diese konservative Umsetzung ist zun¨achst geeignet, um die Korrektheit der Umsetzung mit den vorhandenen Tests zu prufen. ¨
Abbildung 9.20. Vereinfachung und Entfernung der alten Datenstruktur
312
9 Refactoring von Modellen
Schritt 7: Entfernung der alten Datenstruktur Die alte Datenstruktur und mit ihr alle unnotigen ¨ Invarianten werden nun entfernt.3 Abbildung 9.20 zeigt das Ergebnis fur ¨ die Methode receiveBid. Anpassung der Tests an die neue Datenstruktur Naturgem¨aß sind nach einem Refactoring-Schritt nicht mehr alle Tests kor¨ rekt. Oft scheitert bereits die Ubersetzung eines Tests, weil die aufgerufenen Methoden oder beobachteten Attribute nicht mehr vorhanden sind. Die nach dem Schritt 4 vorhandene doppelte Darstellung der Datenstrukturen kann nun in Schritt 5 genutzt werden, um die vorhandenen Tests zu migrieren. Dabei konnen ¨ Tests, die gegen abstrakte Schnittstellen programmiert wurden unter Umst¨anden ganz ohne oder mit einfachen Transformationen auskommen. Im Gegensatz zum Produktionscode/-modell ist es normalerweise nicht notwendig, fur ¨ die Optimierung von Tests in Schritt 6 Ressourcen aufzuwenden. Es ist ausreichend die Tests lauff¨ahig und aussagekr¨aftig zu ¨ halten. Uberfl ussige ¨ Tests konnen ¨ entfernt werden. Ein Test besteht aus verschiedenen UML-Diagrammarten. Objektdiagramme werden zum Beispiel fur ¨ die Darstellung des Testdatensatzes und des Sollergebnisses eingesetzt. Die additive Vorgehensweise fuhrt ¨ dazu, dass Objektdiagramme zun¨achst ebenfalls zu erweitern sind. Das in Abbildung 9.21 dargestelle Objektdiagramm zeigt zum Beispiel einen Ausschnitt eines Testdatensatzes, bei dem bereits die neuen Datenstrukturen hinzugefugt ¨ wurden. Konstruktiv eingesetzte Objektdiagramme mussen ¨ eine vollst¨andige Darstellung der Testdaten beinhalten und sind deshalb bei einem Datenstrukturwechsel fast immer betroffen. Demgegenuber ¨ erweisen sich als Pr¨adikate eingesetzte Objektdiagramme im additiven Verfahren als relativ stabil, weil sie von dem neu hinzugekommenen Anteil oft nicht betroffen sind. In ganz a¨ hnlicher Weise konnen ¨ Sequenzdiagramme im additiven Verfahren systematisch umgebaut werden. Einem Sequenzdiagramm werden die neuen Interaktionen hinzugefugt, ¨ soweit sie von dem dadurch beschriebenen Test beobachtet werden sollen. Wenn eine relativ freie Interpretation der Beobachtung im Sequenzdiagramm zum Beispiel durch den Stereotyp match:free gew¨ahlt wurde, dann mussen ¨ diese neuen Interaktionen nicht in das Diagramm aufgenommen werden und das Diagramm kann unver¨andert weiter verwendet werden. Abbildung 9.22 zeigt die Beobachtung der Interaktion von der Auktion mit den beteiligten Personen in Bezug auf die Verteilung der Nachricht fur ¨ das neue Gebot, in der die Interaktion mit den neuen Chair-Objekten berucksichtigt ¨ wird. 3
Im Auktionssystem wurde die Klasse Company allerdings fur ¨ den eigentlichen Zweck, der Adressdarstellung entsprechend umgebaut und fur ¨ das Management durch unternehmenseigene Administratoren vorgesehen.
9.2 Additive Methode fur ¨ Datenstrukturwechsel
313
Abbildung 9.21. Modifizierte Objektstruktur als Testdatensatz
Abbildung 9.22. Sequenzdiagramm pruft ¨ modifizierten Ablauf
Resumee zur additiven Vorgehensweise Zusammenfassend l¨asst sich fur ¨ die hier ausschnittsweise demonstrierten Beispiele und die zugrunde liegende additive Methode zur Durchfuhrung ¨ von Refactorings Folgendes feststellen: •
Das Zutrauen in die Korrektheit des Refactorings wird nicht nur durch die vorhandenen, auf der alten Datenstruktur beruhenden Tests, die auf die neue Datenstruktur ubertragen ¨ werden, hergestellt. Vielmehr werden
314
•
•
9 Refactoring von Modellen
durch die eingesetzten Invarianten die alte und die neue Datenstruktur in Beziehung gesetzt und dadurch das Zutrauen in die Korrektheit der Transformation weiter erhoht. ¨ Dem zus¨atzlichen Aufwand, diese Invarianten zu entwickeln und vorubergehend ¨ einzubauen, steht der Vorteil gegenuber, ¨ dass durch die additive Vorgehensweise großere ¨ Transformationen als Einheit durchfuhr¨ bar werden. Es ist daher nicht notwendig, den im letzten Beispiel durchgefuhrten ¨ Datenstrukturwechsel in eine Anzahl kleinerer RefactoringRegeln zu zerlegen. Alternativ h¨atten sonst Einzelschritte ausgefuhrt ¨ werden mussen, ¨ um die Klasse Chair zun¨achst einzufuhren, ¨ die einzelnen Attribute zu migrieren, die durch Flags dargestellten unterschiedlichen Chair-Varianten durch Unterklassen zu ersetzen und schließlich die Beziehung zwischen FellowChair und BidderChair herzustellen, bevor am Ende die Company-Klasse entfernt werden kann. Wie an den Beispielen in den Abbildungen 9.21 und 9.22 illustriert, unterstutzt ¨ die additive Vorgehensweise auch die Testmigration, indem sie eine Trennung zwischen dem Hinzufugen ¨ der neuen und dem Entfernen der alten Datenelemente und Interaktionen in zwei Schritten erlaubt.
Insgesamt qualifiziert sich die additive Methode damit fur ¨ die Umsetzung von Datenstrukturwechseln als eine effektive Alternative beziehungsweise Erg¨anzung zu den in [Fow99] beschriebenen Refactoring-Regeln. Damit konnen ¨ zus¨atzlich allgemeinere Refactoring-Regeln fur ¨ UML/P entstehen, die an anderen, a¨ hnlichen Situationen wiederverwendet werden konnen. ¨ Das Prinzip ist hierbei a¨ hnlich wie bei der Entstehung von Frameworks und Entwurfsmustern. Aus einer speziellen Anwendung werden wiederverwendbare Anteile extrahiert und so verallgemeinert, dass dadurch eine allgemeine Regel entsteht. Sonderf¨alle und alternative Situationen konnen ¨ bei weiteren Anwendungen der Regel erkannt und in die Regel eingearbeitet werden.
9.3 Zusammenfassung der Refactoring-Techniken In Kapitel 8 und diesem Kapitel wurden die theoretischen Grundlagen fur ¨ transformationelle Softwareentwicklung auf Basis der UML/P untersucht und mit der Methodik zur Durchfuhrung ¨ von Refactoring-Schritten kombiniert. Statt einer Auflistung einzelner Regeln wurde diskutiert, wie aus vorhandenen Regels¨atzen, zum Beispiel fur ¨ Java, Refactoring-Regeln fur ¨ UML/P ubertragen ¨ werden konnen. ¨ Eine pragmatische, im Wesentlichen auf Invarianten basierende Erweiterung wurde durch die Beschreibung einer additiven Vorgehensweise fur ¨ den Wechsel von Datenstrukturen angegeben. Anhand von Beispielen wurde die praktische Einsetzbarkeit dieser Vorgehensweise demonstriert. Es zeigt sich, dass die UML/P gemeinsam mit den in den Kapiteln 5, 6 und 7 diskutierten Vorgehensweisen zur Testdefinition eine
9.3 Zusammenfassung der Refactoring-Techniken
315
hervorragende Sprache zur evolution¨aren Entwicklung des Produktionssystems und der Testf¨alle darstellt. Refactoring ist eine seit [Fow99] immer popul¨arer werdende Technik, deren Wurzeln in der evolution¨aren Weiterentwicklung bereits in [Opd92] dokumentiert wurden. Tats¨achlich reicht der Gedanke einer transformationellen Softwareentwicklung, die a¨ hnlich zur mathematischen Herleitung von Aussagen durchgefuhrt ¨ wird, noch sehr viel weiter zuruck ¨ [BBB+ 85, Dij76, JP03]. Erst durch [Fow99] wurde die zun¨achst sehr formal pr¨asentierte Vorgehensweise zur Transformation existenter Systeme durch praktische Beschreibungen, die an die Entwurfsmuster aus [GHJV94] angelehnt sind, einer breiteren Verwendung zug¨anglich gemacht. Als wesentlicher Erfolgsfaktor dafur ¨ l¨asst sich die Ersetzung der verifizierenden Ans¨atze zur Sicherstellung der Korrektheit einer Transformation durch automatisierte Tests identifizieren. Diese stellen zwar die Korrektheit nicht sicher, bieten aber, wie praktische Erfahrungen zeigen, einen guten Schutz vor Fehlern bei der Transformation. Die Verwendung von automatisierten Tests zur Festlegung des im Refactoring notwendigen Beobachtungsbegriffs ist eine daraus resultierende, wesentliche Errungenschaft. Zur Zeit findet eine intensive Entwicklung von Refactoring-Regeln statt und es ist unklar wieviele Regeln ein fur ¨ praktische Belange ausreichendes Portfolio zur Verfugung ¨ stellen muss. So gibt es allgemeine Prinzipien, wie Divide-Et-Impera“ oder die Einbettung“ von Methoden in allge” ” meinere Problemstellungen, die meist mit einer Erweiterung der Parameter vorgenommen wird, aus denen sich Regeln ableiten lassen. In [TDDN00] wird beispielsweise die Sprachunabh¨angigkeit von Refactoring-Regeln untersucht, indem Gemeinsamkeiten zwischen Java- und Smalltalk-Regeln extrahiert werden. Daneben gibt es auch sprachspezifische Refactoring-Regeln, wie zum Beispiel der Umgang mit Exceptions in Java oder mit den Statecharts in UML/P. Eine dritte Klasse von Refactoring-Techniken ergibt sich aus der Behandlung von Framework- oder Komponenten-spezifischen Situationen. So kann in Java zum Beispiel ein Vektor durch eine Set-Struktur ersetzt werden, wenn die Reihenfolge und die Anzahl der darin enthaltenen Elemente keine Rolle spielen. Dauerhaft erfolgreich werden Refactoring-Techniken vor allem dann, wenn die Werkzeugunterstutzung ¨ weiter ausreift. Entwicklungsumgebungen erlauben bereits eine effiziente Identifikation und Bearbeitung aller auftretenden Stellen einer Methode oder eines Attributs. Die konkrete Unterstutzung ¨ der Durchfuhrung ¨ von Refactoring-Schritten bis hin zum Vorschlag wenigstens einfacher algebraischer Vereinfachungen wird allerdings in der n¨achsten Zukunft noch einigen Aufwand fur ¨ Werkzeughersteller erfordern. Neben der derzeit stattfindenden stetigen Erweiterung des Regelsatzes ¨ speziell fur ¨ Java, eroffnen ¨ sich vor allem Fragestellungen fur ¨ die Ubertragung von Regeln zwischen Sprachen und zur besseren Fundierung der Korrektheit von Refactoring-Regeln. Dazu ist eine Pr¨azisierung der Regeln not-
316
9 Refactoring von Modellen
wendig, um sie zum Beispiel in Form von Taktiken maschinell umsetzbar und in allen Implikationen (Kontextbedingungen) verst¨andlich zu machen. W¨ahrend die Refactoring-Regeln in [Fow99] fur ¨ die manuelle Anwendung sehr hilfreich sind, sind sie doch in ihren Kontextbedingungen, Sonderf¨allen, etc. zu informell, um einer formalen Untersuchung der Korrektheit zug¨anglich zu sein.
10 Zusammenfassung und Ausblick
Das Entscheidende am Wissen ist, dass man es beherzigt und anwendet. Konfuzius
In Band 1 [Rum04c] und diesem Band 2 wurden mehrere modellbasierte Konzepte und Techniken fur ¨ das sich derzeit rasant weiter entwickelnde Portfolio der Softwaretechnik eingefuhrt, ¨ die basierend auf praktischen Erfah¨ rungen und fundierten analytischen Uberlegungen einen doppelten Brucken¨ schlag vornehmen. Zum einen werden mit Hilfe der UML theoretische Erkenntnisse fur ¨ die Praxis aufbereitet und so fur ¨ die industrielle Softwareentwicklung besser anwendbar. Zum anderen werden Konzepte agiler Methoden auf den Einsatz der UML ubertragen. ¨ Bisher wurde die UML zumeist in plangetriebenen Methoden als Grundlage fur ¨ die Definition von Meilensteinen und Entwicklungsphasen eingesetzt. Mit den in diesem Buch diskutierten Konzepten gelingt es, das Wertesystem, die Prinzipien und die Entwicklungspraktiken agiler Methoden mit der UML zu kombinieren. Die beiden nun erschienenen Bucher ¨ fuhren ¨ eine Reihe existierender Ans¨atze fort und integrieren ihre Ergebnisse. Als wesentliches Ergebnis wurde in Band 1 ein pr¨azises, fur ¨ viele Typen von Anwendungen gut verwendbares Sprachprofil der UML/P erstellt, das (1) auf weniger wichtige Konzepte verzichtet, (2) eine pr¨azisierte Erkl¨arung der Bedeutung einzelner Konstrukte bietet und (3) durch zus¨atzliche Konzepte auf den Einsatz als Programmier-, Modellierungs- und Testfalldefinitionssprache zugeschnitten ist. Die wesentlichen technischen Konzepte agiler Methoden, die die Verwendung einer Sprache direkt betreffen, sind die Codegenerierung, die Moglich¨ keit zur Definition automatisierter Tests, die Testbarkeit des generierten Codes und die Evolution der Modelle aufgrund sich ver¨andernder Anforderungen oder einer verbesserungswurdigen ¨ Softwarearchitektur. Deshalb wurden diese Techniken auf die UML/P ubertragen ¨ und dabei diskutiert, wie mit Hilfe der UML/P-Modelle automatisierte Tests defi-
318
10 Zusammenfassung und Ausblick
niert und Refactoring-Techniken auf UML/P-Modelle angewandt werden. Eine Anzahl von Testmustern, speziell fur ¨ die Verbesserung der Testbarkeit objektorientierter Software sowie fur ¨ funktionale Tests verteilter und nebenl¨aufiger Systeme wurde entworfen. Mit Hilfe dieser Tests wurde ein pr¨aziser Beobachtungsbegriff festgelegt, der als Grundlage fur ¨ RefactoringSchritte dient. Das in dieser Arbeit vorgestellte UML/P-Sprachprofil mit den darauf basierenden Techniken bildet die Basis fur ¨ eine effiziente Entwicklung. Flexibilit¨at, Effizienz und Kosten des Prozesses, Qualit¨at und Wartbarkeit des Produkts, Time-to-Market und letztendlich die Kundenzufriedenheit werden optimiert, indem nicht nur der richtige Prozess mit den dazugehorenden ¨ Entwicklungstechniken ausgew¨ahlt wird, sondern auch, indem die genutzten Notationen auf diesen Prozess zugeschnitten sind. Die vorgestellten Techniken zur Generierung von Code und Testf¨allen sowie zum transformationellen Refactoring von UML Modellen stellen eine hervorragende Grundlage fur ¨ die Model Driven Architecture“ (MDA) ” dar. MDA impliziert eine stark modellgetriebene Vorgehensweise, in der verschiedene Schichten von Modellen nacheinander entwickelt und idealerweise automatisch generiert werden. Die in der UML/P vorhandenen Konzepte, wie die explizite Markierung der Unvollst¨andigkeit ...“, oder Stereotypen ” wie match:initial zur Markierung der Pr¨azision einer durch Sequenzdiagramme gegebenen Beobachtung, erlauben es, sehr elegant Modelle auf verschiedenen Abstraktionsebenen zu entwerfen und durch Transformationen ineinander zu uberf ¨ uhren. ¨ UML/P und die in diesem Band beschriebenen Transformationen unterstutzen ¨ deshalb MDA und erweitern es sogar deutlich. MDA fordert prim¨ar “top-down“-Transformationen von abstrakten, Plattform-unabh¨angigen Modellen zu detaillierten, Plattform-abh¨angigen Modellen und zum Code. Refactoring von Modellen ist demgegenuber ¨ eher “horizontal“ angelegt: Refactoring-Techniken verbessern die Architektur eines Systems, ohne notwendigerweise die Abstraktionsebene zu verlassen. Das Zusammenspiel vertikaler und horizontaler Transformationen ist in diesem Buch durch die Kombination von Codegenerierung und Refactoring beschrieben und erfordert fur ¨ den effizienten Einsatz eine weitestgehende Automatisierung der Generierung. Dies ist, obwohl mittlerweile Stand der Kunst, mit den heutigen Werkzeugen oft noch nicht ad¨aquat zu realisieren und daher ein wesentliches Differenzierungsmerkmal bei der Werkzeugauswahl. Ausblick Mit dem in Band 1 skizzierten Vorgehensmodell und der zugrunde liegenden Notation sollte nun der geneigte Leser weitere praktische Erfahrungen sammeln. Dies kann durchaus ohne ein ausgereiftes Werkzeug erfolgen. Es
10 Zusammenfassung und Ausblick
319
hat sich bereits gezeigt, dass der Einsatz der UML/P und der darauf basierenden Konzepte fur ¨ Tests und Refactoring positive Effekte auf die Systemarchitektur und -qualit¨at hat. Auch die manuelle Umsetzung von Testmodellen verbessert das Verst¨andnis und die Effektivit¨at der Entwicklung und erhoht ¨ die Qualit¨at resultierender Tests. Aber es ist notwendig, dass Werkzeughersteller auch weiterhin intensiv daran arbeiten, uber ¨ Klassendiagramme hinaus Funktionalit¨at zur Generierung von Code und Tests anzubieten. Dabei werden allgemeine Generierungsverfahren genauso benotigt ¨ wie die Moglichkeit, ¨ bei der Generierung spezialisierte Dom¨anen, Frameworks und Komponentenarchitekturen zu adressieren. Aufgrund des Aufwands, der fur ¨ die Erstellung komfortabler graphischer Softwareentwicklungswerkzeuge notwendig ist, ist es fur ¨ eine Konzepterprobung heute oft besser, die zu realisierenden Vorhaben als Erweiterungen (Plugins) zum Beispiel fur ¨ ein verfugbares ¨ Open-Source¨ System zu erstellen. Dazu gehort ¨ beispielsweise die Frage nach Uberdeckungsmetriken von Tests fur ¨ UML/P, die Testfallgenerierung oder der Anschluss verifizierender Werkzeuge fur ¨ Refactoring-Regeln. Zum Zeitpunkt der Publikation der beiden Bucher ¨ ist die UML in der Version 1.4 publiziert und es befindet sich die UML 2.0 kurz vor dem Abschluss. Einige Elemente des UML/P-Sprachprofils werden in der UML 2.0 besser unterstutzt, ¨ aber im Allgemeinen ist die UML 2.0 mit ihren vielen Neuerung noch keineswegs als ausgereifter und stabiler Standard zu betrachten. Gerade deshalb ist die Eleganz, Einfachheit und Klarheit der UML/P, die sich auch durch die vergleichsweise kleine Beschreibung der abstrakten Syntax im Anhang von [Rum04c] widerspiegelt, von wesentlichem Vorteil gegenuber ¨ dem Sprachstandard UML 2.0, der unter zu vielen politischen Einflussen ¨ leidet. Mit der Konsolidierung durch die UML/P ist eine Basis geschaffen, auf der eine Reihe von Erweiterungen und Untersuchungen der Praktikabilit¨at moglich ¨ sind. Dazu gehort ¨ die empirische Untermauerung der Qualit¨at und Effektivit¨at der bekannten Testverfahren, fur ¨ die bisher vor allem Daten basierend auf prozeduralen Sprachen existieren. Die verwendete Entwicklungssprache hat aber wesentlichen Einfluss auf die Testcharakteristika. Auch die Anzahl und Form zielfuhrender ¨ Refactoring-Regeln und die Messung der Qualit¨at einer Softwarestruktur bedarf empirischer Untersuchungen, die fur ¨ einen UML/P-basierten Ansatz zu erheben sind. Auf Basis der UML/P konnen ¨ sehr gut Dom¨anen-spezifische Anpassungen entwickelt werden. So eignet sich die eigenschaftsorientierte Modellierungssprache OCL hervorragend zur Definition von Gesch¨aftsregeln, bei deren Erfullung ¨ jeweils bestimmte Aktionen ausgelost ¨ werden. Komplexe Systeme wie zum Beispiel SAP/R3 bieten heute eine Vielzahl von Parametern, die die Gesch¨aftslogik des Systems widerspiegeln. Statt also OCL und andere UML/P-Modelle direkt in Code zu ubersetzen ¨ und damit die getroffenen Aussagen im System zu fixieren, kann eine Interpretation der Artefakte
320
10 Zusammenfassung und Ausblick
vom System stattfinden und damit ein dynamischer Austausch w¨ahrend der Laufzeit des Systems ermoglicht ¨ werden. Eine weitere Fragestellung besch¨aftigt sich mit der Verwendung der UML/P zur Modellierung von Komponenten und deren Schnittstellen. Fur ¨ eine Komponentenschnittstelle bietet sich die Verwendung eines aus Interfaces bestehenden Klassen- oder Objektdiagramms an. Fur ¨ ein abstraktes Zustandsmodell der Komponente konnen ¨ Statecharts verwendet werden. OCLMethodenspezifikationen beschreiben die Rahmenbedingungen fur ¨ Methodenaufrufe an eine Komponente, und Sequenzdiagramme legen die erlaubten Interaktionsmuster fest. Fur ¨ Tests kann ein Komponenten-Dummy teilweise automatisch erstellt werden. Umgekehrt kann eine Komponente durch ihre Schnittstelle von außen auf Konformit¨at zu den zugesicherten Eigenschaften getestet werden. Der Handel mit Komponenten wird dann interessanter, weil durch automatisierte Tests das Zutrauen in die Korrektheit einer gekauften fremden Komponente deutlich erhoht ¨ werden kann. Die UML/P und ihre Techniken wurden in den beiden Buchern ¨ in eine agile Methode in Anlehnung an Extreme Programming eingebettet und sind damit vor allem fur ¨ kleine und mittlere Projekte konzipiert. Es bleibt zu prufen, ¨ in welcher Form die in dieser Arbeit vorgestellten Techniken fur ¨ großere ¨ Projekte zum Beispiel mit dem dafur ¨ geeigneten V-Modell kombiniert werden konnen. ¨
Literatur
[AG83]
A. Albrecht and J. Gaffney. Software Function, Source Lines of Code, and Development Effort Prediction. IEEE Transactions on Software Engineering, 9:639–648, June 1983. [AM02] K. Auer and R. Miller. Extreme Programming Applied. Playing to win. Addison-Wesley, 2002. [Ast02] D. Astels. Refactoring With UML. In Third International Conference on Extreme Programming and Flexible Processes in Software Engineering, XP2002, May 26-30, Alghero, Italy, pages 67–70, 2002. [Bal98] H. Balzert. Lehrbuch der Software-Technik. Software-Management, SoftwareQualit¨atssicherung, Unternehmensmodellierung. Spektrum Akademischer Verlag. Heidelberg, 1998. [BB00] F. Basanieri and A. Bertolino. A Practical Approach to UML-Based Derivation of Integration Tests. In Proc. 4th Intl. Software Quality Week Europe (QWE’2000), Brussels. QWE, 2000. [BBB+ 85] F.L. Bauer, R. Berghammer, M. Broy, W. Dosch, F. Geiselbrechtinger, R. Gnatz, E. Hangel, W. Hesse, B. Krieg-Bruckner, ¨ A. Laut, T. Matzner, B. Moller, ¨ F. Nickl, H. Partsch, P. Pepper, K. Samelson, M. Wirsing, and H. Wossner. ¨ The Munich Project CIP, Vol 1: The Wide Spectrum Language CIP-L. LNCS 183. Springer-Verlag, 1985. [BBK91] G. Bernot, M. Bidoit, and T. Knapik. Observational Approaches in Algebraic Specifications: a Comparative Study. Technical report Liens-91-6, Laboratoire d’Informatique de l’Ecole Normale Superieure, 1991. [BBWL01] M. Boger, T. Baier, F. Wienberg, and W. Lamersdorf. Extreme Modeling. In G. Succi and M. Marchesi, editors, Extreme Programming Examined, pages 175–189. Addison-Wesley, 2001. [Bei90] B. Beizer. Software Testing Techniques. Int. Thompson Computer Press, New York, 2nd ed., 1990. [Bei95] B. Beizer. Black Box Testing. John Wiley & Sons, New York, 1995. [BFG+ 93] M. Broy, C. Facchi, R. Grosu, R. Hettler, H. Hußmann, D. Nazareth, F. Regensburger, O. Slotosch, and K. Stølen. The Requirement and Design Specification Language S PECTRUM , An Informal Introduction, Version 1.0, Part 1. Technical Report TUM-I9312, Technische Universit¨at Munchen, ¨ 1993.
322 [BG98]
Literatur
K. Beck and E. Gamma. Test-Infected: Programmers Love Writing Tests. JavaReport, July 1998. [BG99] K. Beck and E. Gamma. JUnit: A Cook’s Tour. JavaReport, August 1999. [BHH+ 97] R. Breu, U. Hinkel, C. Hofmann, C. Klein, B. Paech, B. Rumpe, and V. Thurner. Towards a Formalization of the Unified Modeling Language. In M. Aksit and S. Matsuoka, editors, ECOOP’97 – Object Oriented Programming. 11th European Conference, Proceedings. Springer-Verlag, LNCS 1241, 1997. [BHW95] M. Bidoit, R. Hennicker, and M. Wirsing. Behavioural and Abstractor Specifications. Science of Computer Programming, 25(2):149–186, 1995. [Bin94] R. Binder. Design for Testability in Object-Oriented Systems. Communications of the ACM, 37(9):87–101, 1994. [Bin99] R. Binder. Testing Object-Oriented Systems. Models, Patterns, and Tools. Addison-Wesley, 1999. [BL01] L. Briand and Y. Labiche. A UML-based Approach to System Testing. In M. Gogolla and C. Kobryn, editors, UML2001 – The Unified Modeling Language, 4th Intl. Conference, pages 194–208, LNCS 2185. Springer, 2001. [BL02] L. Briand and Y. Labiche. A UML-based Approach to System Testing. Technical report SCE-01-01, Carleton University, 2002. [BLP01] D. Bjorklund, ¨ J. Lilius, and I. Porres. Towards Efficient Code Synthesis from Statecharts. In A. Evans, R. France, A. Moreira, and B. Rumpe, editors, Practical UML-Based Rigorous Development Methods. Workshop of the pUML-Group. October 1st, Toronto, Canada, pages 29–41, LNI P–7. GIEdition, Bonn, 2001. [BM98] J. Bezivin and P.-A. Muller. The Unified Modeling Language. UML’98 Beyond the Notation. Mulhouse. Proceedings. Springer, LNCS 1618, 1998. [BMJ01] L. Bousquet, H. Martin, and J.-M. Jezequel. Conformance Testing from UML Specifications. Experience Report. In A. Evans, R. France, A. Moreira, and B. Rumpe, editors, Practical UML-Based Rigorous Development Methods. Workshop of the pUML-Group. October 1st, Toronto, Canada, pages 43–55, LNI P–7. GI-Edition, Bonn, 2001. [Boe81] B. Boehm. Software Engineering Economics. Prentice Hall, Englewood Cliffs, 1981. [Bog99] M. Boger. Java in verteilten Systemen. Nebenl¨aufigkeit, Verteilung, Persistenz. dpunkt.verlag Heidelberg, 1999. [Bra84] W. Brauer. Automatentheorie: eine Einfuhrung ¨ in die Technik endlicher Automaten. Teubner, 1984. [Bro98] M. Broy. Informatik. Eine grundlegende Einfuhrung. ¨ Band 2. Systemstrukturen und Theoretische Informatik. 2. Auflage. Springer Verlag, 1998. [BS01a] M. Boger and T. Sturm. Tool-support for Model-Driven Software Engineering. In A. Evans, R. France, A. Moreira, and B. Rumpe, editors, Practical UML-Based Rigorous Development Methods. Workshop of the pUML-Group. October 1st, Toronto, Canada, pages 308–318, LNI P–7. GI-Edition, Bonn, 2001. [BS01b] M. Broy and K. Stoelen. Specification and Development of Interactive Systems. Focus on Streams, Interfaces and Refinement. Springer Verlag Heidelberg, 2001. [BvW98] R. Back and J. von Wright. Refinement Calculus. Springer, 1998. [BW82] F. L. Bauer and H. Wossner. ¨ Algorithmic Language and Program Development. Springer-Verlag, Berlin, 1982.
Literatur [BW02a] [BW02b]
[CCD01] [CE00] [CG98]
[Den91] [DH99]
[DHL01]
[Dij76] [DN84] [Dou98] [Dou99] [EEKR99]
[EH00]
[EHHS00]
[EKS00] [EM85] [FELR98a]
[FELR98b]
323
A. Brucker and B. Wolff. A Proposal for a Formal OCL Semantics in Isabelle/HOL. In TPHOLs 2002, LNCS. Springer-Verlag, Berlin, 2002. A. Brucker and B. Wolff. HOL-OCL Experiences, Consequences and Design Choices. In J-M. J´ez´equel and H. Hußmann, editors, UML2002 – The Unified Modeling Language: Model Engineering, Concepts and Tools, 5th Intl. Conference. Springer, LNCS, 2002. C. Crichton, A. Cavarra, and J. Davies. Using UML for Automated Test Generation. http://www.agedis.de/, 2001. K. Czarnecki and U. Eisenecker. Generative Programming. Addison-Wesley Boston, 2000. L. Cardelli and A. Gordon. Mobile Ambients. In Foundations of Software Science and Computation Structures, FoSSaCS’98, LNCS 1378, pages 140– 155. Springer Verlag, 1998. E. Denert. Software-Engineering. Springer-Verlag, 1991. B. Demuth and H. Hußmann. Using UML/OCL Constraints for Relational Database Design. In R. France and B. Rumpe, editors, UML’99 – The Unified Modeling Language: Beyond the Standard, pages 598–613, LNCS 1723. Springer, 1999. B. Demuth, H. Hußmann, and S. Loecher. OCL as a Specification Language for Business Rules in Data Base Applications. In M. Gogolla and C. Kobryn, editors, UML2001 – The Unified Modeling Language, 4th Intl. Conference, pages 104–117, LNCS 2185. Springer, 2001. E. Dijkstra. a discipline of programming. Prentice-Hall, 1976. J. Duran and S. Ntafos. An Evaluation of Random Testing. IEEE Transactions on Software Engineering, 10(7):438–444, July 1984. B. P. Douglass. Real-Time UML. Developing Efficient Objects for Embedded Systems. Addison-Wesley, 1998. B. P. Douglass. Doing Hard Time. Developing Real-Time Systems with UML, Objekcts, Frameworks, and Patterns. Addison-Wesley, 1999. H. Ehrig, G. Engels, H. Kreowski, and G. Rozenberg. Handbook of Graph Grammars and Computing by Graph Transformations, Volume 2: Applications, Languages and Tools. World Scientific, 1999. G. Engels and R. Heckel. Graph Transformation and Visual Modeling Techniques. Bulletin of the European Association for Theoretical Computer Science, 71, June 2000. G. Engels, J.-H. Hausmann, R. Heckel, and S. Sauer. Dynamic MetaModeling: A Graphical Approach to the Operational Semantics of Behavioral Diagrams in UML. In A. Evans, S. Kent, and B. Selic, editors, UML2000 – The Unified Modeling Language, 3th Intl. Conference, pages 323–337, LNCS 1939. Springer, 2000. A. Evans, S. Kent, and B. Selic. UML2000 – The Unified Modeling Language, 3th Intl. Conference. Springer, LNCS 1939, 2000. H. Ehrig and B. Mahr. Fundamentals of Algebraic Specifications I. Springer Verlag, Berlin, 1985. R. France, A. Evans, K. Lano, and B. Rumpe. Developing the UML as a Formal Modelling Notation. In J. Bezivin and P.-A. Muller, editors, The Unified Modeling Language. UML’98 Beyond the Notation. Mulhouse. Proceedings., pages 336–348, LNCS 1618. Springer, 1998. R. France, A. Evans, K. Lano, and B. Rumpe. The UML as a Formal Modeling Notation. Computer Standards & Interfaces, 19:325–334, 1998.
324 [FG99]
Literatur
M. Fewster and D. Graham. Software Test Automation. Effective Use of Test Execution Tools. ACM Press, New York & Addison-Wesley, 1999. [FGJM85] K. Futatsugi, J. Goguen, J.-P. Jouannaud, and J. Meseguer. Principles of OBJ2. In B. Reid, editor, Proceedings of 12th ACM Symposium on Principles of Programming Languages, Association for Computing Machinery, pages 52–66, 1985. [FHNS02] G. Friedman, A. Hartman, K. Nagin, and T. Shiran. Projected State Machine Coverage for Software Testing. In Proceedings of the International Symposium of Software Testing and Analysis. ISSTA’02, New York, 2002. ACM Press. [FK00] D. Fields and M. Kolb. Web Development with Java Server Pages. Manning Greenwich, 2000. [Fow99] M. Fowler. Refactoring. Improving the Design of Existing Code. AddisonWesley, 1999. [Fow00] M. Fowler. Refactoring. Wie Sie das Design vorhandener Software verbessern. Addison-Wesley, dt. fassung von [Fow99] edition, 2000. [FPR01] M. Fontoura, W. Pree, and B. Rumpe. The UML/F Profile for Framework Architecture. Addison-Wesley, 2001. [FR99] R. France and B. Rumpe. UML’99 – The Unified Modeling Language, 2th Intl. Conference. Springer, LNCS 1723, 1999. [FSJ99] M. Fayad, D. Schmidt, and R. Johnson. Building Application Frameworks. Object Oriented Foundations of Framework Design. John Wiley & Sons, 1999. [GH99] A. Gargantini and C. Heitmeyer. Using Model-Checking to Generate Tests from Requirements Specifications. In Proceedings of the 7th European Software Engineering Conference (7th ACM SIGSOFT Symposium on the Foundations of Software Engineering), FSE’99, Toulouse, France. Springer Verlag, LNCS 1687, 1999. [GHJV94] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns. Addison-Wesley, 1994. [GJSB00] J. Gosling, B. Joy, G. Steele, and G. Bracha. The Java Language Specification. Second Edition. Addison-Wesley, 2000. [GK01] M. Gogolla and C. Kobryn. UML2001 – The Unified Modeling Language, 4th Intl. Conference. Springer, LNCS 2185, 2001. [GR99] J. Goguen and G. Rosu. Hiding more of Hidden Algebra. In FM’99, LNCS 1708, pages 1704–1719, 1999. [GS02] J. Grabowski and M. Schmitt. TTCN-3 – Eine Sprache fur ¨ die Spezifikation und Implementierung von Testf¨allen. at – Automatisierungstechnik, 50(3):A5–A8, M¨arz 2002. [GWM+ 92] J. Goguen, T. Winkler, J. Meseguer, K. Futatsugi, and J.-P. Jouannaud. Introducing OBJ. Technical Report CSL-92-03, Computer Science Laboratory, SRI, March 1992. [Hal93] N. Halbwachs. Synchronous Programming of Reactive Systems. Kluwer Academic Publishers, 1993. [Har87] D. Harel. Statecharts: A Visual Formalism for Complex Systems. Science of Computer Programming, 8:231–274, 1987. [HBG01] M. Holcombe, K. Bogdanov, and M. Gheorghe. Functional Test Generation for Extreme Programming. In M. Marchesi and G. Succi, editors, Proceedings of the 2nd International Conference on Extreme Programming and Flexible Processes in Software Engineering (XP2001), May 2001.
Literatur [HDF00]
325
H. Hußmann, B. Demuth, and F. Finger. Modular Architecture for a Toolset Supporting OCL. In A. Evans, S. Kent, and B. Selic, editors, UML2000 – The Unified Modeling Language, 3th Intl. Conference, pages 278–293, LNCS 1939. Springer, 2000. [HG97] D. Harel and E. Gery. Executable Object Modelling with Statecharts. In Proceedings of the 18th International Conference on Software Engineering. IEEE Computer Society Press, 1997. [HHJ+ 87] C. Hoare, I. Hayes, H. Jifeng, C. Morgan, A. Roscoe, J. Sanders, I. Sorensen, J. Spivey, and B. Suffin. Laws of Programming. Communications of the ACM, 30(8):672–686, 1987. [HL02] R. Hightower and N. Lesiecki. Java Tools for Extreme Programming. Wiley Computer Publishing New York, 2002. [HN96] D. Harel and A. Naamad. The STATEMATE Semantics of Statecharts. ACM Transactions on Software Engineering and Methodology, 5(4):293–333, October 1996. [Hop81] G. Hopper. The first bug. Annals of the History of Computing, 3:285–286, 1981. [HP02] G. Halmans and K. Pohl. Modellierung der Variabilit¨at einer SoftwareProduktfamilie. In M. Glinz and G. Muller-Luschnat, ¨ editors, Modellierung 2002, pages 63–74. GI, 2002. [HR00] D. Harel and B. Rumpe. Modeling Languages: Syntax, Semantics and All That Stuff. Technical Report MCS00-16, The Weizmann Institute of Science, Rehovot, Israel, 2000. [HRR98] F. Huber, A. Rausch, and B. Rumpe. Modeling Dynamic Component Interfaces. In M. Singh, B. Meyer, J. Gil, and R. Mitchell, editors, TOOLS 26, Technology of Object-Oriented Languages and Systems. IEEE Computer Society, 1998. [HSSS96] F. Huber, B. Sch¨atz, A. Schmidt, and K. Spies. AutoFocus - A Tool for Distributed Systems Specification. In B. Jonsson and J. Parrow, editors, Proceedings FTRTFT’96 - Formal Techniques in Real-Time and Fault-Tolerant Systems, pages 467–470. LNCS 1135, Springer Verlag, 1996. [HT90] R. Hamlet and R. Taylor. Partition Testing does not Inspire Confidence. IEEE Transactions on Software Engineering, 16(12):1402–1411, December 1990. [HU90] J. Hopcroft and J. Ullman. Einfuhrung ¨ in die Automatentheorie, Formale Sprachen und Komplexit¨atstheorie. Addison-Wesley, 1990. [Huß97] H. Hußmann. Formal Foundations for Software Engineering Methods. LNCS 1322. Springer-Verlag, Berlin, 1997. [ISO92] ISO/IEC. Information Technology – Open Systems Interconnection – Conformance Testing Methodology and Framework – Part 3: The Tree and Tabular Combined Notation (TTCN), 1992. [IT99a] ITU-T. Message Seqeuence Chart (MSC), Recommendation Z.120 (11/99). International Telecommunication Union, 1999. [IT99b] ITU-T. SDL combined with UML, Recommendation Z.109 (11/99). International Telecommunication Union, 1999. [IT99c] ITU-T. Specification and Description Language (SDL), Recommendation Z.100 (11/99). International Telecommunication Union, 1999. [JB04] B. Rumpe J. Botaschanjan, M. Pister. Testing Agile Requirements Models. Journal of Zhejiang University, 5(5), 2004.
326 [Jon96] [JP03]
Literatur
M. P. Jones. An Introduction to Gofer, 1996. B. Rumpe J. Philipps. Refactoring of Programs and Specifications. In H. Kilov and K. Baclawski, editors, Practical foundations of business and system specifications, pages 281–297. Kluwer Academic Publishers, 2003. [JUn02] JUnit. JUnit Testframework Homepage. http://www.junit.org/, 2002. [KCM00] S. Kim, J. Clark, and J. McDermid. The Rigorous Generation of Java Mutation Operators Using HAZOP. In J.-C. Rault, editor, Proceedings of the 12th International Conference on Software & Systems Engineering and their Applications (ICSSEA’99). Bd. 4, Paris, 2000. [KFN93] C. Kaner, J. Falk, and H. Nguyen. Testing Computer Software, 2nd Edition. Thompson Computer Press, 1993. [KLM+ 97] G. Kiczales, J. Lamping, A. Mendhekar, C. Maeda, C. Lopez, J.-M. Loingtier, and J. Irwin. Aspect-Oriented Programming. In ECOOP’97 – Object Oriented Programming, 11th European Conference, Jyv¨askyl¨a, Finnland, LNCS 1241. Springer Verlag, 1997. [KPR97] C. Klein, C. Prehofer, and B. Rumpe. Feature Specification and Refinement with State Transition Diagrams. In P. Dini, editor, Fourth IEEE Workshop on Feature Interactions in Telecommunications Networks and Distributed Systems. IOS-Press, 1997. [Kru00a] P. Kruchten. The Rational Unified Process. An Introduction. Addison-Wesley, second edition, 2000. [Kru00b] ¨ I. Kruger. ¨ Distributed System Design with Message Sequence Charts. Doktorarbeit, Technische Universit¨at Munchen, ¨ 2000. [LF02] J. Link and P. Frohlich. ¨ Unit Tests mit Java. Der Test-First-Ansatz. dpunkt.verlag Heidelberg, 2002. [Lig90] P. Liggesmeyer. Modultest und Modulverifikation. B.I. Wissenschaftsverlag Mannheim, 1990. [Llo87] J. Lloyd. Foundations of Logic Programming. 2nd Edition. Springer-Verlag, Berlin, 1987. [LOO01] K. Lieberherr, D. Orleans, and J. Ovlinger. Aspect-Oriented Programming with Adaptive Methods. Communications of the ACM, 44(10):39–41, 2001. [LRW02] M. Lippert, S. Roock, and H. Wolf. Software entwicklen mit Extreme Programming. dpunkt.verlag, 2002. [Lud02] J. Ludewig. Modelle im Software Engineering – eine Einfuhrung ¨ und Kritik. In M. Glinz and G. Muller-Luschnat, ¨ editors, Modellierung 2002, pages 7–22. GI, 2002. [McL01] B. McLaughlin. Java und XML. Deutsche Ausgabe. O’Reilly, 2001. [Mey97] B. Meyer. Object-Oriented Software Construction. Prentice Hall, Englewood Cliffs, NJ, 1997. [MFC01] T. Mackinnon, S. Freeman, and P. Craig. Endo-testing: Unit Testing with Mock Objects. In G. Succi and M. Marchesi, editors, Extreme Programming Examined, pages 287–301. Addison-Wesley, 2001. [MH00] R. Monson-Haefel. Enterprise JavaBeans. O’Reilly Press, 2000. [MMPH99] P. Muller, ¨ J. Meyer, and A. Poetzsch-Heffter. Making Executable Interface Specifications More Expressive. In C. Cap, editor, JIT ’99 Java-InformationsTage 1999, Informatik Aktuell. Springer-Verlag, 1999. [Moo01] I. Moore. Jester - a JUnit test tester. In M. Marchesi and G. Succi, editors, Proceedings of the 2nd International Conference on Extreme Programming and Flexible Processes in Software Engineering (XP2001), May 2001.
Literatur
327
[MTHM97] R. Milner, M. Tofte, R. Harper, and D. MacQueen. The Definition of Standard ML (Revised). MIT Press, Cambridge, 1997. [Mye79] G. Myers. The Art of Software Testing. John Wiley & Sons, New York, 1979. [Mye00] G. Myers. Methodisches Testen von Programmen. Oldenbourg, Munchen, ¨ 7.te Auflage, 2000. [NPW02] T. Nipkow, L. Paulson, and M. Wenzel. Isabelle/HOL: A Proof Assistant for Higher-Order Logic. LNCS 2283, Springer Heidelberg, 2002. [OB88] T. Ostrand and M. Balcer. The Category-Partition Method for Specifying and Generating Functional Tests. Communications of the ACM, 31(6):676– 686, 1988. [OH98] R. Orfali and D. Harkey. Client/Server Programming with Java and CORBA. John Wiley & Sons, 1998. [OMG03] OMG. OMG Unified Modeling Language Specification. Technical report, Object Management Group (OMG), September 2003. [Opd92] W. Opdyke. Refactoring Object-Oriented Frameworks. Ph.d. thesis, University of Illinois at Urbana-Champaign, 1992. ¨ ¨ [Ove00] G. Overgaard. Formal Specification of Object-Oriented Modelling Concepts. PhD Thesis, Department of Teleinformatics, Royal Institute of Technology, Stockholm, Sweden, 2000. [Par90] H. Partsch. Specification and Transformation of Programs. A Formal Approach to Software Development. Monographs in CS. Springer-Verlag, Berlin, 1990. [Pau94] L. Paulson. Isabelle: A Generic Theorem Prover. LNCS 929, Springer-Verlag, 1994. [Pep84] P. Pepper. Program Transformation and Programming Environments. Report on a Workshop directed by F. L. Bauer and H. Remus. NATO ASI Series F, Vol. 8. Springer, 1984. [PJH+ 01] S. Pickin, C. Jard, T. Heuillard, J.-M. Jezequel, and P. Desfray. A UML-integrated Test Description Language for Component Testing. In A. Evans, R. France, A. Moreira, and B. Rumpe, editors, Practical UMLBased Rigorous Development Methods. Workshop of the pUML-Group. October 1st, Toronto, Canada, pages 208–223, LNI P–7. GI-Edition, Bonn, 2001. [PKS02] M. Pol, T. Koomen, and A. Spilner. Management und Optimierung des Testprozesses, 2te Auflage. dpunkt.verlag, 2002. [PLP01] A. Pretschner, H. Lotzbeyer, ¨ and J. Philipps. Model Based Testing in Evolutionary Software Development. In Proc. 12th IEEE Intl. Workshop on Rapid System Prototyping (RSP01), pages 155–160. IEEE Computer, 2001. [PR97] J. Philipps and B. Rumpe. Refinement of Information Flow Architectures. In M. Hinchey, editor, ICFEM’97. IEEE CS Press, 1997. [PR99] J. Philipps and B. Rumpe. Refinement of Pipe And Filter Architectures. In FM’99, LNCS 1708, pages 96–115, 1999. [Pre97] C. Prehofer. Feature-Oriented Programming: A Fresh Look at Objects. In ECOOP’97 – Object Oriented Programming, 11th European Conference, Jyv¨askyl¨a, Finnland, LNCS 1241. Springer Verlag, 1997. [Pre00] C. Prehofer. Flexible Construction of Software Components: A Feature-Oriented Approach. Habilitation Thesis, Technische Universtit¨at Munchen, ¨ May 2000. [PTLP98] S. Prowell, C. Trammell, R. Linger, and J. Poore. Cleanroom Software Engineering. Technology and Practice. SEI Series on Software Engineering. Addison-Wesley, 1998.
328
Literatur
[PYvB96]
A. Petrenk, N. Yevtushenko, and G. von Bochmann. Testing Deterministic Implementations from Nondeterministic FSM Specifications. In Proceedings of the 9th International Workshop on Testing of Communicating Systems (IWTCS’96), pages 125–140, 1996. [RDT95] T. Ramalingam, A. Das, and K. Thulasiraman. Fault Detection and Diagnosis Capabilities of Test Sequence Selection Methods Based on the FSM Model. Computer Communications, 18(2):113–122, 1995. [RFBLO01] D. Riehle, S. Fraleigh, D. Bucka-Lasse, and N. Omorogbe. The Architecture of a UML Virtual Machine. In Proceedings of the 2001 Conference on Object-Oriented Programming Systems, Languages, and Applications (OOPSLA), pages 327–341. ACM Press, 2001. [RG02] M. Richters and M. Gogolla. OCL: Syntax, Semantics and Tools. In T. Clark and J. Warmer, editors, Object Modeling with the OCL, pages 42–68, LNCS 2263. Springer Verlag, Berlin, 2002. [RLNS00] K. Rustan, M. Leino, G. Nelson, and J. Saxe. ESC/Java user’s manual. Technical Note 2000-02, Compaq Systems Research Center, Palo Alto, CA, 2000. [Roz99] G. Rozenberg. Handbook of Graph Grammars and Computing by Graph Transformations, Volume 1: Foundations. World Scientific, 1999. [RT98] B. Rumpe and V. Thurner. Refining Business Processes. In H. Kilov, B. Rumpe, and I. Simmonds, editors, Seventh OOPSLA Workshop on Precise Behavioral Semantics, I9820. Technische Universit¨at Munchen, ¨ June 1998. [Rum96] B. Rumpe. Formale Methodik des Entwurfs verteilter objektorientierter Systeme. Herbert Utz Verlag Wissenschaft, 1996. [Rum98] B. Rumpe. A Note on Semantics (with an Emphasis on UML). In Second ECOOP Workshop on Precise Behavioral Semantics. Technische Universit¨at Munchen, ¨ TUM-I9813, 1998. [Rum03] B. Rumpe. Model-Based Testing of Object-Oriented Systems. In et.al. F. de Boer, editor, Formal Methods for Components and Objects, page LNCS 2852. Springer Verlag, 2003. [Rum04a] B. Rumpe. Agile Modeling with the UML. In S. Balsamo M. Wirsing, A. Knapp, editor, Radical Innovations of Software and Systems Engineering in the Future, RISSEF 2002, page LNCS 2941. Springer Verlag, 2004. [Rum04b] B. Rumpe. Agile Modellierung mit UML. Codegenerierung, Testflle, Refactoring. Springer Verlag, 2004. [Rum04c] B. Rumpe. Modellierung mit UML. Sprache, Konzepte und Methodik. Springer Verlag, 2004. [RVR+ 01] A. Ramirez, P. Vanpeperstraete, A. Rueckert, K. Odutola, and J. Bennett. ArgoUML User Manual. Version 0.9.9. http://www.argouml.tigris.org/, 2001. [RWH01] B. Reus, M. Wirsing, and R. Hennicker. A Hoare Calculus for Verifying Java Realizations of OCL-Constrained Design Model. In FASE 2001, ETAPS, Genova, LNCS 2029, pages 300–316. Springer Verlag, 2001. ¨ [SD00] J. Siedersleben and E. Denert. Wie baut man Informationssysteme? Uberlegungen zur Standardarchitektur. Informatik Spektrum, 8/2000:247–257, 2000. [Sne96] H. Sneed. Sch¨atzung der Entwicklungskosten von objektorientierter Software. Informatik-Spektrum, 19:133–140, 1996. [Som01] I. Sommerville. Software Engineering, 6th Edition. Addison-Wesley, 2001.
Literatur [Sou01]
329
N. Soundarajan. Refactoring and Re-Reasoning. In G. Succi and M. Marchesi, editors, Extreme Programming Examined, pages 303–319. AddisonWesley, 2001. [Spi88] J. Spivey. Understanding Z. Cambridge University Press, 1988. [ST87] D. Sanella and A. Tarlecki. On Observational Equivalence and Algebraic Specification. Journal of Computer and System Sciences, pages 150–178, 1987. [Sta73] H. Stachowiak. Allgemeine Modelltheorie. Springer Verlag Wien, 1973. [SvVB02] T. Sturm, J. von Voss, and M. Boger. Generating Code for UML with Velocity Templates. In J-M. J´ez´equel and H. Hußmann, editors, UML2002 – The Unified Modeling Language: Model Engineering, Concepts and Tools, 5th Intl. Conference. Springer, LNCS, 2002. [SZ01] E. Sekerinski and R. Zurob. iState: A Statechart Translator. In M. Gogolla and C. Kobryn, editors, UML2001 – The Unified Modeling Language, 4th Intl. Conference, pages 376–390, LNCS 2185. Springer, 2001. [TB01] L. Tokuda and D. Batory. Evolving Object-Oriented Designs with Refactorings. Journal of Automated Software Engineering, 8:89–120, 2001. [TDDN00] S. Tichelaar, S. Ducasse, S. Demeyer, and O. Nierstrasz. A Meta-model for Language-Independent Refactoring. In Proceedings ISPSE 2000, IEEE, 2000. [vdB94] M. von der Beeck. A Comparison of Statecharts Variants. In H. Langmaack, W.-P. de Roever, and J. Vytopil, editors, Formal Techniques in RealTime and Fault-Tolerant Systems (FTRTFT’94), volume LNCS 863, pages 128–148. Springer-Verlag, 1994. [vdB01] M. von der Beeck. Formalization of UML-Statecharts. In M. Gogolla and C. Kobryn, editors, UML2001 – The Unified Modeling Language, 4th Intl. Conference, pages 406–421, LNCS 2185. Springer, 2001. [Viv01] F. Vivaldi. Experimental Mathematics with Maple. CRC Press, Boca Raton, Florida, 2001. [vO01] D. von Oheimb. Hoare Logic for Java in Isabelle/HOL. Concurrency and Computation: Practice and Experience, 13(13):1173–1214, 2001. [Voa95] J. Voas. Software Testability Measurement for Assertion Placement and Fault Localization. In M. Ducasse, editor, AADEBUG, 2nd International Workshop on Automated and Algorithmic Debugging, Saint Malo, France, pages 133–144. IRISA-CNRS, 1995. [W3C00] W3C. Extensible Markup Language (XML) 1.0 (2nd edition 6 October 2000). http://www.w3.org/xml, 2000. [Wil01] A. Wills. Catalytic Modeling: UML meets XP. In A. Evans, R. France, A. Moreira, and B. Rumpe, editors, Practical UML-Based Rigorous Development Methods. Workshop of the pUML-Group. October 1st, Toronto, Canada, pages 288–307, LNI P–7. GI-Edition, Bonn, 2001. [Wir71] N. Wirth. Program Development by Stepwise Refinement. Communications of the ACM, 14:221–227, 1971. [Wol99] S. Wolfram. The MATHEMATICA Book. Cambridge University Press, 1999.
Index
{Equals}, 96 {Hash}, 96 {ToStringVerbosity}, 97 {ToString}, 97 {location}, 238 Abnahmetest, 146 abstract, 11 Abstract Factory, 99 Abstraktion, 262 Abstraktionsrelation, 260, 261 Additive Methode, 299 Aktion, 36 Aktionsbedingung, 35 Akzeptanztest, 145, 268 API Codegenerierung, 64 Assoziation, 11 bidirektional, 85 Codegenerierung, 85 qualifiziert, 90 Assoziationsname, 12, 28 Assoziationsrolle, 12 Attribut, 11, 28 abgeleitet, 78 eager Berechnung, 80 lazy Berechnung, 80 Auslassung, 149 Bedingung OCL, 16 Sequenzdiagramm, 45 Beobachtung, 268, 270, 282 extern, 282
Refactoring, 266 Beobachtungsbegriff, 266, 270 Beobachtungsinvarianz, 270 Beschreibendes Modell, 54 Codegenerator, 53 Codegenerierung, 53, 77 Collection(X), 20 Datenstrukturwechsel, 299, 300 Definiertheit, 273 Deskriptives Modell, 53, 54 Determiniertheit, 273 Dummy, 161, 210 Codeerzeugung, 99 Codegenerierung, 137 Simulation von Zeit, 210 Endzustand, 35 entry-Aktion, 36 Erfolgsfall, 146 exists, 24 exit-Aktion, 36 Factory Codeerzeugung, 99 Fehler, 149 Filter Komprehension, 20 final, 11 flatten OCL, 23 forall, 24 Framework, 298
332
Index
Gemeinsamer Modellbesitz, 254 Generator Komprehension, 20 Grunddatentypen, 18 Infixoperator OCL, 18 instanceof, 17 Interaktionsmuster, 188 Interface, 11 Interface-Implementierung, 11 Interpretation einer Bedingung, 16 Invariante, 16 JUnit, 148, 162, 166 Klasse, 11 Codegenerierung, 95 Klassenattribut Objektdiagramm, 29 Komposition, 12 Codegenerierung, 93 Objektdiagramm, 30 Kompositionslink, 28 Komprehension, 20 Konstante, 11 Konstruktives Modell, 53 Konstruktives Testmodell, 53 Kontext einer Bedingung, 16 von Vor-/Nachbedingungen, 27 Kontextbedingung Transformationsregel, 272 Korrektheit, 145 funktionale, 144 Lebenszyklus, 34 Objekt, 12 Link, 28 Objektdiagramm, 30 List(X), 20 Listenkomprehension, 20 Logik Lifting, 19 Lokation, 239, 244 im Sequenzdiagramm, 237 Mangel, 149
Marken, 7 Markierungen, 7 Mengenkomprehension, 20 Merkmal, 15 {global}, 241 {local}, 241 {location}, 238 {time}, 226 Methode, 11 Codegenerierung, 82 Methodenspezifikation, 16, 27 Modell geschlossen, 264 offen, 264 Modellelement, 15 Modellierungsstandards, 254 Modelltransformation, 258, 270 Semantik, 260, 264 Modifikator, 11 Nachbedingung, 27, 35 Methode, 16 Nachricht Sequenzdiagramm, 45 Navigation, 12 Navigationsrichtung, 11, 28 Nichtdeterminismus, 35 Oberklasse, 11 Object Constraint Language, 15 Objekt, 28 Sequenzdiagramm, 45 Objektname, 28 OCL, 15 Orakelfunktion, 164 Parametrisierung Codegenerierung, 71 private, 11 protected, 11 Prototypisches Objekt, 28, 31 public, 11 Qualifikator Objektdiagramm, 30 Quantor, 24 Quellzustand, 35 Query, 16 Rapid Prototyping, 50
Index readonly, 11 Refactoring, 249, 263, 282 Framework, 298 Regelschablone, 281 Singleton, 296 Refactoring-Regel, 282 Refactoring-Schritt, 282 Robustheit, 144, 145, 153 Rollenname, 11, 12, 28 Roundtrip-Engineering, 89 Schaltbereitschaft, 35 Scheduling mit Sequenzdiagramm, 231 Scheitern, 146 Schemavariablen, 250 Codegenerierung, 74 Seiteneffekt in Tests, 215 Seiteneffektfreiheit, 273 Semantik Codegenerierung, 68, 70 Sequenzdiagramm, 44 Set(X), 20 Sicht, 14 Signatur, 11 Signaturwechsel, 263 Singleton im Test, 296 Skript, 53 Codegenerierung, 68 Snapshot, 266 Startzustand, 35 Statechart, 33 Orakel, 190 static, 11 Stereotyp, 15 trigger, 136 Stimulus, 35 Substitutionsprinzip, 11 Subtyp, 11 Systemablauf, 266 Systemmodell, 265, 266 Template, 53 Terminierung, 273 Test, 145 mit Grenzwertanalyse, 180 mit Methodenspezifikation, 177
333
mit Objektdiagramm, 172 mit OCL-Bedingung, 175 mit Sequenzdiagramm, 182 Refactoring, 267 Test-Istergebnis, 151 Test-Sollergebnis, 151 Testablauf, 151 Sequenzdiagramm, 214 Testdaten, 146, 151 Testerfolg, 151 Testfall, 151 Testling, 151 Testmodell, 53 Testmuster, 207, 208, 296 Framework, 220 Klassenbibliothek, 220 Kommunikation, 242 Objekterzeugung, 219 Scheduler, 230 Simulation von Zeit, 222, 227 Singleton, 217 Thread, 229 Thread-Erzeugung, 232 Timer, 228 Verteilung und Kommunikation, 237, 244 Testprozess, 208 Testsammlung, 151, 167 Testsequenz, 182 Testtreiber, 151, 161, 162 Codegenerierung, 136 Testurteil, 151 Testverfahren, 151 Transformation, 274 Transformationsregel Codegenerierung, 74 Transition, 35 Trigger, 183, 187 typeif, 17 Typkonstruktor OCL, 20 UML Virtual Machine, 63 Unit-Test, 145, 268 Unterklasse, 11 Vererbung, 11 Vererbungsbeziehung, 11 Verfeinerungsrelation, 260
334
Index
Verifikation, 294 Invariante, 273 Versagen, 149 Vorbedingung, 27, 35 Methode, 16 Vorschreibendes Modell, 54 VUnit, 162, 168 Wert
Merkmal, 15 Wiederverwendbarkeit, 52 Zeitlinie Sequenzdiagramm, 45 Zielzustand, 35 Zustand, 35 Zustandsinvariante, 35 Zustandskomponente, 11
Druck: Verarbeitung:
Strauss GmbH, Mörlenbach Schäffer, Grünstadt