Flash Engineering
programmer’s choice
Hier eine Auswahl: Spiele-Programmierung für PC, Handy und PDA Heinz Seyringer 512 Seiten € 39,95 [D] € 41,10 [A] ISBN 978-3-8273-2650-8
Heinz Seyringer zeigt in diesem Buch anhand praxisorientierter Projekte, wie man plattformunabhängige Spiele für PC, PDA und Handy entwickelt. Zu den angesprochenen Themen gehören unter anderem Isometrische Spiele, Sidescroller, Action- und Strategiespiele, transparente Benutzeroberflächen, Registrierverfahren und Softwareaktivierung, Assemblerprogrammierung für besonders zeitkritische Routinen, Objektorientierte Programmierung (OOP), Physik-Engine und Spriteprogrammierung.
Webseiten professionell erstellen Stefan Münz 1236 Seiten € 44,00 [D] € 45,20 [A] ISBN 978-3-8273-2821-2
Das Buch beleuchtet die gegenwärtige Situation im Web, die verfügbaren Technologien, den Browser-Markt und Erwartungen von Anwendern. Darauf aufbauend werden die meistverbreiteten Beschreibungs- und Programmiersprachen für Websites praxisnah und anhand von konkreten Beispielen vermittelt. Das Spektrum reicht dabei von HTML über CSS, JavaScript/DOM bis zu PHP/MySQL. Der Leser wird darüber hinaus aber auch in die Konfiguration eines Webservers wie Apache eingeführt und erwirbt wichtige Grundkenntnisse im servertypischen Betriebssystem Linux. Ein perfekt abgestimmtes Allround-Kompendium also für Webdesigner, Webprogrammierer und Webmaster.
Sven Busse
Flash Engineering Agile Ansätze zum Bau von RIAs mit Flash, Flex und ActionScript
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Bibliografische Information Der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das ®-Symbol in diesem Buch nicht verwendet.
Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Um Rohstoffe zu sparen, haben wir auf die Folienverpackung verzichtet.
10 9 8 7 6 5 4 3 2 1 11 10 09 ISBN 978-3-8273-2783-3
© 2009 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH, Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Lektorat: Brigitte Bauer-Schiewek,
[email protected] Fachlektorat: Sven Claar Herstellung: Martha Kürzl-Harrison,
[email protected] Korrektorat: Sandra Gottmann Coverkonzeption und -gestaltung: Marco Lindenbeck, webwo GmbH,
[email protected] Satz: Reemers Publishing Services GmbH, Krefeld, www.reemers.de Druck und Verarbeitung: Bercker, Kevelaer Printed in Germany
Inhalt 1
Einleitung 1.1
1.2
1.3 1.4
2
9
Zu diesem Buch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Der Aufbau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.2 Danksagung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Software Engineering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.1 Anforderung/Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.2 Entwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.3 Entwicklung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.4 Validierung/Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.5 Projektmanagement/Planung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Objektorientierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12 13 15 16 17 19 22 23 25 25 28
Zuhören und Lernen 2.1 2.2
2.3
3
.........................................................................................
.............................................................................
29
Objektorientierte Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anforderungsanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Geschäftsebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.2 Fachebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.3 Umsetzungsebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.4 Lasten- und Pflichtenheft . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29 31 34 39 50 50 51
Lösungen entwerfen 3.1
3.2 3.3
3.4 3.5 3.6 3.7 3.8
............................................................................
53
Architektur versus Entwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 3.1.1 Softwarearchitektur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 3.1.2 Softwareentwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 Komplexität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Projektziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 3.3.1 Wartbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 3.3.2 Erweiterbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 3.3.3 Wiederverwendbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 3.3.4 Robustheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 Abstrahierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Kapselung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 Zugriffsrechte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 3.7.1 Flexible Schnittstellen mit Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 Lose Kopplung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 3.8.1 Inversion of Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
Inhalt 3.9
3.10
3.11 3.12 3.13
4
Entwurfswerkzeuge 4.1
4.2 4.3
4.4
4.5
4.6 4.7
5
5.2 5.3 5.4
103 104 105 107 109 111 111 112 115 117 119 120 122 130 132
.............................................................................
133
UML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Komponentendiagramm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.2 Klassendiagramm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.3 Aktivitätsdiagramm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.4 Kontrollflussdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Muster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Architekturmuster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.1 Schichtenmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.2 MVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.3 Komponentenarchitektur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.4 Webservices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Entwurfsmuster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.1 Erzeugungsmuster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.2 Strukturmuster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.3 Verhaltensmuster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.4 Weitere Muster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.1 Fremdbibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.2 Eigene Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.1 Architektur-Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
134 136 137 145 147 152 153 153 156 160 161 163 164 174 189 217 228 234 236 236 241 243
Ändern und Testen 5.1
6
Modularität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.9.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.9.2 Realisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.9.3 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10.1 Vererbung von Implementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10.2 Vererbung von Konzepten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10.3 Liskovs Substitutionsprinzip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10.4 Kapselung beibehalten in der Hierarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10.5 Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10.6 Superklasse vor Subklasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10.7 Interfaces und Mehrfachvererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kohärenz und Verantwortlichkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konzept vs. Infrastruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
..............................................................................
245
Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Refactoring-Maßnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testen und Validieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Validierung/Verifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Effektivität/Effizienz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
245 253 276 276 279
Inhalt 5.5
5.6
5.7
6
Inspektionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5.1 Formelle/Informelle Inspektionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5.2 Codelesen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5.3 Automatisierte Inspektionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6.1 Testarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6.2 Testplanung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6.3 Gefahrenstellen beim Schreiben von Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6.4 Strukturierte Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6.5 Automatisierte Unit-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Vorgehensmodelle 6.1 6.2
6.3
6.4 6.5
6.6 6.7 6.8
Index
279 280 282 282 282 283 284 285 286 289 290
..............................................................................
291
Softwareentwicklung ist ein Mannschaftssport . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stärken und Schwächen von Menschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.1 Eher Fehler verhindern als Erfolg ermöglichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.2 Eher selber bauen als wiederverwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.3 Eher stur als anpassungsfähig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.4 Wir finden uns zurecht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.5 Wir lernen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.6 Wir wollen Anerkennung und tun was dafür . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zusammenarbeit im Team . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.1 Lokalisierung von Projektteams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.2 Interne Zwischenergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Teambildung und Zusammenhalt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arten von Vorgehensmodellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5.1 Sequenzielle und nebenläufige Modelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5.2 Das eigene Vorgehensmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Agile Entwicklung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6.1 Anwendung von agilen Vorgehensmodellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mit Änderungen umgehen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturangaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
292 295 295 297 297 297 298 299 300 301 302 303 304 307 308 312 314 322 329
...............................................................................................
331
7
1
Einleitung
Es ist heute nicht mehr so ungewöhnlich, im Flash-Bereich ein Buch über allgemeine Softwareentwicklung anzutreffen, wie noch vor einigen Jahren. Die Flash-Plattform ist in den letzten zehn Jahren enorm gewachsen. Anfang des neuen Jahrtausends war Flash noch am Anfang seiner Karriere als Plattform für Rich Internet Applications. Ein Begriff, der damals noch nicht geprägt war. 2001, zusammen mit Flash 5, wurde ActionScript offiziell eingeführt, als grundlegend neue Skriptsprache für Flash. Davor gab es nur rudimentäre Scripting-Möglichkeiten in Flash, und der Fokus der meisten Flasher lag nicht unbedingt auf Grundlagen des Softwaredesigns. Flash hat seinen Aufstieg als Animationswerkzeug begonnen, hervorgegangen aus dem Programm FutureSplash Animator der Firma FutureWave Software, die 1996 an Macromedia verkauft wurde. Aus FutureSplash Animator wurde so Macromedia Flash 1.0. FutureSplash Animator und später auch die ersten Versionen von Flash waren rein auf Animation ausgerichtet. Doch das Web ist ein interaktives Medium, und so lag es nahe, den Animationen auch Interaktivität zu verleihen. Bis Version 4 wurde dies zum großen Teil über die Flash IDE erledigt. Über vorgefertigte Befehle konnte man z. B. definieren, was nach Klick auf einen Button passieren sollte. Die Skriptmöglichkeiten waren beschränkt, noch in Flash 4 gab es keine Arrays. Die Entwicklung von Flash ist aus dem Gesichtspunkt der Softwareentwicklung sehr interessant, denn es hat seinen Ursprung nicht in der Idee, eine Programmierplattform für User-Interfaces zu sein, sondern darin, simple Animationen abzuspielen. Verglichen mit Java, dessen Ursprung das Entwickeln von plattformunabhängigen User-Interfaces auf digi-
Kapitel 1 talen Endgeräten war (John Byous, http://java.sun.com/features/1998/05/birthday.html) und zu dem es von SUN nie eine offizielle IDE zur Gestaltung dieser Oberflächen gab, kommt Flash also nicht ursprünglich aus der Softwareprogrammierung, sondern aus dem Multimedia-Bereich. Dieser Unterschied ist wichtig, will man die Entwicklung und den Erfolg von Flash verstehen. Über die Flash IDE hat Macromedia seine Community aufgebaut, denn über die IDE erhielten Interessierte ein Tool an die Hand, mit dem sie einfach und schnell visuelle Ergebnisse erzeugen konnten, auch ohne spezielle Programmierkenntnisse. Ohne diesen einfachen Weg, animierte Inhalte ins Web zu bringen (der zweite Erfolgsfaktor, die starke Verbreitung des Flash Players), wäre der Erfolg von Flash sicherlich nicht so groß gewesen. Über die Jahre aber hat sich Flash von einem reinen Animationswerkzeug zu einer sehr mächtigen Entwicklungsplattform für multimediale Benutzeroberflächen entwickelt. Der Fokus ist stark erweitert worden, die Zielgruppen derer, die mit Flash arbeiten, sind breiter gefächert. Zwar ist es immer noch möglich, mit Flash simple Animationen zu erstellen, aber dies allein würde der Flash-Plattform nicht mehr gerecht. Heute werden mit Flash große Webapplikationen umgesetzt, mit Flex ist die Entwicklung verteilter Anwendungen in der Geschäftswelt kosteneffizient möglich. Laut Forrester Research (The Business Case For Rich Internet Applications, Forrester 2007) bewegen sich heutige Rich Internet Applications zwischen ca. 30.000 € für kleine Module in einer Website und über 300.000 € für große Anwendungen. Ein Blick in die Showcases von http://flex.org/showcase/ und andere Webplattformen wie http://www.thefwa.com zeigt, dass Flash aus dem heutigen Internet nicht mehr wegzudenken ist. Mit der Evolution von Flash ist auch die Komplexität der Plattform gestiegen. Inzwischen ist ActionScript eine eigenständige, mächtige Skriptsprache, die mit Java vergleichbar ist. Genau wie Java ist ActionScript objektorientiert. Im Gegensatz zu Java erlaubt sie sowohl statische als auch dynamische Typisierung. Mit ActionScript ab Version 3 und in Verbindung mit der seit den Anfangsjahren enorm in viele Richtungen gewachsenen API des Flash Players sind komplexe webbasierte (oder mit Adobe AIR auch inzwischen desktop-basierte) Anwendungen realisierbar. Zahlreiche Beispiele verdeutlichen dies, nicht zuletzt auch aus dem neuen Haus von Flash, Adobe. Deren Produkte Buzzword, Photoshop Express sowie Acrobat Connect sind nur einige Beispiele dafür, was sich mit Flash alles umsetzen lässt. Solch komplexe Applikationen verlangen von ihren Entwicklern inzwischen mehr ab als die Bedienung eines Animationstools. Sie bestehen aus mehreren Zehntausend Zeilen Code und werden von vielen Entwicklern im Team gemeinsam bearbeitet. Aber auch der einzelne Entwickler, der an einem kleinen Flash-Projekt arbeitet, spürt die zunehmende Komplexität. Immer mehr finden sich im Internet Codebibliotheken, die von Flash-Entwicklern für Flash-Entwickler angeboten werden (ich werde in diesem Buch noch des Öfteren von Flash-Entwicklern sprechen, womit ich Flex-Entwickler einschließe, denn letztlich fußt ja auch Flex auf der Flash-Plattform). Diese Bibliotheken bieten Funktionalität an, die in sich teilweise so komplex ist, dass es für einzelne Entwickler nur noch durch Verwenden dieser Bibliotheken effizient möglich ist, solche Funktionalitäten in ihre Projekte einzubinden (siehe z. B. den Bereich 3D), wollen sie nicht ihr zur Verfügung stehendes Budget überlasten. Somit lassen sich auch in kleineren Projekten schon die Auswirkungen von verteiltem Arbeiten erkennen.
10
Einleitung Somit erlangen in der Flash-Entwicklergemeinde nun auch immer mehr Themen an Bedeutung, die im Bereich der klassischen Softwareentwicklung (oder Software Engineering, um hier langsam einen Kreis zu schließen) schon bereits seit längerer Zeit diskutiert werden. Dies sind zunächst einmal ganz pragmatische Fragen:
Wie strukturiere ich mein Projekt, damit ich oder andere später noch durchblicken? Wie organisiere ich mich mit anderen, wenn wir am gleichen Projekt arbeiten? Wie vermeide ich, dass ich das Rad jedes Mal neu erfinden muss? Das sind nur drei von vielen Fragen, die sich jeder Entwickler stellt, wenn er an einem Projekt sitzt. In früheren Jahren, in denen Flash-Projekte aufgrund der begrenzten Komplexität von Flash selbst nicht so extrem komplex waren, konnte ein einzelner Entwickler diese Fragen für sich noch recht gut allein beantworten. Mit der zunehmenden Popularität von Drittbibliotheken aber arbeitet nun auch ein Entwickler, der scheinbar allein ein Projekt bestreitet, nicht mehr für sich, denn er ist abhängig von den eingesetzten Bibliotheken und muss sich genauso mit anderen Entwicklern auseinandersetzen (und sei es nur über das Supportforum oder per Mail), wie das Entwicklerteam eines größeren Projekts dies untereinander muss. Dadurch wird es zunehmend schwieriger, oben stehende und verwandte Fragen zu beantworten, denn jeder Entwickler findet hier für sich andere Antworten.
Abbildung 1.1: Entwickler müssen sich mit vielen Themenbereichen auseinandersetzen.
11
Kapitel 1 In der klassischen Softwareentwicklung sind diese Probleme schon seit einigen Jahrzehnten bekannt und auch noch lange nicht abschließend gelöst. Das liegt auch zum Teil daran, dass der Bereich der Softwaretechnik im Vergleich zu anderen industriellen Bereichen noch relativ jung und nach wie vor dabei ist, sich selbst vollständig zu definieren. Ende der Sechzigerjahre erst wurde der Begriff »Softwaretechnik« bzw. »Software Engineering« geprägt, auch wenn Software da schon seit vielen Jahren entwickelt wurde. Bevor man sich mit dem Thema Softwaretechnik befasste, gab es einige spektakuläre Fälle von gescheiterten Softwareprojekten. Ein gutes Beispiel ist die Explosion der Ariane-5-Rakete. Die Ariane-5-Mission verwendete für ihr Trägheitsnavigationssystem eine Version der Ariane 4, die unverändert übernommen und auch nicht im Detail getestet wurde. Ein Test des Gesamtsystems, also des Zusammenspiels des alten Trägheitsnavigationssystems mit den neu programmierten übrigen Komponenten, fand aus Kosten- und Kapazitätsgründen nicht statt. Da die Ariane 5 eine andere Flugbahn hatte als die Ariane 4, entstand in einer Abgleichsfunktion ein höherer Wert als zulässig, was zu einem Überlaufsfehler führte, der überdies nicht abgefangen wurde und letztlich zum Versagen des Systems führte (Rozenblit 1997, S. 339). Durch das Scheitern des Projekts gingen vier Satelliten im Wert von ca. 400–500 Millionen Euro verloren, außerdem entstanden Folgekosten von ca. 500 Millionen Euro durch zwei Jahre zusätzliche Entwicklungszeit. Dieses Problem kann man im Kleinen ansatzweise vergleichen mit dem Versuch, bei einem Projekt, das auf einer bestimmten Version von Papervision3D basiert, nur die Version von Papervision3D auszutauschen und das Resultat nicht zu testen und zu hoffen, alles wird schon laufen. Mit etwas Glück läuft hinterher die Anwendung auch noch, aber in manchen Situationen will man sich auf Glück allein nicht verlassen, auch wenn es in einem kleinen Papervision-Projekt vielleicht nicht um Millionenbeträge geht.
1.1 Zu diesem Buch Nun gibt es zu dem Themengebiet der Softwareentwicklung schon viele Bücher, warum noch eines? Aufgrund der Historie von Flash sind viele Flash-Entwickler Quereinsteiger. Viele Designer haben sich z. B. seit jeher für Flash interessiert, und Menschen aus diversen anderen Berufszweigen begeistern sich für die Einfachheit, mit der man mit Flash schnell zu Ergebnissen kommen kann. Viele von ihnen haben aus ihrer Leidenschaft einen Beruf gemacht und verdienen als Flash-Entwickler gutes Geld. Und das nicht zu Unrecht: Flash ist in der Multimedia- und Werbebranche nach wie vor eine extrem nachgefragte Technologie, gute Entwickler werden händeringend gesucht. Mit der zunehmenden Komplexität der Flash-Plattform wird es aber immer schwieriger, die volle Kraft der Plattform allein für sich zu nutzen. Es scheint ein wenig wie ein Teufelskreis. Je mehr Flash kann, desto weniger ist der einzelne Entwickler noch in der Lage, dieses Potenzial voll auszuschöpfen. Arbeitsteilung und effektive Verteilung der Kompetenzen werden wichtiger. Designer und klassische Entwickler müssen im Team zusammenarbeiten. Zudem werden Projekte immer komplexer, die Lebenszyklen der Anwendungen werden länger, und die Anforderungen an Stabilität, Performance und Wartbarkeit steigen
12
Einleitung damit weiter an. Hierbei kommen auch Flash-Entwickler ohne klassische Softwareentwicklungs-Ausbildung immer stärker in Berührung mit grundsätzlichen Fragestellungen zur Organisation von Projekten, von Code, Lesbarkeit, Wiederverwendbarkeit und vielem mehr. Know-how im Umgang mit solchen Fragen und komplexen Projekten wird immer wichtiger. Manche Bücher zum Thema Software Engineering sind sehr Allgemein gehalten und umfassen den gesamten Bereich von der Bedarfsanalyse über die Spezifizierung der Anforderungen, der Systemarchitektur, der Kalkulation, der Mitarbeiterplanung, Change Management, Timing, Qualitätssicherung, Risk Management bis hin zum Deployment-Plan und noch zig anderen Teilbereichen. Wenn man es genau betrachtet, fallen alle diese Disziplinen auch in einem Flash-Projekt an, nur manche von ihnen halt je nach Größe des Projekts in einem sehr viel kleineren Rahmen, und auch nicht alle sind für einen FlashEntwickler unbedingt von Belang. Es ist nicht immer leicht zu entscheiden, welche Bedeutung man all diesen Bereichen in einem konkreten Projekt beimessen sollte. Natürlich sollte jedes Projektteam sein Projekt testen, aber muss dazu immer ein komplexes Testdrehbuch geschrieben und ein Bugreportingsystem aufgesetzt werden? Sicherlich besteht bei jedem Flash-Projekt auch die Gefahr, dass etwas schiefgeht und manches sicher geglaubte Feature sich nicht so umsetzen lässt, wie man gehofft hat – aber benötigt man deswegen gleich einen ausgetüftelten Risikoplan? Andere Bücher hingegen verwenden Programmiersprachen aus der klassischen Softwareentwicklung, wie z. B. C, C++, Java oder Ähnliche. ActionScript ähnelt in seiner Syntax manchen dieser Sprachen, aber Flash an sich funktioniert doch in vielen Bereichen ganz anders als z. B. C++. Es fällt einem Flash-Entwickler sicher leichter, bestimmte Prinzipien anhand von Beispielen in der Technologie zu verstehen, in der er auch selber arbeitet. Es ist deswegen Ziel dieses Buchs, aus dem großen Bereich Software Engineering diejenigen Themen hervorzuheben, die für RIA-Projekte mit Flash von besonderer Bedeutung sind, und die anderen Themen zwar nicht zwingend wegzulassen, sie aber nicht in aller Detailtiefe zu behandeln. Sie werden sehen, dass manche der folgenden Kapitel deutlich länger sind als andere. Das liegt nicht daran, dass es zu den anderen Bereichen nicht viel zu sagen gäbe, aber ich habe mich bewusst dafür entschieden, manche Themenbereiche knapper zu halten und andere Themen ausführlicher zu behandeln, je nachdem, wie relevant ich den Bereich für einen Flash-Entwickler halte. Zusätzlich werden alle Themen mit in Flash und ActionScript entwickelten Beispielen unterfüttert. Ich habe mich bemüht, diese Beispiele auch tatsächlich in Bereichen anzusiedeln, in denen Flash sich von anderen Plattformen unterscheidet. Manche Codebeispiele würden deswegen in anderen Sprachen so keinen Sinn machen, weil andere Technologien ganz andere Voraussetzungen und Einschränkungen bieten.
1.1.1
Der Aufbau
Los geht’s zunächst mit der Analyse des Problems bzw. der Aufgabenstellung. Dies ist auch gleich eine der größten Herausforderungen, vor denen ein jeder Softwareentwickler steht. Jeder kennt das, Fachkonzepte sind unvollständig, Kunden wissen im Detail nicht genau,
13
Kapitel 1 was sie wollen, und im Laufe des Projekts ändern sich Anforderungen ständig. Wie also kann überhaupt im Vorhinein eine genaue Aufgabenstellung definiert werden? Wessen Aufgabe ist das eigentlich? In Kapitel 2 gehe ich diesen Fragen nach.
Abbildung 1.2: Anforderungen zu sammeln, ist nicht immer einfach.
Nach der Analyse der Aufgabenstellung erfolgt der strukturelle Entwurf der Anwendung. Schwierigste Aufgabe hier ist: Wie kommt man von der Aufgabenstellung (oder auch dem Konzeptmodell) zur Programmstruktur (oder auch Softwaremodell)? Hier gibt es viele verschiedene Ansätze. Eines zum Beispiel nennt sich Domain-Driven-Design, bei dem man konzeptuelle Objekte (z. B. ein »Produkt« eines Sortiments) in konkrete Klassen oder Module in der Softwarestruktur umsetzt (also z. B. eine Klasse »Product«). Als besonderen Bereich habe ich hierbei das Thema Softwaredesign in Kapitel 3 identifiziert. Softwaredesign beschäftigt sich mit der konkreten Gestaltung eines konkreten Systemteils einer Gesamt-Softwarelösung. Im Bereich Flash ist hierbei meist der Webclient (oder mit Adobe AIR der Desktop-Client) gemeint, der nicht allein die Gesamtanwendung darstellt, sondern den Frontend-Teil, denn Flash-Anwendungen arbeiten sehr oft mit irgendeiner Art von Backend zusammen, und wenn es auch nur ein simpler Webserver ist, der neben der FlashAnwendung selbst noch Content-XML-Dateien beherbergt. Da einem Flash-Entwickler zur Erstellung des Softwareentwurfs diverse Methodiken und Werkzeuge zur Verfügung stehen, behandelt Kapitel 4 genau diesen Bereich. Es dreht sich hier also um die Methoden, Instrumente und Muster für den Entwurf von RIA-Clients. Anforderungen ändern sich, Ideen ändern sich. Nicht selten muss mitten im Projekt etwas am Code verändert werden. Auch stellt man als Entwickler vielleicht fest, dass die angedachte Struktur doch nicht so passt, wie man es sich vorgestellt hat. In Kapitel 5 geht es darum um das Verändern von bestehendem Code. Da genau hier, so wie auch in allen Phasen eines Softwareprojekts, die Kontrolle dessen, was man da gerade baut, sehr wichtig ist, nutze ich außerdem die Gelegenheit, über das Testen zu sprechen. Eine Tätigkeit, die leider allzu oft der engen Timeline zum Opfer fällt.
14
Einleitung Zusätzlich ist es wichtig, dass Entwickler sich mit der Frage auseinandersetzen, wie sie Softwareprojekte organisieren. In der klassischen Softwareentwicklung sind hier schon seit einigen Jahren interessante Entwicklungen zu beobachten, und auch Flash-Entwickler sollten versuchen, davon zu partizipieren. Unter »Vorgehensmodellen« in Kapitel 6 sind also die persönliche bzw. die Teamplanung eines Projekts zu verstehen. Begriffe wie »Agile Development« oder »Xtreme Programming« erhalten ja auch in der Flash-Welt langsam Einzug. Was aber hat es damit auf sich, welche Methoden zur Strukturierung eines Projektablaufs gibt es und wo liegen die Vor- und Nachteile? Wichtig bei der Betrachtung dieser Themen grundsätzlich ist, dass sie nicht nur in großen Teams und in großen Projekten Sinn machen. Konzepte wie die lose Kopplung zum Beispiel erleichtern auch dem einzelnen Entwickler die Erweiterung von Funktionalität in seinen Projekten. Vorgehensweisen wie das Iterative Modell können auch dem einzelnen Entwickler helfen, zusammen mit Projektmanagern und dem Kunden frühzeitig Resultate zu sehen und gemeinsam die richtige Richtung in Projekten anzusteuern, die von vielen Änderungen gekennzeichnet sind. In den vergangenen Jahren wird aber immer deutlicher, dass auch Flash-Projekte zunehmend komplexer werden und somit nicht immer mehr von einzelnen Entwicklern umsetzbar sind. Und in Projekten, die von einem ganzen Team von Entwicklern bearbeitet werden, tritt die Notwendigkeit zu einem einheitlichen, systematischen Vorgehen noch deutlicher zutage. Dieses Buch gibt Ihnen also einen fokussierten (und deshalb notwendigerweise auch unvollständigen) Überblick über die Welt der modernen Softwareentwicklung. Wenn Sie sich mit diesem Buch zum ersten Mal mit dem allgemeinen Thema der Softwareentwicklung beschäftigen, machen Sie nach diesem Buch nicht halt. Betrachten Sie es als Einstieg in eine neue Lernwelt. Zu jedem einzelnen Themengebiet gibt es Bücher, Internetseiten, Blogs, Foren und Nutzergruppen, die ein großer Quell an Informationen sind. Eine Entwicklergruppe trifft sich bestimmt auch in einer Stadt in Ihrer Nähe. Schauen Sie dort doch mal vorbei. Im direkten Kontakt mit anderen Entwicklern lässt sich meist sehr schnell viel Neues lernen. Nutzen Sie diese Quellen. Für alle, die sich mit bestimmten Themen weiter befassen wollen, werde ich an den geeigneten Stellen auch immer wieder Literaturhinweise geben. Zuletzt sei noch auf die dieses Buch begleitende Website hingewiesen, auf der die meisten Codebeispiele dieses Buches heruntergeladen werden können und wo Sie auch gerne Fragen an mich richten oder über Themen des Buches diskutieren können. Die Website ist zu finden unter: http://www.flash-engineering.de.
1.1.2 Danksagung Wie schnell doch die Zeit des Buch Schreibens im Nachhinein vorbeifliegt! Ein Jahr ist seit den Anfängen vergangen. Dass es überhaupt losgegangen ist, verdanke ich der flashforumKonferenz, auf der sich der Kontakt mit Cornelia Karl von Pearson Education Deutschland ergeben hat. Vielen Dank geht auch an das Lektoren-Team bei Pearson, besonders an Brigitte Bauer-Schiewek, die mich mit Rat und Tat und vor allem mit Geduld unterstützt hat.
15
Kapitel 1 Für so ein Buch muss einiges recherchiert werden, einiges ergibt sich aber auch aus den Erfahrungen und Gesprächen der vergangenen Jahre. Hierfür geht ein Dank an meinen Arbeitgeber, Interone Worldwide GmbH, und speziell natürlich an mein RIA-Team, mit dem es immer wieder Spaß macht, über Softwareentwicklung zu fachsimpeln. Konkreter Dank geht auch an Freunde und Kollegen, die mir mit Kritik und Verbesserungsvorschlägen konkret an diesem Buch geholfen haben: Sven Claar, Jens Franke und Holger Knauer. Zu guter Letzt noch ein Dank an Stefan Poier für die handgezeichneten Illustrationen, die einen wohltuenden Kontrast zu meinen Diagrammen bilden.
1.2 Software Engineering 1968 wurde der Begriff »Software Engineering« von Friedrich Ludwig Bauer geprägt und zum ersten Mal öffentlich auf einer NATO-Wissenschaftskonferenz diskutiert. Um eine Definition zu erhalten, möchte ich jedoch Helmut Balzert und seine Definition von Softwaretechnik bemühen (Lehrbuch der Software-Technik (Band 1): Software-Entwicklung, Spektrum Akademischer Verlag, 1996). Softwaretechnik ist demnach die: »Zielorientierte Bereitstellung und systematische Verwendung von Prinzipien, Methoden, Konzepten, Notationen und Werkzeugen für die arbeitsteilige, ingenieurmäßige Entwicklung und Anwendung von umfangreichen Software-Systemen. Zielorientiert bedeutet die Berücksichtigung z. B. von Kosten, Zeit, Qualität.« Es geht hier also darum, Vorgehensweisen und Methoden zu finden, die von einer Mehrheit der Entwickler als effektiv angesehen werden und mit denen es verlässlich – sprich unabhängig von einem konkreten Projekt reproduzierbar – möglich sein soll, gut funktionierende Anwendungen innerhalb des vorgesehenen Budgets zu entwickeln, die darüber hinaus auch noch genau das tun, was sich der Kunde vorgestellt hat (was keine Selbstverständlichkeit ist). Der Kern dabei ist das systematische Vorgehen. Man soll sich nicht jedes Mal wieder aufs Neue überlegen müssen, wie man ein Projekt organisiert, sondern den Entwicklern sollen Instrumente an die Hand gegeben werden, die sich bewährt haben, damit sich die Entwickler auf ihre eigentlich Aufgabe konzentrieren können. Das ist vergleichbar mit anderen Berufszweigen, in denen systematische Vorgehensweisen ebenfalls üblich sind, wie zum Beispiel im Hausbau. Kein Hausbauer würde mit dem Aufziehen von Wänden anfangen, wenn nicht vorher ein Architekt einen Entwurf gemacht, ein Statiker den Entwurf geprüft und die Gebäudebaufirma eine Planung erstellt hätte. Auch hier gibt es wie selbstverständlich bewährte Vorgehensweisen, die beschreiben, was man sinnvollerweise wann macht und wie. Nun mögen Sie sich sagen, dass das alles recht formell klingt. Das haben sich viele Softwareentwickler in den vergangenen zwanzig Jahren auch gedacht, und es finden deswegen auch in der klassischen Softwareentwicklung einige Umwälzungen statt. Agile Entwicklung ist hier das Stichwort. Wir werden uns später noch damit beschäftigen. Aber auch
16
Einleitung diese neuen Ideen sind strukturiert. Auch hier haben sich Menschen Gedanken darüber gemacht, wie man sinnvoll und effektiv ein Team und ein Projekt organisieren kann, damit es rechtzeitig fertig wird und damit das Resultat den Vorstellungen des Kunden entspricht. Nur versuchen sich diese neuen Ansätze flexibler den heutigen Erfordernissen an Softwareentwicklung anzupassen. Die Definition trifft also immer noch zu. Sie werden später noch sehen, in der Praxis kommen diese Begriffe gar nicht mehr so förmlich daher. Das Software Engineering beschäftigt sich im Gegensatz zur Informatik mit praktischen Herangehensweisen und Instrumenten zur Entwicklung konkreter Softwaresysteme. In der Informatik werden Grundlagen und eine theoretische Sichtweise diskutiert, im Software Engineering wird geklärt, wie man denn nun ein Softwareprojekt umsetzt. Im Software Engineering unterscheidet man grob folgende Felder:
Anforderung/Analyse Entwurf Entwicklung Validierung/Test Projektmanagement/Planung
1.2.1 Anforderung/Analyse In der Anforderung bzw. Analyse wird spezifiziert, welche Ziele erreicht und wie sie erreicht werden sollen. Für ein Flash-Projekt kann dies z. B. die Anforderung sein, dass nur eine bestimmte Flash Player-Version eingesetzt werden darf oder dass der Sicherheit bei der Übertragung von Nutzerdaten eine besondere Bedeutung zugemessen werden soll und dergleichen mehr. Anforderungen können in sehr vielfältiger Weise gestellt werden. Sehr oft sind Entscheidungen, die letztlich auch die Flash-Anwendung betreffen, aber eventuell aus Flash-Sicht unvorteilhaft erscheinen, aufgrund von allgemeinen Anforderungen entstanden. Dabei können auch nichttechnische Aspekte eine Rolle spielen, zum Beispiel kann der Einsatz bestimmter Softwarekomponenten aus unternehmenspolitischen Gründen abgelehnt werden. Eventuell will ein Unternehmen kein OpenSource-Produkt einsetzen, weil es fürchtet, dass es keinen zuverlässigen Support erhält. Es kann auch genau andersherum sein, dass ein Unternehmen aus Kostengründen OpenSource-Lösungen bevorzugt. Hier spielt auch oft Politik eine Rolle. Eventuell wird in einem Unternehmen z. B. ein bestimmtes Content-Management-System eingesetzt. Selbst wenn es nicht ideal für das aktuelle Projekt geeignet ist, kann es sein, dass sich das Unternehmen dennoch in den Anforderungen dafür entscheidet, das System zu verwenden, weil ein Umstieg zu kostspielig wäre. Solche etablierten Systeme in Unternehmen nennt man Legacy-Systeme, also solche, die wie festgewachsen erscheinen und schwer durch modernere auszutauschen sind, aus den unterschiedlichsten firmenpolitischen Gründen. Die größte Schwierigkeit bei den Anforderungen besteht oftmals darin, sie überhaupt detailliert zu spezifizieren. Sehr oft haben Kunden zu Beginn eines Projekts nur eine sehr grobe Vorstellung, was sie eigentlich haben wollen, es kann auch sein, dass sie etwas haben
17
Kapitel 1 wollen, von dem sich im Projekt herausstellt, dass sie es eigentlich gar nicht brauchen können. Es ist Aufgabe der Entwickler (und Projektmanager und Konzepter usw.), den Kunden nach bestem Wissen zu unterstützen, um die bestmöglichen Anforderungen für eine sinnvolle Anwendung herauszukristallisieren. In der klassischen Softwareentwicklung geht das so weit, dass die IEEE (das Institute of Electrical and Electronic Engineering, http://www.ieee.org) zusammen mit der ACM (Association for Computing Machinery, http://www.acm.org/) ethische Regeln für Softwareentwickler vereinbart haben. Auch in Deutschland hat der VDI (Verband Deutscher Ingenieure) solch ein Regelwerk herausgegeben. In ihnen ist niedergeschrieben, nach welchen Prinzipien Ingenieure und Entwickler handeln sollen, um ihren Kunden die bestmögliche Leistung zukommen zu lassen (siehe z. B. http://www.vdi.de/5180.0.html). Der Grund der Schwierigkeit bei der Spezifikation der Anforderungen ist, dass sich hierbei meist zwei Parteien gegenüberstehen, die grundsätzlich zunächst eine unterschiedliche Sprache sprechen. Da ist auf der einen Seite der Kunde, der sich in seiner Welt der Geschäftsprozesse, der Fachbegriffe und mit viel Kenntnis der Themen seines Umfeldes bewegt. Und auf der anderen Seite ist der Dienstleister, der den Kunden bei einem konkreten Projekt beraten soll und der auch seine ganz eigene Welt von Kenntnissen, Fachbegriffen und Prozessen mitbringt. Während der Definition der Anforderungen müssen nun beide Parteien eine gemeinsame Sprache finden, damit beide voneinander verstehen, worum es geht. Das hört sich nun erst einmal sehr hochtrabend an, aber jeder Flash-Entwickler weiß, dass es teilweise schon nicht sehr einfach ist, allein mit einem Designer eine gemeinsame Sprache zu finden. Bei der Definition der Anforderungen geht es nicht immer um umfangreiche Dokumente. In einem kleinen Projekt kann unter Umständen schon eine DIN-A4-Seite reichen. Je nachdem, mit welcher Vorgehensweise das Projekt umgesetzt wird (zu Vorgehensweisen komme ich noch), kann es auch sein, dass zu Beginn nur erst eine grobe Anforderungsdefinition vorgenommen und diese im Laufe des Projekts zusammen mit dem Kunden verfeinert wird. Im Vorgehensmodell der »evolutionären Entwicklung« wird dies z. B. so gemacht, dazu mehr in Kapitel 6. Wichtig ist, dass Versäumnisse oder falsche Annahmen in den Anforderungen zumeist erhebliche Auswirkungen auf den Rest des Projekts haben. Um beim Beispiel der Flash Player-Version zu bleiben: Die Annahme einer bestimmten Flash Player-Version in den Anforderungen, die sich später als nicht haltbar herausstellt, kann ein Projekt in ernsthafte Schwierigkeiten bringen. Man stelle sich vor, in den Anforderungen wurde zunächst Flash Player 9 definiert, und später stellt sich heraus, dass in der Umgebung, in der die Anwendung laufen soll (vielleicht ein Firmen-Intranet), nur Flash Player 8 zur Verfügung steht. Wenn sich in der Anfangsphase eines Projekts herausstellt, dass sich die Anforderungen an die Anwendung noch nicht restlos definieren lassen, so sollte der weitere Entwicklungsprozess dem Rechnung tragen. Mithilfe iterativer Entwicklung kann hier eventuell das Projekt besser organisiert werden.
18
Einleitung
1.2.2 Entwurf Im Bereich Entwurf unterscheidet man zwei unterschiedliche Detaillevels. Zum einen gibt es die Softwarearchitektur, zum anderen den Softwareentwurf. Die Softwarearchitektur beschreibt ein komplettes Softwaresystem eines Projekts. In den meisten Fällen stellt beispielsweise ein Flash-Client nur einen Teil eines ganzen Systems dar. Nehmen wir eine simple Webanwendung, mit der Kunden Kinokarten reservieren können. Eventuell wird diese Anwendung mit einem nutzerfreundlichen Webclient ausgestattet, natürlich in Flash. Dieser Webclient aber ist ja eingebettet in eine HTML-Datei. Diese HTMLDatei wird eventuell von einem Content-Management-System erzeugt, das an sich schon ein komplexes System darstellen kann. Hier sind Webserver und Applikationsserver im Spiel, die eventuell auf Hochverfügbarkeitssystemen laufen, weil die Website jederzeit zur Verfügung stehen soll. Zudem hat der Applikationsserver sicherlich Zugriff auf eine Datenbank, in der die Reservierungen letztlich über einen Webservice abgespeichert werden. An die Datenbank knüpft sich dann auch eine weitere Anwendung, die die Informationen an die Arbeitsplätze im Kino weiterleitet, damit dort der Ticketverkauf stattfinden kann. Man sieht also, eine vermeintlich kleine Flash-Anwendung kann eingebettet sein in eine große Gesamtanwendung. Über der Softwarearchitektur steht letztlich noch die Systemarchitektur. Die Systemarchitektur schließt nun Software- und Hardwarekomponenten mit ein. Der Raum, in dem die Webserver stehen, wurde ja nicht von Softwareentwicklern gebaut, und auch die Kassenterminals wurden nicht von Softwareentwicklern installiert. Auch schon während des Softwareentwurfs kann es also durchaus passieren, dass Softwareentwickler zusammen mit Experten anderer Disziplinen an einem Gesamtsystem arbeiten. Dies ist z. B. bei multimedialen Installationen oder Terminal-Anwendungen offensichtlich. In diesem Buch werde ich mich mit der System- und der Softwarearchitektur nicht im Detail beschäftigen. Ich erachte es aber als wichtig an, dass ein Flash-Entwickler seine unmittelbaren Umgebungen, in denen sich seinen Flash-Anwendungen bewegen, zumindest grundlegend kennt. Und dies sind nicht gerade wenig. Gerade im Zusammenhang mit Adobe AIR sind einige Schnittstellen hinzugekommen. Hier nur ein paar Beispiele:
HTML/JavaScript: Flash-Anwendungen wurden von Beginn an in HTML-Seiten eingebunden, man sollte also ein Grundverständnis von HTML und den Möglichkeiten von JavaScript haben.
Webserver: Webanwendungen bestehen meist aus einem dynamischen und einem nichtdynamischen Teil. Simple HTML-Seiten sind nichtdynamisch und werden von Webservern wie zum Beispiel dem Apache Webserver ausgeliefert.
Applikationsserver: Dynamische Teile einer Webanwendung werden von Applikationsservern ausgeliefert. Bei sehr kleinen Umgebungen kann dies auf der gleichen Hardware stattfinden, die auch den Webserver ausführt. Bei komplexeren Systemen sind dies getrennte Rechner. Die Sprachen, die auf Applikationsservern zur Verfügung stehen, sind vielfältig: PHP, JSP, .Net, Python, um nur einige zu nennen. Man muss sich nicht mit all diesen Sprachen auskennen, aber man sollte ein grundlegendes Verständnis für die allgemeine Funktionsweise von Applikationsservern besitzen.
19
Kapitel 1
Protokolle/Schnittstellen: Flash kann auf vielfältige Weise mit Applikationsservern kommunizieren. Ob einfache POST/GET Requests, SOAP, REST, individuelle XMLAustauschformate, JSON, AMF oder individuelle binäre Austauschformate, es gibt viele Wege, um mit einem Server Daten auszutauschen.
Browser: Auch wenn Flash für sich in Anspruch nimmt, browser- und betriebssystemunabhängig zu sein, in Details ist dies nicht der Fall.
Betriebssystem: Gerade bei Anwendungen, die Adobe AIR einsetzen, ist eine grundlegende Kenntnis der unterstützten Betriebssysteme erforderlich.
Spezielle Hardware: Im Zusammenhang mit Flash Lite oder auch dem neuen Feature der Hardwareunterstützung bei Grafikberechnungen im Flash Player 10 kann eine detaillierte Kenntnis der vom Nutzer eingesetzten Hardware und deren Treiber/Firmware notwendig sein. Die grundlegende Kenntnis dieser angrenzenden Teile ist wichtig, damit man als Flash-Entwickler seine eigene Anwendung korrekt einordnen kann und die Schnittstellen zu diesen Teilen sinnvoll strukturiert. So kann es zum Beispiel aus Sicht der Flash-Anwendung Sinn machen, als Schnittstelle Remote Procedure Calls über AMF einzusetzen, aber aus Sicht des Applikationsservers kann dies unvorteilhaft sein, weil dieser eventuell nicht nur allein mit dem Flash-Client, sondern auch noch mit einem HTML-/JavaScript-Client kommunizieren soll, der mit AMF wenig anfangen könnte.
Abbildung 1.3: Flash-Entwickler sollten ihre angrenzenden Systeme zumindest ansatzweise kennen.
Eine Flash-Anwendung wird innerhalb eines Softwaresystems üblicherweise als Subsystem oder als Systemkomponente bezeichnet, sie stellt also einen bestimmten Teil des Systems dar. Für die Ausgestaltung dieses Teils ist dann der Softwareentwurf (oder auch das Softwaredesign) zuständig.
20
Einleitung Im Softwareentwurf wird skizziert, wie die fachliche Idee, also das fachliche Konzept einer Anwendung, technisch umgesetzt werden kann. Der Grundgedanke hierbei ist, dass man nicht vollends blind drauflosrennt und mit der Programmierung beginnt, sondern die Chance nutzt, während des Entwurfs Fehler machen zu können. Fehler, die man im Entwurf macht, lassen sich eben während dieser Phase noch leicht beheben. Es wurde ja noch kein oder nur wenig Code geschrieben. Man ist also noch sehr flexibel. In grafischen Skizzen oder im Gespräch mit Kollegen kann man unterschiedliche Ideen durchspielen. Auch hier schlagen moderne Vorgehensmodelle wieder vor, dass man den anfänglichen Entwurf nicht übertreiben sollte. Anstatt von Beginn an die komplette Anwendung durchzustrukturieren, sollte man sich dem Gesamtproblem schrittweise nähern. Ziel des Softwareentwurfs ist natürlich zunächst, sich Strukturen und Konzepte auszudenken, die zu einer Anwendung führen, die ihre Aufgaben erfüllt. Neben diesem primären Ziel gibt es aber zumeist noch weitere Ziele. Laut Ian Sommerville (Software Engineering, Pearson Studium Verlag, 2007) lassen sich vier Merkmale für gute Software definieren:
Wartbarkeit: Darunter versteht man, dass eine Anwendung so aufgesetzt sein soll, dass man später noch die Möglichkeit hat, Aspekte zu verändern oder zu erweitern. Codefremde Entwickler sollen in der Lage sein, sich in der Anwendung schnell zurechtzufinden, um Änderungen vorzunehmen oder Fehler zu beseitigen. Unter die Wartbarkeit fallen also auch Lesbarkeit, Änderbarkeit und Erweiterbarkeit.
Zuverlässigkeit: Hier spielt die oben angesprochene funktionale Korrektheit natürlich auch mit rein. Dazu kommen aber auch noch weitere Merkmale, wie z. B. Ausfallsicherheit. Wenn eine Flash 9-Anwendung, die ein bestimmtes Feature nutzt, auf einen Flash Player trifft, der dieses Feature noch nicht unterstützt, sollte die Anwendung nicht unbedingt gleich komplett versagen, sondern vielleicht nur diese Funktion nicht anbieten. Auch gehört zur Zuverlässigkeit, dass in einem Fehlerfall nicht gleich der ganze Rechner abstürzt. Letztlich sollte auch gerade eine Webanwendung nicht zu einem Sicherheitsrisiko für den Anwender werden, indem sie zum Beispiel Hacking-Attacken über Cross-Side-Scripting oder ähnliche Angriffe ermöglicht.
Effizienz: Anwendungen sollen schonend mit den verfügbaren Ressourcen wie Speicher, Prozessorlast usw. umgehen. Gerade im Flash-Bereich gibt es viele beeindruckende Beispiele, die die Möglichkeiten des Flash Players voll ausreizen, aber eben auch die Systemressourcen, meist schnell erkennbar am Pfeifen des Prozessorlüfters. Gerade bei Webanwendungen gehört zur Effizienz auch die Menge an Daten, die von der Anwendung geladen werden müssen.
Benutzerfreundlichkeit: Bekanntlich ein recht schwammiges Feld, denn Benutzerfreundlichkeit ist nicht ohne Weiteres messbar. Grundsätzlich aber ist klar, eine Anwendung soll vom Nutzer bedienbar sein, ohne dass er dazu unverhältnismäßig viel Energie aufwenden muss. Es gibt natürlich gerade im Marketingbereich Anwendungen, die bewusst ungewöhnliche Bedienkonzepte verwenden, um dem Nutzer ein exploratives Erfahren der Anwendung zu bieten. Man sollte das aber nicht als Standardargument verwenden.
21
Kapitel 1 Diese Merkmale wirken sich nicht immer direkt auf die grundsätzliche Funktionalität aus, tragen aber trotzdem entscheidend zur Akzeptanz und Stabilität der Anwendung bei. Je nach Projekt kann die Gewichtung dieser Merkmale unterschiedlich ausfallen. In manchen Situationen ist die Wartbarkeit nicht von elementarer Bedeutung. Bei einer kleinen Flash-basierten Microsite, die nur für wenige Monate online ist, muss man der Wartbarkeit nicht unbedingt eine hohe Bedeutung zukommen lassen, es sei denn, in den Anforderungen wurde spezifiziert, dass Teile der Microsite für spätere Zwecke weiterverwendet werden sollen. Bei einer Klassenbibliothek, die als Standard für viele Module eingesetzt werden soll, ist eine gute Wartbarkeit hingegen eines der wichtigsten Merkmale. Im Entwurf der Anwendung müssen diese Dinge berücksichtigt werden.
Abbildung 1.4: Der Softwareentwurf muss unterschiedliche Anforderungen in Einklang bringen.
Der Softwareentwurf ist eine Disziplin, die gerade auch Flash-Entwickler betrifft, deswegen wird dieses Thema in Kapitel 3 genauer unter die Lupe genommen.
1.2.3 Entwicklung Wird im Softwareentwurf die Struktur bis zu einem gewissen Detailgrad bestimmt, so muss diese in der Entwicklung umgesetzt werden. Selten trennt man wirklich die Entwurfsphase strikt von der Entwicklungsphase ab, weil sich beide Bereiche gegenseitig beeinflussen. So können bei der Entwicklung Unstimmigkeiten im Entwurf entdeckt werden, sodass der Entwurf revidiert werden muss. In der Praxis überlappen sich diese beiden Phasen also. Je nach Vorgehensmodell (zum Beispiel bei iterativer Entwicklung) findet sowieso ein stetiger Wechsel zwischen Anforderungsspezifikation, Entwurf und Entwicklung statt. Obwohl es für fast jeden Bereich des Software Engineerings recht detaillierte etablierte Vorgehensmodelle gibt, ist dies für die eigentlich Implementierung, das Programmieren selbst also, nicht der Fall. In der Fachliteratur wird allgemein anerkannt, dass es sich bei der Programmierung stets um einen kreativen Prozess handelt, weil einem Programmierer zwar im besten Fall ein ausgereifter Softwareentwurf zur Verfügung steht, der ihm den Rahmen vorgibt, es aber trotzdem dem Programmierer überlassen bleibt, wie der Entwurf umgesetzt wird. So weiß ein Flash-Programmierer am besten, wie eine bestimmte Funktionalität
22
Einleitung umgesetzt werden sollte. Nicht zuletzt spielen hier ja auch Gegebenheiten der Technologie, sprich z. B. Flash, eine Rolle, die eventuell eine bestimmte Art der Implementierung oder bestimmte Workarounds erzwingen. Obwohl es keine standardisierten Vorgehensweisen zur Programmierung gibt, so gibt es doch in jedem Fall erprobte und etablierte Techniken in diesem Bereich (ein Buch, das sich mit diesem Thema sehr ausführlich beschäftigt, ist »Code Complete«, 2nd Edition, von Steve McConnell, Microsoft Press Verlag). Dieses Buch wird sich nicht speziell mit der konkreten Tätigkeit des Programmierens, also dem Schreiben von Code, beschäftigen, die anderen Themen zählen aber letztlich zum konkreten Akt der Programmierung dazu.
1.2.4 Validierung/Test Die primäre Anforderung an ein Softwareprodukt ist natürlich, dass es das tut, was es soll, sprich es muss funktional korrekt sein. Allein diese primäre Anforderung ist schon ein schwerer Brocken. Wann ist ein Programm funktional korrekt? Wie kann man überhaupt sicherstellen, dass das Programm wirklich hundertprozentig korrekt arbeitet? Ein Programm kann maximal nur funktional so korrekt sein wie die Anforderung, die man an es stellt. Wenn ich definiere, dass eine Webanwendung mit Flash Player 9 laufen können soll, dann wird meine Anwendung folglich höchstwahrscheinlich unter Flash Player 8 nicht laufen. Nutzer, die den Flash-Player 8 haben, werden sich sagen, »das Programm funktioniert nicht«. Die Anwendung läuft aber dennoch zumindest innerhalb seiner Anforderungen korrekt. Was aber, wenn die Anwendung ein bestimmtes Feature einer speziellen Unterversion von Flash 9 verwendet, beispielsweise den Fullscreen-Modus, der ja nicht sofort im ersten Release von Flash 9 verfügbar war? Die Anforderung spezifiziert hier nicht genau, welche Unterversion verwendet werden darf, ist also ungenau, so kann auch die Anwendung nicht mehr hundertprozentig arbeiten, denn sie funktioniert für manche Versionen von Flash 9 nicht korrekt. Dieses kleine Beispiel soll verdeutlichen, dass die Phase der Anforderungsanalyse und -spezifikation nicht getrennt vom Softwareentwurf betrachtet werden kann. Vielmehr wird in der Praxis zwischen diesen Phasen hin- und hergesprungen, denn oft ergeben Erkenntnisse aus dem Entwurf Ungenauigkeiten in der Anforderungsspezifikation. Die Validierung bzw. das Testen einer Anwendung hängt nun ganz grundlegend von der Korrektheit der Anforderungsspezifikationen ab, denn nur über sie kann man verlässlich überprüfen, ob die Anwendung das tut, was ausgemacht war. Ob die Anwendung auch das tut, was der Kunde braucht, steht noch mal auf einem anderen Blatt. Nicht wenige Softwareprojekte arbeiten korrekt gemäß den festgehaltenen Anforderungen, erfüllen aber dennoch in der Praxis nicht die Bedürfnisse des Kunden. Untersuchungen zeigen, dass Fehler, die in frühen Phasen eines Softwareprojekts gemacht werden, bei fortschreitendem Projekt immer teurer in der Behebung werden. Ein Fehler oder eine falsche Annahme, der bzw. die während der Anforderungsspezifikation gemacht wurde, ist während der Implementierung nur mit großem Aufwand wieder behebbar, weil sich der Fehler meist durch alle Disziplinen zieht, Entwurf, Planung und Entwicklung. Je eher also ein Fehler in welcher Phase auch immer gefunden wird, umso besser. Validierung
23
Kapitel 1 und Testing bezieht sich in der Welt des Software Engineerings deshalb auch nicht nur auf das Testen des fertigen Programms, sondern umfasst auch Reviews und Tests jeder einzelnen Phase im Projekt. Genauso wie die fertige Anwendung getestet wird, so sollten Anforderungsdokumente auch durch mehrere Personen auf Vollständigkeit und Richtigkeit geprüft werden. Fachkonzepte sollten von Technikern und Designern gegengecheckt und auf Machbarkeit geprüft werden, der erste Entwurf eines Entwicklers sollte möglichst mit anderen Entwicklern diskutiert und bewertet werden, und auch während der Entwicklung kann es helfen, Reviews von Einzelmodulen zu machen. Daneben gibt es natürlich zahlreiche Instrumente zur Sicherstellung der Qualität und Funktionalität, darunter Unit-Tests, UI-Tests, Usability-Tests und Inspektionen. Einige dieser Tests können auch im Flash-Bereich schon sehr stark automatisiert werden, darunter die Unit-Tests und teilweise auch UI-Tests. Andere bedürfen einer stärkeren Vorabplanung und Organisation. So können Usability-Tests durchaus recht aufwendig sein, wenn sie z. B. mit einer größeren Gruppe von Testern aus der erwarteten Zielgruppe durchgeführt werden. Um eine hohe Fehlerfreiheit zu erlangen, reicht es nach Erkenntnis heutiger Studien nicht aus, sich nur auf reine Tests zu verlassen. Bei Tests wird ein Teil oder die ganze Anwendung derart untersucht, dass man definierte Daten oder bestimmte Abfolgen von Nutzereingaben in die Anwendung gibt und prüft, was dabei herauskommt. Tests sehen eine Anwendung oder einen Teil einer Anwendung immer als Blackbox an, sprich, sie können nicht in die Klasse gucken und private Variablen und Methoden einzeln begutachten. Es wird also immer gegen die öffentlichen Schnittstellen getestet.
Abbildung 1.5: Die Jagd nach den Bugs
Als besonders effektiv werden Inspektionen oder auch Code Reviews angesehen. Dabei wird der Code von mehreren Prüfern, meist sind das Entwickler, zusammen vorgenommen. Es gibt unterschiedliche Arten von Inspektionen. Manche sind formal, manche weniger. Es gibt auch einfache Treffen zwischen zwei Entwicklern, die sich gemeinsam ein Stück Code ansehen. In Kapitel 5 werde ich diese Inspektionen genauer unter die Lupe nehmen. Allen gemein ist, dass sich im Gegensatz zu einem Test hier Menschen direkt mit dem Quelltext auseinandersetzen und ihn lesen. Dabei achten sie nicht nur auf funktionale Korrektheit, das könnte ein Test im Zweifel besser sicherstellen. Vielmehr schauen sie auf eventuelle Fehler in der Struktur des Codes, vielleicht finden sie Codepassagen, die nicht mehr
24
Einleitung benötigt werden oder redundant sind, sie können Schwächen in der Kapselung einer Klasse oder nicht ausreichender losen Kopplung entdecken, sie finden vielleicht Optimierungen bei einem Algorithmus oder entdecken potenzielle Schwierigkeiten, die sich aus Erfahrung im Zusammenspiel mit der Runtime ergeben könnten. Für sich genommen erreichen auch Inspektionen nur eine Trefferquote von ca. 60%. Am effektivsten ist die Kombination von Inspektionen und Testing. Damit lässt sich die Trefferquote auf deutlich über 90% schrauben. Nun kosten diese Verfahren natürlich Zeit und müssen sowohl zeitlich als auch budgetär eingeplant werden. Damit ist nicht immer einfach zu argumentieren vor Projektmanagement und Kunden. Gerade in Projekten mit sehr engen Zeitplänen wird gerne die Zeit für Tests, Reviews und Inspektionen am ehesten gekürzt.
1.2.5 Projektmanagement/Planung Dieses Buch ist kein Projektmanagement-Ratgeber. Ich werde hier eher auf die Themenbereiche eingehen, die Softwaredesigner und Entwickler im Flash Bereich besonders betreffen, schließlich übernehmen erfahrene Entwickler in größeren Projekten oft für einen bestimmten Bereich einer Anwendung die Rolle eines technischen Projektmanagers, gerade wenn mehrere Entwickler effektiv zusammenarbeiten sollen. Was bedeutet eigentlich agil? Was steckt hinter dieser Philosophie, und wie wird sie in Softwareprojekten angewendet? In Kapitel 6 gehe ich näher auf agile Vorgehensmodelle in der Softwareentwicklung und ihre Merkmale ein.
1.3 Objektorientierung ActionScript ist eine objektorientierte Sprache. Das wissen Sie bereits. Und je nachdem, wie lange Sie schon mit ActionScript arbeiten und was Ihr konkreter Hintergrund ist, haben Sie die Vorzüge oder, sagen wir es etwas neutraler, die 'Eigenheiten' der Objektorientierung dieser Sprache schon kennengelernt. Ich möchte in diesem kleinen Abschnitt auch nicht nur auf die objektorientierten Merkmale von ActionScript und Flash im Allgemeinen eingehen, sondern ich möchte Ihren Blick auf das richten, was sich durch diese Objektorientierung von ActionScript für Sie als Entwickler an Möglichkeiten eröffnet. Was ist die Kernidee der objektorientierten Softwareentwicklung? Geht es nur darum, seinen Code in Klassen zu gruppieren und ein wenig lose Kopplung zu erreichen durch den Einsatz von Interfaces? Oder ist da noch mehr? Da ist noch mehr, und es ist das eigentlich Faszinierende der Objektorientierung, die deswegen momentan so weit verbreitet und populär ist. Die Grundidee der Objektorientierung basiert auf einem Konzept, wie wir Menschen versuchen, komplexe Systeme zu verstehen. Komplexe Systeme in der Natur, bei Menschen, in der Physik und anderen Wissenschaftszweigen begreifen wir zumeist als Netzwerke. Je nach Betrachtungsweise sehen wir die Systeme als Netzwerke von Dingen an oder als Netzwerke von Wechselwirkungen. Nehmen wir die einfache Physik. Planeten interagieren in
25
Kapitel 1 Netzwerken von wechselseitigen Kräften, z. B. Gravitation, Trägheitskräfte, Fliehkräfte usw. Nehmen wir das Schwarmverhalten von Fischen oder Vögeln, auch hier wirken komplexe Netzwerke von Kommunikationssignalen, die sich durch den Schwarm bewegen. Netzwerke helfen uns in vielen Situationen, Beziehungen von Dingen darzustellen und zu verstehen. Sie stellen gewissermaßen eine Abstraktion der Wirklichkeit dar, lassen alles Nebensächliche weg und gewähren uns den Blick auf zentrale Zusammenhänge, die uns interessieren. Dabei interessieren wir uns nicht nur für ein Element eines solchen Systems allein, sondern wir betrachten das einzelne Element eingebettet im Netz mit anderen Elementen, die es beeinflussen. Diese Idee der Betrachtung eines Dings im Zusammenhang mit anderen Dingen, die einen Einfluss ausüben, ist der Vorteil gegenüber anderen Herangehensweisen, in denen die Dinge nur für sich allein untersucht werden und ihr Zusammenspiel mit anderen Elementen spät und entkoppelt betrachtet wird. Die objektorientierte Softwareentwicklung greift diese Idee auf. Das Wichtigste dabei ist aber, dass es hier nicht nur um objektorientierte Programmierung geht, es geht also nicht nur um das konkrete Umsetzen, das Programmieren. Es geht um den gesamten Entwicklungsprozess von der Definition der Aufgabenstellung, den Anforderungen, über das Konzept bis hin zur tatsächlichen Umsetzung, der Programmierung. Die eigentliche Stärke der Objektorientierung liegt in der Tatsache, dass wir sie zu jeder Phase unseres Projekts betreiben können. Schauen wir auf ein Beispiel, einen Online-Videoverleih. Wenn wir uns in die Welt eines Videoverleihs begeben, können wir uns wieder genau so ein Netzwerk zusammenspinnen. Da haben wir Filme, wir haben Kunden, Kunden schauen Filme, dazu gehen sie mit dem Unternehmen Leihverträge ein, ein Leihvertrag bezieht sich immer auf einen oder mehrere Filme usw. Je mehr wir uns in diese Welt unseres Auftraggebers bewegen, desto mehr entsteht in Gedanken ein Netzwerk an Dingen, die miteinander auf ganz unterschiedliche Art verknüpft sind, voneinander abhängen und einander bedingen. Dabei haben wir aber noch gar nicht übers Programmieren nachgedacht. Noch versuchen wir nur zu verstehen, wie das Geschäft des Videoverleihs funktioniert. Und doch sind wir schon mittendrin in der Objektorientierung. Der Clou ist, dass wir Dinge, die wir dort vor unserem geistigen Auge sehen, die Filme, die Kunden, die Verträge usw. genauso gut als Objekte bezeichnen können. Und Objekte, die die gleiche Wesensart haben, lassen sich auch jetzt schon wunderbar durch Klassen repräsentieren. Was haben ein Krimi, ein Thriller und eine Liebesschnulze gemeinsam? Sie sind Filme. Als Entwickler sieht man das Klassendiagramm förmlich vor sich. Das Beste aber ist, diese Objekte und Klassen können wir ganz von Beginn des Projekts, in der Definition der Anforderungen direkt bis zur Umsetzung beibehalten. Wenn also in den Anforderungen von der Definition eines Films die Rede ist, dann ist es nicht unüblich, wenn später in der Programmierung auch eine Klasse »Film« dasteht, die sich an dem gleichen Konzept orientiert, aber natürlich viel konkreter. Über die vielen Phasen eines Projekts müssen wir also niemals die Begrifflichkeit wechseln, wie das bei der strukturierten Analyse bzw. dem strukturiertem Design der Fall ist. Alle Beteiligten reden immer über die gleichen Dinge, der Kunde, die Konzepter und die Entwickler. Das erleichtert es den beteiligten Personen, in jeder Phase des Projekts den Zusam-
26
Einleitung menhang zum Ganzen zu finden. Stellen wir uns z. B. einen Entwickler vor, der während der Umsetzung neu zum Projekt hinzustößt. Wie praktisch für ihn, wenn er die Begrifflichkeiten, die er schon im Fachkonzept gelesen hat, in der Programmierung wiederfindet. Er wird sich schneller zurechtfinden.
Abbildung 1.6: Unterschiedliche Teammitglieder, ein Gedanke: ein Film
Ein weiterer Vorteil ist aber auch, dass wir durch den Fakt, dass wir auch in der Programmierung fachliche Begriffe verankern, indem wir z. B. konkrete ActionScript-Klassen wie »Movie«, »PaymentType« usw. verwenden, unsere konkrete Programmierung nicht einfach nur an der konkreten Anwendung ausrichten, die wir gerade umsetzen, sondern an allgemeine Konzepte aus der Welt unseres Auftraggebers. Was ist damit genau gemeint? In einer Anwendung, in der ich eine Klasse »Movie« definiere, die die typischen Eigenschaften eines Films kapselt, wie z. B. Titel, Filmlänge und Ähnliches, habe ich somit ein Objekt geschaffen, das grundsätzlich erst einmal unabhängig von meiner konkreten Anwendung ist. Denn das Objekt (bzw. die dahinterstehende Klasse) hat idealerweise selbst keinen Bezug zu meiner konkreten Anwendung und könnte in einer anderen Anwendung, die sich auch rund um Filme dreht, ebenso eingesetzt werden. Die Objektorientierung trennt also (wenn sie richtig verwendet wird) konzeptionelle Entitäten von ihrer konkreten Verwendung in einer individuellen Anwendung. Dass das so ist, zeigen uns immer wieder allgemeine Codebibliotheken, die uns in Flash den Alltag erleichtern, wie z. B. TweeningBibliotheken. Ein »Tween« ist ein Konzept, das sich losgelöst von seinem konkreten Einsatz beschreiben lässt. Und weil das geht, haben findige Entwickler allgemeine Tweening-Bibliotheken geschrieben, die wir in unseren konkreten Anwendungen benutzen können. Ohne Objektorientierung wäre das deutlich schwieriger. Dies soll einen Vorgeschmack auf die Möglichkeiten der objektorientierten Entwicklung geben, in den folgenden Kapiteln werde ich noch konkreter auf die einzelnen Methodiken der objektorientierten Analyse (OOA), des objektorientierten Designs (OOD) und der objektorientierten Programmierung (OOP) eingehen. Natürlich muss an dieser Stelle auch gesagt werden, dass nicht alles immer so einfach ist. Die Objektorientierung verlangt, dass man immer in konzeptionellen Entitäten denkt, sprich in Klassen und Objekten. Es gibt natürlich in der Softwareentwicklung auch immer wieder Situationen, in denen es einfacher ist, in technischen Strukturen zu denken, weil es
27
Kapitel 1 ein ganz bestimmtes technisches Problem zu lösen gilt. Nicht zuletzt deswegen haben auch Skriptsprachen oder spezielle domainspezifische Sprachen ihren Platz. Es macht nun mal z. B. mehr Sinn, reguläre Ausdrücke in ihrer eigenen strukturierten Art und Weise zu notieren als umständlich in Form von Klassen und Objekten. Auch für ein einfaches Batch-Skript würde man ungern erst auf Paket- und Klassenstrukturen zurückgreifen wollen. Deswegen verwendet man für Ant-Skripte z. B. seine bekannte XML-Struktur und schreibt nicht plötzlich Java-Klassen. Und deswegen verwendet auch Adobes Pixel Bender eine eigene Sprachsyntax, die nicht auf Klassen aufsetzt.
1.4 Literaturangaben Le Lann, G.: An analysis of the Ariane 5 flight 501 failure–a system engineering perspective, in: Jerzy Rozenblit: Proceedings. Los Alamitos, Calif.: IEEE Computer Society Press, 1997. McConnell, Steve: Code complete. Dt. Ausg. der 2. Edition [Nachdr.]. Unterschleißheim: Microsoft Press, 2007.
28
2
Zuhören und Lernen
2.1 Objektorientierte Analyse Wie ich schon in der Einleitung erläutert habe, besteht eine der Innovationen der Objektorientierung gegenüber den früheren Herangehensweisen in der Softwareentwicklung darin, eine Konzeptsicht anzubieten, die sich stark daran anlehnt, wie wir Menschen tatsächlich komplexe Systeme begreifen. Wenn wir uns komplexe Systeme wie zum Beispiel unsere Galaxie anschauen, dann wenden wir intuitiv Methoden der Komplexitätsbewältigung an, denn es ist uns nicht möglich, die Gesamtheit eines so großen Systems auf einmal zu erfassen. Anstatt also in unserer Galaxie einzelne Planeten zu suchen, unterteilen wir z. B. zunächst Sonnensysteme. Wir verwenden also Aussagen wie »besteht aus«. Eine Galaxie besteht also unter anderem aus Sonnensystemen. Auch ein Sonnensystem besteht wieder aus weiteren Elementen, nämlich einer Sonne und Planeten. Auch bezüglich der Planeten haben wir wieder Planetensysteme, die aus dem Planeten und eventuellen Monden bestehen. Außerdem sind wir gut darin, Dinge zu abstrahieren. Anstatt immer wieder jedes Ding bei seinem konkreten Namen aufzuzählen, bilden wir Abstrahierungen wie »Menschen«, »Bäume«, »Autos« usw. Wohl wissend, dass es viele unterschiedliche Autos gibt, sie aber alle einem gemeinsamen abstrakten Konzept angehören, das wir Auto nennen. Man kann das als eine »Ist ein«-Aussage bezeichnen. Diese Art, unsere Umwelt zu begreifen, lernen wir schon früh, und wir kommen gut klar damit. Was liegt also näher, sich diese Vorgehensweise konkret zu eigen zu machen, wenn wir komplexe Systeme beschreiben wollen, für die wir Softwareanwendungen entwickeln wollen?
Kapitel 2 In der objektorientierten Analyse nähern wir uns deswegen den Anforderungen (die ja letztlich nichts anderes als Beschreibungen des komplexen Systems sind, mit dem wir es in unserem Projekt zu tun haben) aus den beiden genannten Richtungen an, einmal der Beschreibung »besteht aus« und einmal der Beschreibung »ist ein«. Nun ist das einfacher gesagt als getan. Wo fängt man an? Nehmen wir mal wieder ein Beispiel, einen Videoverleih. Wo soll man bei einem Videoverleih anfangen, Objekte zu modellieren, wenn man noch nichts über das System weiß? Erfahrenen Programmierern fällt das vielleicht etwas leichter, aber normalerweise wissen die erfahrenen Programmierer noch zu wenig über das System, um das es geht, und auf Auftraggeberseite wiederum sitzen nur selten erfahrene Programmierer. In der Praxis hat sich deswegen bewährt, sich dieser Aufgabe über ein weiteres Hilfsmittel zu nähern, der Beschreibung von Anwendungsfällen. Das Gute an der Erstellung von Anwendungsfällen ist, dass jeder daran teilnehmen kann, auch und gerade Mitarbeiter auf Auftraggeberseite ohne Softwareprojekterfahrung, denn es sind dazu keine speziellen Kenntnisse erforderlich. Auftraggeber und Auftragnehmer beschreiben hierbei kleine, in sich geschlossene Szenarien, wie die späteren Nutzer innerhalb des komplexen Systems interagieren. Es wird hier nicht alles auf einmal beschrieben, stattdessen pickt man sich einzelne Abläufe heraus. Für unser Online-Videoverleih-Projekt könnte so ein Ablauf z. B. sein: »Kunde leiht sich ein Video aus« (wie das konkret gemacht wird, erläutere ich weiter unten im nächsten Abschnitt).
Abbildung 2.1: Der Kunde erläutert seine firmeninternen Abläufe.
Während der Beschreibung dieser Abläufe lernen die Entwickler und Konzepter auf Auftragnehmerseite sehr viel über das komplexe System und können nun aufgrund ihrer Erfahrung die Objektmodelle anhand der beiden oben genannten Ansätze »besteht aus« und »ist ein« erstellen. Das Gute an den Objektmodellen wiederum ist, sind sie erst einmal erstellt, sind sie auch wieder für den Auftraggeber verständlich, und er wird sein System darin wiederfinden, nur das Erstellen würde ihm wahrscheinlich schwerfallen.
30
Zuhören und Lernen In Kapitel 3 werden wir uns dann anschauen, wie diese Modelle aus der Analyse direkt für das Softwaredesign unserer Anwendung weiterverwendet werden können. Das ist ein großer Vorteil der objektorientierten Entwicklung gegenüber anderen Methodiken wie dem strukturiertem Design, bei dem es einen Bruch der Konzepte zwischen der strukturierten Analyse und dem strukturiertem Design gibt. Bei der Objektorientierung hingegen können die Modelle aus der Analyse direkt als Basis für den Softwareentwurf weiterverwendet werden. Für die Dokumentation des Analyseprozesses gibt es Instrumente, wie z. B. die UML. Mit der UML lassen sich nicht nur konkrete Klassendiagramme erstellen. Man kann mit ihr auch Geschäftsprozesse visualisieren und modellieren. Ich werde an diversen Stellen Gebrauch davon machen und dort dann die verwendeten Diagramme erläutern. Daneben werde ich in Kapitel 4.1 die UML und ihre Diagrammtypen erläutern.
2.2 Anforderungsanalyse Bevor man überhaupt daran denken kann, wie man ein Projekt umsetzt, muss man die Anforderungen kennen. Das mag in manchen Fällen trivial klingen, ist es aber ganz und gar nicht. Die Analyse und Definition der Anforderungen ist der erste Schritt, der in jedem Softwareprojekt gemacht wird. Selbst in sehr kleinen Projekten müssen die Beteiligten ein gemeinsames Verständnis davon entwickeln, was genau das Ziel ist. Die Anforderungen dürfen dabei nicht verwechselt werden mit einem Konzept, das beschreibt, wie man ein Projekt gedenkt umzusetzen. Die Anforderungen setzen noch einen Schritt vorher an. Hier interessiert einen zunächst, vor welcher Problemstellung man eigentlich steht. Das ist nicht immer einfach zu definieren. Denn manchmal hat sich der Auftraggeber schon recht genaue Gedanken gemacht, was er haben will, und es fällt ihm schwer, nun selbst noch einmal einen Schritt zurückzugehen und die dahinterliegenden Anforderungen überhaupt zu artikulieren. Sie zu formulieren ist aber wichtig, denn nur so kann hinterher überprüft werden, ob das umgesetzte Projekt denn auch die Erwartungen erfüllt. Dabei ist es nicht entscheidend, ob es sich bei dem anvisierten Projekt um eine klassische Anwendung oder eher eine multimediale Präsentation handelt. Hinter jedem solchen Projekt stehen Anforderungen, die überhaupt erst zu dem Projekt führen, und diese müssen gemeinsam mit dem Auftraggeber definiert werden. Um dieses gemeinsame Verständnis zu erlangen, ist der erste Schritt die Definition eines ganz allgemeinen Ziels oder einer Vision. Mit wenigen Sätzen versuchen dabei die treibenden Personen des Projekts, dessen Essenz mit seinen Kernfeatures zu beschreiben. Diese Beschreibung ist zumeist nicht länger als ein Absatz. Sie sollte kurz genug sein, dass man einer unbeteiligten Person kurz und knapp im Fahrstuhl erklären könnte, an was für einem Projekt man gerade arbeitet und was das Ziel ist. Im Fall unseres Beispiels des Online-Videoverleihs – nennen wir ihn FilmRegal – könnte diese Beschreibung wie folgt lauten:
31
Kapitel 2
FilmRegal – Projektidee FilmRegal ermöglicht den Verleih und Verkauf aktueller Filme und Filmklassiker online. FilmRegal ist 24 Stunden am Tag verfügbar, sieben Tage die Woche. Ausgeliehene Filme können temporär heruntergeladen oder komplett online angesehen werden. Gekaufte Filme können beliebig oft heruntergeladen werden. FilmRegal verwaltet die Filme und die dazugehörigen Lizenzen im angegliederten Unternehmensmodul. Dieses bietet auch eine Schnittstelle zum Finanzbuchhaltungssystem. Die Kernmodule sind: DRM, Video-Streaming, Callcenter-Modul, FiBu-Schnittstelle, Online-Portal, Webtracking, Katalogverwaltung, Registrierung/Login, Backup/Archivierung
Diese Kurzbeschreibung ist im Detail natürlich nicht sehr aussagekräftig, aber es bündelt die wichtigsten Ziele des Projekts in zwei Absätzen und hilft so, sich immer wieder an dieses generelle Ziel zu erinnern. Die Anforderungen beschreiben nun die Grundlagen, auf denen das Projekt konkret aufsetzen soll. Sie lassen sich grob in funktionale und nichtfunktionale Anforderungen unterteilen. Funktionale Anforderungen beziehen sich auf die konkrete Problemstellung, also zum Beispiel den konkreten Verleih von Videos. Nichtfunktionale Anforderungen kreisen um Themen wie vorhandene Infrastrukturen beim Auftraggeber, Budgets, anvisierte Meilensteine, politische Faktoren und andere Einschränkungen, denen das Projekt unterliegt. Solche nichtfunktionalen Anforderungen können tückisch sein, denn gerade bei politischen Themen kann es gut sein, dass man sie als Dienstleister nicht sofort erkennt, weil auf Seiten des Auftraggebers darüber vielleicht nicht gern gesprochen wird. Nichtfunktionale Anforderungen werden meist in tabellarischer Form notiert. Je nachdem, wie umfangreich sie ausfallen, kann man sie noch mal in Produktanforderungen, Unternehmensanforderungen und externe Anforderungen unterteilen. Ein Beispiel liefert Tabelle 2.1. #
Bezeichner
Beschreibung
1
Art
Produktanforderung
Anforderung
Für die definierte Zielgruppe von FilmRegal ist die durchschnittliche Rechnerausstattung zu ermitteln und als Systemanforderung für das Online-Portal sowie den Video-Manager-Client zu berücksichtigen.
Art
Unternehmensanforderung
Anforderung
Das Hosting soll von Firma XY übernommen werden, mit der FilmRegal bereits seit Längerem zusammenarbeitet.
2
3
Art
Externe Anforderung
Anforderung
Für das Online-Portal sollen eine gute Nutzerführung und ein ansprechendes Design im Zweifel einen höheren Stellenwert einnehmen als allumfassende Browserkompatibilität und Suchmaschinenoptimierung.
Tabelle 2.1: Beispielhafte nichtfunktionale Anforderungen
Die obigen Beispiele sind durchaus diskussionswürdig, aber dennoch kommen sie in der Praxis vor und schränken die Möglichkeiten des Dienstleisters bei der Erfüllung des Projektauftrags ein.
32
Zuhören und Lernen Anwendungen, die mit Flash bzw. Flex umgesetzt werden, können sich in ihrer Größe enorm unterscheiden. Bei sehr kleinen Anwendungen, z. B. einem kleinen Online-Spiel, ist die Definition der Anforderungen natürlich leichter als bei einer großen Unternehmensanwendung, die eventuell eine ganze Reihe an Geschäftsprozessen unterstützen soll. Je komplexer die Anforderungen, umso mehr wird es wichtig, das Definieren derselben in handliche Teile zu zerlegen, damit die Aufgabe weiterhin lösbar bleibt. Dabei geht man so vor, dass man bei sehr komplexen Projekten erst einmal sehr grob und abstrakt an die Anforderungen herangeht. Bei kleinen Projekten ist dies eventuell nicht notwendig, sodass man gleich viel konkreter die Lösung bzw. Umsetzung des Projekts definieren kann. Es lassen sich grob drei unterschiedliche Abstraktionsebenen unterscheiden: 1. Die Geschäftsebene: Hier werden die Geschäftsabläufe, die in unmittelbarem Zusammenhang mit dem Projekt stehen, beschrieben. Daraus ergibt sich auch, dass bei manchem kleinen Projekt keine sinnvollen Geschäftsabläufe bezeichnet werden können, weil das Projekt z. B. nur einen kleinen Teil innerhalb eines Geschäftsablaufs behandelt. Als Geschäftsablauf wird normalerweise ein Ablauf bezeichnet, der einen geschäftlichen Wert (positiv oder auch negativ) erzeugt. So stellt z. B. ein Registrierungsformular, bei dem ein Nutzer sich als neuer Kunde einer Videoplattform registriert, bereits einen Geschäftsablauf dar, denn die validen Daten eines neuen Kunden bedeuten in der Tat einen geschäftlichen Wert, auch wenn dieser vielleicht noch nicht konkret etwas gekauft oder Leistung bestellt hat. 2. Die Fachebene: Während in der Geschäftsebene erst einmal nur erläutert wird, welche unternehmerischen Abläufe von Bedeutung sind, wird hier in der Fachebene nun konkret fachlich definiert, wie diese Abläufe denn vonstatten gehen sollen. Wenn ein Geschäftsablauf zum Beispiel lautet: »Interessent fordert Produktinformationen an«, dann kann der entsprechende fachliche Ablauf lauten: »Interessent fordert über OnlineFormular Print-Broschüre an.« Die systematische Beschreibung der Abläufe und der beteiligten Akteure und Objekte ist ein aufwendiger Prozess, je nachdem, wie komplex die Aufgabenstellung ist. Dennoch ist er enorm wichtig, denn hier werden die fachlichen Details geklärt. 3. Die Umsetzungsebene: Für den Flash-Entwickler wird es in der Regel hier richtig interessant, denn nun gilt es, das fachliche Modell aus der Fachebene umzusetzen in ein technisches Modell für die Realisierung des Projekts. (Ich will nicht ausschließen, dass auch Flash- oder Flex-Entwickler als Systemanalytiker arbeiten, dennoch richtet sich dieses Buch eher an Entwickler als an Analytiker.) Ich widme diesem Bereich die Kapitel 3 und 4. Die drei Abstraktionsebenen Geschäft, Fachlichkeit und Umsetzung helfen, sich dem Projekt schrittweise anzunähern. Auch hier geht es wieder darum, Komplexität zu vermindern. Die Betrachtung dieser Ebenen erfolgt übrigens nicht notwendigerweise hintereinander. Je nach Vorgehensmodell findet die Betrachtung parallel statt. Je nachdem, welche Fragen zu klären sind, bewegt man sich immer wieder in der einen oder anderen Ebene. Manchmal kann es erforderlich sein, für einen abstrakten Geschäftsfall konkrete fachliche Abläufe zu definieren und einen Prototypen zu bauen, um zu klären, ob sich der Geschäftsfall über-
33
Kapitel 2 haupt mit Software umsetzen lässt. Über die verschiedenen Abstraktionsebenen können die Projektbeteiligten immer einen Überblick über das Projekt zum einen oder einen Detailblick in ein konkretes Problem zum anderen erhalten. Dennoch bleiben die Sachverhalte innerhalb einer Ebene eventuell immer noch sehr komplex. Gerade in der Fach- und der Umsetzungsebene steigt die Komplexität normalerweise deutlich an. Um einen bestimmten Sachverhalt besser zu begreifen, ist es hilfreich, sich ihm von unterschiedlichen Sichtweisen zu nähern. In manchen Situationen hilft einem die Beschreibung eines Ablaufs weiter, in einer anderen Situation interessiert man sich vielleicht mehr für die beteiligten Akteure und Objekte. Bei einem sehr großen Projekt mit mehreren Geschäftsfällen, die spezifiziert und verstanden werden müssen, ist es notwendig, auf der Abstraktionsebene eben dieser zunächst allgemein formulierten Geschäftsfälle zu arbeiten. Sind die Geschäftsfälle überschaubar oder vielleicht bereits aus einem vorangegangenen Projekt bekannt und ausformuliert, kann dieser Schritt eventuell kleiner ausfallen. Angenommen, wir hätten für unseren fiktiven Kunden »FilmRegal« schon die gewünschte Plattform umgesetzt und unser Kunde möchte nun im bestehenden Portal eine besonders komfortable Suchfunktion integrieren, dann werden die grundsätzlichen Geschäftsfälle durch ein solch relativ kleines Projekt nicht verändert, sodass sie für dieses Projekt auch nicht mehr im Besonderen definiert werden müssen. Auch kann es sein, dass ein Auftraggeber für bestimmte Geschäftsfälle Standardprozesse bereits im Einsatz hat, die unverändert übernommen werden sollen, wie z. B. Login- und Registrierungsformulare auf einer Webseite. In so einem Fall ist es dann ausreichend, die entsprechenden Dokumente zu diesen Prozessen zu verwenden bzw. auf sie zu verweisen. Für unser Beispielprojekt nehmen wir aber an, dass all diese Vorarbeiten noch nicht stattgefunden haben und wir also ganz frisch herangehen und die Anforderungen, sprich also zunächst mal die Geschäftsfälle, kennenlernen müssen. In der Praxis wird dies über viele Gespräche mit verschiedenen Personen auf Kundenseite stattfinden (diese Personen überhaupt herauszufinden kann schon eine schwierige Aufgabe für sich sein). Dabei besteht die erste Aufgabe – speziell wenn Auftraggeber und -nehmer zum ersten Mal zusammenarbeiten – überhaupt in der Identifizierung der Personen und Abteilungen, die für das Projekt zum einen wertvolle Informationen liefern können, die zum anderen Entscheidungsgewalt haben, und nicht zuletzt der Personen, die grundsätzlich Einfluss auf das Projekt oder die beteiligten Personen ausüben. Denn schließlich steckt in jedem Projekt auch immer ein nicht zu unterschätzender Anteil an Politik. Es ist also durchaus sinnvoll, sich Listen von Ansprechpartnern und ihren Rollen und Verantwortlichkeiten auf Unternehmensseite anzufertigen und diese im Verlauf des Projekts zu verfeinern.
2.2.1 Geschäftsebene In dem Fall, in dem eine Anwendung tatsächlich in besonderem Maße auf die Geschäftstätigkeit des Auftraggebers Einfluss nehmen soll, ist es notwendig, die von ihr berührten Geschäftsfälle und die beteiligten Personen und Institutionen kennenzulernen.
34
Zuhören und Lernen Dies geschieht meistens dadurch, dass sich der beauftragte Dienstleister mit dem Auftraggeber in Interviews zusammensetzt und gemeinsam ein Verständnis von der Aufgabe und den Randbedingungen entwickelt. Dieser Prozess kann sich bis kurz vor Ende des Projekts erstrecken, gerade in iterativen Vorgehensmodellen. Viele Projekte scheitern oder sind nicht in dem gehofften Maße erfolgreich, weil beiden Seiten nicht exakt klar ist, welche Aufgabe eigentlich zu lösen ist. So kann es sein, dass am Ende eines Projekts zwar eine Anwendung steht, die genau das tut, was in der Spezifikation steht, sie aber dennoch nicht den Erwartungen des Auftraggebers entspricht, weil in der Anfangsphase versäumt wurde, gemeinsam das tatsächliche Bedürfnis des Auftraggebers herauszufinden. Anwendungen, die grundsätzlich vollständig entwickelt wurden, aber nie effektiv eingesetzt wurden, gibt es in der Softwareentwicklung leider zuhauf. Die Schuld kann in einem solchen Fall nicht allein dem Auftraggeber gegeben werden, denn es sollte die Aufgabe der beauftragten Softwarefirma oder Agentur sein, zusammen mit dem Auftraggeber den Kern der zu lösenden Aufgabe und ihrer Randbedingungen herauszufinden. Dies erfordert gerade auf Seiten des Auftragnehmers ein besonders feines Gefühl dafür, die richtigen Fragen zu stellen und sich in die Welt und die Arbeitsweise des Auftraggebers hineinzuversetzen. In vielen Fällen fällt es Auftraggebern schwer, von sich aus die wichtigen Informationen zur Verfügung zu stellen, die der Auftragnehmer eventuell benötigt, denn das würde ihrerseits ein tieferes Verständnis der Arbeitsweise eines Softwareunternehmens bzw. eine Agentur voraussetzen, die Auftraggeber in der Regel verständlicherweise nicht haben und auch nicht haben müssen. Eine wichtige Voraussetzung für ein gemeinsames Verständnis ist die Entwicklung einer gemeinsamen Sprache, die beide Seiten verstehen. Sowohl in der Welt des Auftraggebers als auch des Auftragnehmers schwirren in der Regel eine Fülle von Fachbegriffen, Abkürzungen und Definitionen herum, die die jeweils andere Seite nicht kennt. Es ist eine Grundvoraussetzung, dass solche Begriffe und Abkürzungen erläutert und eventuell auch weggelassen oder in allgemeinverständliche Begriffe übersetzt werden, damit es nicht zu Missverständnissen kommt.
Abbildung 2.2: Auftraggeber und -nehmer müssen zu einer gemeinsamen Sprache finden.
35
Kapitel 2 Ist eine gemeinsame Sprachregelung gefunden, kann es an die Definition der Geschäftsfälle gehen, die zusammen die Problemstellung ergeben. In unserem Videoportal ist z. B. das Ausleihen eines Videos natürlich ein sehr prominenter Geschäftsfall. Daneben können aber auch andere Fälle auftreten, wie Reklamationen, Kundensupport und andere. Bernd Oestereich schreibt (Oestereich 2005): »Ein Geschäftsanwendungsfall [...] beschreibt einen geschäftlichen Ablauf, wird von einem geschäftlichen Ereignis ausgelöst und endet mit einem Ergebnis, das für den Unternehmenszweck und die Gewinnerzielungsabsicht direkt oder indirekt einen geschäftlichen Wert darstellt.« Das bedeutet also, hier geht es noch nicht um konkrete Abläufe in der geplanten Anwendung, sondern vielmehr um allgemeine Vorgänge, die sich im Unternehmen des Auftraggebers abspielen. Das Verleihen von Videos an Endkunden ist also zunächst einmal ein Geschäftsfall, der noch nichts darüber aussagt, ob die Filme in einem Ladengeschäft, per Katalog oder über das Internet angeboten werden sollen. Aber er enthält dennoch ganz entscheidende Informationen; dass z. B. überhaupt Filme direkt an Endkunden ausgegeben werden sollen und dass es hier nicht um einen endgültigen Verkauf, sondern um einen Verleih geht. Er hat ein geschäftliches Ereignis als Auslöser, nämlich die Absicht eines Endkunden zum Vertragsabschluss und ein definiertes Ergebnis von geschäftlichem Wert, nämlich einen eingeschränkten Lizenzvertrag mit dem Endkunden. Die Kunst ist es nun, die Geschäftsfälle so abstrakt wie möglich zu beschreiben, um konkrete Umsetzungsvarianten von dem eigentlichen Geschäftsfall zu entkoppeln. Das ist deswegen wichtig, weil sich über die Zeit die Art der Umsetzung eines Geschäftsfalles vielleicht ändern kann, aber der Geschäftsfall wahrscheinlich gleich bleibt. Wenn also unsere Firma FilmRegal Videos verleiht, wird sich der Geschäftsfall »Verleih eines Films an Endkunden« so schnell nicht ändern, unter Umständen aber die konkrete Art und Weise, wie er umgesetzt wird. Heute wird der Verleih vielleicht ausschließlich über das Internet erfolgen, in ein paar Jahren aber kann es sein, dass vielleicht doch noch eine Ladenkette hinzukommt. In dem Geschäftsfall sollte also die Umsetzung nicht enthalten sein, damit er nicht jedes Mal angepasst werden muss. Eine weitere Anforderung ist, dass ein Geschäftsfall auch wirklich nur einen Fall umfasst. So sollten zum Beispiel die Registrierung eines Neukunden sowie das Ausleihen eines Videos nicht in einem Geschäftsfall zusammengefasst werden, denn es sind zwei Fälle, die beide auch allein vorkommen können, die also nicht zwingend zusammengehören. Auch die Registrierung eines Neukunden ist ein eigenständiger Geschäftsfall, denn es beginnt mit dem geschäftlichen Ereignis, dass ein Interessent registrierter Kunde werden möchte, und hat die validierten Kundendaten dieses Neukunden zum Ergebnis. Diese Daten stellen einen nicht unerheblichen Wert für das Unternehmen dar, denn hier handelt es sich um einen konkreten Kunden, der ein Interesse hat, Videos über diese Firma auszuleihen. Über die Geschäftsfälle können wir als Softwareentwickler besser verstehen, um was es in einer Anwendung überhaupt grundsätzlich geht. Erst wenn klar ist, welche Vorgänge der Kunde denn gelöst haben will, sprich, bei welchen Geschäftsanwendungsfällen eine Verbesserung oder überhaupt eine Lösung umgesetzt werden soll, kann man darüber nachden-
36
Zuhören und Lernen ken, wie man an die Aufgabe herangeht und ob sie überhaupt mit Software gelöst werden kann und wenn ja in welchem Rahmen. Über die gemeinsame Definition dieser Geschäftsfälle erhalten sowohl der Auftraggeber als auch der Auftragnehmer ein gutes Verständnis davon, welche Anforderungen sich an die zu erstellende Software ergeben und was vielleicht völlig unwichtig ist, weil es z. B. mit einem Geschäftsfall nichts zu tun hat. Wichtig ist auch, dass während dieser Phase der Definition der Auftragnehmer enorm viel über die internen Abläufe, Abhängigkeiten und Beziehungen des Auftraggebers lernt, was sehr wichtig für den Erfolg des Projekts sein kann. Geschäftsanwendungsfälle werden immer aus Sicht des Unternehmens beschrieben, nicht aus der des Endkunden. Deswegen heißt obiger Fall auch nicht »Kunde leiht Film«, sondern »Verleih eines Films an Endkunden«. Schauen wir uns den Geschäftsfall »Verleih eines Films an Endkunden« genau an. Eine gängige Art, einen Geschäftsfall vollständig schriftlich zu erfassen, ist folgende Tabelle (entnommen aus Balzert 1999):
Geschäftsfall: »Verleih eines Films an Endkunden« Ziel:
Kunde schließt Ausleihvertrag ab.
Vorbedingung
Kunde hat einen Film zum Ausleihen gewählt.
Nachbedingung
Abgeschlossener Vertrag
Akteure
Kunde, evtl. Callcenter
Auslösendes Ereignis
Kunde fragt einen Film an.
Beschreibung
Kunde identifizieren Gewünschte Ausleihart abfragen Zahlungsdetails abfragen Vertrag präsentieren Vertragsabschluss
Tabelle 2.2: Geschäftsfall »Verleih eines Films an Endkunden«
Die Beschreibung ist hier recht einfach gehalten. Im Detail kann es sich mit der Zeit und in Gesprächen mit dem Kunden ergeben, dass noch weitere Zwischenschritte eingefügt werden, die hier fehlen. Zum Beispiel hört sich der Schritt »Kunde identifizieren« noch recht oberflächlich an. Hier könnte es sinnvoll sein, noch detaillierter zu werden, denn im Grunde sind in diesem Schritt zwei Möglichkeiten enthalten: Entweder handelt es sich um einen Neukunden oder einen Bestandskunden. Ein Neukunde müsste sicherlich überhaupt erst einmal erfasst werden. Bei der Modellierung bzw. Definition von Geschäftsfällen ergeben sich ähnliche Möglichkeiten zur Strukturierung wie bei der objektorientierten Programmierung. Auch bei Geschäftsfällen kann es Sinn machen, einen Geschäftsfall sehr abstrakt und dann zwei konkretere Geschäftsfälle zu beschreiben, die diesen abstrakten Fall »erweitern«. Auch die
37
Kapitel 2 gegenseitige Verknüpfung von Geschäftsfällen ist denkbar, dass z. B. ein Geschäftsfall einen anderen Fall verwendet. Die UML sieht hier über entsprechend gekennzeichnete Pfeile die Modellierung solcher Beziehungen vor. Gerade wenn mehrere Geschäftsfälle und ihre Akteure dargestellt werden sollen, ist dies mittels UML-Diagrammen gut möglich. So kann z. B. in unserem Beispielprojekt gezeigt werden, dass bei den wichtigsten Geschäftsfällen als Akteur direkt nur der Endkunde beteiligt ist, also aus Sicht von FilmRegal kein Personal vorgesehen ist, weil alles über die Online-Plattform abgewickelt wird.
«business» Neukunden registrieren
«business» Verleih eines Films an Endkunden Endkunde
«business» Verkauf eines Films an Endkunden
Abbildung 2.3: Die wichtigsten drei Geschäftsfälle mit dem Endkunden als UML-Diagramm.
Zusätzlich zur Definition der für das Projekt relevanten Geschäftsfälle kann es auch Sinn machen, die nicht relevanten Geschäftsfälle zu definieren, um eine klare Abgrenzung zu erhalten. Speziell bei Fällen, die sich sehr ähneln, kann es Sinn machen, die Geschäftsfälle kurz zu beschreiben, die nicht Teil des Projekts sein sollen, damit eine klare Abgrenzung möglich ist. Wie schon eingangs beschrieben, will FilmRegal ja die Filme nur online anbieten. Wir können also einen Abgrenzungsfall beschreiben, in dem sich ein Endkunde einen ausgeliehenen Film per Datenträger schicken lassen will, und definieren, dass dies ein nicht unterstützter Geschäftsfall ist. Neben der Definition der Geschäftsfälle werden innerhalb der Betrachtung der Geschäftsebene noch weitere Informationen gesammelt. So werden z. B. alle Stakeholder definiert, also alle Personen oder Institutionen, die ein wie auch immer geartetes Interesse an den beschriebenen Geschäftsfällen haben können. Darunter sind natürlich Personen auf Seite des Unternehmens selbst, wie z. B. Verkaufsleiter, Projektmanager, Marketingverantwortliche, IT-Ansprechpartner usw. Dann gibt es den Endkunden, Verbraucherschutzorganisationen, Ordnungsämter, Datenschutzämter, Gesetzgeber etc., die auch alle indirekt ein Interesse an den Geschäftsfällen des Unternehmens haben, und sei es nur, um zu überwachen, dass die Interessen anderer nicht verletzt werden. Das Notieren und Klassifizieren
38
Zuhören und Lernen dieser Stakeholder kann wertvolle Informationen und Anforderungen für das Projekt zutage fördern, auf die man sonst eventuell nicht gekommen wäre. Abbildung 2.4 zeigt beispielhaft eine UML-Darstellung möglicher Akteure bzw. Stakeholder für unser Beispielprojekt FilmRegal. Die Akteure mit einem Kreis und einem senkrechten Strich auf der linken Seite sind Akteure, die direkten Kontakt zu anderen Geschäftspartnern, in unserem Fall Endkunden, haben.
Filmrechte Inhaber
Endkunde CallCenter
Firma FilmRegal Agent - Tony Plapper
Lizenzen/Einkauf - Ralf Schnäppchen
Leiter - Rudy Hastig
CEO - Anton Simpel IT - Werner Mahn
Marketing - Tanja Opulent
PM - Sybille Hope Hosting Partner
Fulfillment Partner
Developer - Jens Roger
Sales - Peter Niemand
Support - Kerstin Asap
PM - Tom Sorglos
Abbildung 2.4: Stakeholder – Akteure für die Geschäftsabläufe.
2.2.2 Fachebene In der Fachebene beschäftigt man sich mit der Lösung der Aufgabenstellung, die sich entweder aus dem Briefing oder tatsächlich aus der detaillierten Beschreibung der Geschäftsfälle ergibt. Je nach Umfang des Projekts ergeben sich hier Dokumente wie z. B. Fachkonzepte oder Pflichtenhefte.
39
Kapitel 2 Der wesentliche Unterschied zwischen Geschäftsebene und Fachebene ist, dass in der Geschäftsebene die grundsätzlich benötigten geschäftliche Abläufe definiert werden, aber nicht, wie diese konkret erfüllt werden könnten. Im Falle unseres Videoverleihs stellen wir in der Geschäftsebene also erst einmal ganz banal fest, FilmRegal will Videos an Endkunden verleihen. Wir wissen also, was der Geschäftsgegenstand ist, nämlich Videos, und wer der primäre Geschäftspartner sein soll, nämlich der Endkunde. Wie jetzt nun das Video zum Endkunde gelangen soll, also über welche Kanäle, und wie der Ablauf dabei konkret aussehen soll, das wird nun in der Fachebene beschrieben. Man kann sich den Unterschied auch als eine Art Frage-Antwort-Spiel vorstellen. Auf Geschäftsebene stellen wir fest, was wir eigentlich erreichen wollen, und wir fragen uns, wie wir das wohl erreichen könnten. In der Fachebene beantworten wir nun diese Frage und erläutern konkret, wie die geschäftlichen Anforderungen fachlich gelöst werden können. Später in der Umsetzung wird dann noch konkreter beschrieben, wie diese fachliche Lösung nun auch konkret umgesetzt wird. In der Fachebene gehen wir nicht ins technische Detail, hier wird noch nichts darüber gesagt, ob das Online-Portal Flash nutzt oder mit welchen Softwaresystemen hier überhaupt gearbeitet werden soll. Wir sind immer noch auf einer nichttechnischen, fachlichen Ebene. Das ist keine Selbstverständlichkeit bei Softwareprojekten. Sehr oft fangen Projekte mit Sätzen an wie: »Wir möchten gerne eine richtig multimediale Flash-Site bauen.« Hier wird also schon eine technische Entscheidung getroffen, bevor überhaupt klar ist, was fachlich eigentlich gemacht werden soll. Eine Festlegung zu so früher Zeit kann später eventuell nur sehr schwierig oder gar nicht mehr rückgängig gemacht und sollte deswegen unbedingt vermieden werden. Wie detailliert die Fachebene beschrieben wird, hängt natürlich von der Komplexität des Projekts ab. Folgende Arten der Beschreibungen können je nach Komplexität hilfreich sein:
Anwendungsfälle Ablaufdiagramme Fachliche Klassen und Objekte Schnittstellen und User-Interfaces Sonstige Anforderungen Es ist nicht entscheidend, welche Arten von Beschreibungen man verwendet, solange der Inhalt klar wird. In der Regel ist es aber so, dass die rein textliche Beschreibung eines Sachverhalts – besonders wenn er auf eine technische Umsetzung hinausläuft – Grenzen der Genauigkeit hat. Hier können Diagramme eventuell helfen, um den gewünschten Sachverhalt deutlicher zu machen. Nehmen wir ein kleines Beispiel, einen Login-Dialog. Um den Vorgang zu beschreiben, wie ein Nutzer sich mithilfe des Login-Dialogs authentifizieren kann, könnte man textlich Folgendes schreiben: Der Nutzer gibt in die Textfelder »Benutzername« und »Passwort« seine Nutzerdaten ein und klickt dann auf »Einloggen«. Für den Fall, dass er eines der Felder nicht ausgefüllt hat, wird ihm sofort ein entsprechender Hinweis über dem Feld angezeigt. Anderenfalls wird der Einlog-Vorgang auf dem Server gestartet. Sollten die eingegebenen Nutzerdaten nicht
40
Zuhören und Lernen bekannt sein, wird dem Nutzer eine Fehlermeldung angezeigt, die ihn darauf hinweist, dass die Nutzerdaten nicht bekannt sind. Sind die Nutzerdaten bekannt, wird der Nutzer in Abhängigkeit des Ausgangspunktes, an dem er sich befand, entsprechend weitergeleitet. Diese Beschreibung ist zwar vollständig, aber sie als Leser zu erfassen ist deutlich schwieriger als einfach ein Ablaufdiagramm anzusehen, das die gleichen Informationen enthält (siehe Abbildung 2.5).
Start
Login Formular mit Nutzername und Passwortfeld
Fehlermeldung: "Beide Felder ausfüllen!"
Server prüft die angegebenen Nutzerdaten
Nutzer eingeloggt Nutzername und/oder Passwort nicht angegeben
Fehlermeldung "Nutzer unbekannt"
Abbildung 2.5: Ablauf in einem Login-Dialog als UML-Ablaufdiagramm.
Anwendungsfälle Hat man in der Geschäftsebene mit Geschäftsanwendungsfällen gearbeitet, so ist es hier in der Fachebene sinnvoll, wieder mit Anwendungsfällen zu arbeiten, hier aber mit fachlichen Anwendungsfällen. Diese beschreiben konkret eine fachliche Umsetzung eines Geschäftsfalls. Ging es in einem abstrakten Geschäftsfall also um den »Verleih eines Films an Endkunden«, so geht es jetzt um den »Verleih eines Films online«, also über das Internet-Portal. In unserem Fall der Firma FilmRegal ist dies der einzige konkrete fachliche Fall, der sich auf den Geschäftsfall »Verleih eines Films an Endkunden« bezieht, denn der Verleih soll ausdrücklich nur über das Online-Portal möglich sein. Möchte man eine Beziehung zwischen dem Geschäftsfall und dem konkreten fachlichen Fall darstellen, so kann man das mit der UML tun, wie Abbildung 2.6 zeigt.
Verleih eines Films Online
«business» Verleih eines Films an Endkunden
Abbildung 2.6: Konkreter fachlicher Fall, bezieht sich auf Geschäftsfall.
41
Kapitel 2 Auch wenn aufgrund der Art und Komplexität des Projekts keine Geschäftsfälle beschrieben wurden, so ist es grundsätzlich immer sinnvoll, die fachlichen Anwendungsfälle zu beschreiben, die für das Projekt relevant sind. Über diese Anwendungsfälle ergeben sich dann weitere Erkenntnisse für die Modellierung der involvierten Dialoge, der benötigten Daten und Schnittstellen sowie auch erste Ansätze für – hier noch fachliche – Klassen und Objekte, die später in der Umsetzung dann noch eine größere Rolle spielen werden. Weiter oben hatten wir beispielhaft den Geschäftsfall »Verleih eines Films an Endkunden« beschrieben. Schauen wir uns nun an, wie der dazu passende fachliche Anwendungsfall aussieht:
Anwendungsfall: »Verleih eines Films online« Ziel:
Kunde schließt Ausleihvertrag über das Online-Portal ab.
Vorbedingung
Kunde hat im Online-Portal einen Film zum Ausleihen gewählt.
Nachbedingung
Film ausgeliehen
Akteure
Kunde
Auslösendes Ereignis
Kunde will gewählten Film ausleihen.
Beschreibung
1. Kunde identifizieren Der Kunde erhält eine Login-Aufforderung, falls er nicht bereits eingeloggt ist. Dort gibt er seine Nutzerdaten ein und klickt auf »einloggen«. Die eingegebenen Nutzerdaten müssen bekannt sein. 2. Gewünschte Ausleihart aufnehmen Der Kunde gibt die Art der Ausleihe an. Zur Verfügung stehen »Direktstream« und »zeitlich begrenzte lokale Speicherung«. 3. Zahlungsdetails aufnehmen Der Kunde gibt an, über welche Zahlungsmethode er zahlen will. Je nach Zahlungsart (Rechnung, Lastschrift, Kreditkarte) gibt er die dafür notwendigen Zusatzdaten an. Bei der Zahlungsart Kreditkarte werden die angegebenen Daten direkt über einen Online-Dritt-Dienst auf Validität geprüft. 4. Vertrag präsentieren Alle Details zur Ausleihe werden dem Kunden nochmals präsentiert. Der Kunde muss diese Angaben bestätigen, um den Vertragsabschluss einzuleiten. 5. Vertrag abschließen Der Vertrag wird abgeschlossen, die Kundendaten werden abgespeichert. Je nach Ausleihart wird dem Kunden der Film direkt als Stream oder als Download freigeschaltet. 6. Vertragsbestätigung versenden Abgeschlossener Vertrag wird per E-Mail versandt.
Tabelle 2.3: Konkreter fachlicher Anwendungsfall »Verleih eines Films online«
42
Zuhören und Lernen Wie man im obigen Beispiel sehen kann, ist der Anwendungsfall nun deutlich konkreter beschrieben als noch der entsprechende Geschäftsfall. Es ist wichtig, darauf hinzuweisen, dass die Detailliertheit, mit der der Ablauf hier beschrieben wird, auch davon abhängt, welche weiteren Beschreibungen man noch hinzufügen wird. In der obigen Beschreibung wird zum Beispiel nicht klar, in welcher konkreten Art und Weise dem Nutzer der Login-Dialog angeboten wird. Verlässt er dazu die aktuelle Seite, auf der er sich befand, oder erscheint der Dialog in einem Layer? Wir gehen davon aus, dass sich diese Fragen noch aus anderen Beschreibungen, die ich weiter unten erläutern werde, ergeben. Will man keine weiteren Beschreibungen hinzufügen, dann muss man natürlich bereits an dieser Stelle noch konkreter werden. Ein Anwendungsfall soll immer einen in sich geschlossenen Vorgang beschreiben. Dieser Vorgang soll entweder vollständig oder gar nicht durchlaufen werden. In unserem Beispiel würde es also keinen Sinn ergeben, wenn der Vorgang bei Punkt 3, »Zahlungsdetails abfragen«, enden würde. Natürlich kann der Kunde den Anwendungsfall jederzeit abbrechen, aber das bedeutet dann für uns, dass der Anwendungsfall komplett abgebrochen wird. Alle zuvor gemachten Angaben innerhalb des Anwendungsfalls gingen damit verloren. Kommt man in eine Situation, in der ein Anwendungsfall auch teilweise abgeschlossen werden könnte, wäre das also ein recht sicheres Zeichen dafür, dass man diesen Fall in mehrere Einzelfälle splitten sollte. Es kann sinnvoll sein, den beschriebenen Anwendungsfall noch um weitere organisatorische Details zu ergänzen. Folgende Details können hilfreich sein:
Offene Punkte Priorität Aufwand Änderungshistorie Fertigstellungstermin (gerade bei iterativem Vorgehen wichtig) Ansprechpartner Zu erwähnen ist hier noch, dass man im obigen Anwendungsfall keinerlei Beschreibungen zu Ausnahmen findet, also zum Beispiel Fehlerfällen. Was ist zum Beispiel, wenn der Server ausfällt usw.? Es ist nicht Aufgabe eines Anwendungsfalls, diese Ausnahmen zu beschreiben. Ein Anwendungsfall erfasst nur die Regelfälle, also den gewünschten Ablauf eines Vorgangs, nicht eventuelle Ausnahmen. Es ist natürlich aber wichtig, die Ausnahmen zu kennen und zu beschreiben, was in solchen Fällen passieren soll. Allerdings ist hier in der Beschreibung des Anwendungsfalls nicht der richtige Platz dafür. Stattdessen sollten diese Beschreibungen in konkreten Ablaufdiagrammen erfolgen. Warum diese Trennung? Es ist vielleicht schon aufgefallen, dass wir mit unseren Beschreibungen stückweise immer konkreter werden. Gerade bei agilen Vorgehensweisen ist das essenziell, weil wir davon ausgehen, dass wir zu Beginn eines Projektes nicht sofort alle Details erfassen können. Je mehr wir uns mit dem Projekt beschäftigen, desto detaillierter können wir werden. Das bedeutet letztlich auch, dass zwischen den einzelnen Beschreibun-
43
Kapitel 2 gen eventuell deutliche zeitliche Abstände liegen. So kann es sein, dass der Geschäftsfall »Verleih eines Films an Endkunden« zu Beginn des Projekts definiert wurde, der konkrete fachliche Anwendungsfall aber deutlich später in der Mitte des Projekts, und dass das konkrete Ablaufdiagramm wiederum später erstellt wurde, nämlich in der konkreten Iteration, in der dieser Ablauf auch entwickelt werden soll. Es ist dem Wesen dieses Buches geschuldet, dass die Erläuterung dieser einzelnen Konzeptbeschreibungen auf den Leser wirkt, als würden sie alle direkt nacheinander erfolgen. Das muss bei iterativen Vorgehensweisen aber ganz und gar nicht so sein. Wir beschreiben also nun alle Anwendungsfälle, also jeweils die Normalfälle ohne Ausnahmen. Wichtig ist hier, wie auch schon bei den Geschäftsfällen, dass ein Anwendungsfall kohärent ist, sprich, dass in den Anwendungsfall nur das reinkommt, was tatsächlich hineingehört und das Wesen dieses Falls ausmacht. Es stellt sich zum Beispiel die Frage, warum in unserem obigen Fall nicht Kunden berücksichtigt sind, die noch nicht registriert sind. Müsste das in den Anwendungsfall nicht mit aufgenommen werden? Die Frage, die man sich hier stellen muss, ist, würde das Aufnehmen des Zweigs »Kunde wird registriert« einen Einfluss auf das Ergebnis dieses Anwendungsfalls haben? Die Antwort ist: Nein. Es ist für unseren Anwendungsfall hier nicht von Belang, ob eine Person Kunde werden will und dann einen Film ausleiht oder ob sie schon Kunde ist und sich einen Film ausleiht. Fakt ist, dass sich in unserem Online-Portal nur Kunden einen Film ausleihen können. Wie sie zu Kunden geworden sind, ist nicht Bestandteil dieses Anwendungsfalls und gehört demzufolge auch nicht hier rein. Warum ist das wichtig? Durch die Kohärenz, also die inhaltliche Abgrenzung von Anwendungsfällen, sind wir überhaupt erst in der Lage, modular und objektorientiert und damit auch wieder iterativ zu arbeiten. Dadurch, dass wir hier den Fall der Neuregistrierung eines Kunden nicht mit reinnehmen, beeinflussen sich diese beiden Anwendungsfälle also auch nicht gegenseitig und können getrennt voneinander modelliert und umgesetzt werden. Das wiederum setzt die Komplexität der einzelnen Anwendungsfälle herab und erlaubt uns, die Kombination von Anwendungsfällen freier zu gestalten.
Ablaufdiagramme Bisher haben wir die Beschreibung unserer Anwendungsfälle hauptsächlich textlich vorgenommen. Nur die Beziehungen zwischen Anwendungsfällen waren grafisch. Ich habe schon erwähnt, dass eine rein textliche Beschreibung solcher Abläufe eventuell zu Missverständnissen führen kann und eventuell auch schwerer zu erfassen ist als ein grafisches Diagramm. Dazu kommt, dass wir mit einem Ablaufdiagramm nun noch detaillierter den vollständigen Ablauf eines Anwendungsfalls beschreiben wollen, und zwar mit allen Varianten und Ausnahmen. Solche Abläufe können recht komplex werden. Sie rein textlich zu beschreiben, würde zu großen Textmengen führen, die keiner mehr lesen will. Ablaufdiagramme werden auch als Flussdiagramme bezeichnet. In der UML nennt man sie Aktivitätsdiagramme. Wenn man ein Ablaufdiagramm für einen Anwendungsfall erstellt, fängt man der Einfachheit halber erst einmal damit an, den simplen Normalverlauf, so wie er auch im Anwen-
44
Zuhören und Lernen dungsfall beschrieben ist, grafisch aufzumalen. Für unseren Anwendungsfall »Verleih eines Films online« würde das also so aussehen wie in Abbildung 2.7 zu sehen.
Verleih eines Films online
Kunde identifizieren
Gewünschte Ausleihart aufnehmen
Zahlungsdetails aufnehmen
Vertragsbestätigung versenden
Vertrag abschließen
Vertragsdetails präsentieren
Film ausgeliehen
Abbildung 2.7: Ablaufdiagramm für den Normalfall von »Verleih eines Films online«
Wie man sehen kann, ist dieser Ablauf recht simpel und sequenziell gehalten, also genau so wie auch im Anwendungsfall beschrieben. Ein Ablaufdiagramm hat immer genau einen Startpunkt und einen oder mehrere Endpunkte. Nachdem der Anfang gemacht ist, muss man sich nun die verschiedenen Ausprägungen und Varianten überlegen, die innerhalb dieses Ablaufs denkbar sind, die aber immer noch konkret zu diesem Anwendungsfall gehören (wir erinnern uns, wir wollen kohärent bleiben). Das Einfachste, was einem einfällt, sind die Abbruchbedingungen. Ein Nutzer kann natürlich den Ablauf des Video-Ausleihens jederzeit abbrechen. Während der Identifizierung muss sich der Kunde zudem einloggen. Dabei können natürlich die angegebenen Nutzerdaten falsch sein. Dann muss der Nutzer eventuell erneut den Dialog betreten und die korrekten Daten eingeben. Ein vollständig beschriebener Ablauf für »Verleih eines Films online« könnte letztlich also so aussehen wie in Abbildung 2.8.
45
Kapitel 2
Verleih eines Films online nicht identifiziert
Kunde identifizieren
Abbruch Kunde nicht identifiziert - Abbruch
Kunde identifiziert
Gewünschte Ausleihart aufnehmen
Zahlungsdetails aufnehmen
Abbruch
Vertragsdetails präsentieren
Abbruch
Abbruch
Vertragsbestätigung versenden
Abbruch - Film nicht ausgeliehen
Vertrag abschließen
Film ausgeliehen
Abbildung 2.8: Vollständiges Ablaufdiagramm für »Verleih eine Films online«
Fachliche Klassen und Objekte Während man Anwendungsfälle und Ablaufdiagramme erstellt und sich mit dem Auftraggeber darüber unterhält, spricht man immer wieder über bestimmte Begriffe, die in den Abläufen relevante Rollen übernehmen. Bei unserem Video-Online-Portal fallen einem z. B. Begriffe ein wie: Film, Kunde, Vertrag, Rechnung, Ausleihart, Zahlungsmethode etc. Solche Entitäten, die fachlich für das Projekt eine Rolle spielen, nennt man Fachklassen. Als Entwickler kann man sich diese Klassen tatsächlich wie Klassen aus ActionScript vorstellen, denn ihr Sinn ist gleich. Der einzige Unterschied liegt darin, dass die fachlichen Klassen auf einer allgemeineren sprachlichen Ebene dem Auftraggeber, der vielleicht kein detailliertes technisches Verständnis hat, die wichtigen Entitäten und ihre Zusammenhänge darstellen und deutlich machen sollen. Deswegen wird für die Darstellung dieser Klassen meistens auch auf zu viele Details verzichtet, damit die Diagramme auch für technische Laien verständlich bleiben. Man definiert also keine Attribute und Methoden, und man modelliert auch nur die fachlich wichtigsten Klassen.
46
Zuhören und Lernen Schauen wir uns so ein fachliches Klassenmodell für die wichtigsten Klassen des VideoOnline-Portals der Firma FilmRegal an (Abbildung 2.9).
Kunde
Film
1
1..*
schließt ab regelt Ausleihe * *
Vertrag
1
1
«enumeration» Ausleihart 1
1 führt zu
enthält 1 «enumeration» Zahlungsmethode
1
Rechnung
Abbildung 2.9: Die wichtigsten Fachklassen für das FilmRegal-Online-Portal
Die Linien stellen Beziehungen zwischen den Klassen dar. Diese Linien haben keine Pfeile, denn wir wollen das Diagramm einfach halten. Die Zahlen stehen für die Multiplizitätsangaben. Man liest also z. B.: »Kein oder mehrere Verträge beziehen sich immer auf mindestens einen oder mehrere Filme.« Damit ist also einfach ausgesagt, dass natürlich auch Filme existieren, wenn noch gar kein Vertrag mit irgendwelchen Kunden existieren würde. Andersherum muss in einem Vertrag aber auf mindestens einen Film verwiesen werden, sonst würde ja der Vertrag keinen Sinn machen. Die Klassen, die mit «Enumeration» gekennzeichnet sind, bezeichnen Klassen, die eine zusammengehörige Sammlung von Werten darstellen. Es gibt also mehrere mögliche Zahlungsmöglichkeiten, wie z. B. Lastschrift, Kreditkarte oder Überweisung. Leider gibt es in ActionScript keine nativen Enumeratoren, wie das in Java oder C# der Fall ist, aber man kann sie natürlich simulieren. Es ist wichtig, darauf hinzuweisen, dass diese Fachklassen nicht zwingend eine Entsprechung im späteren tatsächlichen Klassenmodell haben müssen. Auch die Beziehungen mit ihren Mengenangaben stellen nur Informationen dar, die sich im Gespräch mit dem Auftraggeber ergeben haben, und müssen sich nicht zwingend in der späteren Anwendung wiederfinden, wenn sie für die Anwendung nicht erforderlich sind.
47
Kapitel 2 Bei der objektorientierten Analyse und dem objektorientierten Design orientiert man sich eben nur an der fachlichen Realität, das heißt also nicht, dass man sie eins zu eins übernehmen muss, wenn es für eine Anwendung gar nicht erforderlich wäre. Dieser Aspekt ist wichtig. Wir modellieren die fachlichen Klassen und Objekte nicht, um ein absolut vollständiges Modell mit allen denkbaren Abhängigkeiten zu erhalten. Stattdessen soll das Modell gerade so vollständig sein, dass wir damit unsere Anwendung abbilden können. Verfallen Sie also nicht in eine Modellierungswut und stellen Zusammenhänge dar, die für Ihr Projekt gar nicht relevant sind.
Schnittstellen und User-Interfaces Auch wenn wir in der Fachebene noch nicht zu technisch werden wollen, so müssen wir doch mindestens feststellen, wo die Grenzen unserer Anwendung liegen, die wir umsetzen wollen, und welche Schnittstellen also zu anderen Systemen oder Personen notwendig sind. Es muss z. B. geklärt werden, welche Art von Diensten wir nicht selbst in unserer Anwendung umsetzen wollen, sondern von extern beziehen. In unserem Videoportal könnte z. B. das Bezahlen via Kreditkarte ein Service sein, den wir selber nicht implementieren, sondern als Dienst von einem entsprechenden Drittunternehmen beziehen. Demzufolge ergibt sich hier also eine Schnittstelle zu diesem Dienstleister. In der fachlichen Ebene, in der wir uns hier befinden, müssen wir diese Schnittstelle noch nicht im Detail beschreiben, aber wir definieren, welche Services wir vom Dienstleister beziehen wollen. Bei Kreditkartenzahlung ist dies normalerweise einerseits die Gültigkeitsprüfung der angegebenen Kreditkarte, die Belastung der Karte mit dem entsprechenden Betrag sowie später im Hintergrund die Transaktion des Geldbetrages, die allerdings nicht mehr im konkreten Zusammenhang mit dem Videoportal steht und deswegen in der Anwendung nicht näher berücksichtigt werden muss. Die fachliche Beschreibung in tabellarischer Form könnte so aussehen:
Schnittstelle Kreditkartenzahlung Bezeichner
Beschreibung
Service
Zahlungssystem der Firma XY für Kreditkartenverbuchung
Art
Asynchrone Schnittstelle über HTTPS-POST
Genutzte Dienste
Plausibilitätsprüfung Online Autorisierung und Verbuchung eventuell Storno
Wichtigkeit
Hoch
Ansprechpartner
Herr Winkelmeier, Firma XY, Telefon: 123456789
Belastung
Abendstunden, bis zu 2.000 Transaktionen pro Tag
Tabelle 2.4: Mögliche fachliche Schnittstellenbeschreibung Kreditkartenzahlung
48
Zuhören und Lernen Neben Schnittstellen zu externen Systemen gibt es natürlich noch eine ganz wichtige Schnittstelle, nämlich die zu den involvierten Personen, also in unserem Fall den Kunden und natürlich auch intern den Mitarbeitern von FilmRegal sowie eventuellen Geschäftspartnern, die vielleicht auch Zugriff auf die Anwendung erhalten sollen. Gemeint sind natürlich die Benutzeroberflächen. Es hängt sehr stark von der Komplexität und der Art des Projekts ab, wie detailliert Benutzeroberflächen in der Fachebene beschrieben werden müssen. Gerade bei Projekten, die vielleicht weniger als Anwendungen, sondern eher als multimediale Präsentationen zu bezeichnen sind, kommt der Beschreibung der Benutzeroberfläche und des Inhalts eine ganz andere Bedeutung zu als bei einer Applikation. Eine besondere Schwierigkeit liegt darin, dass gerade bei Rich Internet Applications oftmals eine sehr interaktive Benutzeroberfläche gewünscht ist, die sich nicht allein mit einzelnen Screens darstellen lässt, weil sich die Anwendung vielleicht nicht über Seitenzustände definiert, sondern über Interaktionsschritte, die jeweils den Zustand der Darstellung nur in Teilen des Screens verändern. Um solche Interaktionsmöglichkeiten sinnvoll zu beschreiben, bedarf es einer Mischung aus textlicher und visueller Beschreibung, z. B. über Wireframes. Wireframes sind abstrakt gehaltene Darstellungen von Benutzeroberflächen, die nicht das endgültige Design zeigen, sondern nur eine schematische Darstellung der Oberfläche mit den wichtigsten Elementen. Sie sollen die grundsätzliche Darstellung der Informationen und Nutzerbedienelemente zeigen. Wie das Ganze später tatsächlich gestaltet wird, entscheidet der Designer in der Umsetzung. Hier ein kleines Wireframe für einen Login-Dialog:
Login
Username
Submit
Password
Request Password
Abbildung 2.10: Beispiel Wireframe für einen Login-Dialog
Auch die konkrete Erstellung von Clickdummies kann in diesem Fall bereits nötig sein, um im Vorfeld zu testen, ob eine bestimmte Art der Nutzerinteraktion benutzerfreundlich ist. Um besondere Interaktion und Animation zu beschreiben, können auch Storyboards hilfreich sein, die in beispielhaften Schritten eine Interaktion oder eine Animation skizzieren.
49
Kapitel 2
Vorsicht mit Clickdummies! Clickdummies (auch Mockups, Prototypen oder Proof-of-Concept-Anwendungen genannt) sind eine sehr effektive Art, Benutzeroberflächen gestalterisch wie auch funktional zu testen und zu optimieren. Ein Fehler, der allerdings oft gemacht wird, ist, diese Clickdummies im Verlauf des Projekts direkt weiterzuverwenden. Gerade nichttechnische Personen denken oft, dass man die Benutzeroberfläche doch schon umgesetzt hätte, und sehen zunächst keinen Grund, warum die Umsetzung noch mal von vorn beginnen sollte. Auch wenn nicht ausgeschlossen werden muss, dass sich Teile eines Clickdummies weiterverwenden lassen können, so ist doch die Intention eines Clickdummies eine ganz andere als die bei der Erstellung der tatsächlichen Benutzeroberfläche später: Bei einem Clickdummy soll in möglichst kurzer Zeit eine Benutzeroberfläche gebaut werden, die sehr oft verändert wird, weil sie mit vielen Personen diskutiert wird. Der Fokus liegt darauf, zentrale Merkmale des Verhaltens der Oberfläche zu zeigen. Eventuell sind manche Aspekte wie z. B. Ladevorgänge, Performance oder auch manch funktionale Bereiche noch gar nicht berücksichtigt. Bei der tatsächlichen Benutzeroberfläche für die konkrete Anwendung hingegen spielen ganz andere und viel mehr Aspekte eine Rolle, wie z. B. Stabilität, Wartbarkeit, Vollständigkeit, Performance und viele mehr. Deswegen sollte in der Planung zunächst immer davon ausgegangen werden, dass Clickdummies nicht weiterverwendet werden, auch wenn es in der Praxis in Einzelfällen tatsächlich doch sinnvoll ist.
Letztendlich wichtig ist, die einzelnen Benutzeroberflächenbeschreibungen, Wireframes etc. immer wieder in einen Zusammenhang mit den zuvor beschriebenen Anwendungsfällen zu setzen, damit hier nicht indirekt neue oder unterschiedliche Anwendungsfälle erzeugt werden, die eigentlich nicht vorgesehen waren. Andererseits kann auch die Arbeit mit den Benutzeroberflächen zeigen, dass tatsächlich ein Anwendungsfall entweder nicht vollständig beschrieben oder sogar ganz vergessen wurde. Dann aber sollten solche Fälle entweder neu beschrieben oder entsprechend angepasst werden.
2.2.3 Umsetzungsebene In der Umsetzungsebene wird nun das fachliche Modell aus der Fachebene in ein nun sehr konkretes Modell für die Umsetzung in Software übersetzt. Ich gehe an dieser Stelle nicht näher auf die Umsetzungsebene ein, denn damit wird sich das komplette folgende Kapitel 3 beschäftigen.
2.2.4 Lasten- und Pflichtenheft In einer idealen Welt werden die Anforderungen in zwei Teile geteilt, einem Lastenheft und einem Pflichtenheft. Das Lastenheft wird vom Auftraggeber erstellt und beschreibt in natürlicher Sprache die Anforderungen aus Geschäftssicht. In einem Lastenheft für unser Videoportal FilmRegal könnte z. B. stehen: Die Nutzer des Portals sollen auf Wunsch die komplette Historie ihrer ausgeliehenen Filme nachvollziehen können. Es ist wichtig, dass ein Lastenheft möglichst keine Anhaltspunkte auf die Umsetzung der Anforderungen enthalten sollte, da damit eventuell Einschränkungen vorgenommen würden, die von keiner Seite wirklich gewünscht werden. Sehr wohl aber können und sollten
50
Zuhören und Lernen im Lastenheft Randbedingungen festgehalten werden, die sich aufgrund von Notwendigkeiten beim Auftraggeber stellen. Bei unserem Videoportal könnte das z. B. sein: Die vorhandene Hardware- und Softwareinfrastruktur der IT-Abteilung soll für das Portal voll ausgenutzt werden. Dieser Satz ist natürlich sehr allgemein, und es ist nicht abzusehen, wieweit denn in der Praxis die Hard- und Software tatsächlich sinnvoll ausgenutzt werden kann. Diese Details sind aber auch nicht entscheidend. Für die beauftragte Softwarefirma oder die Agentur lässt sich in jedem Fall der Auftrag herausziehen, dass man sich mit der IT-Abteilung des Auftraggebers über die konkret zur Verfügung stehende Hard- und Softwareinfrastruktur unterhält und gemeinsam entscheidet, was davon nutzbar ist. Solche Randbedingungen haben damit natürlich sehr wohl direkte Auswirkungen auf die Umsetzung, sind aber trotzdem ein wichtiger Bestandteil des Lastenhefts. Das Pflichtenheft auf der anderen Seite ist ein korrespondierendes Dokument des Auftragnehmers. In ihm wird der konkrete Lösungsvorschlag beschrieben. Wie detailliert dies getan wird, hängt vom jeweiligen Projekt ab und auch vom Vorgehensmodell, das beide Parteien gewählt haben. In einem iterativen Vorgehen werden im Pflichtenheft nur Eckpunkte definiert und mit der Zeit ausgearbeitet, wohingegen bei einem klassischen Wasserfallmodell alle Merkmale im Voraus beschrieben werden. In der Realität ist diese klare Trennung zwischen Lastenheft und Pflichtenheft selten einzuhalten. Es wird oft als eine gewünschte Leistung seitens des Kunden angesehen, dass der Dienstleister den Kunden bei der genauen Ausarbeitung der detaillierten Anforderungen unterstützt. Das ist verständlich, denn wie in den vorangegangenen Kapiteln schon erwähnt, ist der Prozess der Analyse der Anforderungen schwierig, und es kann durchaus hilfreich sein, wenn ein außenstehender Dienstleister zusammen mit dem Kunden die Anforderungen beleuchtet, denn er sieht unter Umständen die Abläufe klarer als ein interner Mitarbeiter, der viele Sachverhalte eventuell für selbstverständlich hält und sie deswegen nicht näher betrachtet. Nicht zuletzt deswegen weisen alle modernen Vorgehensmodelle zur Softwareentwicklung – allen voran der Bereich der agilen Softwareentwicklung – auf die Notwendigkeit hin, den Kunden in jeder Phase eines Softwareprojekts aktiv mit einzubeziehen und ihn nicht erst das fertige Produkt abnehmen zu lassen.
2.3 Literaturangaben Balzert, Heide: Lehrbuch der Objektmodellierung. Analyse und Entwurf; mit CD-ROM. Heidelberg: Spektrum Akad. Verl., 1999. Oestereich, Bernd: Analyse und Design mit UML 2. Objektorientierte Softwareentwicklung; (inkl. Poster mit UML-Notationsübersicht & OEP-Vorgehensübersicht). 7., aktualisierte Aufl. München: Oldenbourg, 2005.
51
3
Lösungen entwerfen
Einer der großen Stärken der Menschen ist es, als Vernunftwesen Zusammenhänge allein im Kopf zu durchdenken und daraus abstrakte Erkenntnisse zu gewinnen. Wir können aus konkreten beobachteten Vorgängen Rückschlüsse auf zugrunde liegende Zusammenhänge schließen, ohne sie zwingend durch konkrete Erfahrung erlangt zu haben. Wenn wir einmal gesehen haben, wie ein Tennisball vom Boden oder einer Wand abprallt, beginnen wir, in Gedanken die Laufbahn des Balles vorauszuahnen, wenn wir ihn in eine bestimmte Richtung werfen würden, ohne es tatsächlich tun zu müssen. Wir sind außerdem in der Lage, von konkreten Dingen zu abstrahieren, und wir erkennen somit, dass sich mit großer Wahrscheinlichkeit jeder ballrunde Gegenstand, der aus einem elastischen Material besteht, ähnlich zu dem Tennisball verhalten wird.
Abbildung 3.1: Wir haben eine ungefähre Vorstellung davon, wie der Ball fliegen wird.
Kapitel 3 Diese Fähigkeit hilft uns in fast allen Lebenslagen, und sie wird in der Softwareentwicklung gleichfalls enorm gefordert. Ein Softwareentwickler muss Vorgänge und Zusammenhänge aus der realen Welt erkennen und verstehen und sie in einer virtuellen Welt zumeist in abstrakter Weise nachbilden. Neben den vielen Herausforderungen in allen Bereichen der Softwareentwicklung sind das Verstehen und Abstrahieren einer realen Problemstellung eine besonders schwierige Aufgabe. Sie ist es auch deshalb, weil jede Problemstellung in ihren Details meist individuell ist und nicht direkt vergleichbar mit vorangegangenen Projekten. Der Bau von Software wird hier gerne mit dem Bau von Gebäuden verglichen. Wir können uns so den Bau von Software als das langsame Errichten eines Gebäudes vorstellen, angefangen von den ersten Gesprächen mit dem Bauherren, der Arbeit des Architekten, der Bauingenieure und letztlich der Maurer, Elektriker und der vielen anderen Fachleute, die am Bau eines Gebäudes beschäftigt sind. Viele Parallelen lassen sich zwischen der Softwareentwicklung und dem Gebäudebau erkennen, gerade was Prozesse angeht. Der Vergleich hinkt aber auch. In der Softwareentwicklung wird in der Regel nicht mit realen Materialien gearbeitet, die verbraucht werden, abgesehen von der Arbeitskraft selbst. Das klingt zunächst vielleicht trivial, ist aber ein entscheidender Unterschied. Die langwierigen Prozesse im Gebäudebau existieren nicht zuletzt deswegen, weil man sich hier schon aus Gründen des Materialsverlusts keine großen Fehler erlauben darf. Ist das Brett gesägt, die Mauer errichtet oder das Fundament gegossen, dann kann man nicht mehr so leicht zurück, denn nicht nur kostet das viel Zeit, auch muss neues Material besorgt werden, denn das alte ist nun verloren. In der Softwareentwicklung ist das anders. Softwareentwickler verbrauchen kein Material. Ihr Material ist virtuell, lässt sich bei Bedarf verlustfrei duplizieren, teilen, neu zusammensetzen – und das so oft man will. Das ist wohl auch einer der Reize, Software zu bauen. Man kann komplexe Systeme bauen ohne großen Materialaufwand. Es sind vielleicht diese Freiheit und die Entkopplung vom Materialaufwand, die manchen Softwareentwickler zuweilen denken lässt, dass es keinen Grund gäbe, langwierige Planungen vorzunehmen, wie es eben z. B. beim Gebäudebau notwendig ist. Klar, die Bauingenieure brauchen Bau- und Zeitpläne, weil sonst ein schiefes Gebäude entsteht und vielleicht einstürzt. Aber bei Software kann man ja jederzeit, wenn was nicht passt, die Entfernen-Taste drücken und eine verbesserte Version einsetzen. Man ist losgelöst von der Gefahr, Material zu vergeuden und nicht mehr zurückzukönnen. Warum also planen, warum nicht einfach anfangen und dann zuschauen, wie sich das virtuelle Konstrukt langsam entwickelt, und einfach gegensteuern, wenn etwas nicht korrekt funktioniert? Der Gedankengang hat einen Haken. Er nimmt an, wir hätten beliebig viel Zeit zur Verfügung und dass wir innerhalb unserer Zeit über Versuch und Irrtum zum Ziel kommen und dabei alle unsere Ziele erreichen könnten. Das ist natürlich nicht so. Jeder Entwickler weiß, dass Softwareprojekte sich in zumeist enorm engen Zeitplänen bewegen. Bei unsauber geplanten Projekten führt dies oft dazu, dass vielleicht die Anwendung noch grundsätzlich gerade so funktioniert, dass sie aber meistens nicht fehlerfrei ist und dass Nebenziele wie Wartbarkeit und Lesbarkeit selten zufriedenstellend erreicht werden.
54
Lösungen entwerfen Damit erklärt sich auch, dass Softwareprojekte natürlich einen Materialeinsatz haben, nämlich genau die erwähnte Zeit. Wenn ein Flash-Entwickler eine Komponente baut, von der man hinterher feststellt, dass man sie so gar nicht gebrauchen kann, dann ist die Komponente das Resultat von Materialverschwendung, nämlich von Zeit. In einem kleinen Projekt mag es vielleicht nicht so auffallen, wenn mal etwas unnötigerweise programmiert wurde, in großen Projekten, wo vielleicht aufgrund schlechter Planung ein ganzes Modul nicht verwendet werden kann, weil es nicht den Anforderungen genügt, wirkt sich das teilweise erheblich auf die Gesamtkosten und auf das Gesamtprojekt aus, weil oft die anderen Module von diesem in irgendeiner Art und Weise abhängen und meist der Zeitplan nicht mehr gehalten werden kann. Auch stimmt die Annahme nicht, es gebe im Bereich der Softwareentwicklung nicht so was wie Fundamente, die – einmal gegossen – nicht mehr verändert werden können. Auch in Softwareprojekten ist irgendwann ein Zeitpunkt erreicht, wo man zentrale Teile einer Anwendung nicht mehr ohne Weiteres ändern kann, weil viele andere Teile stark davon abhängen. Nun könnte man natürlich argumentieren, dass die Anwendung in diesem Fall schlecht strukturiert wäre, aber auch in einer gut strukturierten Anwendung gibt es Teile, die zentrale Aufgaben übernehmen und z. B. stark mit den zuvor definierten Geschäftsprozessen verknüpft sind. Man kann sie dann zwar auch noch ändern, aber der Aufwand wird erheblich sein. Wichtig ist auch, dass Software zwar virtuell ist, sie aber nie in einer virtuellen Welt eingebettet ist, sondern in einer realen Welt. Das bedeutet, dass sich die Entwicklung eines Programms nicht nur den Zwängen und Limitierungen der Programmiersprache oder der virtuellen Maschine zu unterwerfen hat, sondern auch den realen Sachzwängen der sie umgebenden Welt. Wenn also z. B. ein Auftraggeber durch Nutzertracking herausgefunden hat, dass der überwiegende Teil seiner Zielgruppe den Flash Player einer ganz bestimmten Version verwendet, dann hilft alles nichts, dann muss für diesen Player entwickelt werden, auch wenn dies bedeutet, dass bestimmte Features eventuell nicht genutzt werden können. Wenn ein Aufraggeber intern bei all seinen Projekten eine bestimmte Serverkomponente einsetzt und er aus Kosten- oder auch Know-how-Gründen auch beim neuen Projekt diese Komponente wieder einsetzen will, dann muss das mit den restlichen Anforderungen abgewägt werden. Eine Struktur zu entwickeln, die sowohl die konkreten funktionalen Aspekte einer Aufgabenstellung als auch die Sachzwänge des Projekts berücksichtigt, ist die schwierige, aber auch spannende Aufgabe jedes Softwareentwicklers, im Großen wie im Kleinen. Der Softwareentwurf erlaubt uns hier, Ideen zu entwickeln und in der Theorie auszuprobieren, ohne dass wir bereits große Mengen an Code schreiben müssen. Während des Entwurfs können wir Fehler machen. Da es sich quasi um Trockenübungen handelt, kosten diese Fehler weniger, als wenn wir alles gleich durchprogrammieren würden. Wir können mit Strukturen spielen und ausprobieren, ob sie der Belastung der vielen Anforderungen standhalten würden.
55
Kapitel 3
Abbildung 3.2: Viele unterschiedliche Anforderungen müssen in einem Softwareprojekt berücksichtigt werden.
Das Besondere am Softwareentwurf ist nun, dass es keinen allgemeingültigen Weg zu einer gut strukturierten Anwendung gibt, denn die meisten Anwendungen, die heutzutage entwickelt werden, sind Individualanwendungen, deren Problemkreis sich so gut wie immer von vorangegangenen Projekten unterscheidet. Deswegen ist der technische Entwurf einer Anwendung auch ein kreativer Prozess, denn es geht hier nicht um das Reproduzieren von bekannten Mustern, sondern meistens um das erstmalige Finden einer Struktur, die für das anstehende Problem am besten geeignet scheint. Dass sich in diesen Strukturen dann im Detail doch oft wieder Muster finden (von denen ich in Kapitel 4 einige beschreiben werde), ist tröstlich, ändert aber nichts an der Herausforderung. Wie man nun beim Entwurf genau vorgehen sollte, wird in Kapitel 6 organisatorisch und in diesem Kapitel inhaltlich erklärt. Nicht immer kann oder sollte man die komplette Struktur einer Anwendung von Anfang an erarbeiten, geschweige denn im Kopf haben. Der Entwurf sollte nicht als eine lästige Aufgabe angesehen werden, die man erst erledigen muss, bevor man umsetzen darf. Der Entwurf sollte vielmehr als ein Hilfsmittel angesehen werden, den wir im Wechsel mit der Umsetzung immer wieder einsetzen können, bevor wir den nächsten Schritt tun. Wenn das nächste Modul oder der nächste Teilbereich der Anwendung angegangen werden soll, dann entwerfen wir zuerst ein paar Ideen, wie wir diesen Teil strukturieren könnten, prüfen die Idee auf ihre Tragfestigkeit und setzen sie dann um. So kommen wir Schritt für Schritt zur Gesamtanwendung. Die Entwicklungen im Bereich des Software Engineerings der letzten Jahrzehnte haben auf eines kontinuierlich hingewirkt: die Verringerung von Komplexität beim Bau von Software. Heutige Anwendungen, auch im Flash-Bereich, sind bereits so komplex, dass nur noch selten ein einzelner Flash-Entwickler – sehr kleine Projekte vielleicht ausgenommen – die komplette Anwendung bis in Details überblickt. Und auch bei einem kleinen Projekt, das eventuell Drittbibliotheken verwendet, wird ein Flash-Entwickler eher selten die genaue
56
Lösungen entwerfen Arbeitsweise dieser Bibliotheken kennen. Durch den Einsatz dieser Bibliotheken hat dieser Entwickler aber schon eine wichtige Entscheidung getroffen: Er hat für sich Komplexität reduziert, denn er muss sich nicht für den internen Aufbau der Bibliothek interessieren, er muss lediglich wissen, wie sie funktioniert. Die Entscheidung, in einem Projekt eine Bibliothek einzusetzen, ist eine der vielen Grundinstrumentarien, die Softwareentwickler zur Verfügung haben, um Komplexität zu verringern, neben vielen anderen, die wir noch besprechen werden. Es ist eine Entscheidung im Bereich der Softwarearchitektur.
3.1 Architektur versus Entwurf Im Software Engineering schwirren die Begriffe Architektur und Entwurf bzw. Design mit unterschiedlichen Bedeutungen herum. Wenn manch ein Entwickler sagt, die Architektur seiner Anwendung sei den Anforderungen gerecht geworden, dann ist man nicht immer sicher, ob er die konkrete Klassenstruktur meint oder allgemeine Entscheidungen, beispielsweise dass er Cairngorm eingesetzt hat. Es gibt aber einen Unterschied zwischen der Architektur eines Softwaresystems und dem Entwurf bzw. Design (mit Design ist hier immer der Softwareentwurf gemeint, nicht etwa die visuelle Gestaltung), und es ist auch sinnvoll, diese Unterscheidung zu treffen. Kazman, Clements und Bass schreiben: »The software architecture of a program or computing system is the structure or structures of the system, which comprise software elements, the externally visible properties of those elements, and the relationships among them.« (Bass et al. 2008, S. 21) Man kann also grob sagen: Eine Softwarearchitektur beschreibt den Aufbau und das Zusammenspiel einzelner Softwareelemente. Eine Softwarearchitektur kann dabei eingebunden sein in eine Systemarchitektur, bei der dann auch noch neben der Software die Hardware eine Rolle spielt. Eine Systemarchitektur würde also konkret auch die Hardwarekomponenten und ihre Verknüpfungen beschreiben.
Systemarchitektur
Sicherheitsarchitektur
Softwarearchitektur
Softwareentwurf Netzwerkarchitektur
... Abbildung 3.3: Einordnung von System- und Softwarearchitektur sowie Softwareentwurf
57
Kapitel 3 Der Softwareentwurf hingegen beschreibt jeweils den inneren Aufbau und die Funktionsweise einer einzelnen Softwarekomponente, also zum Beispiel eines konkreten FlashModuls.
3.1.1 Softwarearchitektur In der Softwarearchitektur werden globale Entscheidungen getroffen, die die Anwendung als Ganzes betreffen. Z. B. wird hier die Entscheidung getroffen, ob und, wenn ja, welche Flash Player-Version verwendet wird. Alle Softwarekomponenten werden festgelegt, z. B. welcher Application Server kommt zum Einsatz, welcher Webserver, wird Clustering verwendet usw. Außerdem werden die grundsätzlichen Schnittstellen festgelegt, z. B. über welches Schnittstellenprotokoll spricht Flash mit dem Server (z. B. Remoting, XML etc.). Es werden prinzipielle Vorgaben für den folgenden Softwareentwurf gemacht, z. B. sollen objektorientierte Prinzipien eingesetzt werden oder vielleicht Aspektorientierung. Es werden grundsätzliche Frameworks vorgegeben, also Flex, Einsatz von Architektur-Frameworks wie Cairngorm usw. Auch Anforderungen, die Auswirkungen auf spätere Phasen des Projekts haben können, werden hier definiert, wie dass alle Teile der Anwendung Nutzertracking implementieren sollen und deswegen dafür ein zentrales Modul vorgesehen werden muss. Die Softwarearchitektur schaut bei einem Projekt also auf einen recht hohen Abstraktionslevel. Hier geht es noch nicht um einzelne Module oder gar um Klassen. Hier wird ermittelt, welche grundsätzlichen Teile für die Anwendung notwendig sind und wie diese zusammenwirken sollen. Dabei muss der Softwarearchitekt zusammen mit dem Kunden ausloten, welche Systeme eingesetzt werden können, welche Limitierungen es gibt und welche konkreten Anforderungen an die Anwendung gestellt werden, die eventuell Einfluss auf die Gesamtkonstruktion der Anwendung haben können. In den meisten Fällen bringt der Kunde eine ganze Reihe an Einschränkungen und Anforderungen mit, die in der Softwarearchitektur zu berücksichtigen sind. Dazu kann gehören, dass das Hosting einer Website auf betriebsinternen Servern durchgeführt werden soll, die eine bestimmte Ausstattung haben, die nicht oder nur eingeschränkt verändert werden kann. Das kann Auswirkungen auf die dort installierbaren Komponenten haben, wie z. B. Typ und Version des Webservers oder Application Servers. Die eingesetzte Infrastruktur kann es eventuell nötig machen, dass möglichst wenig serverseitig dynamische Dienste angeboten werden sollen, weil die Belastungsfähigkeit der Server eingeschränkt ist. Eventuell kann nur eine bestimmte Version des Flash Players eingesetzt werden, weil nur diese auf den Rechnern des Kunden verfügbar ist und sie nicht so schnell unternehmensweit geupdatet werden kann. Der Softwarearchitekt muss zusammen mit dem Kunden alle Bedingungen und Anforderungen identifizieren, die einen Einfluss auf die geplante Anwendung haben können, und gerade bei internetbasierten Systemen ist meist eine Menge kleiner Teilbereiche involviert. Auch Anforderungen, die sich aus der Anwendung selbst ergeben, können einen starken Einfluss auf die Architektur haben. Wenn die Anwendung eine sehr hohe Ausfallsicherheit haben muss, weil von ihr z. B. andere Unternehmensprozesse abhängen (z. B. ein OnlineShop bei einem Unternehmen, was seine Produkte hauptsächlich online vertreibt), dann
58
Lösungen entwerfen muss in der Softwarearchitektur entsprechende Redundanz vorgesehen werden, also das mehrfache Vorhandensein einer Komponente, falls eine von ihnen mal ausfällt. Eine Anwendung, die eine sehr breite Zielgruppe ansprechen soll, kann wiederum eventuell nicht nur auf Flash als Client-Technologie setzen, sondern muss Alternativen für Nutzer ohne Flash anbieten. Eine Anwendung, die eine hohe zu erwartende Lebensdauer hat, muss von der Architektur her eventuell modularer aufgebaut sein, damit mit der Zeit veraltete Teile gegen neue ausgetauscht oder damit fehlerhafte Teile isoliert vom Rest der Anwendung verbessert werden können. Die Softwarearchitektur bestimmt also das grundsätzliche Gerüst der Anwendung. Fehlentscheidungen in der Architektur können enorm negative Auswirkungen in allen nachfolgenden Phasen eines Projekts zur Folge haben, die teilweise nur durch gewaltige Aufwände wieder ausgebügelt werden können. Deswegen ist es umso wichtiger, dass die grundsätzliche Architektur einer Anwendung sorgfältig durchdacht wird. Iterative Vorgehensweisen wie bei der agilen Softwareentwicklung versuchen, den Druck vom Bereich der Softwarearchitektur zu nehmen, indem sie in wiederkehrenden Zyklen kleinere Teile einer großen Anwendung beschreiben. Nichtsdestotrotz müssen bestimmte Grundentscheidungen bereits zu Beginn getroffen werden, was bedeutet, dass sich Kunde und Dienstleister auf bestimmte Eckpfeiler bereits zu Beginn eines Projekts einigen müssen. Ich werde in diesem Buch das Thema Softwarearchitektur nicht im Detail behandeln, weil es aus Sicht von Flash-/Flex- und ActionScript-Entwicklung ein vorgelagertes Thema ist und ich die Themen rund um den Softwareentwurf für Flash- und Flex-Entwickler für wichtiger erachte.
3.1.2 Softwareentwurf Beim Softwareentwurf geht man nun wieder eine Ebene tiefer. Wurden in der Architektur die einzelnen Komponenten und ihre grundsätzlichen Schnittstellen erfasst, so werden diese einzelnen Komponenten nun genauer unter die Lupe genommen. Wie schon im vorigen Abschnitt erwähnt, besteht ein Gesamtsoftwaresystem zumeist aus Modulen unterschiedlichster Art. Im Softwareentwurf beschreiben wir diese Module nun im Detail. Dabei orientiert man sich wieder zunächst an den Anwendungsfällen und den fachlichen Klassen und Objekten, die wir in Kapitel 2 kennengelernt haben, und schaut nun, wie man diese technisch so strukturieren kann, dass daraus später eine Anwendung gebaut werden kann. Die Schwierigkeit besteht hier nun einmal darin, dass eine Anwendung nicht nur eine Ansammlung fachlicher Klassen ist, sondern auch aus sehr technischen Objekten besteht, die mit dem fachlichen Problem erst einmal nichts zu tun haben. Eine Preloader-Sequenz z. B. ist ein rein technisches Konstrukt und findet sich natürlich nicht in der fachlichen Beschreibung der Anwendungsfälle wieder. Beim Softwareentwurf müssen wir also zusätzlich zu den fachlichen Klassen und Objekten auch noch technische Objekte hinzufügen, die letztlich die Anwendung komplettieren. Auch die Struktur, die sich dabei herauskristallisiert, ist nun
59
Kapitel 3 eine Mischung aus den Beziehungen, die wir schon in der fachlichen Beschreibung modelliert hatten, und den technischen Schnittstellen, die sich erst jetzt ergeben.
Abbildung 3.4: Aus der konzeptionellen Sicht wird der Entwurf abgeleitet.
Eine wichtige Frage, die sich beim Softwareentwurf immer stellt, ist die Frage, wie tief man einsteigen will. In Kapitel 6 werde ich noch darauf zu sprechen kommen. Die iterativen Vorgehensmodelle legen nahe, dass man zu Beginn nicht die gesamte Anwendung bis ins Detail entwerfen sollte bzw. kann. Softwareprojekte sind häufig von mehreren Änderungen oder Erweiterungen der Anforderungen gekennzeichnet, sodass ein kompletter Entwurf der Anwendung nicht lange Bestand haben würde. Die Grundregel lautet also, man entwirft schrittweise einzelne, überschaubare Teile – und das iterativ immer wieder. Zusätzlich ist es unverzichtbar, für die Gesamtanwendung zumindest eine grundsätzliche Struktur zu entwerfen, die die Basis für die folgenden Iterationen bildet. Der Knackpunkt hier ist, dass der Fokus der Entwickler die meiste Zeit auf Details liegt anstatt auf der Gesamtanwendung. Pro Iteration müssen einzelne Module fertiggestellt und integriert werden, dabei betrachtet man die Module und ihre unmittelbar benachbarten Module, aber nicht zwingend die Gesamtstruktur. Die Gefahr ist, dass man zu spät die Gesamtstruktur begutachtet, um noch eingreifen zu können. Ohne eine Grundstruktur, ein Gerüst, besteht die Gefahr, dass sich das Projekt verselbstständigt und in einer Gesamtstruktur mündet, die irgendwann einem Flickenteppich gleicht und nur noch schwer zu warten ist. Deswegen ist es wichtig, dass bereits zu Beginn eine allgemeine Struktur erarbeitet wird, die den Rahmen für die kommende Entwicklung vorgibt. Innerhalb einer Iteration kann man sich dann detailliert an einen Aspekt der Anwendung machen und diesen entwerfen. Wie detailliert man dabei vorgeht, hängt von vielen Faktoren ab. Ist die Person, die den Entwurf macht, die gleiche, die auch die Umsetzung machen wird? Wenn nein, ist die Person, die die Umsetzung macht, erfahren oder noch ein Berufseinsteiger? Was sind die Anforde-
60
Lösungen entwerfen rungen an die Dokumentation der Anwendung? Wird eine detaillierte Beschreibung des Entwurfs vielleicht für kommende Module noch benötigt? Unabhängig von diesen Fragen wird man während des Softwareentwurfs selten tiefer als bis zur Definition der öffentlichen Methoden und Eigenschaften einer Klasse gehen, die also die Schnittstelle definieren, denn alle Belange, die noch tiefer gehen, werden normalerweise erst während der konkreten Implementierung entschieden.
Objektorientierter Entwurf Objektorientierter Entwurf ist nach der objektorientierten Analyse der nächste Schritt hin zu einem Softwareprodukt. Wobei nächster Schritt, wie in diesem Buch schon oft erwähnt, nicht wörtlich zu verstehen ist, weil je nach Vorgehensweise öfter zwischen Anforderungsanalyse und Softwareentwurf sowie Implementierung hin und her gesprungen wird. Wurde in der Analyse das Problem eingekreist und beschrieben, so versuchen wir beim Entwurf nun, die Lösung zu skizzieren. In der Analyse haben wir das Problemfeld – z. B. den Online-Videoverleih in unserem Beispiel der Firma FilmRegal – analysiert, Geschäftsfälle identifiziert, Anwendungsfälle beschrieben und fachliche Klassen modelliert. Wir haben die vor uns stehende Aufgabe fachlich erfasst. Nun geht es an die Beschreibung der Lösungsidee, also der Idee, die die Umsetzung der fachlichen Aufgabe mittels Software beschreibt. Weil wir objektorientiert arbeiten, gelten auch beim Entwurf wieder ähnliche Methodiken wie bei der objektorientierten Analyse. Am wichtigsten jedoch ist, wir verwenden die Ergebnisse, die Modelle und Beschreibungen aus der Analysephase als Basis für unsere Modelle im Entwurf. Haben wir also ein fachliches Objektmodell in der Analyse- bzw. Konzeptionsphase erstellt, so können wir dies nun nahtlos als Basis für unsere Überlegungen in der Entwurfsphase weiterverwenden. Es kommt nicht selten vor, dass z. B. viele der fachlichen Klassen zu konkreten Klassen in der Anwendung werden. Wurden im Fachkonzept fachliche Klassen wie Film, Leihvertrag und Kunde definiert, so kommen solche Klassen dann oft direkt auch in der Anwendung vor, natürlich viel konkreter.
3.2 Komplexität Die ersten Computerprogramme Mitte der Vierzigerjahre des letzten Jahrhunderts waren hauptsächlich für die Mathematik bestimmt. Sie wurden auf Lochkarten notiert. Die Komplexität dieser Programme hielt sich in Grenzen, wenn man sie mit heutigen Programmen vergleicht. Mit Weiterentwicklung der Hardware, also mit Zunahme der Rechenkapazität, und vor allem mit Erfindung neuer Schnittstellen zu anderen Systemen wie z. B. Druckern, Maschinen usw. ergaben sich immer mehr Einsatzbereiche, und die Computerprogramme mussten nun nicht mehr nur mathematische Berechnungen durchführen, sondern ungleich mehr. Damit stieg natürlich auch unweigerlich die Komplexität der Computerprogramme. Je mehr Möglichkeiten sich ergaben, Probleme und Aufgaben mit dem Computer zu lösen, umso komplexer wurden auch die Anforderungen an diese Programme.
61
Kapitel 3 Die Flash-Entwickler unter Ihnen, die die Geschichte von Flash von relativ früh an mitgemacht haben, können das bestimmt auch nachvollziehen. Ich selbst habe mit Flash 3 begonnen. Die Möglichkeiten von Flash 3 sind mit denen der heutigen Flash-Plattform nicht zu vergleichen. Die Komplexität der Animationen und kleinen Anwendungen, die man zu der Zeit mit Flash umsetzen konnte, war im gleichen Maße überschaubar. Je mehr Flash und ActionScript an Funktionalität wuchsen, je performanter der Flash Player mit der Zeit wurde, umso komplexer wurden auch die Anwendungen, die damit umgesetzt wurden. Bestanden frühe Präsentationen noch aus relativ wenig Code, der meist noch direkt in der Flash IDE direkt eingegeben wurde, so konnte man mit Flash 5 schon recht komplexe Anwendungen mit ausgelagertem Code bauen. Einzig die Performance hielt hier die Komplexität eventuell noch im Zaum, und manche Arten von Anwendungen waren noch nicht möglich, weil die Flash API schlichtweg nicht die entsprechenden Funktionen bereithielt. Heute ist Flash zu einer recht großen Plattform herangewachsen und bietet eine Fülle von Funktionalitäten, die für einen einzelnen Entwickler nur noch schwierig bis gar nicht komplett zu beherrschen ist. Und im gleichen Maße steigt auch die Komplexität der mit Flash umgesetzten Anwendungen, die man im Internet bewundern kann, an. Es ist also nicht nur die Komplexität der zur Verfügung stehenden Funktionen der Flash API gemeint, sondern auch die Komplexität der Anwendungen, die mit Flash und Flex umgesetzt werden können. Was aber ist das Problem bei der Komplexität? Menschen haben Schwierigkeiten mit komplexen Vorgängen oder Zusammenhängen. Wir können zwar viele Informationen abspeichern, aber nicht in kurzen Zeiträumen. Wir erinnern uns zwar an viele Dinge in unserem Leben, aber wir können uns bewusst nur mit einem kleinen Teil an Informationen auseinandersetzen. Deswegen zerlegen wir gedanklich komplexe Strukturen so lange, bis die kleineren Teile wieder von uns erfassbar sind. Besonders schwer fallen uns komplexe Systeme, die sich über den Faktor Zeit erstrecken. Ein simples Beispiel hier ist die Heizung. Wenn wir ein Thermometer in die Hand bekommen und in einem Raum den Thermostaten an einem Heizkörper so einstellen sollen, dass eine bestimmte Temperatur im Raum erreicht wird, dann nähern wir uns dieser Temperatur meist nur mit großen Schwankungen an, weil ein Drehen am Thermostat ja erst nach einiger Zeit seine Wirkung zeigt. Dietrich Dörner schreibt: »Die Existenz von vielen, voneinander abhängigen Merkmalen in einem Ausschnitt der Realität wollen wir als »Komplexität« bezeichnen.« (Dörner 2008, S. 60) Damit ist gemeint, je mehr Eigenschaften sich in einer Problemstellung befinden, die sich gegenseitig beeinflussen oder voneinander abhängen, als desto komplexer empfinden wir die Problemstellung. Dabei ist nicht nur die Anzahl der Eigenschaften von Belang, sondern ihre Vernetztheit. Es ist schwer, ein System zu kontrollieren, das aus vielen Merkmalen besteht, die sich gegenseitig beeinflussen. Software kann zu so einem System werden. In einem Projekt mit Entwicklern, Designern, Konzeptern und anderen Teammitgliedern, in dem Module gebaut werden mit vielen Klassen, Dateien und Schnittstellen, wird schnell ein Level an Komplexität erreicht, den ein einzelner Entwickler kaum noch überschauen kann.
62
Lösungen entwerfen Schon allein die Komplexität der Flash API an sich führt z. B. seit einigen Jahren zu einer Spezialisierung von Kompetenzen unter den Entwicklern. Gerade mit dem Aufkommen von Themen wie 3D, Sound, Grafik, Netzwerkprogrammierung und dergleichen hat die Flash-Welt schon für sich eine Komplexität angenommen, die ein einzelner Entwickler nicht mehr allein in all seinen Facetten komplett beherrschen kann. Sicherlich kann man gewisse Grundkenntnisse in all diesen Themen haben, und ich will auch nicht ausschließen, dass es einige Supertalente gibt, die tatsächlich fast alle Bereiche sehr gut abdecken, aber der durchschnittliche Flash-/Flex-Entwickler ist wohl eher froh, wenn er mit den wichtigsten Neuerungen mithalten kann und sich dann bei Bedarf in Themen einarbeitet, wenn es nötig ist. So ergibt es sich automatisch, dass einige unter uns eher Detailwissen im 3D-Bereich entwickelt haben, andere haben sich ganz und gar der Soundprogrammierung verschrieben, wieder andere experimentieren mit der Verknüpfung von Flash zu anderen Systemen und Programmumgebungen usw.
Abbildung 3.5: Kompetenzverteilung von Flash-Entwicklern, früher und heute
Wenn es aber also so viele Spezialgebiete und sicherlich auch Anwendungen gibt, die einen Teil dieser Gebiete in sich vereinen, dann entsteht bereits dadurch Komplexität, all diese Fachgebiete unter eine gemeinsame Kontrolle zu bekommen und bestmöglich gemeinsam einzusetzen. Ein weiterer Faktor für Komplexität sind die Anwendungen. Aufgrund der weitreichenden Möglichkeiten der Flash API, der vielen Schnittstellen, die sie anbietet, der hohen Performance des Flash Players und der mittlerweile recht ausgereiften Programmiersprache ActionScript ist der Einsatzbereich für Anwendungen kaum begrenzt. Grundsätzlich ergibt
63
Kapitel 3 sich natürlich eine Begrenzung darin, dass mit Flash/Flex hauptsächlich Client-Anwendungen erstellt werden, denn Flash ist eine Client-Technologie, aber innerhalb dieser Domäne gibt es prinzipiell kaum Einschränkungen, außer solche, die sich durch die eingesetzte Hardware und daraus resultierende Limitierungen hinsichtlich Performance, Speicher usw. ergeben. Die hierdurch entstehende Komplexität hat einmal eine inhaltliche Komponente. Die Anwendungen, die entstehen, entspringen ja immer aus einem fachlichen Ursprung. Anwendungen kommen aus dem Bereich des Marketings, des Finanzwesens, Geschäftswesens allgemein, der Medizin, Wissenschaft, des Tourismus, der Unterhaltung, Industrie und viele, viele mehr. Jeder dieser Bereiche hat schon seine eigene fachliche Komplexität. Wenn wir im Bereich der Medizin eine Anwendung entwickeln sollen, dann müssen wir als Entwickler in einem gewissen Maß auch die fachlichen Zusammenhänge verstehen, die in diesem Bereich eine Rolle spielen, wie sollten wir sonst eine sinnvolle Anwendung für diesen Bereich schreiben. Sich dieses Wissen anzueignen, ist also unter Umständen bereits eine enorm herausfordernde Aufgabe an sich. Nicht umsonst kann man heutzutage in vielen Stellenangeboten für Softwareentwickler lesen, dass eine gewisse Expertise in einem fachlichen Segment erwünscht ist. Und nicht zuletzt kann man an den Hochschulen Wirtschafts-, medizinische und Medieninformatik studieren, weil es eben sehr unterschiedliche fachliche Anforderungen für Softwareentwickler in diesen jeweiligen Bereichen gibt. Eine weitere Komponente der Komplexität liegt dann noch in der Größe der Anwendungen, die erstellt werden. Flash- und Flex-Anwendungen können im Prinzip eine beliebige Größe annehmen. Über Nachlademechanismen ist es grundsätzlich möglich, enorm komplexe, modulare Applikationen zu bauen. In der schieren Größe solcher Anwendungen liegt natürlich selbst auch eine Art von Komplexität. Und mit dieser Größe ergeben sich wiederum Anforderungen, die die Komplexität erhöhen. Ein großes Projekt kann nicht von einem Entwickler allein umgesetzt werden. Ein Team von vielen Entwicklern wiederum birgt für sich schon neue Arten von Komplexität, die sich in Form von Kommunikation und der Organisation der Zusammenarbeit zeigen. Viele Flash-Entwickler wissen bereits aus Erfahrung, dass sich große Projekte nicht einfach durch viele Programmierer lösen lassen. Denn je mehr Entwickler an einem Projekt beteiligt sind, umso höher ist der Koordinationsaufwand, sprich die Komplexität der Organisation. Und irgendwann ist dann der Punkt erreicht, wo die vielen Entwickler nicht mehr effektiv zusammenarbeiten können, weil sich die Aufgaben z. B. nicht mehr weiter aufteilen lassen oder weil der Aufwand, die Aufgaben abzustimmen, zu hoch ist. Wir sehen also, Komplexität steckt in vielen Ecken und Winkeln unserer Arbeit als Entwickler. Wir haben aber nun mal nur eine begrenzte Kapazität, Informationen zu verarbeiten. Um also nicht handlungsunfähig zu werden, weil wir mit zu vielen Dingen gleichzeitig fertig werden müssen, müssen wir für eine einzelne Person die Komplexität zu jedem Zeitpunkt im Zaum halten. Steven McConnell sagt: »Ich vertrete [...] die Ansicht, dass das erste Gebot bei der Softwareentwicklung lauten muss: Halte die Komplexität im Griff.« (McConnell 2007, S. 80)
64
Lösungen entwerfen Wir müssen deswegen in all diesen Bereichen versuchen, die Komplexität so weit zu verringern, bis wir unsere Aufgabe erfüllen zu können. Wie aber kann man die Komplexität verringern? Man kann ja schließlich nicht einfach bestimmte Faktoren außer Acht lassen. Müssen nicht alle oben beschriebenen Bereiche berücksichtigt werden? Die Antwort lautet: Ja, aber nicht zur gleichen Zeit und auch nicht zwingend von allen Projektbeteiligten gleichermaßen. Der Schlüssel liegt darin, die Fülle an Aufgaben und Informationen zum einen zeitlich und zum anderen personell zu verteilen. Das Ziel ist also, dass sich ein Entwickler zu einem bestimmten Zeitpunkt nur mit einer Menge an Informationen und Aufgaben beschäftigen muss, die er auch verarbeiten kann, ohne den Überblick über seinen Verantwortungsbereich zu verlieren. Hat er diese Aufgaben erledigt, kann er sich wieder mit einem anderen Bereich beschäftigen, diesen abarbeiten usw. Um also der Komplexität Herr zu werden, müssen wir das Projekt in so handliche Pakete aufteilen, die pro Entwickler einzeln in einer annehmbaren Zeit bearbeitbar sind. Wir werden in Kapitel 6 noch sehen, dass dies auf ganz unterschiedlichste Art gelöst wird. Generell ist bei vielen Vorgehensweisen erkennbar, dass man sich vom Allgemeinen ins Spezielle vorarbeitet, Top-down auf Englisch. Wir haben schon gesehen, dass z. B. das Gesamtprojekt zunächst einmal in grobe Arbeitsabschnitte aufgeteilt wird, z. B. in die Analysephase, die ihrerseits in kleinere Abschnitte unterteilt ist, wie Definition der Geschäftsprozesse, Beschreibung der Anwendungsfälle, Definition der Fachklassen usw. Hier ist klar eine Richtung vom Allgemeinen (Geschäftsprozess) zum immer Spezielleren (fachliche Klassen) erkennbar. Sie mögen sich vielleicht schon gefragt haben, warum denn so viele Arbeitsschritte notwendig sind für die Anforderungsanalyse. Wie schon gesagt, bei kleinen Projekten wird man sicherlich einige Schritte zusammenfassen, aber bei größeren Projekten bedeuten die einzelnen Arbeitspakete auch eine Möglichkeit zur zeitlichen und personellen Aufteilung und also letztlich, dass wir Komplexität für den einzelnen Entwickler reduzieren können. Auch im Softwareentwurf stehen wir wieder vor dem gleichen Problem. Und auch hier müssen wir wieder über die zeitliche oder personelle Aufteilung versuchen, Komplexität zu reduzieren. Die Objektorientierung wird uns auch hier wieder helfen, denn mittels des objektorientierten Entwurfs können wir unsere Anwendung sehr elegant in Komponenten, Module, Pakete, Klassen, Interfaces und Objekte aufteilen, die natürlich nicht unbedingt nur den Zweck haben, die Komplexität für den einzelnen Entwickler zu reduzieren, aber neben anderen Zielen eben auch das. Damit diese zeitliche und personelle Aufteilung gelingt, sollten die einzelnen Aufgaben oder Arbeitspakete idealerweise unabhängig voneinander sein. Das gilt nicht nur für Module in unserer Anwendung, sondern auch für konkrete Arbeitsschritte. Ideal wäre natürlich, wenn alle Arbeitspakete keinerlei Abhängigkeiten von anderen hätten, denn dann könnte man sie komplett frei planen. Das ist natürlich nicht so. Zwischen der Anforderungsanalyse und dem Softwareentwurf gibt es natürlich grundsätzlich die Abhängigkeit, dass der Softwareentwurf nur die Themen schon bearbeiten kann, die in der Anforderungsanalyse schon definiert wurden oder die sich aus allgemeinen, in der Anforderungsanalyse definierten Rahmenbedingungen ergeben. Es gibt also sequenzielle Abhängigkeiten, die wir nicht durchbrechen können. Abgesehen von diesen aber sollten innerhalb
65
Kapitel 3 von einem Arbeitsschritt, also zum Beispiel dem Softwareentwurf, möglichst wenige Querabhängigkeiten existieren. Wenn also Entwickler A ein Flash-Modul entwirft, sollte diese Tätigkeit möglichst wenige Abhängigkeiten zu dem Entwurf eines anderen Flash-Moduls haben, das Entwickler B entwirft, abgesehen von allgemeinen Schnittstellen, die beide Module eventuell miteinander haben. Denn je weniger die beiden Entwickler sich miteinander abstimmen müssen, umso mehr können sie sich auf ihre eigenen Module konzentrieren. Um das zu erreichen, müssen die Module selbst möglichst unabhängig voneinander sein und die eventuell vorhandenen Schnittstellen so simpel wie möglich. Aber dazu kommen wir später. Flash Anwendung
Abbildung 3.6: Die Komplexität einer Anwendung kann in Einzelteile zerlegt werden.
3.3 Projektziele Was sind die Ziele eines Flash-Entwicklers innerhalb eines Projekts? Das primäre Ziel ist natürlich, dass die Anwendung die in den Anforderungen definierten Aufgaben erfüllt unter Beachtung aller Randbedingungen – und das innerhalb des Zeitplans und unter Einhaltung des zur Verfügung stehenden Budgets. Das allein ist schon schwer genug, und in vielen Projekten wird nicht mal dieses Ziel komplett erreicht. Projekte werden zu spät fertig, oder manche Funktionalität muss zurückgestellt werden, damit der Zeitplan eingehalten werden kann, Budgets werden überschritten.
66
Lösungen entwerfen Aber neben diesem primären Ziel gibt es immer auch noch sekundäre Ziele, die für ein Projekt wichtig sein können. Nicht immer sind alle von gleicher Bedeutung, aber zu einem gewissen Anteil spielen sie alle eine Rolle. Die klassischen, zugegebenermaßen sehr allgemein formulierten sekundären Ziele sind: Wartbarkeit, Erweiterbarkeit, Wiederverwendbarkeit und Robustheit. Letztlich bieten sie eine Sicht auf das, was uns antreibt, wenn wir versuchen, gute Software zu bauen. Die vielen Maßnahmen und Instrumente des Softwareentwurfs wie z. B. Abstrahierung, Kapselung, Entkopplung usw. entspringen also den oben genannten Zielen. Bevor wir uns also den Maßnahmen und Instrumenten widmen, lohnt es sich, einen Blick auf diese grundsätzlichen Ziele zu werfen.
3.3.1 Wartbarkeit Es gibt sicherlich Projekte, bei denen man weiß, dass ihre Laufzeit sehr begrenzt ist. Eine kleine unterhaltende Anwendung, die z. B. auf einer Messe einmalig gezeigt und danach nicht benutzt wird, hat also eine sehr begrenzte Laufzeit. Eine solche Anwendung muss nicht unbedingt besonders wartungsfreundlich sein, denn es handelt sich dabei mehr oder weniger um Wegwerfsoftware. Viele andere Anwendungen hingegen haben eine sehr viel längere Laufzeit, mehrere Monate oder sogar viele Jahre. Solche Anwendungen werden unter Umständen sehr intensiv genutzt, vielleicht auch von einer großen Nutzermenge. Bei solchen Anwendungen bleibt es nicht aus, dass entweder immer wieder noch Fehler in der Anwendung zutage treten oder dass die Nutzer Änderungs- oder Erweiterungswünsche haben. Als Entwickler bedeutet dies, man wird solch ein Projekt nicht so schnell los und muss sich immer wieder damit beschäftigen, Teile der Anwendung ändern, Fehler ausmerzen und vieles mehr. Dazu kommt, dass bei einer längeren Laufzeit einer solchen Anwendung die Chance groß ist, dass die Entwickler, die ursprünglich die Anwendung entwickelt haben, inzwischen nicht mehr im Unternehmen oder in der Agentur arbeiten. Es müssen also immer wieder neue Entwickler eingearbeitet werden. Eventuell ist die Anwendung sogar so konzipiert, dass Dritte die Möglichkeit haben, eigene Erweiterungen oder Veränderungen zu entwickeln, was die Sache noch zusätzlich verkompliziert. Gerade bei Projekten, wo nicht direkt eine Anwendung, sondern vielmehr eine Bibliothek entwickelt wurde, ist dies der Fall. Das Warten und Betreuen bestehender Anwendungen oder Codeteile sind für die meisten Entwickler einfach nur lästig. Niemand möchte sich wirklich mehr mit den alten Kamellen beschäftigen, oft findet man im Nachhinein bestimmte Teile im Code nicht gut gelöst, oder man würde die Struktur mittlerweile ganz anders umsetzen. Sehr oft ist es auch einfach der Fakt, dass die Struktur mit der Zeit gewachsen und inzwischen immer schwieriger zu durchblicken ist. All diese Probleme verschlechtern also die Wartbarkeit einer Anwendung oder eines Codeteils. Nun ist die Wartbarkeit aber in den seltensten Fällen eine konkrete Anforderung seitens des Kunden. Es ist vielmehr eine schwierig zu greifende Anforderung, die sich in vielen Details der Struktur und auch der konkreten Implementierung einer Anwendung widerspiegelt. Ich bezeichne es deswegen als eine sekundäre Anforderung.
67
Kapitel 3
Abbildung 3.7: Alter Anwendungscode kann manchmal schwer zu durchdringen sein.
Was macht nun aber eine gut wartbare Anwendung aus, wodurch wird eine Anwendung oder ein Codeteil gut wartbar? Man könnte ja ganz platt sagen, eine Anwendung, die fehlerfrei ist, ist auch automatisch gut wartbar, denn solange keine Änderungswünsche reinkommen, muss sie gar nicht gewartet werden. Das lassen wir aber nicht gelten, denn man kann nicht einfach eine Eigenschaft herstellen, in dem man sich Störfaktoren wegdenkt. Anwendungen sind nie völlig fehlerfrei. Vielmehr wäre Fehlerfreiheit ein wichtiges Merkmal, bevor die Anwendung überhaupt veröffentlicht wird. Danach müssen wir einfach davon ausgehen, dass ab und zu noch Fehler auftauchen werden. Fragen wir uns erst einmal, was für Tätigkeiten wir bei der Wartung und Betreuung von Anwendungen eigentlich ausführen, um das Feld der Wartbarkeit ein wenig einzukreisen.
Lesbarkeit Zuallererst müssen die für die Betreuung zuständigen Entwickler einmal verstehen, wie die Anwendung eigentlich aufgebaut ist und wie sie funktioniert. Hier ist natürlich klar, dass sie nicht zwingend sofort jedes Detail kennen müssen, aber es muss für sie möglich sein, mit so wenig Aufwand wie möglich die Funktionsweise eines Details der Anwendung kennenzulernen. Es geht hier also um Begriffe wie Übersichtlichkeit, Verständlichkeit, Lesbarkeit. Um optimale Wartbarkeit zu erzielen, müssen wir vom schlimmsten Fall ausgehen, also von einem Entwickler, der zuvor noch nichts mit der Anwendung zu tun hatte und sich dem Code zum ersten Mal nähert, bewaffnet nur mit einer groben Vorstellung von der Anwendung, die er vielleicht durch das Ausprobieren, sprich Ausführen der Anwendung erhalten hat, sowie vielleicht einer Handvoll Fachkonzepte und anderer Dokumente, von denen wir hoffen, dass sie wenigstens halbwegs korrekt und aktuell sind. Was können die Anwendung, der Code und die Struktur nun tun, um übersichtlich, verständlich und lesbar zu sein? Letztlich zahlen die meisten Praktiken solider Softwareentwicklung nebenbei auch auf eine gute Lesbarkeit ein. Nehmen wir z. B. den Vorsatz, dass
68
Lösungen entwerfen eine Methode möglichst immer nur eine Aufgabe erfüllen soll und nicht mehrere Dinge implizit hintereinander. Dieser Vorsatz hat vordergründig erst einmal den Sinn, dass sich so eine bessere Entkopplung von Verantwortlichkeiten ergibt und außerdem eine sequenzielle Abhängigkeit innerhalb so einer Methode vermieden wird. Aber der angenehme Nebeneffekt ist auch, dass eine Methode, die einen sprechenden Namen hat und auch wirklich nur eine klar umrissene Aufgabe erfüllt, meist deutlich einfacher zu verstehen ist als eine Methode, die in sich nacheinander oder auch vermischt viele Dinge auf einmal macht. Ein guter Hinweis, dass man als Entwickler gerade dabei ist, solch eine GemischtwarenladenMethode zu schreiben, ist übrigens, wenn einem dafür partout kein vernünftiger Name einfallen will, der kürzer als 16 Zeichen ist. Im Detail werden wir das noch in den folgenden Kapiteln besprechen, hier zunächst mal ganz grundsätzliche Punkte zum Thema gute Lesbarkeit: Struktur, die das Konzept aufgreift: Wenn wir davon ausgehen, dass ein Entwickler von der Anwendung, die er betreuen soll, nur ihre äußerliche Funktionsweise kennt, weil er die Anwendung in Aktion gesehen hat und weil ihm ein fachliches Konzept zur Verfügung steht, dann hat er also auch nur eine fachliche Vorstellung von der Anwendung. Demzufolge ist es äußerst hilfreich, wenn sich die fachlichen Konzepte in der Struktur der Anwendung wiederfinden, denn so kann der Entwickler sofort eine gedankliche Verbindung zwischen den Dingen herstellen, die er aus dem fachlichen Konzept her kennt, und dem Code. Domain Driven Design ist hier einer der Schlüsselbegriffe. Die Anwendung wird also nicht primär nach technischen Gesichtspunkten strukturiert, sondern nach konzeptionellen, nämlich denen aus der Problemdomäne. Wenn es also in der Problem- oder Aufgabendomäne um Film, Videos und Ausleihverträge geht – wie in unserem Beispielprojekt FilmRegal –, dann würde es der Lesbarkeit dienen, wenn sich im Code der infrage kommenden Anwendungen diese fachlichen Objekte auch wiederfinden. Sprechende Struktur, sprechende Namen: Eine einfach lautende Regel, aber oft nicht beherzigt. Paketnamen, Klassen, Interfaces, Methoden und Attribute sollten Namen haben, mit denen man auch etwas anfangen kann. Akronyme, Abkürzungen und dergleichen mögen den initialen Schreibaufwand verringern, aber Steve McConnell bringt es treffend auf den Punkt, wenn er sagt: »Programmcode wird viel öfter gelesen als geschrieben [...]. Sie sparen am falschen Ende, wenn Sie das Schreiben von Code erleichtern, aber das Lesen dadurch schwieriger wird.« (McConnell 2007, S. 146) Unter sprechenden Namen und einer sprechenden Struktur versteht man aber auch, dass diese Bezeichner einen wirklichen Aufschluss darüber geben, was die Verantwortlichkeit einer Klasse oder eines Interface ist oder was die Aufgabe einer konkreten Methode. Eine oft anzutreffende Eigenheit bei Flash-Anwendungen ist es zum Beispiel, dass Eventhandler nach dem Event bezeichnet werden, durch das sie aufgerufen werden, anstatt dass sie danach benannt werden, was sie tun. Ein Eventhandler mit dem Namen »onClick« verrät leider rein gar nichts darüber, was er eigentlich macht, sondern lediglich, wann er aufgerufen wird. Ein Entwickler muss nun also den gesamten Code durchforsten, um die Aufgabe der Methode herauszufinden. Zudem enthält die Bezeichnung eine stärkere Kopplung zur Klasse, die das Event wirft, denn wenn das click-Event später mal gegen ein press-Event
69
Kapitel 3 ausgetauscht werden soll, müsste eigentlich der Name des Eventhandlers geändert werden, obwohl sich eventuell in der Methode gar nichts ändert. Und ändert man den Namen nicht, wird die Verwirrung nur noch größer. Eine Lösung kann sein, dass man den Eventhandler neutraler benennt, z. B. »onButtonAction«. Zudem kann man im Eventhandler einfach eine weitere Methode aufrufen, die die eigentliche Aufgabe übernimmt. Ein Entwickler kann nun den Zusammenhang schnell erkennen zwischen einem aufgetretenen Event und der daraus erfolgenden Aktion. Oft sieht man auch Konventionen, die einem Interface ein »I« voranstellen und die eine Klasse, die das Interface implementiert, dann ohne das I schreiben oder eventuell ein »Impl« hintanstellen. In solchen Fällen muss geprüft werden, ob nicht fachlich eine bessere Trennung von Interface und konkreter Klasse angebracht wäre, die eine bessere Benennung ermöglichen würde. Nehmen wir folgendes Beispiel: Um Videos in Flash abzuspielen, können wir entweder direkt eine FLV Datei streamen, oder wir können ein Video in eine SWF-Datei einbetten und diese progressiv streamen oder ganz vorladen. Für beide Fälle könnten wir nun jeweils eine Klasse schreiben, die das eine oder das andere kontrolliert. Ein Videoplayer, der nun ein Video abspielen will, könnte nun entweder die eine oder die andere Klasse nutzen. Um die Benutzung zu vereinheitlichen, könnte der Videoplayer ein Interface definieren, in dem er angibt, wie er gerne ein Video benutzen möchte. Und die beiden oben genannten Klassen könnten das Interface implementieren, um dem Videoplayer dann ihre Dienste anbieten zu können. Nehmen wir an, wir bauen nun erst einmal das Interface. Wie sollen wir es nennen: »IVideo«? Und die FLVStream-Variante dann »VideoImpl«? Geht nicht, wie sollten wir sonst die SWF-Variante nennen. Es besteht in diesem Fall kein Grund, das Interface mit einem I zu versehen. Der Videoplayer will Videos spielen, also nennt er sein Interface »Video«. Und die FLV-Variante benötigt kein Impl, sondern sie kann sich zum Beispiel »StreamVideo« nennen und die SWFVariante dann »SWFVideo«. Nun mögen manche einwenden, dass durch diese Namen nicht klar wird, welches Interface diese Klassen implementieren. Wenn wir aber davon ausgehen, dass es normalerweise immer mehrere Klassen gibt, die ein Interface implementieren (sonst ist eventuell das Interface überflüssig), dann müssten diese Klassen folglich alle gleich heißen, was offensichtlich keine gute Strategie sein kann, denn nun wird einem nicht mehr klar, worin sie sich denn unterscheiden. Dokumentation: Im Zusammenhang mit der Lesbarkeit von Code ist eine solide Dokumentation unerlässlich. Um die Wartbarkeit von Code zu erhöhen, ist es empfehlenswert, komplexere Bereiche im Code gesondert zu dokumentieren, damit andere Entwickler im Falle von notwendigen Änderungen oder bei der Fehlersuche schneller die Funktionsweise verstehen. Darüber hinaus ist das Dokumentieren von Code mit JavaDoc-kompatiblen Kommentaren unerlässlich. Gerade zusammen mit heutigen IDEs wie Flex Builder und anderen werden diese während des Tippens dem Entwickler schon präsentiert und erleichtern die Entwicklung ungemein.
70
Lösungen entwerfen
«interface» Video
FLVVideo
SWFVideo
Abbildung 3.8: Benennung bei Interfaces. I ist nicht unbedingt notwendig.
Man kann natürlich auch zu viel dokumentieren. Eine Methode mit dem Namen getXPosition():int verrät im Prinzip alles Wissenswerte schon über ihre Deklaration, hier muss nicht mehr viel dokumentiert werden. In diesem Zusammenhang sei erwähnt, dass auch Typisierung enorm zur Lesbarkeit von Code beiträgt. Im obigen Beispiel verrät der Rückgabewert einiges darüber, was man von der Methode erwarten kann, z. B. dass nur ganze Pixelwerte ausgegeben werden. Würde man den Typ weglassen, würde einiges an Information verloren gehen.
Veränderbarkeit Neben der Lesbarkeit spielt für die Wartbarkeit auch noch die Veränderbarkeit eine Rolle. Während des Lebenszyklus einer Anwendung wird an ihr immer wieder herumgeschraubt, auch wenn man das eigentlich ja vermeiden will. Der Grundsatz eines Softwareentwicklers heißt ja normalerweise: »Never touch a running system«, oder auch: »Repariere nichts, was nicht kaputt ist«. Aber über die Zeit verändern sich Anforderungen im Detail, die Arbeit mit der Anwendung enthüllt eventuell konzeptionelle Fehler oder auch technische Fehler, und die Folge ist, die Anwendung muss verbessert oder verändert werden. Die Veränderbarkeit ist nun eine Eigenschaft, die sich in vielen Bereichen des Softwareentwurfs, aber auch der konkreten Implementierung niederschlägt. Im Entwurf ist die Modularität ein starkes Merkmal, das sich auf die Veränderbarkeit auswirkt. Je modularer, sprich stärker voneinander autark einzelne Module und Komponenten in der Anwendung gestaltet sind, desto einfacher kann man eine Änderung innerhalb eines Moduls machen, ohne dass sie sich zwangsläufig auf die anderen Module auswirkt. Und das ist ein hohes Gut bei der Veränderbarkeit: Änderungen machen zu können, ohne dass die ganze Anwendung gleich wie ein Kartenhaus in sich zusammenfällt. Nehmen wir als Beispiel mal einen Parser, der eine Textdatei einlesen und seine Struktur in eine Objektstruktur parsen soll. Dieser Parser, der seinerseits Teil einer Anwendung ist, soll vielleicht optimiert werden, weil man gemerkt hat, dass er mit Textdateien ab einer gewissen Größe nicht mehr schnell genug vorankommt. Wenn nun dieser Parser eine klar defi-
71
Kapitel 3 nierte Schnittstelle hat, die seine innere Funktionsweise nach außen nicht preisgibt, dann stehen die Chancen gut, dass man seinen inneren Parsing-Algorithmus verändern kann, ohne etwas an der Schnittstelle nach außen ändern zu müssen. Das wiederum würde dazu führen, dass alle anderen Teile der Anwendung, die diesen Parser nutzen, nicht angefasst werden müssen, was ja das Ziel sein muss, um den Aufwand so gering wie möglich zu halten. Was aber könnte man nun falsch machen, was würde zu einer schlechten Veränderbarkeit führen? Nun, der Parser könnte z. B. fälschlicherweise bestimmte Methoden oder Attribute, die Auskunft über seine innere Funktionsweise geben, öffentlich machen. Viele Parser, die Baumstrukturen parsen, verwenden rekursive Funktionen, um den Baum durchzugehen. Dabei übergibt man an die Funktion den aktuellen Knoten im Baum, und als Rückgabewert gibt die Funktion den geparsten Objektknoten zurück. Wenn diese Funktion public ist, besteht die Gefahr, dass sie von anderen Teilen der Anwendung vielleicht direkt verwendet wird, um »mal eben schnell« einen kleinen Teilbaum zu parsen. Dies führt nun dazu, dass es nicht mehr möglich ist, diese rekursive Funktion zu ändern oder gar gegen einen anderen nichtrekursiven Ansatz auszutauschen, weil er ja nun schon verwendet wird. Weiter unten im Abschnitt Kapselung werde ich über das Verstecken innerer Funktionalitäten einer Klasse noch detaillierter sprechen.
Testbarkeit Einen letzten Bereich möchte ich im Bereich der Wartbarkeit noch ansprechen, die Testbarkeit. Logisch, wenn man eine Anwendung wartet und an ihr arbeitet, Fehler ausbessert, Optimierungen durchführt, muss man hinterher testen, ob auch noch alles funktioniert. Nun mag man sich fragen, was sich hinter Testbarkeit verbirgt. Testbar ist doch jede Anwendung, oder nicht? Mit Testbarkeit ist hier die Feingranularität gemeint, mit der man in einer Anwendung die einzelnen Teile zum einen isoliert voneinander und zum anderen im Zusammenspiel miteinander testen kann. Bleiben wir beim obigen Beispiel des Textparsers. Wir haben ihn verändert und müssen nun testen, ob die Anwendung auch immer noch genauso funktioniert, wie es gewünscht ist. Auch hier spielt die Modularität und Kapselung wieder eine große Rolle. Je weniger Abhängigkeiten der Parser zu anderen Teilen der Anwendung hat, je einfacher und klarer seine Schnittstellen sind, umso besser kann ich ihn isoliert testen. Wenn mein Parser beispielsweise noch fünf andere Klassen benötigt, um seine Arbeit zu verrichten, bedeutet dies, dass ich diese fünf anderen Klassen indirekt auch mittesten muss. Nun könnte man argumentieren, wenn man an diesen Klassen nichts verändert hat, können dort ja auch keine Fehler auftauchen. Die Veränderung im Parser kann aber sehr wohl Fehler in den anderen Klassen enthüllen, die früher nur einfach nicht in Erscheinung getreten sind, als der Parser noch anders gearbeitet hat. Viele Abhängigkeiten zwischen Klassen erschweren das Testen bzw. das Finden von Fehlern, wenn welche auftreten. Klar voneinander getrennte Klassen oder Komponenten mit einfachen Schnittstellen erleichtern es hingegen enorm. Besonders deutlich wird dies, wenn man automatisierte Tests, z. B. Unit-Tests, schreibt. Bei Unit-Tests besteht ja immer die Möglichkeit, sogenannte Mock Objects zu schreiben für Klassen, die die zu testende Klasse eigentlich benötigt, die man aber nicht direkt mittesten will. Je mehr Mock Objects man
72
Lösungen entwerfen schreiben muss, um eine Klasse oder eine Komponente zu testen, desto eher ist das ein Anzeichen für eine schlechte Modularität. Und der Aufwand steigt natürlich immens. Denn diese Mock-Objekte simulieren ja andere Klassen. Wenn man aber nun diese anderen echten Klassen auch mal verändert, müssen die Mock-Objekte auch geändert werden. Das kann recht schnell in viel Arbeit ausarten.
3.3.2 Erweiterbarkeit Flash gibt es zu der Zeit, in der dieses Buch entstanden ist, in Version 10. Die Flash API und auch die Sprache ActionScript selbst wurden stetig erweitert. Viele Anwendungen, die über einen längeren Zeitraum hinweg verwendet werden, werden um Funktionen erweitert. Erweiterungen sind dabei noch mal anders als Veränderungen, die wir im vorangegangenen Abschnitt behandelt haben. Bei Erweiterungen bestehen zwei gegensätzliche Herausforderungen. Einerseits muss die Anwendung die neue Erweiterung nutzen und ansprechen können, andererseits möchte man für eine Erweiterung möglichst wenig in der eigentlichen Anwendung verändern. Nun könnte man sich eine Anwendung vorstellen, die als eine geschlossene Einheit aufgebaut ist, die in sich alle Funktionen erfüllt, die anfangs definiert wurden, und das war's. Eine solche monolithische Anwendung wäre dann im Prinzip gar nicht erweiterbar, ohne dass man sie nicht selbst stark abändert. Das Ziel ist ja aber, dass man eine Anwendung erweitern kann, ohne am bestehenden Code allzu viel ändern zu müssen. Das klingt nun nach einer nicht so einfachen Anforderung, denn man kann ja heute nicht wissen, was wohl in der Zukunft für Erweiterungen kommen mögen. Und eine Anwendung für alle Eventualitäten zu rüsten, würde einen immensen Aufwand bedeuten und den Code auch enorm aufblähen, was sich wiederum schlecht auf die Wartbarkeit auswirkt. In der Tat ist die Erweiterbarkeit auch ein Merkmal, das nicht zwingend bei allen Anwendungen gleich wichtig ist. Anwendungen, die einen kurzen Lebenszyklus haben, müssen höchstwahrscheinlich nicht so stark erweiterbar sein wie Anwendungen, von denen man sich eine lange Lebensdauer erhofft. Wie kann man aber nun bei solchen länger laufenden Anwendungen für eine gute Erweiterbarkeit sorgen? Erweiterbarkeit muss geplant werden. Man kann eine Anwendung nicht vorsorglich in alle Richtungen erweiterbar machen. In den meisten Fällen muss man das auch nicht. Nehmen wir als kleines Beispiel wieder den Videoplayer für unsere Firma FilmRegal. Wie können wir Erweiterbarkeit für einen Videoplayer planen? Was wollen wir erweiterbar halten bei einem Videoplayer? Zunächst fällt einem da das Videodatenformat ein. Seit einer bestimmten Version von Flash 9 können wir nicht mehr nur FLV-Dateien laden, sondern auch andere Dateiformate, solange sie einen kompatiblen Codec verwenden. Es ist denkbar, dass in der Zukunft weitere Formate dazukommen. Wenn das passiert, wäre es schön, wenn wir dazu nicht jedes Mal unseren Videoplayer komplett umbauen müssen. Das Beziehen der Videodaten sollte innerhalb des Players also vom Rest der Anwendung gekapselt werden, damit wir später weitere Formate hinzufügen können. Wir definieren also das Videodatenformat als einen Punkt, der erweiterbar sein soll.
73
Kapitel 3
Abbildung 3.9: Enorme Erweiterbarkeit zum reinen Selbstzweck bringt meist wenig Nutzen.
Wie viele Stellen man in einer Anwendung für konkrete Erweiterbarkeit identifiziert, ist eine Abwägungsfrage zwischen Aufwand, Wahrscheinlichkeit des zukünftigen Wunsches der Erweiterung an dieser Stelle und negativen Einflüssen auf Wartbarkeit, Leistungsfähigkeit und anderen Eigenschaften der Anwendung. Es ist also letztlich in einem hohen Maß eine wirtschaftliche und strategische Überlegung. Strategisch auch, weil Erfahrungen mit ähnlichen Projekten aus der Vergangenheit Hinweise darauf geben können, wo sich in der Zukunft Anforderungen für Erweiterungen ergeben können. Bei einer Gruppe von Anwendungen, die in der Vergangenheit z. B. oft Änderungen und Erweiterungen mit einem bestimmten Backend-System erfahren hat, kann man davon ausgehen, dass dies ein Punkt für Erweiterungen auch in einem neuen Projekt sein kann, wenn es mit den anderen verwandt ist. In dem Fall sollte man auch in der neuen Anwendung dafür sorgen, dass entsprechend der Entwurf der Anwendung eine Erweiterbarkeit an der Schnittstelle zu besagtem Backend ermöglicht. Um konkret geplante Erweiterbarkeit in einem hohen Maße zu erreichen, stehen uns Entwicklern einige nützliche Entwurfsmuster zur Verfügung, die speziell die Entkopplung von nutzenden zu genutzten Klassen modellieren, wie z. B. Decorator, Facade, Chain of Responsibility und andere, dazu in Kapitel 4.4 mehr. Es gibt aber auch noch grundsätzliche Dinge, die die Erweiterbarkeit einer Anwendung begünstigen oder auch behindern. Modularität und Kapselung begünstigen die Erweiterbarkeit. Wenn in unserem Videoplayer die Anwendung keine direkte Kenntnis vom Videodatenformat hat, dann ist es überhaupt erst möglich, unterschiedliche Formate als Erweiterungen zu unterstützen. Wenn die Grundlogik des Videoplayers sauber getrennt ist vom User-Interface, dann kann später Letzteres um zusätzliche Bedienfeatures erweitert werden usw. Je stärker wiederum einzelne Teile einer Anwendung konkrete Kenntnis von anderen Teilen haben, je verzweigter z. B. die gegenseitigen Methodenaufrufe sind – Klasse A verwendet zwei Methoden von Klasse B, die wieder mehrere Methoden von Klasse C, die wiederum einige von Klasse B und A usw. –, desto geringer ist zwangsläufig die Erweiterbarkeit einer solchen Anwendung.
74
Lösungen entwerfen Anwendungen also, die grundsätzlich stark modular und gekapselt aufgebaut sind, sind auch in den Teilen auf Erweiterungen gut vorbereitet, in denen nicht mit Erweiterungen gerechnet wurde. Man muss hier aber wie gesagt aufpassen, dass man einen gesunden Mittelweg zwischen Modularität und Sicherstellung von Leistungsfähigkeit und beherrschbarer Komplexität der Anwendung weiterhin gewährleistet, denn zu kleinteilige Modularität kann negative Auswirkungen auf andere Eigenschaften einer Anwendung haben, wie zum Beispiel Verständlichkeit und Robustheit.
3.3.3 Wiederverwendbarkeit Dieses Prinzip spricht wunderbar unsere eigene Faulheit an. Softwareentwickler haben wie kaum ein anderer Beruf die Möglichkeit, Dinge, die sie einmal gebaut haben, wieder und wieder zu verwenden. Tischler, Maurer, Handwerker allgemein, sie alle müssen das, was sie einmal erfolgreich hergestellt haben, trotzdem immer wieder neu bauen. Sie können zwar Maschinen zu Hilfe nehmen, und ihre Erfahrung hilft ihnen auch, dass es beim nächsten Mal immer besser läuft, aber nichtsdestotrotz, ihre Erzeugnisse müssen für jeden Einsatzzweck immer wieder neu angefertigt werden. Nicht so bei Software. Die simpelste Art der Wiederverwendung ist Copy&Paste. Jeder Entwickler hat das schon genutzt. Da hat man eine kleine Methode, vielleicht sogar nur eine einfache Schleife, und eh man die noch mal neu schreibt, kopiert man sie sich schnell. Was würde wohl ein Tischler geben, wenn er ein Stuhlbein nur einmal bauen und dann dreimal kopieren könnte! In der Softwareentwicklung geht es aber noch viel besser. Jeder Entwickler, der schon ein wenig Erfahrung beim Programmieren gesammelt hat, weiß, dass das bloße Kopieren auf Dauer keine gute Lösung ist. Denn wenn man in dem kopierten Code hinterher noch einen Fehler findet, muss man ihn überall noch mal abändern. Stattdessen haben wir was viel Besseres. Anstatt etwas zu kopieren, können wir es in der Softwareentwicklung instanziieren und referenzieren. Wir würden unser virtuelles Stuhlbein also nur einmal bauen und nicht dreimal kopieren, sondern einfach vier Instanzen erstellen.
Abbildung 3.10: Einfache Instanziierung nach einem Bauplan geht nur in der Softwareentwicklung.
75
Kapitel 3 Wiederverwendbarkeit wird da kompliziert, wo sie plötzlich dynamisch und flexibel werden soll. Ich habe also Code schon mal geschrieben und könnte ihn eigentlich wiederverwenden, aber da ist ein kleines Detail, das brauche ich diesmal anders. Nehmen wir als Beispiel wieder mal unser Textparser-Beispiel von weiter oben. Nehmen wir an, als wir ihn geschrieben hatten, benötigten wir eine Klasse, die eine Textdatei ausliest und in eine Objektstruktur parst. Der Parser lädt also eine Datei über eine URL, die man ihm übergibt, parst dann seinen Inhalt und gibt die Objektstruktur zurück. Nun habe ich eine andere Anwendung, die auch so eine Textstruktur parsen muss, die die Textdatei aus welchen Gründen auch immer schon fertig geladen vorrätig hat. Ich kann den vorhandenen Textparser nun leider so ohne Weiteres nicht verwenden, denn er funktioniert nun mal so, dass er selber die Textdatei laden will. Dieses kleine Beispiel zeigt uns zwei Dinge. Will man wiederverwendbare Software schreiben, muss man einzelne Teile einer Anwendung losgelöst von ihrem ersten konkreten Einsatzort betrachten. Ein solcher Parser darf also nicht unter Berücksichtigung der konkreten Anwendung, für die man ihn baut, entworfen werden, sondern nur unter Berücksichtigung seiner ureigenen Funktion, nämlich das Parsen von Text. Es soll keine Abhängigkeiten zur konkreten Anwendung geben. Daraus ergibt sich auch das Zweite: Will man wiederverwendbare Software bauen, müssen die wiederverwendbaren Teile in sich nur ihre ureigene Aufgabe erfüllen und keine andere. Ein Modul, das mehrere Verantwortlichkeiten in sich vereint, ist schwerer wiederverwendbar, denn die Chance, dass man beide Verantwortlichkeiten gemeinsam noch mal woanders benötigt, ist geringer als für die jeweiligen Verantwortlichkeiten allein. Die Chance also, dass ich einen Textparser noch mal woanders benötige, der auch gleichzeitig den Text lädt, ist geringer als bei einem Parser, der einfach nur Text parst. Wir sehen also, die Krux liegt in der Granularität dessen, was ich als wiederverwendbar anbiete. Je stärker ein Codeteil sich auf seine eigentliche Aufgabe konzentriert, umso größer die Chance, dass ich ihn wiederverwenden kann. Wir merken schon, da schwingt wieder Modularität mit. Und weil das so ist, muss auch hier wieder darauf hingewiesen werden, dass eine hohe Wiederverwendbarkeit durchaus auch ihren Preis hat. Wenn man für jeden Codeschnipsel, für jede Klasse auf unbedingte Wiederverwendbarkeit achtet, erhöht man unter Umständen die Komplexität der Anwendung und verschlechtert damit eventuell die Wartbarkeit. Auch hier ist wieder eine Abwägung der verschiedenen Anforderungen und Erwartungen notwendig, um den richtigen Mittelweg zu finden. Modularität ist auch nicht das einzige Mittel, um für eine gute Wiederverwendbarkeit zu sorgen. Wie schon oben erwähnt, sorgt Flexibilität für eine hohe Wiederverwendbarkeit. Komponenten, die sich in ihrer Funktionsweise an unterschiedliche Einsatzzwecke anpassen können, sind natürlich stärker wiederverwendbar als solche, die nur genau für einen Einsatzszenario geeignet sind. Variabilität ist also gefragt. Und das erhöht natürlich noch mal die Schwierigkeit bei der Erstellung von wiederverwendbaren Modulen. Nun muss man also nicht nur darauf achten, dass das Modul nur eine Verantwortlichkeit hat, man muss auch noch vorausahnen, in welchen Varianten es eventuell eingesetzt werden könnte.
76
Lösungen entwerfen Auf unseren Textparser übertragen könnte das zum Beispiel heißen, dass er mit unterschiedlichen Trennzeichen in der Datei – also Semikolon, Komma, Leerzeichen usw. – umgehen kann. Um wiederverwendbare Codeteile zu schreiben, muss man also von der konkreten Anwendung abstrahieren und sich vorstellen, in welchen Szenarien der konkrete Codeteil noch einsetzbar sein soll. Auch hier gibt es wieder Entwurfsmuster, die uns helfen, Flexibilität und Variabilität in unseren zur Wiederverwendung vorgesehenen Code zu bringen, darunter z. B. Adapter, Mediator, Bridge und andere.
3.3.4 Robustheit Bei vielen Flash-Anwendungen ist es wichtig, dass sie möglichst stabil laufen. Gerade bei Webanwendungen, die eher Content präsentieren als kritische Prozesse unterstützen, ist es im Zweifel wichtiger, dass eine Anwendung nicht abstürzt, als dass sie in Ausnahmefällen mal einen kleinen Fehler in der Anzeige hat oder unvollständige Daten präsentiert. Das soll nicht heißen, dass man seine Anwendung schon unter diesem Vorzeichen planen sollte, aber letztlich gilt es abzuwägen, ob eine Anwendung, wenn sie einen Fehler bemerkt, eher versucht, den Fehler abzuhandeln und weiterzumachen, oder ob sie sich beendet, weil der Fehler nicht vorkommen darf, wie es z. B. in sicherheitskritischen Anwendungen ja durchaus der Fall sein kann. Eine Anwendung gilt als robust, wenn sie sich auch in Fehlersituationen wieder fängt und weiter arbeiten kann. Zur Robustheit gehört also zu einem großen Teil, mit Fehlern tolerant umzugehen. Und vor allem, Fehler überhaupt zu erkennen und abzufangen. Eine Anwendung gilt wiederum als »korrekt«, wenn sie in einem Fehlerfall nicht mit fehlerhaften Daten weiterarbeitet, sondern sich eher beendet und so gar nicht erst die Option eröffnet, mit falschen Daten oder in einem nicht validen Zustand weiterzuarbeiten.
Abbildung 3.11: Eine robuste Anwendung kann Fehler abfangen.
77
Kapitel 3 Eine große Gefahr bei sehr modularen Anwendungen, die vielleicht in einem großen Team erstellt wurden, bei dem jedes Teammitglied jeweils ein anderes Modul gebaut hat, ist folgende: Sobald man alle Module zur gewünschten Anwendung zusammenführt, kann alles in sich zusammenstürzen. Hohe Modularität kann unter Umständen eine schwache Robustheit einer Anwendung zur Folge haben, wenn nicht die einzelnen Module schon sehr robust sind. Modulare Anwendungen haben die Eigenart, dass sich jedes Modul natürlich nur auf seine Aufgabe konzentriert und zu Recht erwartet, dass die anderen Module ebenso ihre Aufgabe erfüllen. Was aber, wenn ein Modul mal seine Aufgabe nicht erfüllen kann, weil es entweder fehlerhaft ist oder weil bestimmte Bedingungen, unter denen es normalerweise arbeitet, nicht gegeben sind? Was z.B., wenn unser Textparser von weiter oben nicht validen Text zum Parsen bekommt? Zur Robustheit gehört eine gründliche Fehlerbehandlung, und zwar in zweierlei Hinsicht. Module müssen frühzeitig nach außen melden, wenn sie falschen Input bekommen oder in sonstiger Hinsicht in einer nicht unterstützten Art und Weise verwendet werden. Auf der anderen Seite müssen Module, die andere Module benutzen, damit rechnen, dass diese anderen Module fehlerhaft sein könnten oder aus sonstigen Gründen nicht das tun, was sie sollen. Eine Klasse also, die z. B. den Textparser verwendet, muss in dem Fall, dass der Parser nicht wie gewünscht eine Objektstruktur zurückliefert, eine Strategie parat haben, wie sie selbst weiterarbeitet. Das kann heißen, dass sie eventuell die benötigten Daten von einem anderen Modul beziehen kann, oder aber im schlechtesten Fall, dass sie ihrerseits nach außen melden muss, dass ein Fehler aufgetreten ist. Seit Flash 9 gibt es für das Melden von Fehlern zwei offizielle Wege. Zum einen können für synchrone Fälle die klassischen Errors mittels »throw« geworfen werden. Zum anderen können in asynchronen Situationen Error-Events dispatched werden. Werden solche ErrorEvents nicht durch einen Eventhandler abgefangen, hat dies den gleichen Effekt wie das Werfen eines normalen Errors. Um eine robuste Anwendung zu schreiben, sollte also jedes Modul entsprechende Fehlerbehandlungen implementieren.
3.4 Abstrahierung Ich habe schon in der Einleitung von Kapitel 3 von der außerordentlichen Fähigkeit von uns Menschen zur Abstrahierung gesprochen. Der große Vorteil in dieser Fähigkeit liegt darin, dass wir uns von den konkreten Ereignissen und Zuständen unserer Umwelt Modelle im Kopf zurechtlegen. Wir merken uns das Konzept eines Autos nicht dadurch, dass wir ein ganz konkretes Auto abspeichern, sondern dass wir ein abstraktes Modell eines Autos konstruieren, das künftig auf alle Autos passt, die wir sehen. Das tun wir ganz intuitiv mit allen Dingen, die wir sehen und denen wir begegnen. Diese Abstrahierung hat viele Vorteile. Zum einen ist es z. B. viel einfacher, mit anderen Menschen über abstrakte Dinge zu sprechen als über konkrete. Als unser Fahrlehrer die Funktionsweise eines Autos erklärt hat, saßen wir zwar in einem konkreten Auto, unser Fahrlehrer hat aber nicht jedes Mal konkret über dieses Auto gesprochen, sondern ganz all-
78
Lösungen entwerfen gemein von Lenkrad, Bremspedal, Gangschaltung usw. Und wenn wir dann mit Freunden übers Autofahren gesprochen haben, haben wir immer »Auto« gesagt und nicht »Gefährt mit vier Rädern, Verbrennungsmotor, Fahrgastzelle und Schalensitzen«. Abstrahierung heißt hier also auch, dass wir komplexe Dinge unter einem abstrakten Begriff fassen können. Neben der vereinfachten Kommunikation bedeutet Abstrahieren aber auch, dass wir das Wissen über grundsätzliche Eigenschaften und Funktionsweisen von einer konkreten Sache lösen und uns mittels eines abstrakten Objekts vorstellen können, nur um sie in einer anderen Situation wieder auf eine konkrete Sache der gleichen Art anzuwenden. Obwohl wir nämlich das Fahren in einem ganz konkreten Auto erlernt haben, sind die meisten von uns in der Lage, in so gut wie jedes Auto einzusteigen, sich kurz umzusehen und loszufahren. Wir sind also nicht nur in der Lage, konkrete Eigenschaften und Funktionsweisen von einem konkreten Ding zu lösen und uns abstrakt zu merken, wir können diese Eigenschaften und Funktionsweisen auch wieder auf ein ganz konkretes anderes Objekt übertragen, wenn es grundsätzlich mit unserem abstrakten Modell in Einklang gebracht werden kann. Fahren lernen im Fahrschulwagen, durch die Gegend fahren dann im ersten eigenen Wagen. Um dieses Beispiel noch stärker zu belasten, schauen wir uns die Abstrahierung auch noch aus einem dritten Blickwinkel an. Und zwar sehen wir da die Autohersteller, die ihre Autos so konstruieren, dass sie unserer abstrakten Vorstellung von einem Auto so nah wie möglich kommen, damit wir überhaupt in der Lage sind, ihre Autos zu fahren. Der Autohersteller muss von der allgemein bekannten abstrakten Vorstellung von einem Auto ausgehen, wenn er seine konkreten Autos entwickelt. Er geht also von den abstrakt formulierten Eigenschaften und Funktionsweisen aus, die wir einem Auto zuordnen, und implementiert sie in konkreter Form in seine Autos. Wie sehr uns auch nur die kleinste Abweichung auffällt, merkt man, wenn man einem Elektroauto auf der Straße begegnet ist und irritiert war, dass es keine nennenswerten Geräusche von sich gibt, was so weit geht, dass Hersteller solcher Autos mittlerweile optional künstliche Motorgeräusche erzeugen, damit sich unsere abstrakte Vorstellung von einem Auto mit dem konkreten Fahrzeug wieder deckt. Abstrahierung und Modellierung ist also nichts Neues für uns, im Gegenteil, es ist ein Instrument, das wir intuitiv von Geburt an praktizieren. Wo liegt hier also das Problem, gibt es überhaupt eins? Die Probleme fangen dort an, wo wir die abstrakten Eigenschaften und Funktionsweisen einer Sache nicht gleich erkennen oder wir sie zunächst falsch deuten. Eines der berühmtesten Beispiele für die Schwierigkeit, das abstrakte Modell zu erkennen, ist sicherlich das Universum. Richtig erkannt haben wir es immer noch nicht, obwohl wir uns in unserer Geschichte schon diverse Modelle erdacht haben, um den Sternenhimmel zu beschreiben. Da war das Himmelszelt, an dem die Sterne angeheftet waren, da war das geozentrische System, bei dem die Erde im Mittelpunkt stand und sich alles drum herum bewegte. Da war Atlas, der die Erde stemmte, und natürlich die biblische Anschauung, in der die Erde und das Universum von Gott in sieben Tagen erbaut wurden. Heute ist die Urknalltheorie populär und die dunkle Materie. Wer weiß, welche neuen Thesen wir uns in der Zukunft noch ausdenken werden.
79
Kapitel 3 Das Bemerkenswerte hierbei ist, dass die Modelle, die wir Menschen uns bezüglich des Universums über die Jahrtausende ausgedacht haben, jeweils nie schlecht waren, denn sie haben in dem Kontext, in dem sie entwickelt wurden, immer ihren Zweck erfüllt. Die Modelle reichten aus, dass schon vor mehreren Tausend Jahren unsere Vorfahren in der Lage waren, Jahreszeiten zu bestimmen und Aussaat und Ernte festzulegen. Die Gravitationsmodelle, die sich Newton erdacht hat, können nicht alle Phänomene erklären, die wir heutzutage zu beobachten imstande sind, aber sie haben über mehrere Jahrhunderte ein ausreichend genaues Modell über die Verhaltensweisen der Planeten im Weltraum beschrieben, mithilfe derer viele Probleme und Fragen gelöst werden konnten. Die Modelle wurden also nie aus reinem Selbstzweck entwickelt, sondern um ganz bestimmte Aufgaben zu meistern. Wir wissen heute, dass es nicht zwingend notwendig, ja vielleicht gar nicht möglich ist, das vollständig korrekte Modell des Universums zu beschreiben. Es reicht aus, wenn wir ein Modell haben, mit dem wir die Aufgaben, die wir heute erledigen wollen, lösen können. Und wenn sich neue Probleme ergeben, dann müssen wir halt gegebenenfalls unser Modell anpassen. Diese Erkenntnis ist auch in der Softwareentwicklung wichtig. Um in einem Problembereich eine gute Anwendung zu entwickeln, müssen wir nicht ein vollständiges und exaktes Modell des Problems erstellen. Es reicht, wenn wir ein Modell erstellen, das so genau ist, dass wir damit unser Problem gelöst bekommen. Genau das erreichen wir mit unserer Fähigkeit zur Abstrahierung, denn Abstrahieren heißt auch, die Details wegzulassen, die uns im Rahmen unserer Aufgabe nicht interessieren. Und das hilft uns letztlich wieder, die Komplexität der Aufgabe zu reduzieren.
Abbildung 3.12: Hebel plus Henne ergibt ein Ei. Wie die Maschine funktioniert, muss uns nicht interessieren.
Auch in der Softwareentwicklung stehen wir immer wieder aufs Neue vor der Aufgabe, ein abstraktes Modell eines Problems zu erstellen. Selbst schon bei so vermeintlich kleinen Anwendungen wie unserem Videoplayer unserer Beispielfirma FilmRegal haben wir damit
80
Lösungen entwerfen zu tun. Schon der Name Videoplayer ist in diesem Fall doppelt abstrakt. Schon in der Realität bezeichnet der Begriff auf abstrakte Weise eine Gruppe von Geräten, die früher VHSKassetten, heute eher DVDs und Blueray-Disks abspielen. Zig Hersteller bauen die unterschiedlichsten Varianten dieser Player, und doch wissen wir einigermaßen genau, was so ein Gerät zum Videoplayer macht. Die doppelte Abstraktion kommt nun zustande, weil wir diesen eh schon abstrakten Begriff in die Softwarewelt hieven, wo er nur noch in seiner Funktionalität und seiner Grundidee weiterexistiert, aber nicht mehr in seiner Form, denn unser Software-Videoplayer ist nun virtuell. Wenn wir uns heutige Computer ansehen, stellen wir fest, dass sich fast alle Computerprogramme abstrakter Begriffe aus der realen Welt bedienen, um ihren Nutzern ihre Funktionalität in einem vertrauten Umfeld darzustellen. Betriebssysteme arbeiten mit Fenstern, Knöpfen, Zeigern, Schiebern, mit Listen, Tabellen, Ordnern usw. Alles Begriffe, die aus der realen Welt abstrahiert wurden. Da sie in der virtuellen Welt ihr funktionales Konzept behalten haben (in einen virtuellen Ordner kann man z. B. wie in einen echten Ordner Unterlagen ablegen, mit Knöpfen aktiviert man eine bestimmte Aktion), finden sich die Nutzer mit ihnen schnell zurecht, weil die Nutzer das gleiche abstrakte Verständnis von diesen Dingen haben. Im objektorientierten Entwurf versucht man nun konsequent, diese Abstraktion bis ins Detail durchzuhalten. Dabei wird die Abstrahierung meistens da immer schwieriger, wo man sich in Bereiche begibt, für die es noch keine gängigen Abstrahierungen gibt. Schauen wir uns das wieder am Beispiel unseres Videoplayers an. Die ersten Abstrahierungen sind noch einfach. Bei einem Videoplayer sprechen wir von einem Video, dem Nutzer, der Bedienleiste, den Nutzungsrechten, die der Nutzer benötigt, damit er das Video ansehen darf, usw. Diese Begriffe sind eingängig, denn jeder kann etwas damit anfangen. Je mehr wir aber in den technischen Bereich kommen, umso schwieriger wird die Abstrahierung. Beispielsweise wollen wir das Video nicht erst komplett herunterladen, sondern schon während des Ladens abspielen. Das ist ein recht spezieller technischer Vorgang, für den es in der realen Welt kaum übertragbare Vorgänge gibt, von denen man abstrahieren könnte. Uns fällt es hier natürlich deutlich schwerer, sinnvolle Abstrahierungen zu finden. Glücklicherweise haben sich andere Entwickler dazu schon Gedanken gemacht und haben das kontinuierliche Laden und Auswerten von Daten mit einem Strom verglichen, einem Datenstrom, der vom Server zum Client fließt und den wir direkt dort abgreifen können. Deswegen bietet uns die Flash API auch die NetStream-Klasse an. Sie baut auf dieser abstrakten Idee eines Datenstroms auf. Nun könnte man sich fragen, warum es denn so wichtig ist, solcherlei Abstrahierungen zu finden. Warum verwendet man nicht einfach irgendwelche technischen Namen? Wichtig ist zunächst einmal, dass es bei der Abstrahierung nicht vordergründig um die Namen geht. Die Idee des Datenstroms ist nicht deswegen gut, weil Strom so ein tolles Wort wäre oder weil man sich bei einem Strom so schön fließendes Wasser vorstellen könnte. Die Idee ist gut, weil das Konzept des Stroms abstrakt genug ist, um jeglichen kontinuierlichen Transport von Daten von einem System X zu einem System Y zu bezeichnen. Das eignet sich nicht nur für Videostreams, die von einem Server kommen, sondern auch für File-Streams, bei denen eine Datei kontinuierlich von einer Festplatte gelesen wird, oder Audiodaten, die von einem Mikrofon erzeugt werden. Bei der Abstrahierung kommt es also nicht zwingend
81
Kapitel 3 auf einen guten Namen an – obwohl ein sprechender Name immer hilfreich ist, siehe Lesbarkeit –, sondern auf eine Idee, die abstrakt und tragfähig genug ist, um eine Sache auf einem allgemeinen Level zu beschreiben, sodass man davon wieder konkrete Implementierungen ableiten kann. Trotzdem bleibt die Frage, ob dies immer notwendig ist. Muss jeder Teil einer Anwendung eine konkrete Implementierung eines abstrahierten Konzepts sein? Sicherlich nicht. Auch hier ist wieder ein Mittelweg gefordert. Geeignete Abstrahierungen zu finden, ist immer dann besonders nützlich, wenn man ein technisches Konzept fit für eine generelle Wiederverwendung machen will oder wenn man schon weiß, dass ein Programmteil auf einem bereits existierenden abstrakten Konzept basiert. Wenn man also einen Videostream implementiert (nehmen wir mal für einen Moment an, die Flash API würde einen solchen nicht schon konkret anbieten), dann ist es sinnvoll, auf dem schon existierenden abstrakten Konzept des Datenstroms aufzusetzen, weil damit gewährleistet ist, dass andere Entwickler, denen dieses Konzept vertraut ist, auch mit dem Videostream schnell klarkommen werden. In der Praxis wird man mit der Zeit feststellen, dass bereits für sehr viele technische Konzepte entsprechende Abstraktionen gefunden wurden. In der Regel wird man für seine konkrete Implementierungsidee ein bereits bestehendes abstraktes Konzept finden, mithilfe dessen man seine Idee standardisieren kann. Hier hilft z. B. oft ein Blick in bestehende APIBeschreibungen, z. B. die von Flash und Flex, aber durchaus auch die von anderen Plattformen, wie z. B. Java. Die Abstrahierung gilt übrigens nicht nur für konkrete Klassen, sondern auch für Klassenstrukturen. Entwurfsmuster sind letztlich auch Abstrahierungen von konkreten Klassenstrukturen, die bestimmte strukturelle Lösungen anbieten. Bisher haben wir über Abstrahierung nur im theoretischen Rahmen gesprochen. Wie aber stellt sie sich konkret im Code dar? In welcher Form findet sich Abstraktion in Anwendungen konkret wieder? Die Antwort darauf fällt schwer, denn Abstraktion erscheint in vielen unterschiedlichen Formen im Code. Diejenige, die einem sofort einfällt, ist natürlich die Vererbung, auf die ich noch zu sprechen kommen werde. Aber auch das Interface-Konzept fußt letztlich auf der Idee der Abstraktion, genauso wie Pakete, eine konkrete Klasse und sogar eine Methode. Schauen wir uns das an. Die Paketstruktur – oder auch die Struktur von Namespaces, wie ActionScript 3 sie heutzutage gestattet – bietet eine Möglichkeit der Abstraktion. Wenn ich innerhalb unseres Videoplayers ein Paket mit dem Namen user erstelle und dort alle Klassen und Interfaces, die sich um den Nutzer drehen, versammle, dann bietet allein dieses Vorgehen eine grundsätzliche Abstraktion, denn über diese Strukturierung mache ich allein über ein Paket nach außen kenntlich, dass hier das Konzept eines Nutzers implementiert ist, ohne dass man sich zu diesem Zeitpunkt schon mit Details darüber befassen müsste. Natürlich sind Pakete allein ein recht schwaches Instrument der Abstrahierung, denn sie sagen außer dem Namen nicht sehr viel mehr über das Wesen der Sache aus, die dort behandelt wird. Als Nächstes haben wir die Vererbung, ein sehr starkes Instrument zur Abstraktion. Über sie wird deutlich, welchen technisch konzeptionellen Ursprung eine Klasse haben kann. So wird dadurch, dass in der Flash API ein Button letztlich indirekt von DisplayObject erbt, deutlich, dass ein Button nun mal ein visuelles Objekt ist, das die gleichen Grundeigenschaften besitzen sollte wie jedes anderes visuelle Objekt auch.
82
Lösungen entwerfen Interfaces unterstützen Abstrahierung auf eine andere Art und Weise. Anstatt wie bei der Vererbung auszudrücken, dass ein Button ein DisplayObject ist, würde man auf Interfaces übertragen sagen, dass sich ein Button wie ein DisplayObject verhält. Allein die Formulierung lässt erahnen, dass die Idee der Interfaces noch etwas lockerer ist als bei der Vererbung. Denn ein Objekt, das sich nur wie ein anderes verhält, gibt eben nicht preis, was es letztlich wirklich ist, sondern nur, was es sein kann. Das wird einem klarer, wenn man sich vor Augen hält, dass eine ActionScript-Klasse ja mehrere Interfaces implementieren, aber nur von einer anderen Klasse erben kann. Die Idee der Interfaces ist also etwas freier. Mit ihnen lassen sich deswegen besonders gut entkoppelte Modulstrukturen realisieren. Als Nächstes haben wir die konkrete Klasse. Selbst sie stellt eine Abstrahierung dar. Denn eine Klasse präsentiert sich gegenüber anderen Klassen in Form ihrer öffentlichen Schnittstelle. Unser Textparser von weiter oben z. B. hat vielleicht einfach eine öffentliche Methode parseText(text:String):ObjectStructure. Intern wiederum verwendet er unter Umständen weitere Methoden, besitzt mehrere Attribute und verwaltet Hilfsobjekte. Abstrahierung meint in diesem Zusammenhang, dass wir, die den Textparser benutzen, nur das Konzept des Parsens von Text in der Form »Text rein, Objektstruktur raus« verstehen müssen und nicht die Details der Implementierung. Das konkrete Vorgehen wird also abstrahiert, und übrig bleibt die oben genannte Methode. Klassen sind also wichtige grundsätzliche Werkzeuge der Abstrahierung von Elementen einer Anwendung. Auf dem kleinsten Level haben wir schließlich die Methoden. Auch eine einzelne Methode abstrahiert. Nehmen wir eine der ursprünglichsten Methoden von Flash überhaupt: gotoAndPlay(). Wir können uns sicher sein, dass der Aufruf dieser Methode der MovieClipKlasse innerhalb des FlashPlayers einiges bewirkt. Aber die Methode abstrahiert von den vielen kleinen Einzelschritten das Grundkonzept des Springens in einen anderen Frame der Timeline (und des Weiterspielens) und erleichtert uns damit den Umgang mit dieser vermeintlich komplexen Funktionalität. Methoden sind deswegen die kleinstmögliche Form der Abstrahierung, die wir zur Verfügung haben.
3.5 Kapselung Stellen Sie sich vor, in Ihrem Auto wären sämtliche veränderlichen Eigenschaften direkt vom Fahrersitz aus zugänglich, also nicht nur Lenkung, Gas und Bremse, sondern auch Verdichtungsverhältnis, Zündzeitpunkt, Steuerung des Motorlüfters und viele andere. Technikfreaks würden sich darüber vielleicht sogar freuen, weil es ihnen enorm viele Einstellungsmöglichkeiten ermöglicht. Aber grundsätzlich würde diese öffentliche Zugänglichkeit die Gefahr bergen, dass man aus Versehen Einstellungen vornimmt, die entweder die Leistung des Autos beeinträchtigen oder sogar das Fahrzeug zum Stillstand bringen. Auch würden diese vielen Einstellungsmöglichkeiten die meisten Fahrer überfordern, weil man vor lauter Schaltern und Reglern kaum noch die wichtigen von den unwichtigen Funktionen unterscheiden könnte. Einen Flugschein haben nicht zuletzt deswegen relativ wenige Menschen, weil das Erlernen der vielen Funktionen in einem Flugzeug plus die umfangreichen Regeln im Flugverkehr
83
Kapitel 3 sehr aufwendig und deswegen auch teuer ist. Im Flugzeug kapseln die Instrumente im Cockpit zwar auch Funktionalitäten und Vorgänge, es werden aber immer noch deutlich mehr Steuerungsmöglichkeiten und Informationen an den Piloten weitergegeben als an einen Autofahrer. Das Autofahren hat auch deswegen seinen Siegeszug durch die Länder der Welt angetreten, weil es relativ einfach zu erlernen ist, denn die Funktionen und Eigenschaften eines Autos, die man zum Fahren zwingend kennen muss, sind überschaubar. Wie wir schon im vorigen Kapitel gelernt haben, können wir die Komplexität einer Sache durch Abstrahierung verringern. Indem wir uns nur auf die für die Betrachtung einer Sache wichtigen Dinge konzentrieren und alles andere außer Acht lassen, verringern wir die Komplexität und haben eine Chance, den Sachverhalt zu verstehen. Die Kapselung ist nun das praktische Instrument zur Durchsetzung der Abstrahierung. Im Auto findet die Kapselung über die Trennung von Innen- und Motorraum statt. In den Innenraum ragen nur die wichtigen Funktionen hinein, wie eben die Pedale und das Lenkrad. Zudem geben uns die Instrumente nur eine abstrakte Sicht auf das, was einen knappen Meter weiter vorne tatsächlich passiert. Mit der Kapselung im Softwareentwurf verstecken wir explizit bestimmte Codeteile vor anderen Codeteilen bzw. den Entwicklern dieser anderen Teile. Indem wir nur die Teile unseres Codes zugänglich machen, die ein anderer Entwickler auch verwenden soll, ersparen wir ihm, sich durch einen Wust an Code durcharbeiten zu müssen, bevor er versteht, welche Funktionen für ihn wirklich interessant sind. Das klingt natürlich ein wenig nach Bevormundung, und in der Tat ist das auch ein bisschen so. Aber letztlich hilft es den einzelnen Entwicklern, sich nicht mit unzähligen Funktionen und Eigenschaften beschäftigen zu müssen, sondern sich auf die wenigen Funktionen konzentrieren zu können, die für das Benutzen wichtig sind. Kapselung ist somit eine praktische Methode zur Verringerung von Komplexität und auch zur Verringerung von Fehlerquellen, denn was ich nicht benutzen kann, kann ich auch nicht falsch benutzen. Sehen wir uns hierzu ein kleines Beispiel an. package { import flash.geom.Point; public class Headline extends Sprite { private var xOffset:Number = 0; private var yOffset:Number = 0; public static var H1:Point = new Point( -4, -3); public static var H2:Point = new Point( -2, -2); public static var H3:Point = new Point( 0, -1); function Headline(size:Point) { setOffset(size.x, size.y); refresh(); }
84
Lösungen entwerfen public function setOffset(i_xOffset:Number, i_yOffset:Number):void { xOffset = i_xOffset; yOffset = i_yOffset; } private function refresh():void { // Aktualisiert das Layout der Headline ... } } } Listing 3.1: Beispiel für schlechte Kapselung
Das Beispiel in Listing 3.1 zeigt eine einfache – nicht vollständige – Headline-Komponente. Wir gehen davon aus, dass diese Komponente intern ihr Textfeld je nach gewählter Schriftgröße ein wenig zurechtrücken muss, damit es visuell korrekt auf dem Nullpunkt der Komponente sitzt. Dies soll angedeutet werden durch die beiden Werte xOffset und yOffset. Betrachten wir diese Klasse nun aber nur aus dem Blickwinkel der Kapselung. Zunächst einmal sind die beiden Offset-Werte private, was schon mal positiv auffällt, denn so sind sie nicht direkt zugänglich und können nicht fälschlicherweise direkt verändert werden. Die drei vermeintlichen Konstanten allerdings sind nicht mittels const wirklich als Konstanten definiert, sondern per var veränderlich. Das ist an sich keine gute Kapselung, denn nun könnte man den Inhalt der Konstanten ändern, was bei Konstanten eigentlich nicht gemacht werden soll. Viel schlimmer aber ist, dass sie vom Typ eines Points sind und konkrete Offset-Werte enthalten. Das hat zwei Nachteile: Zum einen würde das Verwenden von const für diese drei Attribute keinen großen Unterschied machen, denn die Werte im Point wären nach wie vor veränderbar. Zum anderen wird nach außen ein wichtiges Implementierungsdetail dieser Klasse preisgegeben, nämlich dass sich hinter den Headlinegrößen scheinbar Steuerungswerte verbergen. Der Konstruktor verlangt als Parameter ein Point-Objekt. Nehmen wir mal an, die Dokumentation klärt uns Entwickler darüber auf, dass wir eine der drei Quasi-Konstanten übergeben sollen. Die Tatsache, dass wir schon wissen, was sich hinter den Konstanten verbirgt, versetzt uns nun in die Lage, einfach ein Point mit zwei beliebigen Werten an den Konstruktor zu übergeben und so die schöne Idee mit den festen drei Headlinegrößen zunichte zu machen. Aber das war noch nicht alles. Der Konstruktor ruft intern zunächst die setOffset()-Methode auf und danach die refresh()-Methode. Jemand war zwar so schlau, die refresh()-Methode private zu deklarieren, aber die setOffset()-Methode ist public. Und sie nimmt ganz konkret einen Wert für den X-Offset und für den Y-Offset entgegen. Wenn es vorher keinem klar war, dann spätestens jetzt, wie die Klasse bezüglich der unterschiedlichen Größen arbeitet. Warum ist das aber eigentlich so schlimm? Was ist schon dabei, wenn man einige Details der Implementierung der Klasse kennt? Könnte es nicht vielleicht sogar nützlich sein? Der erste Schwachpunkt dieser Offenheit ist, dass sich für den Entwickler, der diese Klasse nutzt, klammheimlich die Komplexität der Klasse erhöht. Da die setOffset()-Methode nun
85
Kapitel 3 schon für ihn zugänglich ist, ist die Sache der Offsets eine Information, mit der er zwangsläufig konfrontiert wird, obwohl ihm eigentlich egal sein kann, wie die Headline-Komponente es nun genau anstellt, sich korrekt zu positionieren. Man muss hier bedenken, wir gehen von einem Szenario aus, bei dem mehrere Entwickler an einem Projekt arbeiten und unterschiedliche Teile einer Anwendung bauen. Hätten wir nur einen einzelnen Entwickler, der für sich allein ein Projekt stemmt, treten die Probleme nicht direkt in Erscheinung. Wir wollen also verhindern, dass sich ein Entwickler in einem Team mit zu viel Details von Anwendungsteilen beschäftigen muss, die von anderen Entwicklern im Team stammen. Je weniger die Headline-Komponente also unnötigerweise von sich preisgibt, desto übersichtlicher und einfacher stellt sie sich für einen Entwickler dar. Darüber hinaus wird auch die Fehleranfälligkeit verringert. Nehmen wir mal an, ein Entwickler hat sich die öffentlichen Methoden der Headline-Klasse angesehen und denkt sich nun, er könnte ja setOffset() einfach mit eigenen Werten aufrufen, weil er eine ganz spezielle Headlinegröße benötigt. Nun hätte er das Problem, dass ja der Konstruktor nach Aufruf von setOffset() noch refresh() aufruft. Die Methode ist aber private, die kann von außen gar nicht aufgerufen werden. Der manuelle Aufruf von setOffset() wäre also zum Scheitern verurteilt. Bis der Entwickler bemerkt, dass er setOffset() eigentlich gar nicht selbst benutzen sollte, kann aber einige wertvolle Zeit vergehen. Es gibt noch ein weiteres Problem. Da über die beschriebenen Merkmale nun die Implementierung der Headlinemechanik mittels Offsets bekannt ist und manch ein Entwickler sich das eventuell zunutze macht und im Konstruktor ein Array mit eigenen Werten mitgibt, ist es dem Autor der Headline-Klasse unter Umständen nicht mehr möglich, diese Art der Implementierung zu ändern, denn sie wird nun schon konkret von anderen Entwicklern verwendet. Und zu allen Entwicklern in einem Projektteam sagen zu müssen, dass sie ihren Code noch mal ändern müssen, weil man selbst etwas ändern möchte, kann durchaus unangenehm werden. Schauen wir uns also an, wie man es besser machen kann. Listing 3.2 zeigt die überarbeitete Klasse. package { import flash.geom.Point; public class Headline extends Sprite { private var xOffset:Number = 0; private var yOffset:Number = 0; public static const H1:int = 1; public static const H2:int = 2; public static const H3:int = 3; private static const H1_OFFSET:Point = new Point( -4, -3); private static const H2_OFFSET:Point = new Point( -2, -2); private static const H3_OFFSET:Point = new Point( 0, -1);
86
Lösungen entwerfen function Headline() { // Hier nur Initialisierungen ... } public function setSize(size:int):void { switch(size) { case H1: setOffset(H1_OFFSET); break; case H2: setOffset(H2_OFFSET); break; case H3: setOffset(H3_OFFSET); break; default: throw new Error("Ungültige Headlinegröße"); } refresh(); } private function setOffset(i_Offset:Point):void { xOffset = i_Offset.x; yOffset = i_Offset.y; } private function refresh():void { // Aktualisiert das Layout der Headline ... } } } Listing 3.2: Beispiel für eine bessere Kapselung
In dieser Variante haben wir nun die Klasse so verändert, dass nach außen nur noch zwei Dinge sichtbar werden, nämlich zum einen die möglichen Headlinegrößen, die nun aber als Konstanten mit nichtfunktionalen Werten versehen sind (soll heißen, statt der Zahlen 1 bis 3 hätten wir auch andere Zahlen oder irgendwelche Zeichenfolgen nehmen können, es würde keinen Unterschied machen). Das ist wichtig, denn wir benutzen hier die Konstanten eigentlich als Enumeratoren, die es ja leider in ActionScript nicht gibt. Damit wir sie aber zumindest korrekt simulieren können, dürfen die in den Konstanten abgelegten Werte keinerlei funktionale Bedeutung haben. Zum anderen haben wir eine Funktion setSize() eingeführt, die uns erlaubt, die Größe der Headline unter Verwendung einer der drei Konstanten zu setzen. Wie wir an der Implementierung erkennen können, kann hier auch nicht geschummelt werden, denn die tatsächlichen Offsets sind private. Außerdem haben wir die implizite Funktionalität aus dem Konstruktor entfernt. Ein Konstruktor konstruiert, das ist seine Aufgabe. Ihm weitere Aufgaben zu geben, geht gegen die Kohärenz, die wir noch in einem der nächsten Abschnitte besprechen werden.
87
Kapitel 3 Unsere Headline-Komponente hat nun eine klare Schnittstelle, die nur die Funktionalität nach außen verfügbar macht, die wir auch angedacht haben. Dadurch vermeiden wir Fehler durch falsche Benutzung, und wir machen unseren Entwicklerkollegen das Leben ein wenig einfacher, indem wir die Komplexität dieser Klasse nach außen verringern. Nun stellt sich abschließend natürlich noch die Frage: »Was, wenn ein anderer Entwickler nun aber eben eine andere Headlinegröße benötigt?« In einem Projektteam, in dem die Verantwortlichkeiten auf die Teammitglieder verteilt sind, obliegt es dem Autor der HeadlineKlasse zu entscheiden, wie er mit so einer Anfrage umgeht. Er kann entweder eine weitere Größe einführen oder die Schnittstelle um andere Zugriffsmethoden erweitern. Entscheidend aber ist, dass der Autor die Möglichkeit behält, Änderungen vorzunehmen, ohne dass andere Entwickler, die die Headline-Klasse schon benutzen, auch an ihren Code ran müssen. Deswegen ist das richtige Vorgehen, dass der Autor mit Bedacht seine Klasse ändert und nicht jeder andere Entwickler die Klasse auf gut Glück benutzt, wie es bei einer schlecht gekapselten Klasse möglich wäre. Es lässt sich also sagen: Die stärkste Motivation zur Kapselung ist das Verstecken von Implementierungsdetails. Dadurch, dass wir nach außen nur preisgeben, was wir können, aber nicht, wie wir es anstellen, haben wir zum einen die Möglichkeit, die Art der Implementierung falls erforderlich zu ändern, und wir ersparen den Entwicklern, die unsere Klassen benutzen, sich mit zu vielen Informationen herumschlagen zu müssen. Dadurch erhöhen wir die Wartbarkeit und Erweiterbarkeit unseres Codes. Wichtig in dem Zusammenhang ist auch zu verstehen, dass Kapselung nicht allein über die Steuerung von Sichtbarkeit realisiert wird, sondern eben auch dadurch, dass wir statt konkreten Funktionsweisen und Datentypen abstrakte Stellvertreter einsetzen. Statt also die konkreten Offsets als öffentliche Konstanten anzugeben, haben wir abstrakte Enumeratoren eingesetzt, die keinen Rückschluss mehr darauf erlauben, dass überhaupt Offsets verwendet werden. Kapselung verbirgt seine Implementierungsdetails also nicht nur über das Verstecken, sondern auch durch das Abstrahieren. Deswegen muss die öffentlich zugängliche Methode unserer Headline auch setSize() und nicht setOffset() heißen, denn die abstrakte Idee sagt, dass wir die Größe der Headline setzen können, und die Implementierung sagt, dass wir das unter anderem durch Setzen von Offsets realisieren. Kapselung findet natürlich nicht nur bei konkreten Klassen statt, sondern auch in größerem Maßstab. Auch ein Modul oder eine ganze Bibliothek sollte eine gewisse Kapselung besitzen, damit den Entwicklern, die dieses Modul benutzen wollen, die interne Komplexität erspart bleibt. Ein Modul sollte deswegen nur bestimmte seiner Klassen zugänglich machen, mit denen ein Entwickler auch konkret arbeiten können soll. In der Welt der Entwurfsmuster ist die Fassade ein gutes Beispiel für die Kapselung auf Modulebene. In diesem Entwurfsmuster verhindert man nämlich ganz konkret, dass externe Klassen wahllos auf die internen Klassen eines Moduls zugreifen können. Stattdessen definiert man eine klare öffentliche Schnittstelle, die Fassade, die den Zugriff auf das Modul steuert. Alle anderen modulinternen Klassen sind nun hinter dieser Schnittstelle gekapselt. Das macht das Benutzen des Moduls einfacher, denn nun muss sich ein Entwickler nur noch mit der Fassade beschäftigen und nicht mehr mit all den internen Klassen des Moduls.
88
Lösungen entwerfen
3.6 Zugriffsrechte Im vorigen Abschnitt haben wir uns das Thema Kapselung vorgenommen. Man konnte dort bereits erkennen, dass Kapselung in der Praxis unter anderem über die Steuerung von Sichtbarkeiten und Zugriffseinschränkungen erzielt wird. Indem wir Implementierungsdetails verstecken, oder anders ausgedrückt, indem wir nur die Schnittstelle einer Klasse sichtbar machen, vereinfachen wir das Benutzen unserer Klasse. Im konkreten Beispiel haben wir schon gesehen, wie das in der Praxis funktioniert. Über die Zugriffsmodifizierer »public« und »private« haben wir Methoden und Attribute zugänglich gemacht bzw. versteckt. Mit dem Konzept der Zugriffsrechte machen wir deutlich, dass jedes Objekt und jede Klasse unterschiedliche Erscheinungsbilder nach innen und außen haben. Die naheliegendste Form der Steuerung von Zugriffen ist das Verstecken von Attributen, Methoden oder ganzen Klassen. Die beiden bekanntesten Vertreter hier sind die Schlüsselwörter »private« und »public«. Sie sind ein wenig wie Schwarz und Weiß. Etwas ist entweder gar nicht oder ganz zu sehen. In vielen Fällen machen Entwickler keine weitere Unterteilung. Erst wenn Vererbung ins Spiel kommt, merkt man, dass ein »protected« hier und da sinnvoll sein kann, weil eine erbende Klasse dann vielleicht Zugriff auf eine versteckte Eigenschaft der Elternklasse haben soll. Schon hier stellt sich eine interessante Frage. Würde es nicht Sinn machen, grundsätzlich alle Attribute und Methoden statt private immer auf protected zu setzen? Ist es nicht immer so, dass eine erbende Klasse Zugriff auf alle Attribute und Methoden ihrer Elternklasse haben sollte? Bevor wir uns hierfür ein konkretes Beispiel ansehen, erst einmal ein paar Gedanken dazu. Normalerweise stellt eine Elternklasse eine Abstraktion seiner Kinderklasse dar. Fahrrad ist also eine abstraktere Klasse als Mountainbike. Dass Fahrrad abstrakter ist, heißt ja aber nicht, dass es nicht auch ganz konkrete Methoden haben kann. Jedes Fahrrad hat z. B. Pedale, die über die Kette und Zahnräder das Hinterrad antreiben. Nehmen wir also an, Fahrrad hat eine Methode tretePedale() (manch einer könnte einwenden, dass hier ja nun ein Implementierungsdetail preisgegeben würde, nämlich dass ein Fahrrad Pedale hat, mit denen es angetrieben wird. Das stimmt aber in diesem Fall nicht, vielmehr gehören die Pedale zu den wichtigsten Schnittstellen eines Fahrrads). Da jedes Fahrrad auch ein Getriebe besitzt (auch wenn es im einfachsten Fall nur aus einem Kranz besteht), das die Bewegungsenergie auf den Pedalen über Zahnräder an die Kette weitergibt, wäre es wohl sinnvoll, dass die Klasse Fahrrad intern auf eine Getriebeklasse verweist, die die entsprechende Funktionalität bereitstellt. Die Klasse Mountainbike könnte dann beispielsweise in die Lage versetzt werden, ein ganz konkretes Getriebe – also eine konkrete Gangschaltung – zu verwenden. Abbildung 3.13 zeigt, wie das konkret aussehen könnte. Um das Beispiel einfach zu halten, beschränken wir uns hier nur auf die Sache mit dem Getriebe. Die Klasse Fahrrad hat drei Methoden. definiereGetriebe() ist protected (zu erkennen an dem #), tretePedale() ist public und übertrageKraftAufGetriebe ist private. Warum habe ich das so gewählt? tretePedale() ist, wie schon gesagt, die öffentliche Schnittstelle und soll von jedem verwendet werden dürfen, deswegen also public. definiereGetriebe() ist eine Methode, die nicht jeder einfach verwenden können soll. Nur ein
89
Kapitel 3 konkretes Fahrrad soll definieren können, welches Getriebe es verwendet. Deswegen verwenden wir hier also protected. Und die Funktion übertrageKraftAufGetriebe() ist nun eine Funktion, die ganz speziell intern ein Implementierungsdetail der Klasse Fahrrad ist. Sie wird aufgerufen, wenn man in die Pedale tritt, aber das muss ein konkretes Fahrrad wie das Mountainbike nicht mehr interessieren, denn das ist bei jedem Fahrrad so. Es ist also ein Implementierungsdetail des abstrakten Fahrrads. Demzufolge wollen wir dieses Detail vor den konkreten Fahrrädern verstecken, um so einerseits den Entwicklern, die z. B. die Klasse Mountainbike schreiben, etwas Komplexität zu ersparen und um uns andererseits die Möglichkeit zur Veränderung der Implementierung der Kraftübertragung offenzuhalten. Man sieht also, dass protected nicht zwingend immer Sinn macht, manchmal soll es tatsächlich private sein. Nichtsdestotrotz wird man in vielen anderen Fällen eher protected verwenden als private.
Fahrrad Getriebe -
getriebe: Getriebe
# + -
definiereGetriebe(Getriebe) : void tretePedale(Newton) : void übertrageKraftAufGetriebe() : void
Mountainbike
MehrGangGetriebe
Abbildung 3.13: Einfaches Modell eines Fahrrads mit Getriebe
Die Steuerung von Zugriffen kann auch auf höheren Ebenen angewandt werden, z. B. auf Modulebene. Als ein Modul bezeichnen wir hier eine Sammlung von Klassen, die einen bestimmten Teil einer Anwendung darstellen. Es gibt in ActionScript eine gut geeignete Methode, ein Modul zu kapseln, also nur die Teile des Moduls von außen zugänglich zu machen, die auch von anderen Teilen der Anwendung verwendet werden sollen. Es handelt sich um den Zugriffsmodifizierer »internal«, der den Zugriff auf eine Klasse und seine Methoden nur innerhalb des gleichen Pakets erlaubt. (Es gibt noch eine zweite Möglichkeit, das Definieren einer Hilfsklasse einer .as-Datei, die bereits eine Hauptklasse enthält. Die Möglichkeiten innerhalb einer solchen Klasse sind aber sehr eingeschränkt, weswegen ich diesen Weg hier nicht näher erläutern möchte.) Der Modifizierer internal ist recht einleuchtend und einfach anzuwenden, wird aber seltsamerweise trotzdem selten benutzt. Vielleicht deswegen, weil er nur dann seinen Nutzen entfalten kann, wenn ein Modul konzeptionell sauber innerhalb eines Pakets implementiert ist. Stellen wir uns eine Anwendung vor, in der es ein Modul Login gibt. Würde man eine Paketstruktur verwenden, bei der auf oberste Ebene zunächst mal Pakete wie »model«,
90
Lösungen entwerfen »view« und »controller« stehen, würde das implizit bedeuten, dass sich das Modul Login über all diese drei Pakete verteilt. Den Modifizierer »internal« hier zu verwenden, würde dann wenig bringen. Sammelt man die Model-, View- und Controllerklassen des Moduls Login hingegen in einem Paket, dann kann man Klassen, Methoden und Attribute, die nur innerhalb des Moduls verwendet werden, aber nicht Teil der nach außen zugänglichen Schnittstelle sein sollen, auf internal setzen.
login
Dialog
LoginDialog
Process +
startProcess() : void
UserSession
LoginProcess -
userSession: UserSession
~
getUserSession() : UserSession
«internal» RequestUserCredentials
«internal» LoginFailed
ProcessStep
«internal» LoginSucceeded
Abbildung 3.14: Beispiel für den Einsatz von internal als Modifizierer
Abbildung 3.14 zeigt ein Beispiel, wie ein Login-Modul – stark vereinfacht – aussehen könnte. Zunächst habe ich die Methode getUserSession() als internal deklariert (erkennbar an der ~). Innerhalb des Login-Moduls ist der Zugang der UserSession nur Sache des Moduls selbst und nicht Teil der Schnittstelle, deswegen soll nach außen auch nicht sichtbar gemacht werden, dass wir uns der UserSession bedienen. Auch die Hilfsklassen, die die Prozessschritte des Login-Prozesses beschreiben, sind Implementierungsdetails innerhalb des Login-Moduls. Hier sind deshalb gleich die Klassen selbst als internal deklariert. Der Vorteil hier ist jetzt also, dass nach außen nur noch die beiden Klassen, die auch die Schnittstelle nach außen darstellen, sichtbar sind, nämlich LoginDialog aus UI-Sicht und LoginProcess aus Model-Sicht. Ein kleines Problem an diesem Vorgehen kann ich natürlich nicht verschweigen. Der Modifizierer gilt tatsächlich nur für genau das Paket, innerhalb dessen eine Klasse oder seine Methoden oder Attribute auf internal gesetzt wurden. Unterpakete haben folglich auch keinen Zugriff mehr. Das setzt den Strukturierungsmöglichkeiten Grenzen. Wir hätten unsere drei ProcessStep-Klassen also nicht noch in ein Unterpaket stecken können, denn dann hätte LoginProcess auch keinen Zugriff mehr auf sie.
91
Kapitel 3 Manch einer könnte noch anmerken wollen, dass es in ActionScript 3 doch auch noch die Namespaces zur Steuerung von Zugriffsrechten gibt. Das stimmt, Namespaces an sich machen Methoden oder Attribute einer Klasse verfügbar oder nicht. Es gibt aber eine Zwickmühle, wenn wir Wert auf Fehlererkennung zur Kompilierzeit legen (was wir immer tun sollten, denn das erspart uns das Schreiben entsprechender Unit-Tests). Namespaces können nur dann zur Kompilierzeit ausgewertet werden, wenn sie entweder nur innerhalb der gleichen Klasse definiert und verwendet werden oder wenn sie auf Paketlevel oder darüber definiert und somit in jeder beliebigen Klasse verwendet werden können. Das hat aber wiederum im ersten Fall zur Folge, dass ein in einer Klasse definierter Namespace eben wie gesagt nur in der Klasse selbst verwendet werden könnte. Und im zweiten Fall hätte es zur Folge, dass ein auf Paketlevel oder darüber definierter public Namespace eben für alle sichtbar und verwendbar ist, womit die Sichtbarkeit also de facto nicht eingeschränkt wäre. Zwar können Namespaces auch als Variablen umhergereicht werden, um stärker Nutzen aus ihnen zu ziehen, aber sobald sie in dieser Art verwendet werden, erfolgt die Auswertung erst zur Laufzeit, was wieder dazu führen kann, dass eventuelle Fehler erst spät entdeckt werden.
3.7 Schnittstellen Ganze Anwendungen, einzelne Module, Klassen und Methoden, sie alle haben diesen Übergang von dem, was in ihnen ist und was sie nach außen repräsentieren, ihre Schnittstellen. Diese Anknüpfungspunkte, an denen andere Anwendungen, Module, Klassen oder Methoden sich mit ihnen verbinden können, machen die Schnittstelle aus. Die Schnittstelle ist letztlich das Aushängeschild, das sagt, was eine Klasse oder eine Methode kann. Im Prinzip handelten die letzten drei Kapitel Abstrahierung, Kapselung und Zugriffsrechte schon von Schnittstellen. Letztlich stellt die Schnittstelle die nach außen kommunizierte Abstrahierung einer Klasse dar. Über die Schnittstelle macht sich der Entwickler ein Bild von der Klasse. Über die Kapselung sorgen wir ganz konkret dafür, dass sich die offizielle Schnittstelle nicht vermischt mit internen Funktionen und Hilfsobjekten, die bezüglich der Schnittstelle nicht relevant sind. Und die Zugriffsrechte regeln, wer auf welche Teile unserer Schnittstelle zugreifen kann. Schnittstellen sind unabdingbar, wie sollte man sonst eine Methode benutzen, wenn sie keine zugängliche Schnittstelle hätte? Wie sollte man aus einer Klasse Nutzen ziehen, wenn sie keine Schnittstelle in Form von Methoden und Attributen anbieten würde? Schnittstellen machen Code aber auch unübersichtlich, denn je umfangreicher die Schnittstellen sind, umso schwieriger wird es für uns Entwickler, den Überblick zu behalten. Es gibt gerade im Flash-Bereich viele frei verfügbare Bibliotheken, die sehr nützliche Funktionalitäten mit sich bringen, deren Schnittstellen aber teilweise schlecht strukturiert sind, sodass das Benutzen schwerfällt; Methoden, die als Parameter ein untypisiertes Objekt erwarten, was dazu führt, dass man erst einmal in der Dokumentation nachschauen muss, was für ein Objekt da übergeben werden muss. Oder Schnittstellenkonstrukte, deren Sinn sich dem benutzenden Entwickler nicht erschließt, z. B. Klasse A verwenden, um einen Vorgang zu starten, und Klasse B benutzen, um ihn wieder zu stoppen. Schnittstellen, die kei-
92
Lösungen entwerfen nen Raum für Flexibilität lassen, die sich nicht erweitern lassen, Schnittstellen, die noch zig andere Objekte benötigen, um arbeiten zu können. Schnittstellen, bei denen nicht klar wird, dass Methoden in einer bestimmten Reihenfolge aufgerufen werden müssen, und demzufolge auch, was diese Reihenfolge ist. Eine schlechte Schnittstelle zu identifizieren geht recht schnell. Jeder Entwickler erkennt eine schlechte Schnittstelle sofort, wenn er versucht, mit ihr zu arbeiten. Spätestens wenn man anfängt, in Foren zu suchen, sich durch den Quelltext zu arbeiten oder lange Gespräche mit Entwicklerkollegen zu führen, dämmert es jedem, dass die Schnittstelle Platz für Optimierungen bietet. Eigentlich, möchte man meinen, kann es doch nicht so schwer sein, einfache Schnittstellen zu entwickeln. Schließlich wissen wir ja, wie wir eine Schnittstelle gerne benutzen würden. Das scheint aber leider nicht zu klappen. Wo ist das Problem? Ein Hauptproblem besteht oft darin, dass Klassen nicht so gebaut werden, dass die interne Funktionalität sauber von den nach außen zugänglichen Schnittstellenmethoden und -attributen getrennt wird. Zu oft scheint die interne Struktur nach außen durch, was es für andere Entwickler verwirrend macht, die eigentliche Schnittstelle zu erkennen. Damit man aber überhaupt die Möglichkeit hat, die Schnittstelle einer Klasse sauber und verständlich aufzubauen, muss man sich klarmachen, dass eine Klasse überhaupt aus diesen beiden Teilen besteht, Schnittstelle und interne Implementierung, und dass diese, auch wenn sie beide in einer Klasse zusammengefasst sind, zwei unterschiedliche Aufgaben haben. Die Schnittstelle beschreibt, wie man eine Klasse verwendet, die Implementierung beschreibt, wie die Klasse ihre Aufgaben erledigt. Ich spreche hier zwar immer von Klassen, aber das gilt natürlich für einzelne Methoden, Module, Komponenten oder ganze Bibliotheken oder Anwendungen ganz genauso in dem jeweiligen Maßstab. Eine gute Vorgehensweise, um diesen Grundsatz einzuhalten, bietet übrigens das Testdriven Development. Ohne zu stark ins Detail zu gehen, beim TDD schreibt man zuerst die Tests zu einer Klasse, also z. B. Unit-Tests, und dann erst die eigentliche Klasse. Unabhängig von den Vorteilen, die das bezüglich der Testbarkeit und der erhofften Fehlerfreiheit hat, bietet sich der angenehme Nebeneffekt, dass man sich dabei zwingt, zuerst über die Schnittstelle einer Klasse nachzudenken und erst im zweiten Schritt über die Implementierung. Ein Unit-Test testet nämlich immer gegen eine zugängliche Methode, also die Schnittstelle. Schreibt man nun also einen Unit-Test für eine Klasse, die man überhaupt noch entwerfen und entwickeln muss, dann setzt man sich damit indirekt die Brille eines Entwicklers auf, der später diese noch nicht existierende Klasse benutzen wird. Und das ist sehr gut, denn genau so sollte man eine Klasse entwickeln. Zuerst sollte man sich fragen, welche Verantwortlichkeit und Aufgabe die Klasse übernehmen und wie man sie benutzen soll. Dabei soll natürlich das Benutzen so komfortabel, leicht verständlich und effizient wie möglich ausfallen. Erst wenn die Schnittstelle steht, fängt man an, sich darüber Gedanken zu machen, wie man nun die hinter der Schnittstelle liegende Funktionalität baut. Das kann in Einzelfällen natürlich auch mal dazu führen, dass man die Schnittstelle wegen technischer Erfordernisse noch mal anpassen muss. Das sollte aber die Ausnahme bleiben, denn optimalerweise wird die Schnittstelle nicht durch technische Implementierungsdetails beeinflusst. Wenn man das erreicht, ist die Chance groß, dass man eine Schnittstelle erhält, die gut benutzbar ist und die im Zweifel auch gestattet, die Implementierung hinten dran noch mal zu ändern, ohne dass man auch die Schnittstelle ändern muss.
93
Kapitel 3 public class PreloadingManager { private var _queue:Array; public static const DISPLAY:String = "display"; public static const TEXT:String = "text"; public static const BINARY:String = "binary"; public function PreloadingManager(newQueue:Array = null) { ... } public function init():void { ... } public function addLoadItem(url:String, type:String):int { ... } public function addQueue(newQueue:Array):void { ... } public function startLoading():void { ... } ... } Listing 3.3: Schlechte Schnittstelle eines Preloading-Managers. Implementierungsdetails sind sichtbar.
Listing 3.3 zeigt ein Beispiel für einen schlechten Schnittstellenentwurf. Es handelt sich hier um eine Klasse, die Preloading-Vorgänge steuern soll. Die Methode init() ist eine Methode, die für einen Entwickler keinen Nutzen hat, sie muss wohl aufgerufen werden, damit die Klasse richtig funktioniert. Wie schon gesagt, es kann Situationen geben, wo sich aufgrund technischer Einschränkungen solche Methoden nicht umgehen lassen, aber in jedem Fall führen sie zu einer schlechteren Schnittstelle, denn sie bergen Fehlerpotenzial, man könnte z. B. schlicht vergessen, init() aufzurufen. Sowohl der Konstruktur als auch die Methode addQueue() nehmen ein Array entgegen. Schlecht daran ist, dass hiermit öffentlich gemacht wird, dass die Klasse intern mit einer Liste arbeitet, ein Detail, das man nicht unbedingt preisgeben sollte, will man die Klasse vielleicht später auf eine Slot-Mechanik umstellen (also eine Mechanik, in der es limitierte Ladeplätze gibt, sodass nicht beliebig viele Dateien gleichzeitig geladen werden). Hier wird dem Nutzer sogar die Möglichkeit gegeben, eine komplette Queue zu übergeben, was einiges an Wissen über den internen Aufbau der Queue erforderlich macht. Die Klasse wird den Aufbau des Arrays nicht so schnell ändern können, denn dann läuft sie Gefahr, dass andere Klassen, die den Preloading-Manager nutzen, nicht mehr funktionieren. Zudem wurde als Typ nur ein einfaches Array angegeben, das ohne Zuhilfenahme einer Dokumentation keinerlei Aufschluss über den Aufbau der Queue gibt, die man da übergeben soll. Hier hätte man wenigstens eine eigene Klasse schreiben müssen, die den Aufbau der Queue und ihrer Elemente festlegt. Auch die Methode addLoadItem() legt sich zu stark auf Implementierungsdetails fest. Sie nimmt zwei Parameter entgegen, einen konkreten String und einen Typ. Durch diese Festlegung ist es jetzt schwierig, später noch mal weitere Funktionalität hinzuzufügen. Beispielsweise könnte man ein Zielobjekt angeben wollen, in das die
94
Lösungen entwerfen Datei reingeladen werde soll. Besser wäre es, wenn addLoadItem() einen eigenen Typ, z. B. LoadItem, entgegennehmen würde. Eine Klasse LoadItem könnte man später beliebig um weitere Attribute erweitern, ohne dass bereits bestehender Code nicht mehr funktioniert.
3.7.1 Flexible Schnittstellen mit Interfaces In einer modularen Anwendungsstruktur stellen einzelne Module bzw. Klassen einerseits Funktionalität und Daten zur Verfügung und konsumieren wiederum auch Funktionalität und Daten von anderen Modulen. Die Funktionalität und die Daten, die eine Klasse anbietet, werden durch die zugänglichen Methoden und Attribute repräsentiert. Sie stellen sozusagen das Serviceangebot der Klasse dar. Wodurch aber wird das repräsentiert, was eine Klasse an Funktionalität und Daten benötigt? Einen Teil dieser benötigten Funktionalität und Daten lässt sich erahnen über die Parameter, die die Methoden einer solchen Klasse verlangen. Welche weiteren Dinge aber eventuell intern noch angefragt werden, können wir von außen nicht sehen. Müssen wir als Nutzer dieser Klasse ja auch nicht, denn das ist ja gerade das Prinzip der Kapselung. Aus Sicht der Gesamtanwendung allerdings müssen wir natürlich schon dafür sorgen, dass eine Klasse Zugriff auf die anderen Klassen und Module erhält, die sie zum Arbeiten benötigt. Aus dieser Sicht müssen wir also sehr wohl wissen, welche Klassen bzw. Module das sind. Wie also kann eine Klasse kenntlich machen, was sie benötigt? Ein guter Ansatz ist der der »Inversion of Control«, also der Umkehrung des Referenzierungsprozesses (die ich im nächsten Kapitel noch näher erläutern werde). Statt dass sich eine Klasse also intern selbst Referenzen auf benötigte andere Klassen besorgt (indem sie z. B. selbst entsprechende Instanzen erzeugt), hebt sie diese Aufgabe in die öffentliche Schnittstelle und erlaubt nun, dass man ihr von außen diese benötigten Referenzen als Parameter übergibt (z. B. über Setter-Methoden oder über den Konstruktor). Und um dies in möglichst modularer Art und Weise zu tun, benennt sie die benötigten Klassen nicht direkt, sondern in Form von Interfaces (oder in Form von abstrakten Klassen, auch wenn es diese so nicht gibt in ActionScript). Ein Interface kann man im Prinzip mit einer Erwartungshaltung übersetzen. Eine Klasse oder auch ein ganzes Subsystem oder eine Komponente benötigt eine Leistung oder Funktionalität und beschreibt diesen Bedarf in einem Interface. Zusätzlich deklariert sie entsprechende Setter-Methoden, mithilfe derer man dann konkrete Implementierungen des Interface an die Klasse übergeben kann. Betrachten wir nochmals das Beispiel eines Videoplayers für unsere fiktive Firma FilmRegal. Im Videoplayer benötigt die player-Komponente z. B. ein Video, das sie dem realen Nutzer vorspielen will (siehe Abbildung 3.15). Die player-Komponente beschreibt also ihren Bedarf bzw. ihre Erwartungshaltung in einem Interface, Video. Die flashvideo-Komponente wiederum kann nun diese Erwartungshaltung erfüllen und bestätigt dies dadurch, dass sie das Interface-Video implementiert, also eine Klasse zur Verfügung hat, die den Schnittstellenvertrag schließt.
95
Kapitel 3
player
flashvideo
control::Player Video +
flashvideo:: FlashVideo
setVideo(Video) : void
Abbildung 3.15: Darstellung des Interface-Videos im Komponentenmodell
Eine Interface-Beschreibung geht also immer von dem aus, der etwas benötigt. Man kann hierfür eine Analogie zur realen Welt herstellen. In der realen Welt werden Produkte oder Leistungen selten von sich aus angeboten. Zuerst ergibt sich eine Nachfrage (auch wenn sie künstlich erzeugt wurde). Diese Nachfrage ist das Interface. Und erst dann kommen Produzenten oder Dienstleister, um für diese Nachfrage eine konkrete Leistung oder ein konkretes Produkt anzubieten. Das ist dann die Implementierung des Interface. Es macht letztlich wenig Sinn, dass eine Komponente, die eine Leistung anbieten will, ein Interface baut, in dem sie selbst beschreibt, wie die Leistung benötigt werden könnte, um dann dieses Interface auch gleich selbst zu implementieren. Man muss davon ausgehen, dass die Gefahr, dass das Interface dann nicht mit den konkreten Bedürfnissen anderer Klassen übereinstimmt, recht groß ist. Dieses Vorgehen gilt nicht nur für konkrete Interfaces, sondern grundsätzlich für die Definition der Schnittstelle einer Klasse. Deswegen ist der Ansatz, zuerst die Schnittstelle zu definieren und die konkrete Implementierung der Funktionalität hinterher zu machen, erfolgversprechend, denn sie führt dazu, dass man sich darauf konzentriert, konkrete Probleme zu lösen.
3.8 Lose Kopplung Es soll ja Mobiltelefone und Notebooks geben, bei denen man nicht den Akku austauschen kann. Es gibt auch kaum Digitalkameras, bei denen man den Bildsensor gegen einen besseren austauschen könnte. Hingegen kann man bei Spiegelreflexkameras das Objektiv wechseln, man kann in einem Computer meistens die Festplatte gegen eine größere tauschen, in eine Lampenfassung kann man herkömmliche Glühbirnen oder Energiesparlampen reindrehen, und in heutige moderne Webbrowser kann man beliebige Plug-Ins einfügen und aus ihnen herausnehmen. Manche Dinge sind stark mit anderen Dingen verbunden bzw. gekoppelt. Andere Dinge sind sauber voneinander entkoppelt. Ein Güterzug kann beispielsweise recht flexibel aus vielen unterschiedlichen Arten von Waggons aufgebaut werden. Die Erfindung des Containers in der Schifffahrt hat eine starke Entkopplung der konkreten Güter zum Lade- und Löschvorgang ermöglicht, denn egal ob Bananen, Fahrräder oder Computer verladen werden, alles wird in Container verpackt und vereinfacht so das gesamte Prozedere enorm.
96
Lösungen entwerfen Lose Kopplung ermöglicht uns die isolierte Betrachtung und Bearbeitung eines Teils, ohne dabei das Ganze zu stören. Wir können eine defekte Batterie austauschen, ohne dass wir gleich das ganze Gerät wegschmeißen müssen. Wir können ein Modul oder eine Klasse verändern und optimieren, ohne dass wir die ganze Anwendung umbauen müssen. Wir können auch einen Fehler in einer Klasse beheben, ohne Angst haben zu müssen, dass danach die restliche Anwendung nicht mehr funktioniert. Was nicht heißt, dass wir nicht sicherheitshalber die Anwendung testen sollten, aber in der Regel werden deutlich weniger Fehler in der Gesamtanwendung nach der Veränderung eines lose gekoppelten Teils auftreten als bei einem stark gekoppelten Teil. Lose Kopplung bringt uns zudem ein Mehr an Flexibilität. Je weniger eine Klasse A an eine Klasse B gekoppelt ist, je mehr sie sich stattdessen nur an abstrakte Typen wie allgemeine Klassen oder Interfaces bindet, umso flexibler kann sie auch eingesetzt werden.
AppointedTime
Calendar
Reminder +
addEntry(Reminder) : void
-
remindDate: Date ringTone: Sound
Abbildung 3.16: Starke Kopplung von Calendar zu Reminder
Abbildung 3.16 zeigt ein einfaches Beispiel von unnötiger Kopplung einer Kalender-Klasse zu einem Reminder-Element. Normalerweise möchte man in einen Kalender unterschiedliche Arten von Einträgen vornehmen, wobei es die Klasse Kalender gar nicht konkret interessieren muss, welche Arten das sind. Einem Kalender ist letztendlich nur wichtig, dass der Eintrag ein Datum hat und vielleicht noch eine Kurzbeschreibung (zumindest soll uns das für dieses Beispiel reichen). Warum also eine konkrete Kopplung zu einem Reminder zulassen? Besser wäre es, wenn Calendar einfach grundsätzliche Datumseinträge erlauben würde.
Calendar +
AppointedTime + +
addEntry(AppointedTime) : void
date: Date shortDescription: String
Reminder -
remindDate: Date ringTone: Sound
Birthday -
person: Person yearOfBirth: Date
Abbildung 3.17: Lose Kopplung von Calendar zu Reminder
97
Kapitel 3 Abbildung 3.17 zeigt nun, wie die Kopplung zwischen Calendar und Reminder vermindert werden kann, und zwar hier ganz simpel über das Einführen einer abstrakten Klasse AppointedTime. Dadurch können wir nun ganz unterschiedliche Arten von Kalendereinträgen erlauben, zum Beispiel auch Geburtstage. Calendar ist dadurch deutlich flexibler geworden. Die in den vorangegangenen Kapiteln besprochenen Themen zahlen alle auf eine losere Kopplung ein. Die Kapselung führt dazu, dass Implementierungsdetails unsichtbar werden, was zu einer klareren und vereinfachten Schnittstelle führt, die, wenn sie abstrakt genug ist, eine sehr lose Kopplung ermöglicht. Bezüglich der Schnittstelle spricht man hier im Zusammenhang mit loser Kopplung von einem möglichst geringen Fan-out. Das bedeutet, dass eine Klasse möglichst wenige Abhängigkeiten von anderen Klassen haben sollte. Man kann sich leicht vorstellen, dass eine Klasse, die noch zehn andere Klassen benötigt, um arbeiten zu können, nicht gerade lose gekoppelt ist. Dies soll im Umkehrschluss aber nicht bedeuten, dass eine solche Klasse deswegen alles selbst machen soll. Im Gegenteil, eine Klasse, die sehr viele andere Klassen benötigt, um ihre Aufgabe zu erledigen, tut eventuell zu viel. Schauen wir uns ein paar Vorgehensweisen an, mit denen wir lose Kopplung konkret erwirken können. Der Klassiker ist gerade auch in der Flash-Welt sicherlich das Observer-Entwurfsmuster, sprich die Events. Indem also eine Klasse nicht direkt die Methode einer anderen Klasse aufruft, sondern nur unbestimmt eine Nachricht ins Ungewisse sendet, an der sich andere Klassen angemeldet haben, wird zwischen diesen Klassen eine lose Kopplung gewährleistet, zumindest in einer Richtung, nämlich von der Nachrichten sendenden Klasse zu den Zuhörern. Ebenfalls bekannt ist das Mediator-Entwurfsmuster. Es wird auch in einigen ArchitekturFrameworks eingesetzt, auf die ich noch zu sprechen kommen werde. Ein Mediator wird übersetzt mit Vermittler. Das Konzept ist ganz einfach. Anstatt viele Klassen direkt verwenden zu müssen, um eine konkrete Aufgabe zu lösen, setzt man einen Mediator ein, der diese konkrete Aufgabe übernimmt und dafür die verschiedenen Klassen verwendet. Die Kopplung ist dadurch zwar nicht weg, aber verlagert, denn nun hat die benutzende Klasse nur noch eine recht lose Kopplung zu einem Mediator anstatt zu vielen konkreten Klassen. Freilich sitzt die Kopplung nun im Mediator, aber das ist o.k., er dient uns als abstrahierendes Hilfsobjekt. Haben Sie schon mal eine Klasse erstellt, der Sie den Namen IrgendwasManager geben wollten? Dann könnte es eine Mediator-Klasse geworden sein. Abbildung 3.18 zeigt ein Beispiel eines solchen Managers. Sicherlich könnte ein Entwickler auch die Klassen Timer, TweenEffect und so weiter direkt benutzen. Der TweeningManager entkoppelt ihn aber von dem konkreten Zusammenspiel, das für das Animieren notwendig ist. Dadurch herrscht von einer nutzenden Klasse zu den konkreten Klassen wie TweenEffect und Sequence eine lose Kopplung, und Sequence könnte also auch noch in anderen Szenarien verwendet werden, als nur für das Tweening. Auch könnte man Sequence jetzt ruhig anpassen, vielleicht sogar die Schnittstelle ändern, denn man müsste dann nur noch den TweeningManager ändern und nicht alle nutzenden Klassen, die Tweenings verwenden.
98
Lösungen entwerfen
Sequence
Timer TweenEffect
TweeningManager DisplayObject
Abbildung 3.18: Beispiel eines Mediators: TweeningManager
Bei der losen Kopplung unter Verwendung abstrakter Superklassen und Interfaces gibt es nun noch ein Problem. Schauen wir uns noch mal das Kalender-Beispiel an. Calendar erwartet den abstrakten Typ AppointedTime, der selber keine konkrete Funktionalität besitzt und also nicht instanziiert werden kann (genau genommen kann er schon instanziiert werden, weil es in ActionScript ja leider noch keine abstrakten Klassen gibt, aber wir wollen ihn nicht direkt instanziieren). Calendar muss nun einen konkreten Subtyp von AppointedTime bekommen, aber wer gibt ihm den? Das Problem ist generell, wenn eine Klasse ein Objekt einer abstrakten Klasse oder eines Interface erwartet, muss ihr irgendwer eine konkrete Klasse übergeben, die entweder von der abstrakten Klasse erbt oder die das Interface implementiert. Wenn das aber wiederum jemand tut, dann haben wir ja wieder eine Kopplung von diesem Jemand und der konkreten Klasse. Wie vermeiden wir das nun, wie also bekommt eine nutzende Klasse ein konkretes Objekt, wenn sie nur einen abstrakten Typ oder ein Interface erwartet?
Woher bekommt ClassA eine Instanz von einer Klasse, die InterfaceB implementiert (z.B. ClassC)?
«interface» InterfaceB
ClassA
ClassC
Abbildung 3.19: Problem der losen Kopplung, wer erzeugt Instanzen?
99
Kapitel 3 Zunächst einmal müssen wir uns im Klaren sein, zwischen welchen Klassen bzw. Modulen wir denn eine lose Kopplung erreichen wollen. Es ist ineffektiv, lose Kopplung zwischen allen Klassen erreichen zu wollen. Es gibt ganz eindeutig Klassen, die zusammengehören und zwischen denen auch ruhig eine gewisse Kopplung vorhanden sein kann. Nehmen wir zum Beispiel ein Sprite, das auf Klick ein Event vom Typ MouseEvent wirft. Es ist vollkommen in Ordnung, dass Sprite Kenntnis von der Klasse MouseEvent hat. Zwar besteht hier eine Kopplung zwischen Sprite und MouseEvent, aber diese ist in Ordnung, weil MouseEvent ein Hilfsobjekt ist, das von mehreren anderen Klassen auch verwendet wird. Wenn wir die Struktur unserer Anwendung erstellen, müssen wir uns konkret überlegen, welche Teile der Anwendung von anderen Teilen lose gekoppelt sein sollen, damit sie isoliert änderbar oder sogar austauschbar sind. Innerhalb eines solchen Teils ist nicht unbedingt eine lose Kopplung notwendig, es sei denn, wir vermuten dort Klassen, die z. B. nicht nur allein innerhalb dieses Teils verwendet werden sollen, sondern auch woanders. Lose Kopplung setzen wir also dort ein, wo wir Flexibilität brauchen. Zurück zum Kalender-Beispiel. In diesem Fall können wir definieren, dass die Klasse, die Calendar benutzen und dort Termine eintragen will, ruhig die konkreten Arten von Terminen kennen soll. Wir reichen also die Kopplung an die nutzende Klasse weiter. Damit bleibt Calendar entkoppelt von den möglichen Terminarten. Sollten wir dennoch auch die nutzende Klasse – den Klienten – von den konkreten Termin-Klassen entkoppeln wollen, könnten wir das Factory-Method-Entwurfsmuster anwenden. Wir würden dann eine FactoryKlasse erstellen, die eine statische Methode getAppointedTime(type:String) hätte (siehe Listing 3.4). Die Factory-Klasse hätte außerdem pro konkreter Terminart eine Konstante, die man als Typ der Methode übergibt. getAppointedTime würde abhängig vom übergebenen Typ die entsprechende konkrete Termin-Klasse zurückgeben. Nun mögen Sie sagen: »Furchtbar, jetzt wird der Typ über einen String verklausuliert, der Compiler kann nun nicht mehr ansprechen, wenn man einen falschen Typ übergibt.« Das stimmt auch, wir haben hier Komfort hinsichtlich Fehlerprüfung durch den Compiler verloren, aber durch den Umweg über die String-Konstanten (weil wir ja leider keine Enumeratoren in ActionScript haben) haben wir lose Kopplung zwischen dem Client, den Termin-Klassen und Calendar gewonnen. Man muss hier abwägen, was wichtiger ist. public class AppointedTimeFactory { public static const REMINDER:String = "reminder"; public static const BIRTHDAY:String = "birthday"; public static function getAppointedTime(type:String):AppointedTime { switch(type) { case REMINDER: return new Reminder(); case BIRTHDAY: return new Birthday(); default: throw new Error("Ungültiger type."); } } } Listing 3.4: Eine Hilfsklasse nach dem Schema des Factory-Method-Entwurfsmusters
100
Lösungen entwerfen
3.8.1 Inversion of Control Eine andere Möglichkeit, lose gekoppelte Codeteile miteinander zu verbinden, liegt in dem Prinzip der Inversion of Control. Ich hatte es oben schon angesprochen, wenn wir Codeteile erstellen, die losgelöst sein sollen von anderen Codeteilen, dann stellt sich die Frage, wer diese losgelösten Codeteile eigentlich miteinander verbindet. Wenn also eine Klasse A eine Instanz vom Typ eines Interface erwartet und Klasse B dieses Interface implementiert, wie erhält nun Klasse A eine konkrete Instanz von Klasse B, ohne dass wir A doch wieder konkret an B koppeln? Das Prinzip der Inversion of Control besagt, dass eine Klasse sich nicht selbst die Instanzen anderer Klassen besorgen soll, sondern dass jemand anderes ihr diese Instanzen übergeben soll. Statt dass also eine Klasse sich beispielsweise in ihrem Konstruktor Instanzen von anderen Klassen erzeugt, könnte sie z. B. Parameter in der Konstruktorfunktion erwarten, die ihr die benötigten Instanzen bereits übergeben. public class ClassA { private var someInstance:SomeInterface; public function ClassA() { myInstance = SomeFactory.getSomeInterfaceInstance(); } } Listing 3.5: Lose Kopplung mittels Factory Method, ohne Inversion of Control public class ClassA { private var someInstance:SomeInterface; public function ClassA(i_someInstance:SomeInterface) { someInstance = i_someInstance; } } Listing 3.6: Lose Kopplung mittel Inversion of Control im Konstruktor
In Listing 3.5 sehen wir ein herkömmliches Beispiel des Factory-Method-Entwurfsmuster, wie wir es schon kennen. Gegen dieses Beispiel ist nichts zu sagen. ClassA verwendet eine Factory, um sich die Instanz einer anderen Klasse zu besorgen. Sie tut dies selbst, im Konstruktor. Bei Inversion of Control verlagern wir das Problem der Beschaffung einer Instanz gänzlich aus dem Client, hier also ClassA. Listing 3.6 zeigt es, wir definieren einfach, dass man beim Erzeugen von ClassA dem Konstruktor als Parameter bereits eine Instanz mitgeben muss. ClassA interessiert sich jetzt also nicht mehr dafür, woher die Instanz kommt. Damit wurde die lose Kopplung also auf die Spitze getrieben. Die Instanz muss nun also derjenige erzeugen, der ClassA erzeugt.
101
Kapitel 3 Spinnen wir das weiter. Wenn jede Klasse ihre benötigten Instanzen nicht mehr selbst erzeugt, sondern sie sich übergeben lässt, dann kommt die Frage auf, wer überhaupt noch Instanzen erzeugt. Das ist jetzt natürlich übertrieben, denn ich hatte zuvor schon erwähnt, dass wir die absolute lose Kopplung von jeder Klasse zu jeder Klasse nicht brauchen. Manche Klassen können durchaus Kenntnis von anderen Klassen haben. Lose Kopplung setzt man nicht zum Selbstzweck ein, sondern um in bestimmten Situationen mehr Flexibilität und verringerte Komplexität zu erreichen. Es macht also keinen Sinn, jede Klasse nach dem Prinzip der Inversion of Control aufzubauen. Für die Klassen jedoch, bei denen wir das Prinzip anwenden wollen, erreichen wir damit wieder etwas mehr lose Kopplung. Jetzt haben wir nur noch das Problem, dass die Klasse, die in unserem Beispiel ClassA erzeugen würde, somit konkrete Kenntnis von ClassA und den konkreten Klassen, die ClassA benötigt, haben muss. Das klingt wiederum nach recht starker Kopplung. Inversion of Control wird deswegen meist in Verbindung mit allgemeinen Dependency Injection Frameworks eingesetzt. Dependency Injection ist eine konkrete Umsetzung des Prinzips der Inversion of Control. Dependency Injection Frameworks sind Grundgerüste, die Klassen basierend auf Konfigurationen miteinander »verdrahten«. In einer solchen Konfiguration, die entweder über eine externe XML-Datei erstellt wird oder auch direkt in einer allgemeinen Konfigurationsklasse, wird nun festgelegt, dass eine Klasse B, die das Interface SomeInterface implementiert, erzeugt werden soll, um sie dann der ebenfalls zu erzeugenden ClassA zu übergeben. Das Framework wertet diese Konfiguration dann zur Laufzeit aus und erzeugt nach der Vorgabe Instanzen und übergibt diese wiederum an andere erzeugte Instanzen. Daraus bildet sich dann letztendlich der gesamte Applikationsbaum. Die Übergabe der Instanzen kann entweder über den Konstruktor oder über setter-Methoden erfolgen. <property name="someInstance" ref="concreteInstance" /> Listing 3.7: Beispiel einer einfachen setter-basierten Dependency-Injection-Konfiguration
Listing 3.7 zeigt ein simples Beispiel für eine Konfiguration für ein Dependency Injection Framework, die eine Instanz über setter übergibt. Zunächst wird eine Instanz von ClassA mit der id classA definiert. Für sie wird zudem definiert, dass sie die Instanz mit der id concreteInstance übergeben bekommen soll. Diese Instanz ist wird ebenfalls in der Konfiguration definiert, und sie ist vom Typ ClassB. Um nun die lose Kopplung in diesem Beispiel noch nachvollziehen zu können, schauen wir uns die beteiligten Klassen im UML-Diagramm an (Abbildung 3.13). Man kann sehen, dass ClassA ein Attribut someInterface vom Typ SomeInterface hat (das letztlich natürlich eine ActionScript-setter-Methode ist). ClassB wiederum implementiert dieses Interface. Im Code also haben wir eine lose Kopplung zwischen ClassA und ClassB. Die konkrete Verdrahtung erfolgt nun also nur in der Konfiguration in XML.
102
Lösungen entwerfen
ClassA +
«interface» SomeInterface
ClassB
someInstance: SomeInterface
Abbildung 3.20: Klassendiagramm zum Beispiel in Listing 3.1
Eine kleine Warnung muss hier auch noch ausgesprochen werden. Lose Kopplung im Allgemeinen und Dependency Injection, wie hier gezeigt, im Speziellen kann die Lesbarkeit und Übersichtlichkeit des Codes verschlechtern. Die Hilfskonstrukte, die lose Kopplung ermöglichen, führen auch immer ein kleines Stück zu schlechterer Lesbarkeit, denn um die Zusammenhänge von Klassen zu erkennen, muss man jetzt unter Umständen noch über mehrere Schritte und mehrere Klassen hinwegsteigen. Dependeny Injection verstärkt diesen Eindruck noch, denn nun lassen sich die Abhängigkeiten im Code allein gar nicht mehr erkennen, sondern man muss sich zusätzlich noch durch Konfigurationsdateien arbeiten, und diese können recht schnell sehr groß werden. Auch dies ist wieder ein Grund dafür, nicht wahllos jedwede Klasse von jeder anderen entkoppeln zu wollen. Gerade Depency Injection sollte eher auf Modulebene als auf Klassenebene angewandt werden.
3.9 Modularität Als ein Modul bezeichnen wir eine Sammlung von Klassen, die zusammen einen gemeinsamen Zweck erfüllen und deswegen fachlich als zusammengehörig definiert werden können. Module sind demzufolge ein weiteres Instrument zu Kapselung von Code, eine Stufe höher als Klassen. Wozu braucht man aber noch eine weitere Kapselungsstufe? Wie wir noch in Kapitel Kohärenz und Verantwortlichkeit sehen werden, soll sich eine Klasse immer nur genau um eine Aufgabe kümmern. Eine Textparser-Klasse soll Text parsen. Sie soll nicht den Text laden, sie soll auch kein Debug- oder Fehlerlogging durchführen, sie soll nur Text parsen. Denn nur so können Aufgaben voneinander unabhängig implementiert, verbessert oder verändert werden. Nun muss man im Zusammenhang vom Parsen eines Textes ja aber den Text laden, und im Fehlerfall ist es auch durchaus sinnvoll, Debug-Ausgaben irgendwo hinzuschreiben, damit man sie auswerten kann. Es gibt also Aufgaben, die sich in Teilaufgaben und Teilverantwortlichkeiten unterteilen lassen. Wir können in diesen Fällen die Gesamtaufgabe, z. B. Textparser, als ein Modul definieren und die Teilaufgaben, Laden des Textes, konkretes Parsen, Debug- oder Fehlerausgaben, als Klassen innerhalb des Moduls. Ein Modul hat genau wie eine Klasse eine interne Implementierung, die in diesem Fall aus der konkreten Auswahl der involvierten Klassen besteht, und eine öffentliche Schnittstelle, die aus den konkret definierten Klassen besteht, die man von außen benutzen soll. Bei unserem Textparser wäre das zunächst schwierig zu entscheiden. Soll die Ladeklasse die
103
Kapitel 3 Schnittstelle sein oder der Parser? Wahrscheinlich sollte man eher einen Mediator erstellen (Entwurfsmuster, hatten wir schon kurz angesprochen, und dazu kommen wir im nächsten Kapitel auch noch), der die Schnittstelle darstellt und die Zusammenarbeit der drei internen Klassen koordiniert. Wenn ein Modul wie eine Klasse eine Schnittstelle besitzt, dann gelten für Module auch die gleichen Prinzipien, wie wir sie auch schon für Klassen besprochen haben: Abstraktion, Kapselung und lose Kopplung. Wir können diese Prinzipien eins zu eins auf Module übertragen. Es gilt also auch bei Modulen, die internen Implementierungsdetails zu verstecken, eine saubere Schnittstelle zu definieren und möglichst wenige Abhängigkeiten zu anderen Modulen zu erzeugen, also für lose Kopplung zu anderen Modulen zu sorgen.
3.9.1 Motivation Es gibt unterschiedliche Gründe für die Erstellung von Modulen neben den fachlichen, die wir eben betrachtet haben. Da Module komplexe Aufgaben kapseln können, eignen sie sich gut dazu, eine Gesamtanwendung in isoliert zu betrachtende und zu bearbeitende Teile zu zerlegen. Dieses Vorgehen verringert wieder die Komplexität, die ein einzelner Entwickler zu einem bestimmten Zeitpunkt beherrschen muss. Um in der Praxis weitere Vorteile zu bieten, werden Module meistens auch physikalisch isoliert und in einzelne Dateien verpackt. Dieses Vorgehen eröffnet diverse Vorteile:
Mit physikalisch getrennten Modulen muss man bei einer Änderung in einem Modul nicht die gesamte Anwendung neu kompilieren, sondern nur das einzelne Modul.
Module können auf einfache Weise in mehreren unterschiedlichen Anwendungen verwendet werden.
Module können einzeln und unabhängig deployed werden, also z. B. auf einen Webserver gelegt werden.
Module können unabhängig von den Anwendungen, die sie benutzen, eine eigene Historie, sprich Versionierung haben, sie können also unabhängig weiterentwickelt und verbessert werden.
Module ermöglichen innerhalb eines Teams eine besser organisierbare Arbeitsteilung, weil sich Entwickler nicht gegenseitig ins Gehege kommen.
Module ermöglichen sogar die sauber getrennte Aufteilung der Entwicklung einer Anwendung in unterschiedlichen Teams, sogar unterschiedlichen Firmen, wenn die Schnittstellen sauber definiert werden.
Module erleichtern die Entwicklung einer Anwendung in Iterationen, weil man nun pro Iteration definieren kann, welche in sich abgeschlossenen Module entwickelt werden sollen.
Gerade bei Webanwendungen kann eine Aufteilung in physikalische Module ein besseres Ladeverhalten mit sich bringen, wenn Module erst bei Bedarf nachgeladen werden.
104
Lösungen entwerfen Natürlich bleibt die prinzipielle Entscheidung zur Bildung eines Moduls eine fachliche. Es macht keinen Sinn, aus organisatorischen Gründen willkürlich Klassen in ein Modul zusammenzufassen, wenn diese gar keinen Bezug zueinander haben. Ein Modul muss genauso wie eine Klasse eine klar umrissene Aufgabe haben, die sich in einer klar umrissenen Schnittstelle widerspiegelt. Organisatorische Überlegungen kommen da erst an zweiter Stelle. Auch sollte man sich hüten, zu viele kleinteilige Module zu definieren. Denn natürlich bedeutet ein Mehr an Modulen auch ein Mehr an Schnittstellen zwischen diesen Modulen und damit wieder eine Steigerung der Komplexität und Verschlechterung der Übersichtlichkeit. Insofern wird die Entscheidung für die Definition eines Moduls immer aus einer Mischung von fachlichen, organisatorischen und technischen Faktoren entstehen. Daraus lässt sich aber auch erkennen, dass die Entscheidung, ob und in welche Module eine Anwendung aufgeteilt werden soll, durchaus einen deutlichen Einfluss auf die gesamte Projektorganisation haben kann und deswegen nicht leichtfertig getroffen werden sollte.
3.9.2 Realisierung Wie schon erwähnt, werden Module in der Praxis meistens auch physikalisch gekapselt, sprich in eigene Dateien kompiliert. Ein Modul ist somit in der Flash-Welt meistens eine eigene .swf- bzw. .swc-Datei. Die Planung einer solchen physikalischen Trennung ist in Flash nicht trivial. Anders als in Java wird in Flash nicht jede Klasse in eine eigene Bytecode-Datei kompiliert. Vielmehr werden in eine .swf-Datei grundsätzlich erst einmal alle Klassen kompiliert, die vom Compiler während des Parsevorgangs erfasst wurden. Das können deutlich mehr als gewollt sein, wenn man seine Anwendung in Module aufteilen will. Es ist deswegen meistens einfacher, wenn man seine Anwendung erst von den Modulen an aufwärts strukturiert. Sprich, man ist gut beraten, wenn man zuerst die einzelnen Module definiert und als Unterprojekte aufsetzt. In diesen Unterprojekten erzeugt man dann die jeweiligen .swc- und .swf-Dateien. Diese verwendet man dann als externe Bibliotheken im Projekt der eigentlichen Hauptanwendung. Je später man die physikalisch zu trennenden Module definiert, umso aufwendiger wird es, die bereits bestehenden Projektsetups wieder zu ändern und neu aufzuteilen. Ein weiteres Problem, das Flash speziell mit sich bringt, ist die Gefahr von doppelten Klassen- oder Interface-Deklarationen in unterschiedlichen Modulen. Betrachten wir ein simples Szenario. Ein Modul A benötigt eine bestimmte Funktionalität und artikuliert diese in einem Interface. Modul B kann diese Funktionalität liefern und implementiert nun dieses Interface. Beide Module müssten nun also das Interface in ihre SWF-Datei kompilieren, um vollständig zu sein. Nun könnte es aber zum Konflikt kommen, wenn beide Module in den gleichen Applikationskontext geladen werden (ich werde hier nicht näher auf das Thema »ApplicationDomains« von Flash eingehen, empfehle aber sehr, sich dieses Thema in der Dokumentation von Flash oder Flex genau durchzulesen). Da hier theoretisch die Gefahr besteht, dass die beiden Interfaces unterschiedlich sein könnten, weil die eine Version z. B. veraltet ist, sollte das Interface möglichst nur in ein Modul hineinkompiliert werden. Wohin aber gehört das Interface?
105
Kapitel 3 Denken wir noch mal an den Grundsatz eines Interface. Ein Interface beschreibt eine Erwartungshaltung. Die Erwartungshaltung geht von dem Modul aus, das eine bestimmte Funktionalität benötigt. Sie äußert also die Erwartungshaltung, sie definiert sie. Demzufolge muss das Interface in das Modul, das die Funktionalität benötigt. Die Module, die das Interface implementieren, sollen also Letzteres nicht in ihre SWF- bzw. SWC-Datei hineinkompilieren. Daraus entsteht natürlich eine Gefahr. Wenn ein Modul, das ein Interface implementiert, aber nicht kompiliert, in einer Anwendung eingesetzt wird, in der das Interface aus welchen Gründen auch immer nicht definiert wird, dann würde es zu einem Laufzeitfehler kommen, weil nun die Interface-Deklaration vollends fehlt. Hierzu gleich eine Anmerkung vorweg: Wenn man sich in so einer Situation befindet, dann hat man es zunächst einmal mit einem Entwurfsproblem zu tun: Erstens sollte die nutzende Anwendung in jedem Fall das Interface bereitstellen, denn schließlich bittet sie ja um die im Interface definierte Funktionalität, und zweitens sollte das Modul, welches das Interface implementiert, die implementierende Klasse nicht direkt über den Typ der konkreten Klasse, sondern nur über den Typ des Interface zugänglich machen, um die lose Kopplung zu gewährleisten. Dieses Problem kann man also nur auf designtechnischem Wege umgehen, indem man die Anwendung, die das Modul mit der Interface-Implementierung nutzt, zwingt, das Interface bereitzuhalten. Das kann man wiederum durch saubere Kapselung erreichen. Wenn das Modul mit der Interface-Implementierung diese Implementierung selbst – also die Klasse – nicht direkt zugänglich macht, sondern zum Beispiel über eine Factory Method, die wiederum als Typ nur das Interface angibt, dann ist die nutzende Anwendung letztlich gezwungen, das Interface bereitzustellen (siehe Abbildung 3.21).
ModuleA
«interface» NeededFunctionality
User +
setNeededFunctionality(NeededFunctionality) : void
ModuleB
FunctionalityProvider +
ConcreteFunctionality
getNeededFunctionality() : NeededFunctionality
Abbildung 3.21: Beispiel für die Verortung eines Interfaces in Modulen
106
Lösungen entwerfen Die Entscheidung, eine Anwendung in Module aufzuteilen, kann Einfluss auf grundsätzliche Architekturentscheidungen haben. Zum Beispiel erschweren manche Architektur-Frameworks die Bildung physikalisch getrennter Module, andere fördern diese. Auch der Einsatz von Dependency Injection Frameworks kann einen Einfluss auf die Möglichkeit zur Modularisierung haben, denn Dependency Injection läuft grundsätzlich zwischen Klassen ab, das DI Framework muss also in der Lage sein, bei Bedarf Module nachzuladen, um definierte Verbindungen zwischen Klassen unterschiedlicher Module zu erzeugen. Die Aufteilung in Module gehört deswegen mit zu den grundsätzlichen Architekturentscheidungen.
3.9.3 Beispiele Für ein Beispiel bemühe ich wieder das Beispielprojekt des Online-Videoverleihs FilmRegal aus der Analysephase. Dort gilt es unter anderem, einen Videoplayer zu bauen, der die Filme, die man sich auf FilmRegal ausleiht, online abspielt. Innerhalb des Videoplayers unterscheide ich drei Module. Da haben wir einmal die Nutzerverwaltung. Wir nennen das Modul ganz einfach user. In diesem Modul wird der Nutzer, der gerade den Videoplayer benutzt, repräsentiert. Wir müssen hier eine Session verwalten, und wir benötigen Informationen darüber, ob der Nutzer die Berechtigung hat, das Video anzusehen. Der User soll sich aber im Videoplayer nicht einloggen, stattdessen soll der Videoplayer eine bestehende authentifizierte Session ID übergeben bekommen. Als zweites Modul nehmen wir das Video selbst, wir nennen es flashvideo. Jetzt könnte man sich fragen, warum man für ein Video noch ein eigenes Modul braucht, wo Flash doch schon alle Klassen in seiner API bereitstellt. Wir wollen aber die verschiedenen technischen Möglichkeiten, ein Video abzuspielen, kapseln. Letztlich soll das Modul ein Video zur Verfügung gestellt bekommen, und es soll sich nicht darum kümmern müssen, ob es ein per Netstream geladenes Video ist, ob es ein in einem SWF eingebettetes Video ist und ob es gestreamt oder komplett vorgeladen wird. Diese verschiedenen Möglichkeiten plus die Details, wie ein Video z. B. per NetStream geladen wird, sind Implementierungsdetails, für die sich das Modul user nicht zu interessieren braucht, deswegen kapseln wir das in einem Modul flashvideo. Zu guter Letzt haben wir noch das Modul player, das nun die Informationen aus den Modulen user und flashvideo zusammenbringt und letztlich das Video anzeigt. Hier ist auch das User-Interface enthalten. Damit nun das Modul player klarmacht, dass es ein Video benötigt, definiert es ein Interface, in dem die Anforderungen an die Funktionalität beschrieben werden. Abbildung 3.22 zeigt noch mal die grundsätzliche Struktur auf Modulebene. Die »Verdrahtung« der Module zur Laufzeit machen wir über Dependency Injection, aber nicht über ein Framework, sondern ganz einfach über die Hauptklasse VideoPlayer. Dazu übergibt VideoPlayer zunächst die von außen erhaltene Session ID sowie die gewünschte Movie ID an die Klasse User im user-Modul. Nachdem das user-Modul sich Informationen über den eingeloggten Nutzer geholt und überprüft hat, ob der Nutzer legitimiert ist, sich den angeforderten Film anzusehen, übergibt er an VideoPlayer ein MovieObjekt, in dem die URL zum Video hinterlegt ist. Diese URL übergibt VideoPlayer an das flashvideo-Modul und lässt sich ein neues Objekt zurückgeben, welches das Video-Inter-
107
Kapitel 3 face implementiert. Dieses Video-Objekt übergibt VideoPlayer dann an das player-Modul, das die Steuerung des Videos samt Darstellung übernimmt. Über diesen Weg sind das player- und das flashvideo-Modul miteinander verdrahtet worden, ohne sich zu kennen, denn das player-Modul hat nun eine Referenz auf ein konkretes Video-Objekt aus dem flashvideo-Modul.
videoplayer
Videoplayer
Video player
«flow» Movie
Player
«flow» +
setVideo(Video) : void
user MovieContract -
contractID: int movie: Movie
Video
0..* 1 User -
userID: UserIdentification contracts: Array
+ + +
getIdentificationState() : IdentificationStates setRequestedMovieID(String) : void setSession(String) : void
flashvideo
FlashVideo
Abbildung 3.22: Aufbau unseres Videoplayers aus Modulsicht
Durch diese Aufteilung in drei kleine Module haben wir drei grundsätzliche Aufgabenbereiche aufgetrennt, Nutzerverwaltung, Videostreaming und Playersteuerung bzw. -darstellung. Nun ist die Entwicklung eines Videoplayers keine Mammutaufgabe, aber wir könnten, wenn es erforderlich wäre, ohne Weiteres drei Entwickler auf dieses Projekt ansetzen, die jeweils ein Modul übernehmen. Einer von ihnen könnte dann noch die übergreifende VideoPlayer-Klasse verantworten. Außerdem sind wir durch die lose Kopplung zwischen diesen drei Modulen in der Lage, eines der Module isoliert zu optimieren oder intern zu verändern.
108
Lösungen entwerfen Während der Entwicklung eines der Module ist es recht einfach möglich, sich sogenannte Mockup-Versionen der anderen Module zu erstellen, also Versionen, die die Funktionalität nur simulieren, aber eigentlich leer sind. Mithilfe dieser Mockup-Objekte wäre ein Testen der jeweils anderen Module möglich, bevor diese anderen Module fertig sind. Vom userModul erwarten wir z. B. nur ein einziges Objekt, das Movie-Objekt, und daraus benötigen wir letztlich auch nur die URL zum Video. Wir könnten hier die lose Kopplung also noch verbessern, wenn wir statt der ganzen Klasse Movie nur die URL als String an VideoPlayer zurückliefern würden. So ein Mockup lässt sich leicht in wenigen Zeilen Code simulieren. Auch das flashvideo-Modul lässt sich einfacher als gedacht simulieren. Für den player ist es nämlich vollkommen unerheblich, ob das Objekt, das er da bekommt, tatsächlich ein Video ist. Wir können zum Testen genauso gut einfach eine Klasse bauen, die von Sprite erbt und das Video-Interface implementiert mit simulierten Werten, z. B. einem internen Timer, der irgendwann anzeigt, das Video wäre fertig abgespielt. Auf diese Weise könnten also auch schon Unit-Tests für die einzelnen Module geschrieben und ausgeführt werden, während die anderen Module noch in Arbeit sind. Außerdem hat die Bildung der Module noch den Vorteil, dass wir sie in anderen Anwendungen wiederverwenden könnten. Stellen wir uns zum Beispiel vor, im Videoportal FilmRegal gibt es noch eine Anwendung, in der Nutzer ihre ausgeliehenen Filme verwalten können und sehen, wie lange die einzelnen Ausleihverträge noch gelten. In einer solchen Anwendung ließe sich das user-Modul wunderbar wiederverwenden. Auch kann es sein, dass jeder Besucher des Videoportals, ob eingeloggt oder nicht, sich kostenlos Trailer von Filmen ansehen kann. Wir könnten dann eine vereinfachte Version des Videoplayers benutzen, in der nur das flashvideo- und das player-Modul verwendet werden. Es könnte sogar die gleiche Hauptanwendung sein, die, wenn sie in einem bestimmten Modus aufgerufen wird, das user-Modul gar nicht erst nachlädt.
3.10 Vererbung Wenn man sich mit objektorientierter Programmierung beschäftigt, ist die Vererbung mit eines der ersten Prinzipien, das man kennenlernt. Vererbung erscheint auf den ersten Blick sehr simpel. Eine Klasse, die von einer anderen Klasse erbt, hat somit die gleichen Methoden und Attribute wie die beerbte Klasse. In ActionScript ist Vererbung so implementiert, dass eine Subklasse intern ein sogenanntes Trait-Objekt besitzt, in dem alle Methoden und Attribute der Superklasse enthalten sind. Traits sind an sich eine sehr interessante Erfindung von einer Gruppe von Softwareingenieuren der Universität Bern in der Schweiz (ECOOP 2003 – Object-Oriented Programming, 2003) und liefern ein Konzept zur Realisierung von Kompositstrukturen, bei denen eine Klasse sich zusammensetzt aus ihrer eigenen Spezifikation und den Spezifikationen der Traits, die sie verwendet. In ActionScript ist dieses Konzept etwas eingeschränkt verwendet worden in der Art, dass jede Klasse nur ein Trait-Objekt haben kann, in das nämlich die Attribute und Methoden der Superklasse hineinkopiert werden.
109
Kapitel 3 Spannend wird die Vererbung dadurch, dass eine Superklasse steuern kann, ob und in welcher Form sie ihre Attribute und Methoden verfügbar macht für eventuelle Subklassen und ob sie überhaupt Subklassen zulässt. Eine Klasse kann über das Schlüsselwort final in der Klassendeklaration entscheiden, ob sie überhaupt beerbt werden darf. Warum könnte man wollen, dass eine Klasse nicht beerbt werden soll? Schauen wir uns dazu ein Beispiel an. public final class LoginController { // ... public function userIsAccepted(alias:String, password:String):Boolean { var creds:Credentials = new Credentials(alias, password); var auth:Authentication = userService.getAuthentication(creds); return (auth.isKnown && auth.isValidated); } // ... } Listing 3.8: Finale Klasse (Variablennamen gekürzt für bessere Lesbarkeit)
Listing 3.8 zeigt einen Auszug aus einem LoginController. Dieser hat eine Methode userIsAccepted(), die Login-Daten entgegennimmt, intern prüft und als Ergebnis true oder false zurückgibt, je nachdem, ob der Nutzer bekannt und auch schon validiert ist (Informationen, die die Authentication-Klasse uns liefert). Diese Implementierung ist sicherheitsrelevant. Die Business-Logik besagt, dass wir einen Nutzer explizit nur zulassen, wenn er bekannt UND validiert ist. Wäre diese Klasse nicht final, könnten wir von ihr erben und die Methode userIsAccepted() überschreiben und mit einer eigenen Überprüfung ausstatten, die vielleicht weniger strikt ist. Das würde aber die Business-Regel umgehen, und das dürfen wir nicht erlauben. Deswegen deklarieren wir die Klasse LoginController als final. Wir hätten auch eine weniger restriktive Variante wählen und anstatt die ganze Klasse LoginController nur die Methode userIsAccepted() als final deklarieren können. Das kann in manchen Fällen ausreichen. In Fällen aber, wo eine Klasse ganz konkrete Logik, z. B. Business-Logik implementiert, wird man meistens nicht wollen, dass diese Logik noch mal durch Beerbung verändert werden kann, und sollte dann am besten gleich die ganze Klasse als final deklarieren. Im Prinzip gelten in der Vererbung die gleichen Prinzipien zur losen Kopplung, Kapselung und Sichtbarkeit wie zwischen Klassen, die nicht voneinander erben. Vererbung sollte nicht als eine Form der Verknüpfung von zwei Klassen angesehen werden, die dazu führt, dass diese beiden Klassen sich gegenseitig komplett bloßstellen. Im Gegenteil, beide Klassen, die Super- wie die Subklasse, müssen ihre eigene Integrität bewahren. Zunächst mal ist die Kopplung in der Vererbung auf den ersten Blick nur in einer Richtung stark, nämlich von der Subklasse zur Superklasse. Schließlich kennt die Subklasse die Superklasse, aber nicht umgekehrt. Wir werden aber noch sehen, dass das indirekt nicht gilt.
110
Lösungen entwerfen
3.10.1 Vererbung von Implementierung Die Gründe, warum man Vererbung einsetzen will, können unterschiedlich sein, aber im Prinzip lassen sie sich auf zwei Grundideen zurückführen. Entweder will man geschriebenen Code in anderem Zusammenhang wiederverwenden, oder man will die fachliche Ähnlichkeit von Objekten ausdrücken. Bei der Wiederverwendungsidee hat man zumeist bereits eine Klasse, die eine bestimmte Funktionalität bereitstellt. Nun hat man eine weitere Klasse, die etwas Ähnliches tut, die sich aber in ein paar Details unterscheidet und vielleicht zusätzliche Funktionen benötigt. Nehmen wir als Beispiel eine Queue-Klasse. Gehen wir davon aus, wir haben schon eine Queue-Klasse. Sie arbeitet nach dem Fifo-Prinzip. Das erste Element, das man reingetan hat, kommt auch als Erstes wieder raus. Nun kommt der Tag, an dem wir einen PreloadingManager brauchen. Uns fällt nun auf, dass ein PreloadingManager letztlich wie eine Queue arbeitet (wir nehmen dafür an, dass wir nicht mehrere Dateien parallel laden wollen). Die erste Datei, die wir dem PreloadingManager übergeben, soll auch zuerst geladen werden. Was liegt also näher, als dass unser PreloadingManager von Queue erbt? Wir müssen natürlich noch ein paar Zusatzfunktionen hinzufügen, denn eine Queue lädt bekanntlich keine Dateien, sie wirft auch keine Events, wenn ein Element geladen ist, usw. Letzten Endes hätten wir in unserer Klasse die grundsätzliche Funktionalität der Queue nicht mehr bauen müssen, die Methoden für das Hinzufügen oder Herausnehmen stellt die Queue bereits alle zur Verfügung. Es gibt aber einen gedanklichen Haken. Ein PreloadingManager ist nun mal keine Queue. Eine einspurige Einbahnstraße ist eine Queue. Die Schlange vor einer Kasse ist eine Queue. Aber ein PreloadingManager lädt Dateien, und dass er dies mithilfe einer Queue macht, ist ein Implementierungsdetail, das sich ja vielleicht auch noch mal ändern kann. Wir wollen ja Implementierungsdetails grundsätzlich von der Öffentlichkeit fernhalten, damit wir die Implementierung bei Bedarf ändern können. Wenn nun aber der PreloadingManager direkt von der Queue erbt, dann haben wir damit jedermann mitgeteilt, dass hier eine Queue zum Einsatz kommt. Wie sollten wir dem PreloadingManager nun noch beibringen wollen, Dateien parallel zu laden? Vererbung aus dem Antrieb heraus zu betreiben, Implementierung wiederzuverwenden, ist normalerweise keine gute Idee. Im Falle des PreloadingManagers wäre es schlauer, dass er einfach intern eine Queue verwendet, anstatt direkt von ihr zu erben. Das bedeutet zwar, dass der PreloadingManager selbst Methoden zum Hinzufügen oder Wegnehmen von Elementen implementieren muss, aber das ist dennoch das kleinere Übel.
3.10.2 Vererbung von Konzepten Anstatt konkrete Implementierung über Vererbung wiederzuverwenden, entfaltet Vererbung ihre volle Macht eher dann, wenn wir sie dazu nutzen, abstrakte Konzepte zu vererben. Eine Command-Klasse beispielsweise ist zunächst erst einmal ein abstraktes Konzept. Ein Command kapselt eine ganz konkrete Aufgabe in sich. Deswegen hat ein Command meist im Prinzip nur eine öffentliche Funktion, die z. B. execute() heißt oder ähnlich. Diese
111
Kapitel 3 Idee von einem Command ist abstrakt, und wenn man sich konkrete Commands ansieht, dann erledigen sie die unterschiedlichsten Aufgaben, sie scheinen überhaupt nichts miteinander gemein zu haben. Ein Command lässt vielleicht eine andere Klasse Dateien laden, ein anderer Command startet über weitere Klassen eine Anfrage an einen Applikationsserver usw. Und dennoch eint diese Klassen, die alle unterschiedliche Dinge tun, eine Gemeinsamkeit, nämlich das ihnen zugrunde liegende Konzept eines Commands. Hier wird also nicht eine konkrete Implementierung vererbt, wahrscheinlich ist die allgemeine CommandKlasse fast leer. Hier wird ein Konzept vererbt. Andere Klassen, die Commands nutzen, müssen nur das Konzept verstehen, das sich in der simplen Schnittstelle manifestiert, der execute()-Methode. Ein sehr gutes weiteres Beispiel in der Flash-Welt sind Sprites und MovieClips. Ein MovieClip erbt von Sprite, und auch fachlich ist ein MovieClip ein Sprite. Ein MovieClip ist halt ein Sprite mit einer Timeline. Hier wird die Vererbung eines Konzepts gut sichtbar. Wir können gar nicht sicher sein, ob die Implementierung der Methode, die den x-Wert eines Sprites verarbeitet, exakt gleich ist zur Methode, die das beim MovieClip tut. Das ist aber auch egal. Denn hier geht es vorrangig darum, dass hier ein Konzept vererbt wurde, nämlich das eines interaktiven, möglicherweise andere Elemente beinhaltenden, grafischen Objekts. Und es wurde erweitert um eine Zeitkomponente, die Timeline. Es ist hier kein Widerspruch, wenn bei dieser Vererbung auch konkrete Implementierung zur Wiederverwendung vererbt wird. Dies soll aber nicht der treibende Faktor sein, sondern ein zusätzlicher positiver Nebeneffekt.
3.10.3 Liskovs Substitutionsprinzip Im Zusammenhang mit der Vererbung von Konzepten gibt es ein Grundprinzip, das in der Softwarewelt einige Bekanntheit erlangt hat, das Substitutionsprinzip, beschrieben von Barbara Liskov vom MIT Laboratory for Computer Science in Cambridge. In ihrer Abhandlung »Data Abstraction and Hierarchy« (aus Leigh Power, 1988) formuliert sie das Prinzip, dass eine Klasse, die von einer anderen Klasse erbt, das Konzept der beerbten Klasse exakt so beibehalten muss, damit ein Programm entweder die eine oder die andere Klasse nutzen können soll, ohne dass man das Programm auch nur im Geringsten deswegen abändern müsste. Wenn also eine Klasse B von einer Klasse A erbt, dann darf sie das Konzept, also die äußere Funktionalität und Intention von Klasse A, nicht verändern. Ein Programm, das z. B. zuerst Klasse A verwendete, sollte in der Lage sein, Klasse B einzusetzen, ohne dass sonst etwas an dem Programm verändert werden muss. Das klingt sehr streng. Was steckt dahinter? Wenn wir ein Konzept, eine Idee vererben, dann sagen wir damit aus, dass jede Klasse, die von diesem Konzept erbt, sich auch daran hält. Ein MovieClip ist ein Sprite. MovieClip erbt von Sprite, erbt das Konzept von Sprite und verpflichtet sich damit, sich genauso zu verhalten wie ein Sprite. Würde ein MovieClip z. B. in der addChild()-Methode ein neues Objekt nicht an oberster Stelle im Stapel platzieren, sondern an unterster Stelle, dann würde zwar die Schnittstelle oberflächlich noch stimmen, aber dennoch würde sich ein MovieClip demnach anders verhalten als ein Sprite. Dann könnte man nicht mehr sagen, ein MovieClip ist ein Sprite.
112
Lösungen entwerfen Es kommt also nicht nur darauf an, dass eine Subklasse bei der Vererbung eines Konzeptes die Schnittstelle beibehält, sondern dass sie sich wirklich genauso verhält wie die Superklasse. Ein weiteres Beispiel: Nehmen wir eine Text-Komponente. Es ist eine simple Komponente, die Text unter Verwendung der TextField-Klasse darstellt und die Abstandsberechnungen durchführt, damit man andere Komponenten an ihr ausrichten kann. Listing 3.9 zeigt einen Auszug daraus. public class Text { private var textField:TextField; public function Text () { // ... } public function setText(text:String):void { // ... } private function recalculateOffsets():void { // ... } // ... } Listing 3.9: Auszug aus einer einfachen Text-Komponente
Nehmen wir nun an, wir erhalten die Anforderung, eine Kontextverlinkung innerhalb eines solchen Textes vorzunehmen. Bestimmte Wörter in einem Text sollen verlinkt werden, wenn sie in einer Wortliste vorkommen. Wenn man auf das Wort klickt, erscheint dann eine kleine Erläuterung. Wir entscheiden uns, eine abgeleitete Klasse zu schreiben. Wir nennen sie LinkedText. Listing 3.10 zeigt die einfache Erweiterung. public class LinkedText extends Text { // ... public function setContextMapping(map:ContextLinkMap):void { // analysiert den Text und setzt eventuelle Links } private function onContextLinkSelected(e:MouseEvent):void { dispatchEvent(new ContextLinkEvent()); } } Listing 3.10: Erweiterung von Text um kontextuelle Verlinkung
113
Kapitel 3 LinkedText fügt also unter anderem (ich habe hier das Beispiel vereinfacht) zwei Methoden hinzu. Einmal kann man ein Map-Objekt angeben, das die Schlüsselwörter enthält, die, falls sie im Text gefunden werden, verlinkt werden sollen. Und natürlich soll LinkedText andere Klassen benachrichtigen, wenn man auf so einen Kontextlink geklickt hat, daher wirft LinkedText in diesem Fall ein Event. Wir können hier gut erkennen, dass LinkedText wirklich immer noch eine Text-Komponente ist. Ein Programm, das bisher die Klasse Text verwendete, könnte nun LinkedText verwenden, ohne dass das Programm noch in einer anderen Art und Weise angepasst werden müsste. Es sei denn, das Programm will die neue Funktionalität tatsächlich nutzen, dann muss es natürlich verändert werden, aber darum geht es in dem Substitutionsprinzip ja auch nicht. In diesem kleinen Beispiel hat LinkedText die Originalmethoden von Text nicht überschrieben, weil es nicht notwendig war. Das ist aber keine grundsätzliche Forderung des Substitutionsprinzips. Eine Subklasse kann durchaus eine Methode einer Superklasse überschreiben, solange sich dadurch an der Funktionalität nichts ändert. Schreiben wir hierfür noch ein kleines Beispiel. Listing 3.11 zeigt eine Klasse, die das laute Vorlesen von Text über eine externe API ermöglichen soll (auch hier beschränken wir uns wieder auf den wesentlichen Ausschnitt der Klasse). public class AudioText extends Text { private var reader:AudioController; // ... override public function setText(text:String):void { super.setText(text); reader.compileAudioText(text); } } Listing 3.11: Eine weitere Erweiterung von Text, vorgelesener Text
Die Klasse AudioText überschreibt die Methode setText() der Superklasse Text. Wir können aber sehen, dass durch das Überschreiben die Funktionalität und das Konzept der Klasse Text nicht beeinträchtigt werden. Nach wie vor könnte ein Programm statt der Klasse Text nun AudioText verwenden, ohne dass wir das Programm abändern müssten, denn setText() aus AudioText ruft hier zunächst super.setText(text) auf und führt dann nur noch eigene Anweisungen aus, die die Funktionalität der Superklasse Text nicht beeinflussen.
114
Lösungen entwerfen Es gibt viele Situationen, in denen man intuitiv Vererbung einsetzen möchte, weil man der Meinung ist, dass zwei Klassen viel gemeinsam haben und Vererbung hier sinnvoll sein könnte. Wenn Sie merken, dass sie durch Vererbung die Grundidee ihrer Superklasse in der Subklasse verändern würden, dann ist Vererbung eventuell nicht die richtige Maßnahme. Statt Vererbung ist dann vielleicht Komposition die bessere Alternative. Eine Klasse kann, wie schon weiter oben erwähnt, auch einfach eine andere Klasse intern als Instanz verwenden, anstatt direkt von ihr zu erben. Das nennt man dann Komposition.
3.10.4 Kapselung beibehalten in der Hierarchie Eine Superklasse kann definieren, welche Attribute und Methoden in ihren Subklassen sichtbar und welche Methoden zusätzlich auch noch veränderbar sind. Für die Sichtbarkeit zuständig sind die Schlüsselwörter private und protected. Private bedeutet nicht sichtbar, protected bedeutet sichtbar (innerhalb der Vererbungshierarchie). Über das Schlüsselwort final können Methoden als nicht überschreibbar gekennzeichnet werden. Da eine Methode, die private deklariert ist, eh nicht durch eine Subklasse erreichbar ist, macht es keinen Sinn, eine Methode in einer Superklasse als private und final zu deklarieren. Warum aber sollte man die Sichtbarkeit und Veränderbarkeit innerhalb vererbender Klassen überhaupt steuern? Dazu muss man sich zunächst lösen von der Vorstellung, dass erbende Klassen ja mit ihren Superklassen »verwandt« sind und deswegen schon grundsätzlich das Recht hätten, alles von ihren Superklassen zu wissen. Eine Superklasse sollte ihre Implementierungsdetails genauso gut vor ihren Subklassen verstecken, wie eine Klasse Implementierungsdetails generell verstecken sollte. Wir können bei Superklassen von zwei Sichtbarkeiten sprechen: der Sichtbarkeit nach außen, also gegenüber Klassen, die nicht in der Vererbungshierarchie stehen, und der Sichtbarkeit nach innen, also gegenüber Subklassen. Die Sichtbarkeit gegenüber Subklassen sollte genauso kontrolliert werden wie gegenüber äußeren Klassen. Der Grund ist wieder, dass wir unsere Superklasse so gut wie möglich kapseln sollten, damit wir eine möglichst lose Kopplung zu den Subklassen erreichen können. Eine Superklasse, die alle Methoden und Attribute als protected und nicht final deklariert, öffnet im Prinzip fast alle Implementierungsdetails gegenüber ihren Subklassen. Das kann dazu führen, dass Subklassen dies schamlos ausnutzen und zum Beispiel bestimmte Attribute direkt verwenden oder manche Methoden, die eigentlich nicht zur Schnittstelle der Superklasse gehören, direkt benutzen. Wenn das passiert, hat man sich die Möglichkeit, die Superklasse noch mal zu ändern, ohne dass die Subklassen gleich nicht mehr funktionieren, schon verbaut. Schauen wir uns das auch wieder in einem Beispiel an. public class CalendarItem { private var _date:Date; private var _title:String; public final function get date():Date { return _date; }
115
Kapitel 3 public final function set date(value:Date):void { _date = value; validateDate(); } private function validateDate():Boolean { // prüft z.B., ob Datum in der Vergangenheit liegt } public final function get title():String { return _title; } public final function set title(value:String):void { _title = value; } } Listing 3.12: Ein allgemeiner Kalendereintrag
Listing 3.12 zeigt ein vereinfachtes Beispiel eines Kalendereintrags. Es verfügt über ein Datum und einen Titel. Für das Datum wird intern eine Prüfung angestoßen, die z. B. prüfen soll, ob das Datum in der Vergangenheit liegt, was nicht erlaubt wäre. Die Schnittstelle ist hier recht einfach. Die konkreten Attribute sind private deklariert, was bedeutet, dass also auch eventuelle Unterklassen nicht direkt auf die Variablen zugreifen können. Das ist gut, denn so könnten wir die Typen der beiden Variablen später noch ändern, wenn erforderlich. Die getter- und setter-Methoden sind alle als final deklariert, denn zwar sollen sie aufgerufen werden können, aber Unterklassen sollen sie nicht mehr verändern. validateDate() ist eine private Methode, denn die Prüfung der Gültigkeit eines Datums ist die Aufgabe des CalendarItems und soll in jedem Fall stattfinden. Unterklassen sollen sich nicht um die Implementierungsdetails dieser Prüfung kümmern müssen, deswegen wird diese Methode vor ihnen versteckt. Eine konkrete Unterklasse könnte nun wie in Listing 3.13 aussehen. public class Birthday extends CalendarItem{ private var _person:Person; private var _yearOfBirth:uint; public function get person():Person { return _person; } public function set person(value:Person):void { _person = value; } public function get yearOfBirth():uint { return _yearOfBirth; } public function set yearOfBirth(value:uint):void { _yearOfBirth = value; } } Listing 3.13: Konkretes CalendarItem, die Birthday-Klasse
116
Lösungen entwerfen Die Klasse Birthday erbt also von CalendarItem. Sie konzentriert sich ganz auf die Erweiterung ihrer allgemeineren Superklasse und bringt hierfür eine Person sowie einen Wert für das Geburtsjahr mit. Die allgemeinen Funktionen und Attribute der Superklasse sind vor Birthday versteckt. Der Entwickler von Birthday läuft somit nicht Gefahr, die internen Werte von CalendarItem in falscher Weise zu verändern, außerdem ist Birthday vollkommen lose gekoppelt zur Implementierung von CalendarItem, sodass CalendarItem beliebig intern verändert werden könnte, ohne dass Birthday aufhört zu funktionieren.
3.10.5 Polymorphie Polymorphie ist ein starkes Instrument zur Entkopplung von Klassen und ihren Implementierungen. Zur Erinnerung an die Funktionsweise zeigt Listing 3.14 ein kurzes Beispiel. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
public class AbstractClass { public function doSomething():void { trace("AbstractClass is here!"); } } public class A extends AbstractClass { override public function doSomething():void { trace("Class A is here!"); } } public class B extends AbstractClass { override public function doSomething():void { trace("Class B is here!"); } } public class Program { public function Program():void { var listOfItems:Vector. = Vector.([new A(), new B()]); for (var i:int = 0; i < listOfItems.length; i++) { listOfItems[i].doSomething(); } } } // -> Class A is here! // -> Class B is here!
Listing 3.14: Beispiel für Polymorphie
117
Kapitel 3 Zwei Klassen, A und B, erben von der Klasse AbstractClass. In Program erstellen wir dann einen Vektor vom Typ AbstractClass und übergeben ihm jeweils eine Instanz von A und B. Das ist gültig, weil A und B beide AbstractClass als Superklasse haben. Die etwas umständliche Schreibweise gibt ActionScript vor, um in einen Vektor vom Typ einer Superklasse ein Array mit Elementen von Subklassen zuzuweisen. Danach gehen wir den Vektor durch und rufen jeweils die Methode doSomething() auf. Hier kommt nun die Polymorphie ins Spiel, und zwar in zweierlei Hinsicht. Zur Kompilierzeit schaut der Compiler auf den Vektor listOfItems und prüft, ob AbstractClass eine Methode doSomething() anbietet. Wäre das nicht der Fall, dann würden wir in Zeile 30 einen Compilerfehler erhalten. Zur Kompilierzeit weiß der Compiler nicht, dass wir in die listOfItems konkrete Subklassen gepackt haben. (Im Prinzip könnte er es schon wissen, er kann es in Zeile 26–27 sehen, er vergisst es aber schnell wieder.) Zur Laufzeit wiederum befinden sich in listOfItems ja aber konkrete Instanzen, und zwar Instanzen von A und B, die zwar von AbstractClass erben, die aber die Methode doSomething() überschrieben haben. Der FlashPlayer führt nun also die konkreten Methoden von A und B aus, daher also die entsprechenden trace Ausgaben. Polymorphie beschreibt also einen klassischen Entkopplungsmechanismus. An der Stelle, an der wir durch die Liste der AbstractClass-Items iterieren, gehen wir davon aus, dass wir AbstractClass-Elemente vor uns haben. In Wirklichkeit sind es Subklassen, die die gleiche Schnittstelle anbieten. Meine Klasse Program kennt zwar eigentlich die konkreten Elemente, schließlich hat sie sie ja selbst erzeugt in Zeile 27. Aber nehmen wir mal an, Program würde die Liste von jemand anderem übergeben bekommen, dann würde Program nur Kenntnis von AbstractClass besitzen. Zwischen Program und Klasse A und B gäbe es keinerlei Kopplung. Polymorphie ist ein effektives Werkzeug zur Kapselung von Verhalten und Implementierungen innerhalb der Vererbung. Ein klassisches Beispiel für Polymorphie ist das Command-Entwurfsmuster. Vereinfacht ausgedrückt kapselt eine Command-Klasse eine bestimmte Aktion, einen Befehl. Das muss nicht nur eine Zeile Code sein, das können auch mehrere, sogar asynchrone Anweisungen sein. Ein Command kapselt die Anweisungen, die zusammen einen abstrakten Befehl ergeben. Es gibt eine abstrakte Klasse Command, die unter anderem vorgibt, dass ein jeder Command eine execute()-Methode haben soll. Diese Methode tritt die entsprechenden Anweisungen los. Jeder konkrete Command, also jede Subklasse, überschreibt nun execute und implementiert dort genau, was der konkrete Command jeweils tun soll. Eine nutzende Klasse kann nun diese Commands instanziieren und aufrufen, wenn z. B. ein bestimmtes Ereignis eintritt und eine bestimmte Aktion ausgeführt werden soll. Die nutzende Klasse könnte dann in ihre Liste an verfügbaren konkreten Commands greifen und den rauspicken, der für die anstehende Aufgabe geeignet ist. Der Trick mit der Polymorphie ist nun, dass die nutzende Klasse nicht konkret wissen muss, wie man einen konkreten Command benutzen muss, denn das ist durch die abstrakte CommandKlasse festgelegt. Die nutzende Klasse muss also nur wissen, wie die abstrakte CommandKlasse funktioniert, wendet dieses Wissen auf einen beliebigen konkreten Command an, sprich, sie ruft auf dem konkreten Command die execute()-Methode auf, und der konkrete Command führt seine Aktion aus.
118
Lösungen entwerfen Es gibt manche Konstrukte, die bezüglich Polymorphie und also auch Vererbung nicht möglich sind. Stellen wir uns hierzu folgendes Beispiel vor (siehe Listing 3.15). Wir bauen einen PreloadingManager, der Elemente vom abstrakten Typ LoadItem entgegennimmt. LoadItem definiert ein Attribut data, das letztlich das geladene Objekt repräsentiert. In der abstrakten Klasse LoadItem ist es vom Typ Object. Über eine getter-Methode machen wir es zugänglich. Nun leiten wir von LoadItem konkrete Klassen ab, z. B. SoundItem, BitmapItem, XMLItem usw. Sie alle erben nun also das data-Attribut. In den konkreten Klassen aber wissen wir, dass in data nicht einfach ein Objekt vom Typ Object stecken wird, sondern vielmehr konkrete Typen wie Sound, Bitmap und XML. Es wäre also toll, wenn wir in den konkreten Subklassen von LoadItem den getter für data überschreiben könnten, sodass er nun nicht mehr vom Typ Object zurückgibt, sondern die konkreten Typen, die ja auch alle vom Typ Object sind. Das aber ist nicht erlaubt. Man darf in ActionScript beim Überschreiben einer Methode nicht die Deklaration der Methode ändern, sprich, der Typ muss Object bleiben. public class LoadItem { public function get data():Object { // ... } } public class BitmapItem extends LoadItem { override public function get data():Bitmap { // ... }
// 1000 || value < 0.01) { throw Error("invalid delay for MyMovieClip."); }
121
Kapitel 3 stage.frameRate = 1000 / value; } public function get repeatCount():int { return totalFrames; } public function reset():void { gotoAndStop(1); } public function start():void { play(); } public function stop():void { stop(); } } Listing 3.17: MyMovieClip, erbt von MovieClip und implementiert Timer
Man kann sehen, die Implementierung ist recht einfach. In den meisten Fällen bietet uns die Superklasse MovieClip schon Methoden, die konzeptionell auf die Methoden des TimerInterface übertragen werden können. Was haben wir nun davon? Wir haben hier jetzt eine konzeptionelle Mehrfachvererbung realisiert. Unsere Klasse MyMovieClip ist nun sowohl ein MovieClip als auch ein Timer. Wir könnten nun im Prinzip die echte flash.utils.TimerKlasse auch nochmals mit einer eigenen Subklasse MyTimer beerben, die auch unser TimerInterface implementiert, und hätten dann ganz konkret die Verbindung zwischen Timer und MovieClip hergestellt. Dies wiederum könnte sich für Tweening-Klassen als nützlich erweisen, denn diese könnten nun intern unser Timer-Interface als Typ verwenden und dynamisch entscheiden, ob sie für die Realisierung eines Tweenings einen echten MyTimer oder einen MyMovieClip verwenden wollen. Da beide von uns implementierten Subklassen das Timer-Interface implementieren, hätten wir eine saubere lose Kopplung erreicht. Über die Polymorphie wäre es uns möglich, grundsätzlich einfach nur z. B. die start()Methode, die das Timer-Interface definiert, zu verwenden. Zur Laufzeit würde hier dann jeweils entweder die start()-Methode von MyMovieClip oder vom flash.utils.Timer aufgerufen werden. Abbildung 3.23 zeigt diese denkbare Struktur noch mal grafisch. Hier kann man auch sehen, wie eine Tweening-Klasse ihre konkreten Timer-Objekte erhalten könnte, nämlich z. B. über eine Factory.
3.11 Kohärenz und Verantwortlichkeit Mit Kohärenz bezeichnet man den Zusammenhalt von Elementen innerhalb eines Systems. Das können bezogen auf Software z. B. die einzelnen Anweisungen innerhalb einer Methode sein, die Attribute und Methoden innerhalb einer Klasse oder die Klassen und Interfaces innerhalb eines Moduls. Es gibt unterschiedliche Arten von Kohärenz:
122
Lösungen entwerfen
flash
display
utils
MovieClip
Timer
«interface» Timer MyMovieClip
MyTimer
TweenManager -
timer: Timer
«use»
TimerFactory +
createTimer() : Timer
Abbildung 3.23: Das Timer-Beispiel als UML-Diagramm
Zeitlicher Zusammenhalt: Anweisungen in einer Methode oder Methoden in einer Klasse sind nur deswegen dort zusammen enthalten, weil sie in einer aufeinanderfolgenden zeitlichen Reihenfolge ausgeführt werden. Nehmen wir zum Beispiel eine
123
Kapitel 3 Klasse Setup, die Methoden enthält, die zu Beginn einer Anwendung viele unterschiedliche Dinge tut, Laden von Konfigurationsdateien, Initialisieren von Objekten, Abfragen von Werten aus einer umgebenen HTML-Datei usw. Die einzelnen Methoden haben hier semantisch nichts miteinander zu tun, aber sie werden alle zu Beginn der Anwendung aufgerufen. Ein solcher Zusammenhalt widerspricht dem Prinzip der konzeptionellen Abstraktion, denn hier werden in einer Klasse viele unterschiedliche Konzepte untergebracht. Das kann die Flexibilität der Klasse verschlechtern. Besser könnte es sein, zumindest die konkreten Implementierungen der einzelnen Aufgaben in entsprechende eigene Klassen auszugliedern. Wenn z. B. im Setup eine Konfiguration für ein bestimmtes Objekt geladen werden soll, dann könnte es mehr Sinn machen, eine allgemeine Hilfsklasse für dieses Objekt zu schreiben, die die Konfiguration lädt und parst, anstatt diese Aufgabe in die Setup-Klasse zu stecken. So hätte man den Vorteil, dass man die Konfiguration zu jedem beliebigen Zeitpunkt laden kann.
Datenzusammenhalt: Eine Klasse, die Methoden in sich sammelt, die alle die gleichen Daten bearbeiten, hat einen starken Datenzusammenhalt. Nehmen wir als Beispiel das BitmapData-Objekt des Flash Players. Stellen wir uns vor, wir bauen eine Klasse, die viele Methoden bereithält, die mit Bitmap-Daten arbeiten. Eine Methode invertiert z. B. die Helligkeitswerte des Bitmaps. Eine andere Methode macht aus einem BitmapDataObjekt ein JPEG-komprimiertes Binär-Array. Die beiden Methoden arbeiten auf den gleichen Daten, aber sie haben sonst nicht viel gemeinsam. Einmal handelt es sich um die Anwendung von visuellen Effekten auf ein Bitmap, das andere Mal geht es um Konvertierung und Komprimierung, zwei recht unterschiedliche Aufgaben. Hier wäre es besser, mehrere einzelne Klassen zu schaffen, die konzeptionell eindeutigere Aufgaben übernehmen.
Parametrisierter Zusammenhalt: Auch als logischer Zusammenhalt bezeichnet. Damit meint man z. B. eine Klasse, die Methoden enthält, die nicht zwingend überhaupt irgendeinen Zusammenhang haben. Je nach Initialisierung der Klasse, z. B. durch einen bestimmten Parameter im Konstruktor, verhält sich die Klasse anders, führt z. B. andere Methoden aus. Stellen wir uns als Beispiel eine Menü-Klasse vor, die sich an mehrere Buttons als Listener für das Click-Event angemeldet hat. Je nachdem, welcher Buttons geklickt wird, führt sie unterschiedliche interne Methoden aus. Mal kreiert sie einen Dialog, mal lädt sie eine Datei, mal spielt sie eine Animation ab usw. Die Methoden haben keinerlei konzeptionellen Zusammenhalt, sie werden nur über den gemeinsamen Parameter, in diesem Fall ein Event, verknüpft. So ein Zusammenhalt sollte immer vermieden werden. Zwar ist es sinnvoll, eine Menü-Klasse als Mediator zu implementieren, dieser aber sollte die Anfragen an weitere Fachklassen weiterleiten, die sich dann jeweils kompetent darum kümmern.
Funktionaler Zusammenhalt: Oder auch konzeptioneller Zusammenhalt. Im Prinzip der einzig wünschenswerte Zusammenhalt. Anweisungen in einer Methode, Methoden in einer Klasse oder Klassen in einem Modul bilden zusammen eine funktionale bzw. konzeptionelle Einheit. Eine Klasse Video hat zum Beispiel Methoden zum Abspielen, Pausieren, Stoppen, Zurückspulen und besitzt Attribute wie Länge, aktuelle Position, Titel usw. Eine Methode erfüllt eine bestimmte fachliche Aufgabe, sie startet z. B. eine Animation, oder sie berechnet einen Wert. Eine Methode, die eine Animation abspielt
124
Lösungen entwerfen und einen bestimmten Wert berechnet, tut zu viel. Sie verliert an funktionalem Zusammenhalt. Durch die Kopplung der beiden Aufgaben kann man sie z. B. nicht mehr einzeln ausführen. Bei Änderungen steht die Methode eventuell im Wege, weil man vielleicht nun das Abspielen und das Berechnen in einer anderen Reihenfolge durchführen will. Es ist also wünschenswert, innerhalb unserer Methoden, Klassen und Module einen möglichst hohen funktionalen Zusammenhalt zu erzielen. Wenn wir die Prinzipien der Abstraktion richtig anwenden, erreichen wir dies auch meist ganz automatisch. Erinnern wir uns, bei der Abstraktion versuchen wir, ein Objekt zu bilden, das eine bestimmte konzeptionelle Idee verkörpert. Und zwar nur eine Idee, nicht mehrere. Im Sinne der Abstraktion würden wir nie von einer FilmGuckendenPerson sprechen, sondern von einer Person, einem Film und einem Videoplayer. Die Abstraktion hilft uns also auf Klassenebene, einen starken funktionalen Zusammenhalt zu gewährleisten und damit eine klare Verantwortlichkeit unserer Klassen zu bestimmen. Die Regel lautet also: Eine Methode, eine Klasse oder ein Modul soll immer nur eine Verantwortlichkeit haben, nicht mehrere. Um dieses Ziel zu erreichen, muss man die Verantwortlichkeit einer Klasse gut im Blick haben. Was ist das Kernelement der Klasse, die Sie gerade bauen? Wenn Sie die Antwort nicht wissen, ist die Gefahr groß, dass die Klasse zu einem Gemischtwarenladen wird. Deswegen ist es einfacher, zunächst in abstrakten Objekten zu denken und die konkreten Methoden erst später zu definieren. Entwickler, die schon konkrete Implementierungsideen im Kopf haben, werden tendenziell eher versuchen, Klassen um diese Implementierungen herum zu basteln, anstatt zunächst die abstrakten Objekttypen zu finden, aus denen die Anwendung aufgebaut werden soll. Das ist, als würde man für ein Haus schon die Position der Steckdosen festlegen, bevor überhaupt das Haus selbst entworfen wurde. Klassen, die um Methoden herum definiert werden, haben oft einen schwachen funktionalen Zusammenhalt, denn sie dienen mehr oder weniger nur als Container für die Methoden. Schauen wir uns hierzu wieder ein Beispiel an. Zunächst versetzen wir uns in einen Entwickler, der eine konkrete Implementierungsidee hat und diese nun in Methoden und Klassen umsetzen will. Nehmen wir als Szenario eine Animationssteuerung. Aus dem Design kam die Vorgabe, dass zunächst ein Video abgespielt werden soll, das an manchen Stellen anhält, worauf dann kurze textliche Einblendungen erscheinen, die auf Klick wieder verschwinden, sodass das Video dann wieder weiterspielt. Da das Video natürlich erst einmal geladen werden muss, soll diese Zeit mit einer kleinen Preloader-Animation überbrückt werden. Abbildung 3.24 zeigt das Konzept als kleines Flowchart. Wir fangen also an und beginnen, über die Implementierung entlang des Flowcharts nachzudenken. Wir müssen also zunächst das Video vorladen und währenddessen eine Animation anzeigen. Also gut, wir bauen eine Klasse, die nennen wir PreloadVideo, die soll das steuern. In ihr laden wir das Video, beobachten den Ladeprozess und spielen währenddessen eine Animation ab. Die Klasse dazu könnte grob so aussehen, wie Listing 3.18 zeigt.
125
Kapitel 3
PreloadVideo
LoadVideo PlayFirstChapter
Start ShowAnimation
VideoLoaded
PauseVideo
End
UserClicksContinue
n iterations
ShowText 1..n
PlayNextChapter
Abbildung 3.24: Aktivitätsdiagramm für das Animationsbeispiel import flash.events.Event; import flash.media.Video; public class PreloadVideo { private var video:Video; private var videoManager:VideoManager public function PreloadVideo(i_videoManager:VideoManager) { videoManager = i_videoManager; startPreloading(); } private function startPreloading():void { // ... Startet das Laden des Videos }
126
Lösungen entwerfen private function buildAnimation():void { // ... Bereitet die Preloading Animation vor } private function onVideoLoadProgress(e:Event):void { updatePreloadingAnimation(); } private function updatePreloadingAnimation():void { // ... Aktualisiert Prozentzahl in Preloading Animation } private function onVideoLoadComplete(e:Event):void { videoManager.playChapter(0); } } Listing 3.18: PreloadVideo-Klasse, schwacher Zusammenhalt
In dieser Klasse läuft einiges schief. Fragen wir uns zunächst einmal, welches die konkrete Abstraktion ist, was ist die eine Verantwortlichkeit, die eine Klasse haben sollte? Wir erkennen auf Anhieb zwei Verantwortlichkeiten: das Video laden und den Preloader darstellen und aktualisieren. Wir sehen also, dass manche der Methoden die Darstellung des Preloaders übernehmen und manche mit der Ladesteuerung beschäftigt sind. Zudem hat die PreloadVideo-Klasse auch noch Kenntnis von einem VideoManager, den sie aufruft, wenn das Video geladen ist. Eine Klasse sollte aber wie gesagt nur eine Verantwortlichkeit haben, denn nur so können die jeweiligen Verantwortlichkeiten unabhängig voneinander implementiert und damit später auch unabhängig voneinander gewartet oder geändert werden. Nun könnte man einwerfen, irgendeine Klasse muss ja die Steuerung des Prozesses übernehmen, bei dem das Video geladen und währenddessen der Preloader gezeigt wird. Das stimmt, das Steuern des Preloadings ist eine dritte Abstraktion, die sich in unserer PreloadVideo-Klasse wiederfindet. Aber die Steuerung eines Ladevorgangs sollte idealerweise das Laden selbst entkoppeln und auf keinen Fall auch noch die Darstellung des Preloaders implementieren und Kenntnis vom weiteren Ablauf der Anwendung haben. Wenn wir auf die Abstraktionen schauen, dann haben wir hier also drei Stück, die sich direkt oder indirekt mit Vorladen eines Videos beschäftigen: 1. Ladevorgang steuern 2. Video laden 3. Preloader darstellen Die Übergabe an den VideoManager sehen wir nicht als Aufgabe unseres Preloaders an und schieben die Verantwortung erst mal von uns. Natürlich nicht endgültig, wir nehmen uns des Themas später noch an. Es wäre nun ideal, wenn wir aus den drei Verantwortlichkeiten
127
Kapitel 3 drei Klassen machen. Diese haben dann entsprechend für sich nur noch eine Verantwortlichkeit, und es wird uns deutlich leichter fallen, innerhalb dieser Klassen einen starken Zusammenhalt zu gewährleisten. Viele Entwickler stöhnen bei dem Gedanken, für jede kleine Aufgabe eine eigene Klasse zu erstellen, und sie haben damit auch nicht ganz unrecht. Man kann dieses Prinzip der Trennung der Verantwortlichkeiten und damit der Erstellung von lauter Einzelklassen auch übertreiben. Hier muss der gesunde Menschenverstand entscheiden, wo die Grenze zu ziehen ist. Das macht letztlich der Softwareentwurf so herausfordernd, denn es gibt keine absoluten Regeln, die vorschreiben, wie eine Anwendung zu strukturieren ist, sondern nur Herangehensweisen. In unserem Fall, also dem Laden von Dateien unter Verwendung von visuellen Preloader-Animationen würde ich argumentieren, dass dies ein so prominenter Fall ist, der so oft in so vielen Varianten in Flash-Anwendungen benötigt wird, dass es sich in jedem Fall lohnt, ein wenig mehr Sorgfalt walten zu lassen, denn umso eher können wir hier Code und Konzept der Ladesteuerung wiederverwenden. Wir haben auch noch Glück, für die Abstraktion VideoLaden gibt es in der Flash Player API schon eine Klasse, den NetStream. Wir müssen also nur noch eine Klasse für die allgemeine Steuerung und eine für die Preloader-Animation schreiben. Da uns für dieses Beispiel nicht so sehr interessiert, wie genau nun die Preloader-Animation gestaltet ist, abstrahiere ich für diese Klasse zunächst ein Interface, das nur eine einzige Methode definiert, nämlich die Methode setPogressInPercent(percent:Number). Wir gehen davon aus, dass wir letztlich eine Klasse, die z. B. von MovieClip erbt, schreiben könnten, die diese Methode implementiert und so den Verlauf der Animation steuert. Eine einfache Möglichkeit wäre hier z. B. ein in der Flash IDE erstelltes MovieClip-Bibliothekssymbol mit einer Timeline-Animation, die z. B. 100 Frames hat (also für jeden Prozentpunkt einen Frame). Diesem Symbol weisen wir dann unsere Klasse zu, die das Interface implementiert. So haben wir dann eine saubere Trennung zwischen unserer Klassenstruktur und der konkret gestalteten Animation geschaffen. Ich habe in diesem Zusammenhang übrigens noch nicht über Muster wie Model-View-Controller gesprochen (was ich aber im nächsten Kapitel noch tun werde), und doch hat sich durch die Aufsplittung in die einzelnen Verantwortlichkeiten letztlich fast von selbst eine derartige Aufteilung ergeben. Die Preloading-Steuerung können wir als Controller bezeichnen, den Loader als Model, und die Preloader-Animation ist der View. Listing 3.19 und Listing 3.20 zeigen die beiden neu erstellten Klassen VideoLoadingController und das Interface PreloadAnimation. import import import import import
flash.events.Event; flash.events.EventDispatcher; flash.events.NetStatusEvent; flash.net.NetConnection; flash.net.NetStream;
public class VideoLoadingController extends EventDispatcher { private var loader:NetStream; private var preloadAnimation:PreloadAnimation;
128
Lösungen entwerfen public function VideoLoadingController(animation:PreloadAnimation) { preloadAnimation = animation; var netConnection:NetConnection = new NetConnection(); netConnection.connect(null); loader = new NetStream(netConnection); loader.addEventListener( NetStatusEvent.NET_STATUS, onVideoLoadProgress ); // ... } public function get videoStream():NetStream { return loader; } public function loadVideo(videoUrl:String):void { // ... } private function onVideoLoadProgress(e:NetStatusEvent):void { if (e.info.code == "NetStream.Buffer.Full") { // ... Benachrichtigung nach aussen, dass das Video fertig // geladen ist, zum Beispiel per Event. }else { var percentBuffered:Number; // ... Logik, die den Füllstand des Buffers in Prozent ermittelt preloadAnimation.setPogressInPercent(percentBuffered); } } // ... } Listing 3.19: Der VideoLoadingController public interface PreloadAnimation { public function setPogressInPercent(percent:Number):void; } Listing 3.20: Das Interface PreloadAnimation
129
Kapitel 3 Der VideoLoadingController hat nun nur noch eine Verantwortlichkeit, er steuert den Ladevorgang des Videos. Um den Fortschritt an den Nutzer zu kommunizieren, übergibt man ihm eine Instanz einer PreloadAnimation. Wie gesagt, in der Praxis wird dies höchstwahrscheinlich eine Klasse sein, die von MovieClip erbt und das Interface PreloadAnimation implementiert. Die Schnittstelle zwischen dem Controller und der Animation ist hier denkbar einfach. Man übergibt einfach einen Prozentwert. Es muss hier den Controller nicht interessieren, was die Animation daraus macht, und es muss wiederum die Animation nicht interessieren, woher der Controller die Zahl hat. Beide wissen voneinander nur das absolut Nötigste. Und was den Fall angeht, wenn das Video fertig geladen bzw. der Buffer ausreichend gefüllt ist, da endet hier die Verantwortung des Controllers. Deswegen wird er einfach ein Event werfen, das angibt, dass das Video nun bereitsteht, abgespielt zu werden. Sonderfälle, wie dass das Video nicht geladen werden kann oder dergleichen, habe ich hier aus Übersichtlichkeitsgründen weggelassen. Darüber hinaus, muss ich natürlich zugeben, habe ich mir hier sowieso das Leben recht einfach gemacht. Viele Fragen bleiben offen, wie z. B. wer denn die Animation erzeugt und platziert, wer denn auf das Event »Video ist geladen« hört usw. Man hat fast das Gefühl, ich hätte einfach nur Verantwortung von mir geschoben. Das stimmt auch, und es ist o. k. so. Bei der Strukturierung einer Anwendung wird man immer wieder Verantwortlichkeiten von einer Klasse zur anderen schieben oder neue Klassen erstellen, denen man Verantwortlichkeiten unterjubelt. So lange, bis die Kompetenzen klar verteilt sind und man eine modulare und lose gekoppelte Anwendung vor sich hat. Wir würden also nun im weiteren Verlauf eine Klasse suchen, der wir die Aufgabe zur Erzeugung der Preloader-Animation zukommen lassen könnten. Eventuell ist dies eine bestehende Klasse, die sowieso schon die allgemeine Darstellung steuert, eventuell aber müssen wir so eine erst bauen. Eventuell gibt es schon einen übergreifenden Controller, der das »Video ist geladen«-Event annehmen und darauf reagieren kann, anderenfalls schreiben wir halt einen. Dieser Prozess der Verteilung von Verantwortlichkeiten und der Bildung von Abstraktionen kann eine Weile dauern. Eventuell ist man mit der ersten Version auch nicht zufrieden und will noch mal Zuordnungen ändern. Deswegen ist es unter Umständen einfacher, die Bildung der grundsätzlichen Klassenstruktur mit ihren öffentlichen Methoden erst einmal nur als Diagramm zu machen, um dort beliebig Klassen hinzuzufügen, Methoden hin und her zu schieben und Abhängigkeiten ausprobieren zu können, ohne dass man gleich richtigen Code schreiben muss.
3.12 Konzept vs. Infrastruktur In den vorangegangenen Abschnitten haben wir uns mit diversen Instrumenten zur Strukturierung von objektorientierten Systemen beschäftigt – Systemen deswegen, weil diese Instrumente nicht nur für den objektorientierten Entwurf, sondern auch für die objektorientierte Analyse und die objektorientierte Programmierung nützlich sind. Dabei habe ich eines immer wieder herausgestellt, dass man bei dem Entwurf einer Anwendung immer vom Konzept ausgehen und die Klassen, Objekte und ihre Schnittstellen danach ausrichten soll.
130
Lösungen entwerfen In der Tat lässt sich die Objektorientierung sehr anschaulich an den fachlichen Elementen einer Anwendung zeigen. Der Klassiker ist ein Shop, bei dem es Klassen wie Artikel, Kunde, Kategorie, Auftrag, Rabattsatz usw. gibt. Diese Klassen bilden die fachliche Grundlage für eine Shop-Anwendung, man spricht auch vom Domain Model, also dem fachlichen Modell. Diese Beispiele sind auch gut und richtig, denn letztendlich wurde die Objektorientierung unter anderem aus dem Wunsch heraus geboren, fachliche Zusammenhänge auch direkt im Code abbilden zu können. Nun wissen wir aber auch, dass eine Anwendung nun mal nicht nur aus diesen fachlichen Objekten besteht, sondern auch aus einer ganzen Reihe anderer Objekte, die nicht fachlicher, sondern technischer Natur sind: Tweening-Klassen, Bitmap-Objekte, Buttons, Scroller, XML-Parser, Events, Controller und viele andere. Diese Klassen bilden die Infrastruktur der Anwendung, sie haben also nicht unbedingt direkt etwas mit den fachlichen Klassen zu tun, aber sie stützen sie. Ohne die Infrastruktur würden die fachlichen Klassen im luftleeren Raum herumschwirren und hätten keinen Nutzen. In diesem Zusammenhang wird auch oft vom Domänenmodell auf der einen Seite gesprochen, das eben die fachlichen Objekte enthält, und der Applikationslogik auf der anderen Seite, die um das Domänenmodell herum eine konkrete Anwendung für einen bestimmten Zweck konstruiert. Beide Seiten aber verlangen nach den gleichen Prinzipien der Objektorientierung und müssen gleichermaßen sorgsam entwickelt werden. Mit der steigenden Komplexität der FlashPlattform, mit der Verbreitung von Flex und Architektur-Frameworks wie Cairngorm und dergleichen ist es heute zwar möglich, sich als Entwickler mehr auf den fachlichen Teil zu konzentrieren und für den Infrastrukturbereich eben solche Frameworks wie Flex mit Cairngorm oder pureMVC heranzuziehen. Das entbindet einen guten Entwickler aber nicht vor der Aufgabe, sich über Infrastruktur Gedanken zu machen und sie aktiv in die Planung einer Anwendung mit einzubeziehen. Wer sich blind auf ein Framework verlässt, wird Probleme bekommen, wenn das Framework auf ein Problem mal keine Antwort hat oder es eventuell zu strukturellen oder Performance-Problemen kommt. Der Einsatz eines konkreten Infrastruktur-Frameworks bedingt einerseits, dass man zuerst eine Vorstellung davon entwickelt, wie man denn die Infrastruktur der Anwendung selber aufbauen würde. Zweitens muss man die Eigenschaften des infrage kommenden Frameworks gut kennen, und zwar nicht nur seine Stärken und Features, sondern auch seine Schwächen. Erst dann ist man in der Lage, den Kompromiss einschätzen zu können, den man eingeht, wenn man statt einer Eigenentwicklung ein vorhandenes Framework einsetzt. Und ein Kompromiss wird es immer sein, denn ein Framework ist niemals genau so strukturiert und bietet exakt die Funktionalität, die man idealerweise benötigt. Derjenige, der sich trotz Frameworks selbst auch noch mit Infrastrukturen beschäftigt und manche Anwendung auch mal komplett selbst aufbaut, wenn sich der Einsatz eines Frameworks nicht lohnt, ist klar im Vorteil, denn er ist überhaupt erst in der Lage, beurteilen zu können, wo die Stärken und Schwächen anderer Lösungen stecken.
131
Kapitel 3
3.13 Literaturangaben Bass, Len; Clements, Paul; Kazman, Rick: Software architecture in practice. 2. Edition, 12th printing. Boston Mass.: Addison-Wesley, 2008. Dörner, Dietrich: Die Logik des Misslingens. Strategisches Denken in komplexen Situationen. Erw. Neuausg., 7. Aufl. Reinbek bei Hamburg: Rowohlt-Taschenbuch-Verl., 2008. Booch, Grady: Object oriented analysis and design. With applications. 2. Edition, 2. [Dr.]. Redwood City Calif.: Benjamin/Cummings, 1994. ECOOP 2003 – Object-Oriented Programming. 17th European Conference, Darmstadt, Germany, July 21–25, 2003. Proceedings: Springer-Verlag GmbH (2003). Leigh Power.: Addendum to the proceedings OOPSLA '87, 23,5. New York, N.Y., 1988. McConnell, Steve: Code complete. Dt. Ausg. der 2. Edition, [Nachdr.]. Unterschleißheim: Microsoft Press, 2007.
132
4
Entwurfswerkzeuge
Flash-Entwickler verwenden eine Mehrzahl an Werkzeugen, um Anwendungen zu bauen. Da haben wir zunächst die ganz offensichtlichen Werkzeuge, wie z. B. Code-Editor-Programme, IDEs wie der Flex Builder oder die Flash IDE und viele andere. Diese Werkzeuge helfen uns konkret, Code zu schreiben oder generell die Anwendung zu bauen. Wenn wir uns nur mit dem Entwurf unserer Anwendung beschäftigen, dann verwenden wir andere Tools, z. B. Diagramm-Editoren, mit denen wir die Struktur unserer Anwendung skizzieren können, bevor wir anfangen, Code zu schreiben. Innerhalb des Prozesses der Strukturierung einer Anwendung verwenden wir aber auch Werkzeuge, die nicht so offensichtlich sind. Ich möchte hier deswegen auch keine konkreten Tools für die Erstellung von Code oder Diagrammen vorstellen, vielmehr möchte ich die hinter den Tools stehenden Werkzeuge und Prinzipien zeigen, die uns helfen, Anwendungen zu planen und zu strukturieren. Wie immer gilt auch hier, es gibt eine große Vielzahl an Hilfsmitteln, die uns bei der Erstellung einer Anwendung unterstützen können. Das heißt aber nicht, dass man sie auch alle immer einsetzen muss. Es ist manchmal sogar eher nützlich, die grundlegenden Werkzeuge zu kennen, damit man weiß, welche man für ein bestimmtes Projekt gerade nicht braucht. Ein Mechaniker benutzt auch nicht täglich jedes Werkzeug in seinem Koffer, aber er kennt zumindest den Nutzen aller. So sollte es bei einem Flash-Entwickler auch sein.
Kapitel 4
4.1 UML Die UML, die Unified Modelling Language, ist das nützlichste Werkzeug zum Entwerfen und Skizzieren von Strukturen einer Anwendung. Mancher Entwickler empfindet UML aber als zu sperrig und formell. Es mag daran liegen, dass der UML eine gewisse Formalität zugrunde liegt, die stark nach großen technischen Konzepten riecht und schwerfälligen, nicht änderbaren Strukturen. Viele Entwickler denken sich: »Warum soll ich ein großes Diagramm malen, das nach kurzer Zeit sowieso veraltet ist, weil ich meine Anwendung dann doch ganz anders umsetze?!« Hierzu empfehle ich einen Artikel von der Website von Martin Fowler (Martin Fowler 2000). In dem Artikel geht es vordergründig um die Frage, wie viel Energie man in den Entwurf einer Anwendung (auch unter Verwendung von UML) stecken sollte. Fowler sagt hier (frei übersetzt): »Ein allgemeines Problem bei der Verwendung von Diagrammen ist, dass Entwickler versuchen, alles an Information in ihr Diagramm zu stecken. Aber nur der Code selbst ist die beste allumfassende Informationsquelle, denn nur Code ist immer synchron zum Code. Was Diagramme betrifft, führt der Versuch, alles darzustellen, immer zu einer schlechteren Verständlichkeit.« UML ist ein Werkzeug, um Strukturen zu entwerfen und zu skizzieren. Dabei ist es wichtig, dass man UML nicht einsetzt, weil man es muss, sondern weil man für eine bestimmte Fragestellung eine Skizze anfertigen möchte, bevor man eventuell viel Zeit in die Erstellung von konkretem Code investiert. Ein UML-Diagramm zu erstellen muss nicht bedeuten, dass man riesige Konstrukte erstellt. Schauen Sie sich die UML-Diagramme in diesem Buch an. Sie sind meistens recht klein und wurden in relativ kurzer Zeit erstellt. Sie zeigen nie eine komplette Anwendung, meistens nicht einmal ein komplettes Modul. Ein UML-Diagramm soll helfen, einen Überblick über das Zusammenspiel von Klassen oder zeitlichen Abläufen oder dergleichen zu erhalten. Dazu muss man nicht die ganze Anwendung in das Diagramm packen, im Gegenteil. Die hilfreichsten UML-Diagramme sind oft die, die nur einen ganz bestimmten Aspekt beleuchten und alles andere weglassen. In einem Klassendiagramm muss man nicht jede Methode einer Klasse definieren, man muss auch nicht jede Klasse definieren. Je nachdem, was Sie veranschaulichen wollen, stellen Sie auch nur die involvierten Elemente dar. Und wenn Sie nur schnell eine Idee festhalten wollen, zeichnen Sie das Diagramm mit dem Stift auf Papier und nicht mit dem Computer. Das geht in den meisten Fällen viel schneller. UML-Diagramme zu verwenden bedeutet auch nicht, dass man die komplette Anwendung zu Beginn eines Projekts entwerfen muss. Eventuell wollen Sie anfangs einfach nur eine grobe Struktur anlegen, die Sie noch gar nicht detailliert ausdefinieren. Vielleicht kommen Sie mitten in einem Projekt an einen Punkt, wo Sie merken, dass Ihr Code unübersichtlich geworden ist und Sie ihn umstellen wollen. Dann kann es vielleicht hilfreich sein, mittels eines kleinen Diagramms zunächst zu skizzieren, wie die Umstellung aussehen könnte. Mit diesem Diagramm können Sie dann herumspielen, bis Sie mit der Umstellung zufrieden sind. Das verbraucht sicherlich weniger Zeit, als die Umstellung direkt im Code auszuprobieren. Ein UML-Diagramm muss auch nicht mit dem Projekt mitwachsen. Es grenzt an Sisyphusarbeit, wenn Sie versuchen würden, ein UML-Diagramm zu warten, das immer den kom-
134
Entwurfswerkzeuge pletten Stand Ihrer Anwendung widerspiegelt. Wenn man ein Diagramm für einen bestimmten Zweck, die Entwicklung einer Teilstruktur in der Anwendung z.B., verwendet hat, spricht nichts dagegen, es wegzuwerfen, sobald die Teilstruktur fertig ist (vielleicht wollen Sie es archivieren, um später einmal Ihre Gedankengänge nachvollziehen zu können). Auch muss man nicht versuchen, sich sklavisch an einen Entwurf, den man in UML gemacht, zu halten, wenn sich herausstellt, dass es einen besseren Weg gibt. Ein UML-Diagramm hat, genau wie eine ActionScript-Klasse, ein Datum, das zeigt, wann sie erstellt wurde. Es zeigt den Stand des Designs zu diesem Datum. Es ist selbstverständlich, dass sich sowohl der Code mit der Zeit verändert und demzufolge auch ein UML-Diagramm irgendwann obsolet werden kann. Das ist aber nicht schlimm, denn zu dem Zeitpunkt, wo man das Diagramm gezeichnet hat, hat es einem ja geholfen.
Abbildung 4.1: Riesige UML-Diagramme liest keiner.
UML muss in diesem Zusammenhang einfach als das angesehen werden, was es ist: ein Hilfsmittel zum Ermitteln einer guten Struktur für eine Anwendung. Lassen Sie sich von der Mächtigkeit von UML nicht abschrecken, simple Diagramme zu zeichnen. Für die meisten Fragestellungen innerhalb des Softwareentwurfs reichen Klassen- oder Objektdiagramme aus. In speziellen Fällen können Aktivitätsdiagramme oder sogar Sequenzdiagramme nützlich sein. UML-Diagramme kann man grob in strukturelle Diagrammtypen und Verhalten aufzeigende Diagrammtypen unterteilen. Die ersten beiden Diagrammtypen, die ich vorstellen möchte, sind strukturelle. Das sind das Komponenten- und das Klassendiagramm. Sie stellen die Struktur einer Anwendung auf unterschiedlichen Detaillevels dar. Die anderen drei, die ich zeigen möchte, sind das Verhalten aufzeigende Diagramme. Das sind das Aktivitäts-,
135
Kapitel 4 das Kommunikations- und das Sequenzdiagramm. Es gibt natürlich noch weitere Diagrammtypen. Auch erhebe ich hier nicht den Anspruch, die UML-Diagramme mit ihren Möglichkeiten erschöpfend zu behandeln. Vielmehr möchte ich für die wichtigsten Diagrammtypen einen Überblick geben und erläutern, wofür sie nützlich sind. Eine gute, tiefer gehende Erläuterung der UML liefert z. B. Bernd Oesterreich(Oestereich 2005).
4.1.1 Komponentendiagramm Ein Komponentendiagramm ermöglicht die Darstellung der Struktur einer Anwendung auf einem recht hohen Abstraktionslevel. Komponentendiagramme gehen davon aus, dass eine Anwendung aus Komponenten oder Modulen besteht. Mit Komponente oder Modul ist hier ganz allgemein eine Menge an Klassen gemeint, die zusammen eine gemeinsame Aufgabe oder Verantwortlichkeit haben. Bei einem Komponentendiagramm interessieren uns vorrangig diese Komponente und ihre Beziehungen zueinander. Man geht hier also noch nicht direkt bis auf einzelne Klassen (obwohl es auch möglich ist, innerhalb eines Komponentendiagramms einzelne wichtige Klassen darzustellen).
Storyline Statistics
Game
GameStory
Environment
Player
GameWorld Interaction
PlayerImpact «delegate» Forces
Physics
Abbildung 4.2: Beispiel Komponentendiagramm
Abbildung 4.2 zeigt ein Beispiel für ein Komponentendiagramm. Hier wird eine Spielumgebung auf einem hohen Abstraktionslevel dargestellt. Wir haben fünf Hauptkomponenten, wobei wir bei der Komponente GameWorld explizit zeigen, dass sie intern noch aus einer Komponente Physics besteht (und vielleicht noch aus anderen, aber das zeigen wir hier nicht). Außerdem sind die wichtigsten Beziehungen zwischen den Komponenten dargestellt. So hat Game eine einfache Beziehung zur Statistics-Komponente, um dort Highscores und dergleichen abzulegen. Game hat zudem eine Beziehung zum Player. Damit
136
Entwurfswerkzeuge Game richtig funktionieren kann, benötigt es zwei bestimmte Funktionalitäten, nämlich einmal eine Spielewelt und eine Geschichte, in der sich das Ganze abspielt. Game fordert also zwei Schnittstellen an, was durch den Halbkreis symbolisiert wird. GameStory und Gameworld bieten die gesuchten Schnittstellen an, was durch den geschlossenen und gefüllten Kreis symbolisiert wird. Dass der geschlossene Kreis hier direkt im Halbkreis sitzt, soll zeigen, dass beide Komponenten zur Laufzeit miteinander verbunden werden. Dies ist bei Player und GameWorld wahrscheinlich auch der Fall, aber hier war es mir wichtiger zu zeigen, wie die Schnittstelle aufgebaut ist. Hier wird also ganz konkret angegeben, dass es auf Player-Seite ein Interface mit dem Namen Interaction gibt und dass es auf Seiten von GameWorld eine Klasse PlayerImpact gibt, die das Interface implementiert, was durch den Abhängigkeitspfeil zwischen PlayerImpact und Interaction angedeutet wird. Gleichfalls interessant ist, dass die Klasse PlayerImpact die Aufgabe intern weiterreicht an die interne Komponente Physics, was durch den mit delegate gekennzeichneten Pfeil dargestellt wird. Wann hier nun welche Verbindungsart gewählt wird, bleibt vollkommen Ihnen überlassen. Sie müssen sich einfach fragen, welche Fragen Sie mit dem Diagramm beantworten wollen. Zum Beispiel hätte ich die Schnittstellenverbindung zwischen Game und GameStory auch durch eine simple Assoziationslinie darstellen können, wie ich es bei Game und Statistics getan habe. Dann wäre zwar nicht klar geworden, dass hier ein Interface im Spiel ist, aber eventuell wäre das für meine Fragestellung ja auch nicht wichtig gewesen. Komponentendiagramme können gerade zu Projektbeginn interessant sein, wenn man sich grundsätzlich eine grobe Vorstellung von der zu entwickelnden Anwendung und ihrer Bestandteile machen will. Wichtig ist, dass der Begriff Komponente sehr dehnbar ist. Keinesfalls sind damit nur visuelle Komponenten gemeint. Auch eine Serveranwendung könnte eine Komponente sein. Wenn Sie eine Flash-Anwendung bauen, die mit JavaScriptFunktionen im umliegenden HTML kommuniziert, könnten auch diese JavaScript-Funktionen eine Komponente sein. Als Abgrenzung wären allerdings physikalische Strukturen zu nennen. Als Komponenten werden immer Softwareteile bezeichnet, nicht physikalische Teile wie ein Server, eine Datenbank oder ein PC. Für physikalische Strukturen gibt es auch UML-Diagramme, z. B. das Deployment-Diagramm, das werde ich aber nicht weiter behandeln.
4.1.2 Klassendiagramm Der sicherlich bekannteste Diagrammtyp in der UML ist das Klassendiagramm. In den UML-Dokumentationen wird auch immer vom statischen Klassendiagramm gesprochen. Denn ein Klassendiagramm beschreibt – wie der Name schon andeutet – Klassen und nicht deren Instanzen. Würde man die Instanzen darstellen wollen, müsste man streng genommen ein Objektdiagramm verwenden. In der Praxis wird dies allerdings meistens vermischt, und man verwendet statische Klassendiagramme, die die grundsätzlichen Beziehungen zwischen den Klassen aufzeigen, die natürlich letztlich eigentlich erst zur Laufzeit aufgelöst werden.
137
Kapitel 4 Wer ein Klassendiagramm zeichnet, ist meist schon recht tief drin in der Modellierung seiner Anwendungsstruktur. Klassendiagramme erlauben einen sehr detailreichen Einblick in eine Anwendung, der bis zu den einzelnen Attributen und Methoden einer Klasse reichen kann. Klassendiagramme müssen aber nicht zwingend so detailliert ausgearbeitet werden. Wenn Sie gerne einfach einmal ein wenig unterschiedliche mögliche Strukturen ausprobieren wollen, dann werden Sie wohl kaum schon jede Methode einer Klasse beschreiben, sondern eher einfach nur die Klassen an sich aufzeichnen und noch ein paar Beziehungen zwischen ihnen. Steigen wir einfach mal kopfüber ein und sehen uns zwei recht unterschiedliche Klassendiagramme an, die beide ihre Daseinsberechtigung haben. Zunächst ein einfaches Diagramm, wie Abbildung 4.3 zeigt.
Sprite
CloseButton
Layer «event»
+
onClose(MouseEvent) : void
«event» MouseEvent
Abbildung 4.3: Ein einfaches Klassendiagramm, wenig Detailtiefe
In diesem einfachen Beispiel zeige ich vier Klassen und nur für eine Klasse eine Methode. Der eigentliche Sinn dieses Diagramms ist, die Beziehung zwischen dem CloseButton und dem Layer zu zeigen, alles andere soll uns nicht interessieren. CloseButton und Layer erben von Sprite, was man an dem gefüllten Pfeil erkennen kann. Außerdem führt ein Pfeil von Layer nach CloseButton, und an dem Pfeil hängt wiederum MouseEvent. Das sieht erst einmal falsch aus, denn das Event wird ja sicher nicht vom Layer zum CloseButton gereicht. Dieser Pfeil beschreibt aber nicht den Datenfluss, sondern eine Abhängigkeit. Der Layer hat eine Abhängigkeit zum CloseButton, denn er meldet sich aktiv am CloseButton als Listener an. Und die Klasse MouseEvent ist sozusagen der Datenträger für diese Abhängigkeit, denn wenn CloseButton das Event werfen wird, dann wird es eine Instanz dieser MouseEvent-Klasse mitliefern.
138
Entwurfswerkzeuge Natürlich ist dieses Diagramm nicht vollständig. Aber das ist auch gar nicht notwendig, ja, es ist gar nicht gewünscht. Durch das Weglassen von anderen Details in diesem Diagramm können wir das Hauptaugenmerk auf das legen, was wir erläutern wollen – in diesem Fall die Beziehung zwischen CloseButton und Layer –, und somit die Verständlichkeit des Diagramms erhöhen. Schauen wir uns nun das zweite Beispiel an, in Abbildung 4.4. Ich möchte es als Negativbeispiel verwenden. Dieses Diagramm wurde nicht entlang einer Kernaussage entworfen. In der Tat habe ich einfach einen Parser über ein paar Klassen laufen lassen, der mir dieses Diagramm erzeugt hat. Lapidar könnte ich sagen, das Diagramm soll dokumentarischen Charakter haben. Zu diesem Zweck sind hier für die dargestellten Klassen auch nur alle öffentlichen Methoden beschrieben, erkennbar an dem '+'-Zeichen vor dem Methodennamen. Zunächst aber vielleicht kurz eine Erläuterung, um was es hier eigentlich geht. Wir haben hier eine Process- bzw. eine DialogProcess-Klasse, die die Abarbeitung von Commands steuert. Process arbeitet Commands sequenziell ab, deswegen kann man ihm auch einfach per pushCommand-Methode beliebig viele Commands mitgeben. DialogProcess unterstützt eine baumartige Struktur, die durch Steps und Followers gebildet wird. Ein Step steht für einen Command, der ausgeführt wird. Dieser Command beendet sich nun aber nicht einfach durch ein »Fertig«-Signal, sondern mit einem bestimmten Code. Nun kann man pro Step beliebig viele Follower angeben, die für so einen bestimmten Code stehen. Je nachdem, mit welchem Code sich der Command im Step beendet hat, wird einer der Follower angesprochen. Ein Follower definiert nun wieder einen folgenden Step, und das Spiel geht von vorne los. So lassen sich verschachtelte Command-Strukturen aufbauen. Eine Klasse wird in der UML mit einem Kästchen und bis zu zwei Unterteilungen dargestellt. Im obersten Teil steht der Klassen- oder Interface-Name, im zweiten Teil können Attribute stehen und im dritten Teil die Methoden. Da ich hier keine Attribute direkt öffentlich gemacht habe, ist der zweite Teil immer frei. Manche Methodennamen sind kursiv gestellt. Dies ist immer dann der Fall, wenn die Methode abstrakt ist. Bei Interfaces leuchtet das ein, denn dort sind Methoden nie implementiert. Man sieht es aber manchmal auch bei Klassen. Flash selbst erlaubt ja keine abstrakten Klassen oder Methoden. Wenn man also im Zusammenhang mit Flash eine Methode kursiv gestellt sieht, dann soll das andeuten, dass diese Methode einen leeren Rumpf hat und überschrieben werden soll. Im Beispiel der Klasse DialogCommand habe ich bewusst nichts hineingeschrieben, weil DialogCommand seiner beerbten Klasse Command nichts hinzufügt, sondern nur Methoden überschreibt. Auch in diesem Diagramm sind wieder Vererbungen sichtbar und diesmal auch die Implementierung von Interfaces, und zwar anhand des gestrichelten Pfeils mit gefüllter Pfeilspitze. Der gestrichelte Pfeil mit offener Pfeilspitze symbolisiert hingegen eine einfache Abhängigkeit. Wir sehen z.B., dass ICommand über seine Methode execute() eine Instanz vom Typ ParamContainer entgegennimmt. Das ist eine recht simple Abhängigkeit, und sie wird deswegen mit besagtem Pfeil gekennzeichnet.
139
Kapitel 4
«interface» ICommand + + + +
addEventListener(String, Function, Boolean, int, Boolean) : void removeEventListener(String, Function, Boolean) : void execute(ParamContainer) : void cancel() : void 1
EventDispatcher Command
+ +
cancel() : void execute(ParamContainer) : void
«interface» ParamContainer + +
DialogCommand +
setParam(String, Object) : void getParam(String) : Object
EventDispatcher
execute(ParamContainer) : void
DialogProcess
Process
+ + + + + +
+ +
clear() : void getParam(String) : Object pushCommand(ICommand) : void setParam(String, Object) : void start() : void stop() : void
pushGeneralFollower(Follower) : void pushStep(Step) : void
«property get» + currentCommand() : ICommand 1
1
0..*
1..*
Follower
+
Follower(String, Step)
«property get» + code() : String + step() : Step
1
Step
0..*
1..*
+ +
pushFollower(Follower) : void Step(ICommand, Array)
«property get» + command() : ICommand + followers() : Array
Abbildung 4.4: Komplexeres Klassendiagramm
Enger ist da schon die Beziehung zwischen Process und ICommand. Sie wird durch einen Pfeil mit durchgehender Linie gekennzeichnet. Der Unterschied zwischen gestrichelter und durchgehender Linie liegt meist darin, dass bei einer durchgehenden Linie die Klassen direkt Klassenattribute in sich tragen, die vom Typ der anderen Klasse sind. Die Klassenattribute können wir hier nicht sehen, deswegen soll der Pfeil mit der durchgehenden Linie klarmachen, dass Process eine Referenz auf ICommand in sich trägt. Diese Beziehung sehen wir als stärker an als zwischen ICommand und ParamContainer, bei der ParamContainer nur temporär als Parameter in einer Methode verwendet wird.
140
Entwurfswerkzeuge
Klassenbeziehungen in der UML In der UML gibt es in Klassendiagrammen unterschiedliche Arten von Beziehungstypen, deren Bedeutung nicht immer sofort ersichtlich ist. Deswegen möchte ich hier etwas detaillierter auf diese Typen eingehen. Diese Beziehungstypen sind deswegen so wichtig, weil sie einen entscheidenden Teil zur Bedeutung und Aussagekraft eines Klassendiagramms beitragen. In den meisten Fällen sind es ja die Beziehungen zwischen Klassen bzw. Objekten, die uns in einem Diagramm interessieren. Folgende Beziehungstypen kennt die UML: Abhängigkeit ClassA
ClassB
Abbildung 4.5: Ein Abhängigkeitspfeil Eine Abhängigkeit ist die schwächste Form der Beziehung zwischen zwei Klassen, die die UML kennt. Man verwendet sie normalerweise immer dann, wenn eine Klasse A eine Klasse B im Rahmen einer Funktion verwendet, aber nur temporär, z. B. weil einer der Parameter der Funktion eine Instanz der Klasse B erwartet. Diese Beziehungen sind also nie von langer Dauer. Wenn sich z. B. ClassA als Eventlistener an ClassB anmeldet (für den Fall, dass ClassB Events wirft), kann man dies mit dem Abhängigkeitspfeil kennzeichnen, denn es ist nur eine temporäre Abhängigkeit, die ja durch das Abmelden auch wieder gelöst werden kann. Assoziation ClassA
ClassB -parent 1
besitzt
-children *
Abbildung 4.6: Assoziation Die Assoziation ist die nächststärkere Beziehung. Sie verwendet man, wenn man aussagen will, dass eine Klasse direkt ein Klassenattribut hat, das vom Typ einer anderen Klasse ist. Sobald man mit dieser Art von Beziehung arbeitet, werden sofort weitere Fragen interessant, wie z. B. wer hat eine Referenz zu wem, ist es ein einzelnes Klassenattribut oder vielleicht eine ganze Liste von Elementen und in welchem Zusammenhang besteht eigentlich die Beziehung. In werden solche Fragen beantwortet. Über der Assoziationslinie sehen wir links das Wort parent (das Minuszeichen zeigt an, dass es sich um ein privates Attribut handelt). Man liest hier, dass ClassB ein privates Klassenattribut mit dem Namen parent besitzt, das vom Typ ClassA ist. Sie wundern sich vielleicht, warum parent auf der Seite von ClassA steht. Letztendlich ist es eine Konvention. Sie soll klarmachen, dass ClassA der parent von ClassB ist. Mit children ist es umgekehrt. Unter der Linie steht noch eine 1 und rechts ein Sternchen. Die 1 meint, dass ClassB nur genau eine Instanz von ClassA besitzt. Das Sternchen wiederum sagt, dass ClassA null bis beliebig viele Instanzen von ClassB besitzen kann. Wahrscheinlich wird das intern über ein Array oder einen Vektor implementiert, aber das ist hier nicht von Belang. Außerdem kann man einer Assoziation allgemein noch einen Namen geben, der in der Mitte steht, in diesem Fall »besitzt«.
141
Kapitel 4
Eine Assoziation kann auch gerichtet sein, wenn wir damit andeuten wollen, dass es nur eine Beziehung in einer Richtung gibt. ClassA
ClassB -service
verwendet 1
0..1
Abbildung 4.7: Eine gerichtete Assoziation Eine gerichtete Assoziation hat dann an dem entsprechenden Ende der Linie einen Pfeil. Hier wird also gesagt, dass ClassA ein privates Klassenattribut mit dem Namen service besitzt, das vom Typ ClassB ist, und dass ClassA zur Laufzeit entweder keine oder maximal eine Instanz von ClassB haben wird. Die Zahl 1 an der Seite von ClassA fällt noch auf. Sie sagt in diesem Zusammenhang, dass eine bestimmte Instanz von ClassB nur jeweils von einer Instanz von ClassA referenziert werden soll. Was im Klartext heißt, jede Instanz von ClassA verwendet eine eigene Instanz von ClassB, es werden keine gemeinsamen Instanzen verwendet. Assoziationsklasse Dialog
Button -listener
-closeButton
*
1
MouseEvent
Abbildung 4.8: Klassiker für eine Assoziationsklasse, ein Event Man braucht sie nicht unbedingt häufig, aber manchmal können sie zur Erläuterung nützlich sein: Assoziationsklassen. Sie werden an eine Assoziation angehängt mithilfe einer gestrichelten Linie. Im Flash-Bereich sind Events ein gutes Beispiel für die Illustrierung mittels einer Assoziationsklasse. Ein Dialog ist an einem Button als Listener angemeldet. Der Assoziationspfeil stellt dies dar. Der Button wiederum kommuniziert in lose gekoppelter Form mit dem Dialog unter Zuhilfenahme eines Events, speziell dem MouseEvent. Assoziationsklassen können als Darstellungsmittel auch verwendet werden, wenn eine Klasse zu einer anderen Klasse eine Beziehung hat, die durch eine dritte Klasse bestimmt oder eingeschränkt wird. Wenn z. B. eine Klasse Suche eine Beziehung zu vielen Suchergebnissen hat, könnte eine Klasse Suchfilter als Assoziationsklasse fungieren, die konkret bestimmt, wie viele Suchergebnisse es denn nun sind. Aggregation ClassA
ClassB besitzt 1
-elements *
Abbildung 4.9: Eine Aggregation, besitzanzeigende Form der Assoziation
142
Entwurfswerkzeuge
Die Beziehung wird noch etwas enger, obwohl der Unterschied zu einer Assoziation teilweise recht schwierig ist. Im Prinzip geht es hier um Besitzverhältnisse. Mit einer Assoziation sagt man erst mal nur aus, dass eine Klasse A ein Klassenattribut vom Typ einer Klasse B besitzt. Wofür dieses Klassenattribut inhaltlich steht, ist damit noch nicht gesagt. Klasse B könnte eine Utility-Klasse sein, die einfach nur zu Hilfszwecken verwendet wird. Bei einer Aggregation möchte man hingegen explizit aussagen, dass eine Klasse A eine Instanz einer Klasse B besitzt. Gerade in Szenarien, wo man z. B. die Schachtelung von Objekten darstellen will, wird oft die Aggregation verwendet. Ein Hauptmenüpunkt besitzt Untermenüpunkte. Hier eine normale Assoziation zu verwenden, wäre zwar nicht falsch, würde aber die inhaltliche Bedeutung nicht ganz so klarmachen wie die Aggregation. Komposition ClassA
ClassB besitzt 1
-parts *
Abbildung 4.10: Komposition, Darstellung des internen Aufbaus einer Klasse Mal abgesehen von den Pfeilen für Vererbung bzw. Implementierung eines Interface ist die Komposition die stärkste Form der Darstellung einer Beziehung. Die Komposition beschreibt zunächst auch einfach Besitzverhältnisse, wie das die Aggregation auch tut. Im Gegensatz zu ihr besagt eine Komposition aber, dass z. B. eine Klasse A alleinig eine Instanz zu einer Klasse B besitzt und dass diese Instanz einen funktionalen Teil der Klasse A ausmacht. Die Instanz der Klasse B wird nicht auch noch gleichzeitig von anderen Klassen verwendet, sondern dient ausschließlich der Klasse A. Somit beendet das Löschen einer Instanz von Klasse A auch automatisch die Existenz der Instanz von Klasse B. Die Komposition wird dann verwendet, wenn man darstellen will, dass eine Gruppe von Klassen zusammen eine funktionale Einheit ergibt, die von einer Hauptklasse gesteuert wird. Ein XML-Parser z.B., der für bestimmte Tags Hilfsklassen verwenden würde, um die Tags zu parsen, hätte eine kompositorische Beziehung zu diesen Hilfsklassen, denn nur er würde sie verwenden und nur er hätte Referenzen auf sie.
Noch enger ist die Beziehung zwischen Step und Follower. Wie durch das Array followers in Step angedeutet, beinhaltet ein Step mehrere Instanzen vom Typ Follower. Dies wird durch die Linie mit der hell gefüllten Raute am Ende gekennzeichnet. Die Raute liegt immer an dem Ende der Klasse, die die andere beinhaltet. Step hält also Referenzen auf Follower. Zudem sind hier noch Zahlen an den Enden angegeben. Diese kennzeichnen die möglichen Multiplizitäten zur Laufzeit. Das heißt also z.B., dass ein Step eine Menge von null bis unendlich vielen Follower-Instanzen beinhalten kann. Das leuchtet ein, denn das followers-Array in Step deutet ja auch schon darauf hin. Interessant ist aber auch die andere Zahl bei Step. Sie liest sich so: Ein Follower kann von mindestens einem oder mehreren Steps referenziert werden. Das ist eine nicht unwichtige Information. Ein Follower kann also mehrfach verwendet werden. Anhand dieses zweiten Beispiels kann man schon gut sehen, dass eine erhöhte Komplexität eines Diagramms schnell auch zu Unübersichtlichkeit führen kann. Umso wichtiger ist es, sich beim Erstellen eines UML-Diagramms klarzumachen, welche Kernaussage man eigentlich treffen will. Eine Kernaussage ist hier nicht erkennbar. Alle Klassen haben den gleichen
143
Kapitel 4 Detaillevel, immerhin wurden nicht auch noch die privaten Methoden und Attribute abgebildet. Überlegen wir uns doch mal, wie wir hier ein besseres Diagramm bauen könnten. Wir brauchen also zunächst mal eine Kernaussage. Was will ich über die vorstehenden Klassen sagen? Im Prinzip ist die etwas allgemeinere Process-Klasse ja recht simpel, die DialogProcess-Klasse mit ihren Steps und Followers scheint da schon interessanter zu sein. Vielleicht beschränken wir uns einfach mal darauf, das Zusammenspiel der DialogProcessKlasse mit ihren Hilfsklassen darzustellen. Das Ergebnis meiner Bemühungen ist in Abbildung 4.11 zu sehen.
Command
«interface» process::ICommand
process::DialogCommand + #
+ + + +
execute(ParamContainer) : void setResult(String) : void
addEventListener(String, Function, Boolean, int, Boolean) : void removeEventListener(String, Function, Boolean) : void execute(ParamContainer) : void cancel() : void 1
Process process::DialogProcess
+ pushGeneralFollower(Follower) : void + pushStep(Step) : void ::Process + clear() : void + getParam(String) : Object + pushCommand(ICommand) : void + setParam(String, Object) : void + start() : void + stop() : void ::EventDispatcher + addEventListener(String, Function, Boolean, int, Boolean) : void + dispatchEvent(Event) : Boolean + EventDispatcher(IEventDispatcher) + hasEventListener(String) : Boolean + removeEventListener(String, Function, Boolean) : void + willTrigger(String) : Boolean «property get» + currentCommand() : ICommand 1
1
1
0..* process::Follower
+
process::Step 1..*
Follower(String, Step)
«property get» + code() : String + step() : Step
0..*
+ +
pushFollower(Follower) : void Step(ICommand, Array)
«property get» 1..* + command() : ICommand + followers() : Array
Abbildung 4.11: Reduzierte Variante des Diagramms aus Abbildung 4.4
144
Entwurfswerkzeuge Wie man sehen kann, habe ich nun einige Klassen weggelassen. Process fehlt nun ganz, denn wir konzentrieren uns hier nun ganz auf DialogProcess und seine beteiligten Klassen. Ich habe aber auch etwas hinzugefügt. Da ein Entwickler, der den DialogProcess mit dem DialogCommand nutzen will, wahrscheinlich DialogCommand beerben wollen wird, habe ich in DialogCommand auch die protected-Methode setResult sichtbar gemacht. In der Klasse DialogProcess kann man nun auch erkennen, welche Methoden von Process geerbt wurden, nämlich die Methoden, die unter »::Process« stehen. In der rechten oberen Ecke des Kastens der Klasse DialogProcess ist außerdem zu erkennen, dass DialogProcess von Process erbt. Dieses Diagramm lässt nun viel eher eine Kernaussage erkennen, nämlich das Zusammenspiel der Klassen rund um DialogProcess. Klassendiagramme müssen also nicht vollständig sein, sie können sehr abstrakt bleiben, z. B. wenn man einfach nur die Klassen selbst, aber nicht ihre Methoden oder Attribute darstellt. Komplexität kann auch reduziert werden, indem man Klassen, die nur sekundär mit dem eigentlichen Thema zu tun haben, nicht direkt darstellt. In Abbildung 4.11 stelle ich z. B. die Klasse Process nicht direkt dar, dennoch wird klar, dass sie existiert. Die Kunst bei Klassendiagrammen besteht also auch oder gerade aus dem Weglassen von Details.
4.1.3 Aktivitätsdiagramm Der Name klingt erst einmal ein wenig hochgestochen, letztlich sind Aktivitätsdiagramme Flowcharts sehr ähnlich. Wer also schon mal ein Flowchart gesehen hat, der wird ein Aktivitätsdiagramm recht schnell verstehen. Und so auch den Zweck Letzterer. Mit ihnen lassen sich Abläufe aller Art darstellen. Nun meinen manche Entwickler vielleicht, dass Flowcharts doch wohl eher etwas für Konzepter sind. Wenn aber ein Entwickler sicher sein will, dass er einen Ablauf, der vielleicht für seine Anwendung essenziell ist, richtig verstanden hat und es für diesen Ablauf noch keine zufriedenstellende Beschreibung gibt, dann zeichnet er lieber selber ein Diagramm und bespricht es mit den Kollegen aus dem Konzeptbereich, bevor er eventuell eine Menge Code für den Mülleimer produziert. Auch bei Aktivitätsdiagrammen ergibt sich wieder der Vorteil, dass man einen eventuell komplexen Prozess erst einmal schnell grafisch skizzieren und diskutieren kann, bevor man ihn implementieren muss. Nicht selten ergeben sich beim Skizzieren schon neue Fragen oder Stolperstellen, die man dann frühzeitig klären kann. Abbildung 4.12 zeigt ein Aktivitätsdiagramm für den Ablauf eines Login-Dialogs. Jedes Aktivitätsdiagramm hat genau einen Startpunkt. Die kleinen rautenförmigen Vierecke stehen entweder für einen Entscheidungspunkt, aus dem unterschiedlich viele Wege entspringen, oder eine Zusammenführung, in die mehrere Wege münden. Direkt nach dem Startpunkt haben wir so eine Mündung. Und direkt nach der »Zeige-Login-Dialog«-Aktion haben wir einen Entscheidungspunkt.
145
Kapitel 4
Registrierung Account gelöscht
Nutzer registriert sich
Registrierung Ende
Verifizierung Timeout Zeige Registrierungs-Dialog
Sende Verifizierungs-Email
Nutzer hat verifiziert
Account verifiziert
Passwort Erinnerung Zeige Passwort Erinnerung Dialog
Zeige Login Dialog
Nutzer gibt EMail Addresse an
Account Details gesendet
Start
Erinnerung Ende
Nutzer trägt Logindaten ein
Nutzer authentifiziert Ende
Logindaten unbekannt
Abbildung 4.12: Beispiel für ein Aktivitätsdiagramm, ein Login-Dialog
Bestimmte Teilbereiche eines Ablaufs gehören oftmals inhaltlich zusammen. Zu diesem Zweck unterscheidet man zwischen Aktionen und Aktivitäten. Eine Aktion ist ein einzelnes Element, das nicht weiter aufgeteilt wird. »Nutzer authentifiziert« ist z. B. so eine Aktion. Eine Aktivität besteht wiederum aus einzelnen Aktionen oder auch weiteren Unteraktivitäten. »Passwort Erinnerung« und »Registrierung« sind Aktivitäten. Sowohl in »Passwort Erinnerung« als auch in »Registrierung« haben wir eine Gabelung, gekennzeichnet durch den senkrechten Strich. Im Unterschied zur Entscheidung bzw. Mündung bedeutet eine Gabelung (bzw. Vereinigung im Umkehrschluss), dass hier mehrere Wege parallel beschritten werden, wohingegen bei einer Entscheidung immer nur einer der möglichen Wege beschritten wird. Bei »Registrierung« bedeutet dies also, dass, während noch die Verifizierungsmail versendet wird, der Nutzer schon wieder zu »Zeige-Login-Dialog« zurückgeführt wird. Die Elemente »Sende Verifizierungs-E-Mail« und »Nutzer hat verifiziert« stellen einen zeitlich entkoppelten Ablauf dar. Wir können ja nicht wissen, wann der Nutzer die Verifizierungsmail liest und bestätigt, diese zeitliche Unbekannte stellen wir durch die beiden speziell geformten Kästen dar. Außerdem haben wir da noch das Symbol mit der Bezeichnung »Verfizierung Timeout«. Es ähnelt einer Sanduhr und soll andeuten, dass es sich hier um einen Auslöser handelt, der durch ein Zeitintervall ausgelöst wird. Wenn der Nutzer sich nach einer bestimmten Zeit nicht verifiziert, dann soll unsere Anwendung einen Timeout auslösen, und die temporären Accountdaten sollen wieder gelöscht werden.
146
Entwurfswerkzeuge Zu guter Letzt sehen wir in dem Diagramm zwei unterschiedliche Arten von Endpunkten. Zum einen haben wir da den Kreis mit dem Kreuz, wie z. B. in »Registrierung«, zum anderen den Kreis mit einem dicken Punkt im Inneren. Ersterer zeigt an, dass hier nur der entsprechende Teilablauf endet, aber nicht zwingendermaßen der gesamte Ablauf. Letztere Art wiederum symbolisiert das Ende des gesamten Ablaufs.
4.1.4 Kontrollflussdiagramme Um den Kontrollfluss in einer Anwendung darzustellen, gibt es unterschiedliche Diagrammtypen in der UML:
Sequenzdiagramme Kommunikationsdiagramme Statusdiagramme Sie alle können in bestimmten Situationen nützlich sein. Sequenzdiagramme z. B. zeigen sehr detailliert den Kontrollfluss zwischen verschiedenen Objekten. Das geht sogar so weit, dass man den Funktionsstack in seiner Anwendung nachvollziehen kann. Schauen wir uns hierzu ein kleines Beispiel in Abbildung 4.13 an. Es wird recht schnell klar, Sequenzdiagramme können sehr komplex werden, obwohl sie vielleicht, wie in diesem Beispiel, eigentlich einen einfachen Sachverhalt darstellen. Hier lade ich einfach eine XML-Datei und zeige währenddessen eine Preloader-Animation. Es sind vier Objekte beteiligt (vereinfacht, ich habe z. B. das notwendige URLRequest-Objekt weggelassen). Wichtig ist hier schon mal, dass wir von Objekten, also Klasseninstanzen sprechen. Deswegen tragen die Objekte auch konkrete Namen und zeigen nach dem Doppelpunkt ihren Typ. Jedes Objekt hat eine gestrichelte, senkrechte Lebenslinie. Auf die Lebenslinie werden Balken gesetzt, wenn ein Objekt gerade aktiv ist, sprich, wenn sie eine ihrer Funktionen ausführt. Nun kann es natürlich sein, dass eine Funktion in sich wiederum eine andere Funktion des gleichen Objekts aufruft. Das können wir bei der mainApplication zu Beginn sehen. addChild() und loadConfiguration() sind gleichfalls Methoden von mainApplication. Dadurch, dass sie innerhalb einer schon aktiven Funktion aufgerufen werden, kommt noch mal ein Balken dazu. Die Balken stellen hier sozusagen den Funktionsstack dar. Das kann recht interessant sein. Z. B. können wir sehen, dass das Löschen von xmlLoader weiter unten im Diagramm nicht sofort ausgeführt wird, sondern erst später (sichtbar dann durch den Abschluss der Lebenslinie mit einem X). Und zwar verhält sich das deswegen so, weil xmlLoader ja onComplete auf der mainApplication aufgerufen hat, und onComplete ist erst fertig, wenn vom XML-Objekt die length()-Methode aufgerufen wurde. Erst dann kehrt die Ausführung zurück zum xmlLoader. Und erst dann wird die laufende Funktion von xmlLoader vom Funktionsstack genommen und das Objekt intern tatsächlich für den Garbage Collector des Flash Players freigegeben.
147
Kapitel 4
mainApplication :MainApplication
preloaderAnimation :PreloaderAnimation «create» addChild(preloaderAnimation)
loadConfiguration()
xmlLoader :URLLoader «create»
addEventListener(ProgressEvent.PROGRESS, onProgress)
addEventListener(Event.COMPLETE, onComplete)
load(urlRequest)
onProgress(event)
setProgress(percent)
onComplete(event)
new XML(xmlLoader.data)
configuration :XML
«create»
«delete»
«delete» length() :int
Abbildung 4.13: Vereinfachtes Sequenzdiagramm für einen Ladevorgang
In Sequenzdiagrammen werden unterschiedliche Pfeile verwendet. Ein durchgezogener Pfeil mit gefüllter Pfeilspitze steht für einen einfachen Funktionsaufruf. Ein durchgezogener Pfeil mit offener Pfeilspitze – so wie z. B. bei load(), die auf dem xmlLoader-Objekt aufgerufen wird – steht für einen asynchronen Aufruf. Da ja alle Ladevorgänge in Flash asynchron sind, müssen wir also hier einen solchen Pfeil verwenden. Das hat zur Folge, dass der Aktivitätsbalken von mainApplication nach dem load()-Aufruf erst einmal endet, denn nun muss mainApplication einfach auf die Events vom xmlLoader warten. Wenn es also darauf ankommt, den exakten Ablauf eines Kontrollflusses nachzuvollziehen, dann sind Sequenzdiagramme sehr nützlich. Sie können aber auch schnell unübersichtlich
148
Entwurfswerkzeuge werden. Ich habe hier z. B. nur ein onProgress eingezeichnet. Natürlich würde mainApplication in der Realität mehrmals ProgressEvents erhalten, aber das würde das Diagramm noch unübersichtlicher machen. Auch das Hinzufügen des URLRequest-Objekts hätte das Diagramm komplexer gemacht und nicht wirklich mehr nützliche Information gebracht. Das Weglassen von Zwischenschritten zur Steigerung der Übersichtlichkeit hat aber auch seine Tücken. Bei einem Sequenzdiagramm, in dem ein konkreter Ablauf dargestellt wird, gehen Entwickler meist von Vollständigkeit aus, denn die sehr konkreten Aufrufe auf die Objekte suggerieren dies. Ein Entwickler wird also eher nicht annehmen, dass Zwischenschritte weggelassen wurden. Wenn nun aber tatsächlich Zwischenschritte weggelassen werden, dann kann das zu Verwirrung führen. Hier sollte man dann eventuell Notizen in das Diagramm einbringen, die darauf hinweisen, dass hier der Übersichtlichkeit halber Dinge weggelassen wurden. Eine andere Darstellungsform ermöglichen Kommunikationsdiagramme. Sie stellen letztlich auch den Kontrollfluss zwischen Objekten dar, aber sie verwenden dafür keine Lebenslinien, sondern visualisieren eher Kommunikationsnetze. Um das zu veranschaulichen, zeigt Abbildung 4.14 den exakt gleichen Ablauf wie in Abbildung 4.13, nur unter Verwendung eines Kommunikationsdiagramms.
configuration :XML
preloaderAnimation : PreloaderAnimation
3.4: length() :int 3.2: delete() 3.1: new XML(xmlLoader.data)
2.1: setProgress(percent) 1: new PreloaderAnimation()
mainApplication : MainApplication 1.1: addChild(preloaderAnimation) 1.2.1: new URLLoader()
1.2: loadConfiguration()
1.2.2: addEventListener(ProgressEvent.PROGRESS, onProgress) 3: onComplete(event)
1.2.3: addEventListener(Event.COMPLETE, onComplete) 1.2.4: load(urlRequest)
2: onProgress(event) 3.3: delete()
xmlLoader : URLLoader
Abbildung 4.14: Ladevorgang, nun dargestellt mit einem Kommunikationsdiagramm
Man kann hier sehen, dass bei einem Kommunikationsdiagramm eher die Objekte im Mittelpunkt stehen und ihre Beziehungen zu den anderen Objekten. Will man nun den Kommunikationsfluss lesen, muss man der Nummerierung der Pfeile besondere Aufmerksamkeit widmen. Sie geben die Reihenfolge an. Dabei ist auch die Verschachtelung zu beachten. Man liest also:
149
Kapitel 4 1 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 2 usw. Hier kann man auch gleich eine potenzielle Schwäche dieser Diagramme erkennen. Wenn man sehr viele Flüsse in ein und demselben Diagramm darstellen will, dann verliert man schnell den Überblick über die Nummerierung. Letztlich kommt hier wieder derselbe Grundsatz zum Tragen wie schon bei den anderen Diagrammen genannt: Konzentrieren Sie sich auf eine Kernaussage. Um übersichtlich zu bleiben, sollte das Diagramm einen bestimmten Ablauf und nicht sämtliche möglichen Abläufe eines Objekts darstellen. Abgesehen davon aber erscheinen Kommunikationsdiagramme tendenziell weniger komplex, weil sie weniger das exakte Timing visuell darstellen, sondern sich auf die Visualisierung der Beziehungen im Allgemeinen konzentrieren. Je nachdem, auf was man seinen Fokus legen will, wird man also den einen oder anderen Diagrammtyp wählen. Einen letzten Diagrammtyp zur Visualisierung von Kontrollflüssen will ich noch zeigen, auch wenn es sich eigentlich weniger um die Darstellung von Flüssen handelt, sondern vielmehr um das Aufzeigen von Status. Deswegen heißt der Diagrammtyp auch Statusdiagramm. In Fällen, in denen man eine Flash-Applikation baut, die eine Definition von festen Status erlaubt – z. B. wenn die Anwendung von konkreten Screens zu konkreten anderen Screens springt oder wenn z. B. der Status von Daten beschrieben werden soll –, empfiehlt sich die Verwendung eines Statusdiagramms. Dabei kann ein Status zwei gedankliche Dinge beschreiben, nämlich einmal einen passiven Zustand, meistens beschrieben in der Form, dass ein Ding in einem bestimmten Zustand ist, oder einen aktiven Zustand, in dem ein Ding etwas tut. Das klassische Statusbeispiel für ein Ding, das sich in passiven Zuständen befindet, ist der Lichtschalter. Er ist entweder in der Stellung an oder aus. Ihn aktiv zu beschreiben, macht irgendwie keinen Sinn, obwohl es theoretisch auch ginge. Man könnte also auch sagen, ein Lichtschalter kann entweder gerade anschaltend oder ausschaltend sein. Das klingt aber doch eher merkwürdig. Anders sieht es aus bei Objekten, die für einen längeren Zeitpunkt etwas tun. Eine Animation z. B. kann unter anderem im Zustand spielt oder pausiert sein. Hier macht die aktive Beschreibung schon mehr Sinn. Statusdiagramme werden meistens dann eingesetzt, wenn das Verhalten einer Anwendung oder eines Teils der Anwendung stark von unterschiedlichen Zuständen bestimmter Objekte abhängt. Schauen wir uns das an einem einfachen Beispiel eines Videoplayers für unseren Online-Videoverleih FilmRegal an.
150
Entwurfswerkzeuge
Nutzer hat Film ausgeliehen
Film anzeigen Videoplayer prüft Legitimierung
Filmanzeige startet
Film gestoppt Film ist zu Ende
+
do / requestServer
Prüfung beendet [legitimiert] /aktivieren
Prüfung beendet [nicht legitimiert] /deaktivieren
Nutzer stoppt Film
Videoplayer deaktiviert
Nutzer stoppt Film
Film buffert +
entry / requestVideo
Film spielt + +
Film pausiert
do / play do / stream
Abbildung 4.15: Statusdiagramm für einen Videoplayer
Wie auch schon beim Aktivitätsdiagramm beginnt ein Statusdiagramm mit einem Initialstatus (ein schwarzer Punkt), der ein Pseudostatus ist, weil von ihm sofort zum nächsten übergegangen wird. Ein Status wird mit einem abgerundeten Kästchen dargestellt. Pfeile stellen Übergänge von einem zum anderen Status dar. Ein Pfeil kann auch wieder zum gleichen Status zurückführen, z. B. wenn etwas in einer Schleife mehrmals geschieht. An einen Pfeil kann man noch zusätzlich Informationen unterbringen. Im einfachsten Fall schreibt man einfach, was während des Übergangs passiert. Zwischen »Film spielt« und »Film gestoppt« gibt es z. B. einen Pfeil mit dem Label »Nutzer stoppt Film«. Die Labels können aber auch umfangreicher sein. Zwischen »Videoplayer prüft Legitimierung« und »Videoplayer deaktiviert« kann man das sehen. Ein Übergang kann beschreiben, wodurch er ausgelöst wurde (hier dadurch, dass die Prüfung beendet wurde). Er kann dann zusätzlich eine Bedingung angeben, die wahr sein muss, damit der Übergang durchlaufen wird (hier ist der Nutzer nicht legitimiert). Und zuletzt kann man dann noch angeben, was während dieses speziell beschriebenen Übergangs gemacht wird, in diesem Fall wird der Videoplayer deaktiviert. Ein Kästchen besteht entweder nur aus dem Namen des Status oder noch aus internen Beschreibungen. Man kann internes Verhalten und auch noch mal interne Übergänge beschreiben. Internes Verhalten meint, dass man beschreibt, was z. B. passiert, während sich die Anwendung in einem bestimmten Status befindet. Im Status »Film spielt« passiert z. B. zweierlei, einerseits spielt natürlich der Film, andererseits wird der Film gestreamt, es werden also kontinuierlich Daten nachgeladen. Solches Verhalten beschreibt man mit dem Schlüsselwort »do«, nach einem Schrägstrich kommt dann die Aktion. Das kann auch eine ganz konkrete Methode einer Klasseninstanz sein, wenn man so detailliert werden will. Neben dem Schlüsselwort »do« gibt es noch »entry« und »exit«. Sie sind dazu da zu beschreiben, was passiert, wenn der Status betreten bzw. verlassen wird. Beim Betreten des Status »Film buffert« wird z. B. überhaupt das Video vom Server angefordert. Status, die man als zusammengehörig ansieht, kann man gruppieren und daraus dann einen größeren Gesamtstatus bauen. »Film anzeigen« ist so einer. So ein großer Status, oder Composite State, trägt in sich auch wieder einen Initialstatus. Hier ist auch ein Endstatus eingetragen, »Film gestoppt«.
151
Kapitel 4 Innerhalb von solchen zusammengesetzten Status kann man auch sogenannte Regionen bilden, die nebenläufige Status ermöglichen, aber darauf will ich nicht näher eingehen, weil Flash selbst ja keine individuell erzeugbare Nebenläufigkeit unterstützt. Sequenz-, Kommunikations- und Statusdiagramme ermöglichen auf unterschiedliche Art und Weise die Darstellung dessen, was in einer Anwendung zur Laufzeit passiert. Sie ermöglichen einen anderen Blickwinkel auf eine Anwendung, als dies das reine Lesen von Code vermag, denn sie lassen Strukturinformationen weitestgehend außen vor und konzentrieren sich auf die Abläufe. So erkennt man mit ihnen eventuell schneller, wie eine Anwendung bezüglich eines bestimmten Ablaufs funktioniert, oder man kann für eine noch zu erstellende Anwendung im Voraus testen, wie ein Ablauf möglichst schlank gestaltet werden kann. Auch hier ist wieder wichtig, dass man nicht blind für die gesamte Anwendung ein Diagramm erstellt, sondern nur für die Bereiche, für die es einen Nutzen bringt. Zum Beispiel ist es bei komplexer Client-Server-Kommunikation oft hilfreich, die hin und her gesendeten Daten und Signale in einem Sequenz- oder Kommunikationsdiagramm zu skizzieren, um sich einen Überblick zu verschaffen. In einem Spiel, in dem man computer- und nutzergesteuerte Charaktere beschreibt, kann ein Statusdiagramm helfen, die verschiedenen möglichen Zustände der beteiligten Objekte zu visualisieren, um z. B. im Team ein gemeinsames Verständnis von der technischen Idee zu erhalten.
4.2 Muster Viele Vorgehensweisen, die wir in unserem Leben verfolgen, sind Resultate aus gemachten Erfahrungen, Fehlern und Ratschlägen von anderen; sei es das Zubereiten von Mahlzeiten, der Goldene Schnitt, die Aufbereitung der nächsten Steuererklärung oder das richtige Mischungsverhältnis für Beton. Es gibt immer viele unterschiedliche Ideen und Wege, diese Aufgaben zu meistern, aber manche haben sich als Fehlschläge herausgestellt, andere scheinen besser zu funktionieren. Haben wir einmal einen Weg gefunden, der gut funktioniert, verfeinern wir ihn immer weiter, bis wir irgendwann richtig gut darin werden. Die Krönung ist dann, diese Idee auch anderen näher zu bringen, mit dem Ziel, dass sie nicht so viel Zeit in die Entdeckung der Letzteren stecken müssen. Diese Ideen, die sich bewährt haben, gibt es auch in der Softwareentwicklung, und man nennt sie Muster. Nun gibt es unterschiedliche Muster. Es gibt Vorgehensmuster, die einen Ablauf vorschlagen, der besonders effizient ist. Es gibt auch Muster, die die Beschaffenheit oder den Aufbau von etwas beschreiben. Mit diesen letzteren Mustern möchte ich mich in den folgenden Kapiteln beschäftigen. Wir alle schaffen Muster. Und wenn Sie in Ihrem Job den ganzen Tag Flash-Werbebanner bauen, auch dann haben Sie für sich bestimmt schon einen Weg entdeckt, wie sie an diese Aufgabe immer wieder rangehen. Eventuell haben Sie sich ein paar Codefragmente zurechtgelegt, ganz sicher haben Sie bestimmte technische Gedanken im Kopf, die Sie beachten, wenn Sie so ein Banner bauen, um mit Limitierungen wie Dateigröße und Performance umgehen zu können. Auch diese Ideen und Codefragmente zusammen ergeben ein
152
Entwurfswerkzeuge Muster, ein Muster für die Erstellung von Bannern. Dieses Muster würde seine volle Wirkung noch mehr entfalten, wenn Sie es konkret formulieren könnten, wenn Sie einem Kollegen sagen könnten, wie er am besten vorgeht, wenn er ein Banner bauen muss. Wir alle produzieren also Muster während unserer Arbeit. Die meisten haben wir unbewusst im Kopf. Einige Softwareentwickler haben sich die Mühe gemacht und ihre Muster aufgeschrieben. Ein paar von ihnen werde ich in den folgenden Kapiteln besprechen. Aber wichtiger ist fast, dass Sie sich bewusst machen sollten, welche Muster Sie vielleicht heute schon unbewusst verwenden. Der bewusste Umgang mit ihnen ermöglicht vielleicht, dass Sie sie verbessern oder dass Sie sie anderen erklären können. Vielleicht hat Ihr Kollege ein ähnliches Muster, und die Synthese aus beiden würde ein noch besseres ergeben. Verstehen Sie also die heute so gern diskutierten Softwaremuster bitte nicht als die einzig gültigen Muster. Im Gegenteil, die hier vorgestellten Muster sind sehr allgemein und abstrakt. Eventuell ist das Muster, das Sie unbewusst mit sich herumtragen, viel konkreter auf Ihre übliche Aufgabenstellung angepasst. Werfen Sie es dann nicht komplett über Bord, sondern fragen Sie sich eher, ob Sie Ihr bestehendes konkretes Muster anpassen und verbessern können durch Anwendung von den deutlich abstrakteren Mustern, die z. B. in dem bekannten Buch »Entwurfsmuster« der Gang of Four beschrieben werden (Gamma, Riehle 2008).
4.3 Architekturmuster Architekturmuster beschreiben, wie der Name es schon andeutet, den möglichen Aufbau einer Softwarearchitektur. Eine Architektur beschreibt – wir erinnern uns – ein komplettes Softwaresystem. Architekturmuster beschäftigen sich deshalb auch nicht direkt mit konkreten Klassen, sondern noch etwas abstrakter mit Modulen und Subsystemen. Ich habe schon in vorangegangenen Kapiteln gesagt, dass ich mich mit Architekturen nicht allzu sehr im Detail beschäftigen werde. Das gilt auch für Architekturmuster. Es ist aber auch klar, dass die Architektur und damit auch die hier verwendeten Muster einen deutlichen Einfluss auf den Softwareentwurf nehmen. Nicht zuletzt deswegen, weil Flash- und Flex-Anwendungen letztlich immer in eine Gesamtarchitektur eingebunden sind und durch sie bestimmt werden. Deswegen ist es also auch sinnvoll, sich wenigstens grundsätzlich mit diesem Themenkomplex zu beschäftigen. Insbesondere dadurch, dass Flash- bzw. Flex-Anwendungen meistens auch Webanwendungen sind, interessieren wir uns auch im Besonderen für Webarchitekturen bzw. verteilte Systeme und die Muster dahinter. Ich habe hier eine kleine Auswahl an Mustern ausgewählt, die für viele Flash- und FlexEntwickler interessant sein könnten. Das bedeutet aber auch, dass ich die weitaus größere Auswahl der insgesamt bekannten Muster notwendigerweise weggelassen habe.
4.3.1 Schichtenmodell Das Schichtenmodell ist ein sehr grundlegendes Muster und wird im Prinzip in den meisten Internet-Architekturmustern verwendet. Die Idee dabei ist, dass sich ein Softwaresystem, das über mehrere logische Elemente verteilt ist, in Schichten darstellen lässt. Schichten
153
Kapitel 4 deshalb, weil diese logischen Teile alle übereinandergestapelt sind (logisch meint hier einen Teil, der eine bestimmte Verantwortlichkeit übernimmt, das sagt nichts über die tatsächliche physikalische Verteilung aus). Schichten auch, weil die Kommunikation immer nur von einer zur nächsten Schicht erfolgt und nicht eine Schicht überspringen soll. Die klassische Aufteilung ist eine Drei-Schichten-Architektur:
Präsentation Applikationslogik Daten Wie schon gesagt, hier ist noch nicht bestimmt, wie diese drei Schichten physikalisch verteilt sind, also auf wie viele und was für Rechner. Die Datenschicht besteht meistens aus einem Datenbanksystem. Die Applikationslogik stellt die eigentlich Anwendung mit ihren Use Cases und den Domänenobjekten dar (also den Objekten, die jeweils Elemente aus dem Geschäftsbereich der Anwendung repräsentieren, wie z. B. »Kunde«). Und die Präsentation stellt die Schnittstelle zum Nutzer dar. In einem verteilten System sind diese drei Schichten meistens tatsächlich auch physikalisch getrennt. Die Datenbank läuft auf einer eigenen Maschine, die Applikationslogik auch, und die Präsentation läuft sowieso auf dem Rechner des Nutzers. Das Drei-Schichten-Modell kann noch weiter ausdefiniert werden. Uns fällt zum Beispiel auf, dass die Applikationslogik momentan direkt auf die Datenbank zugreift. Das bindet diese beiden Teile stärker zusammen als vielleicht gewünscht. In diesem Fall würde man noch eine Zwischenschicht einziehen, eine Datenzugriffsschicht:
Präsentation Applikationslogik Datenzugriffslogik Daten Der Vorteil ist hier, dass nun die Datenschicht ausgetauscht werden kann, ohne dass die Applikationslogik dadurch geändert werden müsste. Auch und gerade die Applikationslogik kann noch entzerrt werden. In der Applikationslogik befinden sich unterschiedliche Verantwortlichkeiten, wie ich weiter oben schon erwähnt habe. Die wichtigsten beiden Teile sind zum einen die Domänenobjekte und zum anderen die konkreten Applikations-Use-Cases. Die Domänenobjekte repräsentieren das grundsätzliche Wesen des Unternehmens, für das die Applikationen gebaut werden. Im meinem Beispielprojekt FilmRegal hatten wir bereits solche identifiziert. Da war z. B. der Kunde, ein Film, ein Ausleihvertrag usw. Diese wären also Teil der Domänenobjekte. Die konkrete Anwendung arbeitet mit diesen Domänenobjekten, aber sie kann trotzdem getrennt von ihnen betrachtet werden. Das hat den Vorteil, dass es hiermit möglich wird, mehrere Anwendungen zu bauen, die alle die gleichen Domänenobjekte verwenden. Der Grad an Wiederverwendung steigt dadurch. Wir teilen also die Schicht der Applikationslogik noch mal in zwei Teile, einmal die Applikationslogik (die weiterhin so heißen darf) und dann die Domänenlogik:
154
Entwurfswerkzeuge
Präsentation Applikationslogik Domänenlogik Datenzugriffslogik Daten Hier wird nun klar, dass jetzt die Applikationslogik nicht mehr direkt Daten speichert, sondern immer nur mit Domänenobjekten arbeitet und sie manipuliert. Die Domänenobjekte wiederum speichern diese Veränderungen dann eventuell über die Datenzugriffslogik in die Datenbank. Diese Trennung hat zur Folge, dass nun verschiedene Applikationen innerhalb des Unternehmens die gleichen Domänenobjekte nutzen können. Für unser Beispielprojekt FilmRegal könnte eine Architektur nach dem Schichtenmodell grob so aussehen, wie in Abbildung 4.16 gezeigt. Präsentation
Präsentation
Präsentation
Anwendung Videosortiment
Anwendung Video Auslieferung
Anwendung Nutzerverwaltung
Domänenobjekt Ausleihvertrag Domänenobjekt Film
Domänenobjekt Nutzer
Datenzugriffslogik
Datenbank
Domänenobjekt Nutzerkonto
Datenzugriffslogik
Datenbank
Abbildung 4.16: Beispiel für ein Schichtenmodell
155
Kapitel 4 Mit wie vielen Schichten man nun letztendlich arbeitet, hängt auch von der Komplexität der zu erstellenden Anwendung ab.
4.3.2 MVC Manch ein Leser mag sich wundern, warum MVC bei den Architekturmustern steht und nicht bei den noch kommenden Entwurfsmustern. MVC, also Model-View-Controller, ist ein Architekturmuster, denn es beschreibt den allgemeinen Aufbau einer Anwendung, ohne auf konkrete Klassen- oder Objektstrukturen einzugehen. Es besagt, dass man eine Anwendung in die drei grundsätzlichen Teile Model, View und Controller zerteilt. Das Model enthält die Domänenobjekte samt der Datenhaltung. Die View stellt das User-Interface dar, und der Controller reagiert auf Nutzereingaben und löst geeignete Aktionen im Model aus (siehe Abbildung 4.17).
Model
View
Controller
Abbildung 4.17: Das klassische MVC-Modell
Zunächst fällt uns auf, dass es nur einen Teil für das Model gibt. MVC unterteilt dies grundsätzlich nicht weiter. Aber in der Praxis ist es zumeist sinnvoll, weitere Unterteilungen zu unternehmen. Das Model könnte, ähnlich wie beim oben beschriebenen, erweiterten Schichtenmodell, in die drei Teile Domänenobjekte, Datenzugriffslogik und Datenhaltung unterteilt werden. Zusammengefasst stellen diese drei Teile dann das Model dar. Dass diese Unterteilung verlangt wird, kann man an den konkreten Architektur-Frameworks sehen, die momentan in der Flash- und Flex-Entwicklung populär sind. Dort wir über entsprechende Strukturen (z. B. Trennung in Proxies und Value-Objekte) eine Aufteilung der Datenobjekte und der Klassen vorgenommen, die auf die Daten auf dem Server zugreifen. Wenn sich im Model Daten oder Zustände ändern, werden Events geworfen, die sowohl von Controllern als auch von der View ausgewertet werden können. MVC verwendet als zentralen Bestandteil das Beobachter-Entwurfsmuster.
156
Entwurfswerkzeuge In der View wird nur die reine Darstellung von Daten und Bedienelementen vorgenommen. Die Trennung der View vom Model ist die Haupterrungenschaft, die MVC mit sich gebracht hat. Früher wurden in Anwendungen oft Teile der Applikationslogik direkt in die View integriert, was dazu führte, dass auf die gleiche Anwendung nicht einfach eine neue Benutzeroberfläche gelegt werden konnte, weil dadurch die Applikationslogik auch neu entwickelt werden musste. Die Trennung über das Beobachter-Muster schafft zudem eine besonders lose Kopplung, zumindest in der Richtung vom Model zur View. Das ursprüngliche MVC-Konzept sieht vor, dass eine View durchaus auf die für sie relevanten ModelDaten zugreifen darf. In heutigen praktischen Umsetzungen werden die Daten aber meist mit den Events mitgegeben, oder die Kopplung zwischen Model und View wird über Databinding realisiert, bei dem letztlich auch Daten über Events geliefert werden. Führt der Nutzer eine Aktion auf der Benutzeroberfläche aus, so erfährt dies in der ursprünglichen Idee nur direkt der Controller. Das ist in Flash so gar nicht möglich, weil in Flash Nutzereingaben grundsätzlich durch die grafischen Objekte eingefangen werden, auf denen die Interaktion stattfindet. In Flash fungieren die grafischen Objekte im Prinzip sowohl als View als auch als Mini-Controller. Wir weichen also vom Ursprungskonzept leicht ab. In Flash reicht die View eine Aktion vom Nutzer direkt an einen Controller weiter, ohne die Aktion weiter zu beachten. Auch das stimmt in der Praxis nicht ganz. Nutzeraktionen, die keinen Einfluss auf das Model haben, werden meistens direkt innerhalb der View bearbeitet. Wenn z. B. ein Nutzer ein DropDown auf- und wieder zumacht, dann hat er keine Daten verändert, demzufolge muss auch nicht zwingend ein Controller in Kenntnis gesetzt werden. Überhaupt muss gesagt werden, dass MVC keine expliziten Aussagen über den eigentlichen Aufbau der View macht. In der Praxis findet eine mehr oder minder starke Schachtelung der View statt. Views bestehen teilweise wieder aus kleineren Views, verwenden in sich wiederum generische UI-Komponenten usw. Gerade bei hochinteraktiven und animierten Benutzeroberflächen spielt sich in den Views sehr viel ab, wovon eventuell der Rest der Anwendung nichts mitbekommt. Zum Beispiel muss es eine Anwendung nicht unbedingt interessieren, ob ein Video gerade seit fünf oder seit zehn Sekunden spielt, wenn dies keinen Einfluss auf die Applikation an sich hat. Jeder View.Komponente steht ein dedizierte Controller zur Seite, der auf die Eingaben vom Nutzer reagiert. Dieser weiß dann, welche Methoden im Model aufzurufen sind. Die Zuweisung eines Controllers zu einer View erfolgt über das Strategiepattern, was ich später noch erläutern werde. Das hat zur Folge, dass der Controller für eine View auch noch zur Laufzeit einfach ausgetauscht werden kann. In heutigen Frameworks sind auch diese Zugriffe über Events noch stärker entkoppelt. Da natürlich auch Events zur Laufzeit abgemeldet und neu angemeldet werden können, ist es auch hier möglich, Views mit jeweils unterschiedlichen Controllern zu verdrahten. Diese Möglichkeit ist deshalb interessant, weil dadurch für eine View unterschiedliches Verhalten verknüpft werden kann. Ein Klick auf einen Login-Button könnte z. B. in dem Fall, in dem in den Feldern für Nutzername und Passwort noch nichts eingetragen wurde, ein anderes Verhalten auslösen, als wenn tatsächlich Eingaben gemacht wurden.
157
Kapitel 4 Ein Controller ruft Methoden auf dem Model auf, um die vom Nutzer gemachten Eingaben weiterzugeben. Das kann auch nur ein Klick auf einen Button sein. Solange die Nutzereingabe einen Einfluss auf das Model hat, soll der Controller sie auch weitergeben. Im ursprünglichen Konzept ruft der Controller die Methoden des Models direkt auf. Auch hier haben wir heute oft eine stärkere Entkopplung durch Events. MVC ist bereits eine ältere Idee, sie wurde 1979 von einem Mitarbeiter bei Xerox PARC zum ersten Mal dokumentiert. MVC sagt nichts darüber aus, wie MVC-Architekturen verteilt sind. Es wird noch nicht einmal klar, welche Teile auf dem Server und welche auf dem Client laufen sollen. Hier kommen die Begriffe Thin-Client und Rich-Client ins Spiel. Ein ThinClient ist einer, der nur die reine Darstellung übernimmt und selbst keinerlei Applikationslogik enthält. In Webarchitekturen, die nach dem MVC-Prinzip aufgebaut sind und ThinClients verwenden, hat man z. B. meist eine recht simple HTML-Oberfläche, die jedwede Nutzerinteraktion direkt an den Server schickt. Dort gibt es dann einen oder mehrere Controller, die auf die Aktionen reagieren und entsprechende Logik in Komponenten aufrufen, die dem Model zugerechnet werden. Von einem Rich-Client spricht man, wenn Letzterer nicht bei jeder Aktion den Server aufruft, sondern manche Aktionen auch eigenständig durchführt. Flash ist prädestiniert für Rich-Clients, weil es persistent ist, sprich, ein Request an einen Server setzt die Benutzeroberfläche nicht zurück. Flash kann Daten über die gesamte Usersession hinweg im Speicher halten. Das macht einen Rich-Client erst möglich. Rich-Clients können auch den kompletten Controller-Teil in sich tragen. Auf dem Server liegt dann also nicht mehr der Controller, sondern Schnittstellen, die die direkte Manipulation des Models erlauben. Trotzdem ist es immer noch MVC. Eine weitere Möglichkeit gibt es. Im Zusammenhang von modularen Applikationen wird MVC nicht als übergreifende Architektur, sondern als Mikroarchitektur innerhalb jedes einzelnen Moduls verwendet. Die Anwendung baut sich hier also aus Modulen zusammen, und innerhalb eines jeden Moduls befindet sich eine kleine MVC-Struktur. Jedes Modul hat sein eigenes Model, seine eigenen Views und seine eigenen Controller. Auch diese können wieder auf dem Client und Server verteilt sein, aber sie sind trotzdem in sich geschlossen. MVC bringt natürlich einige Probleme mit sich, weswegen es ja auch schon lange nicht mehr in seiner ursprünglichen Form verwendet wird, ich habe das schon an einigen Stellen angedeutet. Über die Zeit haben sich abgewandelte Formen entwickelt, die bestimmten Anforderungen Rechnung tragen. Zum Beispiel besteht das Problem, dass bestimmte Daten zwar im User-Interface angezeigt werden müssen, man sie aber nicht ins Model packen kann, weil sie eigentlich mit den reinen Daten nichts zu tun haben. Style-Informationen zum Beispiel, also Schrifttyp, Farben, Rasterabstände usw., sind Daten, die nur für die View interessant sind, die aber möglichst nicht direkt in der View abgelegt werden sollten, wenn man diese Daten einfach ändern können will. Auch manche Beschriftungen wie z. B. »Layer schliessen« sind Texte, die eigentlich nicht Teil des Models sind, denn sie sind spezifisch zur View, die eingesetzt wird. In einer anderen View ist der Layer vielleicht gar kein Layer, sondern eingebettet in den eigentlichen Screen. Zu diesem Zweck wurde das PresentationModel erdacht und von Martin Fowler beschrieben (Martin Fowler 19.07.2004). Es ist ein Hilfskonstrukt, welches das normale Model ergänzt um Daten, die spezifisch für die Views sind. Abbildung 4.18 zeigt den grundsätzlichen Aufbau.
158
Entwurfswerkzeuge
PresentationModel
Model
View
Controller
Abbildung 4.18: MVC, erweitert um ein PresentationModel
In einem PresentationModel wird nun der gesamte Status einer View gespeichert. Damit wird die View deutlich entschlackt. Das ermöglicht nebenbei auch bessere Unit-Tests, denn Views an sich sind schwer bis gar nicht über Unit-Tests testbar, aber ein PresentationModel durchaus. Martin Fowler erwähnt es selbst nicht, aber ich finde es nützlich, das PresentationModel auch für die Organisation der Daten heranzuziehen, die zwar statisch sind, aber dennoch view-spezifisch, wie z. B. Labeltexte, Farben usw. Zu diesem Zweck könnte sich das PresentationModel aus entsprechenden Datenquellen (z. B. XML-Dateien) die entsprechenden Informationen beschaffen. Nun stellt sich die Frage, ob MVC für jede Anwendung geeignet ist. Kann man MVC für ein Flash-Spiel genauso einsetzen wie für einen Videoplayer oder eine Nutzerregistrierung? Was ist mit Anwendungen, die viel mit Animationen oder Echtzeitvisualisierungen arbeiten oder die hauptsächlich Content präsentieren und kaum Geschäftslogik besitzen? Der Schlüssel zu diesen Fragen liegt in der Betrachtungsweise des Models. Das Model stellt die Daten und Datenlogik der Anwendung dar. Im Falle einer Anwendung, die Geschäftsprozesse eines Unternehmens abbildet, werden im Model diese Geschäftsmodelle abgebildet. Wenn eine Anwendung Content präsentiert, dann ist eben dieser Content das Model. Im Fall eines Spiels sind die Charaktere und die Elemente der Spielwelt das Model. Beim Videoplayer ist das Video mit seinen Metadaten das Model. Wir können den Begriff Model also auf unterschiedlichste Arten von Daten und Objekten anwenden. Was aber ist mit Anwendungen, die sehr stark animierte und sich ständig in Bewegung befindliche Views haben, wie z. B. in einem Spiel? Auch hier passt die Struktur. Eine sich in Echtzeit verändernde Spielwelt ist letztlich eine Repräsentanz ihres Models. Sobald sich das Model verändert, verändert sich die View mit. Dies kann auch in Echtzeit passieren. Wichtig ist hier natürlich nach wie vor, was zum Model gehört und was zur View bzw. zum PresentationModel. In einem 3D Shooter wäre z. B. die Blickrichtung eines Charakters eher eine Information, die im PresentationModel abgelegt würde, weil die Blickrichtung normaler-
159
Kapitel 4 weise keinen Einfluss auf die Spielwelt an sich hat. Die Position des Spielers sowie andere Parameter wie Laufrichtung usw. wiederum würden im normalen Model abgelegt, weil sie sehr wohl einen Einfluss auf die Spielwelt haben. In diesem Zusammenhang spricht man auch vom statusbehafteten Model und der statusbefreiten View. Die View selbst soll keinen eigenen Status besitzen, sondern immer nur den Status des Models bzw. des PresentationModels repräsentieren.
4.3.3 Komponentenarchitektur Die Einführung von Komponenten in einer Softwarearchitektur ist ein logischer Schritt, wenn man eine modulare Struktur aufbauen will. Mit Komponente ist hier nicht eine visuelle Komponente gemeint. Eine Komponente steht hier für ein in sich geschlossenes Modul, das eine bestimmte Verantwortung trägt. Im Serverbereich sind Komponentenarchitekturen weit verbreitet. Enterprise Java Beans zum Beispiel sind Serverkomponenten. In einer Komponentenarchitektur hat man üblicherweise einen Komponentencontainer, der die Komponenten trägt und verwaltet. Er kümmert sich um Speichermanagement, um Parallelität, Sicherheitsmechanismen und vieles mehr. Die Komponenten können sich derweil auf die fachlichen Belange konzentrieren. Man kann drei Typen von Komponenten unterscheiden. Datenkomponenten, auch Entitätskomponenten genannt, Sessionskomponenten und Servicekomponenten. Datenkomponenten halten Informationen so lange, wie die Serveranwendung läuft. Sie greifen meist auch auf Datenbanken und andere Datenquellen zu. Sessionskomponenten leben für die Dauer einer Nutzersession. Servicekomponenten arbeiten typischerweise genau einen Aufruf ab und werden danach wieder beendet. In Richtung des Frontends werden entweder dynamische HTML-Seiten wie z. B. Java Server Pages oder Active Server Pages bereitgestellt oder auch simple XML-Schnittstellen. Bei Letzteren wird ganz simpel ein Request per GET oder POST an ein Servlet oder eine vergleichbare Serveranwendung gestellt, und diese schickt eine Antwort in Form von XML. Daneben gibt es gerade auch für Flash proprietäre Alternativen, z. B. über Remote Procedure Calls über Lösungen wie z. B. die OpenSource-Plattform Blaze DS von Adobe. Hierbei werden direkt von Flash heraus Objekte angesprochen, die sich auf dem Server befinden. Die Remoting-Schnittstelle transformiert diese Aufrufe in ein binäres Format und übernimmt den Nachrichtenaustausch zwischen dem Flash-Client und der Serverschnittstelle. Die meisten Server-Komponenten-Plattformen sind in der Lage, solche Frontend-Schnittstellen zu integrieren. Es muss dabei natürlich bedacht werden, dass man sich mit einem proprietären Format, wie z. B. der Remoting-Schnittstelle von Blaze DS, abhängig macht und dass diese Schnittstelle zunächst einmal nur Flash verwenden kann. Auf der anderen Seite erleichtert die Remoting-Schnittstelle die Entwicklung deutlich und trägt auch durchaus zu einer höheren Performance bei der Übertragung bei, weil hier komprimierte Binärdaten statt aufgeplusterte XML-Textdaten versendet werden. Der Vorteil der Komponentenarchitektur ist die Verringerung von Komplexität auf der Serverseite. Im Gegensatz zu einem offenen reinen objektorientierten Modell sind Komponen-
160
Entwurfswerkzeuge ten Blackboxes, deren Inneres versteckt wird und die nur über ihre öffentliche Schnittstelle zugänglich sind. Hier wird also eine Abstraktionsebene eingezogen, die das Arbeiten von größeren Codeteilen erleichtern soll. Auch für das Frontend bietet das Vorteile. Denn auch für das Frontend ist es übersichtlicher, mit einer überschaubaren Zahl von Komponenten zu kommunizieren als mit vielen einzelnen Klassen. Eine Komponentenarchitektur kann übrigens ohne Weiteres auch wieder MVC implementieren. Diese beiden Ansätze schließen sich also nicht aus, sondern ergänzen sich sogar, denn man kann seine Komponenten natürlich sehr gut dem Model- oder dem Controllerbereich zuordnen. Andererseits kann es auch hier wieder Sinn machen, MVC innerhalb einer Komponente anzulegen und nicht als übergreifendes Konzept anzuwenden.
4.3.4 Webservices Die Idee von Webservices ist primär, einen Dienst nicht unbedingt nur im Rahmen einer konkreten Applikation anzubieten, sondern im Gegenteil losgelöst von einer bestimmten Applikation. Der Wunsch hierbei ist, eine stärkere Wiederverwendung von Funktionalität zu erreichen. In einer Architektur, die keine Services einsetzt, werden die Client- und Serverkomponenten meistens direkt auf den Einsatz innerhalb einer bestimmten Anwendung hin ausgerichtet. Bei unserem Beispielprojekt FilmRegal könnte es z. B. eine Serverkomponente geben, die auf einer Übersichtsseite im Profilbereich die aktuellsten ausleihbaren Filmtitel ausgibt. Diese Komponente wäre recht stark an ihren Einsatzzweck gebunden. Ein Webservice hingegen führt nicht einfach eine bestimmte Aktion in einem bestimmten Kontext aus, er stellt vielmehr ganz allgemein einen Dienst zur Verfügung, der in möglichst vielen unterschiedlichen Kontexten verwendet werden kann. Dazu kommt, dass dieser Dienst dann auch in einer möglichst standardisierten Form zur Verfügung gestellt wird, damit eine Vielzahl anderer Systeme diesen Dienst benutzen kann. Hier wird also möglichst auf proprietäre Formate verzichtet. Webservices werden deswegen meistens unter Verwendung eines XML-basierten Austauschformats angeboten, z. B. SOAP (stand früher für Simple Object Access Protocol, wird heute nicht mehr als Abkürzung benutzt). Der Webservice selber wird definiert über WSDL (Web Service Description Language). WSDL beschreibt also, was ein Webservice kann, und SOAP ist das Protokoll, über das man mit dem Webservice kommuniziert. Auch Flex unterstützt die Kommunikation via SOAP und kennt auch WSDL zur Definition eines Webservice. Ein Webservice sollte bestimmte Eigenschaften aufweisen, um gut und nützlich zu sein. In Rosen et al. 2008 werden unter anderen folgende Eigenschaften als wichtig herausgestellt:
Modularität Kapselung Lose Kopplung Konzentration auf eine Aufgabe pro Service Autonomie gegenüber ihren Nutzern Wiederverwendbarkeit
161
Kapitel 4
Selbstbeschreibende Schnittstelle Verknüpfbar mit anderen Services Programmiersprachen- und plattformunabhängig Rosen et al. erwähnen auch noch die Eigenschaften Statusbefreitheit und die Möglichkeit zum dynamischen Finden und Binden an einen Service. Ein Webservice soll statusbefreit sein, was bedeutet, dass er kein Gedächtnis hat. Er soll keine Kenntnis behalten von vergangenen Aufrufen oder kommenden Aufrufen. Rosen et al. machen aber auch klar, dass dies in der Praxis nicht immer durchzuhalten ist. In vielen Fällen wird nun mal eine Session benötigt, die Informationen über den Nutzer enthält, und wenn es nur der Login-Status ist. Beim dynamischen Finden und Binden an einen Webservice handelt es sich um die Idee, dass neben den Webservices ein öffentliches Verzeichnis gepflegt wird, in dem alle verfügbaren Webservices eingetragen sind. Eine Anwendung, die nun einen Service benötigt, soll nun automatisch (also ohne Eingreifen eines Entwicklers) nach Services in diesem Verzeichnis suchen und die für sie passenden wählen können, um sich danach direkt dynamisch mit ihnen zu verbinden. In der Praxis erscheint zumindest das dynamische Finden als eine schwierige Aufgabe, weil man die Entscheidung darüber, den einen oder anderen Webservice zu verwenden, eher nicht einem Computerprogramm überlassen möchte. Sowohl SOAP als auch WSDL sind große Formate. SOAP XML-Nachrichten bergen eine Menge an Metainformationen im Verhältnis zum Netto-Datengehalt. Das kann in einer Anwendung durchaus einen Einfluss auf die Performance haben. Der Einsatz von Webservices sollte gut bedacht werden. Die heutigen Entwicklungstools erlauben zwar eine teilweise sehr einfache Einrichtung und Konfiguration solcher Webservices, aber die Frage, die man sich stellen sollte, ist: Soll der zu erstellende Dienst ein allgemeingültiger sein, der außer in meiner auch noch in anderen Anwendungen genutzt werden können soll? Die Auswirkungen dieser Entscheidung betreffen insbesondere die Betreuung eines solchen Service. Wenn man einen Dienst nur innerhalb einer Anwendung baut, dann bedeutet dies auch, dass man sicher sein kann, dass es auch nur zwischen der eigenen Anwendung und dem Dienst eine Abhängigkeit gibt. Man ist freier in der Entscheidung, etwas an dem Dienst zu ändern, wenn es der eigenen Anwendung dient. Bei einem Webservice, der auch von anderen Anwendungen genutzt wird, herrschen plötzlich Abhängigkeiten zu anderen Anwendungen. Man kann nun den Dienst nicht mehr einfach ändern, wenn die eigene Anwendung dies erforderlich machen würde, denn nun benutzen ja andere Anwendungen auch schon diesen Dienst und laufen Gefahr, nicht mehr zu funktionieren. Es ist plötzlich sehr viel mehr Abstimmung und Koordination erforderlich, meistens geht dies einher mit deutlich erhöhtem Dokumentations- und Wartungsaufwand. Und für den Fall, dass man nicht erwartet, dass ein Webservice auch von anderen Anwendungen genutzt wird, sei gesagt, dass die Entwicklung und Betreuung einer simplen Schnittstelle über XML oder Remoting im Regelfall einfacher ist als das Aufsetzen eines Webservice und normalerweise auch performanter.
162
Entwurfswerkzeuge
4.4 Entwurfsmuster Entwurfsmuster beschreiben im Unterschied zu Architekturmustern Strukturen im Inneren einer Anwendung. Sie zeigen Taktiken auf, um immer wiederkehrende Probleme bei der Erzeugung von lose gekoppelten, erweiter- und veränderbaren Klassenstrukturen zu lösen. Im Detail beschreiben Entwurfsmuster, wie Objekte möglichst flexibel und effizient zusammenarbeiten können. Es geht also hier fast immer um die Beziehung zwischen Objekten bzw. Klassen. Auch das ist ein Unterschied zu Architekturmustern, bei denen es weniger um konkrete Klassen als mehr um Module, Komponenten und Subsysteme geht. In mancher Hinsicht, muss man aber sagen, unterscheiden sich die Ideen, die auf Modulebene zum Tragen kommen, nicht zwingend von denen, die auf Objekt- bzw. Klassenebene von Bedeutung sind. Letztlich wollen sie alle das Gleiche: Software, die wartbar, erweiterbar und wiederverwendbar ist. Entwurfsmuster zeigen Strukturen auf, die den Prinzipien der Objektorientierung sehr treu sind. Man muss es hier der Vollständigkeit halber erwähnen, alle hier vorgestellten Entwurfsmuster basieren auf der Objektorientierung. Das ist ja nicht selbstverständlich, auch in der strukturierten Programmierung gibt es Muster, über die wollen wir hier aber nicht weiter sprechen. Ein Entwurfsmuster zeichnet sich dadurch aus, dass es für eine Familie von Problemen eine exemplarische Lösung in derart abstrakter Form skizziert, dass diese Lösung möglichst auf alle konkreten Probleme in der Familie anwendbar ist. Das klingt vielleicht noch etwas zu hölzern. Machen wir das kurz an einem konkreten Muster fest: Eines der einfachsten Muster ist das Singleton-Muster (von bösen Zungen auch als Antimuster bezeichnet, aber dazu später mehr). Kurz und bündig gesagt: Ein Singleton ist eine Klasse, die dafür sorgt, dass von ihr nur eine einzige Instanz global zur Verfügung gestellt wird. Das ist nun schon die Lösung. Aber was ist die Familie der Probleme dahinter? Es gibt Situationen, in denen sichergestellt sein soll, dass man von einer Klasse nur eine Instanz innerhalb einer der Laufzeit einer Anwendung hat. Ein Client innerhalb einer Webanwendung z. B. hat normalerweise nur eine Session, nämlich die für den Nutzer, der den Client gerade verwendet. Der Server hat natürlich mehrere, eine pro Client, aber jeder Client hat eben nur seine eine. Eine Flash-Anwendung hat normalerweise eine Hauptklasse, die die Wurzel der gesamten Anwendung darstellt. Von dieser Klasse darf es zur Laufzeit auch nur eine Instanz geben, etwas anderes würde keinen Sinn ergeben. Und auch noch ein Gegenbeispiel: Ein Videoplayer spielt immer nur genau ein Video zur gleichen Zeit ab. Es kann aber mehrere Videoplayer gleichzeitig geben, und auch wenn jeder von ihnen nur ein Video abspielt, bedeutet das aber letztendlich, dass innerhalb einer Anwendung mehrere Videos gleichzeitig existieren können. Die Familie des Problems ist also eine Situation, in der es von einem Ding nur ein Exemplar geben darf, und das Singleton-Muster bietet hierfür eine Lösung an. Zusätzlich zur Problemfamilie und der Lösung beschreibt man für ein Entwurfsmuster auch immer noch Vor- und Nachteile oder auch Konsequenzen, wie Gamma et al. es nennen. Denn der Einsatz eines Musters bringt nicht immer nur Vorteile mit sich, er kann auch
163
Kapitel 4 in manchen Fällen neue Probleme heraufbeschwören. Zum Beispiel löst ein Singleton zwar das Problem, nur eine Instanz von einer Klasse zu erlauben, bringt aber das Problem des nun implizit globalen erreichbaren Status mit sich, was eigentlich gegen das Prinzip der Kapselung läuft. Der Einsatz eines Entwurfsmusters ist also immer auch eine Abwägungssache. Deswegen sollte man nicht blind alle möglichen Muster in seine Anwendung integrieren. Vielmehr sollte man sich zuerst die Frage stellen, welches Problem man eigentlich lösen will. Habe ich überhaupt ein Problem? Wenn Sie diese Frage nicht konkret beantworten können und eher gefühlsmäßig denken, Sie möchten ein Muster verwenden, könnte das ein Indiz dafür sein, dass Sie an dieser Stelle kein Muster brauchen. Wenn Sie hingegen feststellen, dass bestimmte Klassen nicht sauber genug voneinander entkoppelt sind, oder Sie bemerken, dass viele Ihrer Klassen eng in Abhängigkeit von anderen stehen, dann könnte ein Blick in den Katalog der Entwurfsmuster helfen, eine bessere Struktur zu finden und so mehr Ordnung zu schaffen. Um den Umfang dieses Buchs erträglich zu halten, kann ich nicht alle bekannten Entwurfsmuster behandeln und auch nicht die behandelten Muster in der Tiefe beschreiben, wie es andere Bücher tun. Ich habe eine Auswahl an Mustern herausgegriffen, die Flash- und FlexEntwickler meiner Ansicht nach häufig benötigen. Für eine detailliertere Diskussion der Entwurfsmuster empfehle ich zum einen den Klassiker der sogenannten Gang of Four (Gamma, Riehle 2008). Eine gute Diskussion einer Auswahl dieser Muster aus Flash-Sicht liefert auch »ActionScript 3.0 design patterns« (Sanders, Cumaranatunge 2007).
4.4.1 Erzeugungsmuster Erzeugungsmuster sind solche, die bei der Instanziierung von Objekten und dem Bau von Objektstrukturen helfen. Oft hat man den Fall, dass eine Klasse, nennen wir sie Klient, ein Objekt oder eine Struktur von Objekten benötigt. Die Erzeugungsmuster unterstützen den Klienten dabei, Objekte und Objektstrukturen zu erzeugen, ohne dass der Klient die genaue Herkunft bzw. die detaillierte Art der Objekte kennen muss. Gerade in Situationen, in denen ein Klient über ein Interface einen Bedarf für ein Objekt angemeldet hat und eine andere Klasse diesen Bedarf decken will, indem sie das Interface implementiert, müssen die beiden Partner ja irgendwie zusammengebracht werden. Der Klient soll möglichst keine direkte Kenntnis von der anderen konkreten Klasse haben, denn sonst würde er sich von ihr abhängig machen und könnte nicht einfach einen anderen Kandidaten verwenden, der vielleicht auch das Interface implementiert und besser zu ihm passt. Wir brauchen also ein Konstrukt, das quasi als Vermittler auftritt und die konkrete Erzeugung übernimmt. Erzeugungsmuster werden in diesem Zusammenhang z. B. für Parser oder Initialisierungen benötigt. Wenn der Parse- bzw. Initialisierungsvorgang getrennt werden soll von der Erzeugung der konkreten Objekte, damit man die Typen der Objekte auch noch später ändern kann, dann kommen die Erzeugungsmuster zum Einsatz, wie z. B. das Erbauer-Muster.
164
Entwurfswerkzeuge Gamma et al. beschreiben fünf Erzeugungsmuster (in Klammern jeweils ihre englischen Namen):
Abstrakte Fabrik (Abstract Factory) Erbauer (Builder) Fabrikmethode (Factory Method) Prototyp (Prototype) Singleton (Singleton) Von diesen fünf werde ich Erbauer, Fabrikmethode und Singleton beschreiben.
Erbauer (Builder) Parser bauen wir ständig in unterschiedlichsten Formen. Entweder parsen wir eine XMLDatei, oder wir müssen eine schon bestehende Objektstruktur umwandeln in eine andere. In diesem Parsing-Vorgang stecken zwei Aufgaben. Der Parser muss zunächst mal die Quellstruktur kennen, also zum Beispiel den Aufbau der XML-Datei. Das ist die eigentliche Verantwortlichkeit des Parsers. Die andere Verantwortlichkeit ist, aus der Quellstruktur die Zielstruktur zu erzeugen, also die Objektstruktur, die z. B. aus der XML-Datei erzeugt werden soll. Das wäre die Verantwortlichkeit eines Erzeugers oder auch Erbauers. Es ist nützlich, diese beiden Verantwortlichkeiten voneinander zu trennen. Das hat zwei Vorteile. Zum einen könnte sich die Quellstruktur ändern. Die XML-Datei könnte plötzlich anders aussehen, z. B. andere Tagnamen verwenden. Wenn sich die Quellstruktur ändert, heißt das ja noch nicht, dass sich auch die Zielstruktur verändern soll. Schließlich soll unsere Anwendung ja noch genauso funktionieren wie vorher. Wenn wir die beiden Verantwortlichkeiten sauber getrennt haben, bedeutet eine Änderung der Quellstruktur, also der XML-Datei, zwar immer noch genug Arbeit, denn wir müssen den Parser anpassen, aber zumindest müssen wir sonst nichts mehr ändern. Zum anderen könnte sich auch die Anforderung hinsichtlich der Zielstruktur ändern. Eventuell brauchen wir plötzlich eine leicht andere Objektstruktur als vorher. Eventuell haben wir bestimmte Attribute in der XML-Datei zuvor ignoriert und brauchen sie aber nun. Wenn wir Parser und Erbauer sauber getrennt haben, müssen wir jetzt nur den Erbauer ändern oder im einfachsten Fall einfach nur einen neuen Erbauer einklinken. Die Erkenntnis, dass ein Parsing-Vorgang aus dem Parsen und dem Bauen besteht, ist also grundsätzlich schon mal viel wert. Wie setzt man das nun aber in der Praxis um? Da hilft das Erbauer-Muster. Schauen wir uns zunächst die Struktur im Diagramm in Abbildung 4.19 an. Der Parser, der im Erbauer-Muster offiziell Direktor genannt wird, ist der eine Teil unseres Zweiergespanns, und der KonkreterErbauer ist der zweite Teil. Da wir den Parser ja nicht konkret an einen bestimmten Erbauer binden wollen – dann hätten wir zwar immer noch eine Trennung der Verantwortlichkeiten, aber eine leider stark gekoppelte –, setzen wir ein Interface dazwischen, das Erbauer-Interface. Dieses Interface ist ganz konkret an den Parser gebunden. Sprich, der Parser wird in seiner parse-Methode konkret die Methode baueTeil()
165
Kapitel 4
«interface» Erbauer
Parser (Direktor) + +
parse() : void Parser(Erbauer) : void
+
Klient
baueTeil() : void
KonkreterErbauer +
Produkt
baueTeil() : void
«property get» + produkt() : void
Abbildung 4.19: Die Struktur des Erbauer-Musters
von Erbauer verwenden. Der Klient wiederum weiß, welchen konkreten Erbauer er denn verwenden will, und übergibt diesen an den Parser. Der Parser hat nun also seinen Erbauer und kann mit der Arbeit beginnen. Ich habe hier jetzt im Erbauer nur eine Methode baueTeil() vorgesehen, aber es sei gesagt, dass Erbauer für jede Art von Objekt, das erzeugt werden muss, eigene Methoden haben wird. In meinem Beispiel ist es nur ein Objekt, nämlich Produkt. In einer XML-Struktur kann das natürlich aber eine Vielzahl von Objekten sein, die eventuell ineinander verschachtelt sind. Das ist dann Aufgabe des Erbauers, diese unterschiedlichen Objekte zu erzeugen. Dabei wird der Parser durch die XML-Struktur (oder welche Quellstruktur auch immer) gehen. Immer wenn er auf einen bestimmten Knotentyp stößt, ruft er die hierzu passende Methode des Erbauer-Interface auf. Jetzt könnte man einwenden, dass ja durch die konkreten Methoden im Interface Erbauer schon eine Kopplung zwischen der Quellstruktur und der Zielstruktur besteht. Um diesen Gedanken aus der Welt zu schaffen, müssen wir uns klarmachen, dass das Interface Erbauer erst einmal grundsätzlich für jeden Elementtyp in der Quellstruktur eine Erzeuger-Methode bereitstellen muss, damit grundsätzlich alle Möglichkeiten abgedeckt sind. Das Interface ist insofern stark an den Parser gebunden, was aber auch o. k. ist, denn das Interface stellt praktisch die maximale Vielfalt an Elementtypen dar, die der Parser erkennen kann. Wie nun aber ein konkreter Erbauer das Interface implementiert, welche konkreten Objekte er also erzeugt, bleibt ihm überlassen. Nehmen wir als Beispiel mal einen RSS Feed als Datenquelle. Ein Parser würde den strukturellen Aufbau des RSS Feeds kennen, und das Erbauer-Interface würde alle fachlichen Elemente eines Blogs kennen. Die fachlichen Elemente im Interface sind schon von den konkreten Tags im RSS Feed abstrahiert, deswegen ist es auch möglich, statt eines RSS Feed-Parsers einen Atom-Parser drunter zu setzen, die fachlichen Elemente im Interface könnten dabei gleich bleiben.
166
Entwurfswerkzeuge Nun könnte man unterschiedliche konkrete Erbauer bauen, die jeweils unterschiedliche Dinge aus den Elementen bauen, die sie vom Parser erhalten. Ein Erbauer könnte z. B. sich nur für die Überschriften und die Datumsangaben interessieren und daraus eine Übersichtsliste bauen, ein anderer konkreter Erbauer könnte sich für die Autoren und die Datumsangaben interessieren, um daraus Statistiken zu bauen, welche Autoren wie viele Beiträge geschrieben haben, und ein Dritter interessiert sich vielleicht nur für die Tags, um eine Tag-Cloud zu erzeugen. Sowohl auf Parserebene lässt sich ein Austausch bzw. eine Veränderung vornehmen als auch auf Erbauerebene, denn beide sind durch das Interface voneinander getrennt.
Fabrikmethode (Factory Method) Das Muster für die Fabrikmethode beschreibt den klassischen Fall von Auslagern von Verantwortung. Statt dass ein Klient selbst eine Instanz einer bestimmten Klasse erzeugt, verlangt der Klient eine Instanz eines Interface (also die einer konkreten Implementierung des Interface natürlich) und überlässt es einer anderen Klasse, die über ein Factory-Methode verfügt, die konkrete Implementierung zu wählen und zurückzugeben. Das Problem, dass jemand letztendlich doch konkrete Kenntnis von den Klassen haben muss, die das Interface implementieren, wird dadurch zwar nicht gelöst, aber es wird aus dem Klienten entfernt und in eine neutrale, eine technische Klasse ausgelagert. Dieses Verfahren wird in der Objektorientierung oft angewendet. Man erschafft eine eher technisch anmutende Klasse, die von den fachlichen Klassen losgelöst ist, und überträgt ihr die Aufgabe zur losen Kopplung von eher fachlichen Klassen, die selbst nicht gekoppelt sein sollen. Die Annahme dabei ist, dass sich an einer technischen Hilfsklasse seltener etwas ändert, wenn sie einmal korrekt aufgesetzt wurde, als das bei den fachlichen Klassen der Fall ist. So ist das auch bei der Klasse, die die Fabrikmethode enthält. Sie besitzt ja nun eine starke Kopplung zu den Klassen, die das besagte Interface implementieren, aber das ist nicht so schlimm, wie eine starke Kopplung zwischen einem Klienten und den anderen Klassen. Diese sind fachlich und sollen deswegen entkoppelt sein. Von dem Muster der Fabrikmethode gibt es eine Anzahl von Varianten. Das Fabrikmethodenmuster ist zudem einem anderen Muster, der abstrakten Fabrik, recht ähnlich. Die offizielle Variante ist in Abbildung 4.20 zu sehen. Man kann erkennen, dass es in dieser Variante immer eine Parallelität zwischen abstrakter bzw. konkreter Fabrik bzw. Produkt gibt. Für jede konkrete Fabrik gäbe es also auch immer ein entsprechendes konkretes Produkt. Der Klient aber interessiert sich nur für die konkrete Fabrik. Er erwartet letztlich eine Instanz, die das Interface-Produkt implementiert. Welches konkrete Produkt das nun ist, ist für ihn irrelevant. In dieser Variante wird die Erweiterung an neuen Fabrik- bzw. Produkttypen jeweils anhand der Erstellung von zwei neuen Klassen gemacht. Für den Fall, dass diese Fabriken sich eigentlich nur in der liefereProdukt()-Methode unterscheiden, die ja jeweils eine unterschiedliche konkrete Instanz von Produkt zurückliefert, könnte man das Muster auch dementsprechend verändern, dass aus dem Interface Fabrik eine Hauptklasse wird, die schon
167
Kapitel 4 die Grundfunktionalität, die diese Fabrik sonst so braucht, beinhaltet, und die konkreten Fabriken würden dann davon erben. Der Effekt wäre für den Klienten gleich, aber man hätte mehr Code wiederverwendet.
«interface» Produkt
«interface» Fabrik +
KonkretesProdukt
liefereProdukt() : Produkt
KonkreteFabrik +
liefereProdukt() : Produkt
Klient -
meinProdukt: Produkt
Abbildung 4.20: Struktur des Musters der Fabrikmethode
Es gibt auch noch die Variante, dass man die unterschiedlichen konkreten Fabriken nicht durch unterschiedliche Klassen darstellt, sondern dass man nur eine Klasse hat – in diesem Fall wäre auch das Interface Fabrik nicht mehr unbedingt notwendig –, die wieder die Methode liefereProdukt() anbietet, diesmal aber mit einem Parameter. Außerdem könnte sie eine Liste öffentlicher Konstanten definieren, die für die Werte stehen, die man der liefereProdukt()-Methode als Parameter übergeben kann. Je nachdem, welchen Wert man übergibt, bekommt man ein anderes konkretes Produkt zurück. Diese Variante ist nicht so ganz sauber, weil zum einen hier eine Entkopplung über Konstanten, die letztlich wahrscheinlich Strings sind, vorgenommen wird. Solche Verfahren sind nicht mehr durch den Compiler überprüfbar, sondern allenfalls durch entsprechendes Exception-Handling in der liefereProdukt()-Methode zur Laufzeit. Zum anderen müsste in der liefereProdukt()-Methode nun ein switch-case-Konstrukt pro Produkttyp aufgebaut werden. Und immer, wenn neue Produkttypen hinzukämen oder sich vorhandene änderten, müsste die Methode verändert werden. Das geht letztlich gegen das sogenannte OpenClosed-Prinzip, das besagt, dass eine Methode immer offen für Erweiterung, aber geschlossen gegenüber direkter Veränderung sein soll. Das leuchtet insofern ein, denn eine Methode, die z. B. mittels Polymorphie erweitert wird, muss selbst nicht neu getestet werden. Eine Methode hingegen, die verändert wird, muss auch wieder neu getestet werden. Hier muss man einfach in der Praxis abwägen, wo man für sich den größeren Vorteil sieht.
Singleton (Singleton) Das Singleton-Muster scheint mit das einfachste Muster zu sein, sorgt aber mitunter für Kontroversen, die so weit gehen, dass manche Entwickler es schon als Anti-Muster bezeichnen. Bevor wir uns diesem Thema widmen, aber zunächst mal die Grundlagen. Das Single-
168
Entwurfswerkzeuge ton-Muster beschreibt eine Klasse, von der nur eine Instanz erzeugt werden kann. An diesem Vorhaben ist auch erst einmal nichts Verwerfliches zu finden. Situationen, in denen man von einer Klasse nur eine Instanz benötigt, gibt es recht oft. So oft, dass auch andere Entwurfsmuster intern ein Singleton benutzen, wie wir auch noch sehen werden. Eine Nutzersession zum Beispiel ist ein gutes Beispiel. Innerhalb einer Anwendung speichert man Daten, die für die Laufzeit der Anwendung in Bezug auf den Nutzer der Anwendung von Bedeutung sind, oft in einem sogenannten Sessionsobjekt. Dieses Objekt ist nur so lange aktiv, bis die Anwendung beendet wird. Zum Beispiel kann man dort speichern, ob der Nutzer sich schon eingeloggt hat, welches die letzten Seiten oder Screens waren, die er sich angesehen hat, oder was man auch immer an Informationen speichern können wollte. Der Knackpunkt ist, dass es in einer Anwendung, die auf einem Desktop läuft (oder in einem Browser, der auf einem Desktop läuft), per Definition fast immer nur einen Nutzer gibt. Ausnahmen sind öffentliche Computer, aber lassen wir die hier mal beiseite. Wenn wir also wissen, dass das so ist, dann wissen wir im Umkehrschluss auch, dass wir es mit einem Fehler zu tun hätten, wenn es in der Anwendung plötzlich zwei aktive Sessions gäbe (auf einem Server wiederum sieht das natürlich anders aus, deswegen spreche ich hier explizit vom Desktop, also vom Client-Rechner). In diesem Fall würde das Singleton-Muster also bestens passen. Sich das Singleton-Muster in der UML anzusehen, ist nicht sehr ergiebig, weil es sich nur um eine Klasse handelt, deswegen schauen wir in eine beispielhafte Implementierung. Man muss dazu sagen, dass die Implementierung eines Singletons in ActionScript leider mehr Fragen aufwirft als das eigentliche Muster, wie wir noch sehen werden. 01 package { 02 03 public class Singleton { 04 05 private static var instance:Singleton; 06 protected static var allowInstance:Boolean = false; 07 08 public function Singleton() { 09 10 if (!allowInstance) { 11 throw Error("Direkte Instanziierung nicht erlaubt!"); 12 } 13 trace("ha, es klappt!"); 14 } 15 16 public static function getInstance():Singleton { 17 18 if (instance == null) { 19 allowInstance = true; 20 instance = new Singleton(); 21 allowInstance = false; 22 } 23 24 return instance; 25 }
169
Kapitel 4 26 27 28 }
}
Listing 4.1: Beispielhafte Implementierung eines Singletons für ActionScript 3
Listing 4.1 zeigt eine Möglichkeit, das Singleton-Muster in ActionScript 3 zu implementieren, wie sie unter anderen von Grant Skinner vorgeschlagen wird. Dazu muss man nun natürlich einiges sagen. Das originale Singleton-Muster sieht eigentlich vor, dass man den Konstruktor der Singleton-Klassen private deklariert, damit erst einmal niemand eine Instanz von dieser Klassen erzeugen kann. Hier wird es in ActionScript schon problematisch, weil ActionScript 3 keine privaten Konstruktoren unterstützt. Puristen sagen nun, dass man nun schon deshalb eigentlich gar nicht erst versuchen sollte, Singletons in ActionScript umzusetzen, weil die Sprache das halt schlicht nicht erlaubt. Verhalten wir uns an dieser Stelle mal ein wenig pragmatischer, um das Muster weiter besprechen zu können. Um in ActionScript doch noch durchzusetzen, dass von unserer Singleton-Klasse von außen keine Instanz erzeugt wird, bedienen wir uns im obigen Beispiel eines Tricks. Wir definieren eine statische, boolesche Variable allowInstance, die auf protected gesetzt, also von außen nicht erreicht werden kann. Wir setzen sie erst einmal auf false. Im Konstruktor wird nun ein Error geworfen, wenn die Variable false ist. Damit kann man also schon mal nicht mehr einfach den Konstruktor aufrufen. Nun müssen wir noch dafür sorgen, dass Singleton selbst aber den Konstruktor aufrufen kann. In der getInstance()-Methode, also der Methode, über die man nun die einzige Instanz beziehen können soll, wird erst einmal geschaut, ob die Variable instance, die vom Typ des Singletons ist, schon gesetzt wurde. Ist dies nicht der Fall, wird jetzt die allowInstance-Variable temporär auf true gesetzt, dann wird die Singleton Instanz erzeugt – das klappt jetzt, weil allowInstance ja nun true ist –, und danach wir allowInstance schnell wieder auf false gesetzt, damit nicht doch noch jemand von außen über den Konstruktor weitere Instanzen erzeugen kann. Wieder aus der if-Schleife draußen, kann nun die Instanz zurückgegeben werden. Also auch in dem Fall, in dem die Instanz schon bestand. Kurz gefasst, erzeugt getInstance() eine Singleton-Instanz, wenn noch keine erzeugt wurde, oder gibt die schon erzeugte zurück. So weit, so gut. Treten wir nun kurz noch mal einen Schritt zurück und beleuchten die Frage, wofür man ein Singleton eigentlich braucht. Wir haben hier in der Implementierung gesehen, dass getInstance() eine statische Methode ist. Wieso kann man nicht einfach die ganze Singleton-Klasse durch statische Methoden und statische Variablen aufbauen? Wo ist der Vorteil, diesen vermeintlich umständlichen Weg über die getInstance()-Methode zu gehen? Die Antwort ist: Statische Klassen können nicht erben oder vererben. Statische Klassen können auch kein Interface implementieren. Machen wir eine Klasse statisch (meint, geben wir ihr nur statische Methoden und Attribute), berauben wir uns der grundlegenden Mechanismen, unsere Klasse in die objektorientierten Beziehungsgeflechte unserer restlichen Anwendungsstruktur einzubetten. Ein Singleton hingegen ist grundsätzlich eine normale Klasse. Sie besitzt nur halt ein kleines Konstrukt, das dafür sorgt, dass die Klasse nur einmal instanziiert werden kann.
170
Entwurfswerkzeuge Wie aber sieht die Vererbung eines Singletons aus, speziell in unserem Fall mit den kleinen eingebauten Workarounds? Listing 4.2 zeigt eine Klasse, die die zuvor gezeigte SingletonKlasse beerbt. 01 package { 02 03 public class SubSingleton extends Singleton { 04 05 private static var instance:SubSingleton; 06 07 public function SubSingleton() { 08 09 super(); 10 trace("Ich bin jetzt ein SubSingleton!"); 11 } 12 13 public static function getInstance():SubSingleton { 14 15 if (instance == null) { 16 Singleton.allowInstance = true; 17 instance = new SubSingleton(); 18 Singleton.allowInstance = false; 19 } 20 21 return instance; 22 } 23 } 24 25 } Listing 4.2: Eine Subklasse von Singleton aus Listing 4.1
Da statische Methoden und Variablen ja nicht mit vererbt werden, bleibt uns leider nichts anderes übrig, als die Methode getInstance() in unsere SubSingleton-Klasse zu kopieren, was nicht ideal ist. Im Original der Gang of Four wird ein Ansatz beschrieben, bei der die Hauptklasse Singleton die entsprechenden Unterklassen durch Angabe eines Identifizierers erzeugt. Diesen Ansatz finde ich aber nicht wirklich besser. Letztlich muss man zwischen dem einen oder anderen Übel abwägen. Ich entscheide mich also dafür, die getInstance()-Methode zu kopieren und sie aber leicht zu modifizieren für die Subklasse. Und zwar nur in dem Punkt, dass die Klasse SubSingleton selbst keine Variable allowInstance mehr definiert, sondern stattdessen die von der Oberklasse Singleton verwendet. Das hat den Grund, dass ich auch den Konstruktor von Singleton wiederverwenden will. Würde ich meine eigene statische allowInstance-Variable definieren, würde der beerbte Konstruktor von Singleton trotzdem auf die allowInstanceVariable der Hauptklasse Singleton zugreifen anstatt auf die in der Klasse SubSingleton. Deswegen verwende ich gleich die allowInstance-Variable von Singleton (die ich auch genau aus dem Grunde als protected deklariert wurde und nicht private). In diesem Zusammenhang können wir von Glück reden, dass Flash keine Threading unterstützt,
171
Kapitel 4 sonst wären solche Konstrukte brandgefährlich, denn hier verwenden nun mehrere Klassen unabhängig voneinander die gleiche Steuerungsvariable, um eine wichtige Entscheidung zu treffen. In einer Umgebung, die Threading unterstützt (also die Möglichkeit, dass mehrere Skripte quasi parallel laufen), könnte es theoretisch passieren, dass ein Skript versucht, unerlaubt den Konstruktor von Singleton aufzurufen, just in dem Augenblick, in dem ein Skript in einem anderen Thread getInstance() auf der SubSingleton-Methode aufruft und somit die allowInstance-Variable von Singleton kurzzeitig auf true setzt. Wir haben nun also eine funktionierende Lösung in ActionScript für ein Singleton, von dem man auch erben kann. Aber es ist Ihnen vielleicht schon aufgefallen, wirklich ideal ist die Lösung nicht, denn hier wird doch recht viel mit Tricks und Workarounds gearbeitet, um das gewünschte Verhalten hinzubekommen. Jeder Entwickler muss für sich selbst entscheiden, ob er diese Konstruktion in seiner Anwendung einsetzen will. Kommen wir zum Schluss noch mal auf die Andeutung mit dem Antipattern zurück. Ich hatte gesagt, dass das Singleton-Muster unter Softwareentwicklern recht kritisch und zwiespältig betrachtet wird. Das liegt weniger an den oben beschriebenen Tricks in Bezug auf ActionScript, sondern an einer anderen Eigenheit von Singletons. Singletons verkörpern globale Status, und globale Status sind schlecht. Was heißt das? Ein Singleton speichert seine einzige Instanz selbst in einer statischen Variablen, die indirekt über die Methode getInstance() erreichbar ist. Das bedeutet nun einerseits, dass die Singleton-Instanz über die ganze Anwendung hinweg erreichbar ist, und andererseits, dass diese SingletonInstanz, nachdem sie das erste Mal angefragt wurde, bis zur Beendigung der Anwendung existieren wird, denn das Singleton-Muster sieht grundsätzlich nicht vor, dass die eine Instanz auch wieder gelöscht werden könnte. Eine Instanz, die global über die ganze Anwendung hinweg erreichbar ist, wird tendenziell auch über die gesamte Anwendung hinweg benutzt, denn die Einfachheit, mit der ein Singleton »mal eben schnell« geholt werden kann, ist oft einfach zu verlockend. Das bedeutet aber letztlich, dass nun an potenziell vielen Stellen im Code direkt auf eine Klasse, nämlich unser Singleton, direkt zugegriffen wird. Und dieser Zugriff ist nur direkt in der Implementierung der jeweiligen Klassen, die das tun, sichtbar, nicht etwa durch ihre sichtbaren Schnittstellen, also ihre Klassen-Attribute, Klassen-Methoden und deren Parameter. Dadurch wird es zunehmend schwerer festzustellen, wer denn eigentlich das Singleton benutzt. Singletons werden also zunehmend aus den unterschiedlichsten Winkeln einer Anwendung direkt referenziert und verwendet. Das entspricht einer starken Kopplung von vielen zu einer Klasse. Wollte man sich nun entscheiden, die Singleton-Klasse gegen eine andere auszutauschen, hätte man ein Problem, denn man müsste jede Referenzierung ändern. Die Tatsache, dass ein Singleton bis zur Beendigung der Anwendung selbst existiert (vorausgesetzt, es wurde jemals erzeugt), erschwert auch das Testen einer Anwendung. Testcases brauchen normalerweise einen definierten Startzustand, um korrekt und nachvollziehbar abzulaufen. Eine Testcase auf eine Funktion, die ein Singleton verwendet, kann aber nur einmal innerhalb der Laufzeit einer Anwendung getestet werden, denn das Singleton kann nach dem ersten Testlauf nicht mehr zurückgesetzt werden, es sei denn, man beendet jedes Mal die gesamte Anwendung, was das Testen enorm verlangsamt.
172
Entwurfswerkzeuge Die Meinungen über Singletons sind nicht einhellig, aber zumindest kann man sagen, dass der Einsatz von Singletons mit besonderem Bedacht gewählt werden sollte, will man sich nicht seine Anwendungsstruktur unnötig verbauen. Insbesondere sollte man sich überlegen, wer denn alles eine Referenz auf diese eine Instanz benötigt und ob diese Objekte, wenn es nicht zu viele sind, nicht einfach ein normales Objekt statt eines Singletons verwenden können. Zum Schluss möchte ich noch eine Alternative zeigen, ein Singleton zu implementieren. Dabei verwende ich das Factory-Pattern, um die Singleton-Klassen zu erzeugen. Es sei gesagt, dass ich dabei außerdem die nicht direkt dokumentierten privaten Klassen von ActionScript verwende, die zwar auch in der Adobe-Dokumentation auftauchen und auch im Flex Framework verwendet werden, aber nicht direkt selbst dokumentiert sind. Schauen wir uns die Implementierung an. Zunächst definiere ich ein Interface, das die Funktionalität des künftigen Singletons beschreibt. Listing 4.3 zeigt dieses Interface. package { public interface ISingleton { function get someName():String; function set someName(value:String):void; function doSomething():void; } } Listing 4.3: Interface für ein Singleton
Als Beispiel soll uns dieses Interface genügen. Als Nächstes schreiben wir eine SingletonFactory und in der gleichen Klassendatei das konkrete Singleton (siehe Listing 4.4). package { public final class SingletonFactory { private static var _singletonInstance:ISingleton; public static function getInstance():ISingleton { if (_singletonInstance == null) { _singletonInstance = new PrivateSingleton(); } return _singletonInstance; } } } class PrivateSingleton implements ISingleton { private var _someName:String; public function get someName():String { return _someName;
173
Kapitel 4 } public function set someName(value:String):void { _someName = value; } public function doSomething():void { trace("Doing something with: " + _someName); } } Listing 4.4: SingletonFactory und PrivateSingleton in einer Klassendatei zusammen
Wichtig ist hierbei, dass die Klasse PrivateSingleton, die das konkrete Singleton repräsentiert, eine Klasse ist, die außerhalb der package-Definition steht. Damit ist sie von außen nicht erreichbar, und nur die Klasse SingletonFactory, die wiederum ganz normal innerhalb der package-Definition deklariert ist, kann auf PrivateSingleton zugreifen. Um nun diese Konstruktion zu verwenden, würde man einen Aufruf, wie in Listing 4.5 gezeigt, schreiben. var mySingleton:ISingleton = SingletonFactory.getInstance(); mySingleton.someName = "Hans"; mySingleton.doSomething(); Listing 4.5: Beispielhafter Aufruf der SingletonFactory
Diese Implementierung hat einige Vorteile, aber natürlich auch Nachteile. Die Vorteile sind: Die Verantwortlichkeiten bezüglich Erzeugung und Verwaltung des Singletons auf der einen Seite und der fachlichen Verantwortung der Klasse andererseits (also das, was eigentlich die konkrete Aufgabe der Klasse ist) sind hier sauber voneinander getrennt. Die Klasse PrivateSingleton weiß nicht einmal, dass sie ein Singleton ist, denn sie trägt, abgesehen vom Namen, keinerlei Singleton-Funktionalität in sich. Außerdem ist hier keine spezielle Behandlung bezüglich des Konstruktors der SingletonKlasse notwendig, wie wir das in Listing 4.1 gesehen haben. Das führt zudem dazu, dass hier bei falscher Verwendung von PrivateSingleton schon zur Kompilierzeit Fehler geworfen werden und nicht erst zur Laufzeit. Die meisten Auto-Completion-Mechanismen heutiger CodeEditoren würden PrivateSingleton außerhalb sogar gar nicht anbieten, sodass ein Entwickler schwerlich auf die Idee kommen wird, PrivateSingleton direkt zu benutzen. Ein klarer Nachteil ist aber auch, dass PrivateSingleton nicht beerbt werden kann, denn da auf die nicht sichtbare Klasse von außen niemand zugreifen kann, kann auch keine Klasse von ihr erben.
4.4.2 Strukturmuster Strukturmuster beschreiben Ansätze, den Aufbau von Objektstrukturen zu gestalten. Es geht hierbei meistens darum, mehr Flexibilität in Strukturen zu bringen. Objekte, die direkt miteinander verwoben sind, können oft schlecht ausgetauscht oder erweitert werden. Strukturmuster fügen oft Zwischenobjekte ein, um ein Netz aus Objekten wieder flexibel zu machen.
174
Entwurfswerkzeuge Die meisten Strukturmuster sind objektbasiert. Das heißt, sie verwenden taktische Objektbeziehungen, um ihre Aufgabe zu erfüllen. Das Brücken-Muster trennt zuerst die Schnittstelle und Implementierung einer Klasse in zwei getrennte Klassen und fügt sie dann wieder über eine Aggregation (also eine Besitzt-Beziehung) zusammen. Beziehungen, die über Assoziationen bzw. Aggregationen realisiert werden, sind loser, weil sie z. B. zur Laufzeit auch wieder gelöst und in anderer Form neu geknüpft werden können. Beziehungen, die durch Vererbung oder Interface-Implementierung gekennzeichnet sind, sind für die Laufzeit permanent und können nicht einfach wieder geändert werden. Der Musterkatalog der Gang of Four kennt sieben Strukturmuster:
Adapter (Adapter) Brücke (Bridge) Dekorierer (Decorator) Fassade (Facade) Fliegengewicht (Flyweight) Kompositum (Composite) Proxy (Proxy) Davon werde ich Brücke, Dekorierer, Fassade und Proxy besprechen.
Brücke (Bridge) Es gibt Situationen, in denen man feststellt, dass eine Klasse insgeheim zwei Verantwortlichkeiten in sich trägt. Nehmen wir ein Beispiel, eine UI-Komponente, z. B. einen Button. Ein Button hat in sich ein gewisses Verhalten, z. B. soll er ein Event werfen, wenn man auf ihn klickt. Daneben hat ein Button natürlich ein Aussehen. Das Aussehen eines Buttons lässt sich kaum abstrahieren, so unterschiedlich können Buttons aussehen. Wenn nun eine Klasse sowohl das Aussehen als auch das Verhalten eines Buttons in sich trägt, dann ist dies eine Klasse, mit der man nicht mehr viel anstellen kann. Nun könnte man natürlich eine abstrakte Button-Klasse entwerfen, die zunächst ein Standardaussehen sowie das Verhalten des Buttons implementiert, und man könnte dann pro unterschiedlichem Äußeren Unterklassen bilden, die den Button jeweils wie gewünscht anders aussehen lassen. Abbildung 4.21 zeigt die Struktur hierfür. Nun kann es passieren, dass sich eine neue Anforderung ergibt, dass z. B. ein ToggleButton benötigt wird, also einer, der in seiner Stellung an oder aus verharren kann. ToggleButton würde nun auch von Button erben. Damit es aber einen runden ToggleButton und einen Link-ToggleButton geben kann, müssten zwei neue Klassen erstellt werden, die wiederum von ToggleButton erben. Das ist sehr umständlich. Mit einfacher Vererbung kommt man hier nicht weiter, denn es wird versucht, zwei unterschiedliche Verantwortlichkeiten in einer Klasse unterzubringen, was die Struktur unflexibel macht.
175
Kapitel 4
Button + + -
blur() : void highlight() : void onPress() : void onRelease() : void
RoundButton
Link
Abbildung 4.21: Veränderung des Äußeren eines Buttons über Vererbung
Hier kommt nun das Brücken-Muster ins Spiel. Wir trennen nun die beiden Verantwortlichkeiten auf, einmal in das Verhalten von Buttons und einmal in das Äußere eines Buttons (damit bewegen wir uns auch zugleich schön in Richtung Model-View-Controller). Wir definieren also eine Klasse Button, die nur das Verhalten eines Buttons beschreibt und die zusätzlich eine Referenz auf eine ButtonStyle-Klasse trägt, die wiederum nur das Äußere von Buttons beschreibt, und zwar zunächst ein Standardaussehen. Und nun können von dem Standardverhalten und von dem Standardaussehen jeweils Unterklassen gebaut werden, die das Verhalten bzw. das Aussehen spezialisieren. Abbildung 4.22 zeigt, wie das in der Struktur aussieht.
Button -
style: ButtonStyle
+ + +
blur() : void highlight() : void onHighlight() : void onPress() : void setStyle(ButtonStyle) : void
ButtonStyle +style 1
1
ToggleButton «property get» + toggled() : Boolean
+ +
getBlurLook() : Sprite getHighlightLook() : Sprite
RoundButton + +
getBlurLook() : Sprite getHighlightLook() : Sprite
Link + +
getBlurLook() : Sprite getHighlightLook() : Sprite
«property set» + toggled(Boolean) : void
Abbildung 4.22: Trennung von Verantwortlichkeiten durch das Brücken-Muster
176
Entwurfswerkzeuge Wer jetzt also einen Button oder einen ToggleButton benutzen möchte, erzeugt einen und übergibt dann auch noch den gewünschten ButtonStyle. Die linke Seite im Diagramm zeigt nun also die Vererbungshierarchie für das Verhalten und die rechte Seite die für das Aussehen. Dadurch, dass wir beides voneinander getrennt haben, können nun unterschiedliche Verhaltensweisen von Button unabhängig vom Aussehen gebaut werden. Genauso können unterschiedliche Buttonstile entwickelt werden, unabhängig vom speziellen Verhalten eines Buttons. Wichtig hierbei ist natürlich, dass die Unterklassen ihre Hauptklassen in ihrer Wesensart nicht verändern, dass also das bereits im vorigen Hauptkapitel erwähnte Substitutionsprinzip gewahrt bleibt. Ein spezieller Button z.B., der plötzlich drei Status durchschalten können soll im Gegensatz zu den zweien, die der ToggleButton unterstützt, könnte durch die allgemeine Button-Klasse, die ja nur die Zustände highlight und blur kennt, nicht mehr gedeckt werden. Demzufolge würden auch die Buttonstile nicht mehr dazu passen, denn diese kennen auch nur zwei Zustände.
Dekorierer (Decorator) Das Dekorierer-Muster ermöglicht es, das Verhalten einer Klasse um weiteres Verhalten zu erweitern, ohne dabei die äußere Schnittstelle zu verändern und ohne Vererbung anwenden zu müssen. Dadurch entstehen sehr flexible und jederzeit, sogar zur Laufzeit, austauschbare Objekte mit variablem Verhalten. Nehmen wir als Beispiel wieder den Button. Wir gehen von einer ähnlichen Ausgangslage aus wie im Abschnitt zum Brücken-Muster. Wir haben also einen Button, der in diesem Fall ein gewisses minimales Standardaussehen bereits implementiert hat. Nehmen wir an, dieses minimale Aussehen besteht nur aus einem Sprite für den Nicht-gedrückt-Zustand und einem Sprite für den Gedrückt-Zustand. Wir haben des Weiteren wieder einen ToggleButton, der von Button erbt und einen toggle-Zustand mit bringt (Abbildung 4.23 zeigt die Ausgangslage). Wir wollen nun das Aussehen des Buttons verändern können, ohne über Vererbung eine starre Struktur zu erzeugen. Das Dekorierer-Muster sieht hierfür vor, dass wir eine abstrakte Klasse Dekorierer erstellen, die von Button erbt und zweierlei tut. Einerseits führt sie eine neue Klassenvariable namens component ein. Über den Konstruktor oder über eine setter-Methode kann man an diese Variable die Komponente übergeben, die man dekorieren will, in unserem Fall also entweder den Button oder den ToggleButton. Und zweitens muss die Dekorierer-Klasse alle öffentlichen Methoden von Button überschreiben und intern an die Referenz, die wir in der Klassenvariablen component gespeichert haben, weitergeben. Als Ergebnis haben wir nun erst einmal noch nicht viel gewonnen, aber wir haben eine Klasse, die sich in die Vererbungskette von Button und ToggleButton mit eingeklinkt hat und die selber wie ein Button wirken kann, ohne selbst überhaupt etwas tun zu müssen. Man könnte sich jetzt fragen, wozu man denn eine Instanz von Button oder ToggleButton an den Dekorierer übergeben muss, wo er doch selber ein Button ist und durch die Vererbung schon die Eigenschaften von Button geerbt hat. Der Sinn liegt darin, dass Dekorierer nun die Vorzüge der Vererbung mit denen der Objektkomposition verbindet.
177
Kapitel 4
Button -
blurSprite: Sprite higlightSprite: Sprite
+ # + -
blur() : void buildAppearance() : void highlight() : void onPress() : void onRelease() : void
ToggleButton «property set» + toggled(Boolean) : void «property get» + toggled() : Boolean
Abbildung 4.23: Die Ausgangslage für das Dekorierer-Muster
Einerseits ist der Dekorierer tatsächlich ein Button, weil er von Button erbt. Das ist praktisch, weil wir so schon zur Kompilierzeit Typenchecks machen können und weil Mechanismen wie Polymorphie hier wunderbar greifen. Andererseits behalten wir uns die Entscheidung, welche konkrete Art von Button wir wirklich sind, also z. B. Button oder ToggleButton, bis zur Laufzeit vor und können das sogar beliebig ändern. Der Dekorier ist also anders als bei alleiniger Vererbung nicht festgelegt darauf, was er konkret sein soll, sondern kann das offenlassen. Um diesen Vorteil nun nutzen zu können, implementieren wir vom abstrakten Dekorierer beliebig viele Unterklassen, die das Aussehen unseres Buttons oder ToggleButtons verändern sollen (in diesem Zusammenhang passt das Wort dekorieren natürlich besonders schön). Als Stile verwenden wir diesmal einen Stil, der wie ein HTML-Link aussehen soll, und einen Stil, der dem Button ein bestimmtes Icon verleiht. Die endgültige Struktur ist in Abbildung 4.24 zu sehen. Wir sehen hier, dass ButtonDecorator, der die Rolle des abstrakten Dekorierers einnimmt, im Konstruktor einen Parameter vom Typ Button erwartet, um so eine Referenz auf den zu dekorierenden Button zu erhalten. Außerdem überschreiben ButtonDecorator sowie seine Unterklassen IconButton und Link buildAppearance() (eine Funktion, die initial die Komponente zeichnet), blur() und highlight(). Das müssen sie wie gesagt tun, denn intern leiten sie den Aufruf an die besagte component-Referenz weiter und fügen nach Bedarf noch eigene Operationen hinzu. Erst dieses Prozedere führt ja überhaupt dazu, dass die Eigenschaften des Buttons erweitert werden. Im Prinzip ist dieses Vorgehen ähnlich, als würde man in einer normalen Vererbungshierarchie super verwenden, nur dass hier statt super die component-Instanz verwendet wird.
178
Entwurfswerkzeuge
Button -
blurSprite: Sprite higlightSprite: Sprite
+ # + -
blur() : void buildAppearance() : void highlight() : void onPress() : void onRelease() : void
+decoree 1
ToggleButton
ButtonDecorator + # + +
«property set» + toggled(Boolean) : void «property get» + toggled() : Boolean
blur() : void buildAppearance() : void ButtonDecorator(Button) : void highlight() : void
IconButton #
buildAppearance() : void
1
Link + + #
blur() : void highlight() : void buildAppearance() : void
Abbildung 4.24: Komplette Struktur des Dekorierer-Musters anhand des Button-Beispiels
Um nun dieses ganze Konstrukt noch ein wenig besser zu verstehen, schauen wir es uns noch mal in konkretem Code an (den ich hier vereinfacht wiedergebe, um keine seitenlangen Codewüsten abzudrucken). Zunächst schauen wir uns die Button-Klasse in Listing 4.6 an. public class Button extends Sprite { protected var blurSprite: Sprite; protected var higlightSprite: Sprite; public function Button() { addEventListener(MouseEvent.MOUSE_DOWN, onPress); addEventListener(MouseEvent.MOUSE_UP, onRelease); buildAppearance(); } public function buildAppearance(): void { // baue highlight und blur Anzeigen auf blur(); }
179
Kapitel 4 public function highlight(): void { higlightSprite.visible = true; blurSprite.visible = false; } public function blur(): void { blurSprite.visible = true; higlightSprite.visible = false; } private function onPress(): void { highlight(); } private function onRelease(): void { blur(); } } Listing 4.6: Die Klasse Button
Wie gesagt, ich habe den Aufbau stark vereinfacht. Im Wesentlichen soll der Code zeigen, dass der Button sich nach der Initialisierung aufbaut und dass die Eventhandler onPress() und onRelease() jeweils intern einfach die highlight()- bzw. die blur()-Methoden aufrufen, um die Anzeige des Buttons zu verändern, wenn ein Nutzer den Button drückt. highlight() und blur() wiederum schalten einfach die visible-Attribute der beiden Sprites an und aus, um die entsprechenden Grafiken darzustellen. Den Code des ToggleButtons sparen wir uns hier aus Platzgründen und stellen uns stattdessen vor, dass der ToggleButton in der onRelease()-Funktion einfach immer den toggled-Status munter wechselt, wann immer ein Nutzer den Button drückt. Außerdem wird highlight() und blur() nun nach Maßgabe des toggled-Status gesetzt und nicht mehr danach, ob der Button gerade gedrückt ist oder nicht. Somit wird dem Nutzer visualisiert, ob der Button nun gerade an- oder ausgeschaltet ist. Schauen wir uns nun die Klasse ButtonDecorator an. Listing 4.7 zeigt sie. public class ButtonDecorator extends Button { protected var component:Button; public function ButtonDecorator(i_component:Button) { component = i_component; } override public function blur(): void { component.blur(); } override public function buildAppearance(): void { component.buildAppearance(); }
180
Entwurfswerkzeuge override public function highlight(): void { component.highlight(); } } Listing 4.7: Der ButtonDecorator, der abstrakte Dekorierer
ButtonDecorator ist ja eine eigentlich abstrakte Klasse, von der nicht wirklich Instanzen erzeugt werden sollen. Da aber in ActionScript 3 keine abstrakten Klassen unterstützt werden, nehmen wir eine normale und implementieren die Methoden stumpf in einer Standardform, indem wir die Methodenaufrufe einfach an die zu dekorierende Instanz weiterleiten. Hier kann man noch mal schön sehen, wie die Referenzvariable component im Prinzip das super ersetzt, was man sonst normalerweise bei klassischer Vererbung verwenden würde. Interessanter wird es nun bei einem konkreten Dekorierer, denn dieser leitet die Aufrufe nicht nur stumpf weiter, sondern macht auch selber etwas. Wie das aussehen kann, schauen wir uns am Beispiel des IconButton in Listing 4.8 an. public class IconButton extends ButtonDecorator { [Embed(source='assets.swf', symbol='icon')] private var iconSymbol:Class; private var icon:Sprite; override public function buildAppearance(): void { component.buildAppearance(); icon = new iconSymbol(); addChild(icon); icon.x = 3; icon.y = component.height/2 - icon.height/2; } } Listing 4.8: IconButton, ein konkreter Dekorierer
Wir können sehen, IconButton überschreibt nur buildAppearance(). Konkret ruft IconButton erst die Originalmethode des zu dekorierenden Buttons und fügt dann noch ein Icon hinzu, das zuvor aus einer externen SWF-Datei eingebettet wurde. Da sich das Icon bei gedrückter bzw. losgelassener Maustaste grafisch nicht verändern soll, brauchen wir die Methoden blur() und highlight() nicht zu überschreiben und verlassen uns so auf die Standardimplementierung von ButtonDecorator. Zu guter Letzt wollen wir natürlich noch mal sehen, wie denn so ein Button, der als IconButton dekoriert ist, erstellt wird. Das kurze Skript in Listing 4.9 zeigt dies. var myButton:Button = new IconButton(new Button()); addChild(myButton); Listing 4.9: Verwenden eines dekorierten Buttons
181
Kapitel 4 Das Dekorierer-Muster hat neben den Vorteilen der Entkopplung auch Einschränkungen, die einen Einsatz nicht in jeder Situation empfehlenswert machen. Eine wichtige Eigenheit ist, dass ButtonDecorator zwar von Button erbt, aber auf Button über eine Referenz zugreift. Somit hat ButtonDecorator nur Zugriff auf öffentliche Methoden und Attribute der durch die Referenz repräsentierten Button-Instanz. Nicht mal auf Elemente, die als protected deklariert sind, kann zugegriffen werden, weil ButtonDecorator eben im Kontext einer normalen Referenz auf Button zugreift und nicht innerhalb der Vererbungskette. In der Praxis wird man aber durchaus auf Elemente zugreifen wollen, die als protected deklariert sind, denn ansonsten ist man darauf angewiesen, dass alle wichtigen Eigenschaften, die man manipulieren will, public sind, was wiederum aus Sicht der externen Schnittstelle eines Buttons sicher nicht wünschenswert ist. Eine zweite Eigenheit ist, dass sich alle beteiligten Klassen strikt an die Einhaltung der öffentlichen Schnittstelle halten müssen, und zwar über das Liskov'sche Substitutionsprinzip hinaus. Das Substitutionsprinzip sagt ja, dass eine Subklasse die Schnittstelle und das konzeptionelle Verhalten einer Oberklasse nicht verändern darf. Sehr wohl ist aber normalerweise erlaubt, zusätzliche Funktionalität hinzuzufügen. Innerhalb des Dekorierer-Musters hingegen wäre dies vergebens, denn dadurch, dass die konkreten Dekorierer-Klassen wie z. B. IconButton nur von Button erben und demzufolge auch seine Methoden implementieren, würde jegliche zusätzliche öffentliche Methode, die z. B. ToggleButton anböte, nicht verfügbar sein, denn IconButton kennt wie gesagt nur die Methoden von Button. Auch ein konkreter Dekorierer kann keine zusätzlichen Methoden anbieten, denn auch er könnte ja wieder dekoriert werden. Nehmen wir hierzu an, IconButton hätte eine Methode, mithilfe der man ein individuelles Icon-Symbol übergeben könnte. Und nun sehen Sie sich mal folgenden Aufruf in Listing 4.10 an. var myButton:Button = new Link(new IconButton(new Button())); Listing 4.10: Doppelt dekorierter Button
Hier wurde nun ein Button zweifach dekoriert, einmal als Link und zusätzlich als IconButton. Das ist vollkommen zulässig, es ist sogar eine der Stärken des Dekorierer-Musters. Es bedeutet aber auch, dass jede individuelle Methode, die der Dekorierer IconButton haben mag, die über die in Button deklarierten Methoden hinausgeht, nicht mehr erreicht werden kann. Diese beiden Eigenheiten schränken die Nutzbarkeit des Dekorier-Musters durchaus ein. Das bedeutet nicht, dass man das Muster nicht einsetzen sollte, aber man sollte sich über die Limitierungen bewusst sein. Eine Alternative zum Dekorierer-Muster kann das Strategie-Muster sein, das ich später im Abschnitt über Verhaltensmuster noch besprechen werde.
Fassade (Facade) Anwendungen bestehen oft aus Modulen oder Teilen von Klassen, die fachlich zusammengehören. Eine Tweening-Bibliothek besteht z. B. meistens nicht nur aus einer Klasse, sondern aus mehreren. Wenn Sie einen Parser bauen, besteht der vielleicht auch nicht nur aus einer Klasse. Er benutzt vielleicht auch noch weitere Klassen, die einzelne Aufgaben über-
182
Entwurfswerkzeuge nehmen. Eine Schnittstelle zu einem Server verwendet meist auch mehrere Klassen. Manche sind für die direkte Kommunikation zuständig, andere speichern die Daten zwischen, die zwischen Flash-Anwendung und Server hin- und hergeschickt werden. Wieder andere halten vielleicht Konfigurationswerte usw. Je umfangreicher so ein Modul ist, umso schwieriger kann es irgendwann für einen Entwickler werden, dieses Modul zu benutzen. Bei so vielen Klassen verliert man schnell den Überblick, und nicht alle Klassen sollen ja auch von außen benutzt werden. Genauso, wie eine einzelne Klasse eine klare und verständliche Schnittstelle über ihre Methoden und Attribute haben sollte, so sollten auch ganze Klassenverbände, die zusammen eine Aufgabe erledigen, eine klare und verständliche Schnittstelle haben. Das Fassade-Muster hilft dabei, dieses Vorhaben umzusetzen. Eine Fassade ist ganz einfach eine Klasse, die als Schnittstelle zwischen dem Modul und der Außenwelt steht und es somit Entwicklern erleichtern soll, mit dem Modul zu arbeiten. Das heißt nicht zwingend, dass die Kommunikation nur noch über die Fassade laufen muss. In manchen Situationen benötigt man ganz einfach direkten Zugriff auf eine Klasse innerhalb des Moduls. Aber eine Fassade bietet eine saubere Schnittstelle für die meisten immer wieder benötigten Funktionen, die das Modul anbietet. Fassaden helfen somit auch, die lose Kopplung zwischen Modulen und ihren Nutzern zu gewährleisten. Denn eine nutzende Klasse, die ein Modul über seine Fassade verwendet, hat nur die Abhängigkeit zur Fassade, aber nicht zu den internen Klassen innerhalb des Moduls. Das ermöglicht es, die internen Klassen und ihre Struktur noch zu ändern, falls das erforderlich wird. Solange die Fassade weiterhin funktioniert, ist alles in Ordnung. Um diese lose Kopplung besonders effektiv zu gestalten, sollten natürlich so wenige Klassen wie möglich innerhalb des Moduls direkt nach außen zugänglich sein. Dies ist in ActionScript günstigerweise über das Schlüsselwort internal möglich. Eine Klasse, die als internal deklariert ist, ist nur innerhalb ihres Pakets sichtbar, aber nicht außerhalb. Sie könnten nun also theoretisch ein Modul schreiben, bei dem alle Klassen bis auf die Fassade als internal deklariert sind. Die Fassade wäre natürlich als public deklariert und würde folglich als einziges Tor zur Außenwelt fungieren, denn da sie auch innerhalb des gleichen Pakets liegt wie die internal deklarierten Klassen, hat sie sowohl Zugriff auf diese Klassen, kann aber auch gleichzeitig von außen angesprochen werden. Schauen wir uns auch hier wieder ein Beispiel an. Diesmal nehmen wir ein Beispiel direkt unter Verwendung von Klassen aus der Flash Player API. Und zwar Klassen rund um den URLLoader im Paket flash.net. Die Klassen, die sich um den URLLoader ranken, sind ungefähr folgende:
URLLoader URLLoaderDataFormat URLRequest URLRequestHeader URLRequestMethod URLVariables
183
Kapitel 4 Es gibt noch URLStream, aber das ist eher eine Alternative zu URLLoader, wenn man eine Datei streamen will, deswegen betrachten wir sie hier mal nicht. Wie gesagt, es macht durchaus Sinn, dass all diese Klassen da sind, sie haben alle ihre Daseinsberechtigung, und in manchen Fällen kann man sehr froh sein, dass wir die Möglichkeit haben, einen Request an einen Server so feinteilig zu steuern. In anderen Fällen hingegen erscheint es manchmal ein wenig mühsam, all diese Klassen zu kennen und zu benutzen, um einen konkreten Request an einen Server auszuführen oder eine Datei zu laden. In der UML betrachtet, sieht das Beziehungsgeflecht dieser Klassen so aus wie in Abbildung 4.25 gezeigt.
EventDispatcher
Object
URLLoader + + + +
bytesLoaded: uint bytesTotal: uint data: var dataFormat: String
+ + +
close() : void load(request :URLRequest) : void URLLoader(request :URLRequest) : var
URLLoaderDataFormat + + +
BINARY: String {readOnly} TEXT: String {readOnly} VARIABLES: String {readOnly}
+
URLLoaderDataFormat() : var
1 Object
Object
URLRequest 1 +
URLRequest(url :String) : var
«property get» + contentType() : String + data() : Object + digest() : String + method() : String + requestHeaders() : Array + url() : String Object URLRequestHeader + +
name: String value: String
+
URLRequestHeader(name :String, value :String) : var
*
1
URLVariables
1
«property set» + contentType(value :String) : void + data(value :Object) : void + digest(value :String) : void + method(value :String) : void + requestHeaders(value :Array) : void + url(value :String) : void
+ 1 + +
decode(source :String) : void toString() : String URLVariables(source :String) : var
Object URLRequestMethod + +
GET: String {readOnly} POST: String {readOnly}
+
URLRequestMethod() : var
Abbildung 4.25: Struktur der Klassen rund um URLLoader (Stand Flash 9)
Wie man sehen kann, gibt es ein paar Abhängigkeiten, letztendlich arbeiten die Klassen gut zusammen und bieten zusammen eine sehr vielfältige Funktionalität. Wer nun einen Request an einen Server schicken will, muss im ungünstigsten Fall alle gezeigten Klassen verwenden, was zur Folge hat, dass man wenigstens grundsätzlich die Bedeutung aller Klassen kennen muss und man ganz praktisch einiges an Tipparbeit bewältigen muss. Schön wäre es, wenn das zumindest für einfache Fälle etwas weniger mühsam erledigt werden könnte. Hier soll uns nun eine Fassade helfen. Wir werden eine Schnittstellenklasse bauen, die zwar nicht jede Funktionalität bietet, die die einzelnen Klassen zur Verfügung stellt, die aber alles für die wichtigsten Regelfälle parat hat, damit man »mal eben schnell« einen Server-Request durchführen kann. Um das zu tun, müssen wir ein paar Annahmen und Einschränkungen treffen, um unsere Fassade nicht ausarten zu lassen. Denn eine Fassade, die in einer Klasse die Funktionalität von hier sechs Klassen komplett in sich trägt, wäre ein Monstrum und würde mit Sicherheit keine Erleichterung bringen. Folgende Annahmen und Einschränkungen treffen wir also:
184
Entwurfswerkzeuge
Wir unterstützen nur textbasierte Serverantworten, also keine binären und auch keine key-value-Paare.
Wir unterstützen die Attribute digest und contentType von URLRequest nicht. Wir unterstützen das Setzen von URLRequestHeader nicht. Wir unterstützen die Methoden decode() und toString() von URLVariables nicht. Diese Einschränkungen sollen uns reichen. Die genannten Features werden nicht unbedingt häufig verwendet und sollen deshalb nicht durch unsere Fassade unterstützt werden. Was wir hier übrigens bisher nicht berücksichtigt haben und auch zwecks Übersichtlichkeit nicht berücksichtigen werden, sind die Events, die die Klassen werfen können. Wir gehen der Einfachheit halber davon aus, dass alle notwendigen Events von der Fassade durchgereicht werden und man sich also für diese Events direkt an der Fassade anmelden kann. Abbildung 4.26 zeigt nun, wie die Fassade aussieht. Wir können sehen, die wichtigsten Methoden und Attribute für einen Server-Request sind vorhanden. Was dieses Diagramm nicht zeigt, was aber natürlich wichtig ist, ist die Tatsache, dass die Fassade selbst keinerlei Status speichert. Die Fassade holt sich alle Werte direkt aus den anderen konkreten Klasseninstanzen der Flash Player API, die sie intern verwendet. Würde sie selbst Status wie z. B. den jeweils aktuellen bytesLoaded-Wert speichern, würde eine Implementierungsabhängigkeit zwischen der Fassade und den dahinterliegenden Klassen bestehen, zusätzlich zur Schnittstellenabhängigkeit, die natürlich ohnehin besteht. Die Fassade leitet also Anfragen direkt weiter und reicht auch Daten und Events direkt durch, ohne sie selbst zwischenzuspeichern oder sie gar zu bearbeiten. Würde sie Daten manipulieren, gäbe es einen Bruch zwischen der Art und Weise, wie die Originalklassen, die ja auch immer noch direkt verwendet werden können, funktionieren, und der Art, wie die Fassade die Funktionalität anbietet. Das wiederum würde eher zu mehr Verwirrung führen, als dass es eine Vereinfachung bewirken würde.
EventDispatcher URLFacade + +
GET: String {readOnly} POST: String {readOnly}
+ + +
close() : void load(url :String, method :String, data :Object) : void URLFacade(url :String, method :String, data :Object) : void
«property get» + bytesLoaded() : uint + bytesTotal() : uint + method() : String + requestData() : Object + responseData() : var + url() : String «property set» + method(value :String) : void + requestData(value :Object) : void + url(value :String) : void
Abbildung 4.26: Die URLFacade bündelt die wichtigsten Funktionen rund um URLLoader.
185
Kapitel 4
Proxy (Proxy) Klassen bzw. Objekte sollen nur eine Verantwortlichkeit haben, damit bessere Wiederverwendbarkeit dieser Verantwortlichkeiten erreicht und damit Verantwortlichkeiten für sich allein gewartet, verändert und getestet werden können. Viele der Entwurfsmuster versuchen, speziell bezüglich dieses Prinzips Verantwortlichkeiten aufzutrennen. Das ProxyMuster gehört ebenso dazu. Einer Klasse, die in einer kontrollierten Art und Weise benutzt werden soll, kann man eine weitere Klasse vorne anstellen, die diese Kontrolle ausübt. Es gibt unterschiedliche Szenarien, in denen man eine kontrollierende Klasse vor eine eigentliche Klasse stellen wollte.
Sicherheit: Eventuell soll ein Zugriff auf eine bestimmte Klasseninstanz nur zu bestimmten Konditionen erfolgen, z. B. wenn ein bestimmter Status gesetzt ist.
Lokalisierung entfernter Systeme: Eine Serveranwendung, die bestimmte Dienste für eine Flash-Anwendung zur Verfügung stellt, muss über eine Flash-externe Schnittstelle, z. B. über XML, aufgerufen werden. Ein Proxy kann diese nichtnative Schnittstelle verstecken und stattdessen eine ActionScript-Schnittstelle anbieten. Dies ähnelt ein wenig dem Adapter-Muster, das ich nicht besprochen habe.
Kontrolle über Erzeugung eines Objekts: Manche Objekte sind bezogen auf ihren Speicherverbrauch oder ihren Performancebedarf sehr teuer. Zudem kann es sein, dass sie während ihres Lebenszyklus nicht sofort in voller Gänze zur Verfügung stehen müssen. Ein Proxy kann hier als effizienter Stellvertreter fungieren und einfache Grundfunktionalität bieten, bis das teure Objekt wirklich direkt benötigt wird. Im Vergleich zum Dekorierer-Muster, das ein Objekt mit zusätzlicher Funktionalität ausstattet, tut dies ein Proxy nicht, er fungiert vielmehr als Stellvertreter, und das eventuell auch nur temporär. Dennoch gleicht sich die Struktur des Proxy-Musters mit der des DekoriererMusters. Die Schnittstelle eines Proxys entspricht auch hier direkt der des eigentlichen Objekts. Der Proxy implementiert deswegen das gleiche Interface oder erbt von der gleichen Oberklasse wie das zu vertretende Objekt. Das Proxy-Muster sieht grundsätzlich so aus wie in Abbildung 4.27 gezeigt. Anders als beim Dekorierer-Muster wird in einem Proxy das dahinter stehende Objekt normalerweise zur Laufzeit nicht konfiguriert, die Beziehung steht also von vornherein fest. Auch hier schauen wir uns wieder ein Beispiel an. Wir stellen uns folgendes Szenario vor. Ein Nutzer muss sich innerhalb unseres Online-Videoverleihs FilmRegal in die Plattform einloggen, wenn er einen Film ansehen will. Innerhalb dieses Prozesses muss die Anwendung also einen Login-Dialog anzeigen, damit der Nutzer dort seinen Nutzernamen und sein Passwort eingibt, das abschickt und dann – wenn alles gut geht – authentifiziert ist. Ein Nutzer, der oft die Plattform nutzt, will sich aber nicht ständig neu einloggen. Deswegen bieten wir ihm bei der Registrierung an, dass er seine Login-Details in einem lokalen FlashCookie speichert, um den Login-Vorgang zu vereinfachen.
186
Entwurfswerkzeuge
AbstractObject
ConcreteObject
ObjectProxy -
object: ConcreteObject
Abbildung 4.27: Grundsätzliche Struktur des Proxy-Musters
Beim nächsten Login-Vorgang gibt es nun zwei Möglichkeiten, entweder der Nutzer muss den normalen Login-Vorgang durchlaufen, oder wir kennen seine Login-Daten schon über einen gesetzten Flash-Cookie, dann loggen wir ihn automatisch ein. Für diese Entscheidung wollen wir einen Proxy einsetzen. Neben einer originalen LoginProcess-Klasse, die normalerweise den Login-Vorgang steuern würde, bauen wir also einen LoginProxy, der in diesem Fall direkt von LoginProcess erbt (weil es keine abstrakte Oberklasse für LoginProcess gibt). LoginProxy schaut nun also erst einmal, ob es einen gesetzten Flash-Cookie mit Login-Daten gibt, und loggt sich, wenn vorhanden, mit diesen direkt ein anstatt den LoginProcess zu starten. Nur wenn kein Flash-Cookie vorhanden ist, wird der normale LoginProcess gestartet. Das Wichtigste hierbei ist, dass sich LoginProxy nach außen hin ganz so verhält, wie LoginProcess das tut, denn die Klasse, die das ganze Prozedere in Gang bringt, soll ja von dem neuen Konstrukt möglichst nichts mitbekommen. Abbildung 4.28 zeigt, wie die Struktur hierzu aussieht. Lassen Sie sich vom Diagramm übrigens nicht verwirren. Zwar führt vom Client eine Assoziationslinie zu LoginProcess anstatt zu LoginProxy. Dies soll aber nur verdeutlichen, dass Client nach wie vor eine Instanz vom Typ LoginProcess erwartet. Tatsächlich würden wir zur Laufzeit dafür sorgen, dass der Client eine Instanz vom Typ LoginProxy erhält, der ja auch ein LoginProcess ist, weil er von ihm erbt. Realisieren könnte man das wiederum über eine Factory oder auch zum Beispiel über sogenannte Dependency-Injection-Verfahren, die wir noch besprechen werden. LoginProcess ist im Grunde genommen eine nach außen hin sehr einfach wirkende Klasse, wahrscheinlich handelt es sich hier wieder um eine Fassade. Letztlich hat man nur die Möglichkeit, den Login-Prozess zu starten und im Notfall auch abzubrechen. Wurde der gesamte Login-Vorgang inklusive Dialog und allem erfolgreich beendet, wirft LoginProcess ein ProcessEvent mit dem speziellen Typ PROCESS_COMPLETE und teilt dem Client somit mit, der Nutzer ist eingeloggt. Wenn das einloggen nicht geklappt hat, wird der Typ PROCESS_FAILED mitgegeben.
187
Kapitel 4
Event ProcessEvent + +
PROCESS_COMPLETE: String {readOnly} PROCESS_FAILED: String {readOnly}
EventDispatcher
Client
LoginProcess + +
abort() : void start() : void
LoginProxy
Prüft zunächst auf lokalen Cookie.
-
loginProcess: LoginProcess
+ +
abort() : void start() : void
Flash Cookie
Abbildung 4.28: Der LoginProxy, Vertreter für den LoginProcess
LoginProxy wiederum erzeugt nun intern – ähnlich dem Dekorierer-Muster – eine LoginProcess-Instanz und meldet sich als Listener an LoginProcess an. Alle Events von LoginProcess leitet er dann an den Client weiter. Die abort()-Methode überschreibt LoginProxy und leitet den Aufruf an LoginProcess weiter. Die start()-Methode wird auch überschrieben, aber hier fügt LoginProxy nun erst einmal die Flash-Cookie-Prüfung ein. Sollte einer mit den benötigten Daten vorhanden sein, veranlasst er ein direktes Einloggen unter Umgehung des LoginProcess-Prozederes und wirft danach selber das ProcessEvent mit dem Typ PROCESS_COMPLETE, obwohl der LoginProcess selbst gar nicht erst gestartet wurde. Das hier angewendete Muster nennt man im Detail auch virtueller Proxy, weil der Proxy nach außen eine Funktionalität darbietet, die er intern gar nicht vollzieht. Neben virtuellen Proxies gibt es auch noch Remote-Proxies. Diese stellen einen Übergang zu einem entfernten System dar. Serverschnittstellen, die in Flash durch eine Klasse repräsentiert werden, sind im Allgemeinen Remote-Proxies. Daneben gibt es noch Sicherheits-Proxies. Sie fungieren sozusagen als Türsteher für sensible Klassen. Anstatt ein Objekt direkt zu benutzen, muss man dann den Sicherheits-Proxy verwenden, der zunächst prüft, ob der Zugriff zulässig ist.
188
Entwurfswerkzeuge Im Endeffekt kann man also sehen, dass Proxies hauptsächlich kontrollieren, ob und wann ein Zugriff auf das vertretende Objekt stattfindet. Sie verändern aber die Funktionalität und die Eigenschaften des Objekts nicht.
4.4.3 Verhaltensmuster Immer wenn es darum geht, wie Objekte miteinander zusammenarbeiten können, um eine größere Aufgabe zu erledigen, oder wenn es um die elegante Steuerung des Kontrollflusses innerhalb einer Anwendung geht, dann können Verhaltensmuster eine Antwort liefern. Verhaltensmuster versuchen insbesondere, die Beziehung zwischen zwei oder mehr zusammenarbeitenden Objekten zu lockern, damit andere Objekte an ihre Stelle treten können, wenn dies erforderlich. Hierzu ist eine der grundlegenden Ideen, dass man versucht, Arbeitsschritte oder Unteraufgaben in eigene Objekte zu kapseln, um dann später diese Schritte oder Unteraufgaben flexibel zur Laufzeit zusammensetzen zu können. Anders als bei Strukturmustern stehen hier also nicht der Aufbau und die Repräsentierung von Objekten im Vordergrund, sondern die flexible Handhabung des Zusammenspiels von Objekten. Der Bereich der Verhaltensmuster enthält die meisten individuellen Muster im Katalog von Gamma et al. Dies sind:
Befehl (Command) Beobachter (Observer) Besucher (Visitor) Interpreter (Interpreter) Iterator (Iterator) Memento (Memento) Schablonenmethode (Template Method) Strategie (Strategy) Vermittler (Mediator) Zustand (State) Zuständigkeitskette (Chain of Responsibility) Von diesen werde ich mich mit Befehl, Beobachter, Iterator, Schablonenmethode und Zuständigkeitskette befassen.
Befehl (Command) Das Befehlsmuster beschreibt die Kapselung von einer Aufgabe in eine Klasse, und zwar unabhängig von dem, der die Aufgabe ausführen will. Der Befehl steht damit für sich und hat selbst nur Abhängigkeiten zu den Objekten, die er manipuliert.
189
Kapitel 4 In vielen Situationen lässt sich das Ausführen einer Aufgabe von demjenigen trennen, der den Aufruf auslösen könnte. Es kann zum Beispiel sein, dass Sie zur Entwicklungszeit noch gar nicht wissen, wer die Aufgabe ausführen lassen wird, weil dafür unterschiedliche Objekte infrage kommen. Oder Sie wollen in der Lage sein zu bestimmen, welche von mehreren möglichen Aufgaben ausgeführt wird, z. B. basierend auf unterschiedlichen Status in Ihrer Anwendung. Abbildung 4.29 zeigt den grundsätzlichen Aufbau des Befehlsmusters. Beachten Sie, dass der Empfänger kein wesentliches Element des Musters ist, weil ein Befehl theoretisch auch ohne Verwendung anderer Objekte eine Aufgabe ausführen könnte. In der Regel aber führt er intern wieder Aktionen auf anderen Objekten durch, ruft als Methoden auf oder fragt Werte ab und dergleichen.
Auslöser
AbstrakterBefehl
Empfänger
KonkreterBefehl
Abbildung 4.29: Das Befehlsmuster
Ein gutes Beispiel für die Trennung zwischen auslösendem Objekt und ausführendem Objekt findet sich im Model-View-Controller-Architekturmuster. Wenn ein Nutzer in einem View-Objekt etwas tut, z. B. einen Button anklickt oder eine Tastenkombination drückt, dann sollen mitunter wichtige Operationen ausgeführt werden. Diese Operationen direkt im View zu implementieren, würde zwei Verantwortlichkeiten in einer Klasse bündeln, die Darstellung und die Ausführung einer Operation. Demzufolge läuft es gegen das MVCPrinzip, die Operation direkt im View zu implementieren. Stattdessen wird die vom Nutzer erzeugte Anfrage an einen Command im Controllerbereich von MVC weitergeleitet. Dieser Command – also der Befehl im Befehlsmuster – kann dann auf die Anfrage reagieren und entsprechend die Operation durchführen. So eine Operation kann ganz unterschiedliche Ausmaße haben. Manche Commands leiten die Anfrage auch nur an eine Klasse im Model weiter, andere führen durchaus umfangreiche Aktionen durch, eventuell auch unter Ausschluss des Models, wenn es nicht benötigt wird. In Flash wird die Verknüpfung von Commands zu entsprechenden View-Komponenten innerhalb von MVC meist über das Beobachter-Muster bzw. ganz konkret über Events gelöst. Die Hauptapplikation meldet z. B. einen Command an einer View-Komponente als
190
Entwurfswerkzeuge Listener an. Wenn dann die View-Komponente ein entsprechendes Event wirft, reagiert der Command darauf und führt seine Aktionen aus. Strukturell sieht das z. B. so aus wie in Abbildung 4.30.
Event
View
Controller
ViewComponent
Command +
execute() : void
Model
DataObject
Abbildung 4.30: Befehlsmuster innerhalb von MVC
Gerade in Bezug auf Flash ist eine Unterscheidung von synchronen bzw. asynchronen Commands wichtig. Da viele Commands auch durchaus asynchrone Operationen ausführen oder ausführen lassen, ist es also erforderlich, dass die Commands selbst auch asynchron laufen können. Dies wird dadurch erreicht, dass per Definition ein Command nicht beendet ist, indem seine execute()-Methode beendet ist, sondern erst, wenn ein Event geworfen hat, das die Beendigung mitteilt. So ist es dann möglich, dass ein Command auch veranlasst, dass Dateien geladen oder Requests zu einem Server gemacht werden oder dergleichen mehr. Commands kann man auch schachteln. Man könnte zum Beispiel ein MacroCommand definieren, das von Command erbt, aber zusätzlich die Funktionalität besitzt, weitere Commands zu beherbergen, die alle nacheinander ausgeführt werden. Auf diese Weise lassen sich ganze Befehlskaskaden bilden, die sehr komplexe Operationen durchführen. Diese Kaskadierung erfolgt dann nach dem Kompositionsmuster. Das MacroCommand hat hierzu ganz einfach ein Array, in dem es Unter-Commands aufnehmen kann. Durch dieses Array geht es nun durch und führt nacheinander die Commands aus. Für asynchrone Com-
191
Kapitel 4 mands muss man natürlich auch hier wieder eine entsprechende Implementierung vorsehen, die jeweils auf das Fertig-Event jedes einzelnen Commands wartet, bevor der nächste Command aufgerufen wird. Bei der sequenziellen Abarbeitung von solchen Commands kann man zudem auch noch das Prinzip der Lazy-Instantiation implementieren. Dieses Prinzip besagt, dass ein Objekt erst erzeugt wird, wenn es wirklich gebraucht wird. Dieses Prinzip haben wir im ProxyMuster schon in der Form des virtuellen Proxys kennengelernt. Bei Commands spielt einem die Tatsache in die Hände, dass ein Command im Prinzip inaktiv ist, bis seine execute()Methode aufgerufen wird, also die Methode, die dem Command das Signal gibt, seine Operation auszuführen. Es spricht in der Regel also nichts dagegen, den Command überhaupt erst zu erzeugen, wenn er auch ausgeführt werden soll. Gerade im Zusammenhang mit Command-Queues ist dieses Vorgehen von Vorteil, denn hier würden sonst eventuell viele inaktive Objekte im Speicher gehalten werden. Im Folgenden zeige ich eine beispielhafte Implementierung einer solchen Queue. Zunächst schreiben wir die Queue, das Herzstück unseres Konstrukts. Listing 4.11 zeigt sie. 01 package { 02 03 import flash.events.EventDispatcher; 04 05 public class CommandQueue extends EventDispatcher { 06 07 protected var queue:Array; 08 protected var currentCmdIndex:Number; 09 protected var currentCmd:Command; 10 11 public function start():void { 12 currentCmdIndex = 0; 13 reset(); 14 doNext(null); 15 } 16 17 public function stop():void { 18 reset(); 19 } 20 21 public function clear():void { 22 reset(); 23 queue = null; 24 } 25 26 public function pushCommand(newCommand:Class):void { 27 28 if (queue == null) queue = new Array(); 29 queue.push(newCommand); 30 } 31 32 protected function reset():void { 33
192
Entwurfswerkzeuge 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 }
if(currentCmd == null) return; currentCmd.removeEventListener( CommandEvent.COMPLETED, doNext ); currentCmd.removeEventListener( CommandEvent.FAILED, onCommandFailed ); currentCmd.cancel(); currentCmd = null; } protected function doNext(evt:CommandEvent):void { if(currentCmdIndex < queue.length) { if(currentCmd != null) reset(); currentCmd = new queue[currentCmdIndex++](); currentCmd.addEventListener( CommandEvent.COMPLETED, doNext ); currentCmd.addEventListener( CommandEvent.FAILED, onFailed ); currentCmd.execute(); } else end(); } protected function onFailed(evt:CommandEvent):void { reset(); dispatchEvent(new CommandEvent(CommandEvent.FAILED)); } protected function end():void { reset(); dispatchEvent(new CommandEvent(CommandEvent.COMPLETED)); } }
Listing 4.11: Die CommandQueue-Klasse arbeitet Commands sequenziell und asynchron ab unter Verwendung von Lazy-Instantiation.
Am interessantesten sind hier die Methoden pushCommand() und doNext(). pushCommand() erlaubt das Hinzufügen von Commands in die Queue. Dabei fällt auf, dass der zu übergebende Parameter vom Typ Class ist. Man übergibt hier nämlich nicht eine Instanz eines Commands, sondern die Klasse selbst. Aus Gründen der Übersichtlichkeit habe ich hier Prüfungen zur Typsicherheit weggelassen, aber es leuchtet ein, dass pushCommand() prüfen sollte, ob die übergebene Klasse ein Command ist.
193
Kapitel 4 doNext() wird initial und nach Beendigung eines jeden Commands aufgerufen und holt sich aus dem internen Array queue das nächste Command, auch hier also direkt die Klasse und
nicht etwa eine Instanz. Von dieser Klasse wird nun eine Instanz erzeugt (zu sehen in Zeile 51; da das Element im Array eine Referenz auf ein Klassenobjekt ist, kann davon mittels new eine Instanz erzeugt werden). Dann meldet es sich bei den Events COMPLETED und FAILED an. Und dann wird das Command ganz klassisch ausgeführt über die execute()-Methode (Zeile 58). Das alles passiert natürlich nur, solange noch Commands in der Queue vorhanden sind (Zeile 47), ansonsten wird die Queue beendet (Zeile 60). Ist die Queue fertig, so wirft sie selbst auch ein COMPLETED-Event (Zeile 70). Als Nächstes schauen wir uns einen Command an (Listing 4.12). package { import flash.events.EventDispatcher; public class Command extends EventDispatcher { public function execute():void { setResult(true); // Standardimplementierung, muss überschrieben werden } public function cancel():void { dispatchEvent(new CommandEvent(CommandEvent.FAILED)); } protected function setResult(successful:Boolean):void { if (successful) { dispatchEvent(new CommandEvent(CommandEvent.COMPLETED)); }else { dispatchEvent(new CommandEvent(CommandEvent.FAILED)); } } } } Listing 4.12: Ein Command, Kern des Befehlsmusters
Dies ist die abstrakte Command-Klasse. Sie wird selbst nicht direkt benutzt. Vielmehr soll man von ihr nun konkrete Commands ableiten, die dann individuelle Aufgaben ausführen. Das Wichtigste der Command-Klasse ist natürlich ihre execute()-Methode, die hier abstrakt implementiert ist und von erbenden Subklassen überschrieben werden muss. Hier findet die Abarbeitung der durchzuführenden Operation statt. Man kann hier sehen, dass die Standardimplementierung die Methode setResult() aufruft. Ich hatte ja erwähnt, dass innerhalb von Flash asynchrone Commands noch höheren Nutzen haben als synchrone. Die Methode setResult() ermöglicht uns nun, beliebig viele asynchrone Operationen durchzu-
194
Entwurfswerkzeuge führen. Erst wenn wir irgendwann setResult() aufrufen, wirft der Command ein Event, dass er fertig ist. Auf dieses Event ist die CommandQueue angemeldet und stößt auch erst dann den nächsten Command an. Somit können wir also komplexe asynchrone Aufrufe in unserem Command abarbeiten. Selbstredend kann setResult(successful) auch synchron, also noch direkt in der execute()-Methode, aufgerufen werden, wie es auch standardmäßig hier in der abstrakten Command-Klasse implementiert ist. Über den Parameter in setResult(successful) können wir zudem angeben, ob dieser Command erfolgreich beendet wurde oder nicht. Wenn nicht, wird anstelle des COMPLETED-Events das FAILED-Event geworfen, und die CommandQueue wird sofort beendet, weil wir annehmen, dass ein fehlgeschlagener Schritt in einer Queue die gesamte Queue infrage stellt. Das Gleiche tun wir, wenn das Command gecancelt wird. Dies gleicht einem nicht geplantem Abbruch und führt deswegen auch zum Scheitern des Commands und damit zum Scheitern der Queue. Das ist natürlich nur eine mögliche Definition, das kann man auch anders implementieren. Nun wollen wir uns noch zwei konkrete Commands ansehen, und zwar einmal ein synchronen und einen asynchronen. Zuerst den synchronen Command (Listing 4.13). package { public class CommandA extends Command { override public function execute():void { trace("Synchroner Befehl ausgeführt!"); setResult(true); } } } Listing 4.13: Synchroner Command
Dazu kann man gar nicht viel sagen, wir begnügen uns hier mit einer einfachen trace()– Anweisung, und weil sonst nichts zu tun ist, rufen wir dann auch gleich setResult(successful) mit true auf, um anzuzeigen, dass hier alles gut gegangen ist. Wir sehen also, im einfachsten Fall kann ein konkreter Command sehr schlank sein. Innerhalb von MVCKonstrukten, wo ein Command tatsächlich Anfragen aus der View mehr oder minder nur weiterreicht an zuständige Model-Klassen, sehen Commands tatsächlich so schlank aus. Nun aber zum Vergleich noch mal ein asynchroner Command. Listing 4.14 zeigt ihn. package { import flash.events.Event; import flash.net.URLLoader; import flash.net.URLRequest; public class CommandB extends Command { private var xmlLoader:URLLoader; private var someRecipient:Recipient;
195
Kapitel 4 override public function execute():void { xmlLoader = new URLLoader(); xmlLoader.addEventListener(Event.COMPLETE, onComplete); xmlLoader.load(new URLRequest("configuration.xml")); } private function onComplete(evt:Event):void { trace("Asynchroner Befehl ausgeführt!"); someRecipient.setConfiguration(new XML(xmlLoader.data)); setResult(true); } } } Listing 4.14: Asynchroner Command
Dieser Command soll eine XML-Datei laden und sie dann an einen Empfänger übergeben. Der Empfänger interessiert uns hier nicht weiter, aber der asynchrone Command. In der execute()-Methode wird hier also erst einmal ein URLLoader erzeugt. Dieser lädt nun die benötigte XML-Datei. Aus Gründen der Übersichtlichkeit melde ich mich hier nur an Event.COMPLETE, also für den Punkt, an dem die Datei geladen ist. In der Praxis würde man sich natürlich auch noch am Fehlschlag des Ladevorgangs anmelden und dann entsprechend auch als Command ein FAILED-Event weitergeben. Hier begnügen wir uns mit der onComplete()-Methode, die also aufgerufen wird, wenn die XML-Datei geladen ist. Wir übergeben das XML-Objekt an den Empfänger und rufen wieder die inzwischen bekannte setResult(successful)-Methode mit true auf, um bekannt zu geben, dass auch dieser Command sich erfolgreich beendet hat. Um das Beispiel zu komplettieren, wollen wir nun noch sehen, wie man nun so eine Queue konfiguriert und benutzt. Dies zeigt Listing 4.15. // ... var queue:CommandQueue = new CommandQueue(); queue.addEventListener(CommandEvent.COMPLETED, onComplete); queue.addEventListener(CommandEvent.FAILED, onFailed); queue.pushCommand(CommandA); queue.pushCommand(CommandB); queue.start(); // ... private function onComplete(event:CommandEvent):void { trace("CommanQueue fertig!"); }
196
Entwurfswerkzeuge private function onFailed(event:CommandEvent):void { trace("CommanQueue fehlgeschlagen!"); } Listing 4.15: Konfiguration und Verwendung der CommandQueue
Die Verwendung ist recht simpel, wie man sehen kann. Wir erzeugen die Queue, melden uns an den Events für Erfolg und Fehlschlag an und fügen die gewünschten Commands hinzu. Wichtig auch hier wieder, man sieht deutlich, dass wir nicht Instanzen der Commands hinzufügen, sondern einfach die Klassenobjekte selbst. Das ist der springende Punkt bezüglich der Lazy-Instantiation. Danach wird die Queue einfach gestartet, und die Operationen nehmen ihren Lauf. Ist die Queue fertig durchgelaufen, wird onComplete() aufgerufen, im Fehlerfall onFailed(). Das Beispiel zeigt, wir stark man Befehlsobjekte trennen kann von den Klassen, die den Befehl auslösen. Die Queue erlaubt eine beliebige Anordnung der Commands und schafft so eine vollkommene Loslösung von der auslösenden zur ausführenden Klasse, in diesem Fall die Klasse, die die Queue startet. Ein weiterer Vorteil der Trennung von auslösendem zu ausführendem Objekt ist, dass mehrere unterschiedliche auslösende Objekte das gleiche ausführende Objekt ansprechen können. Nehmen wir an, wir haben in unserem User-Interface einen Button, der eine bestimmte Aktion auslösen soll. Diese Aktion definieren wir also in einem Command. Das Command wird wie gehabt als Listener an den Button angemeldet. Nun wäre es kein Problem, auch noch einen Menüeintrag innerhalb eines Befehlsmenüs, das es vielleicht auch noch im User-Interface gibt, vorzusehen, der genau den gleichen Command ansteuert. Alles, was wir tun müssen, ist, denselben Command auch noch als Listener an den Menüeintrag anzumelden, und die Funktionalität steht. Ähnliche Möglichkeiten lassen sich auch in ganz anderen Bereichen finden. Stellen wir uns hierzu ein Spiel vor. In einem Spiel gibt es z. B. eine Operation, die den Spielstand in Form einer Score hochzählt, je nachdem, wie geschickt man ist. Unterschiedlichste Dinge können nun die Score nach oben treiben. Vielleicht muss man bestimmte Spielsteine sammeln, besonders viele Computergegner unschädlich machen und auch noch in einer bestimmten Zeit fertig werden. Wann immer eine dieser Anforderungen erfüllt wird, könnte dieses Ereignis einen IncreaseScore-Command auslösen, der dann die Punktezahl erhöht. Das Befehlsmuster kann um eine Undo-Funktionalität erweitert werden, wenn es möglich ist, eine Operation, die ein Command ausgeführt hat, auch wieder umzukehren. Das ist unter Umständen ein sehr aufwendiges Unterfangen. Je nachdem, wie komplex die Aktionen sind, die ein Command durchführt, kann die Implementierung einer Undo-Funktionalität sehr kompliziert bis unmöglich werden. Wenn ein Command z. B. viele andere Klassen verwendet, um seine Aufgaben auszuführen, dann hängt die Möglichkeit einer UndoFunktion auch davon ab, ob und in welchem Umfang Daten und Status in diesen anderen Klassen verändert wurden und ob man diese Veränderungen dort auch wieder zurücknehmen kann. Wenn Sie sich überlegen, dass diese Objekte ja auch wieder Abhängigkeiten zu weiteren Objekten haben könnten, in denen nun eventuell Daten verändert wurden, dann merken Sie, eine Undo-Funktionalität ist nicht trivial.
197
Kapitel 4
Beobachter (Observer) Das Beobachter-Muster ist eines der effektivsten Muster zur kompletten Entkopplung von zwei Klassen. Es hat zudem gerade in der Flash-Welt eine lange Tradition. Erstmals in einer recht einfachen Form als eher inoffizieller ASBroadcaster in Erscheinung getreten, wurde daraus über die Jahre der EventDispatcher. Events sind mittlerweile ein fester Bestandteil der Flash Player API, auch wenn sie noch nicht in den Sprachkern von ActionScript mit eingeflossen sind, wie das bei anderen Sprachen wie z. B. C# der Fall ist. Der in ActionScript 3 implementierte EventDispatcher lehnt sich stark an das BeobachterMuster an, weicht aber in einigen Punkten vom ursprünglichen Muster ab. Die Unterschiede wollen wir uns ansehen, und wir fangen zunächst mit dem ursprünglichen Muster an. Das ursprüngliche Muster besagt, dass es ein Subjekt gibt, das einen veränderlichen Zustand besitzt, und dass es eine beliebige Anzahl von Beobachtern gibt, die sich für diesen Zustand und insbesondere für die Veränderung des Zustands interessieren. Nun könnte so ein Beobachter natürlich ständig beim Subjekt nachfragen, ob sich was verändert hat. Das nennt man »Polling«, also man fragt ständig nach: »Gibt's was Neues?« Nicht sehr effektiv, weil sich wahrscheinlich die meiste Zeit nichts ändert. Besser ist, wenn das Subjekt allen Interessierten mitteilt, wenn sich etwas verändert hat, das nennt man dann »Push«. Das Subjekt teilt anderen mit, wenn sich etwas tut. Nun könnte das Subjekt natürlich einfach die Objekte direkt kennen, die sich für seinen Zustand interessieren. Aber was, wenn neue dazukommen, dann muss jedes Mal die Subjekt-Klasse angepasst werden. Und was, wenn sich manche Beobachter nur temporär für den Zustand interessieren und nicht die ganze Zeit benachrichtigt werden wollen? Das Beobachter-Muster bietet hier die Lösung. Zunächst definiert man ein BeobachterInterface, das beschreibt, wie ein konkreter Beobachter auf eine Zustandsveränderung des Subjekts reagieren kann, sprich, welche Methode beim Beobachter aufgerufen wird, wenn sich der Zustand geändert hat. Das Subjekt wiederum bietet Methoden zum An- und Abmelden von Klassen, die das Beobachter-Interface implementieren. Das Subjekt speichert alle diese Beobachter in einem internen Array. Wenn sich nun der Zustand im Subjekt ändert (wenn sich also zum Beispiel ein Wert ändert), dann durchläuft das Subjekt sein Array mit den Beobachtern und ruft auf jedem Beobachter die im Interface vereinbarte Methode auf. Nun weiß also der Beobachter, dass etwas passiert ist, und kann sich vom Subjekt die neuen Werte abholen. Abbildung 4.31 zeigt den strukturellen Aufbau des Beobachter-Musters. Bevor wir uns nun die Eigenheiten des EventDispatchers von Flash im Vergleich dazu betrachten, seien ein paar Besonderheiten des eigentlichen Musters hervorgehoben. Zunächst muss das Interface des Beobachters (in Englisch: Observer) genannt werden. Das Interface beschreibt, wie ein Beobachter überhaupt von einer Zustandsveränderung erfahren kann. Hier wird also ein offizieller Schnittstellenvertrag geschlossen zwischen dem Beobachter und dem Subjekt. Die zweite Eigenheit ist, dass das Beobachter-Muster nur die Information übermittelt, dass sich der Zustand im Subjekt verändert hat. Weder enthält die Nachricht die Information, was sich genau am Zustand geändert hat, noch den neuen Wert.
198
Entwurfswerkzeuge Und letztlich besteht eine direkte zweite Verbindung vom konkreten Beobachter zum konkreten Subjekt (in der Grafik also ConcreteObserver zu ConcreteSubject), über die der Beobachter, nachdem ihm mitgeteilt wurde, dass sich etwas verändert hat, die aktuellen Werte abholen kann.
«interface» Observer
Subject -observers + + +
addObserver(Observer) : void removeObserver(Observer) : void update() : void
*
1
+
onUpdate() : void
ConcreteSubject ConcreteObserver «property set» + importantValue(String) : void
-subject
beziehe importantValue
+
onUpdate() : void
«property get» + importantValue() : String
Abbildung 4.31: Struktur des Beobachter-Musters
Diese Eigenheiten zu benennen ist wichtig, wenn wir uns nun dem EventDispatcher zuwenden. Wir wollen uns in diesem Zusammenhang nicht mit jeder Eigenschaft und jedem Feature von EventDispatcher beschäftigen. Dazu sei die offizielle Adobe-Dokumentation empfohlen. Hier geht es vielmehr darum, die Ähnlichkeiten bzw. die grundsätzlichen Unterschiede des EventDispatchers zum originären Beobachter-Muster zu identifizieren.
Object IEventDispatcher EventDispatcher + + + + + +
addEventListener(type :String, listener :Function, useCapture :Boolean, priority :int, useWeakReference :Boolean) : void dispatchEvent(event :Event) : Boolean EventDispatcher(target :IEventDispatcher) hasEventListener(type :String) : Boolean removeEventListener(type :String, listener :Function, useCapture :Boolean) : void willTrigger(type :String) : Boolean
Abbildung 4.32: Der EventDispatcher in Flash 9
Abbildung 4.32 zeigt die Klasse EventDispatcher. In welcher Beziehung steht der EventDispatcher zum Beobachter-Muster? Ganz klar, er stellt das abstrakte Subjekt dar. Bei ihm kann man sich als Beobachter an- und abmelden, und er informiert seine Beobachter auch, wenn sich der Zustand eines konkreten Subjekts verändert hat. Konkrete Subjekte sind dann z. B. ein MovieClip oder ein URLLoader. Wie man in der Abbildung auch sehen kann, bietet die
199
Kapitel 4 Flash Player API zum EventDispatcher auch noch ein Interface, das IeventDispatcher-Interface. Es ist dazu da, Vorgaben für Klassen zu machen, die kompatibel zum EventDispatcher sein wollen. Natürlich ist der EventDispatcher zu diesem Interface kompatibel. Wie sieht es nun aus mit dem Beobachter? Im Beobachter-Muster ist ja ein Interface vorgesehen, das definiert, wie ein Beobachter beschaffen sein muss, um Nachrichten vom Subjekt erhalten zu können. Sprich, welche Eventhandler-Methoden er implementieren muss. So ein Interface ist im Konzept des EventDispatchers nicht vorgesehen. Das wird klar, wenn man sich anschaut, wie bzw. was man beim EventDispatcher eigentlich als Listener anmeldet. In der addEventListener()-Methode sehen wir, dass als Listener ein Objekt vom Typ Function übergeben wird. Hier wird also gar nicht ein Beobachter im ursprünglichen Sinne übergeben, sondern direkt eine Referenz auf eine Methode eines Beobachters. EventDispatcher interessiert sich also nicht direkt für die Klassen, die sich als Beobachter bei ihm anmelden, sondern einfach nur direkt für die Eventhandler-Methoden dieser Beobachter. Das hat Vor- und Nachteile. Ein klarer Vorteil ist, der Beobachter kann individuell bestimmen, über welche Methode er benachrichtigt werden will, wenn das Subjekt ein Event wirft. Er muss sich also nicht an irgendwelche Benennungen halten. Das ist komfortabel, denn es kann durchaus passieren, dass eine Klasse gleichzeitig Beobachter mehrerer Subjekte ist. Wenn diese im ungünstigsten Fall Interfaces mit den gleichen Methodennamen vorgäben, würde eine Unterscheidung der unterschiedlichen Events schwierig. Ein weiterer Vorteil ist darüber hinaus dann auch, dass man sich das Interface sparen kann, denn Interfaces können nur Vorgaben für Klassen machen, nicht für einzelne Methoden. Hier haben wir nun aber auch schon einen Nachteil. Dadurch, dass es nun kein Interface gibt, das einem Beobachter vorgibt, wie die Eventhandler-Methode zu heißen hat und wie sie aufgebaut sein soll, gibt es nun auch keine Vorgabe, welche Parameter die zugewiesene Eventhandler-Methode zur Verfügung stellen soll. Anhand der Klasse EventDispatcher und seiner Methode addEventListener() ist nicht ersichtlich, dass die listener-Methode einen Parameter vom Typ eines bestimmten Events zur Verfügung stellen muss. Das muss sich ein Entwickler aus der Dokumentation herausziehen. Folglich wirft der Compiler auch keinen Fehler, wenn die listener-Methode gar keine Parameter erwartet. Der Fehler würde erst zur Laufzeit als Error auftreten. In der Praxis wird man diesen Nachteil verschmerzen können, die Vorteile überwiegen hier. Im Übrigen ist die Tatsache, dass EventDispatcher mit der Benachrichtigung an seine Beobachter bezüglich einer Statusveränderung auch ein Event-Objekt mitgibt, auch eine Abweichung vom ursprünglichen Beobachter-Muster. Hier ergeben sich gleich zwei Besonderheiten. Erstens repräsentiert das Event-Objekt meistens bereits einen bestimmten Eventtyp, der eine Aussage darüber trifft, was sich eigentlich bezüglich des Status des Subjekts verändert hat. Und zweitens ist es nicht unüblich, dass im Event auch schon die veränderten Werte mitgegeben werden, zumindest wenn es sich um primitive Werte wie Zahlen, Strings und dergleichen handelt. Man könnte hier natürlich argumentieren, dass damit eine Typeigenschaft auf zwei Klassen dupliziert wird, die nun beide immer gleichermaßen verändert werden müssen, wenn sich die Typeigenschaft ändert. Andererseits kann ein Wert in einem Event auch ein aggregierter Wert sein, der vom eigentlichen Subjekt abstrahiert ist. Stellen wir uns zum Beispiel eine
200
Entwurfswerkzeuge Preloader-Klasse vor, die intern die Werte für geladene Bytes einer Datei und die Gesamtzahl an Bytes der Datei kennt. In einem Status-Event zum Ladefortschritt des Preloaders könnte man nun einfach eine Prozentzahl mitgeben, die hier als Aggregation aus den beiden Einzelwerten angesehen werden kann. Die Prozentzahl stellt in diesem Fall eine Abstraktion von den beiden konkreten Werten dar und schafft so wieder ein wenig Entkopplung. So könnte dieses Event dann auch bei einem StreamLoader eingesetzt werden, der nicht mit Bytes arbeitet, sondern mit Bufferzeiten. In dem Fall würde die Prozentzahl dann also den Füllstand des Buffers widerspiegeln. Ein weiterer Vorteil bezüglich der Wertweitergabe über das Event ist, dass somit eine Konstellation vorstellbar ist, in der sich weder das Subjekt, noch der Beobachter direkt kennen. Wenn eine dritte Klasse zum Beispiel die Methode eines Beobachters an einem Subjekt anmeldet und dieses Subjekt bei einer Statusveränderung ein Event mit dem neuen Wert verschickt, dann besteht für den Beobachter eventuell keine Notwendigkeit mehr, überhaupt noch auf das Subjekt zuzugreifen. Somit sind dann beide Objekte komplett voneinander entkoppelt und halten die Verbindung nur über das Event-Objekt. Dies wäre sogar für eine Kommunikation in beide Richtungen denkbar, denn es spricht nichts dagegen, dass ein Subjekt gleichzeitig Beobachter ist und umgekehrt. Wir sehen also, EventDispatcher ist eine Verfeinerung des eigentlichen Beobachter-Musters mit einigen interessanten und nützlichen Eigenschaften. Es weicht an manchen Stellen vom ursprünglichen Konzept ab, ist auch nicht frei von kleineren Nachteilen, hat sich aber in der Praxis als enorm nützlich erwiesen und ist somit aus der täglichen Arbeit mit Flash nicht mehr wegzudenken. In diesem Zusammenhang bleibt zu wünschen, dass Events eines Tages integraler Teil der Sprache werden, so könnte dann der Compiler noch mehr potenzielle Fehler im Vorfelde abfangen. Das Beobachter-Muster ist ein Paradebeispiel für die sehr lose Kopplung zwischen Objekten. Der eigentliche Trick dabei liegt in der Tatsache, dass nicht eine Klasse A eine konkrete Methode einer Klasse B aufruft. Stattdessen wird das Konzept der Nachricht eingeführt. Eine Nachricht ist eine standardisierte Form des Methodenaufrufs, die losgelöst ist von einer konkreten Klasse. Dadurch, dass zwei sich fremde Klassen grundsätzlich auf eine standardisierte Nachrichtenempfangsmethode einigen, müssen diese Klassen ansonsten nichts voneinander wissen. Im Falle des EventDispatchers wird dieses Konzept auf die Spitze getrieben, weil hier das Subjekt noch nicht mal eine Referenz auf die BeobachterKlasse hat, sondern nur direkt auf eine Methode. Diese starke Entkopplung hat aber grundsätzlich auch Nachteile. Sie verschlechtert die Lesbarkeit von Code. Ein Entwickler wird Schwierigkeiten haben, den Kontroll- und Datenfluss einer Klasse zu verstehen, die auf mehrere Events hört und die sich vielleicht noch nicht mal selbst an diese Events anmeldet, sondern angemeldet wird von einem Dritten. Aus der Klasse selbst kann er nun nicht mehr erkennen, woher eigentlich die Events kommen. Wenn die komplette Anwendung unter Verwendung diverser Events aufgebaut ist, kann man unter Umständen die Orientierung darüber verlieren, wie die Klassen miteinander in Beziehung stehen. Hier sollte man wieder bedenken, dass lose Kopplung kein absolutes Gut an sich ist. In einem Modul, in dem Klassen direkt zusammen einen bestimmten Zweck erfüllen, ist es unter Umständen vollkommen in Ordnung, dass sie sich kennen. Das
201
Kapitel 4 schafft Transparenz und Klarheit für Entwickler, wie diese Klassen miteinander arbeiten. Meistens will man auch nicht unbedingt innerhalb eines Moduls Klassen extrem lose voneinander koppeln. Vielmehr will man Module voneinander entkoppeln. Dort spielen dann das Beobachter-Muster und Events ihre Stärken aus.
Iterator (Iterator) In Programmen arbeitet man häufig mit irgendeiner Form von Listen, Bäumen oder sonst wie verketteten Elementsammlungen. ActionScript selbst bietet die Klasse Array an. Im Flex-Framework befinden sich noch Erweiterungen, wie z. B. die ArrayColletion und einige weitere Klassen rund um die ArrayCollection. Neben einem Array lohnt es sich manchmal, eigene Repräsentationen von Listen zu bauen, die spezielle Eigenschaften haben. Zum Beispiel können Elemente sich auch direkt miteinander verketten und somit eine Liste ergeben, ohne dass es einen übergeordneten Container gäbe. Vorstellbar wäre auch eine alphabetische Liste, bei der ein Element, wenn es in die Liste eingefügt wird, sofort alphabetisch einsortiert wird, sodass die Liste immer sofort korrekt sortiert ist. Auch XML-Strukturen können manchmal Listen beschreiben. Welche Art von Liste man auch immer vor sich hat, immer wird man durch die Liste durchgehen wollen, um mit den Elementen der Liste etwas zu tun. Entweder sucht man ein bestimmtes Element, oder man möchte anhand der Liste bestimmte Aktionen pro Element ausführen. Durch eine Liste durchzugehen, z. B. per Schleife, nennt man auch: durch eine Liste iterieren. In ActionScript kann man mittels der for-each-Schleife auch über normale Objekte iterieren, sofern diese Objekte dafür vorbereitet sind (siehe hierzu auch die AdobeDokumentation zur Methode setPropertyIsEnumerable() von Object). Nun gibt es also die for-Schleife und die for-each-Schleife, ist das schon das Iterator-Muster? Die Antwort lautet: fast. Eine Schleife ohne besondere Verwendung des Iterator-Musters arbeitet direkt auf der Liste. Wenn man also eine for-Schleife schreibt, dann fragt diese direkt die Länge der Liste ab, und man greift meistens direkt auf den Index der Liste zu (z. B. bei der Verwendung eines Arrays). In vielen Fällen ist dagegen auch nichts einzuwenden. Es führt aber dazu, dass die Klasse, die diese for-Schleife schreibt, direkt an das Array als Listenform gebunden ist. Stellen wir uns z. B. eine View-Komponente vor, die eine beliebige Zahl von Buttons untereinander platzieren soll. Im ersten Entwurf wird dieser Komponente ein Array übergeben. Der Entwickler schreibt also eine for-Schleife, iteriert somit über das Array und baut entsprechend viele Buttons untereinander auf. Nun teilt ihm einige Zeit später sein Kollege mit, er musste leider an der Übergabe der Daten etwas ändern, es wäre jetzt ein Objekt mit konkreten Attributen pro Button, in dem die Informationen stehen. Nun müsste unser Entwickler seine for-Schleife wieder ändern, und er ist bestimmt etwas argwöhnisch, ob das das letzte Mal war, dass er an den Code ranmusste. Helfen könnte hier, zwischen die konkrete Listenquelle – also Array oder Object – einen Iterator zu schalten, der die unterschiedliche Art und Weise, an die Elemente in der Liste heranzukommen, für den Klient abstrahiert.
202
Entwurfswerkzeuge Schauen wir uns das Iterator-Muster in der UML an (siehe Abbildung 4.33).
«interface» List +
createIterator() : Iterator
«interface» Iterator + + +
hasNext() : Boolean next() : void start() : void
«property get» + currentElement() : *
ConcreteList +
createIterator() : Iterator
ConcreteIterator + + +
hasNext() : Boolean next() : void start() : void
«property get» + currentElement() : *
Abbildung 4.33: Das Iterator-Muster
Das Muster sieht vor, dass eine Liste von ihrem Iterator getrennt wird. Die Liste an sich repräsentiert die Daten und der Iterator gewissermaßen einen Controller für den Zugriff auf die Daten. Nun gibt es zwischen der Datenhaltung und dem Iterator eine recht enge Kopplung, denn der Iterator muss ja wissen, wie man auf die Daten zugreift und über sie iteriert. Das deutet der Pfeil zwischen ConcreteIterator und ConcreteList an. Weil das so ist, ist das ideale Szenario, dass die Liste den für sie gemachten Iterator selbst erzeugt, und zwar immer über den abstrakten Typ Iterator. Ein Klient hätte also eine Referenz auf eine ConcreteList, die er aber nur als List kennt (erhalten hat er sie z. B. über eine Fabrikmethode). Nun ruft er die Methode createIterator() auf, die grundsätzlich ein Objekt vom Typ Iterator zurückliefert. Da es sich aber um eine ConcreteList handelt, ist dort die Methode createIterator() so implementiert, dass sie in Wirklichkeit ein Objekt vom Typ ConcreteIterator zurückliefert, denn nur der passt für die ConcreteList. Somit arbeitet der Klient nach seiner Kenntnis auf einer abstrakten Liste mit einem abstrakten Iterator. Dadurch können wir nun sehr schön die Listen bzw. ihre Iteratoren im Hintergrund tauschen, wenn es nötig ist. Das Iterieren wird für den Klient immer gleich bleiben, solange jeder konkrete Iterator das Interface Iterator implementiert. Leider implementieren die vorhandenen Klassen der Flash Player API dieses Konzept nicht, sodass man sich eigene Wrapper-Klassen bauen müsste, die z. B. intern ein Array verwenden und gleichzeitig die createIterator()-Methode implementieren. Dies ist aber eine Aufgabe, die man nur einmal machen muss. Eine derart erweiterte Klasse kann man immer wieder verwenden. In der Flash-Entwicklergemeinde existieren auch schon einige Implementierungen des Iterator-Musters für Arrays und andere listenartige Strukturobjekte.
203
Kapitel 4 Bezüglich des Iterator-Musters gibt es auch noch eine alternative Umsetzung. Man spricht hier von internen bzw. externen Iteratoren. Das oben erläuterte Konzept beschreibt externe Iteratoren. Das bedeutet, dass die Funktionalität des Iterierens nach außen für den Klienten verfügbar gemacht wird und er das Iterieren vornehmen kann. Es gibt auch noch die interne Variante, in der die Liste das Iterieren selbst übernimmt und dem Klient anbietet, eine Funktion zu definieren, die auf jedes Element der Liste angewendet werden soll. Die Array-Klasse der Flash Player API bietet diesbezüglich einige Methoden an. Genannt sei hier beispielhaft nur eine, die Methode map(). Diese Methode ermöglicht die Angabe einer Funktion, die dann pro Element ausgeführt wird. Diese Funktion muss drei Parameter annehmen, einen für das aktuelle Element im Array, einen für den Index des aktuellen Elements im Array und einen dritten für eine Referenz auf das Array selbst. Dies ist ein klassischer interner Iterator. Die Iteration macht Array selbst, der Klient benennt nur die besagte Funktion. Die Verwendung interner Methoden kann den Code lesbarer machen, denn es werden tendenziell weniger Schleifenkonstrukte verwendet und mehr benannte Methoden, die für sich sprechender sind als ein unbenannter Codeblock einer Schleife. Darüber hinaus ist das Verwenden eines internen Iterators wie z. B. map() in Verbindung mit einer Methode nicht viel aufwendiger als z. B. das Schreiben einer for-Schleife. Iteratoren können auch verwendet werden, um auf baumartigen Strukturen zu arbeiten. Hierfür sind interne Iteratoren meistens besser geeignet, weil sie rekursiv arbeiten können (also sich selbst aufrufen). Der Vorteil der Verwendung eines Iterators zur Traversierung einer Baumstruktur ist, dass dem Klienten die Komplexität der Traversierung verborgen bleibt und es für ihn somit keinen Unterschied macht, ob eine simple Liste iteriert oder eine komplexe Baumstruktur traversiert wird.
Schablonenmethode (Template Method) In Kapitel 3.10.4 habe ich von Sichtbarkeiten innerhalb einer Vererbungshierarchie gesprochen. Grund dafür, über dieses Thema nachzudenken, ist, dass auch in einer Vererbungshierarchie Verantwortlichkeiten klar getrennt werden sollten. Wenn eine Klasse B von einer Klasse A erbt, dann sollte klar sein, welche Verantwortlichkeit die Klasse B in sich trägt und welche Verantwortlichkeit Klasse A, denn nur so wird auch ersichtlich, welchen Nutzen die eine Klasse überhaupt gegenüber der anderen hat. Stellen wir uns vor, ein Sprite hätte auch schon eine Timeline und MovieClip würde einfach nur die zusätzlichen Methoden wie play() und gotoAndStop() beinhalten. Bei so einer komischen Aufteilung wären die Verantwortlichkeiten unlogisch verteilt. Deswegen gehört die Timeline in den MovieClip. Das Schablonenmethode-Muster ist ein Muster, das genau diese Trennung der Verantwortlichkeiten auf Implementierungsebene unterstützt, und zwar auf eine ganz bestimmte Art. Die Idee ist, dass die Superklasse auf einem höheren Level Operationen definiert, die intern unter anderem durch den Aufruf von weiteren Methoden realisiert werden. Nehmen wir also an, eine Superklasse mit dem Namen PagePrintManager hat eine Methode mit dem Namen print(). Diese Methode verteilt nun intern diese große Aufgabe auf mehrere kleine Methoden, z. B. createHeader(), printBody(), createFooter() und aggregatePages(). Eventu-
204
Entwurfswerkzeuge ell hat PagePrintManager für diese Detailmethoden eine standardmäßige Implementierung, das Schablonenmethode-Muster geht aber normalerweise davon aus, dass die Methoden nicht implementiert sind und zwingend von Subklassen überschrieben werden müssen. In der Methode print() des PagePrintManagers ist also das übergreifende Wissen zum Drucken von Seiten gekapselt. Nun gibt es in der zugrundeliegenden Anwendung vielleicht mehrere unterschiedliche Arten von Seiten, jeweils mit unterschiedlichen Arten von Headern und Footern. Zum Beispiel wollen wir Seiten entweder im Längs- oder im Querformat drucken. Nach dem Schablonenmethode-Muster erzeugt man nun Subklassen vom PagePrintManager. Diese Subklassen wiederum überschreiben nun nur die Detailmethoden, wie z. B. createHeader() oder createFooter(). In der Praxis würde man wohl die Methode print() im PagePrintManager sogar final deklarieren, damit sie auch nicht versehentlich überschrieben wird. Wir haben jetzt eine gute Trennung der Verantwortlichkeiten zwischen Superklasse und seinen Subklassen. Erstere kümmert sich um die grobe Koordination, Letztere kümmern sich um die Details. Dadurch können nun beliebig viele konkrete PagePrintManager-Subklassen gebaut werden für die unterschiedlichen Seitenarten, die es zu drucken gilt. In der UML sieht das dann so aus wie in Abbildung 4.34.
PagePrintManager +
Ruft intern auf: createHeader(); createFooter(); printBody();
print() : void
«abstrakt» + createFooter() : void + createHeader() : void + printBody() : void
HorizontalPrint + + +
createFooter() : void createHeader() : void printBody() : void
VerticalPrint + + +
createFooter() : void createHeader() : void printBody() : void
Abbildung 4.34: Das Schablonenmethode-Muster am Beispiel des PagePrintManagers
Das Schablonenmethode-Muster erinnert an das Prinzip eines Frameworks, das ich später in einem der folgenden Abschnitte noch erläutern werde. Hier nur so viel: PagePrintManager kann als feste Klasse eines Frameworks angesehen werden, die Funktionalität auf einem etwas höheren Abstraktionslevel anbietet, eben z. B. die print()-Methode. Diese weiß, was alles getan werden muss, um etwas zu drucken. In einem Framework sind bestimmte Stel-
205
Kapitel 4 len immer variabel angelegt, sodass sie individuell überschrieben werden können bzw. müssen. Das haben wir hier bei den drei Methoden createHeader(), createrFooter() und printBody(). Sie sind die variablen Teile, die vom Nutzer des Frameworks konkret implementiert werden müssen. In unserem Fall tun das die beiden Klassen HorizontalPrint und VerticalPrint. In Frameworks wird deswegen recht oft mit Schablonenmethoden gearbeitet.
Zuständigkeitskette (Chain of Responsibility) Die Zuständigkeitskette ist ein interessantes Muster zur Entkopplung von Klienten, die eine Anfrage für eine bestimmte zu lösende Aufgabe haben, und der Klasse, die die Anfrage bearbeiten kann. Gerade in Situationen, in denen ein Klient eventuell gar nicht wissen kann oder soll, welche Klasse eine bestimmte Aufgabe erledigen soll, kann das Muster gut eingesetzt werden. Bevor wir uns einem konkreten Beispiel widmen, möchte ich einmal das offizielle Muster zeigen. Abbildung 4.35 zeigt es. #successor
Client
Worker +
handleRequest() : void
«property set» + successor(Worker) : void «property get» + successor() : Worker
ConcreteWorkerA +
handleRequest() : void
ConcreteWorkerB +
handleRequest() : void
Abbildung 4.35: Das Zuständigkeitsketten-Muster
Das originale Zuständigkeitsketten-Muster beschreibt eine abstrakte Klasse oder ein Interface, das eine Methode deklariert, die eine Anfrage entgegennimmt (in der Abbildung ist das der Worker). Alternativ können auch mehrere unterschiedliche Methoden für unterschiedliche Anfragen definiert werden, wenn es erforderlich ist. Zusätzlich definiert sie eine Referenz auf einen Nachfolger (successor in der Abbildung). Worker wird nicht direkt selbst benutzt, sondern seine Subklassen. Ein außenstehender Controller baut nun aus Instanzen dieser Subklassen eine Kette (in der obigen Abbildung wäre das eine zugegebenermaßen kurze Kette mit zwei Elementen, aber das soll für die Anschaulichkeit reichen).
206
Entwurfswerkzeuge Zum Beispiel würde er definieren, dass ConcreteWorkerA das erste Element ist und als Nachfolger ConcreteWorkerB erhält. Dem eigentlichen Klienten wiederum, also der Klasse, die potenziell eine Anfrage haben könnte, würde man nun z. B. eine Instanz von ConcreteWorkerA mitgeben. Wenn nun der Klient eine Anfrage stellt, stellt er sie also an ConcreteWorkerA, und falls der sie nicht beantworten kann, reicht er sie direkt an ConcreteWorkerB weiter, in der Hoffnung, dass der sie beantworten kann. Es kann bei einer Zuständigkeitskette durchaus passieren, dass gar kein Objekt eine Anfrage beantworten kann. Wenn nun mal kein Objekt in der Kette hängt, das für eine bestimmte Anfrage zuständig ist, dann kann eine Anfrage also auch ins Leere gehen. Das originale Zuständigkeitsketten-Muster ist schon sehr hilfreich, aber ein paar Eigenheiten stören mitunter. Dem Klient muss eine konkrete Referenz auf ein Element in der Kette direkt übergeben werden. Der Grund ist, dass das originale Muster einer Fallback-Strategie gleicht. Der Klient kennt grundsätzlich seinen potenziellen Bearbeiter, weiß aber nicht sicher, ob dieser die Anfrage auch wirklich abarbeiten können wird. Sollte es tatsächlich nicht klappen, dann springen die Nachfolger ein. Das birgt zwei Nachteile in sich. Einmal muss jemand entscheiden, welches konkrete Element der Klient erhält, und allein in dieser Entscheidung steckt unter Umständen schon wieder eine ungewünschte Kopplung, die man ja gerade vermeiden will. Außerdem kann es bei dieser Art von Kette sein, dass ein Klient mit einem Element in der Kette seine Anfrage beginnt, das schon hinter dem Element liegt, das die Anfrage beantworten könnte. In diesem Fall würde sich also in der Kette zwar ein Element befinden, das die Anfrage bearbeiten kann, aber der Klient könnte es nie erreichen, weil sein konkreter Startpunkt ungünstig gewählt ist. Wir stellen hier also fest, dass die Reihenfolge der Kette eine Rolle spielt, als auch die Wahl des Elements, mit der ein Klient seine Anfrage startet. Wir können aber das Muster ein wenig erweitern bzw. abändern, um diese Nachteile wettzumachen. Um diese Lösung zu skizzieren, betrachten wir zunächst ein beispielhaftes Szenario, in der wir eine Zuständigkeitskette gut gebrauchen können. Stellen wir uns z. B. vor, wir haben eine Anwendung, die es einem Nutzer erlaubt, Fotos aus unterschiedlichen FotoCommunity-Portalen zusammenzusuchen und gemeinsam darzustellen. Die meisten Portale bieten heutzutage Schnittstellen an, die es erlauben, Fotos aus den Portalen zu laden. Aber natürlich unterscheiden sich die Schnittstellen voneinander im Detail. Unsere Anwendung soll gerüstet sein für neue Portale oder für den Wegfall eines Portals. Wir wollen also von der konkreten Auswahl der Portale und ihrer Schnittstellen innerhalb der Anwendung möglichst nicht abhängig sein. Innerhalb unserer Anwendung soll der Nutzer in einem Dialog einfach aus einem Dropdown eines der von uns unterstützten Foto-Community-Portale auswählen können, aus dem dann über einen Suchfilter die Fotos geladen werden. Diesen Vorgang kann er mehrmals wiederholen, um so aus unterschiedlichen Portalen Fotos zu beziehen. Für den Suchfilter interessiere ich mich für dieses Beispiel nicht so sehr, vielmehr aber für die Auswahl der entsprechenden Schnittstellen zu den Portalen. Wir erstellen also zunächst einmal ein Interface, das in abstrakter Weise unserer Anwendung gestattet, aus irgendeinem Fotoportal Fotos zu beziehen.
207
Kapitel 4
«interface» PhotoAdapter + +
setSearchFilter(SearchFilter) : void startRequest() : void
«property get» + photos() : Vector
Abbildung 4.36: Interface PhotoAdapter
Wie in Abbildung 4.36 zu sehen, erlaubt uns unser Interface PhotoAdapter, einen Suchfilter zu setzen, eine Anfrage zu starten und nach Erfolg (gemeldet über ein Event) die Fotos abzuholen. Für das Attribut photos verwenden wir hier mal die seit Flash Player 10 verfügbare Vector-Klasse, die es uns erlaubt, Array-ähnliche Listen zu bauen, die nur aus Elementen eines bestimmten Typs bestehen dürfen, in unserem Fall nur Bitmaps (für eine ausführliche Erläuterung lesen Sie bitte die Adobe-Dokumentation zur Vector-Klasse). Das Interface tut natürlich noch nichts. Wir müssen nun konkrete Klassen implementieren, die pro Foto-Community eine entsprechende Schnittstelle implementieren, um von dort Fotos abzuholen. Diese Klassen müssen natürlich unser PhotoAdapter-Interface implementieren. Nehmen wir als Beispiele mal flickr, Picasa und sevenload.
«interface» PhotoAdapter + +
setSearchFilter(SearchFilter) : void startRequest() : void
«property get» + photos() : Vector
FlickrAdapter + +
setSearchFilter(SearchFilter) : void startRequest() : void
«property get» + photos() : Vector
PicasaAdapter + +
setSearchFilter(SearchFilter) : void startRequest() : void
«property get» + photos() : Vector
SevenloadAdapter + +
setSearchFilter(SearchFilter) : void startRequest() : void
«property get» + photos() : Vector
Abbildung 4.37: Die konkreten Adapter für die einzelnen Foto-Communities
In Abbildung 4.37 können wir nun die einzelnen konkreten Adapter sehen. Die konkrete Implementierung soll uns hier auch nicht weiter interessieren. Nun haben wir noch den Klient. Der Klient ist in unserem Fall eine Klasse, die einen Controller darstellt, der von einer View-Dialog-Komponente benachrichtigt wird, wenn ein Nutzer eine Foto-Community gewählt, bestimmte Suchparameter definiert und danach auf einen Button »Hinzufügen« geklickt hat. Der Klient erhält nun ein Event, in dem die gewählten Parameter enthalten sind. Er selber soll nun wie gesagt nicht an die einzelnen verfügbaren Adapter
208
Entwurfswerkzeuge gekoppelt sein. Deswegen will er seine Anfrage an eine Kette von möglichen Bearbeitern weiterreichen, von denen ein Bearbeiter die Anfrage hoffentlich bearbeiten kann. Abbildung 4.38 zeigt unseren Klienten und das Event, das er aus der View erhält. PhotoRequestEvent bietet drei Properties, die den Request spezifizieren. Einmal den Suchfilter, dann einen String, der die gewünschte Community spezifiziert (also eine der drei Konstanten), und noch ein zunächst merkwürdig anmutendes Property requiredInterface(), das ein benötigtes Interface als Klassenreferenz angibt. Zu diesem dritten Property kommen wir später.
Event
Client PhotoRequestEvent +
onRequestNewPhotos(PhotoRequestEvent) : void
Nimmt nur eine der drei Konstanten an.
+ + +
FLICKR: String {readOnly} PICASA: String {readOnly} SEVENLOAD: String {readOnly}
«property get» + searchFilter() : SearchFilter + community() : String + requiredInterface() : Class «property set» + searchFilter(SearchFilter) : void + community(String) : void
Abbildung 4.38: Der Client mit dem Event in der Foto-Anwendung
Momentan aber haben wir ja aber noch gar keine Vorbereitungen bezüglich des Zuständigkeitsketten-Musters getroffen. Das kommt also jetzt. Ich hatte ja erwähnt, ich möchte das Muster in etwas abgewandelter Form implementieren. Folgende Änderungen am originalen Muster nehme ich vor:
Die Kette: Im originalen Muster ergibt sich die Kette indirekt aus der Verknüpfung der konkreten Bearbeiter untereinander. Ich ziehe diese Eigenschaft aus den Bearbeitern heraus und erstelle eine eigene Klasse, die die Kette organisiert und die auch als zentraler Einstiegspunkt für die Kette fungiert. Das hat den Vorteil, dass jeder Klient immer den gleichen Einstiegspunkt verwenden kann. Dadurch sind die Bearbeiter, also die Mitglieder der Kette, stärker entkoppelt von den Klienten, und es können einfacher neue Bearbeiter hinzugefügt werden, ohne dass am Klienten etwas geändert werden muss. Diese Kette wird letztlich intern das Iterator-Muster verwenden, um die zunächst ja an sie selbst gerichtete Anfrage an die Mitglieder der Kette weiterzuleiten.
Damit die Bearbeiter mit der Ketten-Klasse korrekt zusammenarbeiten, definiere ich ein weiteres Interface, das den Bearbeitern vorgibt, wie sie sich als Mitglieder in der Kette zu verhalten haben. Da unsere drei Adapter-Klassen später die Bearbeiter darstellen werden, müssen sie also jetzt zusätzlich dieses zweite Interface implementieren.
Im originalen Muster wird jeder Bearbeiter durch einen einfachen Methodenaufruf angefragt. Gamma et al. haben aber auch selbst schon geschrieben, dass statt eines einfachen Methodenaufrufs auch ein Anfrage-Objekt verwendet werden kann, also ein
209
Kapitel 4 Objekt, das noch mehr Informationen bezüglich der Anfrage enthält, um den Bearbeitern zu ermöglichen zu entscheiden, ob sie die Anfrage bearbeiten können oder nicht. Das werde ich hier so übernehmen. Schauen wir uns nun also die Zuständigkeitsketten-Klasse und das zusätzliche Interface an, das den Bearbeitern sagt, wie sie sich als Mitglieder in der Zuständigkeitskette verhalten sollen (Abbildung 4.39).
WorkerChain -workers + + +
addWorker(WorkerInChain) : void deleteWorker(WorkerInChain) : void processRequest(PhotoRequestEvent) : Boolean
+ 1..* +
«interface» WorkerInChain checkAptitude(PhotoRequestEvent) : Boolean processRequest(PhotoRequestEvent) : void
Abbildung 4.39: Zuständigkeitskette mit Bearbeiter-Interface
Die Ketten-Klasse WorkerChain bietet einmal Methoden zum Hinzufügen und Wegnehmen von einzelnen Bearbeitern. Sie kann hierüber also konfiguriert werden. Außerdem nimmt sie Anfragen über die Methode processRequest() entgegen. Ich habe mich der Einfachheit halber dafür entschieden, das bereits bestehende PhotoRequestEvent als Anfrage-Objekt zu verwenden, über das die Bearbeiter prüfen sollen, ob sie eine Anfrage entgegennehmen können. Demzufolge muss man das Event an die processRequest()-Methode von WorkerChain mitgeben, damit dieser seinerseits eben dieses Event-Objekt an die konkreten Worker weiterleiten kann. WorkerInChain ist also das Interface, das vorgibt, wie ein Bearbeiter sich innerhalb einer Kette verhalten soll. Die Methode checkAptitude() (zu Deutsch: prüfe Eignung) überprüft, ob der Bearbeiter die Anfrage bearbeiten kann oder nicht. Deswegen ist sein Rückgabewert auch ein Boolean. Als Mittel zur Überprüfung steht ihm wie schon gesagt das PhotoRequestEvent-Objekt zur Verfügung. Wenn er die Anfrage bearbeiten kann, dann wird an ihm die Methode processRequest() aufgerufen, und der Bearbeiter kann loslegen. So weit, so gut. Schauen wir uns nun mal die Implementierung dieser Zuständigkeitskette an. Der Übersichtlichkeit halber werde ich hier nicht alle Klassen zeigen, sondern die interessantesten herausgreifen. Wir fangen natürlich mit dem Klienten an, denn er ist unser Ausgangspunkt. Listing 4.16 zeigt seine Implementierung. import flash.events.EventDispatcher; public class Client
{
private var chain:WorkerChain; private var view:EventDispatcher; public function Client(i_chain:WorkerChain, view:Sprite) { chain = i_chain; view.addEventListener(
210
Entwurfswerkzeuge PhotoRequestEvent.PHOTO_REQUEST_EVENT, onRequestNewPhotos ); } public function onRequestNewPhotos(event:PhotoRequestEvent): void { trace( +
"Kette war erfolgreich? " chain.processRequest(event.clone() as PhotoRequestEvent)
); } } Listing 4.16: Der Klient
Unser Klient ist eine recht einfache Natur. Ihm wird im Konstruktor eine bereits fertige Zuständigkeitskette übergeben (die WorkerChain) sowie ein view Objekt. Die Kette merkt sich der Client, und an der view meldet er sich als Listener an für das PhotoRequestEvent. Wir warten nun also, dass in einem Dialog ein Nutzer neue Fotos von einer bestimmten Plattform anfordert. Wenn das passiert, wird die Eventhandler-Methode onRequestNewPhotos() aufgerufen. Dort kommt nun ein Event an. Weil es sich hier nur um eine Beispielimplementierung handelt, tun wir nun nicht mehr, als der Kette eine Kopie des PhotoRequestEvents über die Methode processRequest() mitzugeben und den Rückgabewert der Methode – der den Erfolg oder Misserfolg der Kette signalisiert – in einer trace() Meldung auszugeben. public class WorkerChain
{
private var workers:Vector.<WorkerInChain>; private var currentIndex:int = 0; private var currentRequest:PhotoRequestEvent; public function WorkerChain(){ workers = new Vector.<WorkerInChain>(); } public function addWorker(worker:WorkerInChain): void { workers.push(worker); } public function deleteWorker(worker:WorkerInChain): void { var checkFunction:Function = function ( item:WorkerInChain, index:int, list:Vector.<WorkerInChain>):Boolean {
211
Kapitel 4 return item != worker; } workers = workers.filter(checkFunction, null); } public function processRequest(request:PhotoRequestEvent): Boolean { currentRequest = request; return workers.some(checkAndExecute, null); } private function checkAndExecute( item:WorkerInChain, index:int, list:Vector.<WorkerInChain>):Boolean { if (item.checkAptitude(currentRequest)) { item.processRequest(currentRequest); return true; } return false; } } Listing 4.17: Die Zuständigkeitsketten-Klasse WorkerChain
Nun schauen wir uns sinnvollerweise als Nächstes die Kette an, also die Klasse WorkerChain (Listing 4.17). Hier passiert nun schon mehr. WorkerChain verwendet intern einen Vektor vom Typ WorkerInChain (das Interface, ich hatte es weiter oben schon erläutert). Es gibt die beiden Methoden addWorker() und deleteWorker(), die entsprechend BearbeiterObjekte hinzufügen oder wegnehmen. In der deleteWorker()-Methode kann man sehen, wie mithilfe des internen Iterator-Musters, das den Vektor in Form der filter()-Methode anbietet, das zu löschende Element im workers-Vektor herausgefiltert wird. Hier ist es ausreichend, eine temporäre Funktion zu erstellen, die den Filter repräsentiert. Man könnte natürlich auch eine reguläre Methode in der Klasse deklarieren, wie ich es bei der checkAndExecute()-Methode gemacht habe, die innerhalb von processRequest() als CallbackMethode verwendet wird. Die nächste Methode ist denn auch processRequest(). Sie merkt sich das übergebene Event erst einmal und wendet dann die some()-Methode des workers-Vektors an unter Verwendung der Methode checkAndExecute(). Die Methode some() funktioniert ja derart, dass sie durch den Vector iteriert und für jedes Element die checkAndExecute() Methode aufruft, bis letztere für ein Element einmal true zurückgibt. Dann hört sie auf und gibt selber true zurück. Wenn checkAndExecute() niemals true zurückgab, gibt some() seinerseits false zurück. Die Methode checkAndExecute() wiederum ruft auf jedem Element, also auf jeder Instanz von WorkerInChain (also den konkreten Bearbeitern), die Methode checkAptitude() auf. Diese prüft ja, ob dieser Bearbeiter die Aufgabe annehmen kann. Wenn er es kann, dann wird auf ihm die processRequest()-Methode aufgerufen, und die Bearbeitung kann losgehen.
212
Entwurfswerkzeuge Diese Implementierung ist recht schlank und garantiert, dass entweder der richtige Bearbeiter das Anfrage-Objekt erhält (also in unserem Fall das PhotoRequestEvent) und die Anfrage bearbeitet oder dass WorkerChain über den booleschen Wert false zurückmeldet, dass eine Bearbeitung leider nicht möglich war. Nachdem wir nun die ganze Zeit über die Bearbeiter, also die WorkInChain, sprechen, wollen wir dann auch mal einen sehen. Wir greifen uns einen heraus, z. B. den FlickrAdapter (da es sich hier um eine Beispielanwendung handelt, sind ohnehin alle gleich implementiert). Listing 4.18 zeigt ihn. import flash.display.Bitmap; public class FlickrAdapter implements PhotoAdapter, WorkerInChain { // Methodenimplementierungen von PhotoAdapter public function setSearchFilter(filter:SearchFilter): void { // Filter für Suche setzen } public function startRequest(): void { // Fotos anhand von Filter suchen } public function get photos(): Vector. { return new Vector.(); } // Methodenimplementierungen von WorkerInChain public function checkAptitude(check:PhotoRequestEvent): Boolean { return ( &&
check.community == PhotoRequestEvent.FLICKR this is check.requiredInterface
); } public function processRequest(request:PhotoRequestEvent): void { if (!checkAptitude(request)) { throw new Error("FlickrAdapter ist nicht zuständig!"); } trace("FlickrAdapter arbeitet"); } } Listing 4.18: Eine konkrete WorkerInChain-Klasse, der FlickrAdapter
Bei dieser Klasse muss unser Blick erst einmal oben bei der Deklaration innehalten. Wie ich schon weiter oben erwähnt habe, implementiert unser Adapter hier zwei Interfaces, denn er erfüllt auch zwei Rollen. Zum einen ist er ein PhotoAdapter, der also in der Lage ist, Fotos von
213
Kapitel 4 Community-Portalen abzuholen (in diesem Fall flickr). Zum anderen ist er ein WorkerInChain, also ein Bearbeiter im Sinne des Zuständigkeitskette-Musters. Um hier nicht auszuarten, habe ich die Methoden bezüglich des PhotoAdapters nicht wirklich implementiert. Das ist ja auch nicht der Kern dessen, was hier gezeigt werden soll. Wir stellen uns einfach vor, die Klasse kann sich mit der flickr-API verbinden und von dort Fotos holen. Interessanter sind die Methoden bezüglich WorkerInChain, also checkAptitude() und processRequest(). In checkAptitude() können wir nun endlich sehen, was es mit diesem requiredInterface-Property aus dem PhotoRequestEvent auf sich hat. Unser Adapter macht zwei Prüfungen. Einmal prüft er, ob in der Anfrage überhaupt nach flickr-Fotos gefragt wird, er vergleicht also den community-String mit der FLICKR-Konstante. Außerdem prüft er, ob er selbst denn das gewünschte Interface ist, denn so muss man die Überprüfung lesen: this is check.requiredInterface. this bezieht sich ja auf die Klasse selbst. FlickrAdapter implementiert das Interface Photo-
Adapter. Und in requiredInterface steckt eine direkte Referenz auf das Interface PhotoAdapter, das werden wir gleich noch sehen, wenn wir uns die Implementierung von PhotoRequestEvent ansehen. Und da also FlickrAdapter ein PhotoAdapter ist, ist diese Prüfung erfolgreich. Schauen wir uns also nun die PhotoRequestEvent-Klasse an (Listing 4.19). import flash.events.Event; public class PhotoRequestEvent extends Event
{
static public const FLICKR: String = "flickr"; static public const PICASA: String = "Picasa"; static public const SEVENLOAD: String = "sevenload"; static public const PHOTO_REQUEST_EVENT:String = "onPhotoRequestEvent" private var _searchFilter:SearchFilter; private var _community:String; private var _requiredInterface:Class; public function PhotoRequestEvent(){ super(PHOTO_REQUEST_EVENT, false); _requiredInterface = PhotoAdapter; } // ... Simple getter und setter für // searchFilter, community und requiredInterface // ... override public function clone():Event { var result:PhotoRequestEvent = new PhotoRequestEvent(); result.searchFilter = searchFilter; result.community = community;
214
Entwurfswerkzeuge return result; } } Listing 4.19: PhotoRequestEvent
Zur Klasse PhotoRequestEvent gibt es gar nicht allzu viel zu sagen. Es ist ein normales Event mit den schon angesprochenen Konstanten für die unterschiedlichen Foto-Communities. Hier könnte man die Implementierung auch noch anders gestalten. Man könnte die Identifizierer-Strings für die einzelnen Portale in eine extern XML packen, auf die sich dann der Dialog, in dem man ein Portal auswählen kann, als auch die konkrete Bearbeiter-Klasse, wie z. B. FlickrAdapter, beziehen. Damit wäre das Konstrukt unabhängiger von den konkret verfügbaren Adaptern. Wichtig aber ist, dass im Konstruktor _requiredInterface auf das PhotoAdapter-Interface gesetzt wird. Hierüber wird ja dann die Prüfung gemacht, ob ein Worker zuständig ist, wie wir weiter vorne gesehen haben. Um dieses Beispiel zu komplettieren, wollen wir noch einen Blick auf die Hauptapplikationsklasse werfen, die hier für dieses Beispiel den Zusammenbau übernommen hat. Hier werden wir sehen, dass es in meinem Beispiel gar keine echte View-Komponente gibt, sondern dass meine Hauptapplikationsklasse so tut, als wäre sie eine View. Um das Zuständigkeitsmuster zu demonstrieren und um nicht noch eine Klasse in diesem Beispiel ins Spiel zu bringen, soll das ausreichen. Listing 4.20 zeigt die Hauptapplikationsklasse Main. import flash.display.Sprite; public class Main extends Sprite { private var myClient:Client; public function Main():void { var chain:WorkerChain = new WorkerChain(); chain.addWorker(new FlickrAdapter()); chain.addWorker(new PicasaAdapter()); chain.addWorker(new SevenloadAdapter()); myClient = new Client(chain, this); var photoRequest:PhotoRequestEvent = new PhotoRequestEvent(); photoRequest.searchFilter = new SearchFilter(); photoRequest.community = PhotoRequestEvent.PICASA; dispatchEvent(photoRequest); } } Listing 4.20: Die Hauptapplikationsklasse Main
215
Kapitel 4 Wir sehen, die Klasse Main erzeugt die Zuständigkeitsketten-Klasse WorkerChain und konfiguriert es auch mit den notwendigen Bearbeitern, unseren Adaptern. Dies wäre im Prinzip auch wieder ein klassisches Szenario, in dem ein Dependency-Injection-Framework zum Einsatz kommen könnte, aber dazu kommen wir in einem der folgenden Abschnitte. Die Hauptapplikationsklasse erzeugt auch den Client, übergibt ihm die Kette und sich selbst (hier tut also die Main so, als wäre sie die besagte View-Komponente). Und um keine Zeit zu verlieren, wirft Main dann auch gleich das PhotoRequestEvent, das es zum einen mit dem SearchFilter konfiguriert (der uns nicht weiter interessiert) und – ganz wichtig – zum anderen mit dem konkret gewünschten Foto-Community-Portal. Jetzt haben wir alle Teile dieses erweiterten Zuständigkeitsketten-Musters beleuchtet. Wir sehen, dass es mit diesem Muster recht einfach wäre, beliebig viele neue Foto-Communities hinzuzufügen, dazu müsste nur ein weiterer Identifizierer-String in das Event und eine entsprechende PhotoAdapter-/WorkerInChain-Klasse hinzugefügt werden. Und auch das ließe sich wie gesagt noch über eine externe, z. B. in XML gepflegte, Konfiguration vereinfachen. In diesem Zusammenhang kann man sich das Zuständigkeitsketten-Muster auch als einen Plug-In-Mechanismus vorstellen. Die Adapter-Klassen fungieren hierbei als Plug-Ins, und die Kette ist der Container, in den die Plug-Ins eingefügt werden können. In einem Szenario, in dem die einzelnen Bearbeiter-Klassen, also die Plug-Ins, jeweils thematisch unterschiedliche Aufgaben übernehmen würden, also nicht alle als PhotoAdapter fungieren würden, sondern jeweils andere Aufgaben ausführten, könnte man sogar auf den Identifizierer-String verzichten. Dann würde die Wahl des geeigneten Bearbeiters z. B. allein über die Interface-Prüfung stattfinden, die ich anhand des FlickrAdapters in seiner Methode checkAptitude() erläutert hatte. Wenn also jeder Bearbeiter ein anderes zweites Interface implementieren würde, könnte man sehr elegant mittels des Propertys requiredInterface im Anfrage-Objekt (was hier die Event-Klasse PhotoRequestEvent war) und der Methode checkAptitude() in den jeweiligen Bearbeiter-Klassen die Entscheidung treffen, welcher Bearbeiter die Anfrage entgegennehmen sollte. Zu guter Letzt sei noch auf ein Beispiel hingewiesen, das Sie bestimmt schon kennen, das Bubbling von Events innerhalb von Display-Objekten in der Flash Player API. Events, die von einem DisplayObject geworfen werden, wandern ja, wenn es nicht unterdrückt wird, die Verschachtelungshierarchie erst von der Wurzel herunter zum Auslöser (die sogenannte Capture-Phase) und gehen dann vom Auslöser wieder hoch zur Wurzel (die BubblingPhase). Zugegeben, die Verhältnisse sind hier genau umgedreht. Der Klient ist in diesem Fall der Bearbeiter, denn er hat sich irgendwo in der Hierarchie (bzw. in diesem Zusammenhang besser Kette genannt) als Listener angemeldet und will das Event »bearbeiten«. Der Auslöser – das Objekt, das normalerweise den Klient repräsentiert – ist in diesem Fall ein Mitglied der Kette und stößt die Kette an. Davon abgesehen sind aber sonst alle Eigenheiten des klassischen Zuständigkeitsketten-Musters gegeben.
216
Entwurfswerkzeuge
4.4.4 Weitere Muster Vielen Entwickler verbinden mit dem Begriff Entwurfsmuster – oder auch Design Patterns im Englischen – die Gang of Four und ihr Buch, in dem sie die heute bekanntesten Entwurfsmuster beschreiben: Gamma, Riehle 2008. Die letzten drei Unterkapitel beschrieben Muster aus ihrem Katalog. Es ist jedoch so, dass es neben den von Gamma et al. beschriebenen Mustern auch noch andere gibt, die auch durchaus bekannt sind. Folgende beiden Muster möchte ich hier herausgreifen, die auch für Flash-Anwendungen sehr nützlich sein können.
Inversion of Control Fluent Interfaces
Inversion of Control Inversion of Control kann ehrlicherweise nicht wirklich als ein Muster betrachtet werden, das neben den in den vorigen Unterkapiteln erläuterten stehen könnte. Inversion of Control (also die Umkehrung eines Kontrollflusses) ist ein sehr grundlegendes Konzept, und letztlich haben wir es schon in einigen der bereits beschriebenen Muster sehen können. Um das Konzept zu erklären, sollten wir uns erst anschauen, wo denn das zugrundeliegende Problem liegt. Wir schreiben oft Klassen, in denen wir auf andere Klassen Bezug nehmen. Ein Sprite benutzt z. B. eine Tweening-Klasse, um eine Animation zu definieren. Diese Beziehung zwischen dem Sprite und dem Tween enthält zwei Stränge. Einmal benutzt das Sprite den Tween, um die Animation durchzuführen, und andererseits erzeugt das Sprite auch den Tween, um ihn dann später zu benutzen. Schauen wir uns zunächst nur den ersten Beziehungsstrang an, also die direkte Verwendung. Wenn wir als Entwickler diese Beziehung lose gekoppelt gestalten wollen, z. B. weil wir uns nicht an diesen einen konkreten Tween binden wollen, dann können wir definieren, dass der Tween ein Interface implementieren soll, und wir arbeiten dann nur noch auf den Methoden des Interface. Wir tun einfach so, als wäre uns egal, was sich denn hinter dem Interface für ein konkreter Tween verbirgt, wir arbeiten ab jetzt nur noch mit dem Interface, z. B. könnte es ITween heißen. Das ist natürlich aber eine sehr naive Denkweise. Zwar können wir Tween ein Interface implementieren lassen und Sprite kann dann gerne nur noch mit einem Interface arbeiten, aber ein Interface kann man ja nun mal nicht instanziieren, es ist ja keine konkrete Klasse. Damit kommen wir zum zweiten Beziehungsstrang, der Erzeugung des Tweens. Was bringt es uns also, dass wir für den ersten Beziehungsstrang ein Interface gebaut haben, wenn wir nun bei der Erzeugung damit gar nichts anfangen können? Das Beste, was wir tun könnten, ist, im Sprite eine Klassenvariable zu deklarieren, aber dann z. B. im Konstruktor doch eine konkrete Instanz vom Tween zu erzeugen, damit wir auch eine konkrete Klasse an der Hand haben (Listing 4.21).
217
Kapitel 4 import flash.display.Sprite; public class MySprite extends Sprite { private var tween:ITween; public function MySprite() { tween = new Tween(); } } Listing 4.21: Nutzloses Interface ITween?
Aber was würde uns das bringen? Nun, zumindest bezüglich des Beziehungsstranges Benutzen wäre nun eine Entkopplung erreicht, denn die Klassenvariable tween ist vom Typ ITween, also dem Interface. Sie ist damit also lose gekoppelt zu irgendeiner konkreten Implementierung. Das ist schon mal gut, aber dennoch haben wir die konkrete Klasse Tween ja immer noch im Konstruktor stehen. Für den Beziehungsstrang Erzeugung haben wir also noch keine lose Kopplung erreicht. Wir haben in den vergangenen Unterkapiteln schon einige Muster gesehen, die letztlich versuchen, die Erzeugung von Objekten aus den konkreten Anwendungsklassen (ich nenne sie meistens Klienten, weil sie ja die Nutznießer dieser Konstrukte sind) herauszuziehen. Da war z. B. die Fabrikmethode oder der Erbauer. Sie versuchen, die Erzeugung der konkreten Instanzen von Klassen, die ein Klient braucht, zu übernehmen, damit die Kopplung nicht im Klienten, sondern in diesen Hilfsklassen ausgelagert ist. Hinter diesen Mustern steht eine Idee: Die Verantwortung für die Erzeugung soll so weit wie möglich ausgelagert werden. Etwas knackiger hat es da Richard E. Sweet ausgedrückt: »Don‘t call us, we’ll call you (Hollywood’s Law).« (Association for Computing Machinery. Sweet et al. 1985, S. 218). Statt dass sich also der Klient selbst einen Service »ruft«, wird er ihm von einer anderer Klasse gegeben, z. B. einer Fabrikmethode. Dieses Prinzip ist bereits das Prinzip Inversion of Control. Nicht der Klient steuert die Erzeugung, sondern jemand anderer, und er übergibt dem Klient dann, was er braucht. Dabei dürfen wir uns keinen Illusionen hingeben. Das eigentliche Dilemma, dass nämlich irgendwo doch eine Kopplung zwischen einem abstrakten Interface, wie z. B. ITween, und einer konkreten Klasse, wie z. B. Tween, erfolgen muss, wird nicht gelöst. Alle Methodiken, Muster, Frameworks und dergleichen kommen um das Problem nicht herum. Es gibt aber eben Wege, um diese letztlich notwendige Kopplung möglichst elegant und flexibel zu gestalten. Die wichtigste Anforderung ist meistens einfach, dass die Kopplung nicht in den Klassen der Anwendung selbst stattfinden soll, sondern möglichst außerhalb, in irgendwelchen Hilfsklassen, die eher zur technischen Infrastruktur gehören, die nicht wirklich Teil des fachlichen Programmcodes sind. Ein spezielles, nicht nur in der Flash-Welt zurzeit recht populäres Muster ist hier die Dependency Injection (also zu Deutsch: Injizierung von Abhängigkeiten). Dieses Konzept schlägt vor, dass eine Klientenklasse, die eine konkrete Implementierung eines Interface oder einer abstrakten Klasse benötigt, diese Instanzen von außen übergeben bekommen soll. Die
218
Entwurfswerkzeuge Erzeugung wird also auch hier ausgelagert. Anders aber, als z. B. bei der Fabrikmethode, wo der Klient selber die Fabrikmethode bemüht, die gewünschte Instanz zu beschaffen, soll sich der Klient bei der Dependency Injection völlig passiv verhalten. Die Instanzen werden ihm deswegen z. B. direkt über den Konstruktor oder über definierte setter-Methoden mitgegeben. Der Klient hat also nicht nur keine Kopplung zur konkreten Instanz, die er benutzen will, er hat auch keine Kopplung zu irgendeiner Klasse, die ihm die Instanz beschaffen kann (siehe Listing 4.22 als Beispiel). Die Umkehrung des Kontrollflusses (also die Inversion of Control) findet als nicht nur bei der Erzeugung der benötigten konkreten Klasse statt, sondern auch bei der Benennung des ausgelagerten Hilfskonstrukts. import flash.display.Sprite; public class MySprite extends Sprite { private var tween:ITween; public function MySprite(newTween:ITween) { tween = newTween; } } Listing 4.22: Dependency Injection, die benötigte Instanz wird übergeben.
Nun stellt sich aber die Frage, woher denn nun die konkrete Instanz kommt, wer erzeugt sie denn jetzt? Das ist eine Implementierungsfrage und im Prinzip nicht mehr Teil des Dependency-Injection-Konzepts. Die Erzeugung der konkret benötigten Instanzen kann man auf viele Arten implementieren. Für eine konkrete Anwendung wichtig ist hier immer die Anforderung, dass die Erzeugung möglichst nicht im Code der konkreten Anwendung liegen soll, sondern eher in recht allgemein gehaltenen technischen Hilfsklassen. Diese sind dann unabhängig von der konkreten Anwendung und können deswegen gut wiederverwendet werden. Es gibt in der Flash-Gemeinde bereits einige Frameworks, die diese Aufgabe lösen. Manche von ihnen orientieren sich an Vorbildern aus der Java-Welt, wo dieses Thema schon seit einigen Jahren bearbeitet wird. Ich möchte an dieser Stelle kein einzelnes Framework herausgreifen, sondern stattdessen eine simple eigene Implementierung zeigen, um die Funktionsweise der Dependency Injection zu verdeutlichen. Ich trete damit ganz sicher nicht in Konkurrenz zu den existierenden Frameworks, die heute schon eine beachtliche Funktionsvielfalt bieten, die deutlich über das hinausgeht, was ich hier im Rahmen der Dependency Injection demonstrieren könnte. Für meine Beispielimplementierung bleiben wir ruhig bei dem Sprite und dem Tween. Wir wollen die beiden nun mittels eines Dependency-Injection-Konstrukts verbinden. Zuerst ein sehr simples Beispiel, das Listing 4.23 zeigt. import flash.display.Sprite; public class Main extends Sprite {
219
Kapitel 4 private var client:MySprite; public function Main():void { client = new MySprite(new Tween()); } } Listing 4.23: Simple Dependency Injection, in der Main-Klasse
In diesem kurzen Beispiel wird einfach eine Instanz von MySprite erzeugt und dieser die konkrete Tween-Instanz übergeben. Das geht natürlich, und es ist auch ein einfaches Beispiel für Dependency Injection, aber wir hatten ja gesagt, dass wir die Erzeugung aus unserer konkreten Anwendung möglichst raus haben wollen, sie soll also auch nicht in der Hauptapplikationsklasse stattfinden. Wir bauen uns also technische Hilfsklassen, die losgelöst sind von unserer konkreten Anwendung. Einmal brauchen wir eine Möglichkeit, die Abhängigkeiten zu konfigurieren, und außerdem benötigen wir eine Steuerung, die uns dann Instanzen mit den aufgelösten Abhängigkeiten liefert, damit diese Abhängigkeiten nicht mehr in unserem Code der Anwendung auftauchen. Fangen wir mit der Konfiguration an. Zunächst schreibe ich eine allgemeine Configuration-Klasse, die als Superklasse für konkrete Konfigurationen dienen wird. Listing 4.24 zeigt sie. import flash.utils.Dictionary; public class Configuration { private var registry:Dictionary; public function Configuration() { registry = new Dictionary(); } protected function register( clientDefinition:Class, serviceDefinition:Class):void { registry[clientDefinition] = serviceDefinition; } public final function getDependency(clientDefinition:Class):Class { return registry[clientDefinition]; } } Listing 4.24: Die allgemeine Configuration-Klasse
220
Entwurfswerkzeuge Ich habe sie bewusst sehr simpel gehalten. Unsere Konfiguration bietet nicht viel mehr als die Möglichkeit, für einen Klienten genau eine abhängige Klasse zu definieren, und zwar via die register()-Methode. Das ist natürlich nicht besonders mächtig, aber es soll für unser Beispiel reichen. Wir können hier also ganz simpel ausdrücken, dass ein Klient zum Arbeiten einen Service braucht und welcher das sein soll. Damit modellieren wir also die Abhängigkeiten zwischen diesen beiden Klassen. Weil wir das auf einer abstrakten Ebene tun, verwenden wir hierfür direkt Referenzen auf die Klassenobjekte und nicht etwa konkrete Instanzen, denn an dieser Stelle soll ja noch keine Instanz erstellt werden. Diese Abhängigkeit speichern wir in einem Dictionary, den ich registry genannt habe. Damit man diese gespeicherten Abhängigkeiten nun auch später auflösen kann, gibt es noch eine Methode getDependency(), die für einen Klient die konfigurierte abhängige Klasse liefert. Auch hier wird wieder mit Referenzen auf Klassenobjekte gearbeitet und nicht mit Instanzen. Eine konkrete Konfiguration für unser Beispiel mit MySprite und Tween könnte nun so aussehen wie in Listing 4.25 gezeigt. public class MySpriteConfig extends Configuration { public function MySpriteConfig() { super(); register(MySprite, Tween); } } Listing 4.25: Konkrete Konfiguration
Auch hier wieder sehr einfach. Wir lassen unsere konkrete Konfiguration MySpriteConfig von Configuration erben und konfigurieren unsere gewünschte Abhängigkeit. Zur Erinnerung, MySprite darf mit Tween konfiguriert werden, weil MySprite ja ein Objekt vom ITween in seinem Konstruktor erwartet, und Tween implementiert das Interface ITween. Als Nächstes schauen wir uns die Klasse an, die nun die Konfiguration auswertet und die Abhängigkeit konkret auflöst, ich habe sie DependencyResolver genannt (Listing 4.26). public class DependencyResolver { private var _configuration:Configuration; public function DependencyResolver(newConfig:Configuration) { _configuration = newConfig; } public function getNew(classDefinition:Class):Object { var dependendClass:Class = _configuration.getDependency(classDefinition);
221
Kapitel 4 return new classDefinition( new dependendClass() ); } } Listing 4.26: Der DependencyResolver verarbeitet die Konfiguration.
Auch sie ist einfach gehalten. Über seinen Konstruktor übergibt man ihr eine Konfiguration. Danach kann man mit der getNew()-Methode eine Instanz von der angegebenen Klasse erzeugen. getNew() holt sich dazu zuerst aus der Konfiguration die Klasse, die in Abhängigkeit von der im Parameter angegebenen Klasse steht. Danach werden beide instanziiert, und die abhängige Klasse wird der angegebenen im Konstruktor übergeben. Das ist hier natürlich letztlich eine Konvention. Die Injizierung der abhängigen Klasse kann in meinem Beispiel nur über den Konstruktor erfolgen, und – wie ich schon sagte – es darf auch nur eine abhängige Klasse sein. Wie gesagt, es existieren im Flash-Bereich Frameworks, die deutlich mehr Möglichkeiten bezüglich dieses Themenbereichs bieten. Nachdem wir nun wissen, wie die Dependency Injection in meiner Beispielimplementierung grundsätzlich funktioniert, wollen wir die Benutzung natürlich auch noch sehen, hier also die angepasste Hauptapplikationsklasse (Listing 4.27). import flash.display.Sprite; public class Main extends Sprite { private var client:MySprite; private var resolver:DependencyResolver; public function Main():void { resolver = new DependencyResolver(new MySpriteConfig()); client = resolver.getNew(MySprite) as MySprite; } } Listing 4.27: Die neue Hauptapplikationsklasse
Wir erzeugen hier also eine Instanz vom DependencyResolver, übergeben ihm im Konstruktor unsere gewünschte Konfiguration und lassen uns danach eine Instanz von MySprite geben. Das Schöne ist hier nun, dass wir uns nicht mehr um die tatsächliche Erzeugung kümmern müssen, also auch nichts von Tween und ITween wissen müssen, das übernimmt nun unsere Dependency-Injection-Hilfskonstruktion. Fassen wir noch mal zusammen, was haben wir durch diese Implementierung erreicht?
Die fachlichen Klassen unserer Anwendung, also Main und MySprite, kennen keine Kopplung zwischen MySprite und Tween mehr. MySprite kennt nur noch das Interface ITween.
222
Entwurfswerkzeuge
MySprite könnte nun wunderbar für sich allein getestet werden, denn statt einer konkreten Tween-Klasse könnten wir ja auch ebenso gut eine leere Testvariante an MySprite übergeben, um das Testen zu vereinfachen.
Wir müssen keine individuellen Fabrikmethoden-Konstrukte mehr bauen, denn wir haben nun einen generischen Ansatz für die lose Kopplung bei der Erzeugung von Objekten (theoretisch jedenfalls, wenn meine Implementierung nicht einfach nur dem Zweck dieses Beispiels hätte genügen wollen). Echte Dependency-Injection-Frameworks arbeiten freilich etwas anders und verfügen über deutlich mehr Komfort. Ein wesentlicher Unterschied ist z.B., dass sie die Konfiguration auch über externe XML-Dateien erlauben. Auch für das Konzept der Dependency Injection gibt es wieder ein paar Punkte, die man bedenken sollte, bevor man nun alle Klassen komplett losgelöst voneinander strukturiert. Anwendungen, die per Dependency Injection verdrahtet werden, sind für Entwickler schwer zu durchdringen. Eine Klasse, die alle ihre benötigten abhängigen Klassen von außen übergeben bekommt, steht komplett für sich, was ja auch der Sinn ist. Dadurch aber wird es für einen Entwickler schwer zu verstehen, woher eine Klasse denn nun ihre Objekte bekommt. Zwar lassen sich die Beziehungen nun anhand der Konfiguration erkennen, aber die liegt nun mal an einem anderen Ort als die Klassen selbst, wodurch sich für einen Entwickler kein idealer Lesefluss im Code ergibt. Heutige Entwicklungsumgebungen z. B. bieten ja Möglichkeiten, per Tastendruck von Referenz zu Referenz zu springen. Diesen Komfort kann man hier schwer nutzen, denn man würde immer nur in Interfaces oder abstrakte Klassen springen. Man sollte sich also gut überlegen, welche Balance man zwischen den Vorzügen der losen Kopplung und damit verbesserter Wartung einerseits und guter Lesbarkeit andererseits wählt. Im Allgemeinen ist es auch hier wieder ausreichend, wenn nur in sich geschlossene Module einer Anwendung über Dependency Injection miteinander verdrahtet werden anstatt jede einzelne Klasse.
Fluent Interfaces Als letztes »Muster« in diesem Kapitel möchte ich noch etwas ganz anderes zeigen. Die vorigen Muster und Konzepte hatten immer Ziele wie lose Kopplung, Wiederverwendbarkeit, Testbarkeit, Erweiterbarkeit usw. In diesem Muster soll es nun um etwas anderes gehen, nämlich um gute Lesbarkeit. Ob das Konzept der Fluent Interfaces dies ermöglicht, möchte ich nicht vorwegnehmen, letztlich müssen Sie das sowieso für sich selbst beurteilen. Der Begriff »Fluent Interfaces« wurde von Martin Fowler und Eric Evans geprägt. Er bezeichnet Klassenschnittstellen, die mit Methoden aufgebaut sind, die zusammen eine kleine eigene Sprache ergeben. Um hier nicht viel Worte zu machen, zeige ich einfach ein Beispiel: var calculatedValue:Number = new Calculation(4).plus(6).dividedWith(2).result;
Das ist ein recht einfach aussehendes Beispiel für ein Fluent Interface. In diesem Fall wird hier das spezielle Konzept des »Method Chainings«, also des Verkettens von Methodenauf-
223
Kapitel 4 rufen, verwendet, um das allgemeinere Konzept der Fluent Interfaces zu realisieren. Fluent Interfaces müssen aber nicht immer so aufgebaut sein. Das obige Beispiel könnte auch anders realisiert sein: var calculation:Number; startCalculation(4); plus(6); dividedWith(2); result(calculation);
Dieses Beispiel verwendet kein Method Chaining. Stattdessen würde die umgebene Klasse wahrscheinlich von einer bestimmten Hilfsklasse erben und sich einen Kontext merken, damit die Methodenaufrufe jeweils auf dem Ergebnis des Vorgängers arbeiten könnten. Speziell aber die erste Variante erzeugt einen Lesefluss, der in bestimmten Situationen das Programmieren vereinfachen kann, erst recht, wenn man bedenkt, wie die Auto-Vervollständigung heutiger Entwicklungsumgebungen hier nach jeder Methode die möglichen Folgemethoden anbieten könnte. Wie gesagt, hier geht es nicht um Erweiterbarkeit, Kopplung oder dergleichen, hier geht es um Lesbarkeit und Verständlichkeit von Code, eine Qualitätskomponente von Software, die man nicht unterschätzen sollte, schließlich schreiben wir Code in heutigen Programmiersprachen nicht für den Computer, sondern für uns. Wenn sich Computer eine Sprache wünschen könnten, hätten viele von uns wohl selber keinen Spaß mehr am Programmieren. Wenn wir aber also Code für Menschen schreiben, dann sollten wir auch dafür sorgen, dass wir ihn gut verstehen können. Martin Fowler verwendet im Zusammenhang mit Fluent Interfaces auch das Wort der Domain-Specific Languages. Gemeint ist hier, dass ja speziell in meinem ersten Beispiel, in dem ich das Method Chaining verwendet habe, im Idealfall sich fast ein richtig lesbarer Satz bilden lässt: »Erzeuge eine neue Rechnung mit der Zahl 4, addiere dann 6 und dividiere mit 2. Gib dann das Resultat«. Dies ist auch der Grundgedanke bei Fluent Interfaces. Anhand der Aneinanderreihung der Befehle, die hierfür auch sprechend benennt sein müssen, soll sich ein fließendes, satzähnliches Konstrukt ergeben. Die Wörter, also die Befehle, die hier zur Verfügung stehen, bilden den Sprachstamm, und das Themenfeld, das sie abdecken, also in meinem Fall simple Grundrechenarten, bildet die Domäne. Deswegen Domain-Specific Language, also grob übersetzt: einsatzbereichsspezifische Sprache. Domain-Specific Languages stehen damit im Gegensatz zu allgemeinen Hochsprachen wie z. B. Java oder ActionScript. Solche Sprachen sollen universell einsetzbar sein und grundsätzlich mit jedem Themengebiet klarkommen. DSLs sollen sich nur um eine bestimmte, klar abgegrenzte Aufgabe kümmern. Sie sind nicht unbedingt besonders mächtig, aber innerhalb ihres Bereichs sehr effizient, weil sie die Dinge sehr direkt beim Namen nennen können, da sie ja sonst nichts zu leisten brauchen. Der Schlüssel zum Erfolg einer DSL liegt denn auch darin, wie gut sie den Themenbereich widerspiegelt, den sie abdecken soll. Bekannte Beispiele für DSLs sind reguläre Ausdrücke, SQL Statements oder auch BatchSkripte. Nun hat die Erstellung einer DSL mittels Fluent Interfaces einen Haken. Sie zu konstruieren, ist alles andere als trivial. Abgesehen von der Schwierigkeit, erst einmal die Sprache an sich zu definieren, damit sie auch wirklich ihre Stärken ausspielen kann, ist das Konstruie-
224
Entwurfswerkzeuge ren von Fluent Interfaces mittels Method Chaining auch auf der Implementierungsseite eine durchaus harte Nuss. Im Java-Bereich, in dem Method Chaining und Fluent Interfaces bereits etwas länger bekannt sind, gibt es deswegen auch schon erste Modellierungswerkzeuge, die helfen sollen, eine solche »Sprache« auf visuellem Weg zu gestalten. Für ActionScript gibt es zurzeit noch kein solches Werkzeug. Wir wollen dennoch wagen, eine kleine beispielhafte Implementierung eines Fluent Interface zu realisieren anhand meines kleinen Rechenbeispiels. Wir bauen also ein auf Method Chaining basierendes Fluent Interface, letztlich also eine eigene kleine DSL, und zwar für simple Rechenoperationen. Dabei müssen wir uns zunächst überlegen, welche Konstrukte wir erlauben wollen. Da in Fluent Interfaces immer Sätze gebildet werden von aufeinanderfolgenden Operationen, müssen wir eine Vorstellung davon gewinnen, welche Satzkombinationen wir denn erlauben und unterstützen wollen. Um einen einfachen Einstieg zu haben, können wir erst einmal mit den grundsätzlichen Funktionen anfangen, die wir unterstützen wollen. Für dieses Beispiel sollen das sein:
Angabe eines Anfangswerts, mit dem die Rechnung beginnen soll Einen Wert zum bisherigen Zwischenergebnis hinzuaddieren Einen Wert vom bisherigen Zwischenergebnis abziehen Einen Wert mit dem bisherigen Zwischenergebnis multiplizieren Das bisherige Zwischenergebnis durch einen Wert dividieren Das bisherige Zwischenergebnis als Endergebnis vom Typ Number ausgeben Das sind recht einfache Regeln, nämlich die vier Grundrechenarten. Wir unterstützen keine Klammersetzung, wir unterstützen auch nicht Punkt-vor-Strich-Rechnung, wir unterstützen aber indirekt Variablen, weil wir statt eines konkreten Werts ja durch ActionScript eh schon Variablen verwenden können. Deswegen ist dies auch keine Funktionalität, die wir extra angeben müssen. Bezüglich der Bildung der möglichen Sätze müssen wir uns nun noch überlegen, ob es irgendwelche Einschränkungen geben soll, wie die verschiedenen Funktionen zu einem Satz kombiniert werden dürfen. In der Mathematik gibt es hier keine Einschränkungen, man kann alle vier Grundrechenarten beliebig miteinander kombinieren. Die einzige Einschränkung soll sein, dass mit dem Endresultat nicht direkt weitergerechnet werden darf. Dies ist eine technische Einschränkung, die wir treffen müssen, denn das Endresultat wird vom Typ Number sein, und die Number-Klasse kennt natürlich unsere Sprachfunktionen nicht. Verwendet man also die Funktion für das Endresultat, ist der Satz damit beendet. Die Tatsache, dass wir bezüglich der Kombinationsmöglichkeiten im Prinzip keine Einschränkungen haben, macht die Konstruktion unseres Fluent Interfaces deutlich einfacher, denn es bedeutet, dass uns nach jedem Zwischenergebnis – egal welche Funktion gerade als Letztes verwendet wurde – immer der genau gleiche Funktionsumfang zur Verfügung steht, wir also hier keinerlei Unterscheidungen treffen müssen. Das führt letztlich dazu, dass jede Funktion als Rückgabewert einfach immer wieder eine Instanz der gleichen Hilfsklasse zurückgeben kann, die dann wieder die gleichen Funktionen für den nächsten Satzteil anbietet. Nehmen wir an, die Funktion Multiplizieren würde aus irgendeinem Grund
225
Kapitel 4 vorgeben, dass direkt nach dem Multiplizieren kein Subtrahieren erlaubt wäre, dann sähe unsere Konstruktion gleich etwas komplizierter aus, denn dann müsste die Funktion Multiplizieren die Instanz einer anderen Klasse zurückgeben, als es die anderen Funktionen täten. Denn in dieser von Multiplizieren zurückgegebenen Klasse dürfte ja nun die Funktion Subtrahieren nicht mehr angeboten werden. Je weiter wir das spinnen, umso komplizierter kann das werden. Es könnte zum Beispiel auch sein, dass eine Funktion unterschiedlich viele Parameter erwarten muss, je nachdem, welche Funktion nach ihr kommt. Das wäre ein kniffliges Problem, denn wie sollte sie wissen, was nach ihr aufgerufen würde. Mit diesen vielen Detailproblemen wollen wir uns hier aber nicht weiter beschäftigen, wir haben diese Einschränkungen glücklicherweise nicht (ein Vorzug der Freiheit eines Autors). Außerdem kommt uns ein weiterer Umstand zugute. Jede Funktion arbeitet mit dem gleichen Parametertyp, nämlich Zahlen. Auch das vereinfacht die Konstruktion. Schauen wir uns also die Implementierung an. Da es sich hier um ein wirklich recht einfaches Beispiel handelt, kann der gesamte Sprachkern in einer übersichtlichen Klasse abgehandelt werden. Ich nenne sie Calculation. Listing 4.28 zeigt sie. public class Calculation { private var _result:Number; public function Calculation(startValue:Number) { _result = startValue; } public function plus(value:Number):Calculation { return new Calculation(_result += value); } public function minus(value:Number):Calculation { return new Calculation(_result -= value); } public function dividedBy(value:Number):Calculation { return new Calculation(_result /= value); } public function times(value:Number):Calculation { return new Calculation(_result *= value); } public function get result():Number { return _result; } } Listing 4.28: Unser einfaches Fluent Interface, Calculation
226
Entwurfswerkzeuge Wir können sehen, die Klasse ist recht einfach zu verstehen. Der Konstruktor gilt als Startpunkt eines neuen Satzes. Ihm übergibt man den Anfangswert. Das Zwischenergebnis wird immer in der privaten Variablen _result zwischengespeichert. Die vier Grundrechenarten sind als einzelne Methoden dargestellt. Beachten Sie hier, dass die Namen nicht zwingend danach benannt sind, dass sie für sich allein genommen sinnvoll sind, sondern dass sie es innerhalb eines Satzes sind. Die Methode times z. B. ergibt vom Namen her für sich erst wenig Sinn, innerhalb eines Satz aber, der z. B. lauten könnte: new Calculation(4).times(2). result;, ergibt es dann deutlich mehr Sinn (times steht hier übersetzt für »mal«). Auch die anderen Namen sind danach gewählt, dass sie innerhalb eines Satzes sprechend sind anstatt für sich allein. Aufgrund dieser anderen Sichtweise ist es schwierig, eine Standard-API-Dokumentation für ein Fluent Interface zu erstellen, in dem die einzelnen Methoden ja normalerweise jede für sich dokumentiert werden. So ein Vorgehen würde bei einem Fluent Interface zu Verwirrung führen, denn die einzelnen Methoden machen eben nur im Zusammenhang mit den gebildeten Sätzen einen Sinn. Die Dokumentation muss deswegen auch eher auf die Satzbildung und die unterstützte Grammatik eingehen. Der eigentliche Knackpunkt aber ist, dass jede Methode, abgesehen von result, wieder ein neues Calculation-Objekt erzeugt und ihm das bisherige Zwischenergebnis übergibt, damit dieses neue Objekt mit dem Wert gleich weiterrechnen kann. So ergibt sich letztlich der Satz. Fall Sie sich über die Schreibweise in den Methoden wundern, dort habe ich, um Platz zu sparen, in einer Zeile gleich drei Dinge gemacht. Einmal wird auf dem Zwischenergebnis _result der neue Wert value jeweils hinzuaddiert, abgezogen, dividiert oder multipliziert. Dann wird dieses Ergebnis wieder in _result gespeichert und gleichzeitig als Parameter an den Konstruktor für das neue Calculation-Objekt übergeben. Auseinandergezogen hätte das auch so aussehen können (beispielhaft für Addition): _result = _result + value; return new Calculation(_result);
Schauen wir uns der Vollständigkeit halber an, wie wir nun unsere neue kleine DSL benutzen können (Listing 4.29). import flash.display.Sprite; public class Main extends Sprite { public function Main():void { var test:Number = new Calculation(4).plus(6).dividedBy(2).result; trace(test); } } Listing 4.29: Verwendung von Calculation
Gönnen Sie sich ruhig mal die Zeit, und implentieren Sie Calculation testweise nach (oder laden es von der begleitenden Website dieses Buches herunter). Wenn Sie eine Entwicklungsumgebung verwenden, die Autovervollständigung unterstützt, werden Sie rasch
227
Kapitel 4 sehen, wie schnell und einfach man die Funktionen hintereinander wegtippen kann. Das ist eine der Stärken des Method Chainings. Wir können dieses Kapitel natürlich nicht abschließen, ohne noch ein paar Warnungen auszusprechen. Ich habe ja schon mehrfach drauf hingewiesen, Fluent Interfaces zu konstruieren ist alles andere als einfach, auch wenn das mein Beispiel nicht vermuten lässt, aber es ist halt auch ein sehr simples Beispiel (und übrigens letztlich bezüglich seiner Effektivität auch ein fragwürdiges, denn statt meiner Funktionen könnte man ja auch einfach schreiben: var test:Number = (4+6)/2;). Dazu kommt, dass man aufpassen muss, wie man mit den Daten umgeht, die innerhalb eines solchen Fluent Interface bearbeitet. Nehmen wir an, wir würden nicht mit simplen Zahlen arbeiten, sondern wir würden mittels eines Fluent Interface eine Animation konfigurieren (Listing 4.30). var myTween:Animate = new Animate(mySprite) .forAttribute("x") .from(20) .to(40) .withAnimationStyle("easeOut"); Listing 4.30: Beispiel eines Fluent Interface für Tweenings
In diesem Fall arbeiten wir nicht auf primitiven Daten, sondern auf durchaus komplexeren, nämlich Sprites oder MovieClips und Tweenings. Es ist hier sinnvoll, gedanklich zwischen den manipulierten Daten und der Sprache, die die Daten manipuliert, zu unterscheiden. In seinem Artikel (Martin Fowler 08.04.2008) beschreibt Martin Fowler, dass es eine gute Idee ist, die Definition, also die Klassen, die die Sprache beschreiben, zu trennen von den Objekten, die durch die Sprache manipuliert werden. Wir hätten also auf der einen Seite Klassen, die die Sprache beschreiben und somit die Konstruktion von Sätzen wie in Listing 4.30 ermöglichen. Und auf der anderen Seite hätten wir die Klassen, wie z. B. Sprite oder MovieClip, sowie normale Tweening-Klassen, die theoretisch auch ganz normal ohne die Sprache verwendet werden könnten. Diese Trennung hat den Vorteil, dass die manipulierten Objekte dann nicht von der Sprache abhängig sind, denn es gibt sicherlich auch Situationen, in denen man so ein Objekt direkt manipulieren will, ohne ein Fluent Interface zu benutzen.
4.5 Bibliotheken Das Wort Bibliothek meint im Zusammenhang mit Software und Code erst mal nichts anderes als eine Ansammlung von Code. In ActionScript sind hier meistens Klassen gemeint, aber im Prinzip könnte auch eine Bibliothek von Codeschnipseln gemeint sein, die kleinere Aufgaben erledigen. So gut wie jeder Entwickler baut in seiner Karriere eine kleine eigene Bibliothek von Codeschnipseln, Klassen und vielleicht auch größeren Konstrukten auf, die er über die Zeit entwickelt und für weiterverwertbar befunden hat. Hier haben wir auch schon einen der Gründe für die Existenz von Bibliotheken. Es geht um Wiederverwendung von Code, Klassen oder ganzen Klassenstrukturen.
228
Entwurfswerkzeuge Bibliotheken, die Sie für sich selbst verwenden, sind meistens mehr oder minder durchstrukturierte Sammlungen, die nicht unbedingt stark dokumentiert sind. Warum auch, sie werden ja nur von Ihnen selbst verwendet, es besteht kein großer Bedarf für eine saubere Strukturierung oder Dokumentation. Erst wenn eine Bibliothek oder Teile daraus für andere zugänglich gemacht werden sollen, entstehen plötzlich neue Anforderungen an sie. Denn dann muss der Code plötzlich verständlich sein, er muss zumindest grundsätzlich dokumentiert werden, und auch die Struktur muss so gewählt werden, dass andere Entwickler damit flexibel und effizient arbeiten können. Auch treten dann plötzlich Anforderungen hinsichtlich einer verständlichen Schnittstelle auf, die den Nutzern der Bibliothek eine schnelle Einarbeitung und Nutzung der bereitgestellten Funktionalität ermöglichen. Solange wir also unsere eigene kleine Codesammlung nur für uns selbst verwenden, bleibt die Komplexität der Pflege und Weiterentwicklung dieser Sammlung überschaubar. Erst wenn wir öffentlich machen, was wir da gebaut haben, wird die Sache komplizierter. Vielleicht haben Sie schon mal eigenen Code anderen Entwicklern, vielleicht auch nur im kleinen Kreis, zur Verfügung gestellt. Wenn ja, haben Sie sicher die Erfahrung gemacht, dass relativ schnell Wünsche für Veränderungen oder Erweiterungen aufkommen. Hier wird noch ein Parameter gebraucht, da noch eine weitere Methode. Fehler oder inkonsistentes Verhalten werden bemängelt (auch wenn Sie dafür gute Gegenargumente nennen könnten), und manch ein Entwickler braucht »ganz schnell« einen Bugfix oder eine Änderung, weil er ihren Code in einem Projekt verwendet, das morgen früh fertig werden muss. Die spannenden Themen rund um Bibliotheken ranken sich also um die, die anderen Entwicklern zugänglich gemacht werden, nicht so sehr um die privaten kleinen Codesammlungen, auch wenn diese manchmal beachtliche Größen annehmen können. Dennoch sind Sie bei solchen Codesammlungen Ihr alleiniger Herr und müssen sich grundsätzlich mit niemandem abstimmen. Wir wollen uns deswegen hier mit den öffentlich gemachten Bibliotheken beschäftigen, die sich den Ansprüchen und Bedürfnissen von anderen Entwicklern aussetzen. Ich verwende hier im Weiteren den Begriff Bibliothek als Sammelbegriff für unterschiedliche Konstrukte wieder verwendbarer Software. Folgende Arten möchte ich unterscheiden:
Klassensammlung: Hierunter sind meistens recht simple einzelne Klassen zu sehen, die nicht unbedingt in Beziehung zueinander stehen, sondern eher für sich jeweils kleinere Aufgaben lösen. Meistens handelt es sich um kleine Utility-Klassen. Der Flash Player bietet z. B. auch solche Klassen an, darunter z. B. Proxy, Timer, RegExp und einige mehr.
Klassenstrukturpaket: Eine Sammlung von größtenteils untereinander in Beziehung stehenden Klassen, die zusammen eine bestimmte Aufgabe oder Aufgabenfamilie erfüllen. Diese Bibliotheken gibt es recht oft, darunter fallen z. B. Bibliotheken für Animationssteuerung, 3D-Darstellung, physikalische Simulationen und mehr. Sie werden meistens in Form der konkreten Klassendateien oder in Form der in Flash bekannten .swcDateien (Flash-spezifische Archivdateien, die Klassendefinitionen in vorkompilierter Form beinhalten) zur Verfügung gestellt. In der Regel werden diese Bibliotheken in die nutzende Anwendung mit hineinkompiliert, das muss aber nicht so sein. Klassenstrukturpakete sind zudem meistens Whiteboxes, ihre innere Implementierung ist also nicht zwingend versteckt, sondern bewusst einsehbar.
229
Kapitel 4
Komponente: Für Softwarekomponenten gibt es recht klare Definitionen. Für die Diskussion von Bibliotheken innerhalb dieses Buches möchte ich auf diese Definitionen aber nicht genauer eingehen, weil sie uns recht schnell in die Betrachtung von Strukturen und Paradigmen großer Softwaresysteme bringen. Im Umfeld von Java EnterpriseApplikationen oder auch Microsoft .NET-Applikationsumgebungen haben Softwarekomponenten eine recht konkret definierte Bedeutung, die sich auch in ganz konkreten Anforderungen hinsichtlich Schnittstellen, Schnittstellenverträgen usw. niederschlägt. Eine Betrachtung dieser Bereiche würde uns hier zu weit führen. Deswegen verwende ich für Komponenten folgende Definition: Eine Komponente ist eine in sich geschlossene Einheit mit einer definierten Schnittstelle. Eine Komponente versteckt ihre interne Implementierung und stellt somit eine Blackbox dar. Eine Komponente ist idealerweise unabhängig von anderen Komponenten oder einzelnen Klassen. Eine Komponente kann einzeln ausgespielt werden, was bedeutet, dass sie als .swf-Datei von einer Anwendung nachgeladen werden kann, wenn sie benötigt wird. Wenn Sie sich mit den offiziellen Definitionen von Softwarekomponenten beschäftigen, werden Sie feststellen, dass diese Definition von den offiziellen nicht so weit entfernt ist. Warum muss man diese Arten überhaupt unterscheiden? Alle drei Arten haben ihre ganz eigene Daseinsberechtigung und werden in unterschiedlichen Szenarien angewandt. Alle drei haben auch sehr unterschiedliche Anforderungen hinsichtlich Grad der Strukturierung und vor allem dem Aufwand, den man investieren muss, wenn man die eine oder andere Art öffentlich anbietet und betreut. Aus Nutzersicht haben alle drei auch unterschiedliche Vor- und Nachteile, was die Benutzbarkeit angeht. Bevor wir uns ein wenig mehr im Detail mit den Arten beschäftigen, möchte ich ein paar Beispiele aus der Flash-Welt nennen und einordnen, damit wir eine Vorstellung von den Arten bekommen.
as3corelib: Eine Klassensammlung, die viele kleine nützliche Helferklassen bietet. Easing Equations von R. Penner: Eine Klassensammlung, aber ein Grenzfall. Die Klassen stehen insofern in einem Zusammenhang untereinander, dass sie alle die gleiche Art von Schnittstelle mit den Methoden easeIn, easeOut, easeInOut anbieten. Es gibt aber keine zusammenführende abstrakte Schnittstellenklasse oder dergleichen.
Papervision3D: Ein Klassenstrukturpaket: Die Klassen erfüllen eine gemeinsame Aufgabe und haben größtenteils abstrahierte Schnittstellen. Die Struktur ist aber bewusst offengehalten, sodass eine Erweiterung durch den Benutzer möglich und gewollt ist.
Google Maps API for Flash: Eine Komponente. Bei der Google Maps API gibt Google eine konkrete Funktionalität vor und bietet eine definierte Schnittstelle, die die Nutzung der Komponente ermöglicht. Im Gegensatz zu einem Klassenstrukturpaket ist die Implementierung verborgen und auch nicht so angelegt, dass sie ausgetauscht oder verändert werden könnte. Eine Erweiterung ist stattdessen nur über die definierte Schnittstelle möglich bzw. gewollt.
Flash Player: Das klingt erst einmal verwunderlich, aber letztlich ist der Flash Player eine Komponente. Er hat eine definierte Schnittstelle, nämlich die Flash Player API, und die Erweiterungsmöglichkeiten sind auch nur über die Schnittstelle möglich. Der Flash Player ist auch eine Blackbox.
230
Entwurfswerkzeuge Weil nun der Begriff schon fiel, noch ein Wort zum Begriff API. API steht für Application Programming Interface, bezeichnet also eine Schnittstelle. Die API bezeichnet also nicht eine Komponente als solche, sondern nur ihre Schnittstelle. Deswegen spricht man z. B. auch vom Flash Player (die Komponente) und von der Flash Player API (die Schnittstelle). Im Zusammenhang mit dem Begriff API bezeichnet eine Schnittstelle durchaus eine recht große Schnittstelle, die aus vielen, vielen Klassen bestehen kann, so wie es beim Flash Player oder auch bei der Google Maps API der Fall ist. Alle diese Klassen zusammen ergeben dann die Schnittstelle. Von den drei hier vorgestellten Bibliotheksarten möchte ich mich mit den Klassenstrukturpaketen und den Komponenten noch etwas detaillierter beschäftigen.
Klassenstrukturpakete Ein wesentlicher Kern von Klassenstrukturpaketen ist ihr Whitebox-Merkmal. Whitebox bedeutet, dass das Innere der Bibliothek nicht versteckt, sondern ganz bewusst nach außen sichtbar gemacht wird. Dies passiert aus zwei Gründen. Entweder soll Nutzern solcher Bibliotheken die Erweiterung mittels Vererbung ermöglicht werden, um zusätzliche individuelle Funktionalität zu ermöglichen, oder man möchte den Nutzern der Bibliothek den größtmöglichen Einfluss auf die Nutzung der Klassen geben. Klassenstrukturpakete haben deswegen meist eine sehr breite Schnittstelle, die letztlich fast alle Klassen der Bibliothek öffentlich zugänglich macht, es gibt also kaum interne verborgene Klassen, somit liegt die Implementierung der Bibliothek fast komplett offen. Bibliotheken dieser Art bieten oft eine sehr hohe Flexibilität, weil Nutzer der Bibliothek jede Einstellungsmöglichkeit erreichen und mittels Beerbung durch eigene Klassen die Funktionalität fast beliebig erweitern oder verändern können. Solche Bibliotheken haben aber auch den Nachteil, dass eben gerade aufgrund dieser breiten Schnittstelle die Lernkurve recht steil ist, weil ein neu auf die Bibliothek zugehender Entwickler erst einmal einen Überblick gewinnen muss, welche Klassen primär von Bedeutung sind und welche er vielleicht nur in speziellen Situationen benötigt. Zudem steigt für die Betreiber der Bibliothek der Aufwand für die Dokumentation und tendenziell auch der Aufwand für zu leistenden Support, denn je mehr Klassen für andere Entwickler zu erreichen sind, umso mehr Fragen und Anfragen ergeben sich auch bezüglich Änderungswünschen, Fehlermeldungen und Verständnisschwierigkeiten. Es gibt aber fachliche Bereiche, in denen trotzdem eine so offene Struktur Vorteile haben kann. Gerade bei Bibliotheken, die noch im experimentellen Status sind, kann eine offene Struktur den Betreibern ermöglichen, von den Nutzern umfangreiches Feedback einzuholen. Dies ist gerade bei Open-Source-Projekten oft der Fall. Ansonsten aber brechen Whitebox-Klassenstrukturpakete eher mit dem Grundsatz, Komplexität zu verstecken und zu abstrahieren, um die Schnittstelle zu vereinfachen. Im Allgemeinen führt der gegenteilige Ansatz, der Blackbox-Ansatz zu besserer Verständlichkeit, damit zu besserer Benutzbarkeit, weniger Fehlern und auch verringertem Betreuungsaufwand für den Betreiber.
231
Kapitel 4
Komponenten Komponenten sind grundsätzlich nach dem Blackbox-Prinzip angelegt. Die innere Implementierung wird so gut es geht verborgen, und nur eine klar definierte Schnittstelle ist nach außen sichtbar. Komponenten können deswegen theoretisch enorme Größen annehmen, was Codemenge und Anzahl von Klassen angeht. Solange die Schnittstelle überschaubar und verständlich bleibt, kann eine Komponente im Prinzip auch eine komplette kleine Anwendung beinhalten, wie es zum Beispiel bei der Google Maps API der Fall ist. Das klassische Beispiel für Komponenten sind gerade im Flash-Bereich die visuellen Komponenten, wie sie auch das Flex-Framework anbietet oder auch viele Drittanbieter. Oft wird deswegen im Flash-Bereich der Begriff Komponente mit visueller Komponente gleichgesetzt. Komponenten müssen aber nicht visuell sein. Eine Komponente kann auch die Funktionalität einer bestimmten Serverschnittstelle oder sonst einer Funktionalität kapseln. Eine Komponente kann auch so umfangreich sein, dass sie einen kompletten Teil eines Systems in sich birgt. Zum Beispiel könnte innerhalb unseres Beispielprojekts FilmRegal die Suche nach Filmen eine ganz eigene Flash-Komponente darstellen, die aus einer Serverschnittstelle, einer Dialogsteuerung und der Anzeige zur Suche und Ergebnisdarstellung bündelt. Warum aber braucht man solche Komponenten? Das Konzept einer Komponente (oder auch Moduls) ist im Prinzip nur eine weitere Abstraktionsschicht nach dem Objekt. Ein Klassenobjekt kapselt ja auch bereits in sich Funktionalität und stellt sie über ihre Klassenschnittstelle, die öffentlichen Methoden, zur Verfügung. Ein Klassenobjekt ist also demnach eine Komponente im Kleinen. Eine Komponente wiederum stellt die gleiche Idee dar, aber im größeren Maßstab. Eine Komponente ist also eine Sammlung aus mehreren Klassen und Interfaces, die zusammen eine Funktionalität anbieten. Damit aber unter der Menge an Klassen die Verständlichkeit, Erweiterbarkeit und Wartbarkeit nicht leidet, müssen auch hier die gleichen Prinzipien der Objektorientierung gelten, wie schon bei einer einzelnen Klasse. Wir sollten die interne Implementierung kapseln und vor den Nutzern verstecken und nur die öffentliche Schnittstelle zugänglich machen. Im Falle einer Komponente besteht nur halt die interne Implementierung nicht aus privaten Methoden oder Variablen, sondern aus internen Klassen. Und die öffentliche Schnittstelle wird hier nicht durch öffentlich Methoden dargestellt, sondern durch Klassen und Interfaces, die extra als Bindeglieder zwischen dem Nutzer und der Komponente definiert wurden (Sie erinnern sich hier vielleicht an das Fassaden-Muster in einem der vorigen Abschnitte). Wenn also die interne Implementierung gut gekapselt ist und die öffentliche Schnittstelle klar strukturiert, dann haben wir, nun auf einer etwas höheren Ebene als der einer Klasse, Komplexität verringert, denn wir haben eine vermeintlich komplexe Anwendung hinter einer klaren Schnittstelle verpackt. Dieses Vorgehen hilft uns, auch in einer größeren Anwendung immer noch den Überblick zu behalten und nicht vor lauter einzelnen Klassen die Grundstruktur nicht mehr zu erkennen. Wie man nun die internen Klassen kapselt, dass sie nach außen nicht sichtbar sind, habe ich schon in mehreren vorangegangenen Abschnitten erläutert. Zum einen können entsprechende Paketstrukturen helfen, in denen interne Klasse dann als internal deklariert werden,
232
Entwurfswerkzeuge zum anderen können Entwurfsmuster wie z. B. Fassade, Proxy oder Vermittler helfen, interne Klassen nicht direkt verfügbar zu machen, sondern über eine definierte Schnittstelle. Es muss natürlich gesagt werden, dass das Komponentenmodell hauptsächlich dann seine Stärken ausspielt, wenn ein Projekt arbeitsteilig durchgeführt wird. Wenn z. B. nur ein Entwickler allein eine Anwendung baut, die aus sechs Komponenten besteht, dann bedeutet das, dass in einem Fehlerfall dieser Entwickler ja doch immer in das Innere der Komponenten blicken und den Fehler beheben muss, denn er hat die Komponenten ja selbst gebaut. Er muss in diesem Fall also tatsächlich das Innere all seiner Komponenten kennen und beherrschen, insofern bringt ihm die Abstraktionsebene gar nichts, sie macht wahrscheinlich nur mehr Arbeit.
Abbildung 4.40: Komponenten ermöglichen eine gute Arbeitsteilung.
Erst wenn mehrere Entwickler an einer Anwendung arbeiten und jeder Entwickler für unterschiedliche Komponenten zuständig ist, ergibt sich für die jeweils anderen Entwickler der Vorteil, dass sie sich bezüglich der Komponenten, die sie selber nicht gebaut haben, nur für deren öffentliche Schnittstelle interessieren müssen und nicht für die interne Implementierung. Hier haben die Entwickler dann einen echten Vorteil, denn sie müssen sich nun mit weniger Klassen beschäftigen, als wenn statt der Komponenten alle Klassen direkt offenlägen. Ein weiterer potenzieller Vorteil kann sich ergeben, wenn man eine Komponente tatsächlich so eigenständig und in sich geschlossen entwickelt, dass sie auch als eigenständige Datei behandelt werden kann. Wenn eine Komponente also einmal als eigene .swc-Datei für die Entwicklung und als .swf-Datei für die Laufzeit zur Verfügung gestellt wird, ergeben sich mehrere Vorteile. Zum einen kann mit der .swc-Datei, die im Projekt der eigentlichen Anwendung als »externe Bibliothek« eingebunden wird, ganz normal entwickelt werden, die Klassen der Komponente werden aber nicht in die .swf-Datei der Anwendung mit hineinkompiliert.
233
Kapitel 4 Zur Laufzeit kann die Anwendung die .swf-Datei der Komponente dann laden, wenn sie benötigt wird. Je nach Wahl der sogenannten ApplicationDomain (die bestimmt, ob und wie nachgeladene Klassen in der Hauptanwendung verfügbar sind, für Details hierzu schauen Sie bitte in die Adobe-Dokumentation) gehen dann die Klassen der nachgeladenen Komponente in die Hauptanwendung über und können ganz normal verwendet werden. Dadurch ist es nun möglich, dass die Komponente separat von der eigentlichen Hauptanwendung betreut und gewartet werden kann. Wenn also in der internen Implementierung ein Fehler auftaucht oder eine Optimierung eingebaut werden soll, die die Schnittstelle der Komponente nicht verändert, dann kann man eine neue Version der Komponente erstellen, auf den Server spielen, ohne dass die eigentliche Hauptanwendung angefasst werden muss. Für den Fall, dass mehrere Hauptanwendungen existieren, die diese Komponente nutzen, wird der Vorteil noch offensichtlicher, denn anstatt bei einer Änderung der Komponente jede Anwendung neu kompilieren und auf den Server spielen zu müssen, muss jetzt nur noch die Komponente einmal zentral neu hochgespielt werden.
4.5.1 Fremdbibliotheken Es ist ein Unterschied, ob innerhalb eines Entwicklerteams unterschiedliche Entwickler unterschiedliche Komponenten oder Klassenpakete erstellen und jeweils andere Entwickler des Teams diese benutzen oder ob grundsätzlich eine Bibliothek verwendet wird, die von Dritten stammt. Bei der Wahl einer Drittbibliothek kommt immer die latente Gefahr zum Tragen, dass man als Entwickler kaum Einfluss darauf hat, wie diese Bibliothek betreut wird und vor allem wie schnell eventuelle Fehler behoben oder fehlende Features eingebaut werden. Auch bei einer Open-Source-Bibliothek ist das nicht anders. Zwar könnte man hier selber aktiv werden und entsprechende Bugfixes oder Änderungen bauen, aber das würde bedeuten, dass man sich plötzlich mit der internen Implementierung auseinandersetzen muss. Etwas, was man ja eigentlich gerade vermeiden wollte, nicht zuletzt deswegen wollte man ja eine Bibliothek einsetzen. Bei der Wahl einer Fremdbibliothek sollten deswegen nicht nur rein funktionale Belange eine Rolle spielen, sondern auch andere:
Wie stark verbreitet ist die Bibliothek? Eine starke Verbreitung bedeutet zumeist, dass auch viele Entwickler Erfahrungen mit ihr haben und man dort eventuell Tipps erhalten kann.
Wie lange gibt es die Bibliothek schon? Eine ganz frisch erschienene Bibliothek hat oft noch Kinderkrankheiten oder beinhaltet noch nicht alle Funktionen, die man erwartet. Auf der anderen Seite ist das Team rund um die Bibliothek meist noch recht motiviert, schnell Änderungen und Bugfixes einzuspielen, weil es die Bekanntheit und Verbreitung erhöhen will. Eine recht neue Bibliothek hat wiederum eher noch wenige Benutzer, die schon von Erfahrungen berichten könnten.
Wie aktiv wird die Bibliothek betreut? Steckt vielleicht sogar ein Unternehmen hinter der Bibliothek? Das könnte Sicherheit hinsichtlich der Lebenszeit der Bibliothek bedeuten (obwohl es kein Garant ist). Ist das letzte Update Jahre her? Wird die Bibliothek
234
Entwurfswerkzeuge überhaupt noch aktiv weiterentwickelt? Bibliotheken, die nur noch geführt, aber nicht mehr weiterentwickelt werden, neigen sich meist dem Ende ihres Bestehens, hier sollte man tendenziell lieber eine andere Bibliothek verwenden.
Wie ist es um Dokumentation und Support bestellt? Viele funktional gesehen gute Bibliotheken haben eine schlechte oder gar keine Dokumentation. Lassen Sie sich nicht blenden von der Existenz einer API-Dokumentation, also einer Beschreibung der Klassen. API-Dokumentationen sind Nachschlagewerke für Entwickler, die grundsätzlich wissen, wie die Bibliothek funktioniert. Für Entwickler, die sich ganz neu der Bibliothek annähern, sind sie meistens zumindest zu Beginn nutzlos. In einer API-Dokumentation kann man nicht die grundsätzliche Idee und Funktionsweise einer Bibliothek erkennen. Gibt es sonst noch anderen Support? Vielleicht ein Forum? Ist es gut besucht? Schreiben dort auch die eigentlichen Entwickler der Bibliothek Beiträge, oder wird es nur von Benutzern verwendet? Unterschätzen Sie nicht die Wichtigkeit einer guten Dokumentation und von grundsätzlichem Support. Gerade in einem Projekt, in dem Sie auf Ihr Budget und Ihr Timing achten müssen (also im Prinzip in jedem Projekt), kann eine gute Dokumentation entscheidend sein.
Wie wird das Release-Management gehandhabt? Gibt es zu wenig oder zu viele Releases? Wird jeden zweiten Tag ein neues Release herausgebracht? Sind neue Releases kompatibel zu alten Versionen? Nehmen Sie sich in Acht vor Bibliotheken, die bei jedem zweiten Release ihre Schnittstelle verändern, auch wenn es nur Feinheiten sind. Bei solchen Bibliotheken ist man nie davor gefeit, dass beim nächsten Update Ihre Anwendungen nicht mehr funktionieren. Ein gutes Release-Management verändert die Schnittstelle innerhalb von Punkt-Releases (also z. B. von 2.1 auf 2.2) nicht und auch bei HauptReleases nur in Ausnahmefällen.
Open Source oder kommerziell? Das ist keine triviale Entscheidung. Open Source kann bedeuten, dass Sie schneller und unkomplizierter neue benötigte Features bzw. Bugfixes bekommen. Open Source kann aber auch bedeuten, dass die Bibliothek chaotisch und unstrukturiert ist und dass die Planung neuer Features und Bugfixes schlecht und undurchsichtig ist, wenn es keine starke Projektführung gibt. Natürlich ist das bei kommerziellen Bibliotheken auch möglich. Tendenziell bieten aber etablierte und bekannte Unternehmen eher einen ordentlichen Support für die Bibliotheken, die sie vertreiben. Pauschal lässt sich keine grundsätzliche Empfehlung für oder gegen Open Source bzw. kommerzielle Produkte aussprechen. Auch Open Source Produkte, gerade wenn sie z. B. von Unternehmen offiziell unterstützt werden, können eine exzellente Dokumentation und einen umfangreichen Support bieten. Sie sollten also Open Source und kommerzielle Produkte gleichermaßen in Betracht ziehen. Wenn ein kommerzielles Produkt eine sehr gute Dokumentation und einen verlässlichen Support bietet, dann kann sich der initiale Kaufpreis schon durch die Zeit, die Sie sparen, um sich in die Bibliothek einzuarbeiten, amortisieren, wenn Sie das mit einer Open-Source-Bibliothek vergleichen, die keine gute Dokumentation hat. Andersherum wäre es allerdings natürlich ärgerlich, wenn Sie Geld für eine Bibliothek ausgeben, die dann auch noch eine unzureichende Dokumentation und einen unbrauchbaren Support aufweist.
235
Kapitel 4
4.5.2 Eigene Bibliotheken Mit eigenen Bibliotheken sind hier solche gemeint, die Sie auch öffentlich anbieten wollen, sei es nur innerhalb Ihrer Firma oder für Ihre Kunden oder ganz öffentlich als Open-SourceVariante oder sogar kommerziell. Ich werde hier allerdings nicht auf etwaige wirtschaftliche Gesichtspunkte eingehen, das können andere Bücher besser. Viele Entwickler, die eine gute Idee bezüglich einer Funktionalität haben, die andere vielleicht brauchen können, denken darüber nach, diese Funktionalität als kleine Bibliothek in oben genannten Kreisen zu veröffentlichen. Tun Sie's. Eine eigene Bibliothek zu schreiben und sie, wenn auch nur in einem kleinen Kreis, zu veröffentlichen, verschafft Ihnen einen neuen Blick auf Softwareentwicklung. Sie werden dadurch viel Erfahrung nicht nur im Bereich der Entwicklung, sondern auch im Bereich der Softwarebetreuung und -organisation sammeln. Sie erweitern somit Ihr Wissen, und falls Ihre Bibliothek wirklich gut ist, winken auch ein wenig Ruhm und Anerkennung. Das Aufbauen und Betreuen einer Bibliothek zwingt uns als Entwickler, ganz besonders auf die Prinzipien der Objektorientierung zu achten, auf Kapselung, auf klare und verständliche Schnittstellen, denn so entstehen elegant und effizient verwendbare Bibliotheken. Sie zwingt uns auch, über die Organisation von Softwareprojekten nachzudenken, denn man wird an Sie herantreten und Änderungswünsche äußern, Kritik üben, Fehler bemängeln etc. Nicht immer aber ist die Kritik gerechtfertigt, nicht immer sind die Änderungswünsche sinnvoll, und manche Fehler stellen sich einfach als falsche Bedienung heraus. Welche Änderungswünsche sollen angenommen werden, ohne dass die Grundidee der Bibliothek zerstört wird? Bedeuten die vielen Falscher-Alarm-Fehlermeldungen vielleicht, dass die Schnittstelle unverständlich ist? Wie ernst müssen Sie die Kritikpunkte nehmen? Wenn Sie eine Bibliothek ernsthaft bauen und betreuen wollen, werden Sie sich mit vielen solcher Fragen beschäftigen. Das kann spannend und herausfordernd sein, ganz sicher aber ist es sehr zeitaufwendig. Bedenken Sie schließlich auch, dass es in Deutschland nicht möglich ist, eine Bibliothek öffentlich, unter völligem Ausschluss von Haftungsansprüchen gegen Sie, anzubieten, auch wenn die Bibliothek über eine Open-Source-Lizenz vertrieben wird. Hier ist die Rechtslage in Deutschland anders als z. B. in den USA.
4.6 Frameworks Anders als Bibliotheken, die nur eine Teilfunktionalität für eine Anwendung darstellen, sind Frameworks selbst bereits Anwendungen, wenn auch unvollständige Anwendungen. Wolfgang Pree schreibt: »a framework is a set of abstract and concrete classes providing a software system as a generic application for a specific domain.« (Pree 1995, S. 57) Frei übersetzt heißt das: »Ein Framework ist eine Sammlung von abstrakten und konkreten Klassen, die zusammen ein Softwaresystem bilden, das als generische Applikation für einen bestimmten Anwendungsbereich fungiert.« Unter generischer Applikation muss man
236
Entwurfswerkzeuge hier verstehen, dass es sich um ein Applikationsskelett handelt, das nicht vollständig ausprogrammiert ist, sondern eben nur ein Grundgerüst bietet, auf dem eine konkrete Anwendung basieren kann. Frameworks sind also normalerweise allein nicht lauffähig, oder zumindest lösen sie ohne eigenes Dazutun keine konkrete Aufgabe. Sie bestehen zu einem großen Teil aus generischer Implementierung, die den grundsätzlichen strukturellen Aufbau der späteren Anwendung vorgibt. Zu einem gewissen Teil wird auch konkrete Funktionalität schon implementiert, wenn man sicher sein kann, dass viele konkrete Anwendungen, die auf dem Framework basieren werden, die Funktionalität auch nutzen können. Die internen Bereiche des Frameworks, die nicht dazu gedacht sind, verändert zu werden, nennt man Frozen Spots. Die Teile, die verändert oder erweitert werden sollen, nennt man Hot Spots (siehe auch Abbildung 4.41).
Framework
«hot spot» TemplateMethodClass
«hot spot» AbstractClass
«frozen spot» InternalModel «frozen spot» InternalComponent «frozen spot» InternalFunctionality
Abbildung 4.41: Abstrakte Darstellung eines Frameworks mit Frozen Spots und Hot Spots
Eine Framework, das eine große Menge an Frozen Spots bietet, also viel vorgefertigte Funktionalität in sich trägt, ist unter Umständen sehr stark auf einen ganz bestimmten Typus von Anwendung ausgerichtet. Ein Framework z.B., das die Organisation mehrerer Nutzer in Verbindung mit physikalischen Simulationen und in Echtzeit gerenderter 3D-Grafik bietet, ist stark auf bestimmte Computerspiele ausgerichtet. Frameworks wiederum, die wenig Funktionalität bieten, geben eher Strukturen vor, als selbst direkt schon allgemeine Aufgaben zu übernehmen. Dies sind im Allgemeinen Architektur-Frameworks, die ich später auch noch beschreiben werde. Die Menge an Hot Spots in einem Framework spiegelt die Flexibilität eines Frameworks wider. Im negativen Sinne kann es aber auch einfach bedeuten, dass ein Framework mit vielen Hot Spots eine schlechte Kapselung bietet. Framework mit wenigen Hot Spots sind im
237
Kapitel 4 Allgemeinen nicht sehr stark an individuelle Anforderungen anpassbar. Dafür ist die Arbeit mit ihnen auch weniger aufwendig, denn je weniger Hot Spots verfügbar sind, umso weniger müssen auch ausprogrammiert werden. Viele Hot Spots im Umkehrschluss bedeuten zwar viel Spielraum, diesen bezahlt man aber oft durch hohe Aufwände, weil viele dieser Hot Spots auch ausprogrammiert werden müssen. In jedem Fall ist die Verwendung eines Frameworks immer ein Kompromiss im Vergleich zu einer kompletten Eigenentwicklung, denn ein Framework, das nun generisch aufgesetzt ist, versucht, eine ganze Bandbreite an Anwendungen innerhalb seines Bereiches beherbergen zu können. Eine individuelle Applikation hingegen kann sich leisten, sich nur auf sich selbst zu konzentrieren und die Struktur entsprechend zu gestalten. Deswegen ist eine ideale Vorgehensweise, sich zunächst Gedanken darüber zu machen, wie man selber die Anwendung zumindest grob strukturieren will und welche Funktionalität man benötigt. Erst danach sollte man schauen, ob es Frameworks gibt, die eine ähnliche Struktur wie die benötigte Funktionalität haben. Wie weit man dann von der ursprünglich geplanten Struktur abweicht, ist eine Frage von Qualität und von Kosten- und Zeitplanung.
Eigene Frameworks Wenn die Entwicklung eigener Bibliotheken schon schwierig und aufwendig sein kann, dann ist es die Entwicklung und Betreuung von Frameworks erst recht. Einer der gewichtigsten Unterschiede ist, Bibliotheken haben grob gesehen eine Bottom-up-Abhängigkeit. Das heißt, die individuelle Anwendung ist für einen bestimmten funktionalen Teil abhängig von einer Komponente. Sollte es Probleme mit der Komponente geben, muss sie im schlimmsten Fall gegen eine andere ausgetauscht werden. Die Probleme beschränken sich aber auf die Verbindung zwischen der Anwendung und der Komponente. Der Rest der Anwendung ist meist nicht betroffen. Sollte eine neue Version der Komponente nicht ganz kompatibel sein zur alten Version, müssen einige Verbindungen von der eigentlichen Anwendung zur Komponente umgebaut werden. Das kann aufwendig genug sein, hält sich aber oft noch im Rahmen des Erträglichen. Ein Framework auf der anderen Seite stellt eine Top-down-Abhängigkeit dar. Das Framework ist selbst die Anwendung, und einige individuell implementierte Klassen machen die konkrete Applikation aus. Ein Framework hat zu den individuell implementierten Klassen viel mehr Beziehungen und Abhängigkeiten als eine individuelle Applikation zu einer Bibliothek. Deswegen wirken sich inkompatible Änderungen in einem Framework meist sehr schnell sehr dramatisch aus. Auch im Fehlerfall bedeutet dies unter Umständen bei einem Framework, dass die ganze Anwendung nicht mehr funktioniert, weil das Framework ja den Anwendungsfluss kontrolliert. Wenn eine Bibliothek einen Fehler hat, funktioniert oft einfach nur die spezielle Funktionalität nicht, die die Bibliothek übernimmt. Um ein Framework zu entwickeln, muss an erster Stelle eine Definition stehen, für welchen Bereich das Framework eine Anwendungsbasis darstellen und wie breit diese Basis sein soll, sprich, wie flexibel das Framework auch Randfälle noch abdecken soll. Soll zum Beispiel ein Spiele-Framework sowohl Echtzeit-Shooter-Spiele als auch Adventure-Spiele abdecken können, oder soll es sich doch eher auf eine Sache konzentrieren? Wenn der Rah-
238
Entwurfswerkzeuge men abgesteckt ist, hat man es für die weitere Entwicklung leichter zu entscheiden, ob eine bestimmte Funktionalität noch in das Framework mit eingebaut werden soll oder nicht. Danach muss entschieden werden, welche Funktionalität das Framework selbst bereits anbieten soll und welche Funktionalität vom Nutzer des Frameworks dazu implementiert werden muss. Dabei sollte sich möglichst eine konsistente und nachvollziehbare Grenze ergeben. Wenn bei einer Funktionalität das Framework alles selbst macht und keinerlei Individualität erlaubt, bei einer anderen Funktionalität aber gar nichts mitbringt, führt das bei Nutzern (hier: Entwicklern) des Frameworks eher zur Verwirrung. Den genauen Rahmen abzustecken, was alles individuell durch Hot Spots anpassbar gemacht wird und wie viel Funktionalität das Framework mitbringen soll, ist sicher mit eine der schwierigsten Fragen und kann nicht pauschal beantwortet werden. Bei der Umsetzung eines Frameworks und speziell bei der Umsetzung der Hot Spots helfen uns wieder einige Entwurfsmuster. Ein Hot Spot stellt einen Punkt im Framework dar, wo der Nutzer des Frameworks eigene bzw. erweiterte oder veränderte Funktionalität in die Anwendung einbringen kann. Ein Hot Spot kann auch derart definiert sein, dass überhaupt erst eine Implementierung vom Nutzer erfolgen muss, damit die Anwendung funktioniert. Eines der klassischen Muster für Frameworks im Zusammenhang mit Hot Spots ist das Schablonenmethode-Muster, das ich in Kapitel Verhaltensmuster schon besprochen habe. Hier kann es seine Wirkung voll entfalten. Eine Hot-Spot-Klasse des Frameworks selbst kann hier die Schablonenmethode implementieren, die in sich andere Methoden aufruft, die in der Hot-Spot-Klasse abstrakt (in ActionScript heißt das also leer) implementiert sind. Der Nutzer des Frameworks muss nun also eine Subklasse dieser Hot-Spot-Klasse implementieren, die die abstrakten Methoden mit Leben füllt, nämlich mit den Funktionen, die er konkret haben will. Wie das in der Praxis aussehen kann, will ich anhand eines kleinen Beispiels zeigen. Stellen wir uns ein Framework vor, das Seiten anzeigt, die einen Template-Mechanismus verwenden. Eine Seite besteht hierbei aus einem Template, welches das Layout der Seite bestimmt, und einer XML-Datei, die den Inhalt der Seite liefert. Es gibt eine allgemeine Klasse PageTemplate, die grundsätzlich beschreibt, wie ein Template funktioniert, also z. B. wie es sich aufbaut, abbaut, was es dabei alles tut etc. Und es gibt konkrete Templates, die von PageTemplate erben und ein konkretes Layout beschreiben. Abbildung 4.42 zeigt die grundsätzliche Struktur. In der Abbildung sehen wir die Klasse PageTemplate. Sie hat zwei öffentliche Methoden, zu erkennen an dem Plus vor dem Methodennamen und drei protected-Methoden, zu erkennen am Hash-Zeichen »#«. Die Kursivschreibweise zeigt zudem an, dass die drei protectedMethoden abstrakt (also leer) implementiert sind. Die öffentliche Methode ruft nun intern createComponents() und danach updateLayout() auf. Die Methode disintegrate() wiederum ruft intern unter anderem removeComponents() auf. PageTemplate ist in diesem Fall also eine Hot-Spot-Klasse, die das Schablonenmethode-Muster verwendet. Und das konkrete Template TwoColumnsTemplate nutzt nun diesen Hot Spot, um ein konkretes Seitenlayout zu implementieren. Um das zu tun, muss TwoColumnsTemplate nur die drei protectedMethoden überschreiben. Sie werden später vom Framework automatisch aufgerufen.
239
Kapitel 4
Page Framework
PageController
Ruft auf: createComponents(); updateLayout();
Ruft auf: removeComponents();
PageTemplate + + # # #
buildPage() : void disintegrate() : void createComponents() : void updateLayout() : void removeComponents() : void
TwoColumnsTemplate # # #
createComponents() : void updateLayout() : void removeComponents() : void
Abbildung 4.42: Ein Seiten-Framework mit einem Hot Spot, dem PageTemplate
Für Frameworks gilt genauso wie für Bibliotheken, dass zwischen der internen Implementierung des Frameworks (Frozen Spots inklusive der Verbindungen zu den Hot Spots) und der öffentlichen Schnittstelle (die durch die Hot Spots dargestellt wird) eine gute Kapselung stattfinden muss. Ein Hot Spot soll genau wie die API einer Bibliothek möglichst keine internen Implementierungsdetails preisgeben. So wenig wie möglich interne Klassen sollen nach außen dringen, denn je schmaler und klarer die Schnittstelle ist, desto einfacher ist es für den Nutzer und desto eher kann die Implementierung bearbeitet und geändert werden, ohne dass sich das zwingend auf die Schnittstelle, sprich auf die Hot Spots, auswirken muss. Das Beibehalten der Schnittstelle, also die Integrität der Hot Spots, ist bei einem Framework noch wichtiger als die Integrität einer API einer Bibliothek, weil üblicherweise zwischen einem Framework und den Klassen, die die konkrete Anwendung ausmachen, viel mehr Beziehungen existieren als zwischen einer Anwendung und einer Bibliothek, die Schnittstelle ist also grundsätzlich schon breiter. Letztlich muss noch auf die Versionierung bei Frameworks ein besonderes Augenmerk gelegt werden, denn da gerade bezüglich der Hot Spots so ein großer Wert auf Kompatibilität gelegt werden muss, muss auch bei Versionssprüngen bezüglich Frameworks vorsichtig
240
Entwurfswerkzeuge umgegangen werden. Da ein Framework selbst die Anwendung darstellt, ist besondere Sorgfalt beim Testing erforderlich. Der Aufwand, eine neue Version eines Frameworks zu erzeugen, ist höher als bei einer Bibliothek, deswegen erfahren Frameworks im Allgemeinen nicht ganz so oft eine neue Version als Bibliotheken. In der Praxis wird man eher selten den konkreten Entschluss fassen, ein Framework von Grund auf zu entwickeln. Vielmehr entstehen Frameworks mit der Zeit aus vielen konkreten, einander ähnlichen Projekten. Ein Dienstleister, der z. B. sein zehntes Spieleprojekt hinter sich hat, wird mit der Zeit Klassenpakete, Muster und Anwendungsstrukturen entwickelt haben, die er in leicht abgewandelter Form immer wieder verwendet. Aus solchen Ressourcen entwickeln sich dann Frameworks, wenn die Entwickler merken, dass sich manche ihrer Entwicklungen verallgemeinern lassen. Ein solcher Vorgang ist tendenziell sinnvoller, weil hier ein Framework aus einem gewachsenen praktischen Erfahrungsschatz entspringt anstatt künstlich aus einer spontanen Idee.
4.6.1 Architektur-Frameworks Eine besondere Art von Framework, die auch schon kurz angesprochen wurde, sind Architektur-Frameworks. Im Gegensatz zu ihnen stehen die Applikations-Frameworks. Ein Applikations-Framework ist eines, wie ich es weiter oben z. B. mit dem Page-Framework beschrieben habe. Es sind Frameworks, die für einen bestimmten fachlichen Bereich eine Basis bieten, um konkrete Anwendungen zu bauen. Sie enthalten mehr oder minder viel an Eigenfunktionalität. Ein recht bekanntes Applikations-Framework in der Flash-Welt dürfte das Flex-Framework sein. Es ist das offizielle Universal-Framework von Adobe. Universell deswegen, weil es sich nicht direkt auf ein fachliches Spezialgebiet stürzt, sondern vielmehr eine Applikationsbasis für vielerlei Anwendungen bieten will. Das Flex-Framework enthält deswegen neben den grafischen Komponenten sehr viel Grundfunktionalität, die die Entwicklung konkreter Anwendungen aller Art erleichtern soll. Ein Architektur-Framework hingegen hat normalerweise kaum eigene konkrete Funktionalität. Architektur-Frameworks könnte man auch als Struktur-Frameworks bezeichnen, denn sie wollen dem Nutzer die Arbeit des Aufbaus einer Applikationsstruktur abnehmen und schlagen selber eine universell einsetzbare Struktur vor. Die Klassen, die in diesen Frameworks enthalten sind, übernehmen deswegen auch eher konstruktive und kommunikative Aufgaben, als dass sie konkrete Funktionalität bieten würden. Momentan sehr populär sind Architektur-Frameworks, die das Model-View-Controller-Muster implementieren und als Struktur vorgeben. Das Framework bildet hier also die Grundstruktur, und die Hot Spots sind abstrakte Model-, View- und Controller-Klassen (sowie natürlich noch weitere im Detail), die konkret beerbt werden müssen, um letztlich eine konkrete Anwendung zu erstellen. Der Sinn eines Architektur-Frameworks ist also, eine generische Grundstruktur vorzuschlagen, die man im Zuge der Entwicklung einer konkreten Anwendung dann verfeinert und anreichert um eigene konkrete Klassen und Klassenstrukturen. Es gilt hier außerdem zu unterscheiden zwischen Architektur-Frameworks, die für einen bestimmten fachlichen Bereich – eCommerce z. B. – eine generelle Architektur vorschlagen, oder Architektur-
241
Kapitel 4 Frameworks, die universell für jedwede Art von Anwendung die Grundstruktur vorschlagen. In Flash ist momentan eher letztere Art präsent. Da ein Architektur-Framework direkt keine Funktionalität bietet, beschränkt sich der Nutzen also darauf, dass man eine Grundstruktur an die Hand bekommt, die als Ausgangspunkt für eine im Detail zu definierende Anwendung dienen kann. Die Zeit wird zeigen, ob das für ein Framework genug ist. In anderen Programmiersprachen bzw. Technologien sind Architektur-Frameworks meist mit konkreter Funktionalität angereichert, sodass man dann schon wieder eher von Applikationsframeworks sprechen kann, die zusätzlich auch eine bestimmte Architektur vorgeben. Die Gefahr beim Einsatz eines Architektur-Frameworks gerade bei unerfahrenen Entwicklern ist, dass sie das Gefühl vermitteln, man müsste sich nicht so stark um den Entwurf einer Anwendung kümmern, sondern könnte sich rein auf die Implementierung von konkretem Code konzentrieren. Das Resultat ist hier oft, dass solche Entwickler zuerst die Entscheidung treffen, ein Architektur-Framework einzusetzen, und sich danach mit den Anforderungen der konkreten Anwendung auseinandersetzen. Das halte ich für grundfalsch. Der Einsatz eines Frameworks ist immer ein Kompromiss im Vergleich zu einer individuell auf einen Anwendungsfall bezogenen Struktur. Erst wenn man die theoretisch ideale Struktur kennt, kann man den Kompromiss einschätzen, den man durch den Einsatz eines Frameworks eingeht. Das klingt nach einiger Arbeit, und das ist es auch. Oft werden Architektur-Frameworks für Kleinstanwendungen eingesetzt, bei denen das Aufsetzen einer individuellen Struktur eine Sache von ein paar Stunden gewesen wäre, dann aber den Vorteil eines gut passenden Designs gehabt hätte. Und für große Anwendungen wiederum keine Forschung bezüglich einer guten Architektur zu betreiben und stattdessen gleich ein Framework einzusetzen, ist fahrlässig. Hinzu kommt, dass z. B. MVC Architektur-Frameworks einen Fokus auf das Zusammenspiel von Model, View und Controller haben. Sie versuchen also, die klassische Trennung dieser drei Teile zu gewährleisten, was ja auch wichtig und gut ist. In einer konkreten Anwendung aber, die nur ein kleines Modell hat mit wenigen modellierten Geschäftsobjekten und wo der Fokus eher auf einer sehr interaktiven und animierten Präsentation liegt, löst ein MVC-Framework die eigentlich Herausforderung nicht, nämlich die konkrete Strukturierung der Präsentation. Das muss nicht zwingend heißen, dass man hier kein MVC-Framework einsetzen sollte, aber man sollte sich bewusst machen, welche Herausforderungen und Aufgaben eine Anwendung an die Entwicklung stellt und welche dieser Herausforderungen man mit welchem Framework oder welcher Bibliothek lösen kann. Eine Architektur selber aufzusetzen, und wenn es gleichfalls nach MVC erfolgt, führt in den meisten Fällen zumindest dazu, dass man mehr Erfahrungen bezüglich Software-Design sammelt, denn man zwingt sich dadurch, stärker über den Entwurf und seine Konsequenzen nachzudenken. Konkrete Applikations-Frameworks, wie z. B. Dependency-Injection-Frameworks, hingegen haben oft den Vorteil, dass sie sich nicht direkt in die Strukturierung der Anwendung einmischen (man sagt auch, sie sind nicht invasiv), aber trotzdem konkrete Funktionalität bieten, die die Entwicklung ebenso vereinfacht. Dadurch ist man sehr flexibel in der Art, wie komplex man seine Anwendung aufbaut und wo man seine Schwerpunkte legen will.
242
Entwurfswerkzeuge
4.7 Literaturangaben Association for Computing Machinery. ACM Special Interest Group on Programming Languages. ACM Sigsoft (Hg.) (1985): Proceedings of the ACM SIGPLAN 85 Symposium on Language Issues in Programming Environments. Papers presented at the symposium in Seattle, Washington, 25–28 June, 1985. New York N. Y. , Baltimore MD/New York, N.Y, Baltimore, MD: Association for Computing Machinery; May be ordered from ACM Order Dept.; ACM (SIGPLAN notices, 20, no. 7/20,7). Gamma, Erich; Riehle, Dirk: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software. (Nachdr.) München: Addison-Wesley, 2008. Fowler, Martin: Presentation Model (2004). Online verfügbar unter http://martinfowler.com/eaaDev/PresentationModel.html, zuletzt aktualisiert am 19.07.2004, zuletzt geprüft am 23.03.2009. Fowler, Martin: Is Design Dead (2000). Online verfügbar unter http://martinfowler.com/ articles/designDead.html, zuletzt aktualisiert am 05.2004, zuletzt geprüft am 08.03.2009. Fowler, Martin: Implementing an Internal DSL (2008). Online verfügbar unter http://martinfowler.com/dslwip/InternalOverview.html, zuletzt aktualisiert am 08.04.2008, zuletzt geprüft am 01.04.2009. Oestereich, Bernd: Analyse und Design mit UML 2. Objektorientierte Softwareentwicklung; (inkl. Poster mit UML-Notationsübersicht & OEP-Vorgehensübersicht). 7., aktualisierte Aufl. München: Oldenbourg, 2005. Pree, Wolfgang: Design patterns for object-oriented software development. Reprint. Wokingham: Addison-Wesley, 1995. Rosen, Michael; Lublinsky, Boris; Smith, Kevin T.; Balcer, Marc J.: Applied SOA. Serviceoriented architecture and design strategies. Indianapolis, Ind.: Wiley Pub, 2008. Sanders, William B.; Cumaranatunge, Chandima: ActionScript 3.0 design patterns. 1. Aufl. Beijing: O'Reilly, 2007.
243
5
Ändern und Testen
5.1 Refactoring Der Begriff Refactoring existiert wahrscheinlich schon länger. Bekannt wurde das Prinzip mit der Sprache Smalltalk und mit einer Dissertation von William F. Opdyke 1992. Das bekannteste Werk zum Thema stammt in jüngerer Zeit aber von Martin Fowler: »Refactoring: Improving the design of existing code« (Fowler, Beck 2007). Er schreibt: »Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.« (Fowler, Beck 2007, S. xvi) Der wichtigste Teil des Zitats betrifft den Punkt, dass beim Verändern des Codes das externe Verhalten des vorhandenen Codes nicht verändert werden soll, wohl aber die interne Struktur. Refactoring beschäftigt sich somit nicht direkt mit dem Hinzufügen von Funktionalität, sondern mit der Änderung von vorhandenen Strukturen und Code. Meistens mit dem Ziel, die Struktur flexibler zu gestalten, damit z. B. neue Funktionalität einfacher hinzugefügt werden kann. Stellen wir uns zum Beispiel eine Anwendung vor, die Layer anzeigt, die animiert erscheinen und auch animiert wieder verschwinden. Im ersten Briefing wurde uns gesagt, dass die Animation ein einfaches Ein- bzw. Ausfaden sein soll. Wir haben das also entsprechend in den Layer eingebaut. Es könnte aussehen wie in Listing 5.1. package { import flash.display.MovieClip; import yet.another.TweenManager; public class Layer extends MovieClip {
Kapitel 5 // ... public function show():void { TweenManager.animate(this, new Tween("alpha", 1)); } public function hide():void { TweenManager.animate(this, new Tween("alpha", 0)); } // ... } } Listing 5.1: Auszug aus einer Layer Klasse
Nun entscheiden sich die Designer um und sagen, dass manche der Layer nicht faden sollen, sondern von links aus dem Nichts kommen und dorthin auch wieder verschwinden sollen. Wir haben nun das Problem, dass unsere Klasse grundsätzlich immer fadet. Sie ist darauf vorbereitet, unterschiedliche Animationsarten zu unterstützen. Refactoring bedeutet nun, dass wir im ersten Schritt die Struktur unserer Anwendung mit den Layern so umbauen, dass es möglich ist, den Layern künftig mehrere Arten von Animationsanweisungen zu übergeben. Dies soll aber so passieren, dass die bisherige nach außen sichtbare bestehende Funktionalität nicht verändert wird. Warum? Weil eine Änderung im Verhalten der Layer sich nicht auf die ganze restliche Anwendung auswirken soll, und wenn sich die Layer nach unserem Refactoring äußerlich noch genauso verhalten wie vorher, haben wir genau das erreicht. Im zweiten Schritt würden wir dann die konkrete Änderung, also die neue Art von Animation, einbauen. Wir wollen das Animieren eines Layers auslagern in unterschiedliche Layer-Animationsklassen, damit wir künftig flexibel sind hinsichtlich der Wahl einer Animationsart. Einem Layer kann man dann eine dieser Animationsklassen übergeben, und er wird sich dann entsprechend animieren (siehe Abbildung 5.1). Wir erstellen also zunächst das LayerAnimation-Interface und die konkrete Klasse FadingAnimation, denn bisher war ja auch nur das Fading bekannt. Das Interface schenke ich mir hier. Ich gehe davon aus, dass Sie sich ungefähr vorstellen können, wie das Interface aussieht. Die beiden show()- und hide()-Methoden, die bisher in der Layer-Klasse waren, kopieren wir und fügen sie nun in die FadingAnimation-Klasse ein. package { import flash.display.DisplayObject; import yet.another.TweenManager; public class FadingAnimation implements LayerAnimation { public function show(target:DisplayObject):void { TweenManager.animate(target, new Tween("alpha", 1)); }
246
Ändern und Testen
public function hide(target:DisplayObject):void { TweenManager.animate(target, new Tween("alpha", 0)); } } } Listing 5.2: Neue FadingAnimation-Klasse
«interface» LayerAnimation
Layer + +
show() : void hide() : void
+ +
show(DisplayObject) : void hide(DisplayObject) : void
«property set» + showHideAnimation(LayerAnimation) : void «property get» + showHideAnimation() : LayerAnimation
FadingAnimation + +
show(DisplayObject) : void hide(DisplayObject) : void
RollingAnimation + +
show(DisplayObject) : void hide(DisplayObject) : void
Abbildung 5.1: Auslagerung der Animationen
Wir müssen die beiden Methoden nun natürlich so ändern, dass sie nicht mehr »this« animieren, sondern die Referenz auf das zu animierende Objekt übergeben bekommen. Nachdem das getan ist, ändern wir nun die Layer-Klasse um, sodass sie künftig die entsprechende LayerAnimations-Klasse verwendet. package { import flash.display.MovieClip; public class Layer extends MovieClip { private var _showHideAnimation:LayerAnimation = new FadingAnimation(); // ... public function show():void { _showHideAnimation.show(this); } public function hide():void { _showHideAnimation.hide(this); } public function get showHideAnimation():LayerAnimation {
247
Kapitel 5 return _showHideAnimation; } public function set showHideAnimation(value:LayerAnimation):void { _showHideAnimation = value; } // ... } } Listing 5.3: Veränderte Layer-Klasse, nutzt nun LayerAnimation
Es fällt sofort auf, dass die private Variable _showHideAnimation mit einer Instanz vom Typ FadeAnimation initialisiert wird. Das ist natürlich nicht ideal, aber vorübergehend erforderlich, weil wir ja die ursprüngliche Funktionalität nicht verändern wollten. Wenn wir uns weiter mit der Anwendung beschäftigen würden, könnten wir dieses unschöne Detail eventuell noch loswerden. Nachdem wir nun das Refactoring beendet haben, könnten wir einem Layer in der eigentlichen Anwendung sagen, dass er eine ganz bestimmte Art von Animation verwenden soll. Das gehört aber dann nicht mehr zum Refactoring. Wichtig beim Refactoring ist in jedem Fall, dass man seine Änderungen ständig überprüfen und testen sollte, um sicherzugehen, dass sich keine neuen Fehler eingeschlichen haben. Hierfür bieten sich automatisierte Tests an (siehe Kapitel 5.2). Sie sollten möglichst nach jeder in sich geschlossenen Änderung am Code durchgeführt werden. Je öfter man testet, umso eher kann man im Fehlerfall nachvollziehen, wann und wo man den Fehler eingeschleust hat. Refactoring wendet man nicht nur bei konkret anstehenden Änderungen an. Manchmal ist man gezwungen, einen Teil einer Anwendung auf die Schnelle zu implementieren, weil ein Abgabetermin bevorsteht. In diesem Fall ist eventuell nicht genügend Zeit, um den Entwurf und die Umsetzung des Anwendungsteils so ordentlich vorzunehmen, wie man das eigentlich will. In diesem Fall sollte man nach dem Abgabetermin den fraglichen Teil einem Refactoring unterziehen und die Struktur so anpassen, dass sie ordentlich strukturiert ist. Refactoring wird unter anderem oft dann durchgeführt, wenn:
neue Funktionalität hinzugefügt werden soll Bugs im Code entdeckt wurden ein Code-Review stattfand in einem iterativen Vorgehensmodell eine Iteration beendet wurde sich die Gelegenheit bietet Der letzte Punkt klingt vielleicht etwas merkwürdig, aber Refactorings können und sollten immer wieder durchgeführt werden. Das wird umso öfter passieren, je aktiver man sich mit dem Code einer Anwendung beschäftigt. Wenn man sowieso an einer Anwendung arbeitet, dann wird man häufig über Stellen im Code stolpern, bei denen man denkt, dass man hier und da vielleicht etwas vereinfachen könnte. Wenn man auf solche Stellen trifft, ist es nütz-
248
Ändern und Testen lich, das Refactoring gleich durchzuführen, denn sonst wird man es wahrscheinlich nie tun. Auch und gerade hier ist es aber umso wichtiger, jede Änderung zu testen. Die Gefahr, bei einer »mal eben« gemachten Änderung einen Flüchtigkeitsfehler zu machen, ist natürlich recht hoch. Refactoring ist in den letzten Jahren auch für Flash- und Flex-Entwickler etwas einfacher geworden, weil es mittlerweile Entwicklungsumgebungen gibt, die einige Aufgaben des Refactorings automatisiert übernehmen. Das heißt nicht, dass sie die Notwendigkeit des Refactorings selbst erkennen würden. Letztlich sind sie eigentlich nur in der Lage, komplexe Suchen- und Ersetzen-Anweisungen auszuführen. Refactoring ist aber weit mehr als einfach nur Methoden oder Klassen umzubenennen oder umzuschieben. Refactoring ist an sich eine heikle Angelegenheit. Gerade wenn man umfangreiche Umstrukturierungen vornimmt, muss sichergestellt werden, dass dabei keine Funktionalität zerstört wird. Deswegen sind automatisierte Tests hier auch so hilfreich. Wenn nach einer Refactoring-Maßnahme die unveränderten Tests immer noch ohne Fehler durchlaufen, dann ist das ein gutes Anzeichen dafür, dass das Refactoring erfolgreich war. Die Tests sind hierbei auch die Prüfung, dass sich das externe Verhalten durch das Refactoring nicht verändert hat. Refactoring soll aber auch nicht als ein großer, risikoreicher Prozess missverstanden werden. Vielmehr geht es darum, notwendige Veränderungen vorzunehmen, um gewappnet für die eigentlichen Aufgaben zu sein, wie eben z. B. das Hinzufügen von Funktionalität oder die Verbesserung der Verständlichkeit des Codes. Verlieren Sie sich deswegen nicht gleich in der Gesamtstruktur der Anwendung. Wenn man mit dem Refactoring erst mal angefangen hat, kann man leicht auf den Gedanken kommen, dass man eigentlich die ganze Anwendung umstrukturieren könnte. Das ist natürlich nicht der Sinn. Konzentrieren Sie sich vielmehr auf bestimmte kleine Teile der Anwendung, mit denen Sie vielleicht sowieso gerade zu tun haben. Überlegen Sie sich Folgendes: Beim ersten Entwurf der Gesamtanwendung kam eine Struktur heraus, die nicht perfekt war. Wenn Sie jetzt durch Refactoring versuchen würden, auf einen Schlag eine komplett neue Struktur zu erzeugen, wieso sollte diese diesmal perfekt werden? Refactoring zielt eher darauf ab, Verbesserungen Schritt für Schritt einzuführen als auf einen Schlag. Sie verbessern dort, wo Ihnen gerade Möglichkeiten zur Verbesserung auffallen.
Softwareentwurf und Refactoring Wenn man mit Methoden des Refactorings die interne Struktur einer Anwendung im Nachhinein optimieren kann, dann stellt sich die Frage, wie viel Zeit man eigentlich überhaupt in den anfänglichen Entwurf einer Anwendung stecken sollte. Könnte man nicht einfach drauflosprogrammieren und per Refactoring hinterher schlechte Strukturen wieder geradebiegen? Fakt ist, dass das Refactoring natürlich Zeit kostet, genauso wie auch der Softwareentwurf Zeit kostet. Den Entwurf macht man vor der Implementierung, das Refactoring danach. Die
249
Kapitel 5 Entscheidung, ob man eher im Voraus einen Entwurf macht oder eher im Nachhinein die Struktur durch Refactoring optimiert, hängt von einigen Faktoren ab:
Wie viel weiß man schon im Voraus über konkrete Anforderungen, die einen Einfluss auf die Struktur haben könnten?
Wie hoch schätzt man den Aufwand ein, eine Struktur im Nachhinein noch mal zu verändern, wenn sie doch nicht passt?
Welche Personen oder auch Abläufe im Allgemeinen könnten davon profitieren, wenn ein Entwurf im Voraus feststünde, oder welche Risiken würde das mit sich ziehen, falls dieser Entwurf dann doch nicht passt? Welche Nachteile ergäben sich für Personen oder Abläufe, wenn man nach der Implementierung noch mal die Struktur verändern müsste? Letzten Endes ist es eine Abwägung von Aufwänden für einen selbst und Entwicklerkollegen, ob man eher der einen oder der anderen Variante den Vorzug gibt. Gänzlich wird man sich eher selten für die eine oder andere Seite entscheiden. Es geht eher darum, wie detailliert man den Entwurf anfangs gestaltet und wie stark man hinterher noch Refactoring zulässt. Diese Entscheidung muss von Projekt zu Projekt neu getroffen werden.
Refactoring vom Gesamtentwurf Refactoring hat seine Grenzen. Der Sinn der Optimierung der Struktur einer Anwendung liegt darin, sie im Kleinen stetig zu verbessern. Wer vor dem Gedanken steht, umfangreiche Umstrukturierungen in der Gesamtanwendung oder einem großen Teil der Anwendung vorzunehmen, sollte sich zunächst fragen, wie dieser Aufwand im Verhältnis zu einer Neuprogrammierung steht. Das Refactoring hat gegenüber der Neuprogrammierung den Nachteil, dass man gedanklich ja immer bemüht ist, die grundsätzliche Funktionalität beizubehalten. Man steckt also gewissermaßen in einem gedanklichen Korsett. Das oberste Ziel ist, keine neuen Bugs einzuführen und die Aufwände so gering wie möglich zu halten. Wenn allerdings die Gesamtstruktur einer Anwendung so instabil oder verbaut ist, dass man eigentlich fast überall ranmuss, dann würde man sich beim Refactoring eher verheddern, denn der Fokus wäre dann zu sehr auf viele kleine Details gesetzt. Stattdessen könnte dann eher der bessere Weg sein, einen klaren Strich zu ziehen und die Anwendung strukturell neu aufzusetzen. Hier muss sehr sorgfältig abgewägt werden. Aufwände müssen gegenübergestellt werden. Auch strategische Faktoren sind zu berücksichtigen. Wenn Sie Ihrem Projektmanager gegenübertreten und sagen »Wir bauen alles neu!«, hat das naturgemäß eine andere Wirkung, als wenn Sie sagen »Wir optimieren die Struktur der bestehenden Anwendung«. So sollten Sie also gar nicht erst anfangen zu argumentieren. Stattdessen sollten Sie Ihre Optionen mit Aufwänden und Fakten vortragen. Wenn es dann tatsächlich zu einer Neuentwicklung kommt, versetzen Sie sich zurück in die Erstellung der ersten Version (falls Sie dafür verantwortlich waren). Haben Sie damals den kompletten Entwurf zu Beginn vorgenommen? Oder haben sie direkt drauflosprogrammiert? Wie ist Ihr Kenntnisstand jetzt im Vergleich zum ersten Mal? Können Sie jetzt mehr
250
Ändern und Testen Entwurfsentscheidungen im Voraus treffen, weil Sie inzwischen ein tieferes Verständnis von der Anwendung und dem Problemfeld haben? Oder haben Sie erkannt, dass das Projekt grundsätzlich zu unberechenbar ist? All diese Faktoren sollten bei der Frage mit einbeziehen, wie viel Entwurf und wie viel Refactoring Sie bevorzugen.
Refactoring der öffentlichen Schnittstelle Eine große Frage beim Überarbeiten der Struktur einer Anwendung stellt sich, wenn die Überarbeitung die öffentliche Schnittstelle verändern würde. Die Maxime von Martin Fowler lautet ja, dass sich das externe Verhalten des Codes nicht ändern soll, also die öffentliche Schnittstelle. Das leuchtet ja auch ein. Wenn mehrere Personen meinen Code benutzen und ich ihn verändere, sollen diese Personen ja danach nicht auch alle ihren Code ändern müssen. Was aber, wenn die öffentliche Schnittstelle genau das Problem ist? Wenn genau dort eine strukturelle Schwäche ist oder die Schnittstelle ganz einfach sehr schlecht zu benutzen oder zu verstehen ist? Die Antwort ist etwas unbefriedigend. Es gibt bisher dafür keine wirklich elegante Lösung. Wenn Sie ein Modul bauen, das von anderen Entwicklern benutzt wird, dann benutzen diese Entwickler die Schnittstelle des Moduls. Das ist das Prinzip der Kapselung. Dadurch verringern Sie für Ihre Kollegen die Komplexität der internen Implementierung Ihres Moduls. Ihre Entwickler müssen und sollen sich nicht dafür interessieren, wie Sie das Innere des Moduls gebaut haben. Stattdessen interessieren sie sich für die öffentliche Schnittstelle. Wenn Sie nun aber die Schnittstelle verändern wollen oder müssen, dann werden die anderen Entwickler alle ihre eigenen Anwendungen abändern müssen. Es gibt eine Lösung für dieses Problem, das aber nicht alle Sorgen in Luft auflöst. Sie können ihre überarbeitete Schnittstelle parallel zur alten Schnittstelle anbieten. Sie können für die alten Methoden und Klassen in die Dokumentation schreiben, dass sie alt sind und nicht mehr verwendet werden sollten (auf Englisch »deprecated«). Stattdessen solle man die neue Funktion oder Klasse verwenden. Sie kennen sicherlich Beispiele aus der Praxis, in denen so verfahren wird. Allein die API des Flash Players hat einige Beispiele zu bieten. »tellTarget« oder »add« sind nur Beispiele für Funktionen, die irgendwann als veraltet bezeichnet und gegen neue Pendants oder einfach andere Konzepte ausgetauscht wurden. Dennoch blieben sie aus Kompatibilitätsgründen für eine Weile erhalten, damit alte Anwendungen nicht plötzlich aufhörten zu funktionieren. Seit Flash Player 9 wird sogar eine komplette »veraltete« virtuelle Maschine mitgeschleppt. Die nämlich, die noch ActionScript 1 und 2 und die entsprechenden älteren Flash-Anwendungen unterstützt.
251
Kapitel 5 Wenn Sie über diese Strategie des parallelen Beibehaltens alter Schnittstellen ein wenig nachdenken, werden Sie bald den leicht üblen Beigeschmack bemerken. Zwei Schnittstellen parallel zu betreuen, bedeutet natürlich, dass auch beide weiterhin funktionieren müssen. Immer wenn Sie interne Funktionalität verändern, müssen Sie darauf achten, dass auch beide Schnittstellen noch funktionieren. Eventuell geht das so weit, dass Sie manche interne Strukturen oder Implementierungen nicht einfach verändern können, weil dann die alte Schnittstelle nicht mehr richtig funktionieren würde. Und das, obwohl es aus Sicht der neuen Schnittstelle vielleicht sogar nötig wäre. Zudem wollen Sie ja auch nicht zu viel Code doppelt pflegen. Deswegen leiten alte Schnittstellenmethoden meistens einfach auf Pendants in der neuen Schnittstelle weiter, sind also so weit wie möglich entkernt. Hierdurch ergeben sich aber wiederum durchaus recht dichte Abhängigkeiten und Verflechtungen. Der Supergau entsteht, wenn Sie vielleicht sogar irgendwann an einen Punkt kommen, an dem Sie theoretisch schon eine dritte Schnittstellenvariante bräuchten. Irgendwann blickt niemand mehr durch, welche Methoden noch aktuell sind und welche noch mitgeschleppt werden müssen. Die Quintessenz daraus können zwei Dinge sein:
Halten Sie die öffentlichen Schnittstellen so klein wie möglich. Je umfangreicher Ihre öffentliche Schnittstelle sein muss, umso mehr Gedanken müssen Sie sich um einen sauberen Entwurf der Schnittstelle im wir Vorausmachen. Wenn Sie in einem Projekt viele in sich geschlossene Module mit jeweils öffentlichen Schnittstellen bauen, dann schaffen Sie so unter Umständen viele schwer veränderliche Schnittkanten, an denen man im Nachhinein nicht mehr rütteln kann, ohne große Aufwände mit sich zu ziehen. Wenn Sie weniger modular arbeiten würden und die Anwendung stattdessen auf mehr Ebenen Daten austauschen würde, wäre die Anwendung in sich offener und damit flexibler für Änderungen. Der Nachteil ist aber natürlich, dass dadurch die Komplexität für jeden einzelnen Mitarbeiter steigt. Denn nun hat er nicht mehr einfach nur eine klare Schnittstelle vor sich, sondern unter Umständen ein komplexes Geflecht aus einzelnen Klassen und Konstrukten. Wenn Sie auf der anderen Seite lieber diese Komplexität vermeiden wollen, bedeutet dies, dass Sie mehr Energie in eine saubere Strukturierung der Schnittstellen stecken müssen, denn Sie wissen, dass Sie diese hinterher nicht mehr so einfach ändern können. Beide Vorgehensweisen haben ihre Vor- und Nachteile. Es kommt nicht zuletzt auch darauf an, wie das Team gewohnt ist, zusammenzuarbeiten, wie viel Komplexität die einzelnen Entwickler verkraften, wie komplex das Projekt überhaupt ist usw.
Generische Lösungen funktionieren nicht Viele Entwickler sehen die Erstellung von generischen Bausteinen und Modulen als ein sehr erstrebenswertes Ziel an. Wir wollen ja wieder verwendbaren Code schreiben, damit wir es beim nächsten Mal etwas leichter haben.
252
Ändern und Testen Oft denken wir mitten in einem Projekt, dass sich die aktuelle Problemstellung bestens dafür eignet, eine Lösung zu bauen, die man auch in anderen Projekten einsetzen kann. Wir erkennen, dass das Problem eines ist, das wir bestimmt öfter vor uns haben werden, und wir streben eine generische Lösung an. Die Praxis zeigt aber, dass in den meisten Fällen solche generisch gemeinten Lösungen nicht funktionieren. Die Gründe hierfür sind vielfältig:
Wir stecken gedanklich in der konkreten Problemstellung dieses Projekts. Über künftige Problemstellungen ähnlicher Art können wir nur Mutmaßungen anstellen.
Das aktuelle Projekt bringt konkrete Anforderungen technischer und fachlicher Art mit, die wir für unsere Lösung berücksichtigen müssen, die aber wahrscheinlich in künftigen Fällen nicht gegeben sind. Es wird uns schwerfallen, die aktuellen von den generischen Anforderungen zu unterscheiden.
Das aktuelle Projekt bringt meist strenge zeitliche und budgetäre Beschränkungen mit sich. Wir schaffen es meist nicht, innerhalb dieser Beschränkungen eine saubere generische Lösung zu bauen.
Wenn wir zum ersten Mal vor dem konkreten Problem stehen, haben wir meist noch zu wenig Wissen von dem gesamten Ausmaß des Problems, um seinen Kern wirklich greifen und in eine generische Lösung gießen zu können. Können wir also keinen generischen, wieder verwendbaren Code erstellen? Doch, aber wir müssen anders vorgehen. Anstatt von Anfang an einer generischen Lösung hinterherzujagen, sollten wir lieber für das konkrete Projekt die beste und einfachste Lösung finden. Sie soll nicht unbedacht dahin programmiert werden, aber sie soll unter den Gesichtspunkten des aktuellen Projekts elegant und sauber aufgesetzt werden. Wenn das nächste Projekt mit einer ähnlichen Problemstellung kommt, dann kann man die Lösung aus dem alten Projekt wieder ansehen und versuchen, Gemeinsamkeiten zu finden. Eventuell kopiert man einen Teil der alten Lösung, führt ein wenig Refactoring durch und setzt das Resultat, das nun schon ein wenig generischer geartet ist, in dem neuen Projekt ein. Auf diese Weise entsteht Schritt für Schritt auf natürliche Weise eine generische Lösung. Der Punkt ist, dass diese Lösung aus der Praxis heraus erwächst und herausgefeilt wird anstatt künstlich aus unvollständigen Vorstellungen.
5.1.1 Refactoring-Maßnahmen In den folgenden Abschnitten schauen wir uns nun einige konkrete Refactoring-Maßnahmen aus Martin Fowlers Katalog an (Fowler, Beck 2007). In diesem Buch kann ich nicht alle der über 70 beschriebenen Maßnahmen aufführen. Stattdessen seien hier einige prominente Vertreter dargestellt, um ein Gefühl für die möglichen Refactoring-Maßnahmen zu bekommen.
253
Kapitel 5
Code extrahieren in eine Methode bzw. Klasse Eines der Grundprinzipien in der Objektorientierung ist, dass ein Ding immer nur eine Verantwortlichkeit haben soll. Das gilt für Methoden genauso wie für Klassen. Schauen wir uns eine Methode buildLayer() an (siehe Listing 5.4). public function buildLayer():void { layer = new DialogLayer(); layer.setStyle(LayerStyles.BORDERLESS); layer.addEventListener(DialogLayerEvent.CLOSE, completeDialog); addChild(layer); layer.x = 20; layer.y = 50; layer.alpha = 0; TweenManager.animate(layer, new TweenProperties("alpha", 1, 100)); } Listing 5.4: Einfache buildLayer()-Methode
In dieser Methode verstecken sich zwei Verantwortlichkeiten. Einmal wird ein Layer erzeugt und konfiguriert, und zweitens wird er positioniert und animiert. Das sind zwei unterschiedliche Aktionen, die auch getrennt ausgeführt werden könnten. Hier ist zu berücksichtigen, es ist nicht unbedingt immer so, dass man diese beiden Aktionen auch wirklich trennen muss. Aber wenn es dazu kommt, dass der Code erweitert werden muss, dann kann man hier unter Umständen an einen Punkt kommen, an dem der Layer erst einmal nur erzeugt und später erst angezeigt werden soll. In diesem Fall kommt hier die Maßnahme des Extrahierens von Code in eine neue Methode ins Spiel. Wir würden dann also den Code folgendermaßen abändern, wie in Listing 5.5 gezeigt. public function buildLayer():void { createLayer(); showLayer(); } public function createLayer():void { layer = new DialogLayer(); layer.setStyle(LayerStyles.BORDERLESS); layer.addEventListener(DialogLayerEvent.CLOSE, completeDialog); } public function showLayer():void { if(layer == null) return; addChild(layer); layer.x = 20; layer.y = 50;
254
Ändern und Testen layer.alpha = 0; TweenManager.animate(layer, new TweenProperties("alpha", 1, 300)); } Listing 5.5: Extrahierte Methoden für buildLayer()
Wir sehen hier bereits das Endergebnis. Es ist, wie schon erwähnt, sinnvoll, diese Abänderung in einzelnen Schritten durchzuführen. Man würde also zuerst die createLayer()Methode erstellen, diese dann in buildLayer() einsetzen und dann erst einmal testen. Danach würde man für die showLayer()-Methode genauso verfahren und wiederum testen. Je öfter man Zwischenschritte während des Refactorings testet, umso schneller kann man auftretende Fehler lokalisieren. Eine besondere Schwierigkeit beim Extrahieren von Methoden kommt in den Fällen hinzu, in denen innerhalb der ursprünglichen Methode mit lokalen Variablen gearbeitet wird. Auch hierzu wieder ein Beispiel. public function showButtons():void { var buttonList:Vector.<SimpleButton> = new Vector.<SimpleButton>(); var currentChild:DisplayObject; for(var i:int = 0; i < numChildren; i++) { currentChild = getChildAt(i); if(currentChild is SimpleButton) { buttonList.push(currentChild); currentChild.visible = true; } } } Listing 5.6: Methode mit lokalen Variablen
Die Methode showButtons() in Listing 5.6 tut wieder zwei Dinge. Sie sucht Buttons und sie zeigt sie an. Nehmen wir an, wir haben festgestellt, dass wir schon öfter in unserer Anwendung eine temporäre Liste der Buttons in einem DisplayObject suchen. Es wäre besser, wenn wir das nicht jedes Mal neu programmieren würden, sondern wiederverwenden könnten. Wir wollen also das Suchen und das Anzeigen trennen. In der jetzigen Methode showButtons() werden die lokalen Variablen buttonList und currentChild verwendet. Wir müssen jetzt besonders darauf Acht geben, wie wir den Code zum Suchen aus der jetzigen Methode heraustrennen, denn die lokalen Variablen hier wären ja in einer neuen Methode nicht sichtbar. Momentan wird currentChild z. B. sowohl für das Suchen als auch für das Sichtbarmachen der Buttons verwendet. Wir müssen den Code also verändern, wenn wir ihn extrahieren. Wir erzeugen also zunächst eine neue Methode getButtons(). Diese enthält im Wesentlichen einen großen Teil der for-Schleife. Wir ändern den Code so, dass getButtons() einen Vektor vom Typ SimpleButton zurückliefert (siehe Listing 5.7).
255
Kapitel 5 public function getButtons():Vector.<SimpleButton> { var result:Vector.<SimpleButton> = new Vector.<SimpleButton>(); var currentChild:DisplayObject; for(var i:int = 0; i < numChildren; i++) { currentChild = getChildAt(i); if(currentChild is SimpleButton) { result.push(currentChild); } } return result; } Listing 5.7: Methode getButtons()
Nun müssen wir noch showButtons() so abändern, dass sie getButtons() benutzt. public function showButtons():void { for each(var elmt:SimpleButton in getButtons()) elmt.visible = true; } Listing 5.8: Die neue showButtons()-Methode
Listing 5.8 zeigt, dass die showButtons()-Methode jetzt sehr vereinfacht wurde. Sie konzentriert sich jetzt nur noch ganz auf ihre eigentliche Aufgabe, das Anzeigen aller Buttons. Eine kurze Zwischenfrage mag hier aufkommen, ob jetzt nicht getButtons() bei jedem Schleifendurchgang aufgerufen wird. Das ist nicht so, Flash ruft getButtons() nur am Anfang einmal auf. Eine weitere Frage hinsichtlich der Performance stellt sich. In unserer ursprünglichen showButtons()-Methode hatten wir nur eine Schleife. Jetzt haben wir effektiv zwei. Wir haben also ein wenig Performance verloren. Wir stehen hier vor einer Abwägungsfrage. Wird sich der Verlust an Performance auswirken? Wenn wir es nicht mit mehreren Tausend Elementen in der Display-Liste zu tun haben, wird man die Auswirkung nicht bemerken. In dem Fall können wir die bessere Wiederverwendbarkeit als höher einstufen. Performanceoptimierungen sollten nicht zum Selbstzweck durchgeführt werden, sondern dann, wenn sich die Anwendung nicht mehr den Anforderungen entsprechend verhält. Ähnlich wie beim Extrahieren von Code in eine Methode geht man beim Extrahieren von Methoden und Attributen in eine neue Klasse vor, nur eben auf einer höheren Ebene. Manchmal stellen Sie während der Entwicklung fest, dass eine Klasse recht groß geworden ist und viele Methoden beherbergt oder vielleicht auch viele Attribute. Fragen Sie sich, ob die Klasse wirklich noch genau eine Verantwortlichkeit oder bereits mehrere Themen übernommen hat. Nehmen wir z. B. eine Drop-down-Komponente, wie im Diagramm in Abbildung 5.2 gezeigt.
256
Ändern und Testen
DropDown + + + +
open() : void close() : void setCSVData() : void loadXMLData() : void
«property set» + data(Array) : void «property get» + data() : Array
Abbildung 5.2: Drop-down-Komponente
Sie hat die wichtigsten Attribute und Methoden, die ein Drop-down haben muss. Man kann das Drop-down auf- und zumachen und ein Daten-Array setzen. Zudem aber kann man auch CSV-Daten eingeben und sogar eine XML-Datei mit Daten laden lassen. Das kann nicht wirklich die Aufgabe eines Drop-downs sein, unterschiedliche Datenformate zu verstehen. Wir sollten diese Aufgaben aus dem Drop-down herausziehen. Hierfür gibt es nun mehrere Möglichkeiten. Für den Fall, dass loadXMLData() und setCSVData() von sehr wenigen oder keinen anderen Klassen verwendet werden, könnten wir sie ganz aus dem Dropdown entfernen. Für den Fall, dass sie schon sehr oft verwendet werden, ist das wahrscheinlich keine Option mehr, denn der Aufwand wäre zu groß. In beiden Fällen können wir eine extra Klasse erzeugen, die sozusagen als Daten-Konvertierer fungiert und die Daten für das Drop-down aufbereitet. Theoretisch könnten wir sogar eine abstrakte DataConverter-Klasse schreiben, die vorgibt, wie so ein Konvertierer arbeitet, und dann zwei konkrete Klassen für XML und für CSV, die von DataConverter schreiben. Für dieses kleine Beispiel wollen wir es aber bei einer alleinigen Klasse belassen.
DropDown + +
open() : void close() : void
DropDownDataConverter + +
convertXMLData(XML) : Array convertCSVData(String) : Array
«property set» + data(Array) : void «property get» + data() : Array
Abbildung 5.3: Aufteilung in zwei Klassen, Methoden aus Drop-down entfernt
Abbildung 5.3 zeigt nun die Variante, in der wir die Methoden aus dem Drop-down entfernen können. Geht das nicht, dann können wir wenigstens die Aufgaben delegieren. In Abbildung 5.4 deuten die beiden Notizen an, dass nun setCSVData() und loadXMLData() intern den DropDownDataConverter verwenden, um die Konvertierung vorzunehmen. Idealerweise wird Drop-down für das Laden der XML-Datei einen bereits vorhandenen Preloader verwenden, denn auch das Laden ist natürlich nicht die Aufgabe einer Dropdown-Komponente.
257
Kapitel 5
DropDown + + + +
open() : void close() : void setCSVData() : void loadXMLData() : void
DropDownDataConverter + +
convertCSVData(String) : Array convertXMLData(XML) : Array
«property set» + data(Array) : void «property get» + data() : Array
converter.convertCSVData(csv);
converter.convertXMLData(loadedXML);
Abbildung 5.4: Drop-down verwendet Delegation, um die Funktionalität auszulagern.
Eine erläuternde Variable einsetzen Es kommt vor, dass eine Berechung oder eine Prüfung recht komplex wird. Diese Berechnung kann eventuell verwendet werden, um in einer if-Anweisung eine Entscheidung zu treffen. Hier wieder ein kleines Beispiel. In Listing 5.9 sehen wir eine if-Anweisung, die prüft, ob ein Punkt point mehr als 50 Pixel vom Nullpunkt entfernt ist. Das ist nicht sofort zu durchschauen, man muss sich den Code genauer ansehen, um das zu verstehen. if((Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2))) > 50) { // Irgendeine Aktion } Listing 5.9: Komplizierte if-Anweisung
In so einem Fall ist es verständlicher, wenn man die Prüfung in einer Variablen mit einem aussagekräftigen Namen zwischenspeichert und diese dann in der if-Anweisung verwendet. Das macht den Code besser lesbar. Listing 5.10 zeigt das entsprechende Refactoring. var distanceFromNullpoint:Number = Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2)) if(distanceFromNullpoint > 50) { // Irgendeine Aktion } Listing 5.10: Verständlichere Variante der if-Anweisung
Mehrfach verwendete Variable ersetzen Temporäre, lokale Variablen braucht man immer wieder. Das beste Beispiel ist das berühmte i in for-Schleifen. Manchmal aber denkt man nicht groß darüber nach und verwendet eine temporäre Variable mehrfach in unterschiedlichen Zusammenhängen. So etwas kann dann den Code schwer lesbar machen. Listing 5.11 zeigt ein Beispiel.
258
Ändern und Testen var tmpValue:Number = height + box.height; circle.y = tmpValue; tmpValue = circle.y + circle.height; labelText.y = tmpValue + 5; Listing 5.11: Mehrfachverwendung einer Variablen
Hier wird tmpValue in unterschiedlichen Zusammenhängen verwendet. Nun ist dieses Beispiel sehr kurz und eventuell gerade noch so zu überblicken (über den Sinn des Codes wollen wir hier nicht lange diskutieren). Stellen wir uns aber vor, der Code wäre länger und die Stellen, an denen tmpValue verwendet wird, lägen weiter auseinander. Es ist für einen Entwickler schwer zu verstehen, dass tmpValue in unterschiedlichen Zusammenhängen verwendet wird, nämlich einmal, um eine Höhe für den Kreis zu berechnen, und einmal um den labelText fünf Pixel unter den Kreis zu platzieren. Es würde die Verständlichkeit des Codes erhöhen, wenn hier zwei eigene Variablen mit entsprechenden Namen verwendet würden. Folgendes zeigt, wie das aussehen könnte. var circlePositionY:Number = height + box.height; circle.y = circlePositionY; var circleBottomline:Number = circle.y + circle.height; labelText.y = circleBottomline + 5; Listing 5.12: Verbesserte Verständlichkeit der Berechnungen
Eine Methode oder ein Attribut verschieben Das Verschieben von Methoden oder Attributen ist in einigen heutigen Werkzeugen wie dem Adobe Flex Builder bereits automatisiert möglich. Methoden oder Attribute verschiebt man in eine andere Klasse, wenn man merkt, dass z. B. die Methode besonders viele Methoden und Attribute einer anderen Klasse verwendet. Das ist ein Indiz dafür, dass diese Methode eher in die andere Klasse gehört. Das Verschieben einer Methode oder eines Attributs in eine andere Klasse soll meistens dafür sorgen, dass zum einen die ursprüngliche Klasse entschlackt wird und sich auf ihre eigene eine Verantwortlichkeit konzentrieren kann und dass zum anderen eventuell die Kopplung zwischen Klassen verringert wird. Wenn nämlich eine Methode zum Arbeiten viele Methoden oder Attribute einer anderen Klasse benötigt und man sie deswegen in diese andere Klasse verschiebt, dann hat man damit auch die Abhängigkeit der beiden Klassen voneinander verringert. Die Gefahr beim Verschieben einer Methode oder eines Attributes ist immer, dass man in Klassen, die diese Methode bereits benutzen, vergisst, die Referenz anzupassen. Deswegen sollte man hier möglichst auf Werkzeuge zurückgreifen, die eine automatisierte Anpassung erlauben.
Eine fremde Klasse erweitern Das ist Ihnen bestimmt auch schon öfter begegnet. Sie verwenden eine bestehende Klasse, z. B. aus der Flash Player API. Alles funktioniert gut, aber Sie vermissen eine ganz bestimmte
259
Kapitel 5 Funktion, die eigentlich noch in die Klasse gehören würde. Nun können Sie speziell Klassen der Flash Player API nicht nachträglich ändern. In diesem Fall können Sie die vorhandene Klasse entweder durch Vererbung oder durch Komposition erweitern. Auch hier schauen wir uns wieder ein kleines Beispiel an. In der Abbildung sehen wir die Sprite-Klasse, die wir um ein Attribut position ergänzen wollen. Wir machen es mittels getter- und setter-Methoden verfügbar.
Sprite
XTSprite «property set» + position(Point) : void «property get» + position() : Point
Abbildung 5.5: Sprite wird durch XTSprite erweitert.
In diesem Fall ist die Erweiterung für den Zweck dieses Beispiels einfach eine kleine Komfortverbesserung. Wir ermöglichen es, die Position eines Sprites nicht nur einzeln über die x- und y-Attribute von Sprite zu setzen, sondern auch durch die Angabe einer PointInstanz. Wir haben hier Vererbung verwendet. In manchen Fällen können Klassen nicht beerbt werden, weil sie z. B. als final deklariert wurden, oder es kann sein, dass die Originalklasse von einer Klasse erzeugt wird, die wir nicht beeinflussen können, was bedeutet, dass wir nicht dafür sorgen können, dass statt der Originalklasse unsere Subklasse benutzt wird. In diesen Fällen können wir einen Wrapper bauen, der nicht von der Originalklasse erbt, sondern sie intern benutzt. Dieses Vorgehen bedeutet aber auch, dass wir alle Methoden der Originalklasse nachbauen und intern an die Originalklasse weiterleiten müssen. Keine sehr schöne Aufgabe und deswegen nur zu empfehlen, wenn einerseits die Originalklasse eine überschaubare Anzahl von Methoden hat, sich voraussichtlich nicht so schnell ändert und andererseits der letztendliche Nutzen die Arbeit rechtfertigt.
Ein Array mit unterschiedlichen Werten in ein Objekt verwandeln Idealerweise enthält ein Array immer Elemente gleichen Typs. Eine Liste von Namen, eine Liste von MovieClips, Buttons, URLs usw. Wenn man auf Fälle im Code stößt, wo im Array unterschiedliche Dinge gespeichert werden, bei denen die Position wichtig ist, um zu wissen, was sie bedeuten, dann sollte das geändert werden, denn so eine Logik ist schwer zu verstehen. Nehmen wir an, wir haben ein Array, in dem das erste Element ein Text ist, das zweite ein Button, das dritte wieder ein Text, vierte ein Button usw. Der Text soll hier immer das Label des darauffolgenden Buttons sein. Das ist eine fehleranfällige Logik und sollte vermieden werden. Für so ein Array kann man besser eine simple kleine Klasse erstellen, die als Attribute das Label und eine Referenz auf den Button enthält. Instanzen dieser Klasse kann man dann
260
Ändern und Testen wiederum in das ursprüngliche Array einsortieren. Listing 5.13 und Listing 5.14 zeigen das Ausgangsskript bzw. die überarbeitete Variante. var elements:Array = new Array(); elements.push("Weiter"); elements.push(nextButton); elements.push("Zurück"); elements.push(previousButton); elements.push("Abbrechen"); elements.push(cancelButton); Listing 5.13: Unterschiedliche Elementtypen im Array public class ButtonLabeling { public function ButtonLabeling(i_label:String, i_button:SimpleButton) { label = i_label; button = i_button; } private var _label:String; private var _button:SimpleButton; public function get label():String { return _label; } public function set label(value:String):void { _label = value; } public function get button():SimpleButton { return _button; } public function set button(value:SimpleButton):void { _button = value; } } // in der benutzenden Klasse dann ... var elements:Array = new Array(); elements.push(new ButtonLabeling("Weiter", nextButton)); elements.push(new ButtonLabeling("Zurück", previousButton)); elements.push(new ButtonLabeling("Abbrechen", cancelButton)); Listing 5.14: Verbessertes Array, verwendet nun nur noch gleiche Elemente
Daten aus einer Benutzeroberfläche in ein Model Objekt duplizieren Die meisten Benutzeroberflächen-Komponenten halten zumindest temporär die Daten, die sie anzeigen. Ein einfaches Textfeld kennt natürlich den Text, den es anzeigt. Eine komplette Trennung zwischen Benutzeroberfläche und Daten, so wie es in MVC (siehe hierzu
261
Kapitel 5 Kapitel 4) vorgeschlagen wird, ist somit gar nicht möglich. Nichtsdestotrotz sollte man immer versuchen, neben der Benutzeroberflächen-Komponente ein reines Datenobjekt zu betreiben, das unabhängig von der Benutzeroberfläche die Daten hält. Manchmal sieht man Code, in dem das nicht durchgängig eingehalten wurde, und die Daten werden direkt in der Benutzeroberfläche gehalten. In diesem Fall sollte man den Code umarbeiten und ein reines Datenobjekt einführen, das die Daten verwaltet und mittels Beobachter-Muster (oder in Flex auch mittels Data-Binding, das letztlich intern auch das Beobachter-Muster anwendet) eine Synchronisation der Daten zwischen Datenobjekt und Benutzeroberfläche gewährleistet. Als Beispiel nehmen wir ein kleines Info-Fenster, das in einer Flash-Website den aktuellen Seitentitel anzeigt. Wir nehmen an, dass die Anwendung den Seitentitel aus einer XMLDatei ausliest und direkt in das Info-Fenster schreibt. Die Klasse für das Info-Fenster könnte wie in Abbildung 5.6 aussehen.
PageController
InfoLayer setzt den Seitentitel «property set» + pageTitle(String) : void «property get» + pageTitle() : String
Abbildung 5.6: Der Seitentitel wird direkt im InfoLayer verwaltet.
Der PageController, der hier das Laden und den Aufbau von Seiten übernimmt, setzt den Seitentitel direkt im InfoLayer. Das ist ungünstig, denn somit übernimmt der InfoLayer, der ja die Verantwortlichkeit zur Anzeige von Daten hat, noch eine zweite Verantwortlichkeit, nämlich das Verwalten von Daten. Das soll er aber nicht, deswegen sollten wir ein Datenobjekt erzeugen, das diese Aufgabe übernimmt. Nun wäre es etwas mühsam, ein Objekt nur für einen einzigen String zu erzeugen. Wir sollten schauen, ob es nicht ein bestehendes Objekt gibt, das die Verwaltung übernehmen kann. Gehen wir davon aus, dass es bereits ein Datenobjekt gibt, das bereits den eigentlichen Inhalt der Seite verwaltet. Es ist die Klasse PageContent. Man kann durchaus sagen, dass der Seitentitel als Teil des Seiteninhalts angesehen werden kann. Wir werden also die PageContent-Klasse erweitern, dass sie den Seitentitel beherbergen kann. Nachdem wir also den Seitentitel der PageContent-Klasse hinzugefügt haben, müssen wir den PageController so abändern, dass er künftig den Seitentitel nicht mehr direkt in den InfoLayer schreibt, sondern in das PageContent-Objekt. Außerdem muss der InfoLayer als Eventlistener beim PageContent-Objekt angemeldet werden, für den Fall, dass der Seitentitel gesetzt oder verändert wird (alternativ könnten wir hier wie gesagt Data-Binding einsetzen, wenn wir eine Flex-Anwendung bauen würden). Die veränderte Struktur zeigt Abbildung 5.7.
262
Ändern und Testen
InfoLayer +
updateData(Event) : void
«property set» + pageTitle(String) : void «property get» + pageTitle() : String
PageContent PageController «property set» + pageTitle(String) : void «property get» + pageTitle() : String
Abbildung 5.7: Abgeänderte Struktur, nun über ein konkretes Datenobjekt, den PageContent
Einen Typbezeichner gegen eine echte Typklasse ersetzen In ActionScript gibt es keine echten Enumeratoren wie z. B. in C#. Normalerweise werden Enumeratoren in ActionScript über Konstanten simuliert. Das Problem ist, hinter einer Konstanten steht immer ein normaler Wert, z. B. ein String oder eine Zahl. Das bedeutet aber auch, dass eine Methode, die eigentlich einen der Enumeratoren benötigt, tatsächlich nur so einen String oder eine Zahl verlangen kann. Hierzu wieder ein kleines Beispiel (Listing 5.15). Wir sehen, dass die Methode alignButton() einfach eine Zahl erwartet. Zwar gehen wir davon aus, dass die Methode ausreichend dokumentiert ist, aber der Compiler kann hier nicht prüfen, ob auch ein korrekter Wert übergeben wird, das würde erst zur Laufzeit festgestellt werden. Schließlich könnte man an die Methode alignButton() auch ganz einfach den Wert 4 übergeben. public class Layer extends Sprite { public static const LEFT:int = 1; public static const RIGHT:int = 2; public static const CENTER:int = 3; private var button:SimpleButton; // ... public function alignButton(position:int):void { switch(position) { case LEFT: {
263
Kapitel 5 button.x = x; break; } case RIGHT: { button.x = x + width - button.width; break; } case CENTER: { button.x = x + (width / 2) - (button.width / 2); break; } case default: { throw new Error("Wert nicht erlaubt!"); break; } } } } Listing 5.15: Simulierte Enumeratoren als Konstanten
In so einer Situation kann es helfen, statt der normalen zahlenbasierten Konstanten eine Typenklasse zu erstellen, die konkrete Typeninstanzen als Ersatz für die simulierten Enumeratoren verwendet. Wir erstellen also zuerst eine solche Klasse. Hier muss leider für Flash bzw. ActionScript ein nicht ganz so schöner Umweg beschritten werden, weil es in ActionScript 3 keine privaten Konstruktoren gibt. package { import flash.display.SimpleButton; import flash.display.Sprite; public class Layer extends Sprite { public static function get LEFT():Alignment { return Alignment.LEFT; } public static function get RIGHT():Alignment { return Alignment.RIGHT; } public static function get CENTER():Alignment { return Alignment.CENTER; } private var button:SimpleButton; // ... public function alignButton(position:Alignment):void {
264
Ändern und Testen switch(position) { case LEFT: { button.x = x; break; } case RIGHT: { button.x = x + width - button.width; break; } case CENTER: { button.x = x + (width / 2) - (button.width / 2); break; } default: { throw new Error("Wert nicht erlaubt!"); break; } } } } } class Alignment { public static const LEFT:Alignment = new Alignment(); public static const RIGHT:Alignment = new Alignment(); public static const CENTER:Alignment = new Alignment(); } Listing 5.16: Enumeratoren, simuliert über eine private Klasse
Listing 5.16 zeigt die Implementierung. Weil es keine privaten Konstruktoren in ActionScript 3 gibt, behelfe ich mir mit einer privaten Klasse, die ich Alignment nenne und die außerhalb der package-Deklaration definiert wird. Diese Klasse ist nur für die Klasse Layer sichtbar. Alignment dient nur als abstrakter Typ und benötigt keinerlei Methoden oder Attribute, lediglich die drei statischen Konstanten, die die benötigten Enumeratoren repräsentieren. Wichtig hier ist, dass die Konstanten als Wert eine Instanz von Alignment selbst verwenden. Der Trick hierbei ist, dass die Klasse Alignment nur durch die Klasse Layer erzeugt werden könnte und sonst durch niemanden. Dadurch ist gewährleistet, dass niemand einfach eine weitere Konstante einführen kann, denn niemand sonst kann noch eine weitere Instanz von Alignment erzeugen. Die Klasse Layer wiederum kann nun definieren, welche Konstanten sie nach außen zulässt. In diesem Fall sind das alle drei. Sie ermöglicht dies über drei statische getterMethoden. Die Methode alignButton() wiederum verlangt nun als Parameter eine Instanz von Alignment. Da solche Instanzen nur über die drei statischen getter-Methoden von der Layer-Klasse selbst bezogen werden können, ist gewährleistet, dass keine falschen Werte an alignButton() übergeben werden können. Über diesen Weg ist außerdem sichergestellt,
265
Kapitel 5 dass bereits zur Kompilierzeit klar ist, dass ein korrekter Alignment-Wert übergeben wird. Es können also zur Laufzeit keine bösen Überraschungen mehr eintreten.
Bedingungskonstrukte gegen Polymorphie austauschen Viele strukturelle Schwächen im Code einer Anwendung wachsen langsam und waren nicht unbedingt von Anfang an vorhanden. Hier muss schnell eine kleine Erweiterung eingebaut werden, da eine Änderung vorgenommen werden usw. So entstehen über die Zeit Konstrukte, die irgendwann immer schwerer zu warten sind. Ein Beispiel hierfür sind switch-case-Konstrukte, die in einer Klasse das Verhalten bestimmen. Eine Klasse soll sich aufgrund von einer bestimmten Situation unterschiedlich verhalten. In Listing 5.17 sehen wir ein Beispiel für eine Klasse, die ihr Verhalten basierend auf einem switch-case-Konstrukt verändert. public class VideoPlayer extends Sprite { public static const SWF_STREAM:int = 1; public static const NET_STREAM:int = 2; private var _type:String = NET_STREAM; private var videoContainer:DisplayObject; private var _videoURL:String; private var connection:NetConnection; private var stream:NetStream; // ... public function get type():String { return _type; } public function set type(value:String):void { _type = value; } public function get videoURL():String { return _videoURL; } public function set videoURL(value:String):void { _videoURL = value; } public function startStream():void { switch(_type) { case SWF_STREAM: { videoContainer = new Loader(); (videoContainer as Loader).addEventListener( ProgressEvent.PROGRESS, swfStreaming );
266
Ändern und Testen (videoContainer as Loader).load(_videoURL); break; } case NET_STREAM: { connection = new NetConnection(); connection.connect(null); stream = new NetStream(connection); videoContainer = new Video(); (videoContainer as Video).attachNetStream(stream); stream.play(_videoURL); break; } } addChild(videoContainer); } private function swfStreaming(e:ProgressEvent):void { if ((e.bytesLoaded / e.bytesTotal) > 0.4) { (videoContainer as Loader).(content as MovieClip).play(); } } } Listing 5.17: VideoPlayer mit zwei unterschiedlichen Video-Modi
Es handelt sich um einen Videoplayer, der entweder ein Video über die NetStream-Klasse streamt oder eine SWF-Datei progressiv lädt und ab einem gewissen Ladestand parallel zum weiteren Laden schon abspielt. Im Zentrum steht hier die Methode startStream(), die entweder die eine oder die andere Variante initiiert. Im Prinzip sind hier zwei unterschiedliche Arten von Videoladen und -abspielen in einer Klasse integriert. Das macht die Klasse etwas verworren und schwerer zu handhaben. Wir wollen also nun das Konstrukt entzerren. Polymorphismus heißt ja z.B., dass mehrere Klassen von der gleichen Hauptklasse erben und bestimmte Methoden der Hauptklasse überschreiben mit spezifischem Verhalten. Es liegt hier also nahe, dass wir eine Hauptklasse VideoStream schreiben unter anderem mit einer Methode startStream(). In der Hauptklasse werden wir die Methode in diesem Fall leer implementieren, weil es eigentlich kein gemeinsames Verhalten gibt, das in die Hauptklassen-Methode integriert werden könnte. Dann werden wir zwei Unterklassen erstellen, einmal SWFVideo und einmal NetStreamVideo. Diese überschreiben dann die Methode startStream() aus der Hauptklasse im Prinzip mit dem Inhalt aus den momentanen case-Zweigen der jetzigen startStream()-Methode von VideoPlayer. Aber der Reihe nach. Zuerst schreiben wir die abstrakte Hauptklasse VideoStream (Listing 5.17).
267
Kapitel 5 public class VideoStream { protected var _videoContainer:DisplayObject; protected var _videoURL:String; public function get videoURL():String { return _videoURL; } public function set videoURL(value:String):void { _videoURL = value; } public function get videoContainer():DisplayObject { return _videoContainer; } public function startStream():void {} } Listing 5.18: Die abstrakte Hauptklasse VideoStream
In VideoStream sind hier also nur die wichtigsten gemeinsamen Attribute und Methoden integriert. Als Nächstes bauen wir die beiden konkreten Klassen für das SWF-Streamen und das Streamen über die NetStream-Klasse (Listing 5.19 und Listing 5.20). public class SWFVideo extends VideoStream { override public function startStream():void { _videoContainer = new Loader(); (_videoContainer as Loader).addEventListener( new ProgressEvent.PROGRESS, swfStreaming ); (_videoContainer as Loader).load(videoURL); } private function swfStreaming(e:ProgressEvent):void { if ((e.bytesLoaded / e.bytesTotal) > 0.4) { (_videoContainer as Loader).(content as MovieClip).play(); } } } Listing 5.19: Die SWFVideo-Klasse public class NetStreamVideo extends VideoStream { private var connection:NetConnection; private var stream:NetStream; override public function startStream():void {
268
Ändern und Testen connection = new NetConnection(); connection.connect(null); stream = new NetStream(connection); _videoContainer = new Video(); (_videoContainer as Video).attachNetStream(stream); stream.play(videoURL); } } Listing 5.20: Die NetStreamVideo-Klasse
Man kann gut sehen, wir haben jetzt im Prinzip die jeweiligen case-Zweige aus der ursprünglichen VideoPlayer-Klasse in die beiden Klassen SWFVideo und NetStreamVideo transferiert. Zusätzlich mussten natürlich auch die entsprechenden Klassenvariablen verschoben werden und speziell bei der SWFVideo-Klasse auch noch die EventhandlerMethode swfStreaming(). Als Letztes müssen wir nun nur noch VideoPlayer so umbauen, dass er von den neuen Klassen auch Nutzen zieht. Listing 5.21 zeigt das Resultat des Umbaus. public class VideoPlayer extends Sprite { private var _type:VideoStream; private var video:DisplayObject; private var videoURL:String; // ... public function get type():VideoStream { return _type; } public function set type(value:VideoStream):void { _type = value; } public function get videoURL():String { return _videoURL; } public function set videoURL(value:String):void { _videoURL = value; } public function startStream():void { _type.videoURL = videoURL; _type.startStream(); video = _type.videoContainer; addChild(videoContainer); } } Listing 5.21: Die abgeänderte Klasse VideoPlayer
269
Kapitel 5 Zunächst sehen wir, dass _type nun nicht mehr ein String ist, sondern vom neuen Typ VideoStream. Der VideoPlayer wird nun also mit einem konkreten VideoStream konfiguriert, entweder einem NetStreamVideo oder einem SWFVideo. Und die startStream()Methode konnte gründlich entrumpelt werden. Hier kommt nun die Polymorphie zum Einsatz. Wir rufen nun einfach auf dem konfigurierten _type die startStream()-Methode auf. Je nachdem, was konfiguriert wurde, wird nun die korrekte konkrete Methode unserer beiden VideoStream-Klassen aufgerufen. Mit dieser geänderten Konstruktion könnten nun beliebig viele weitere VideoStream-Typen hinzugefügt werden, und außerdem wurden hier auch wieder Verantwortlichkeiten besser aufgeteilt. Dies macht die Arbeit mit den einzelnen Klassen einfacher.
Ein Standard-Null-Objekt einführen Wenn eine Klasse eine andere verwendet, die ihr übergeben wird, dann muss die erstere prüfen, ob sie sie schon erhalten hat oder nicht. Das endet dann meist in einer Reihe von ifAnweisungen, die auf null prüfen, beispielhaft zu sehen in Listing 5.22. public class Main extends Sprite { private var _configuration:Configuration; private var tracker:Tracking; private var header:HeaderLayer; public function Main():void { // ... } public function get configuration():Configuration { return _configuration; } public function set configuration(value:Configuration):void { _configuration = value; } public function start():void { tracker = new Tracking(); if (configuration != null) tracker.baseURL = configuration.trackBaseURL; else tracker.baseURL = "/tracking/core/tracker.jsp"; if (configuration != null) header.setTitle(configuration.mainTitle); else header.setTitle("Standardanwendung");
270
Ändern und Testen // ... } } Listing 5.22: Eine Klasse mit null-Prüfungen
Dies sei eine Hauptklasse einer kleinen Anwendung, die zum Arbeiten eine Konfiguration benötigt. Da es sein kann, dass ihr die Konfiguration nicht übergeben wurde, werden diverse if-Anweisungen, die auf null prüfen, verwendet. Hier sind beispielhaft nur zwei gezeigt, einmal für einen Basispfad für ein Trackingsystem und zum anderen der Titel der Anwendung. Wie kann man diese Null-Prüfungen nun anders lösen? Indem man eine Klasse schreibt, die von Configuration erbt, die aber als Null-Stellvertreter fungiert. Diese NullConfiguration überschreibt alle Methoden von Configuration und implementiert stattdessen default-Werte, die gerade ausreichen, damit die Anwendung weiter arbeiten kann. Werfen wir einen kurzen Blick auf die originale Configuration-Klasse. public class Configuration { private var _trackBaseURL:String; private var _mainTitle:String; public function get trackBaseURL():String { return _trackBaseURL; } public function set trackBaseURL(value:String):void { _trackBaseURL = value; } public function get mainTitle():String { return _mainTitle; } public function set mainTitle(value:String):void { _mainTitle = value; } // ... } Listing 5.23: Die Configuration-Klasse
Wir sehen eine einfache Ansammlung von privaten Attributen und ihren öffentlichen Gettern und Settern. Davon leiten wir nun also die NullConfiguration-Klasse ab wie in Listing 5.24 zu sehen. public class NullConfiguration extends Configuration { override public function get trackBaseURL():String { return "/tracking/core/tracker.jsp"; } override public function set trackBaseURL(value:String):void {} override public function get mainTitle():String {
271
Kapitel 5 return "Standard Anwendung"; } override public function set mainTitle(value:String):void {} // ... } Listing 5.24: Die NullConfiguration-Klasse
Wir sehen, NullConfiguration wirft ganz simpel Standardwerte zurück anstelle von tatsächlich konfigurierten Werten. Weil das so ist, interessiert sich die Klasse auch nicht für die Setter, weswegen sie leer implementiert sind. Nachdem wir nun diese NullConfigurationKlasse erstellt haben, müssen wir nun noch unsere Hauptklasse dahingehend ändern, dass sie die NullConfiguration auch nutzt. Wie das aussieht, sehen wir in Listing 5.25. public class Main extends Sprite { private var _configuration:Configuration = new NullConfiguration(); private var tracker:Tracking; private var header:HeaderLayer; public function Main():void { // ... } public function get configuration():Configuration { return _configuration; } public function set configuration(value:Configuration):void { _configuration = value; } public function start():void { tracker = new Tracking(); tracker.baseURL = configuration.trackBaseURL; header.setTitle(configuration.mainTitle); // ... } } Listing 5.25: Die veränderte Main-Klasse verwendet nun die NullConfiguration.
Wichtig ist, dass hier nun die private Variable _configuration bereits mit der NullConfiguration vorbelegt wird. Wenn also keine wirkliche Konfiguration gesetzt wird, kann mit der NullConfiguration gearbeitet werden. Das hat zur Folge, dass wir in der start()-Methode
272
Ändern und Testen keine Null-Prüfungen mehr durchführen müssen, sondern einfach auf die Konfiguration zugreifen können. Hier interessiert uns nun nicht mehr, ob die tatsächliche Konfiguration da ist oder nicht. Nun hat dieses Vorgehen ein Problem. Unter Umständen bemerkt man nun gar nicht mehr, ob denn nun jemals eine korrekte Konfiguration übergeben oder ob immer mit der NullConfiguration gearbeitet wird. Zu diesem Zweck ist es sinnvoll, die originale Configuration-Klasse mit einer isNull()-Methode auszustatten, die hier standardmäßig false zurückliefert. Nur die NullConfiguration-Klasse würde diese Methode wieder überschreiben und true zurückgeben. Listing 5.26 zeigt hier die entsprechenden Auszüge aus den beiden Klassen. public class Configuration { private var _trackBaseURL:String; private var _mainTitle:String; public function isNull():Boolean { return false; } // ... } public class NullConfiguration extends Configuration { override public function isNull():Boolean { return true; } // ... } Listing 5.26: Hinzugefügte isNull()-Methode
Mit dieser Methode können wir nun zur Sicherheit zumindest eine trace-Meldung oder einen Eintrag in eine log-Datei vornehmen, damit wir darauf aufmerksam gemacht werden, ob tatsächlich eine korrekte Konfiguration ankommt (siehe Listing 5.27). public class Main extends Sprite { // ... public function start():void { trace("Konfiguration vorhanden? " + !configuration.isNull()); // ... } } Listing 5.27: Hinzugefügte Meldung, ob eine echte Konfiguration vorhanden ist
273
Kapitel 5
Einzelne Parameter zu einem Parameterobjekt zusammenführen Eine recht einfache, aber sinnvolle Maßnahme ist das Zusammenführen von Parametern, die oft schon zusammen übergeben werden, in eine Klasse. Wenn Sie in verschiedenen Methoden immer wieder die gleichen Parameter übergeben, dann können Sie diese eventuell in einer bündelnden Klasse zusammenführen und künftig eher eine Instanz dieser Klasse übergeben. Das verbessert die Verständlichkeit, denn die Klasse beschreibt über ihren Namen den übergreifenden Rahmen, und es ist auch einfacher zu schreiben, denn nun muss man nicht mehr mit einzelnen Variablen hantieren, sondern nur noch mit einem Objekt. Schauen wir uns hierzu ein kleines Beispiel an (Listing 5.28). Hier haben wir drei Methoden, die gleiche Parameter entgegennehmen. Eventuell finden Sie solche Methoden nicht unbedingt in der gleichen Klasse wieder, aber in mehreren Klassen verteilt. Dann können Sie die Parameter in einer neuen Klasse zusammenfassen. public function setLoginLayerStyle(bgColor:int, borderColor:int):void { // ... } public function setInfoLayerStyle(bgColor:int, borderColor:int):void { // ... } public function setTooltipStyle(bgColor:int, borderColor:int):void { // ... } Listing 5.28: Gleiche Parameterpaare
Wir erstellen also ganz einfach eine neue Klasse LayerStyle, die als Attribute die beiden Parameter übernimmt (Listing 5.29). Nun können wir in den drei Methoden statt der einzelnen Parameter Instanzen von LayerStyle übergeben (Listing 5.30). Wenn nun später weitere Charakteristiken für das Aussehen eines Layers hinzugefügt werden sollen, muss einfach nur LayerStyle angepasst werden, aber nicht die Parameterübergabe der drei Methoden. public class LayerStyle { private var _bgColor:int; private var _borderColor:int; public function get bgColor():int { return _bgColor; } public function set bgColor(value:int):void { _bgColor = value; } public function get borderColor():int { return _borderColor; } public function set borderColor(value:int):void {
274
Ändern und Testen _borderColor = value; } } Listing 5.29: Die Klasse LayerStyle bündelt die Parameter. public function setLoginLayerStyle(layerStyle:LayerStyle):void { // ... } public function setInfoLayerStyle(layerStyle:LayerStyle):void { // ... } public function setTooltipStyle(layerStyle:LayerStyle):void { // ... } Listing 5.30: Die drei Methoden verwenden nun Instanzen von LayerStyle.
Explizite Typecasts in einer Methode kapseln Diese Maßnahme ist recht einfach, erspart aber Tipparbeit. Nehmen wir an, wir haben eine Methode, die ein DisplayObject zurückgibt, wir wissen aber, dass tatsächlich zur Laufzeit immer ein MovieClip zurückgegeben wird. In dem Fall sollten wir bereits in der Methode die Instanz auf einen MovieClip casten und auch als MovieClip zurückgeben anstatt als DisplayObject. Wenn Sie zum Beispiel eine eigene Klasse geschrieben haben, in der Sie unterschiedliche MovieClips abspielen, und Sie haben eine Methode, die einzelne dieser MovieClips zurückgibt, dann sollten Sie bereits in der Methode das Casten vornehmen, damit die Entwickler, die Ihre Methode benutzen, das nicht jedes Mal selber schreiben müssen. public function getAnimation(animationIndex:int):DisplayObject { return animationStage.getChildAt(animationIndex); } Listing 5.31: Unkomfortable Methode liefert vom Typ DisplayObject. public function getAnimation(animationIndex:int):MovieClip { return MovieClip(animationStage.getChildAt(animationIndex)); } Listing 5.32: Komfortabler Zugriff direkt auf einen MovieClip
275
Kapitel 5
5.2 Testen und Validieren Jeder, der schon mal eine Flash-Anwendung gebaut hat, weiß, was Testen bedeutet. Man nimmt die Anwendung und prüft, ob sie das tut, was sie soll. Die Anwendung wird also gestartet, und man schaut sich alle Screens an, die die Anwendung hat, gibt hier und da in Formularfeldern Beispielwerte ein und guckt, ob es zu keinem Fehler kommt. Sich nur auf solch einen oberflächlichen Test zu verlassen, ist aber gefährlich. Zwar können so auch Fehler gefunden werden, aber ohne eine strukturierte Vorgehensweise ist es sehr wahrscheinlich, dass bestimmte Bereiche einer Anwendung gar nicht oder zu wenig betrachtet werden. Sehr gefährlich ist auch, wenn nur der Entwickler selbst seine eigene Anwendung händisch testet. Entwickler neigen dazu, nur die Bereiche der Anwendung zu testen, die ihnen gefallen oder von denen sie überzeugt sind, dass sie fehlerfrei sind. Solche Tests bringen dann natürlich nichts. Wie umfangreich man eine Anwendung prüft, hängt letztlich natürlich einerseits davon ab, wie zuverlässig die Anwendung laufen muss. Ein Online-Shop hat sicherlich andere Anforderungen hinsichtlich Stabilität und Zuverlässigkeit als ein Werbebanner. Der Aufwand, den man in die Prüfung der Anwendung steckt, muss dem Rechnung tragen. Im Folgenden beschreibe ich die im Software Engineering bekannten Methoden zur Prüfung einer Anwendung. Welche davon in einem konkreten Projekt angewandt werden sollten, hängt also von der erwarteten Zuverlässigkeit des Endprodukts ab.
5.3 Validierung/Verifikation Im Software Engineering unterscheidet man bezüglich der Prüfung einer Anwendung zwischen der Validierung und der Verifikation. Validierung meint: Erfüllt die Anwendung den vom Kunden oder Nutzer erwarteten Zweck? Im Falle eines Online-Shops könnte das also konkret bedeuten: Können Kunden des Händlers über den Shop schnell und einfach Waren finden und kaufen? Werden auch alle zusätzlichen Nebenprozesse eines typischen Shops (z. B. Rücksendungen, Reklamationen, Hilfe usw.) zur Zufriedenheit angeboten? Ein anderes Beispiel wäre eine Online-Microsite für ein bestimmtes Produkt. Hier würde Validierung die Frage stellen: Informiert die Anwendung die Nutzer ausreichend über das Produkt? Schafft es die Anwendung, neue Interessenten für das Produkt oder gar Käufer zu generieren? Die Fragestellungen lassen erahnen, Validierung ist keine einfache Angelegenheit. Die Beantwortung dieser Fragen kann zumeist nur über statistische Auswertungen erfolgen. Entweder man schafft ein künstliches Testszenario, indem man viele Testpersonen (heutzutage werden ja gerne »geschlossene« oder »offene« Betatests gemacht) einlädt, die Anwendung zu benutzen und ihre Erfahrungen zu schildern (informell) oder auch die Anwendung nach festgelegten Prozeduren zu benutzen (formell), um typische Fälle der Nutzung zu prüfen. Darunter fallen z. B. Usability-Tests. Auch wenn die Anwendung bereits veröffentlicht wurde, kann zumindest bei Webanwendungen über anonymes Webtracking zu einer gewissen Genauigkeit festgestellt werden, wie die Anwendung genutzt wird, und es können eventuelle Schwächen identifiziert werden.
276
Ändern und Testen Bei der Verifikation wiederum wird die Frage gestellt, ob die Anwendung korrekt funktioniert, sprich, ob sie stabil und fehlerfrei läuft. Hier geht es also eher um funktionale Korrektheit als um Nutzbarkeit. Üblicherweise beschäftigen sich Softwareentwickler eher mit der Verifikation als mit der Validierung, wenngleich Schwächen, die bei der Validierung entdeckt werden, ihre Ursachen in fehlerhaftem Verhalten haben können. Wenn zum Beispiel in einem Online-Spiel fälschlicherweise die Hilfe nicht aufgerufen werden kann, dann wird dies in der Validierung als Schwäche der Anwendung bemerkt werden (und hoffentlich auch bei der Verifikation). In der Verifikation ist man also konkreten Fehlern in der Anwendung auf der Spur. Hierbei sind aber nicht nur Fehler im Code gemeint. Fehler können ja in allen Phasen eines Projekts entstehen: In den Anforderungen, im Design und natürlich in der Entwicklung. Die Verifikation genauso wie die Validierung beziehen sich deswegen auch auf alle diese Bereiche. Um nun also Fehler zu finden, gibt es grundsätzlich zwei Verfahren: Inspektionen und Tests. Bei einer Inspektion (oder auch Review) wird geprüft, indem man liest und bewertet. Ein Prüfer kann also die Anforderungsdokumente lesen und bewerten, die SoftwareDesigns (also z. B. UML-Diagramme, IT-Konzepte, Skizzen usw.) lesen und bewerten oder auch Code-Reviews machen und dabei den Code lesen und nach Fehlern oder Schwachstellen suchen. Inspektionen sind in diesem Sinne für alle Phasen eines Projekts geeignet. Tests können in Bezug auf die Anforderungen und in Bezug auf den Code vorgenommen werden. In Bezug auf die Anforderungen sind hiermit z. B. wieder Usability-Tests gemeint, die also auf die Validierung einzahlen. Es ist nach momentanem Stand nicht möglich, die Korrektheit des Software-Designs über Tests sicherzustellen. Wie ich schon in Kapitel 3 erläutert habe, ist das Software-Design eine Aufgabe, die nicht eine eindeutige (deterministische) Lösung zur Folge hat. Ein Test ergibt aber normalerweise nur, ob er bestanden oder nicht bestanden wurde, das ist für Software-Designs nicht möglich. Auch ist es nicht wie bei Usability-Tests, wo Testpersonen durch Ausprobieren prüfen können, ob die Anwendung korrekt läuft. Einen Entwurf kann man nicht testweise laufen lassen. Tests können auch in Bezug auf den Code vorgenommen werden, und das ist für viele Flash-Entwickler sicher der Bereich, mit dem sie sich am ehesten identifizieren können. Es gibt viele Möglichkeiten, wie man seinen Code testen kann:
Funktionstests Unit-Tests Integrationstests Lasttests Es gibt noch weitere, wie z. B. spezielle Abnahmetests oder Regressionstests (Tests, die schon durchgeführt wurden und die aufgrund einer Änderung noch einmal ausgeführt werden), aber mit denen wollen wir uns nicht näher beschäftigen, denn sie sind letztlich oft einfach Unterarten von den oben genannten. Ein Unit-Test z. B. kann wunderbar als Regressionstest eingesetzt werden, weil man ihn auf einfache Art und Weise immer wieder ablaufen lassen kann.
277
Kapitel 5 Ein wichtiger Unterschied zwischen Tests und Inspektionen im Bereich Code ist die Tatsache, dass Tests immer nach dem Blackbox-Prinzip ablaufen und Inspektionen immer nach dem Whitebox-Prinzip. In Kapitel 3 habe ich bereits erläutert, was das Blackbox-Prinzip ist. Es besagt, dass wir die Anwendung nur so sehen, wie die Pakete, Namespaces und Klassen es uns erlauben. Wir können also nur Klassen und Klassenmitglieder sehen, die public sind und auf deren Namespace wir Zugriff haben. Was darüber hinaus im Inneren der Klasse geschieht, interessiert uns nicht. Das ist das Prinzip der Kapselung. Alle Tests, die wir auf unserer Anwendung ausführen, kommen über diese Kapselung nicht hinweg, denn Tests laufen immer auf der ausgeführten Anwendung. Das ist auch gut so, denn letztlich wollen wir wissen, ob die Daten, die wir in unsere Anwendung hineingeben, oder ob die Nutzerinteraktionen, die wir auf der Anwendung ausführen, nicht zu einem Fehler führen. Wie die Anwendung das intern anstellt, kann und soll in einem Test nicht betrachtet werden. Auch in einem Lasttest wird man nur feststellen, dass eine Anwendung eventuell einer hohen Beanspruchung nicht standhält, aber man erfährt nicht warum. Tests können also Fehler nur entdecken, aber nicht isolieren. Sprich, ein Test sagt mir, dass ich einen Fehler habe, aber nicht wo. Über manuelle Suche oder über Debugging- oder Profiler-Tools muss ich dann selber herausfinden, wo der Fehler liegt. Jeder Flash-Entwickler weiß, dass ein Fehler, der in einem Bereich einer Anwendung zutage tritt, nicht zwingend auch dort zu finden ist. Bei Inspektionen herrscht ein anderes Prinzip, das der Whitebox. Hier führt man die Anwendung nicht aus, sondern man liest sich direkt den Quellcode durch, Zeile für Zeile. Whitebox meint hier also, dass man in diesem Fall keinerlei Grenzen von public oder private ausgesetzt ist, man hat stattdessen alles direkt vor sich und kann bis in das Innerste des Codes vorstoßen. Inspektionen finden deshalb meistens ganze andere Arten von Fehlern, als Tests das können. Während einer Inspektion kann einem z. B. auffallen, dass sich mehrere Schleifen zusammenfassen lassen, um Performance zu gewinnen. Hier wird also nicht ein Funktionsfehler, sondern ein Strukturfehler gefunden. Inspektionen können Verstöße gegen eigene CodingRichtlinien aufdecken, die die Lesbarkeit des Codes gewährleisten sollen. Inspektionen können auch Codestellen aufdecken, die gegen gute Entwurfsprinzipien wie lose Kopplung oder Kapselung verstoßen. Auf der anderen Seite erscheinen Inspektionen vielen Entwicklern erst einmal suspekt. Das liegt vielleicht auch daran, dass Inspektionen zunächst nicht an irgendwelche Tools geknüpft sind, sondern nur den gedruckten Quellcode, ein strukturiertes Vorgehen und ein paar willige Leser erfordern. Viele Entwickler scheuen den Aufwand, händisch etwas durchzugehen, was sich mit automatisierbaren Tests vermeintlich viel schneller und zuverlässiger erledigen lässt. Auch gibt es da das ungute Gefühl, dass andere Personen den eigenen Quellcode eventuell übermäßig kritisieren und einen selbst für unfähig halten. Wie ich noch erläutern werde, ist eines der wichtigsten Prinzipien bei Inspektionen, dass sie kontrolliert nach bestimmten Regeln vorgenommen werden, damit sie ihre volle Effektivität entfalten und damit sie nicht zu Verstimmung zwischen den Entwicklern führen. Dann aber können sie ein sehr wirkungsvolles Instrument zu Fehlerfindung und -isolation sein. Denn ein in einer Inspektion gefundener Fehler ist nicht nur einfach entdeckt wie in einem Test, sondern auch immer gleich exakt lokalisiert. Näheres zu den einzelnen Inspektionen und Tests folgt in den kommenden Abschnitten.
278
Ändern und Testen
5.4 Effektivität/Effizienz Es gibt viele Studien über die Effektivität und die Effizienz von Tests bzw. Inspektionen. Hier kurz eine Erläuterung zum Unterschied der beiden Begriffe:
Effektivität meint die Anzahl der insgesamt gefundenen Fehler in einer Anwendung im Verhältnis zu den tatsächlich vorhandenen Fehlern. Wenn man also genau wüsste, dass sich in einer Anwendung zehn Fehler versteckt halten, und man hat während der Validierung/Verifikation fünf gefunden, dann hat man eine Effektivität von 50% erreicht.
Effizienz meint die Zahl von Fehlern, die man in einem bestimmten Zeitraum findet, z. B. zwei Fehler pro Stunde. Die Ergebnisse der Studien sind nicht einheitlich. Manche sagen, dass Inspektionen effektiver sind, andere sagen, Tests sind effektiver. In einer übergreifenden Studie wurden zwölf Einzelstudien untersucht und miteinander verglichen (Runseson et al. 2006). Das Ergebnis der Forscher ist, dass sie Inspektionen für die Anforderungen und das Design empfehlen und Tests für den Code. Steve McConnell empfiehlt darüber hinaus, die Verfahren zu kombinieren (McConnell 2007). Beide Quellen zeigen, dass sowohl Tests als auch Inspektionen im Einzelnen meist nicht über 50% an Fehlertreffern hinauskommen. Kombinierte Methoden jedoch können auf 80% oder mehr kommen. Bezüglich der Effizienz sind in den meisten Studien Tests im Vorteil. Hier kann kaum ein guter durchschnittlicher Wert angegeben werden, die gemessenen Werte reichen von ca. 0,2–2 Fehlern pro Stunde bei Inspektionen und 0,03–4,7 Fehlern pro Stunde bei Tests. Da viele Testverfahren heutzutage unter Zuhilfenahme von Automation geschehen, ist es durchaus nachvollziehbar, dass hier die Effizienz höher ist, wobei man natürlich z. B. bei einem Unit-Test nicht nur die Durchführung, sondern auch das Schreiben des Tests mit einbezieht. Der grundsätzliche Unterschied zwischen Tests und Inspektionen bezüglich ihrer Effektivität liegt noch in einem anderen, etwas weicheren Faktor. Bei Inspektionen findet grundsätzlich ein Austausch zwischen den Entwicklern statt. Zwei oder mehr Flash-Entwickler z. B. diskutieren Vor- und Nachteile von Teilen des geprüften Codes. Das hat neben dem reinen Finden von Fehlern noch viele weitere »Nebenwirkungen«: Es führt zu Wissensaustausch, es fördert die Teambildung und es festigt Standards. Aspekte, die reine Tests üblicherweise nicht bieten können.
5.5 Inspektionen Inspektionen bezeichnen das Lesen eines Dokuments oder Quellcodes, um darin Fehler zu entdecken. Wie schon in vorhergehenden Abschnitten beschrieben, sind Inspektionen Whitebox-Verfahren, weil sich dem Prüfer das gesamte Innenleben des Codes oder Dokuments darlegt, im Gegensatz zu Blackbox-Tests, wo sich dem Prüfer nur die Schnittstelle offenbart.
279
Kapitel 5 Inspektionen gibt es in unterschiedlichen Ausprägungen. Die mir wichtigsten sind:
Formelle Inspektionen Informelle Inspektionen Einzelnes Codelesen Die Auflistung ist nicht sehr präzise, weil letztlich einzelnes Codelesen auch eine informelle Inspektion ist, aber es soll verdeutlichen, dass Inspektionen nicht zwingend immer in Meetings münden müssen, dazu gleich mehr.
5.5.1 Formelle/Informelle Inspektionen Formelle Inspektionen werden in allen zu dem Thema vorhandenen Studien als deutlich effektiver angesehen als informelle. Das liegt wohl an dem zielstrebigeren Verfahren, das z. B. durch einen Moderator begleitet wird, im Gegensatz zu informellen Methoden, in der sich die Gruppe meist lose selbst organisiert.
Autor übergibt Code an Moderator
Prüfer einigen sich in Meeting mit Moderator und Autor auf Liste der Fehler.
Autor behebt Fehler.
Moderator bereitet Code für Prüfung vor.
Prüfer lesen zunächst den Code allein und identifizieren Fehler.
Bei Bedarf erneute Inspektion, wenn durch Fehlerbehebung die Codestruktur stark verändert wurde.
Abbildung 5.8: Prinzipieller Ablauf einer formellen Inspektion
Formelle Inspektion klingt zunächst etwas steif, ist aber letztlich halb so wild. Eine formelle Inspektion besteht aus einem Moderator, dem Autor (des Quellcodes oder eines anderen Dokuments, z. B. Softwareentwurf) und mehreren Prüfern. Die Gruppe sollte nicht zu groß werden, zwei oder drei Prüfer reichen in der Regel aus. Der Moderator bereitet zunächst den Quellcode oder das Dokument des Autors auf, das bedeutet: Meistens werden die Dokumente oder der Quellcode tatsächlich ausgedruckt und mit einem Kommentarrand versehen, damit man Notizen machen kann.
280
Ändern und Testen Die Ausdrucke werden nun an die Prüfer verteilt. Je nach Komplexität kann es Sinn machen, dass der Autor den Prüfern zunächst einen kurzen Überblick über das Projekt und das Dokument bzw. den Quellcode gibt, damit die Einarbeitung nicht zu schwer fällt. Gerade bestimmte Begriffe aus der Geschäftswelt, aus der das Projekt entstammt, eignen sich, vorher erläutert zu werden. Dieser Überblick soll aber nicht schon im Detail das Dokument oder den Quellcode erklären oder gar verteidigen. Sätze wie: »Ich weiß, dass da noch viel verbessert werden kann, aber es war keine Zeit« helfen in der Regel nicht weiter. Im Falle von Quellcode sollte dieser kompilierbar sein. Fehler also, die offensichtlich direkt zu Kompilierfehlern führen, sollten in jedem Fall bereits ausgemerzt sein. Die Prüfer sollten außerdem eventuelle Dokumente und Checklisten bezüglich allgemeiner Programmier- oder Entwurfstandards zur Hand haben, falls es solche im Unternehmen gibt. Die Prüfer haben nun Zeit, Dokument oder Quellcode zu lesen und nach Fehlern zu suchen. Der Moderator nennt zudem einen Termin zur Besprechung der Ergebnisse. Während der Besprechung wird nun noch mal der gesamte Code oder das Dokument durchgegangen, und es werden gemeinsam alle Fehler auf den Tisch gebracht. Dabei entscheidet die Gruppe gemeinsam, ob ein Fehler auch wirklich ein Fehler ist. Gerade bei Entwurfsdokumenten kann es durchaus vorkommen, dass ein Prüfer eine Entwurfsentscheidung für einen Fehler hält, wohingegen andere Prüfer den Entwurf für sinnvoll halten. Nur mehrheitlich als Fehler identifizierte Punkte – der Moderator kann hier im Zweifel eine Entscheidung treffen – kommen in die Liste der zu korrigierenden Fehler. Wichtig während dieses Prozedere ist, dass es nur um die Feststellung und Lokalisierung eines Fehlers geht, nicht um Fehlerbehebung. Wenn also ein Fehler identifiziert und lokalisiert ist (sprich, man hat notiert, wo er entdeckt wurde), dann ist er abgehakt, und man fährt fort. Dieses gemeinsame Inspektionsmeeting sollte zwei Stunden nicht überschreiten. Grundsätzlich geht man für Quellcode von ungefähr 200 Zeilen pro Stunde aus, die man gemeinsam durchgehen kann, bevor die Konzentration deutlich schwindet. Nach Beendigung des Meetings übergibt der Moderator die Liste an gefundenen Fehlern an den Autor, und dieser muss nun die Liste abarbeiten und die Fehler im Dokument oder Quellcode beheben. Im Zweifel kann eine Nachinspektion gemacht werden, wenn es sehr viele tiefgreifende Änderungen zu machen gab. Es mehren sich Stimmen, die sagen, dass das gemeinsame Durchgehen des Codes nicht unbedingt nötig ist, weil dabei im Schnitt nur etwa noch 10% der insgesamt in einer Inspektion gefundenen Fehler entdeckt werden. Das meiste wird während der Vorbereitung von den einzelnen Prüfern entdeckt. Eine abgewandelte Form des Inspektionsmeetings könnte also vorsehen, dass man nur noch die einzeln gefundenen Fehler durchgeht, diskutiert und dann auf die Liste setzt, dass kann das Inspektionsmeeting stark verkürzen. Neben formellen Inspektionen gibt es informelle, darunter fallen z. B. Walk-throughs. Im Prinzip tun sie das Gleiche, aber nicht so strukturiert wie die formelle Variante. Ein Walkthrough kann bedeuten, dass ein Autor sein Dokument oder Quellcode an ein paar Kollegen schickt mit der Bitte, den Text durchzulesen und Feedback zu geben. Es kann auch sein, dass man sich dazu direkt in einem Meetingraum trifft und sich den Text durchliest. Solche Walk-throughs entstehen meist spontan und sind deshalb nicht besonders vorbereitet. Sie sind deshalb oft leider auch lange nicht so effektiv.
281
Kapitel 5
5.5.2 Codelesen Effektiver ist hier schon eher individuelles Codelesen. Wenn ein Entwickler einen Kollegen bittet, sich einen Codeteil durchzulesen und ihm Fehler zu nennen, geschieht dies meist direkt zwischen zwei Mitarbeitern und ist deshalb weniger beliebig als bei einer Gruppe. Auch kann der Entwickler dies für sich besser organisieren, als wenn er gleich mehrere Kollegen zeitlich unter einen Hut kriegen muss. Simple Varianten dieses Codelesens kennt jeder. Ein Entwickler kommt zu einem Kollegen und bittet ihn, ihm bei einem Problem zu helfen. Oft hilft es schon, wenn man sich dann zu zweit an den Code setzt und den Quelltext durchgeht, manchmal fällt der Fehler dann sogar dem Autor selbst als Erstem auf, weil die Präsenz des Kollegen ihn dazu bringt, den Code noch mal aus einer anderen Perspektive zu betrachten. Stark ausgeprägt ist dieser Effekt auch beim »Pair Programming«, bei dem zwei Entwickler zusammen an einem Rechner arbeiten. Pair Programming hat nicht direkt etwas mit Inspektionen oder Codelesen zu tun, hier geht es eher um Fehlervermeidung als um Fehlerfinden, aber auch beim Pair Programming lesen zwei Programmierer den gleichen Code und helfen sich gegenseitig, Fehler frühzeitig (während des Tippens) zu entdecken. In Kapitel 6 gehe ich näher auf Pair Programming ein.
5.5.3 Automatisierte Inspektionen Neben diesen persönlichen Inspektionsvarianten, bei denen Entwickler direkt den Code lesen und Fehler suchen, gibt es für viele Programmiersprachen auch automatisierte Inspektionen. Diese Tools analysieren den Code z. B. auf Einhaltung eines Styleguides, sie können ungünstige Strukturen wie zirkuläre Referenzen erkennen (Klasse A hat eine Referenz auf Klasse B, die auf Klasse C und die wieder auf Klasse A), nicht verwendete Methoden oder Variablen ausmachen und vieles mehr. Der Unterschied zu Tests, die ich im folgenden Kapitel beschreibe und die teilweise auch automatisiert werden können, ist, dass automatisierte Inspektionen den Code nicht ausführen, sondern auch »nur lesen«. Leider gibt es zurzeit keine Werkzeuge im Flash-Bereich, die automatisierte Inspektionen erlauben.
5.6 Tests Das Testen einer Anwendung oder eines Teils einer Anwendung gehört wie selbstverständlich zur Entwicklung dazu. »Test« ist ein so allgemeines Wort, dass es zu diesem Thema komplette Bücher im Bereich der Softwareentwicklung gibt, die die vielen Möglichkeiten des Testens einer Anwendung unter die Lupe nehmen. Es gibt enorm viele Aspekte einer Softwareanwendung, die man testen kann. Hier nur ein paar Beispiele:
Nutzbarkeit Performance Stabilität Kompatibilität
282
Ändern und Testen
Integrität Robustheit Es gibt einige Tests, die eher die Fachlichkeit einer Anwendung testen als deren konkrete Funktionalität, darunter fällt zum Beispiel die Nutzbarkeit, aber auch die Vollständigkeit hinsichtlich der ursprünglichen Anforderungen. Diese Aspekte wollen wir hier nicht weiter betrachten. Stattdessen soll es uns eher um das technisch getriebene Testen gehen. Das Testen einer Anwendung oder eines ihrer Teile erfolgt immer durch die Ausführung dieses Teils oder des Ganzen. Im Gegensatz zur Inspektion lassen wir unsere Anwendung also immer laufen und beobachten bestimmte Aspekte während der Ausführung. Der Knackpunkt ist, dass wir unsere Anwendung immer nur unter einem von uns selbst definierten Kriterium testen können. Es gibt keinen allgemeinen Test, der prüft, ob die gesamte Anwendung fehlerfrei ist. Stattdessen können wir mit unseren Tests nur einzelne Fragen stellen und beobachten, welche Antwort wir erhalten. Diese Tatsache ist auch schon die größte Herausforderung beim Testen. Nur wenn wir die richtigen Fragen stellen, können die Antworten uns ein gutes Bild über die Qualität unserer Anwendung geben. Wir können also theoretisch zigtausend Unit-Tests für eine Anwendung schreiben, die alle mit zufriedenstellendem Ergebnis beantwortet werden, und dennoch haben wir eine vor Fehlern strotzende Anwendung. Die richtigen Tests zu definieren ist also die Kunst.
5.6.1 Testarten Tests lassen sich aus zwei Perspektiven beschreiben. Die eine ist der Rahmen, in dem der Test stattfindet. Hier kann man folgende Größen unterscheiden:
Unit-Tests: Testet isoliert vom Rest der Anwendung einzelne Klassen oder Klassenpakete. Daten, die diese Klassen benötigen, werden in Form von simulierten Testobjekten bereitgestellt und nicht von echten Schnittstellen.
Integrationstests: Testet das Zusammenspiel von zwei oder mehreren Klassen oder Klassenpaketen isoliert vom Rest der Anwendung und auch wieder isoliert von eventuellen echten Schnittstellen.
Systemtests: Testet die Endanwendung mit den echten Schnittstellen in einer der endgültigen Zielumgebung entsprechenden Testumgebung.
Abnahmetests: Testet die fertige Anwendung auf dem Zielsystem. Die zweite Perspektive ist eine inhaltliche. Hier wird beschrieben, welcher Aspekt der Anwendung oder eines Teils der Anwendung unter die Lupe genommen werden soll. Hier kommen wieder einige Aspekte vor, die ich schon weiter oben erwähnte:
Performance: Testet, ob die Anwendung schnell genug arbeitet und ob sie schnell genug auf Nutzerinteraktion oder andere Schnittstellen (z. B. Ladevorgänge) reagiert.
Robustheit: Testet, ob fehlerhafte Nutzereingaben oder fehlerhafte Daten an den Schnittstellen die Anwendung beeinträchtigen oder ob die Anwendung diese Fehler abfangen kann.
283
Kapitel 5
Kompatibilität: Testet, ob die Anwendung auf allen spezifizierten Zielumgebungen mit dem gleichen Funktionsumfang und in der gleichen Qualität läuft.
Funktion: Testet, ob die Anwendung funktional korrekt arbeitet. Schnittstellen: Testet, ob die Schnittstellen der Anwendung zur Außenwelt genauso funktionieren wie geplant.
Use-Cases: Testet inhaltlich, ob bestimmte Nutzungsszenarien der Anwendung fehlerfrei ablaufen.
Strukturelle Tests: Bei strukturellen Tests schaut man sich den Code genauer an und testet ganz zielgerichtet auf funktionale Aspekte der Anwendung mit dem Wissen, was im Code genau passiert. Strukturelle Tests sind also keine Blackbox-Tests, sondern solche, die versuchen, Besonderheiten im Inneren der Anwendung zu testen. Auf sie werde ich noch genauer eingehen. Über die Verknüpfung des zu testenden Aspekts mit dem Rahmen, innerhalb dessen getestet wird, lassen sich nun also diverse Testszenarien herleiten. Allein die Fülle dieser möglichen Tests macht deutlich, dass das Testen einer Anwendung nicht aus dem Bauch heraus durchgeführt werden sollte, nach dem Motto: »Ich schau' mal schnell drüber, ob ich was finde.« Stattdessen sollte das Testing einer Anwendung gut geplant werden, und es muss entsprechend Zeit zur Verfügung stehen.
5.6.2 Testplanung Testing muss in einem Projekt mit eingeplant werden. Im Prinzip sind sich hier auch alle einig, und dennoch passiert es immer wieder, dass dieser Abschnitt im Projekt engen Timings oder knappen Budgets zum Opfer fällt. So gehen immer wieder Flash-Projekte online, die nur extrem ungenügend getestet sind und denen man das auch ansieht. Die Faustregel besagt, dass das Testing zwischen 8% und 25% der Entwicklungszeit einnehmen sollte (McConnell 2007). Je kleiner das Projekt, umso größer wird hierbei der Anteil des Testings, weil andere Tätigkeiten wie Software-Design, Systemintegration und Architektur stärker in den Hintergrund treten. Gerade im Bereich von Flash-Online-Projekten ist das Testing besonders kritisch, weil hier oft Flash mit vielen anderen Systemen zusammenarbeitet. Je mehr Abhängigkeiten zu anderen Systemen aber bestehen, umso mehr Fehler können sich ergeben und umso genauer muss getestet werden. Dabei sollte das Testing nicht erst zum Schluss der Entwicklung starten. Je früher ein Fehler gefunden wird, umso geringer sind die Aufwände, ihn zu beheben. Wenn ein Fehler in einer Klasse bereits durch einen Test kurz nach Schreiben der Klasse gefunden wird, ist es wesentlich einfacher, ihn zu lokalisieren und zu beheben, als wenn er erst innerhalb der Gesamtanwendung gefunden und somit auch innerhalb der Gesamtanwendung lokalisiert werden muss. Die Testplanung sollte also bereits in frühen Phasen der Entwicklung greifen. Vorgehensweisen wie z. B. Testdriven Development (siehe z. B. Beck 2008) führen diese Idee auf die
284
Ändern und Testen Spitze. Beim Testdriven Development werden zuerst die Tests für ein Modul geschrieben und dann das Modul selbst. Die Idee dahinter ist einleuchtend: Wenn man eine Klasse oder ein ganzes Klassenpaket schreiben will, dann ist das Erste, was man hat, die Beschreibung dessen, was diese Klasse oder das Paket tun soll, samt der Beschreibung der Schnittstelle. Es liegt nun nahe, dass man diese Beschreibung in Form von Tests festhält, die genau die Einhaltung dieser Beschreibung überprüfen. Wenn nun die Tests hierfür stehen, kann man mit der Implementierung des eigentlichen Moduls beginnen und regelmäßig die Tests durchführen, von denen mit der Zeit immer mehr erfolgreich abschließen werden. Diese Vorgehensweise birgt mehrere Vorteile in sich:
Durch das Schreiben der Testfälle beschäftigt man sich intensiv mit der Schnittstelle des zu entwickelnden Moduls, bevor man es implementiert, und findet eventuell sogar noch Schwachstellen in ihrem Aufbau, die man noch verbessern kann.
Wenn es bisher keine schriftliche Beschreibung des Moduls und ihrer Schnittstelle in irgendeiner Form gab, dann gibt es sie jetzt zumindest in Form von konkreten Testfällen. Diese können durchaus anderen Entwicklern später helfen, die Arbeitsweise des Moduls zu verstehen.
Während der Implementierung des konkreten Moduls kann der Entwickler anhand der erfolgreich abschließenden Tests den Fortschritt seiner Arbeit gut ablesen. Dies kann bei Einschätzungen hinsichtlich der noch erforderlichen Arbeitszeit helfen und ist auch durchaus ein Motivator bei der Arbeit. Verfechter von Testdriven Development und Testdriven Design sagen, dass ein SoftwareDesign, das darauf abzielt, in sich gut isoliert testbare Module und Klassen vorzusehen, indirekt zu besonders wartbarer und modularer Software führt. Das bedeutet also, wenn man Klassen und Klassenpakete schreibt, die für sich genommen gut für sich allein testbar sind ohne Abhängigkeiten zu anderen Klassen und Klassenpaketen, dann sind mit großer Wahrscheinlichkeit Prinzipien wie Kapselung, lose Kopplung und andere automatisch gegeben. Oder im Umkehrschluss: Klassen, die viele Abhängigkeiten zu anderen Klassen aufweisen und über eine schlechte Kapselung verfügen, sind auch schlechter testbar. Wenn also eine Klasse für ihre Funktionalität viele andere Klassen benötigt, dann ist es umso schwieriger, Tests zu definieren, die Fehler nur in dieser Klasse selbst aufzeigen, denn die Fehler könnten ebenso gut durch einen Fehler in den verwendeten Klassen ausgelöst worden sein. Eine Klasse, die wiederum wenige Abhängigkeiten zu anderen aufweist, lässt sich gut testen, denn ein Fehler in einem solchen Test lässt sich dann schnell auf einen Fehler in genau dieser Klasse zurückführen.
5.6.3 Gefahrenstellen beim Schreiben von Tests Testdriven Development ist im Bereich der Entwicklertests angesiedelt, was bedeutet, dass der Entwickler des Moduls selbst die Tests dafür schreibt. Grundsätzlich ist das natürlich sinnvoll, denn der Entwickler kennt natürlich den Aufbau am besten und kann gezielt Tests dafür schreiben. Dieser Umstand birgt aber auch einige Gefahren, die zu unvollständigen Tests führen können.
285
Kapitel 5 Zunächst kennt jeder Flash-Entwickler das Phänomen der eigenen Blindheit von potenziellen Fehlerquellen. Eventuell ist man auf manche Stellen im Code besonders stolz, weil einem hier eine elegante Umsetzung gelungen ist, eventuell hat man bei anderen Stellen bereits ein ungutes Gefühl oder ist sogar genervt von diesem Teil, weil er besonders viel Zeit gekostet hat. Warum auch immer, es gibt Stellen im Code eines Entwicklers, die er selbst nicht sofort als testwürdig annimmt, die aber sehr wohl getestet werden sollten. Des Weiteren neigen viele Entwickler dazu, Positivtests zu schreiben. Ein Entwickler will durch Tests intuitiv beweisen, dass sein Code funktioniert. Er ignoriert damit den Fakt, dass Tests dies grundsätzlich nicht leisten können. (Wie schon Edsger Dijkstra 1972 feststellte: »Tests können die Anwesenheit von Fehlern zeigen, aber nicht deren Abwesenheit.«) Es ist effektiver, Negativtests zu schreiben. Das Ziel des Schreibens und Durchführens von Tests muss grundlegend sein, Fehler zu finden. Das ist aber aus menschlicher Sicht nicht unbedingt etwas, was ein Entwickler anstrebt. Dies ist ein Grund, warum z. B. Inspektionen, bei denen fremde Entwickler bzw. Prüfer involviert sind, so effektiv sind. Auch beim Schreiben von Tests kann es unter Umständen sinnvoll sein, wenn der Schreiber der Tests und der Entwickler nicht dieselbe Person sind, was aber eventuell die Kosten erhöht. Um diese menschlichen Faktoren, die zu schlechten oder unvollständigen Tests führen können, zu umgehen, ist umso mehr eine gründliche Planung der Tests hilfreich. Wenn in vergangenen Projekten zum Beispiel immer wieder bestimmte Fehler aufgetaucht sind, dann ist es nützlich, eine Checkliste dieser Fehler zu erstellen und als Vorlage für die Tests des aktuellen Projekts zu verwenden. So kann die eigene Betriebsblindheit abgemildert werden. Außerdem können strukturierte Vorgehensweisen beim Erstellen der Testfälle helfen, die Beliebigkeit der Testfälle zu verringern. Dies wird im folgenden Abschnitt näher erläutert.
5.6.4 Strukturierte Tests Damit beim Testen nicht die Ungewissheit bleibt, ob man alle wichtigen Testfälle abgedeckt hat, gibt es Verfahren, die beim Finden dieser Fälle helfen. Um diese Verfahren näher zu betrachten, sei folgender Codeausschnitt als Beispiel genommen: /** * Sortiert die Kinder eines DisplayObjectContainers * nach deren Fläche. Große Kinder kommen nach hinten, * kleine nach vorne. * @param container Enthält die zu sortierenden Kinder. */ public function sortByArea( container:DisplayObjectContainer):void { var var var var
numChildren:int = container.numChildren; sortArray:Array = new Array(); child:DisplayObject; sortObject:Object;
for (var i:int = 0; i < numChildren; i++) {
286
Ändern und Testen child = container.getChildAt(i); sortObject = { child:child, area:(child.width * child.height), originalIndex:i }; sortArray.push(sortObject); } sortArray.sortOn("area", Array.DESCENDING); for (var j:int = 0; j < numChildren; j++) { if (sortArray[j].originalIndex != j) { container.setChildIndex( sortArray[j].child, j); } } } Listing 5.33: Eine einfache Sortierfunktion für DisplayObjects unterschiedlicher Fläche
Diese Funktion ist recht simpel. Sie nimmt einen DisplayObjectContainer entgegen und sortiert seine Kinder nach deren Fläche in absteigender Reihenfolge, also große Objekte hinten, kleine vorne. Beim strukturierten Erstellen von Testfällen gibt es zunächst das Verfahren, bei dem man die Ein- und Ausgabewerte in sogenannte Äquivalenzklassen einteilt. Eine Äquivalenzklasse ist eine Menge an Werten oder Daten, die grundsätzlich in einer Funktion das Gleiche bewirken. Wenn ich also z. B. in meine Funktion sortByArea() ein Sprite mit drei Kindern gebe, passiert im Prinzip das Gleiche, als wenn ich ein Sprite mit fünf Kindern hineineingebe. In beiden Fällen werden die Kinder nach ihrer Fläche sortiert. Beide Fälle liegen also in einer Äquivalenzklasse, die wir hier Normalbereich nennen wollen, weil sie ja den gedachten Anwendungsfall widerspiegelt. Ich könnte aber auch ein Sprite in die Funktion geben, das ein oder gar kein Kind enthält. In dem Fall kann auch nichts sortiert werden. Hier passiert also etwas anderes als in unseren vorigen beiden Fällen, also handelt es sich um eine andere Äquivalenzklasse. Zu guter Letzt könnte ich noch null übergeben, was unsere dritte Äquivalenzklasse darstellt, denn null ist ja noch mal ein anderer Fall, als wenigstens ein Sprite ohne Kinder zu übergeben. Um nun strukturiert vorzugehen, empfiehlt es sich, aus jeder Äquivalenzklasse ein Beispiel im mittleren Bereich dieser Menge an möglichen Fällen zu wählen und jeweils ein Beispiel, das unmittelbar an die nächste Klasse grenzt. Da wir drei Äquivalenzklassen ausgemacht haben, wären also theoretisch sieben Fälle denkbar. Gehen wir die Möglichkeiten durch. Zunächst der Fall mit null. Hier gibt es nur einen Fall, nämlich null selbst. In der nächsten Äquivalenzklasse mit keinem oder einem Kind sind nur zwei Fälle möglich, also nehmen wir beide. In der letzten Klasse, dem Normalbereich, gibt es fast unendlich viele Möglichkeiten, hier greifen wir also den Grenzfall zur vorhergehen-
287
Kapitel 5 den Klasse heraus, nämlich ein DisplayObjectContainer mit zwei Kindern und einen Fall im mittleren Bereich, sagen wir zehn Kinder. Durch das strukturierte Vorgehen beim Analysieren des einen Parameters dieser Funktion haben wir also schon fünf Testfälle identifiziert, die getestet werden sollten. Ein weiteres Verfahren zu Identifizierung von Testfällen ist die Analyse des Kontrollflusses. Dieses Verfahren setzt voraus, dass man in den Code genau hineinschauen kann, es ist also ein Whitebox-Test. Bei der Analyse des Kontrollflusses zählt man alle Bedingungen in seinem Programm. Darunter fallen if-Anweisungen, for- und while-Schleifen und natürlich auch try...catch-Anweisungen, also alle Stellen, wo der Programmfluss aufgrund einer Bedingung verändert wird. Bei Anweisungen, die aus mehreren Bedingungen bestehen, wie z. B. a == 1 $$ b == 4, zählt man einfach eins pro Bedingung. Der Gesamtsumme addiert man schließlich noch eins hinzu und erhält somit alle Testfälle, die sich durch den Kontrollfluss ergeben. Schauen wir uns das anhand meiner kleinen Funktion an. Ich habe zwei Schleifen und in der zweiten Schleife eine if-Anweisung. Bei allen drei Stellen sind einfache Bedingungen, ich zähle also drei plus noch einen dazu, macht vier. Nach Analyse des Kontrollflusses ergeben sich also zunächst vier mögliche Testfälle. Nun müssen wir sie noch bestimmen. Als Erstes bestimmt man den Normalfall. Das ist der Fall, wenn alle Bedingungen true ergeben. Um das zu bewirken, benötige ich einen DisplayObjectContainer, der mindestens ein Kind enthält (für die for-Schleifen) und der unsortiert ist (für die if-Anweisung). Nach dem Normalfall kreieren wir nun einzeln Fälle, wo die Bedingungen jeweils false ergeben. Da haben wir die for-Schleifen. In meinem Fall können wir sie als eins betrachten, weil sie beide genau das Gleiche prüfen, nämlich initial, ob numChildren größer als 0 ist. Meine Testfälle haben sich also schon einmal um einen auf drei reduziert. Für diesen Fall benötige ich ein DisplayObjectContainer ohne Kinder. Als Letztes betrachten wir die if-Anweisung. Damit diese Bedingung false ergibt, benötige ich einen DisplayObjectContainer, in dem mindestens ein Kind schon an der richtigen Stelle im Stack steht, der Einfachheit halber könnte ich also einen schon fertig sortierten DisplayObjectContainer übergeben. Damit habe ich also konkret drei Testfälle ermittelt. Betrachtet man die Testfälle der Äquivalenzanalyse und vergleicht sie mit den Testfällen der Kontrollflussanalyse, dann sieht man, dass hier zumindest teilweise unterschiedliche Testfälle gefunden wurden. Diese Verfahren ergänzen sich also unter Umständen. Wenn die Beschreibung dieser Verfahren im geschriebenen Wort vielleicht auch ein wenig akademisch klingen mag, so sind es in der Praxis recht simple Verfahren, die auf Ebene von Methoden und Klassen schnell durchgeführt werden können und den Vorteil haben, dass hier zuverlässig die wichtigen Testfälle erkannt werden. Ein letztes Verfahren sei noch erwähnt, aber nicht mehr in der Tiefe beschrieben. Es nennt sich Datenflussanalyse. Es beschäftigt sich insbesondere mit den Variablen, also den Daten einer Klasse oder Methode. Es wird definiert, dass eine Variable drei grundsätzliche Status hat, definiert, verwendet und gelöscht. Im normalen Lebenszyklus einer Variablen würde
288
Ändern und Testen diese definiert, dann verwendet und letztendlich gelöscht. Wenn sich im Code einer Methode Stellen finden, wo dieser Zyklus nicht eingehalten wird, kann dies ein Hinweis für einen guten Testfall sein. Dabei muss natürlich auch der Kontrollfluss beachtet werden. Wenn also z. B. eine Variable in einer if-Anweisung einen Wert erhält und später diese Variable weiterverwendet wird, so muss beachtet werden, dass die Variable eventuell keinen gültigen Wert besitzt, falls die if-Anweisung nicht betreten wurde.
5.6.5 Automatisierte Unit-Tests Bisher war nur vom Testen an sich die Rede. All die bisher beschriebenen Testmethoden können natürlich händisch durchgeführt werden. Ein wichtiges Kriterium beim strukturierten Testen ist aber auch, dass die Tests nachvollziehbar sind, sprich, dass der gleiche Test mit dem gleichen Code auch das gleiche Ergebnis zur Folge hat, sonst lässt sich der Fehler nur sehr schwer lokalisieren und beheben. Aus diesem Grund ist ein automatisiertes Testen immer sinnvoll. Im Flash-Bereich gibt es inzwischen etablierte Tools, die die Erstellung von automatisierten Tests ermöglichen, wie z. B. FlexUnit für Unit-Tests. Sie lassen das Nacheinander-Abarbeiten von Einzeltests zu und können bei Bedarf sogar pro Test das gesamte Anwendungsszenario komplett zurücksetzen, um die gegenseitige Beeinflussung der Einzeltests auszuschließen. Die Automatisierung hat außerdem den Vorteil, dass sie ihrerseits auch in größere automatisierte Abläufe integriert werden kann. Zum Beispiel kann man mit Ant (http://ant. apache.org, Werkzeug zum automatisierten Kompilieren und Zusammenbauen von Anwendungen) Abläufe programmieren, die den Code erst testen und nur dann in einem Versionskontrollsystem (siehe auch Kapitel 6.7.2) einchecken, wenn die Tests erfolgreich beendet wurden. Diese Abläufe lassen sich auch noch weiterspinnen. Man kann eine Anwendung immer automatisch testen lassen, wenn man sie neu baut. Man kann eine Anwendung erst auf einen Webserver spielen lassen, wenn alle Tests erfolgreich beendet wurden. Es gibt in diesem Bereich viele Möglichkeiten der Automatisierung. Dieses Buch beschäftigt sich nicht im Detail mit Automatisierung, aber wenn Sie das Thema interessiert, empfehle ich einen Blick in Werkzeuge wie Apache Ant, FlexUnit und Apache Ivy. Ein einzelner UnitTest sollte immer einen sehr kleinen eingeschränkten Teil testen, meistens nicht mehr als ein Funktionsaufruf mit bestimmten Parametern. Je kleiner der Test, umso besser lässt sich im Fehlerfall auf die Ursache des Fehlers schließen. Wenn Sie übertrieben ausgedrückt in einem einzigen Test Ihre ganze Anwendung testen würden, wüssten Sie im Fehlerfall nicht, wo denn der Fehler aufgetreten ist. Also sollten die Tests atomar sein. In den vorangegangenen Abschnitten habe ich schon darüber gesprochen, wie Sie die einzelnen Testfälle herleiten können. Nun ist es in der Praxis eventuell nicht immer zeitlich möglich, so methodisch vorzugehen. Scheuen Sie sich in diesem Fall nicht, Tests zu schreiben. Schreiben Sie wenigstens die Tests, die Sie zeitlich schaffen. Das ist zwar nicht optimal, aber besser, als gar keine Tests zur Verfügung zu haben. Eine gute Vorgehensweise ist zudem, immer, wenn Sie einen Fehler von anderen Personen gemeldet bekommen oder selbst gefunden haben, hierfür gleich einen Test zu schreiben, der diesen Fehler reproduziert. Abgesehen davon, dass es Ihnen bei der Fehlerbeseitigung hilft, wächst so Ihre Testsammlung stetig und deckt immer mehr Teile Ihrer Anwendung ab.
289
Kapitel 5 In der Anwendungsentwicklung werden Unit-Tests auch in Flash- und gerade in Flex-Projekten schon oft eingesetzt. In Projekten hingegen, die stark grafiklastig sind, trifft man Unit-Tests hingegen seltener an. Das hängt damit zusammen, dass ein Unit-Test eine Sache funktional testet. Bei Benutzeroberflächen kommt es aber meistens eher darauf an, ob Grafiken pixelgenau platziert wurden und ob eine Animation z. B. weich genug ausläuft. Das kann man mit Unit-Tests nicht testen. Auch wenn das Problem damit nicht gelöst ist, so ist es in jedem Fall hilfreich, wenn im Code der Benutzeroberfläche so wenig wie möglich Logik steckt. Denn jede Logik kann ja wiederum per UnitTest getestet werden. Und je sauberer nun also Grafik und Logik voneinander getrennt sind, desto einfacher fällt es, entsprechende Unit-Tests zu schreiben.
5.7 Literaturangaben Fowler, Martin; Beck, Kent: Refactoring. Improving the design of existing code. 20. Auflage. Boston: Addison-Wesley, 2007. McConnell, Steve: Code complete. Dt. Ausg. der 2. Edition (Nachdr.). Unterschleißheim: Microsoft Press, 2007. Runseson, Per; Andersson, Carina; Thelin, Thomas; Andrews, Anneliese; Berling, Tomas: What Do We Know about Defect Detection Methods. In: IEEE Software, Jg. 23 (2006), H. 3, S. 82–90. Beck, Kent: Test-driven development. By example. 12. print. Boston, Mass.: Addison-Wesley, 2008
290
6
Vorgehensmodelle
Die größte Herausforderung in einem Softwareprojekt ist nicht der Code. Die größte Herausforderung ist die Frage, worum es im Projekt eigentlich geht. Herauszukristallisieren, was eigentlich das Ziel ist. Dieses Ziel in konkrete Anforderungen zu gießen und aus diesen Anforderungen eine konkrete Idee für eine Anwendung zu formulieren, ein Design zu entwickeln und die Anwendung zu bauen. Menschen mit unterschiedlichen Aufgaben zu betrauen, sodass sie sich nicht im Wege stehen, sondern effektiv das Projekt umsetzen können. Für Instrumente und Methodiken zu sorgen, die einen reibungslosen Ablauf ermöglichen und die jedem Projektmitglied zu jeder Zeit die Information zur Verfügung zu stellen, die es braucht. Projekte starten manchmal mit einem wirklich motivierten Team und enden mit einem frustrierten Team, das einfach nur noch das Projekt beenden will, um sich dann endlich wieder anderen Dingen widmen zu können. Manche Projekte scheitern sogar komplett, weil das Team die Komplexität oder die Limitierungen hinsichtlich Zeit, Budget oder Ressourcen nicht überwinden kann. Andere Projekte hingegen enden mit einem Team, das trotz allen Stresses immer noch etwas von der anfänglichen Begeisterung behalten hat und somit eher in der Lage ist, das Projekt auch fortzuführen. Was führt in dem einem Projekt zur Frustration der Projektmitglieder, was führt gar zum Scheitern eines Projekts, und welche Kriterien lassen ein Projekt gut enden? Ich gehe davon aus, dass nicht unbedingt jeder Leser dieses Buches mit der Planung und Koordination von Projekten betraut ist. Aber alle haben wir in jedem Fall gemein, dass wir in irgendeiner Art und Weise Teil von Projektteams sind. Grundlagen von Vorgehensmodellen für Projekte nachvoll-
Kapitel 6 ziehen zu können, kann helfen, sich in ein Projekt besser integrieren zu können. Man kann so die grundlegenden Spielregeln besser verstehen und die unterschiedlichen Begebenheiten und Situationen in einem Projekt besser einschätzen. Viele Projekte sind deswegen erfolgreich, weil ihre Mitglieder sich nicht nur sklavisch an ihre ihnen zugedachte Rolle halten, sondern auch links und rechts schauen. Diese Mitglieder äußern zumindest ihre Beobachtungen, wenn ihnen etwas auffällt, was negative Auswirkungen mit sich bringen könnte. Betrachten Sie dieses Kapitel also als einen Exkurs, falls Sie selber keine Projekte leiten. Es liefert Ihnen einen Einblick in die Grundprinzipien von Vorgehensmodellen für Softwareentwicklung. Ich verzichte in diesem Kapitel bewusst darauf, ein bestimmtes Vorgehensmodell herauszugreifen und im Detail zu erläutern. Eine der wichtigsten Erkenntnisse zum Thema Vorgehensmodelle soll sein, dass es für die vielen unterschiedlichen Arten von Projekten nicht das eine Vorgehensmodell geben kann. Nicht nur die Projekte sind hierfür zu unterschiedlich, sondern auch die Teams, die Agenturen, Firmen, Kunden, Nutzer, die Nationalitäten und Kulturen, sogar die regionalen Eigenheiten und auch die Gruppenkultur innerhalb eines einzelnen Teams. Sie alle haben so entscheidenden Einfluss auf ein Projekt, dass man nicht sagen kann, dass Extreme Programming oder Scrum oder Crystal Clear oder welches Modell auch immer das geeignete ist. Es ist sogar möglich, dass gar kein Modell direkt auf Ihr Projektteam passt. Vielmehr könnte es sein, dass Sie Ihr bestehendes Modell (das Sie vielleicht gar nicht konkret benennen können) einfach nur hier und da ein wenig anpassen müssen, um etwas effektiver zu sein. Vielleicht stellen Sie auch fest, dass Sie von einem der populären Vorgehensmodelle gar nicht so weit entfernt sind. Erläuterungen zu konkreten Vorgehensmodellen würden den Rahmen dieses Buches sprengen. In den Literaturempfehlungen finden Sie aber Hinweise auf weitergehende Lektüre. Beginnen wir zunächst mit dem Wesen von Softwareprojekten.
6.1 Softwareentwicklung ist ein Mannschaftssport Cockburn sagt: »Software development is a (resource-limited) cooperative game of invention and communication. The primary goal of the game is to deliver useful, working software. The secondary goal, the residue of the game, is to set up for the next game. The next game may be to alter or replace the system or to create a neighboring system.« (Cockburn 2003, S. 31) (zu Deutsch: »Softwareentwicklung ist ein Mannschaftssport, ein Spiel von Erfindung und Kommunikation. Das primäre Ziel des Spiels ist das Fertigen von nützlicher und funktionierender Software. Das sekundäre Ziel, der verbleibende Rest also, ist die Vorbereitung auf das nächste Spiel. Das kann die Veränderung oder den Austausch der Anwendung bedeuten oder die Erstellung einer anknüpfenden Anwendung.«) Softwareentwicklung mit einem Mannschaftsspiel zu vergleichen, hat den Vorteil, dass wir in diesem Zusammenhang nicht sofort an die konkreten Details eines Softwareprojekts den-
292
Vorgehensmodelle ken müssen, sondern etwas freier über das Zusammenwirken von Menschen nachdenken können. Cockburn verwendet als Beispiel das Bergsteigen. Bergsteigen macht man in der Regel nicht allein, sondern im Team. Es gibt ein konkretes Ziel, nämlich das Erreichen des Gipfels (Pessimisten würden vielleicht eher das Unten-wieder-heil-Ankommen als Ziel ansehen). Der Spaß ergibt sich unter anderem in der Herausforderung, denn Bergsteigen ist nicht einfach. Zudem ist es anstrengend, denn man muss neben seinem eigenen Gewicht meist auch noch eine Menge an Ausrüstung mitschleppen. Beim Bergsteigen gibt es einzelne Leute, die einfach gut darin sind, und andere, die vielleicht weniger Talent haben, dafür aber Arbeitstiere, die sich durchboxen. Bergsteigen will gelernt sein, man muss es trainieren, um auch gefährliche Passagen zu meistern. Um das Hauptziel, den Gipfel, zu erreichen, bedarf es einer guten Planung, denn ansonsten wird man es nicht bis zum Untergang der Sonne oder bis zum nächsten angekündigten Wetterumschwung schaffen, den Gipfel zu erreichen. Auf dem Weg zum Gipfel können aber trotz guter Planung immer wieder unvorhergesehene Dinge passieren, die nur durch Erfahrung und Improvisationsgeschick gemeistert werden können.
Abbildung 6.1: Bergsteigen, ein Teamsport
All diese Merkmale lassen sich auch auf Softwareentwicklung übertragen, auch wenn Softwareentwicklung eher mental anstrengend ist als physisch (wobei Softwareentwickler, die die Nächte durcharbeiten, irgendwann auch an ihre physischen Grenzen stoßen). Das Herausfordernde an der Softwareentwicklung ist die Übertragung von Ideen, Wünschen und Meinungen, die in den unterschiedlichsten Formen geäußert werden, in konkrete Anweisungen in einem Programm, das daraufhin etwas tut, was diesen Ideen und Wünschen möglichst nahe kommt. Wir wissen, dass es meistens eine Kluft gibt zwischen der ursprünglichen Idee und dem endgültigen Resultat. Die Herausforderung ist also, diese Kluft möglichst zu überwinden und trotzdem den Einschränkungen wie begrenzter Zeit, Budget und Ressourcen Herr zu werden.
293
Kapitel 6 Cockburn sagt weiter, dass Softwareentwicklung mit Erfindungsgeist und Kommunikation zu tun hat. Erfindungsgeist können sich die meisten in Verbindung mit Softwareentwicklern ja noch vorstellen. Programmierer müssen erfinderisch sein, wenn sie eine Lösung erarbeiten sollen, die in einem sehr kurzen Zeitrahmen zu einem sehr geringen Budget einen möglichst großen Funktionsumfang bieten soll. Aber Kommunikation? Softwareentwickler sind nicht unbedingt berühmt dafür, dass sie Kommunikationstalente wären. In vielen Bereichen herrscht noch immer das Klischee vom pickeligen, eine Hornbrille tragenden, weißhäutigen Entwickler, der mit niemandem etwas zu tun haben will, vor. Dabei sind die Anforderungen an Entwickler ganz anders. Softwareentwickler müssen sich in ein Team integrieren können, sie müssen von anderen Projektmitgliedern die Informationen beschaffen, die sie brauchen, um ihre Arbeit zu machen. Sie müssen ihre Kollegen, z. B. die Projektmanager und Konzepter, beraten, was möglich ist und was nicht, müssen frühzeitig darauf hinweisen, wenn gewünschte Änderungen einen starken Einfluss auf die bestehende Anwendung haben, und sie müssen vor allem in der Lage sein, die Anforderungen, die ihnen von Konzeptern und vielleicht sogar direkt vom Kunden vorgetragen werden, zu übersetzen in ein technisches Konstrukt, das dann auch planmäßig umgesetzt werden kann. Es gibt manche Entwickler, die Kommunikation für sich eher als Einbahnstraße ansehen. Sie interessieren sich hauptsächlich für Informationen, die sie erlangen können, um ihre eigene Aufgabe zu erledigen. Als Teammitglied müssen sie aber auch lernen, Informationen an andere zu geben, damit die ihre Aufgabe erledigen können. Das bedeutet auch, auf Dinge hinzuweisen, an die andere vielleicht gerade nicht denken. Softwareprojekte werden auch im Flash-Bereich kaum noch allein erarbeitet. Meist gibt es in einem Projekt eine Arbeitsteilung: Projektmanager, Konzepter, Designer, Texter, HTMLEntwickler, Java-Entwickler, Flash-Entwickler, um nur ein paar zu nennen. Sie alle übernehmen Teilaufgaben. Zusammen sorgen diese Projektmitglieder dafür, dass ein Projekt durchgeführt wird. Je nach Komplexität arbeiten auch mehrere Flash-Entwickler am gleichen Projekt. All diese Menschen haben persönliche Eigenheiten, Befindlichkeiten, Eitelkeiten usw. Sie haben mal einen schlechten Tag, sind am nächsten Tag voller Elan, werden krank, haben keine Lust, sind mehr oder weniger zu Überstunden bereit, sind gut organisiert oder unordentlich, reden viel oder schweigen nur, sind dominant oder unterwürfig. Dazu kommen kulturelle Unterschiede. Seien es Unterschiede zwischen Mann und Frau, zwischen unterschiedlichen Nationalitäten, die ihre eigenen Kulturen mitbringen, zwischen Religionen, politischen Grundhaltungen, zwischen Professionen wie Programmierern und Designern, zwischen Menschen mit und ohne Kundenkontakt. Außerdem bringt jeder Mensch seinen eigenen Erfahrungsschatz mit. Manche Menschen sind dadurch besonders vorsichtig, manche sind aufgeschlossen, manche besonders in sich gekehrt, manche können andere mitreißen, manche sind analytisch, manche skeptisch, andere sind Optimisten.
294
Vorgehensmodelle
Abbildung 6.2: Projektteams können sehr heterogen sein.
6.2 Stärken und Schwächen von Menschen Wären wir Menschen reine Vernunftwesen, würden Softwareprojekte wohl reibungsloser vonstatten gehen. In der Tat wird unser Handeln aber von unterschiedlichen Reizen und Antrieben bestimmt, sodass das Resultat nicht durchweg vernünftig erscheint. Viele dieser Faktoren sind von Mensch zu Mensch unterschiedlich, einige Faktoren lassen sich aber grob auf eine Mehrheit übertragen.
6.2.1 Eher Fehler verhindern als Erfolg ermöglichen Vor die Wahl gestellt, ob wir eher bereit sind, etwas zu riskieren, um einen Vorteil zu ergattern, oder ob wir eher dafür sorgen, Fehler zu vermeiden, tendieren die meisten Menschen zu Letzterem. Folgendes Beispiel dazu. Stellen Sie sich vor, Sie haben einige Erfahrung mit 3D in Flash. Sie haben in der Vergangenheit eine Bibliothek verwendet, die zwar nicht besonders komfortabel und performant ist, aber sie erfüllt ihre Aufgabe, und Sie kommen ganz gut klar damit. In einem neuen Projekt sollen Sie nun wieder einige grafische 3D-Elemente integrieren. Inzwischen haben Sie von einer neuen 3D-Engine gehört, die viel einfacher zu bedienen sein soll und wohl auch performanter ist. Ihr Projekt hat aber natürlich einen engen Zeitplan. Sie gehen jetzt zu Ihrem Projektmanager und sagen ihm, es gäbe zwei Möglichkeiten: 1. Sie könnten das Projekt wieder mit der gewohnten Bibliothek bauen, die Sie schon kennen. Es gäbe zwar das Risiko, dass die Performance eventuell nicht optimal ausfallen wird, aber Sie sind sich ansonsten sicher, dass Sie das Projekt schaffen werden. 2. Sie könnten das Projekt auch mit dieser neuen 3D-Engine bauen, von der Sie gehört haben, dass sie performanter ist und einfacher zu bedienen. Sie kennen die Bibliothek aber noch nicht im Detail und müssten sich erst einarbeiten. Eventuell würden Sie die Zeit aber wieder rein holen, weil ja die Bedienung einfacher sein soll. Jetzt überlegen Sie, für welche Variante sich der Projektmanager wohl entscheiden wird. Studien (siehe z. B. Piatelli-Palmarini 1994) zeigen, dass sich eine Mehrheit für Variante 1
295
Kapitel 6 entscheiden wird. Menschen neigen eher dazu, sich für die Variante zu entscheiden, in der sie einen möglichst stabilen Gewinn erwarten, anstatt für die Variante, in der sie viel gewinnen, aber auch viel verlieren könnten.
Abbildung 6.3: 100 Euro sicher oder 200 Euro mit Risiko?
Hierzu kann man auch noch ein umgedrehtes Beispiel formulieren. Stellen Sie sich wieder vor, Sie haben eine Anwendung gebaut. Die Anwendung funktioniert an sich, aber manchmal stürzt sie einfach ab. Sie wissen nicht genau warum, aber Sie wissen ungefähr, in welchem speziellen Teil der Anwendung der Fehler auftritt. Sie gehen zu Ihrem Projektmanager und schlagen ihm wieder zwei Möglichkeiten vor: 1. Sie schalten den speziellen Teil der Anwendung ab. Es ist nicht ideal, aber den Teil braucht man vielleicht nicht unbedingt, und zumindest würde dann die Anwendung an sich nicht mehr abstürzen. 2. Sie investieren noch mal Zeit, um den Code sehr detailliert zu studieren und dem Problem auf die Spur zu kommen. Sie hoffen, dass Sie das Problem finden und beheben können, es kann aber auch sein, dass sich das Problem nicht lösen lässt. Genaueres können Sie jetzt noch nicht sagen. Überlegen Sie wieder, für welche Option sich der Projektmanager entscheiden wird. Die Studien sagen hier, dass sich die meisten Projektmanager für Variante 2 entscheiden werden, weil hier die Möglichkeit besteht, nichts zu verlieren. Sie riskieren hier eher die Möglichkeit, Zeit für nichts und wieder nichts zu verlieren, als sich für die Möglichkeit zu entscheiden, in der Sie garantiert etwas verlieren. Bedenken Sie, dass in Variante 2 theoretisch mehr verloren werden kann als in Variante 1, denn wenn Variante 2 fehlschlägt, ist viel Zeit verloren worden, und der fehlerhafte Teil muss zudem doch noch abgeschaltet werden.
296
Vorgehensmodelle
6.2.2 Eher selber bauen als wiederverwenden Gerade erfahrene Entwickler neigen dazu, eher für ein bestimmtes Problem selber eine Lösung zu bauen, anstatt nach bestehenden Lösungen zu suchen, die man wiederverwenden könnte. Im Flash-Bereich mag noch ansatzweise die Ausrede gelten, dass noch vor nicht allzu langer Zeit nicht besonders viele Bibliotheken und Frameworks auf dem Markt existierten, die man verwenden konnte. Das hat sich aber inzwischen deutlich geändert. Viele Bibliotheken aus allen denkbaren Bereichen sind inzwischen verfügbar und können Eigenentwicklungen unter Umständen überflüssig machen. Das Problem ist nur, solche Bibliotheken muss man finden, und das kostet Zeit. Hinzu kommt die Tendenz, dass Entwickler eher dann ein hohes Ansehen erreichen, wenn sie selber tolle Bibliotheken oder Anwendungen schreiben. Ein Entwickler, der eine Anwendung unter Verwendung von diversen fertigen Bibliotheken entwickelt, ist nicht unbedingt ein Vorbild. Obwohl er im Prinzip viel kosteneffektiver gearbeitet hat. Aber Entwickler, die nur vorgefertigte Komponenten zusammenbauen, sind uns suspekt. Diese Einstellung sollten wir ändern. Gerade in der Softwareentwicklung ist Wiederverwendung ein starkes und effektives Instrument. Da Softwareentwicklung eigentlich keinen Materialverbrauch kennt, bietet sich Wiederverwendung hier ganz besonders an. Entwickler, die intelligent bestehende Bibliotheken und Komponenten mit eigenem Code verbinden, sollten stärker gewürdigt werden, denn sie arbeiten meist besonders effektiv.
6.2.3 Eher stur als anpassungsfähig Fast jeder Entwickler ist in seinem Leben schon irgendwann mit Zeiterfassungsanwendungen in Berührung gekommen (und wenn es auch nur eine Excel-Tabelle ist). Und alle hassen es. Es ist eindeutig effektiver, die Zeiterfassung jeden Tag zu machen, denn nur dann kann man sich überhaupt noch einigermaßen daran erinnern, was man genau gemacht hat. Und doch sind manche von uns nicht in der Lage, ihre Gewohnheit zu ändern, und schreiben ihre Stunden nur einmal im Monat auf, dann meistens grob gebündelt. Und dabei stoßen sie dann Flüche aller Art aus, was für eine unsinnige Arbeit die Zeiterfassung doch ist. Gewohnheiten, die wir einmal angenommen haben, werden wir nur schwer wieder los. Selbst wenn unsere Gewohnheiten augenscheinlich negativ sind, ändern wir sie trotzdem nicht. Insofern kann ein Vorgehensmodell, das gute Prinzipien für ein Projekt vorschlägt, allein daran scheitern, dass die Projektmitglieder ihre alten Gewohnheiten nicht ablegen können. Nachdem wir uns einige der Schwächen von uns Menschen angeschaut haben, wollen wir uns auch noch ein paar Stärken ansehen.
6.2.4 Wir finden uns zurecht Wir Menschen haben die Fähigkeit, uns schnell einen Überblick über Dinge zu verschaffen. Wir tun dies manchmal sogar lieber, als alles vorsortiert zu bekommen. Die wenigsten Menschen sortieren z. B. die Bücher in ihrem Regal komplett durch. Eventuell sind sie grob nach
297
Kapitel 6 Themenbereichen geordnet, aber nicht noch weiter. Viele Menschen verwenden in ihrem Mailprogramm nur das allgemeine Posteingangsfach und unternehmen keine weitere Ordneraufteilung, denn sie finden sich auch so zurecht. Viele Entwickler schauen sich lieber den konkreten Code mit seinen Kommentaren an, als eine lange Dokumentation zu lesen. Wir können daran erkennen, dass wir Menschen Ordnung, Sortierung und größtmögliche Detailliertheit nicht unbedingt immer benötigen. Wir sind sehr gut in der Lage, mit eher oberflächlichen Informationen einen Themenkomplex zu erfassen. Wir sollten uns diese Gabe zunutze machen. Code-Dokumentation ist wichtig, aber wenn sie ausartet, sodass sie keiner mehr liest, verfehlt sie ihr Ziel. Stattdessen sollte sie dann lieber einen Einstieg und Überblick enthalten und für Details auf eine API-Dokumentation oder den Code selbst verweisen. Das soll natürlich nicht heißen, dass keine allgemeinen Dokumentationen geschrieben werden sollen. Eine Bibliothek, die nur eine API-Dokumentation hat, ist meist sehr schwer verständlich. Das gleiche Prinzip gilt für Instrumente im Projektmanagement. Für manche kleine Projekte sind z. B. Systeme, die Fehler und Aufgaben dokumentieren und Entwicklern zuweisen (sogenannte Bugtracking-Systeme), einfach zu viel. Eventuell ist es einfacher, einem Entwickler über den Tisch zuzurufen, dass an einer Stelle ein Fehler aufgetreten ist. In größeren Projekten wiederum kann das anders sein. Die Erkenntnis ist, dass wir Menschen durchaus mit einem recht groben Detailgrad bezüglich einer Problemstellung zurechtkommen und uns dann selbst einarbeiten.
6.2.5 Wir lernen Das ist offensichtlich. Menschen lernen dazu. Unerfahrene Entwickler werden über die Zeit zu erfahrenen Entwicklern. Im Laufe eines Projekts lernen wir die Details des Projekts besser verstehen und können Anforderungen immer besser einschätzen. Deswegen ist es so wichtig, dass wir uns der Aufgabenstellung am Anfang eines Projekts erst auf einem sehr groben Level nähern. Am Anfang haben wir noch wenig Erfahrung. Jetzt schon das ganze Projekt durchzuplanen mit allen Details muss zwangsläufig dazu führen, dass wir unseren Plan ständig erneuern und ändern müssen. Deswegen sollte ein Projekt eher so organisiert werden, dass ein Lernprozess unterstützt wird. Wenn wir ein Projekt in kleine Schritte aufteilen und uns innerhalb eines Schrittes nur mit den Details für diesen Schritt beschäftigen, dann wird unsere Fähigkeit zu lernen gefördert. Denn im nächsten Schritt wissen wir schon wieder etwas mehr über das Projekt und können dann genauere Einschätzungen zum nächsten Schritt liefern. Deswegen kann es hilfreich sein, auch fachliche und technische Konzepte in Schritten anzufertigen anstatt komplett am Anfang.
298
Vorgehensmodelle
6.2.6 Wir wollen Anerkennung und tun was dafür Viele Menschen engagieren sich in den unterschiedlichsten Bereichen, obwohl sie dafür gar keine Entlohnung erhalten. Seien es ehrenamtliche Tätigkeiten in lokalen Gemeinden oder auch die Mitarbeit in einem Open-Source-Projekt. Etwas spornt Menschen an, sich für solche Projekte einzusetzen. Einer der Motivatoren ist der Wunsch nach Anerkennung. Anerkennung kann ein sehr starker Motivator sein, manchmal stärker als ein Gehaltsscheck. Wenn wir merken, dass ein Projekt oder auch nur eine einzelne Aufgabe Anerkennung und eine gute Reputation mit sich bringen kann, sind viele bereit, hart dafür zu arbeiten. Viele haben das sicherlich schon selbst an sich bemerkt. Wenn ein Projekt ansteht, das eine Herausforderung bietet und von vielen als wichtig erachtet wird, dann sind wir besonders motiviert. Manche Projekte, die ein vermeintlich schwaches Vorgehen aufweisen, werden trotzdem erfolgreich beendet, weil die einzelnen Projektmitglieder hoch motiviert waren und deswegen alle an einem Strang gezogen haben. In solchen Projekten achtet jedes Mitglied nicht nur auf sich und seine Aufgabe, sondern auch darauf, ob die Kollegen Hilfe gebrauchen können, auch wenn dies gar nicht direkt mit ihrer eigenen Aufgabe zu tun hat. Diese Stärken und Schwächen haben einen direkten Einfluss darauf, wie wir uns im Projekt organisieren sollten. Um nur ein paar Beispiele zu nennen:
Erfahrene Entwickler, die eher in sich gekehrt sind, sollten nicht eine Führungsrolle übernehmen, denn die unerfahrenen Entwickler werden eher wenig von ihnen lernen können.
Menschen, die sehr harmoniebedürftig sind, haben eventuell Schwierigkeiten, Entscheidungen zu treffen, die nicht von allen Mitgliedern befürwortet werden.
Manche Projektteams arbeiten sehr selbst organisiert, sodass der Projektmanager wenige Vorgaben machen muss, andere erwarten vom Projektmanager klare Vorgaben.
Menschen machen Fehler. Projekte sollten unter Berücksichtigung von Fehlern geplant werden.
Manche Methodiken lassen sich mit Disziplin durchsetzen. Im Vorgehensmodell Extreme Programming arbeiten Entwickler paarweise und erinnern sich gegenseitig daran, bestimmte Vorgehensweisen wie z. B. das konsistente Schreiben von Tests für ihren Code einzuhalten.
Skizzen helfen uns, komplexe Sachverhalte zu verstehen. Skizzen, die nicht formell, sondern frei gezeichnet sind (z. B. per Hand), laden Kollegen zum Ändern oder Hinzufügen ein. So entsteht ein Dialog. Formelle Zeichnungen werden seltener angefasst.
Wir lernen gut, wenn wir ein Beispiel als Ausgangspunkt haben. Mit einer Technologie zu arbeiten, die uns neu ist, wird einfacher, wenn wir z. B. eine Beispielanwendung als Basis nehmen und sie verändern. Im gleichen Zusammenhang kann es hilfreich sein, eine Open-Source-Bibliothek, die nicht exakt das tut, was wir brauchen, als Ausgangspunkt für die eigene Arbeit zu verwenden.
299
Kapitel 6
Entwickler brauchen einen Mix aus Zeitfenstern, in denen sie Ruhe haben und nicht gestört werden, und solchen, in denen sie sich miteinander und mit anderen Projektmitgliedern austauschen können.
6.3 Zusammenarbeit im Team Wenn eine Person ein Projekt komplett allein bearbeitet, muss sie zwar alles selbst machen, sie muss sich aber auch nur mit dem Kunden direkt abstimmen und mit niemandem sonst. Ein Fehler, den manche unerfahrene Projektmanager machen, ist zu denken, dass das Addieren von Personen die Arbeit entsprechend aufteilt und diese somit in entsprechend geringerer Zeit zu schaffen ist. Es wird schnell klar, dass das nicht so ist, wenn man berücksichtigt, dass sich nun diese Personen untereinander abstimmen müssen. Die simple Formel hierfür lautet theoretisch (n · (n–1)):2 = Anzahl Verbindungen (theoretisch, weil natürlich nicht jedes Projektmitglied mit jedem spricht). Demnach gäbe es bei zwei Projektmitgliedern eine Kommunikationsverbindung und bei fünf Mitgliedern schon zehn. Da heutige Projekte nicht mit einer Person allein zu bewältigen sind, muss also zum einen die Gesamtaufgabe in entsprechende Teilaufgaben zerteilt werden. Zum anderen müssen sich die Projektmitglieder untereinander austauschen. Ergebnisse, die ein Mitglied erzielt, müssen an andere Mitglieder weitergereicht werden. Je schneller nun Informationen von einem Mitglied zum anderen gelangen, umso effektiver kann das Projekt laufen. Zusätzlich entscheidend ist auch, dass die Informationen vollständig übertragen werden. Wenn ein Projektmitglied einem anderen Informationen zukommen lässt, muss es dies in einer Art und Weise tun, dass möglichst unterwegs keine wichtigen Teile verloren gehen. In diesem Zusammenhang tragen zwei Dinge entscheidend zur Effektivität des Projektteams bei:
Projektteams sollten so nah wie möglich beieinander sitzen. Ein Team, das z. B. aus ca. acht Personen besteht, sollte möglichst in einem Raum zusammensitzen. Dadurch können die Mitglieder ohne große Formalien direkt Anforderungen und Fragen miteinander besprechen, ohne nennenswerte Hürden überwinden zu müssen. Wenn ein Team nicht in einem Raum zusammensitzen kann, sollten sie zumindest in benachbarten Räumen sitzen. Ist auch das nicht möglich, können unter Umständen technische Hilfsmittel wie Chat-Programme und Webcams am Arbeitsplatz helfen.
Interne Zwischenergebnisse, also Dinge, die intern produziert und nicht vom Kunden verlangt werden, sollen so weit wie möglich reduziert werden. Dazu gehören Dokumente, Diagramme, Prototypen etc. Das bedeutet nicht, dass gar keine Zwischenergebnisse produziert werden sollen. Aber es sollen nur dann welche erzeugt werden, wenn sie für das Voranschreiten des Projekts relevant sind. Wenn ein Kunde beispielsweise offiziell kein technisches Konzept verlangt, reicht es unter Umständen, intern in einem Wiki die wichtigsten technischen Gedanken zu notieren. Gerade so detailliert, dass das Team effektiv arbeiten kann. Diese beiden Punkte sind wichtig, deswegen schauen wir sie uns noch etwas genauer an.
300
Vorgehensmodelle
6.3.1 Lokalisierung von Projektteams Wenn in einem Projekt ein Entwickler eine Frage zum Konzept hat und der Konzepter im gleichen Raum sitzt, dann kann die Frage direkt geklärt werden. Sitzt der Konzepter im benachbarten Raum, kann der Entwickler ihn eventuell schon nicht mehr sehen, müsste aufstehen und nachschauen. Das klingt erst einmal albern, aber es führt schon dazu, dass kleine Fragen nicht mehr gestellt werden, weil sich die Person nicht erst in den anderen Raum bemühen will. Sitzt der Konzepter in einem anderen Stockwerk, kriegt der Entwickler gar nicht mehr mit, ob der Konzepter überhaupt grundsätzlich da ist. Er muss anrufen, aber wenn der Konzepter nicht ans Telefon geht, weiß der Entwickler immer noch nicht, ob der Konzepter an dem Tag grundsätzlich anwesend und nur momentan nicht am Platz ist. Sitzt der Konzepter an einem anderen Standort, erhöhen sich die Probleme noch mal. Hier kann eventuell eine andere Arbeitszeitregelung gelten, andere Feiertage usw. Eventuell haben sich Entwickler und Konzepter noch nie persönlich kennengelernt, entsprechend ist das Verhältnis zwischen den beiden anders als bei Kollegen, die im gleichen Büro sitzen. Fast alle Vorgehensmodelle empfehlen, dass sich die Mitglieder eines Projektteams so nah beieinander wie möglich zusammenfinden sollen, um Kommunikationswege zu verkürzen.
Abbildung 6.4: Beieinander sitzende Teams kommunizieren effektiver.
Andererseits helfen manche technischen Hilfsmittel, die Entfernung zumindest teilweise zu überbrücken. Das einfachste Mittel sind Chat-Programme. Die meisten von ihnen zeigen an, welcher Teilnehmer gerade verbunden ist. Viele sogar, ob der Teilnehmer gerade aktiv am Rechner arbeitet oder nicht. Das kann den Vorteil eines im gleichen Büro versammelten Teams nicht ersetzen, hilft aber trotzdem, die Kommunikation zu verbessern.
301
Kapitel 6 Es muss aber auch gesagt werden, dass ein zu großes Team, das im gleichen Büro versammelt ist, die Effektivität ab einem gewissen Punkt beeinträchtigen kann. Wenn im Raum Personen sitzen, die viel und laut mit anderen Personen reden oder telefonieren, kann dies die anderen natürlich stören. Außerdem entsteht ab einer gewissen Personenzahl ein gewisses Grundrauschen, das eventuell störende Ausmaße annehmen kann. Und wenn es nur die klackernden Tastaturen sind. Manche Personen wollen auch einfach nicht in einem großen Büro sitzen. Sie brauchen ihre Privatsphäre. Das darf man nicht ignorieren, denn extrem unzufriedene Mitarbeiter können in einem Büro eventuell alle anderen Personen mitziehen.
6.3.2 Interne Zwischenergebnisse Die schnellste und direkteste Art der Kommunikation ist die verbale Kommunikation. Ein Entwickler spricht mit einer Designerin über ein konkretes Layout. Schon wenn die Kommunikation nur indirekt über eine Photoshop-Datei erfolgt, kommt es meist zu Missverständnissen. Ein Layout aber nur über verbalen Austausch zu besprechen, ist auch nicht vorstellbar. Meistens setzen sich Entwickler und Designerin zusammen vor die PhotoshopDatei und besprechen diese. In diesem Fall ist dann die Photoshop-Datei ein Zwischenergebnis, das gut und notwendig ist. Wenn zwei Entwickler sich Gedanken über die Struktur einer Anwendung machen, reden sie miteinander. Während ihrer Diskussion sprechen sie eventuell über Themen, die sich nicht einfach nur verbal ausdrücken lassen. Einer skizziert vielleicht eine Struktur auf Papier. Ist das Gespräch beendet und beide Entwickler setzen nun die Struktur um, kann es sein, dass die Skizze nicht mehr benötigt wird. Wenn dem so ist, muss die Skizze nicht aufgehoben werden. Wenn die beiden Entwickler hingegen das Gefühl haben, dass diese Skizze auch für andere Entwickler von Bedeutung ist, sollten sie sich überlegen, wie sie das Diagramm für die Kollegen festhalten. Das kann bedeuten, dass die Skizze an einer Pinnwand befestigt wird oder dass ein Diagramm noch mal am Computer erzeugt und an zentraler Stelle im Netzwerk abgelegt wird. Teams, die zusammen bestimmte Meilensteine erreichen müssen, erstellen eventuell einen großen Plan, den sie an die Wand heften, damit alle Mitglieder jederzeit sehen können, wo sie gerade stehen. Einzelne Gruppen im Projekt können diesen Plan dann um eigene Statusmeldungen ergänzen, die wiederum für andere Gruppen wichtig sind. Jede Art von verbaler und nichtverbaler Kommunikation, die hilft, das Projekt voranzutreiben, ist gut. Alle weiteren Maßnahmen sind oft nicht effektiv. Ausgenommen sind natürlich solche Dinge, die der Kunde konkret anfordert. Die Werkzeuge, die Entwicklern, Designern und Konzeptern heute zur Verfügung stehen, versuchen, die Zusammenarbeit zwischen den verschiedenen Tätigkeiten immer weiter zu verbessern. Das große Ziel hierbei ist, dass die Programme, mit denen die jeweiligen Personen arbeiten, die Dateiformate der jeweils anderen Programme kennen und einbinden können. Ein Designer soll also z. B. ein Layout erstellen, das ein Flash-Programmierer direkt in sein Flash-Projekt einbinden kann. Der Vorteil ist, dass hierbei Zwischenergebnisse entstehen, die tatsächlich weiterverwendet werden können. Damit wird die Effektivität der Arbeit jedes Einzelnen im Team erhöht. Dieses Ziel ist noch nicht ganz erreicht, an vielen
302
Vorgehensmodelle Stellen hakt es heute noch, aber die Richtung ist erkennbar. Heutige Werkzeuge müssen neben ihrer primären Funktion auch immer stärker die Zusammenarbeit zwischen Projektmitgliedern unterstützen.
6.4 Teambildung und Zusammenhalt Wir haben bisher über Kommunikation und Zusammenarbeit gesprochen. Damit dies in einem Team gut funktioniert, muss das Team überhaupt zusammenarbeiten wollen. Viele Personen haben neben ihrer Rolle innerhalb eines Projektteams noch weitere individuelle Rollen und Ziele. Einer hat vielleicht gerade seinen Gehaltswunsch nicht durchbekommen, eine andere ist gerade zum ersten Mal Art Director in einem Projekt und will unbedingt ihre Vorgesetzten beeindrucken. Ein Dritter ist mit der Idee für das Projekt grundsätzlich nicht einverstanden. Projektmitglieder haben oft unterschiedliche Vorstellungen davon, wie sich ein Projekt entwickeln sollte und was die wichtigsten Ziele sind. Damit aber ein Projekt effektiv bearbeitet werden kann, sollten alle am gleichen Strang ziehen. Dazu gehört, dass die Mitglieder überhaupt wissen, was die primären Ziele des Projekts sind und wie die Projektphilosophie ist. Manche Projekte sind einer Firma so wichtig, dass sie zur Not auch bereit sind, Mehrkosten zu tragen. In anderen Projekten soll eine neue Technologie ausprobiert werden. Eventuell soll auch eine besonders hohe Qualität erreicht werden, oder der Abgabetermin darf unter keinen Umständen verfehlt werden. Das Projektmanagement stellt diese grundsätzlichen Ziele auf. Dann wissen alle Projektmitglieder, woran sie sind. In einer guten Teamatmosphäre helfen sich die Teammitglieder gegenseitig und sind offen für Kritik. Wenn ein Konzept nicht exakt definiert, wie ein Detail umzusetzen ist, können die Entwickler Vorschläge machen, welche Lösungen denkbar wären und welche innerhalb des Budgets umsetzbar wären. Ein Flash-Entwickler, der aufgefordert wird, eine Website in Flash umzusetzen, die genauso gut in HTML gebaut werden könnte, sollte dies sagen. Selbst wenn die Website dann doch in Flash gebaut wird, so ist den Verantwortlichen wenigstens bewusst, dass es auch anders möglich gewesen wäre. Oft bedarf es eines gewissen Fingerspitzengefühls, wie man in einer Situation reagiert. Wenn ein Entwicklerkollege den eigenen Entwurf kritisiert, weil er einen bestimmten Nachteil mit sich bringt, ist nicht jeder in der Lage, dies einfach so zu akzeptieren und den Entwurf zu überdenken. Hier sollte sowohl derjenige, der die Kritik erhält, als auch derjenige, der Kritik übt, auf den jeweils anderen eingehen und ihn mit Respekt behandeln. Im Vordergrund soll stehen, wie das Projekt vorangebracht werden kann. Kritik wird also nicht an Personen, sondern an bestimmten Handlungen oder Resultaten geübt. Damit in einem Projektteam ein Zusammengehörigkeitsgefühl entsteht, reichen oft schon erste kleine Erfolge, die man zusammen feiern kann. Ein Prototyp beispielsweise, der schon grob zeigt, wie die Anwendung einmal sein wird. Wenn ein Team zusammen erfolgreich so einen Meilenstein meistert, stellt sich meistens ein gewisses gegenseitiges Vertrauen in die Fähigkeiten jedes Einzelnen ein, falls sich die Mitglieder nicht eh schon kennen. In Teams,
303
Kapitel 6 die an verschiedenen Standorten verstreut sind, kann es sehr hilfreich sein, wenn diese sich zu Beginn einmal persönlich kennenlernen. So hat man ein Gesicht vor Augen, wenn man später mit dem Kollegen telefoniert.
6.5 Arten von Vorgehensmodellen Cockburn sagt, dass die meisten Softwareprojekte nach heuristischen Vorgehensweisen ablaufen. Das bedeutet, dass es für Softwareentwicklung (noch) keine exakten Prozessbeschreibungen gibt, wie man eine Softwareapplikation baut. Stattdessen gehen wir nach Erfahrungswerten vor. Eine Vorgehensweise, die sich bewährt hat, wird weiterverwendet. Auch die agilen Vorgehensweisen basieren auf Erfahrungswerten und formulieren ihre Prinzipien ganz bewusst mit ausreichend Spielraum. Es gibt durchaus Berufszweige, in denen das anders ist. In der Medizin z. B. gibt es für viele Situationen festgelegte Standardvorgehensweisen. Erste Hilfe ist hier ein gutes Beispiel. Können Sie sich noch an Ihren letzten Erste-Hilfe-Kurs erinnern? Selbst wenn nicht, Sie erinnern sich bestimmt, dass es eine recht klare Vorgabe gab, was man wann machen soll (z. B. als Erstes Hilfe rufen). So weit sind wir in der Softwareentwicklung noch nicht. Da es hier keine klar festgelegten Prozedere gibt, beruhen die meisten Projekte auf den Erfahrungen ihrer Mitglieder. Zwar wurde in der Vergangenheit immer wieder versucht, Vorgehensmodelle aus anderen Industrien in die Softwareentwicklung zu übertragen, viele Firmen der Softwarebranche erkennen aber inzwischen für sich, dass das eventuell nicht der beste Weg ist. Unter der allgemeinen Bezeichnung »Agile Softwareentwicklung« gruppieren sich deshalb immer mehr alternative Ideen zur Durchführung von Projekten, die auf die besonderen Anforderungen im Softwarebereich ausgerichtet sein wollen. Sie wollen flexibel auf Änderungswünsche reagieren, sie versuchen sicherzustellen, dass eine Anwendung so schnell wie möglich auf den Markt kommt und dass das Projektteam so organisiert wird, dass alle Mitglieder so effektiv wie möglich zusammen arbeiten können. »A methodology is the conventions that your group agrees to.« (Cockburn 2003, S. 115) Übersetzt ist hiermit gemeint, dass ein Vorgehensmodell eine Sammlung aus Konventionen ist, auf die sich ein Projektteam verständigt hat. Mit den Konventionen sind alle Dinge gemeint, die für ein Projekt relevant sind:
Die unterschiedlichen Disziplinen im Projekt, wie Designer, Konzepter, Texter, Software Architekt, Entwickler, Consultant, Projektmanager etc.
Kommunikationswege Werkzeuge Standards, wie z. B. Coding Guidelines, Corporate-Design-Richtlinien, interne Projektmanagementvorschriften etc.
Meilensteine
304
Vorgehensmodelle
Technologien Projektergebnisse, intern wie extern Nicht alle Vorgehensmodelle gehen auf diese Dinge gleich stark ein. Je nachdem, in welchem Bereich der Softwareentwicklung sie definiert wurden, setzen sie einen Fokus auf bestimmte Aspekte und lassen wiederum andere eventuell völlig weg. Zum Beispiel kommen die vielen Vorgehensmodelle wie Extreme Programming und Scrum aus Softwarezweigen, die sich mit klassischen Geschäftsanwendungen beschäftigen. Dort ist auch die korrekte Abbildung von Geschäftsprozessen tendenziell wichtiger als z. B. eine besonders interaktive, Spaß machende Benutzeroberfläche. Erst in jüngerer Vergangenheit, mit dem Aufkommen der Begriffe Experience Design und User Experience, beschäftigt man sich mehr und mehr damit, den Prozess der Gestaltung und Erarbeitung von besonders interaktiven und wohldurchdachten Benutzeroberflächen in moderne Vorgehensmodelle einzuplanen. Viele Agenturen sind hier gegenüber klassischen Softwarefirmen etwas im Vorteil, denn sie waren im Prinzip schon immer in der Pflicht, in kürzester Zeit sehr individuelle, emotional aufgeladene Benutzeroberflächen für ihre Kunden zu gestalten und umzusetzen. Im Agenturumfeld liegt der Fokus meist auf der Benutzeroberfläche. Die Vorgehensmodelle, die eine Agentur anwendet, unterscheiden sich deswegen meist merklich zu denen klassischer Softwarefirmen. Zum Beispiel arbeiten Agenturen selten in Iterationen, weil die Projekte oft zu kurz sind, um das Prinzip einzuführen (mehr zu Iterationen folgt in kommenden Abschnitten). In klassischen Softwarefirmen hingegen, in denen Projekte eventuell mehrere Monate bis Jahre dauern, ergibt sich der Nutzen von kurzen, in sich geschlossenen Produktionsphasen viel eher. Es ist also Vorsicht geboten bei der Wahl eines Vorgehensmodells. Es gibt kein Modell, das allgemein auf Softwareentwicklung passt. Alle Modelle haben einen bestimmten Fokus. Manche sind für große Teams zugeschnitten, andere für kleine. Manche beziehen bestimmte Disziplinen nicht mit ein. Manche sind recht schwergewichtig, haben also viele Vorgaben, andere bestehen nur aus einigen wenigen Grundsätzen. Manche verlangen ein hohes Maß an Disziplin, andere setzen auf Toleranz und Eigenverantwortung von Mitarbeitern. Wenn Sie sich dafür entscheiden, ein Modell näher zu studieren, sollten Sie dies immer unter Berücksichtigung Ihres eigenen Teams und seine Bedürfnisse sowie der konkreten Rahmenbedingungen Ihrer Projekte tun. Manchmal kann auch die Organisationsstruktur Ihres Kunden den Einsatz eines bestimmten Vorgehensmodells unmöglich machen. Für Flash- und Flex-Entwickler stellt sich die Frage: Was sind die besonderen Anforderungen, die Flash als Technologie bzw. der Bereich der stark interaktiven und individuellen Benutzeroberflächen an Vorgehensmodelle stellt? Folgende Punkte sind hier zu nennen:
Hier arbeiten Entwickler und Designer sehr eng zusammen. Benutzeroberflächen sind schwer über automatisierte Werkzeuge testbar (gerade wenn es um die Einhaltung von Layoutrastern, Farbwerten und gut aussehenden Animationen geht).
Benutzeroberflächen haben die meisten Abhängigkeiten zu anderen Teilen der Anwendung und des Projekts (zum Design, zum Konzept, zur Applikationslogik, zur Art der Daten, zur Zielgruppe, zum Kunden, zum Endgerät etc.).
305
Kapitel 6
Jeder hat eine Meinung zu Benutzeroberflächen. Benutzeroberflächen stehen repräsentativ für die Anwendung. Für die Nutzer sind sie die Anwendung.
Benutzeroberflächen sind der Teil einer Anwendung, der während eines Projekts am häufigsten verändert wird. Designs und Layouts sind für lange Zeit instabil.
Flash als Technologie ist immer noch recht jung im Vergleich zu anderen Plattformen wie Java oder C, C++. Die Entwicklergemeinde im Flash-/Flex-Bereich ist noch dabei, sich zu definieren. Der Umschwung von einem Animations- und Designwerkzeug hin zu einer Entwicklungsplattform ist noch nicht allzu lange her. Es gibt also viele Flasher, die sich eher als Designer sehen, andere definieren sich als Schnittstelle zwischen Design und Technik, wiederum andere begreifen sich als Entwickler. Daraus folgen einige Besonderheiten bei der Organisation von Flash-Projekten im Zusammenhang von agilen Vorgehensmodellen:
Flash-Entwickler und Designer sollten nah beieinander sitzen und arbeiten. Für bestimmte Aspekte der Benutzeroberfläche ist sogar denkbar, dass ein Flash-Entwickler und ein Designer Pair Programming betreiben, wenn es zum Beispiel an die Feinabstimmung von Layouts und Animationen geht.
Weil die Umsetzung von interaktiven Designs in Flash einiges an Aufwand bedeutet, kann es sinnvoll sein, dass zum Testen eines Designs Entwickler und Designer erst an einem Dummy arbeiten. Erst wenn dieser erfolgreich getestet wurde (z. B. durch Usability-Tests), wird die Umsetzung in der wirklichen Anwendung vorgenommen. Die Gestaltung und Erprobung solcher Dummies sollte nicht in eigenen Iterationen erfolgen, sondern gemeinsam mit der Umsetzung eines erfolgreich getesteten Designs in der wirklichen Anwendung. Nur so ergibt sich am Ende der Iteration wieder funktionierende Software. Die zeitliche Länge der Iterationen muss entsprechend gewählt werden.
Designer werden während des gesamten technischen Umsetzungsprozesses eingebunden, um frühzeitig Fehler in der Umsetzung bezüglich Layouts zu erkennen.
Der Kunde und die Endnutzer (bzw. Vertreter der Endnutzer) werden so stark wie möglich während des Designs und der Umsetzung eingebunden, um zu verhindern, dass fertige Benutzeroberflächen noch mal komplett geändert werden müssen.
Die Flash-Entwicklung wird – insofern die Größe des Projekts dies erlaubt – in technische und grafiklastige Entwicklung aufgeteilt. Flash-Entwickler mit besonderer Affinität und Erfahrung zur grafischen Umsetzung entwickeln die direkten Benutzeroberflächen, andere Flash-Entwickler übernehmen die Grundstruktur und die Applikationslogik sowie das Domänenmodell (siehe Kapitel 3 für eine Erläuterung des Domänenmodells). In manchen Fällen ist es auch möglich, dass Designer Teile der Benutzeroberfläche in Flash vorbereiten oder auch Animationsteile vorbereiten. Die Weiterentwicklung der Werkzeuge für Designer und Flash-Entwickler in der Zukunft wird zeigen, wie sehr sich dieser Arbeitsablauf verzahnen lässt.
306
Vorgehensmodelle
6.5.1 Sequenzielle und nebenläufige Modelle Normalerweise machen wir gerne »eins nach dem anderen«. Sich einen Plan auszudenken, bei dem wir nacheinander Tätigkeiten ausführen, um etwas zu erreichen, ist einfach und praktisch. Man kann recht einfach planen, was wann passiert, und der Vorteil ist auch, dass Projektmitglieder, die ihre Aufgabe erfüllt haben, schon wieder an einem anderen Projekt teilnehmen können. Natürlich können solche sequenziell geplanten Projekte auch sehr komplex und umfangreich sein, dennoch bleibt das Grundprinzip einfach und verständlich. Ein klassisches Beispiel für sequenzielle Planung ist das Wasserfallmodell, deswegen so genannt, weil die Ergebnisse der ersten Tätigkeit kaskadenförmig zur nächsten Tätigkeit führen, siehe Abbildung 2.1.
Anforderungen Konzeption Design Umsetzung
Abbildung 6.5: Das Wasserfallmodell, ein sequenzieller Ablauf
Wäre es möglich, sich für ein Projekt genügend Zeit zu reservieren und alles bis ins kleinste Detail durchzuplanen, wäre diese Vorgehensweise eventuell auch ausreichend. Softwareprojekte haben aber nicht so viel Zeit. Vielmehr sollen sie in manchmal unglaublich kurzer Zeit realisiert werden, was ein Abarbeiten nacheinander unmöglich macht. Wenn aber das Projekt nicht bis ins Detail durchgeplant und konzipiert werden kann, dann folgt daraus zwangsläufig, dass Fragen offenbleiben. Dass Kunden noch keine exakte Vorstellung haben, was sie wollen oder brauchen, und dass das Projektteam die Bedürfnisse des Kunden noch nicht hundertprozentig verstanden hat. Das wiederum führt dazu, dass diese Fragen erst während des Projekts beantwortet werden, vielleicht auch erst sehr spät im Projekt. Dann aber haben sequenzielle Abläufe den Nachteil, dass z. B. in der Designphase eigentlich nichts mehr am Konzept geändert werden soll und schon gar nicht an den Anforderungen. Das ganze Planungsgerüst kommt nun durcheinander. In einem sequenziellen Ablauf ist es nicht vorgesehen, dass Erkenntnisse aus einer späteren Phase noch mal Auswirkungen auf eine frühere Phase haben können. Diesem Problem kann man durch Überlappung der Phasen begegnen. Indem die Phasen teilweise nebenläufig angeordnet werden, gibt es Überlappungen, während der ein Austausch zwischen den jeweiligen Disziplinen stattfinden kann, siehe Abbildung 6.6. Nun stellt sich dabei natürlich die Frage, wie weit sich die einzelnen Phasen überlappen sollen. Cockburn sagt dazu, dass eine folgende Phase beginnen kann, wenn ein Vorläufer so stabil ist, dass Änderungen die folgende Phase nicht mehr wesentlich behindern würden.
307
Kapitel 6 Hierbei spielen drei Faktoren eine Rolle: Stabilität bzw. Vollständigkeit der Arbeit des Vorgängers, Kapazitäten der Arbeitsgruppe des Nachfolgers und Effektivität der Kommunikation zwischen beiden Disziplinen.
Anforderungen Konzeption Design Umsetzung
Abbildung 6.6: Nebenläufiges Modell
Wenn also z. B. die Anforderungen an eine Anwendung grundsätzlich geklärt sind bis auf wenige Details und dem Kunden im Prinzip klar ist, was die Ziele sind und was er sich wünscht, dann kann man sagen, dass die Anforderungen relativ stabil bzw. vollständig sind. Wenn nun der Nachfolger, in diesem Fall die Konzepter, genügend Kapazitäten hat, könnten sie schon loslegen mit ihrer Arbeit und den Rahmen der Anwendung erarbeiten. Wenn genügend Konzepter verfügbar sind, könnten sich zur Not auch noch Anforderungen ändern, die Konzepter könnten die notwendigen Änderungen trotzdem schaffen. Wenn es wiederum nur einen Konzepter gibt, wird man darauf achten, dass die Anforderungen besonders klar und vollständig sind, denn man kann sich nun nicht leisten, dass der eine Konzepter ständig nacharbeiten muss. Und schließlich, wenn zwischen der Gruppe der Leute, die die Anforderungen erarbeiten, und den Konzeptern eine schlechte Kommunikation herrscht, weil die beiden Gruppen beispielsweise an unterschiedlichen Standorten arbeiten, dann sollten die Anforderungen eventuell auch vollständiger sein, damit nicht zu viel Zeit in die Kommunikation investiert werden muss. Wenn also eine der Prioritäten die Einhaltung eines engen Zeitplans ist, dann sollte man versuchen, Tätigkeiten möglichst nebenläufig zu organisieren.
6.5.2 Das eigene Vorgehensmodell Ich hatte es schon weiter vorn gesagt, eventuell arbeiten Sie schon mit einem Vorgehensmodell, das grundsätzlich gut für Ihre Projekte geeignet ist. Eventuell fehlen nur hier und da ein paar kleine Anpassungen, um es noch weiter zu verbessern. Um herauszufinden, welche Anpassungen das sein könnten, kann es hilfreich sein, Projektnachbesprechungen zu machen in Form von Interviews. In solchen Gesprächen mit Projektmitgliedern können wertvolle Hinweise zutage treten, die das nächste Projekt vielleicht noch etwas glatter ablaufen lassen. Dabei muss man allerdings ein wenig detektivisches Gespür mitbringen. Nicht immer geben Projektmitglieder gerne zu, dass sie bestimmte Vorgehensweisen nicht wirklich befolgt haben. Wichtig ist dabei, dass sich die beteiligten Inter-
308
Vorgehensmodelle viewpartner bewusst sind, dass es im Interview um die Verbesserung des Vorgehens geht und dass das auch bedeuten kann, dass man einen unnötigen Schritt weglassen kann, wenn er das Projektteam nicht weiterbringt. Cockburn definiert sieben Prinzipien, die zu guten Vorgehensmodellen führen sollen. Diese sind:
Kommunikation von Angesicht zu Angesicht ist billig und schnell, um Informationen auszutauschen.
Übertrieben komplexe Vorgehensmodelle sind teuer. Große Teams brauchen komplexere Vorgehensmodelle. Kritischere Projekte brauchen striktere Regeln. Mehr internes Feedback und mehr interne Kommunikation reduzieren den Bedarf an internen Zwischenresultaten (Dokumente, Dateien, Prototypen etc.).
Disziplin, Fähigkeiten und Verständnis schlagen Prozesse, Formalien und Dokumentation.
Die Effizienz sollte in Disziplinen mit geringer Kapazität eines Projekts verstärkt und in den anderen Bereichen zugunsten dieser Disziplinen verringert werden. Einige der Prinzipien sollten grundsätzlich klar sein, ein paar möchte ich noch näher erläutern.
Übertrieben komplexe Vorgehensmodelle sind teuer Ein Vorgehensmodell sollte sich auch an der Projekt- und Teamgröße orientieren. Größere Projekte benötigen meistens eine stärkere Arbeitsteilung und damit auch eine komplexere Organisation des Teams. Somit fällt auch die Organisation eines solchen Projekts komplexer aus, und ein Vorgehensmodell muss dem Rechnung tragen. Kleine Projekte hingegen lassen sich viel einfacher organisieren. Ein Team, das aus unter zehn Leuten besteht, braucht nicht zwingend detaillierte Zeitpläne, die jeden Schritt auf dem Kalender vorgeben. Wenn dieses Team in einem Raum zusammensitzen kann, können alle organisatorischen Fragen sofort und direkt geklärt werden. Hier reicht dann vielleicht einfach eine grobe Übersicht mit Meilensteinen. Ein detaillierter Zeitplan würde Zeit und somit Geld kosten. Auch Größe und Umfang von Konzeptdokumenten können je nach Größe des Projekts und des Teams variieren. In dem Projekt, wo die Teammitglieder zusammen im Raum sitzen, kann ein Konzept relativ grob ausfallen, denn im Zweifel sitzt der Konzepter oder Designer ein paar Stühle weiter und kann befragt werden. Das Gleiche gilt für das technische Konzept. Wenn der Hauptentwickler eine grobe Linie vorgibt, wie das Projekt technisch umgesetzt werden soll, also die Architektur z.B., dann können die Details während der Arbeit im Gespräch und mit kleinen Hilfsskizzen geklärt werden. Das spart Zeit und Geld. In größeren Projekten, wo eventuell einzelne Programmiererteams getrennt voneinander arbeiten, ist das natürlich nicht ausreichend.
309
Kapitel 6
Abbildung 6.7: Zu viele Regeln hemmen den Arbeitsfluss.
Kritischere Projekte brauchen striktere Regeln Eine Anwendung in einem Werbekampagnenprojekt ist nicht so geschäftskritisch wie eine Anwendung zur Abfrage von Bankkontodaten. Eine Anwendung, die Inhalte multimedial präsentiert, ist nicht so geschäftskritisch wie eine Anwendung, in der sich Kunden registrieren und persönliche Daten verwalten können. Je kritischer eine Anwendung bewertet wird, umso kritischer sollten auch die Regeln zur Organisation des Projekts ausfallen. Denn ein Fehler in einer geschäftskritischen Anwendung hat mehr Schaden zur Folge. Deswegen muss in einer geschäftskritischen Anwendung mehr Energie in Fehlerfreiheit investiert werden. Auch die projektinternen Regeln müssen strenger sein, damit möglichst keine Fehler unentdeckt bleiben. Dies kann bedeuten, dass Formalien eingeführt werden, die zwar Aufwand bedeuten, die aber gerechtfertig sind, um die benötigte Qualität zu gewährleisten.
Mehr internes Feedback und mehr interne Kommunikation reduzieren den Bedarf an internen Zwischenresultaten Das Thema hatte ich zuvor schon angesprochen. Je mehr ein Projektteam in die Lage versetzt wird, direkt und unkompliziert miteinander zu sprechen, desto weniger werden interne Zwischenresultate wie z. B. dicke Konzeptdokumente benötigt. Wie gesagt, das bedeutet nicht, dass sie gar nicht benötigt werden, aber sie können in ihrem Umfang und ihrer Anzahl deutlich reduziert werden. Ein Flash-Entwickler z.B., der einen Designer direkt einen Stuhl weiter neben sich weiß, kann ihm direkt zwischendurch einen Screen, eine umgesetzte Komponente oder eine Animation zeigen und Feedback bekommen, noch bevor er die Anwendung komplett umgesetzt hat. Schon wenn Entwickler und Designer in unterschiedlichen Räumen sitzen, wird der Entwickler das nicht mehr so oft tun. Er wird sich sagen: »Ich baue diesen Teil erst mal fertig und gehe dann rüber.« Er produziert hier also wieder ein größeres Zwischenergebnis, das unter Umständen wieder geändert werden muss.
310
Vorgehensmodelle Die gilt im Prinzip auch in der Zusammenarbeit mit dem Kunden. Ein Kunde, der starke Präsenz zeigt und immer ansprechbar ist und auf Fragen schnell reagiert, wird vom Ergebnis wohl weniger überrascht sein als einer, der immer nur zu einigen Meilensteinen auf das Projekt schaut.
Disziplin, Fähigkeiten und Verständnis schlagen Prozesse, Formalien und Dokumentation Vielleicht ist es Ihnen auch schon so gegangen. Sie lesen ein recht detailliertes Fachkonzept. Später sprechen Sie mit dem Konzepter wegen ein paar Details, und er sagt Ihnen: »Ja, das steht da noch so drin, aber wir machen das jetzt anders ...« Im Laufe des Projekts wurden also manche Details geändert. Wie viel ist dieses Fachkonzept jetzt noch wert? Selbst wenn 90% des Inhalts immer noch stimmen, wie kann jemand, der es liest, herausfinden, welches die anderen 10% sind? Das Gleiche gilt für technische Konzepte, Layouts in Photoshop-Dateien, Projektpläne etc. Ein Projektteam kann sein Wissen nicht komplett in Dokumente verpacken. Viele Absprachen, Informationen »zwischen den Zeilen«, Abschätzungen etc. stehen in keinem Dokument. Deswegen sind Personen in laufenden Projekten nicht einfach beliebig austauschbar, denn dabei geht viel an wertvollem Wissen verloren. Effektiver ist es, das Wissen und die Fähigkeiten der Projektmitglieder ganz aktiv zu nutzen. Anstatt große Regelwerke aufzustellen, die den Ablauf eines Projekts definieren, ist es hilfreicher, eine Atmosphäre zu schaffen, indem jeden Projektmitglied seine Rolle und Verantwortung aufgezeigt werden und sich die Projektmitglieder selbst verpflichtet fühlen, das Projekt voranzutreiben. In so einer Arbeitsatmosphäre stützen sich die einzelnen Mitglieder gegenseitig, und es bedarf keiner großen Formalien, um den Ablauf festzulegen. Wenn den Projektmitgliedern die Wichtigkeit ihrer Aufgabe und ihrer Konsequenzen bewusst ist, entwickeln sie eher eine gewisse Selbstdisziplin, als wenn ihnen von oben Kommandos gegeben werden, deren Zusammenhang sie nicht direkt erkennen können.
Die Effizienz sollte in Disziplinen mit geringer Kapazität eines Projekts verstärkt und in den anderen Bereichen zugunsten dieser Disziplinen verringert werden. In jedem Projekt gibt es Bereiche, die nicht so üppig mit erfahrenen Mitarbeitern ausgestattet sind wie andere Bereiche. Nehmen wir an, in einem Projekt arbeiten nur zwei recht unerfahrene Flash-Entwickler, weil einfach nicht mehr und erfahrenere Entwickler verfügbar sind. Diese beiden Entwickler brauchen grundsätzlich länger, um eine Aufgabe zu erledigen, und sie benötigen auch mehr Anleitung. Die Projektmitglieder, die den beiden FlashEntwicklern zuarbeiten, sind üblicherweise Konzepter und Designer. Wir erinnern uns, idealerweise sollten sich ja die einzelnen Phasen eines Projekts so stark wie möglich überlappen, um das Projekt in möglichst kurzer Zeit fertigzustellen. Wenn nun also die Flash-Entwickler bereits frühzeitig anfangen, während das Design auch noch mitten in der Arbeit ist, dann steht zu befürchten, dass die beiden Flash-Entwickler oft
311
Kapitel 6 Änderungen bezüglich des Designs einbauen müssen. Da es aber nur zwei unerfahrene Entwickler sind, können sie das zeitlich gar nicht leisten. In diesem Fall muss man also von dem eigentlichen Prinzip der Überlappung abweichen und die beiden Flash-Entwickler erst mit der Arbeit anfangen lassen, wenn die Designs und das Konzept schon ziemlich stabil und final sind, sodass sich nur noch kleinere Änderungen ergeben werden. Das bedeutet natürlich, dass die Designer und Konzepter auch wieder mehr Dokumente, also detailliertere Fachkonzepte und klarere Designdateien liefern müssen, damit dann die Flash-Entwickler möglichst ohne große Unterbrechung arbeiten können. In einem Prozedere, wo die Phasen stark überlappen, wäre das nicht unbedingt notwendig, weil sich hier Konzepter, Designer und Entwickler direkter über die meisten Themen mündlich oder anhand von Skizzen oder anderen Hilfsmitteln absprechen können. Liegen die Phasen weiter auseinander, wird der Bedarf an detaillierteren Dokumenten wieder größer. Was in diesem Fall aber nicht anders geht, weil eben zu wenige erfahrene Flash-Entwickler vorhanden sind. Es muss also natürlich einmal das Ziel sein, den Flash-Entwicklern zu helfen, ihre Arbeit schneller erledigen zu können oder, wenn möglich, weitere Entwickler heranzuholen. Andererseits können die anderen Disziplinen helfen, indem sie selbst ihre eigene Arbeit noch genauer und detaillierter aufbereiten, damit dann die Entwickler mit möglichst wenigen Änderungen mehr rechnen müssen und sehr genau sehen können, was sie machen müssen. Generell kann man sagen, ist es eher anzuraten, die Effektivität eines Teams zu erhöhen als einfach nur mehr Leute ins Projekt zu holen. Stattdessen sollte man nach Stolpersteinen suchen, die den Mitarbeitern die Arbeit erschweren. Eventuell kann auch eine Schulung in einem bestimmten Thema helfen. Wenn z. B. eine Flash-Anwendung mit vielen 3D-Elementen gebaut werden soll und das Team hat noch wenig Erfahrung damit, könnte es sich lohnen, einen Experten für ein paar Tage zu buchen, der das Team auf die richtige Spur bringt, bis sie den Rest selber zu Ende entwickeln können. Wenn Sie sich im Detail für Vorgehensmodelle interessieren, sei Ihnen das exzellente Buch von Cockburn: »Agile Software Development« (Cockburn 2003) empfohlen.
6.6 Agile Entwicklung Im Prinzip haben die vorangegangenen Abschnitte schon einige Grundlagen für agile Softwareentwicklung beschrieben. Hier will ich nun auf die offiziellen Prinzipien der agilen Softwareentwicklung eingehen und wie sie sich in Projekten niederschlagen können. Der Begriff der agilen Softwareentwicklung wurde offiziell im Februar 2001 geprägt, als sich 17 Persönlichkeiten aus der klassischen Softwareentwicklung in Utah, USA, trafen. Zu diesem Zeitpunkt hatten sich viele von Ihnen schon ausgiebig mit Vorgehensmodellen im Allgemeinen und konkreten eigens entwickelten Modellen im Speziellen auseinandergesetzt. Darunter z. B. Adaptive Software Development, präsentiert von Jim Highsmith, Extreme Programming unter anderem von Kent Beck und Scrum, präsentiert von Ken Schwaber.
312
Vorgehensmodelle Sie versuchten, aus den verschiedenen Vorgehensmodellen und den Erfahrungen der einzelnen Personen Gemeinsamkeiten herauszukristallisieren. Heraus kamen vier Grundprinzipien, die das sogenannte Agile Manifest bilden, sowie bisher zwölf unterstützende Bemerkungen, die die Grundprinzipien etwas detaillierter beschreiben. Schauen wir uns zunächst das grundsätzliche Manifest an (übersetzt ins Deutsche): Durch die eigene praktische Anwendung und die Unterstützung anderer entdecken wir bessere Vorgehensweisen zur Entwicklung von Software. Während dieser Arbeit haben wir folgende Prinzipien zu schätzen gelernt: Individuen und Interaktionen gehen über Prozesse und Instrumentarien. Funktionierende Software geht über umfangreiche Dokumentation. Die Einbindung des Kunden geht über Vertragsverhandlungen. Das Aufgreifen von Änderungen geht über das Verfolgen eines Plans. Das bedeutet, obwohl die rechts stehenden Punkte einen Wert haben, erachten wir die Punkte links als wertvoller. (Original auf: http://www.agilemanifesto.org/) Das erste Prinzip deutet an, dass es in Softwareprojekten im Zweifel klüger ist, sich auf die Fähigkeiten und Erfahrungen guter Mitarbeiter zu verlassen als auf Formalien und Vorschriften. Außerdem werden die Zusammenarbeit und Kommunikation zwischen Mitarbeitern höher eingestuft als umfangreiche Dokumentationen oder überbordende Planungen. Ein Punkt, den man in diesem Zusammenhang nicht außer Acht lassen sollte, ist die Tatsache, dass dieses Prinzip nur funktioniert, wenn die Mitarbeiter sich motiviert am Projekt beteiligen und erfahren sind. Ein Team von unerfahrenen Mitarbeitern benötigt eher formale Vorgehensweisen, an denen entlang es arbeiten kann, als ein Team von erfahrenen Mitarbeitern. Das zweite Prinzip sagt aus, dass die Anwendung selbst, also ihr Code, die Benutzeroberfläche und die Funktionalität, die beste Dokumentation des zuletzt vereinbarten Standes ist. In den seltensten Fällen sind fachliche wie technische Konzepte vollständig und aktuell. Andererseits muss auch gesagt werden, dass Code, gerade wenn er schlecht kommentiert ist, keine besonders gut lesbare Dokumentation darstellt. Dokumentationen sind also nicht grundsätzlich schlecht. Aber sie sollten dazu verwendet werden, das zu beschreiben, was aus dem Code nicht herauszulesen ist, die Intention des Projekts z. B. oder die Grundidee der Architektur, Diskussionen darüber, warum die Anwendung so oder so strukturiert wurde. Das dritte Prinzip besagt, dass der Kunde als gleichwertiges Mitglied des Projektteams angesehen werden sollte, der weder über noch unter dem Projekt steht. Vielleicht ist er nicht jeden Tag anwesend, aber er soll aktiv eingebunden werden, damit er den Verlauf einschätzen und frühzeitig seine Ideen und Wünsche äußern kann und damit das Projektteam so früh wie möglich Feedback erhält und die Richtung erkennen kann, in die sich das Projekt bewegen soll.
313
Kapitel 6
Abbildung 6.8: Kunde und Auftragnehmer auf Augenhöhe
Das vierte Prinzip letztendlich besagt, dass der Nutzen der zu entwickelnden Anwendung im Vordergrund steht und dass alle Änderungen, die das Projekt besser machen und die von allen Seiten akzeptiert sind, zu begrüßen sind. Dabei geht es nicht ohne Planung, aber wenn die Planung verhindert, dass Änderungen in das Projekt einfließen können, dann ist die Planung hinderlich. Die Planung sollte also entsprechend flexibel genug aufgestellt werden und eventuelle Änderungen berücksichtigen und nicht ausschließen. In vielen Projekten werden Änderungen als Störfaktor angesehen, obwohl allen Beteiligten zu Beginn des Projekts intuitiv klar ist, dass es welche geben wird, weil man noch zu wenig über das Projekt weiß. In agilen Vorgehensmodellen sind Änderungen ein integraler Bestandteil der Planung und kein Störfaktor.
6.6.1 Anwendung von agilen Vorgehensmodellen Die meisten Prinzipien von agilen Vorgehensmodellen haben wir schon diskutiert. Hier bringe ich sie jetzt noch mal zusammen, unter Verwendung von acht Punkten: 1. Zusammenarbeit im Team 2. Kurze, regelmäßige Ergebnisse 3. Einschätzung und Priorisierung von Features 4. Reflexion der eigenen Arbeit 5. Einbeziehung des Kunden 6. Testgetriebene Entwicklung 7. Paarweise Programmierung 8. Refactoring
314
Vorgehensmodelle Die unterschiedlichen konkreten agilen Vorgehensmodelle gehen mit diesen Prinzipien unterschiedlich um und setzen auch unterschiedliche Schwerpunkte. Generell aber sind diese acht Punkte in jedem agilen Vorgehensmodell anzutreffen.
Zusammenarbeit im Team Dieses Thema haben wir schon ausführlich behandelt. Ein Team soll möglichst nah beieinander sitzen. Idealerweise sitzen bis zu acht Personen in einem Raum. Das Team wird nicht nach Disziplinen aufgeteilt und separat in Räume aufgeteilt, denn gerade der Austausch zwischen den Disziplinen ist wichtig. Wenn das Team aus mehr als acht Personen besteht, muss man von der Idealsituation abweichen und angrenzende Räume verwenden. Der Punkt ist aber, dass jegliche, auch kleine, Barrieren für die Kommunikation so weit als möglich abgebaut werden sollen. Da Mitarbeiter immer auch mal ihre Ruhe brauchen, sollte es Rückzugsmöglichkeiten geben. Einzelne Räume z.B., in die sich ein oder zwei Mitarbeiter zurückziehen können, wenn sie sich besonders konzentrieren müssen. Dazu kommt, dass die Projektmitglieder diese Nähe nutzen und auch hauptsächlich von Angesicht zu Angesicht miteinander kommunizieren und, wo es geht, auf umfangreiche schriftliche Kommunikation verzichten. Dicke Konzeptdokumente und Anforderungskataloge werden als möglichst vermieden, und Dokumente sollen nur so umfangreich sein wie notwendig, um das Projekt voranzubringen. Dabei muss berücksichtigt werden, dass nach Fertigstellung eines Projekts dieses eventuell noch lange gewartet und erweitert werden muss. Wenn die Wartung oder Erweiterung ein anderes Team durchführen soll, muss die Dokumentation unter Umständen umfangreicher ausfallen, falls ein fließender Übergang zwischen dem einen und dem anderen Team nicht möglich ist. Schließlich nutzen die Mitarbeiter die direkte Kommunikation, um immer über die Vorgänge im Projekt informiert zu sein. Sie sind so in der Lage, auch über ihre Disziplin hinaus Hinweise und Vorschläge zu machen, die das Projekt vorantreiben. Wenn z. B. ein Projektmanager mit einem Kunden über ein Feature spricht und ein Entwickler sitzt in Reichweite, kann er bei Unklarheiten direkt einspringen und helfen, Fragen zu beantworten. So werden drohende Missverständnisse vermieden.
Kurze, regelmäßige Ergebnisse Die meisten agilen Vorgehensmodelle unterteilen ein Gesamtprojekt in kleinere Zwischenschritte, sogenannte Iterationen. Diese Zwischenschritte bauen meistens aufeinander auf, sind also inkrementell. Ein Zwischenschritt wird auf ungefähr einen Monat angesetzt. (Hier sind sich die unterschiedlichen Modelle uneinig, Extreme Programming fordert zwei Wochen, andere Modelle erlauben auch Iterationen von bis zu drei Monaten Länge.) Diese Iterationen sind innerhalb des Projekts grundsätzlich immer gleich lang. Sie orientieren sich nicht unbedingt an den zu erstellenden Anwendungsteilen. Vielmehr überlegt man, welche Teile der Anwendung in einer Iteration konzipiert, gestaltet, umgesetzt, getestet und in die Anwendung integriert werden können. Der Fokus liegt also darauf, dass eine Iteration eine endliche, festgelegte Länge haben soll. Grund hierfür ist, dass vor bzw. zwi-
315
Kapitel 6 schen jeder Iteration das Projektteam inklusive Kunden von Neuem entscheidet, wie das Projekt weitergehen soll. Es wird geprüft, was man bisher geschafft hat, ob die Vorgehensweise passt oder es etwas zu verbessern gibt. Es wird außerdem auf die noch ausstehenden Anwendungsteile geschaut, und es wird priorisiert, welche Teile in der nächsten Iteration angepackt werden sollen. Die Aufwände hierfür werden geschätzt, und wenn die Teile in die nächste Iteration passen, werden sie für die nächste Iteration eingeplant. Alles, was nicht in die Iteration passt, bleibt auf der Liste der offenen Punkte und wird dann in einer der nächsten Iterationen erledigt. Wichtig dabei ist, dass in diesem Planungsprozess alle Projektmitglieder und der Kunde anwesend sind, sodass es auch hier zu keinen Missverständnissen oder falschen Einschätzungen kommen kann.
Einschätzung und Priorisierung von Features Damit die Iterationen gut funktionieren, ist die Einschätzung und Priorisierung der einzelnen Aufgaben sehr wichtig. Wirklich gut funktioniert das nur in Projekten, die nicht mit einem Festpreis arbeiten. Die einzelnen Aufgaben werden vor jeder neuen Iteration bewertet und die Aufwände geschätzt. Das ist deswegen vorteilhaft, weil die Teammitglieder nur die Dinge einschätzen muss, die unmittelbar vor ihnen liegen und die sie mit ihrem bisherigen Wissen über das Projekt am besten einschätzen können. In der ersten Iteration können das Aufgaben sein, wie z. B. die Erstellung eines Grundgerüsts einer Flash-Anwendung oder der Abgleich von ersten Designs an das Corporate Design des Kunden. Dies sind Aufgaben, die das Team am Anfang schon recht gut einschätzen kann, denn sie gehen noch unbedingt zu stark in die Tiefe. In späteren Iterationen kann das Team dann das gelernte Wissen aus den vorigen Iterationen nutzen, um komplexere Themen einzuschätzen. Die Priorisierung der einzelnen Aufgaben wird zusammen mit dem Kunden gemacht. Hiermit wird sichergestellt, dass sich das Team nicht auf Aufgaben konzentriert, die dem Kunden im Zweifel gar nicht so wichtig sind. Auch kann das Team wiederum sagen, dass bestimmte Aufgaben zuerst angegangen werden müssen, weil diese eine Basis für andere Aufgaben bilden. Wenn diese Priorisierung zusammen mit dem Kunden besprochen wird, erhält der Kunde somit ein gutes Bild über die Komplexität des Projekts und lernt selbst einzuschätzen, wo die Schwerpunkte des Projekts liegen.
Reflexion der eigenen Arbeit Zwischen jeder Iteration blickt das Team zusammen mit dem Kunden auf den bisherigen Projektverlauf. Hat die Zusammenarbeit zwischen den einzelnen Disziplinen gut funktioniert? Hat man alle gesteckten Ziele erreicht? Haben sich neue Fragen oder Probleme ergeben? Fühlen sich alle Projektmitglieder gut informiert, funktioniert die Kommunikation? Hier geht es in erster Linie darum zu sehen, ob das gewählte Vorgehensmodell funktioniert. Eventuell klagen einzelne Mitglieder, dass sie bei bestimmten Themen den Überblick verloren haben, dann ist eventuell mehr schriftliche Dokumentation dieser Themen notwendig oder auch einfach ein Meeting mit den entsprechenden Experten, die das Thema noch näher erläutern.
316
Vorgehensmodelle Auch Engpässe in einer Disziplin können behandelt werden. Wenn man beispielsweise merkt, dass die Designer mit ihrer Arbeit nicht mehr hinterherkommen, dann muss darauf reagiert werden. Entweder müssen weitere Designer hinzugeholt werden, oder man muss versuchen, den Designern ihre Arbeit zu erleichtern, indem sie zum Beispiel mit ihrer Arbeit erst anfangen, wenn die zugrunde liegenden Konzepte sehr stabil sind und Änderungen unwahrscheinlich.
Einbeziehung des Kunden Die vorangegangenen Abschnitte haben es schon gezeigt. Der Kunde soll so aktiv wie möglich einbezogen werden. Er soll gut informiert sein und soll so oft wie möglich und so schnell wie möglich Anmerkungen geben können, Fragen beantworten usw. In den agilen Vorgehensmodellen ist vorgesehen, dass jedes Teammitglied Kontakt zum Kunden haben kann und soll. Die Kommunikation soll also nicht nur über den Projektmanager erfolgen, denn diese Kommunikation ist meist nicht direkt genug und beinhaltet zudem das Problem, dass ein Projektmanager die Themen eventuell nicht so stellt und so im Detail erörtert, wie das ein Designer oder Entwickler tun würde. Folgenden Dialog haben Sie vielleicht in irgendeiner Form auch schon erlebt (nicht zwingend mit einem Projektmanager, das ist hier nicht entscheidend): Entwickler: Und, welche Variante will er nun? Projektmanager: B, die fand er am besten. E: Aber dass er dann das Feature X nicht mehr haben kann, hat ihn nicht gestört? P: Ach so, darüber haben wir jetzt gar nicht so direkt gesprochen. Wäre hier der Entwickler im Gespräch dabei gewesen, wäre es wahrscheinlich nicht zu dem Missverständnis gekommen. Manchmal sind für Projektmitglieder Entscheidungen, die in einem Gespräch getroffen wurden, in dem sie nicht dabei waren, auch schwer zu begreifen. Gerade wenn es Entscheidungen sind, die sie selbst als negativ empfinden. Wenn die Projektmitglieder oft in die Gespräche mit dem Kunden einbezogen werden, können sie eher ein Verständnis für die Beweggründe des Kunden oder anderer Projektmitglieder entwickeln, als wenn die Kommunikation immer über Dritte läuft.
Testgetriebene Entwicklung Für viele agile Modelle ist testgetriebene Entwicklung ein Muss. Dabei schreibt man zuerst die Tests für den Teil der Anwendung, den man umsetzen will. Da man diese Teile noch nicht entwickelt hat, schlagen also zunächst alle Tests fehl. Nun implementiert man die Anwendungsteile, und nach und nach enden alle Tests erfolgreich. Die umgesetzten Teile werden der eigentlichen Anwendung erst hinzugefügt, wenn alle Tests erfolgreich beendet wurden. In agilen Modellen wird dieses Vorgehen als besonders wichtig angesehen. Je nach Art von Flash-Projekten kann diese Maxime aber schwierig umzusetzen sein. Flash-Anwendungen sind oft sehr grafiklastig, und ein recht hoher Anteil des Codes dreht sich um die Erzeu-
317
Kapitel 6 gung der grafischen Oberfläche. Diesen Code kann man über automatisierte Testwerkzeuge nur schwer testen, denn diese Werkzeuge können nicht prüfen, ob Komponenten pixelgenau an der richtigen Stelle sitzen, ob Farbwerte stimmen oder ob eine Animation den Vorstellungen der Designer entspricht. Zumindest kann man auch in Flash-Projekten anstreben, so viel Logik wie möglich aus der Programmierung der grafischen Oberfläche herauszuziehen und zumindest diese Logik dann über automatisierte Testwerkzeuge zu testen.
Paarweise Programmierung Bei der paarweisen Programmierung sitzen zwei Entwickler vor einem Rechner und entwickeln einen Teil einer Anwendung. Das muss nicht immer bedeuten, dass sie direkt entwickeln, es kann auch heißen, dass sie zuerst zusammen eine Struktur erarbeiten. Einer der beiden übernimmt dabei das Keyboard und die Maus, der andere sitzt nebenbei. Durch diese Aufteilung ergibt sich meist automatisch, dass der Entwickler am Keyboard gedanklich konkreter am Code denkt und der Beisitzende etwas abstrakter in Strukturen. So können sich die beiden Kollegen gut ergänzen. Der Beisitzende hat einen besseren Überblick, kann schneller übergreifende Aspekte oder Probleme erkennen und sieht oft auch einfachere Fehler im Code, die der andere Entwickler nicht sieht. Der am Keyboard sitzende Entwickler wiederum kann sich nun stärker auf konkreten Code und Algorithmen konzentrieren. Paarweise Programmierung ist zunächst teurer und aufwendiger, als wenn Entwickler für sich arbeiten. Es kann immerhin nur einer aktiv Code schreiben. Andererseits ist durch paarweise Programmierung erarbeiteter Code oft mit weniger Fehlern durchzogen und auch besser strukturiert.
Abbildung 6.9: Paarweise Programmierung bringt einige Vorteile mit sich.
Zudem tendieren Entwickler in einem paarweisen Szenario eher dazu, Standards und Guidelines einzuhalten, weil sie sich gegenseitig dabei kontrollieren. Entwickler sind auch eher bemüht, ordentlichen Code zu produzieren, wenn ein anderer Entwickler direkt dabei ist. Daneben ergibt sich auch der positive Effekt, dass Entwickler bei diesem Vorgehen Erfah-
318
Vorgehensmodelle rungen und Wissen austauschen können. Und nicht zuletzt verteilt sich das Wissen über bestimmte Bereiche der Anwendung auf mehrere Köpfe, insbesondere wenn man dafür sorgt, dass nicht immer dieselben Personen miteinander an einem Rechner arbeiten.
Refactoring Gerade in den Vorgehensmodellen, in den mit sehr kurzen Iterationen gearbeitet wird, geht man davon aus, dass die erste Implementierung nicht unbedingt die beste ist. Da man in der Iteration noch nicht weiß, welche Features in den nächsten Iterationen gebaut werden, fehlt einem eventuell auch der Überblick, Strukturen entsprechend aufzusetzen. Wie schon in Kapitel 5 erläutert, beschreibt Refaktorieren, oder Refactoring auf Englisch, den Vorgang, den Code und die Struktur einer Anwendung oder eines Teils der Anwendung im Nachhinein noch mal zu bearbeiten, um die Qualität zu erhöhen. All diese Vorgehensweisen und Prinzipien kommen nicht ohne Fragen oder Kritik. Funktionieren diese Ideen in der Praxis wirklich so gut? Gibt es einen Haken? Dazu muss man zuerst sagen, dass die Ideen der agilen Vorgehensmodelle noch recht jung sind. Es gibt noch kaum Studien zu diesem Thema, die deren Wirksamkeit glaubhaft be- oder widerlegen könnten. Insofern ist grundsätzlich ein wenig Vorsicht geboten. Agile Vorgehensmodelle befinden sich eher noch im experimentellem Stadium und werden momentan von vielen Firmen in der Praxis angewandt und getestet. Die Zukunft wird zeigen, wie effektiv sie wirklich sind. Einige Fragen zur agilen Softwareentwicklung habe ich im Folgenden notiert.
Wie geht man in agiler Softwareentwicklung mit Festpreis-Projekten um? In vielen Bereichen der Softwareentwicklung, auch im Flash-Bereich, werden FestpreisAngebote ausgegeben. Festpreis-Projekte gehen meistens vom Kunden aus. Der Kunde hat ein bestimmtes Budget oder will eine Vergleichbarkeit zwischen konkurrierenden Firmen herstellen und fordert deswegen für ein bestimmtes Briefing einen Gesamtkostenplan an. In vielen Fällen sind die Briefings nur ein paar Seiten lang und skizzieren grob die wichtigsten Anforderungen. Festpreis-Projekte erwecken den Anschein, als wäre von Beginn an der genaue Umfang des Projekts klar und man könnte deswegen eine feste Kalkulation veranschlagen. Wir wissen, dass dies nicht funktioniert. Wichtige Details ergeben sich erst im Laufe des Projekts. Eventuell erkennen Auftragnehmer und Kunde auch erst im Projekt, was die wirklichen Anforderungen sind. Festpreis-Projekte können eigentlich nur dann sinnvoll angesetzt werden, wenn Kunde und Auftragnehmer die Art von Projekt schon mehrmals durchgeführt haben und es im Prinzip keine Unklarheiten mehr gibt. In der Praxis wird der Widerspruch meist durch Change Requests auszugleichen versucht. Dabei vermerkt der Auftragnehmer, dass eine gewünschte Änderung oder ein zusätzlicher Wunsch des Auftraggebers nicht in der ursprünglichen Planung enthalten war und deswegen zusätzlich berechnet werden muss. Change Requests sind der Inbegriff eines Störfaktors im Projekt, denn sie sind formal, sie werden von allen Seiten als unangenehm empfunden, und in einem Festpreis-Projekt stören sie den Projektfluss, weil nun der Kunde erst mal klären muss, ob der Change Request berechtigt ist und, wenn ja, ob noch zusätzli-
319
Kapitel 6 che finanzielle Mittel vorhanden sind. Im schlimmsten Fall steht während so einer Phase das Projekt. Nicht selten werden dann alle verfügbaren Dokumente herausgeholt, und es wird um einzelne Sätze in den Dokumenten gefeilscht. Für die meisten Projekte eignen sich Festpreis-Projekte nicht. Aber Kunden befürchten verständlicherweise, dass ihnen bei einem nach Aufwand berechneten Projekt die Kosten und die Kontrolle darüber entgleiten. Wie gehen agile Projekte mit dieser Befürchtung um? Festpreis-Projekte können auch in agilen Projekten umgesetzt werden, auch wenn dies ein wenig mehr Aufwand bedeutet. Die meisten agilen Projekte arbeiten iterativ, das heißt, das Projekt wird aufgeteilt in einzelne Schritte, in denen jeweils ein Teil der Anwendung gebaut und dann von allen Beteiligten (natürlich auch vom Kunden) begutachtet und abgenommen wird. Es ist nun möglich, dass ein gewisser Aufwand noch vor der ersten Iteration, also dem ersten Schritt, betrieben wird, um grob den Umfang der Anwendung und die gewünschten Funktionen und Eigenschaften zu skizzieren und mit groben Aufwänden zu beziffern. Danach gibt man ihnen Prioritäten und definiert die Funktionen und Eigenschaften, die unbedingt enthalten sein müssen. Pro Iteration definiert man außerdem, welche Features aus der Liste umgesetzt werden. Der Kunde hat durch die Iterationen ständig einen Überblick über den genauen Stand seines Projekts und kann pro Iteration entscheiden, in welche Features investiert werden soll. In enger Zusammenarbeit mit dem Kunden kann so das bestmögliche Resultat erzielt werden. Für den Auftragnehmer ist die Differenz zwischen der Anzahl aller Features und der Anzahl der zwingend benötigten Features der Puffer, der die Ungenauigkeit in der Schätzung ausgleichen soll. Mit diesem Vorgehen ist das eigentliche Problem von FestpreisAngeboten nicht gelöst, aber es ist zumindest möglich, die Vorteile von agiler Softwareentwicklung so gut wie möglich zu nutzen.
Was passiert mit Änderungswünschen, die den Zeitplan oder das Budget in Gefahr bringen? Das Problem, dass man zu Beginn eines Projekts den tatsächlichen Umfang nicht überblicken kann, kann auch ein agiles Vorgehensmodell nicht lösen. Die Aufteilung Iterationen aber und die Priorisierung der Aufgaben nach den Bedürfnissen des Kunden ermöglicht, sich genau auf die Bereiche zu konzentrieren, die wichtig sind. Wenn der Zeitplan fix ist, kann man überlegen, welche Features bis zum Erreichen des Endtermins fertiggestellt werden müssen und welche man in einem späteren Release nachschieben kann. Wenn das Budget fix ist, kann man nach jeder Iteration schauen, wie die Budgetsituation aussieht und welche Features noch ins Budget passen. Der Vorteil der Iterationen ist grundsätzlich, dass das Projekt nicht wie ein unaufhaltbarer Zug dem Gesamtprojektende entgegenrollt. Stattdessen fungieren die Iterationen als Weichen auf der Strecke, die den Projektbeteiligten die Möglichkeit geben zu entscheiden, in welche Richtung es weitergehen soll. Auch Änderungswünsche werden in die Iterationen eingegliedert, als neue Features. In der Regel werden notwendige Änderungen in einem agilen Prozess viel früher erkannt, wenn
320
Vorgehensmodelle das Team zusammen mit dem Kunden regelmäßig und viel kommuniziert. Somit kommt es seltener vor, dass in späten Phasen des Projekts noch komplett grundlegende Dinge aufkommen, die ein Projekt umwerfen könnten.
Bleibt in agilen Projekten noch Zeit für eine eingehende Analyse der Anforderungen und Konzeption? Francisco A. C. Pinheiro schreibt in einem Papier zum Thema Anforderungsanalyse in agilen Vorgehensmodellen: »[...] the main source of XP extremeness is the length of its iterations. This is extreme because it may hinder the requirements principles.« (TCRE et al. 2002, S. 6) Er beschreibt in seinem Papier das Problem, dass eine sehr kurze Iteration zwar eine gute Flexibilität ermöglicht, es aber sehr schwierig macht, Anforderungen ausreichend zu analysieren und zu beschreiben. Pinheiro geht dabei beispielhaft von Extreme Programming aus, bei dem die Iterationen nur zwei Wochen betragen. Das Gleiche gilt im Prinzip auch für die Konzeption der Anwendungsbereiche. In einem Zyklus von zwei Wochen, in dem sowohl die Anforderungen als auch die Konzeption erarbeitet werden und dann auch noch umgesetzt werden sollen, bleibt wenig Zeit. Andere Vorgehensmodelle gehen deswegen anders vor und erlauben Iteration von der Länge eines Monats oder sogar mehr. Die Art des Projekts sollte hier den Ausschlag für kürzere oder längere Iterationen geben.
Wie geht man vor, wenn der Kunde sich nicht so stark wie gewünscht einbringen kann? Normalerweise sollte es im eigenen Interesse des Kunden liegen, sich so stark wie möglich in das Projekt zu involvieren, damit das Resultat seinen Wünschen entspricht. In der Praxis verhindern aber die unterschiedlichsten Dinge diesen Vorsatz. Eventuell bearbeitet der Mitarbeiter auf Kundenseite mehrere Projekte gleichzeitig und kann sich nicht bis ins Detail einbringen. Oder aber der Kunde ist nicht organisatorisch so aufgestellt, dass er ein Projekt durchgehen betreuen kann. In jedem Fall leidet das Projekt darunter, wenn der Kunde nicht aktiv ins Projekt mit einbezogen wird. Softwareprojekte sind zu komplex, als dass ein Kunde ein Projekt einfach laufen lassen und hoffen kann, dass am Ende schon alles passen wird. Das erste Ziel sollte also sein, den Kunden so stark wie möglich einzubeziehen. Wenn das nicht oder nur begrenzt geht, kann dem nur mit entsprechend mehr Formalien und Dokumenten entgegengewirkt werden, was man in agilen Modellen eigentlich vermeiden will.
Kann agile Softwareentwicklung auch in Agenturen eingesetzt werden? Agenturen, die z. B. im Online-Bereich oder im Bereich von Terminals oder Installationen tätig sind, produzieren auch Software. Viele dieser Agenturen bewegen sich auf Auftraggeber-Seite eher im Marketingbereich oder allgemeiner ausgedrückt im Bereich der Unternehmenskommunikation. Das ist insofern wichtig, als diese Auftraggeber in der Regel andere
321
Kapitel 6 organisatorische Strukturen aufweisen, als Auftraggeber aus anderen Bereichen wie Controlling, Produktion, Finanzen. Im Bereich der Unternehmenskommunikation sind Kreativausschreibungen nicht unüblich. Das bedeutet auch, dass die Mehrzahl der Projekte Festpreis-Projekte sind und dass den Ausschlag einerseits die kreative Idee gibt und andererseits die Höhe des Angebots. Es gibt in der Fachliteratur und im Internet bisher wenig fundierte Quellen zu agiler Softwareentwicklung im Agenturbereich. Zwar wird der Begriff agil fast überall verwendet, es gibt aber keine tiefergehenden Informationen darüber, was Agenturen darunter verstehen und wie sie agile Vorgehensweisen im Projektgeschäft zusammen mit Kunden anwenden. Zumindest ist zu erkennen, dass sich Leute aus dem Bereich User Experience Design (z. B. Jeff Patton, http://agileproductdesign.com/blog, oder auch Law et al. 2008) und aus dem Bereich Web Development (z. B. Wallace et al. 2003) dafür interessieren, wie agile Vorgehensmodelle auch hier erfolgreich eingesetzt werden können – und dies durchaus auch schon seit Längerem. Die hier gewonnenen Erkenntnisse könnten auch für Agenturen interessant sein. Letztlich muss die Frage wohl nicht lauten, ob agile Vorgehensmodelle in Agenturen angewendet werden können, sondern wie agil Agenturen ihre Projekte organisieren können. Letztlich liegt in Agilität an sich kein Wert, sondern allein in den Resultaten, die durch Projekte erzeugt werden. Wenn bestimmte einzelne Prinzipien der agilen Vorgehensmodelle helfen, auch in Agenturen den Projektablauf zu verbessern, reicht das schon.
6.7 Mit Änderungen umgehen Steve McConnell schreibt: »Entwickler, die überzeugt sind, Anforderungen ließen sich in Stein meißeln und würden nie verändert, sind wahrscheinlich auch eine dankbare Zielgruppe für UFO-Andenken und glauben, dass die Erde flach ist.« (McConnell 2007, S. 39) In einem Softwareprojekt kann sich alles ändern, von den grundsätzlichen Anforderungen, dem Konzept über das Design bis zur technischen Umsetzung. In jedem Bereich können neue Erkenntnisse, Wünsche, Limitierungen, Gesetze etc. zu Änderungen führen. Kent Beck, Mitbegründer des Vorgehensmodells Extreme Programming, schreibt dazu: »Everything in software changes. The requirements change. The design changes. The business changes. The technology changes. The team changes. The team members change. The problem isn't change, because change is going to happen; the problem, rather, is our inability to cope with change.« (Beck, Andres 2007, S. 11) In den vorangegangenen Abschnitten haben wir gelernt, wie man mit entsprechenden Vorgehensmodellen versuchen kann, Änderungen frühzeitig zu begegnen, um sie so klein wie möglich zu halten. In diesem Abschnitt wollen wir uns nun mit der Änderung selbst beschäftigen. Wie ich schon sagte, können Änderungen in jedem Bereich eines Projekts auftreten. Im Rahmen dieses Buchs sollen uns aber nur solche im technischen Bereich, also in
322
Vorgehensmodelle der Struktur und im Code, interessieren. Denn letztlich schlägt sich fast jede Änderung in einem Softwareprojekt, egal wo sie aufschlägt, auch in der Anwendungsstruktur und im Code nieder. Das Stichwort hier ist die Versionierung von Varianten der gleichen Anwendung. Viele kleine Flash-Anwendungen werden einmal entwickelt, online gestellt und nach einiger Zeit einfach wieder entfernt. Sie durchlaufen einen recht kurzen Lebenszyklus. Andere hingegen durchlaufen verschiedene Phasen mit jeweils erhöhter Funktionalität oder anderen Inhalten. Daneben gibt es auch Anwendungen, die so konzipiert sind, dass sie theoretisch sehr lange verfügbar sein und auch immer wieder verbessert werden sollen. Egal ob lange oder kurze Lebensdauer, in jedem Entstehungsablauf einer Anwendung gibt es unterschiedliche Versionen der Anwendung. Selbst wenn eine Anwendung nach ihrem Debüt nicht mehr verändert und nach kurzer Zeit wieder aus dem Web genommen wird, gibt es immer bestimmte Grundphasen in einem Softwareprojekt: 1. Entwicklung der Grundanwendung mit allen wichtigen Basisfunktionen (das Resultat wird oft auch Alpha-Version genannt) 2. Testen und Korrigieren der Grundanwendung und Hinzufügen fehlender Details (BetaPhase) 3. Finaler Test der funktional fertigen Anwendung, letzte Korrekturen, meistens nur noch Fehlerbereinigung (Release Candidate) 4. Fertige Anwendung (Release). Danach erfolgende Fehlerbehebung oder Korrekturen werden oft durch Patches oder Hotfixes vorgenommen (Anwendungen, die direkt im Webbrowser laufen und nicht installiert werden müssen, können hier natürlich besonders flexibel auf den neuesten Stand gebracht werden) Selbst kleinste Projekte haben fast immer auch diese vier Phasen, auch wenn sie nicht offiziell so benannt werden.
Beta-Phase In manchen Fällen wird die Beta-Phase öffentlich abgehalten oder zumindest in einem geschlossenen Kreis von Endnutzern, die sich für die Beta-Phase anmelden können. In manchen Fällen geschieht dies sogar schon für späte Alpha-Versionen. Offene Beta-Phasen bieten den Vorteil, dass man als Entwickler früh Rückmeldungen von den »echten« Endnutzern erhält, die später auch die Anwendung benutzen sollen. Hierdurch können auch konzeptionell manche Dinge noch verbessert werden. Offene Beta-Phasen eignen sich aber natürlich nicht für jede Art von Anwendung. Zum einen muss seitens der Endnutzer ein Interesse vorhanden sein, eine Anwendung bereits im »unfertigen« Beta-Stadium zu testen und zu benutzen. Nicht jede Zielgruppe ist dazu bereit. Auch die Art der Anwendung spielt eine Rolle. Kaum ein Mensch würde wohl gerne in der Beta-Phase einer Online-Banking-Anwendung teilnehmen. Zudem sind offene oder auch geschlossene Beta-Phasen sehr aufwendig. Je nachdem, wie viele Nutzer an dieser Phase teilnehmen, kann die Betreuung dieser Nutzer viel Zeit in
323
Kapitel 6 Anspruch nehmen. Beta-Tester, die ihre Zeit dafür opfern, mit einer noch unfertigen Anwendung zu arbeiten, wollen Fehler möglichst komfortabel und schnell abliefern können und das Gefühl haben, dass ihre Rückmeldungen auch ernst genommen werden. Sie wollen außerdem erkennen, dass gemeldete Fehler auch tatsächlich behoben werden. Ein Unternehmen, das offene oder geschlossene Beta-Phasen mit Endnutzern durchführt, setzt sich also bewusst einem nicht unerheblichen zusätzlichen Druck aus. Andererseits erhält es dafür auch sehr wertvolle Informationen von den Endnutzern, die letztendlich der endgültigen Anwendung zugute kommen können. Michael Fine schreibt: »Beta testing is the managed distribution of a product to its target market; the gathering of feedback from that market; the evaluation of the feedback into manageable data forms; and the integration of the data into the organizations it affects.« (Fine 2002, S. 11) Wichtig ist hier der Teil, dass die Rückmeldungen der Endnutzer ausgewertet und in eine verwertbare Form gebracht werden müssen. Viele Unternehmen verwenden hierfür z. B. Bugtracking-Anwendungen, in die Fehler oder auch Änderungswünsche formell eingetragen werden können. Diese Meldungen durchlaufen dann einen formellen Prozess im Unternehmen, werden an entsprechende Entwickler oder andere Projektbeteiligte weitergeleitet und von diesen dann bearbeitet. Die Endnutzer können dabei ihre und andere Meldungen jederzeit verfolgen und ihren Status und Fortschritt begutachten.
Lokale Versionierung des Source-Codes Die Source-Dateien werden von den Entwicklern ständig bearbeitet. Neue Dateien kommen hinzu, und vorhandene werden verändert. Mit dem Einzug der Objektorientierung in Flash und dem Speichern von Klassen in ihre eigenen Textdateien entsteht schon bei kleinen Projekten eine stattliche Zahl von Dateien. Zwei Aspekte führen dazu, dass man sich mit der Verwaltung und Versionierung von Quelldateien beschäftigen sollte.
Nicht jede Änderung einer Datei ist immer beabsichtigt oder führt zum beabsichtigten Ergebnis. Das bedeutet, dass man zuweilen zu einer früheren Version zurückspringen möchte. Es kann sein, dass einem erst nach mehreren Tagen und vielen Änderungen an der gleichen Datei auffällt, dass man hier in eine falsche Richtung gegangen ist und wieder den alten Stand braucht.
Wenn mehrere Entwickler an einem Projekt arbeiten, kommt es nicht selten vor, dass sie zumindest zeitweise an der gleichen Datei zur gleichen Zeit arbeiten wollen. Diese parallelen Änderungen müssen später wieder zusammengeführt werden. Entwickler müssen zudem erkennen können, wer zuletzt an einer Datei etwas geändert hat und was das war. Aus diesen und anderen Gründen sind aus den meisten heutigen Flash-Projekten Versionskontrollsysteme nicht mehr wegzudenken. Ein prominenter Vertreter ist hier z. B. Subversion, das eine Open-Source-Lösung ist. Versionskontrollsysteme legen Dateien meist auf einem zentralen Server ab und kopieren temporär Arbeitskopien auf die lokalen Rechner der Entwickler. Die Entwickler können aktiv jederzeit den aktuellsten Stand vom Server anfordern, Dateien bearbeiten und hinterher wieder an den Server zurücksenden. Beim Hochspielen von Dateien auf den Server werden automatisch Versionsnummern hochge-
324
Vorgehensmodelle zählt. Alte Versionen bleiben immer erhalten und können jederzeit hervorgeholt werden. Daneben bieten diese Systeme noch weitere Komfortfunktionen wie das unterstützte Beheben von Konflikten für den Fall, dass zwei Entwickler zur gleichen Zeit an derselben Stelle in derselben Datei eine Änderung vornehmen wollen. Außerdem gibt es meistens Funktionen wie das Vergleichen von Versionsständen usw. Versionskontrollsysteme helfen so, die Quelldateien während der Entwicklung zu organisieren (das gilt übrigens nicht nur für reine Source-Code-Dateien, sondern für alle Dateien, die währen der Entwicklung anfallen). Eine spezielle Funktionalität, die viele Versionskontrollsysteme anbieten, ist das sogenannte Branching. Dabei wird ein Flash-Projekt grundsätzlich in einem Ordner mit dem Trunk entwickelt. Der Trunk repräsentiert also die Hauptentwicklungslinie. Er enthält immer den offiziellen Stand. Nun kann es aber vorkommen, dass neue Teile der Anwendung entwickelt oder bestehende Teile umgebaut oder optimiert werden müssen. Man könnte dies zwar auch im Trunk tun, dort würde man dann aber in Konflikt mit dem momentanen Stand kommen, der ja meistens auch noch parallel weiter betreut werden muss. Gerade wenn bestehende Teile geändert werden sollen, kommt hier schnell die Schwierigkeit, die originale und die gerade veränderte Datei parallel zu organisieren. In Versionskontrollsystemen kann man neben dem Trunk-Ordner beliebig viele sogenannte Branches (zu Deutsch: Äste) anlegen. Oft kopiert man dafür den aktuellen Stand aus dem Trunk (oder auch nur einen Teil daraus, wenn nur an einem Teil etwas verändert werden soll) in den neu angelegten Branch. Nun entwickelt man losgelöst vom Trunk in dem eigenen Branch-Ordner. So können sich Dateien und Entwickler nicht in die Quere kommen, und es ist sogar möglich, parallel zur Entwicklung im Branch auch kleinere Wartungen, z. B. Bugfixes im Trunk, vorzunehmen.
Entwicklung im Trunk
Erstellung eines Branches
Wartung im Trunk
Entwicklung im Branch
Merge zurück in den Trunk
Abbildung 6.10: Ablauf der Entwicklung in einem Branch
325
Kapitel 6 Wenn die Entwicklung im Branch abgeschlossen ist, unterstützen einen die Versionskontrollsysteme dabei, diesen Teil wieder zurück in den Trunk zu geben (auf Englisch: mergen). Dabei muss natürlich immer auch manuell darauf geachtet werden, dass die Integration korrekt durchgeführt wird, dass also keine aktuelleren Dateien von älteren überschrieben werden. Besonders muss darauf geachtet werden, dass Dateien im Trunk, die inzwischen Bugfixes erhalten haben, nicht einfach blind durch Dateien aus dem Branch überschrieben werden. Die meisten Versionskontrollsysteme bieten hierfür Werkzeuge an, die genau anzeigen, was sich in welcher Datei wann geändert hat, sodass der Entwickler eine gute Entscheidungsgrundlage hat, welche Datei oder welcher Teil in einer Datei erhalten bleiben soll. Es lohnt sich, sich mit dem verwendeten Versionskontrollsystem näher zu beschäftigen. Diese Systeme bieten neben der offensichtlichen Funktion der Versionierung der Dateien oft noch viele weitere nützliche Funktionen, wie z. B. das Referenzieren von Teilen eines Projekts in einem anderen Projekt.
Versionierung der kompilierten Anwendung/Module Neben der Versionierung von projektrelevanten Quelldateien sollte man sich über die Versionierung der kompilierten Anwendung natürlich auch Gedanken machen. In den Fällen, in denen man schon weiß, dass die Anwendung eine kurze Lebensdauer haben wird, in der keine nennenswerten Änderungen zu erwarten sind, geht das eventuell recht schnell. In anderen Situationen ist die Versionierung der kompilierten Dateien wichtig, um den Überblick zu behalten. Konkrete Versionsnummern sind meist für die Entwickler von höherer Bedeutung als für die Endnutzer. Oft ist es sowieso so, dass das endgültige Produkt mit Versionsnummern oder Namen versehen wird, die vom Marketing vorgegeben werden und nicht zwingend der internen Versionierung entsprechen. Es empfiehlt sich also, diese beiden Welten von vornherein voneinander zu trennen. Warum aber sind Versionen so wichtig für Anwendungen? Sie werden dort wichtig, wo Abhängigkeiten zwischen Anwendungen, Modulen, Bibliotheken oder Frameworks bestehen. Stellen Sie sich vor, Sie haben eine Anwendung erstellt unter Verwendung einer bestimmten Version von PaperVision3D (eine AS3 3D-Engine) und Tweener (eine Animationsbibliothek). Sie haben die Anwendung fertig programmiert. Das Projekt ruht eine Weile, bis man Sie beauftragt, noch ein kleines Modul zur Anwendung hinzuzufügen. Inzwischen gibt es von beiden Bibliotheken neuere Versionen. Würden Sie jetzt einfach die neueren Versionen der beiden Bibliotheken herunterladen und verwenden? Bestimmt nicht, denn die Gefahr ist, dass Ihre ansonsten funktionierende Anwendung mit den neuen Bibliotheken aus welchen Gründen auch immer nicht mehr funktioniert. Vielleicht wurden einfach ein paar Zugriffsmethoden leicht geändert. Eventuell wurden Methoden oder Klassen entfernt, die sowieso schon als veraltet galten. Wenn Sie die Funktionen der neueren Versionen der Bibliotheken nicht zwingend brauchen, werden Sie wohl eher die alten Versionen der Bibliotheken verwenden, denn da wissen Sie, dass Ihre Anwendung funktioniert. Versionen bilden also Entwicklungsstände von Softwarekomponenten ab, die wir verwenden können, um Abhängigkeiten zwischen Anwendungen und Modulen zu bestimmen. In der Versionierung gibt es keine allgemeingültigen Standards, aber eine Variante wird relativ
326
Vorgehensmodelle häufig verwendet, die dreistufige Nummerierung, z. B. Version 1.3.12. Die erste Zahl steht für ein Haupt-Release. Das sind normalerweise Versionen, bei denen die Anwendung runderneuert wurde und lange geplante Funktionen implementiert wurden. Die zweite Zahl bezeichnet innerhalb des Haupt-Release Fehlerbereinigungen und kleinere Optimierungen oder zusätzliche Funktionen. Die dritte Zahl bezeichnet konkrete Erstellungsversionen. Bevor eine Anwendung herausgegeben wird, muss sie ja getestet werden, dabei wird die Anwendung eventuell mehrmals neu erstellt. Diese Erstellungsversionen werden dann also mit der dritten Zahl gekennzeichnet. Das ist nur eine mögliche Definition der Versionierung. Wie gesagt, Softwareentwickler arbeiten hier nicht unbedingt einheitlich. Letztlich hängt die Versionierungsart auch von den internen Abläufen im Projektteam ab. Versionsabhängigkeiten können zu einer Herausforderung werden, wenn eine Anwendung viele unterschiedliche Bibliotheken verwendet. Es kann vorkommen, dass Entwickler, die am gleichen Projekt arbeiten, unbewusst die gleiche Bibliothek, aber in unterschiedlichen Versionen einsetzen. Im schlimmsten Fall führt dies zu Fehlern, wenn die Programmteile der beiden Entwickler zusammengeführt werden. Es kann auch passieren, dass eine alte Anwendung, die schon jahrelang nicht mehr angefasst wurde, wieder herausgeholt und überarbeitet werden muss. Sie verwendet wahrscheinlich recht alte Versionen von Bibliotheken, die vielleicht gar nicht mehr verfügbar sind. Wenn dann die entsprechende Version nicht mitgesichert wurde, stehen die Entwickler vor einem Problem, denn die aktuellen Versionen der Bibliothek sind unter Umständen nicht mehr kompatibel. Das Verwalten und Organisieren unterschiedlicher Softwarebausteine mit unterschiedlichen Versionen fällt unter den Bereich Konfigurationsmanagement. Hierfür gibt es Werkzeuge, die die Verwaltung vereinfachen und zumindest teilweise automatisieren, z. B. die OpenSource-Lösungen Maven oder auch Apache Ivy. Mit diesen Werkzeugen ist es möglich zu bestimmen, wo sich bestimmte Bibliotheken befinden und welche Versionen von Bedeutung sind. Außerdem kann man definieren, welche dieser Bibliotheken das aktuelle Projekt in welcher Version benötigt. Bisher haben wir darüber gesprochen, wie man mit fremden Bibliotheken und ihren Versionen umgeht, aber wie sieht es mit der eigenen Anwendung oder der eigenen Bibliothek aus? Sobald Sie ein Stück Software erstellen, die andere Entwickler in ihren Projekten einsetzen sollen, sollten Sie sich Gedanken über die Versionierung Ihres Stückes Software machen, egal ob das eine ganze Anwendung, eine kleine Komponente, eine Bibliothek oder ein Framework ist. Denn egal was Sie da bauen, Sie werden es mit der Zeit verändern, Sie werden Fehler entfernen und neue Funktionen hinzufügen. Die Entwickler, die Ihr Produkt verwenden wollen, werden wissen wollen, was sie da gerade herunterladen und wie es sich zu der Variante unterscheidet, die sie noch vor ein paar Wochen heruntergeladen hatten. Wenn Sie anfangen, Ihre Entwicklungsergebnisse zu versionieren, dann werden Sie auch anfangen, einzelne Aufgaben stärker zeitlich zu planen. Größere neue Funktionalitäten werden Sie nicht mal eben bauen, sondern für ein bestimmtes Haupt-Release einplanen, kleinere Funktionen oder wichtige Bugfixes dagegen können Sie zwischendurch mit PunktReleases (also solchen Veröffentlichungen, die die zweite Zahl in der Versionsnummer erhöhen) abarbeiten. Es ist üblich und empfehlenswert, dass zumindest innerhalb eines Haupt-
327
Kapitel 6 Release (also solange die erste Zahl gleich bleibt) vorhandene Schnittstellen und Funktionen nicht verändert werden, also kompatibel bleiben. So können sich Entwickler, die Ihre Anwendung oder Bibliothek verwenden, sicher sein, dass sie innerhalb eines Hauptrelease einigermaßen gefahrlos Updates herunterladen können. Bevor Sie eine neue Version erzeugen, versetzen Sie sich in die Lage der Leute, die Ihre Anwendung oder Ihre Bibliothek verwenden. Werden sie Ihre Anwendung umbauen müssen, wenn sie die neue Version verwenden? Können Sie die Programmierung so abändern, dass die neue Version kompatibel bleibt? Kompatibilität ist eine der wichtigsten Anforderungen und Wünsche von Entwicklern. Erinnern Sie sich an die Umstellung von Flash 8 auf Flash 9 und die Einführung einer neuen ActionScript-Version? Diese Umstellung hat weltweit bei Entwicklern, Softwarefirmen und Agenturen nicht unerhebliche Kosten verursacht. Viele alte Anwendungen wurden dazu verdammt, über kurz oder lang entsorgt zu werden, denn eine Portierung rechnete sich aus Kostengründen oft nicht, und die Weiterentwicklung mit der alten Technologie wird in absehbarer Zeit in einer Sackgasse enden. Es ist also wichtig, dass Sie nicht ständig nur neue Features bauen und neue Versionen erzeugen. Vielmehr sollten Sie sich einen Plan erstellen, eine sogenannte Roadmap, in der Sie wichtige neue Features künftigen Hauptversionen zuordnen und kleinere Optimierungen und Änderungen in Punkt-Releases schieben. Viele heutige Bibliotheken konzentrieren sich stark auf die Entwicklung immer neuer Funktionen und schenken der Stabilität ihre Bibliotheken weniger Beachtung. Dies ist auch darauf zurückzuführen, dass die Entwicklung dieser Bibliotheken keinem generellen Plan folgt, sondern spontanen Entschlüssen unterliegt. Das führt zwar dazu, dass in kurzer Zeit ein großer Funktionsumfang entsteht, die Bibliothek aber zu Teilen instabil ist und bleibt und sich die nutzenden Entwickler nicht auf eine Planung einstellen können. Das macht die Arbeit mit solchen Bibliotheken leider unberechenbar. Zu guter Letzt sei noch auf die Dokumentation von Versionen hingewiesen. Für Entwickler ist es von besonderer Bedeutung, nachvollziehen zu können, was der Gehalt einer bestimmten Version ist. Wie unterscheiden sich Versionen untereinander, welche Funktionalitäten wurden in welcher Version eingeführt, was wurde in welcher Version geändert oder welche Funktionalität wurde gar entfernt? Eine Versionshistorie ist für die meisten nutzenden Entwickler unerlässlich, damit sie entscheiden können, welche Version sie für ihr Projekt einsetzen sollen. Wenn Sie ein Versionskontrollsystem verwenden, in dem Sie auch Ihre kompilierten Anwendungen bzw. Bibliotheken ablegen und zur Verfügung stellen, können Sie in vielen dieser Systeme sogenannte Tags einrichten. Das sind im Prinzip Lesezeichen auf bestimmte interne Versionen, denen Sie einen konkreten Namen, z. B. Ihre offizielle Versionsnummer, geben können. Nicht jede interne Versionsnummer in einem Versionskontrollsystem ist für den öffentlichen Gebrauch bestimmt. Manchmal lädt man auch Zwischenstände hoch, die noch nicht dafür gedacht sind, daraus eine kompilierte Anwendung zu erstellen. Deswegen sollten Sie die Tags nutzen, um eine Trennung von Ihren geplanten offiziellen Versionsnummern zu den im Versionskontrollsystem automatisch vergebenen Versionen zu gewährleisten.
328
Vorgehensmodelle In den meisten Versionskontrollsystemen können Sie dann bei der Erstellung eines solchen Tags entsprechende dokumentierende Beschreibungen hinzufügen, die erläutern, was sich in dieser neuen offiziellen Version alles getan hat. Daneben empfiehlt es sich natürlich, bei jedem internen Hochladen geänderter Quelldateien aussagekräftige Kommentare hinzuzufügen.
6.8 Literaturangaben Beck, Kent; Andres, Cynthia: Extreme programming explained. Embrace change. 2. Edition, 6. Auflage. Boston: Addison-Wesley, 2007. Cockburn, Alistair: Agile software development. 4. Auflage. Boston: Addison-Wesley, 2003. Fine, Michael: Beta testing for better software. New York, NY: Wiley Technology Publ., 2002. Law, Effie Lai-Chong; Hvannberg, Ebba Thora; Cockton, Gilbert; Jeffries, Robin; Wixon, Dennis: Maturing usability. Quality in software, interaction and value. London: Springer, 2008. McConnell, Steve: Code complete. Dt. Ausg. der 2. Edition, (Nachdr.). Unterschleißheim: Microsoft Press, 2007. Piatelli-Palmarini, Massimo: Inevitable illusions. How mistakes of reason rule our minds. 2. Aufl. New York; Brisbane; Chinchester; Toronto; Singapore: John Wiley & Sons, 1994. TCRE, Eberlein Armin; Cesar Sampaio do Prado Leite, Julio (Hg.) (2002): Proceedings of the International Workshop on Time Constrained Requirements Engineering. Wallace, Doug; Raggett, Isobel; Aufgang, Joel: Extreme programming for Web projects. Boston, MA: Addison-Wesley, 2003.
329
Index A>>> Abhängigkeit Versionsstände 326 Ablaufdiagramm 44 Abbruchbedingung 45 Ablaufdiagramm siehe Aktivitätsdiagramm Abstrahierung 53, 78 Code 82 fachlich 81 Interfaces 83 Modell 80 Vererbung 82 ActionScript 10 Agile Entwicklung 312 Agenturen 321 Änderungen 320 Anforderungsanalyse 321 Anwendung 314 Einbeziehung des Kunden 317 Festpreis 319 Kunde 321 paarweise Programmierung 318 Priorisierung 316 Refactoring 319 Reflexion 316 regelmäßige Ergebnisse 315 testgetriebene Entwicklung 317 Zusammenarbeit im Team 315 Agiles Manifest 313 Aktivitätsdiagramm 145 Analyse 13, 17 Änderungen 245, 322 Anforderungen 14, 17, 31 Fachebene 33, 39 funktionale 32 gemeinsame Sprache 18, 35 Geschäftsebene 33, 34 nichtfunktionale 32
Sachzwänge 55 Umsetzungsebene 33, 50 Ziel 31 Anforderungsanalyse 31 in agiler Entwicklung 321 Interview 35 Anpassungsunfähigkeit 297 Antipattern 172 Anwendungsfall 30, 41 Fehlerfall 43 Application Programming Interface (API) 231 Applikations-Framework 241 Arbeitsteilung 65 Komponenten 233 Architektur versus Entwurf 57 Architektur-Framework 131, 241 Architekturmuster 153 Komponentenarchitektur 160 MVC 156, 190 Schichtenmodell 153 Webservices 161 ASBroadcaster 198
B>>> Benutzerfreundlichkeit 21 Benutzeroberfläche 48 Beobachter 98 Bibliothek 228 eigene 236 fremde 234 Klassensammlung 229 Klassenstrukturpaket 229, 231 Komponente 230, 232 veröffentlichen 229 Blackbox Komponenten 232 Testen 278
Index
C>>> Clickdummy 49
D>>> Dependency Injection 102, 218 Deployment Komponenten 233 Diagramm 134 Dokumentation 70 Bibliothek 235 Versionen 328 Domain-Specific Language 224
E>>> Effizienz 21 Eigene Bibliotheken 236 Entwicklung 22 Beta-Phase 323 Branching 325 Phasen 323 Versionierung Resultat 326 Versionierung Source-Code 324 Entwurfsmuster 163 Beobachter 98 Fabrikmethode 101 Mediator 98 Enumerator 263 Erweiterbarkeit 73 Erzeugungsmuster 164 Erbauer (Builder) 165 Fabrikmethode (Factory Method) 167 Singleton 163, 168 Ethik 18 Event 198 Event-Bubbling 216 EventDispatcher 198
F>>> Fabrikmethode 101 Fachebene Schnittstellen 48 User-Interfaces 48
332
Fachklassen 46 Fachkonzept 39 Fallbackstrategie 207 Fehler 23 Fehlerbehandlung 78 Fehlertoleranz 77 final 110 Flash 9 Flowchart siehe Aktivitätsdiagramm Fluent Interfaces 223 Framework 205, 236 Applikations- 241 Architektur- 241 eigenes 238 Frozen Spot 237 Hot Spot 237, 239 Versionierung 240 Fremdbibliotheken 234
G>>> Generische Lösungen 252 Geschäftsfall 33, 36 Abgrenzungsfall 38 Globaler Status 172
I>>> Informatik 17 Infrastruktur-Framework 131 Inspektion 24, 277, 279 automatisiert 282 Codelesen 282 Effektivität/Effizienz 279 formell 280 informell 281 Interface 95 Erwartungshaltung 95 Mehrfachvererbung 120 Inversion of Control 95, 101, 217 Dependency Injection 102, 218 Iterator extern 204 intern 204
Index
J>>> Java 9
K>>> Kapselung 83 Verstecken von Implementierung 88 Klasse final 110 Schnittstelle 92 Zugriff 89 Klassendiagramm 137 Kohärenz 122 Datenzusammenhalt 124 funktionaler Zusammenhalt 124 parametrisierter Zusammenhalt 124 zeitlicher Zusammenhalt 123 Kommunikation 294 Komplexe Systeme 25, 29 Komplexität 10, 61 Abhängigkeiten 62 fachliche 64 Größe 64 Spezialisierung 63 Verringerung 56, 65 Komponentendiagramm 136 Konfigurationsmanagement 327 Konzept vs. Infrastruktur 130 Konzeptionelle Rollen 120
Method Chaining 223 Mikroarchitektur 158 Mockup siehe Clickdummy Modell 79 Modularität 74, 103, 182 Arbeitsteilung 104 Beispiele 107 Deployment 104 Klassenkonflikte 105 Motivation 104 Muster 152 MVC PresentationModel 158
N>>> Nachricht 201
O>>> Objektorientierter Entwurf 61 Objektorientierung 25 Analyse 29, 30 besteht aus 29 Entwurf 61 ist ein 29 Netzwerke 25 Objekt 26 Observer 98 Open Source Bibliothek 235
L>>> Lastenheft 50 Lazy-Instantiation 192 Lernfähigkeit 298 Lesbarkeit 68 Dokumentation 70 Struktur 69 Liskovs Substitutionsprinzip 112, 182 Lose Kopplung 96
M>>> Mannschaftssport 292 Mediator 98 Mehrfachvererbung 120
P>>> Pflichtenheft 39, 50 Planung 54 Plug-In 216 Polymorphie 117 Projektmanagement 25 Projektziele 66 Proof-of-Concept siehe Clickdummy Prototyp siehe Clickdummy Proxy Remote 188 Sicherheit 188 virtuell 188
333
Index
R>>> Refactoring 14, 245 Rückwärtskompatibilität 251 Schnittstelle 251 vs. Entwurf 249 vs. Neuentwicklung 250 Refactoring-Maßnahmen 253 Code extrahieren 254 fremde Klasse erweitern 259 mehrfach verwendete Variable 258 Methode verschieben 259 Model-Objekt 261 Null-Objekt 270 Parameterobjekt 274 Polymorphie 266 Typecasts kapseln 275 Typklasse 263 Variable einsetzen 258 Werte umwandeln 260 Release-Management 327 Bibliothek 235 Review 24 siehe Inspektion Rich-Client 158 Risiko 295 Roadmap 328 Robustheit 77
S>>> Schnittstelle 92 Fan-out 98 Sichtbarkeit 89 Software Engineering 16 Gebäudebau 54 Vorgehensweisen 16 Softwarearchitektur 19, 58 Softwaredesign siehe Softwareentwurf Softwareentwurf 14, 21, 53, 59 iterativ 60 objektorientiert 61 Werkzeuge 133 Softwaretechnik siehe Software Engineering
334
Stakeholder 38 Strukturiertes Testen 286 Analyse des Kontrollflusses 288 Äquivalenzklassen 287 Datenflussanalyse 288 Strukturmuster 174 Brücke (Bridge) 175 Dekorierer (Decorator) 177 Fassade (Facade) 182 Proxy (Proxy) 186 Substitutionsprinzip 112 Superklasse vor Subklasse 119 Systemarchitektur 19, 57 Systemkomponente 20
T>>> Teamarbeit 300 interne Zwischenergebnisse 302, 310 Lokalisierung 301 Teambildung 303 Technische Hilfsklasse 167 Testarten 283 Abnahmetests 283 Integrationstests 283 Systemtests 283 Unit-Tests 283, 289 Testbarkeit 72 Testdriven Development 93, 285 Testen 23, 276, 282 Effektivität/Effizienz 279 Funktion 284 Kompatibilität 284 Mockup 109 Performance 283 Robustheit 283 Schnittstellen 284 Strukturelle Tests 284 Strukturiert 286 Use-Cases 284 Testplanung 284 Thin-Client 158 Traits 109
Index
U>>> Überblick verschaffen 297 UML 134 Abhängigkeit 141 Ablaufdiagramm 41, 44 Aggregation 142 Akteure 39 Aktivitätsdiagramm 44, 145 Anwendungsfall 41 Assoziation 141 Assoziationsklasse 142 Beziehungen 141 Fachklassen 47 Geschäftsfall 38 Kernaussage 143 Klassendiagramm 137 Kommunikationsdiagramm 147, 149 Komponentendiagramm 136 Komposition 143 Kontrollflussdiagramme 147 Sequenzdiagramm 147 Stakeholder 39 Statusdiagramm 147, 150 Umsetzung siehe Entwicklung Undo 197
V>>> Validierung 23, 276 Veränderbarkeit 71 Verantwortlichkeit 93, 122 Vererbung 109 Kapselung 115 Liskov 112 von Implementierung 111 von Konzepten 111 Verhaltensmuster 189 Befehl (Command) 189 Beobachter (Observer) 198 Iterator (Iterator) 202 Schablonenmethode (Template
Method) 204 Zuständigkeitskette (Chain of Responsibility) 206 Verifikation 277 Vorgehensmodelle 15, 291, 304 Fähigkeiten vs. Formalien 311 Flash-/Flex-Entwicklung 305 Kapazität 311 Komplexität 309 Konventionen 304 Nebenläufig 307 Prinzipien 309 Sequenziell 307 Striktheit 310
W>>> Wartbarkeit 21, 67 Wasserfallmodell 307 Whitebox Inspektion 278 Klassenstrukturpakete 231 Wiederverwendbarkeit 75 Wiederverwendung 297 Vererbung 111 Wireframe 49
Z>>> Ziele 66 Zugriffsrechte 89 internal 90 Namespaces 92 private 89 protected 89 public 89 Zusammenhalt im Team 303 Zuständigkeitskette (Chain of Responsibility) Alternative 209 Zuverlässigkeit 21
335