This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Liebe Leserin, lieber Leser, wir freuen uns, dass Sie sich für ein Buch der Reihe Galileo Computing entschieden haben. Galileo Computing bietet Titel zu allen wichtigen Sprachen, Tools und Techniken der Programmierung. Die Bücher der Reihe zeigen, wie es wirklich geht und warum – immer mit Blick auf die praktische Anwendung. Galileo Computing ist Fachliteratur für Experten von Experten. Kompakt in der Darstellung und benutzerfreundlich gestaltet. Wo wir Übersetzungen in unser Programm aufnehmen, garantieren wir deren herausragenden Informationswert. Hohe Qualität der Übertragung ist selbstverständlich. Jedes unserer Bücher will Sie überzeugen. Damit uns das immer wieder neu gelingt, sind wir auf Ihre Rückmeldung angewiesen. Bitte teilen Sie uns Ihre Meinung zu diesem Buch mit. Ihre kritischen und freundlichen Anregungen, Ihre Wünsche und Ideen werden uns weiterhelfen. Wir freuen uns auf den Dialog mit Ihnen. Ihr Galileo-Team Galileo Press Rheinaustraße 134 53225 Bonn [email protected]
Christian Gross
Windows DNA E-Business-Applikationen mit Windows 2000, COM+, Visual Studio
Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei der Deutschen Bibliothek erhältlich ISBN 3-934358-75-6
Das vorliegende Werk ist in all seinen Teilen urheberrechtlich geschützt. Alle Rechte vorbehalten, insbesondere das Recht der Übersetzung, des Vortrags, der Reproduktion, der Vervielfältigung auf fotomechanischem oder anderen Wegen und der Speicherung in elektronischen Medien. Ungeachtet der Sorgfalt, die auf die Erstellung von Text, Abbildungen und Programmen verwendet wurde, können weder Verlag noch Autor, Herausgeber oder Übersetzer für mögliche Fehler und deren Folgen eine juristische Verantwortung oder irgendeine Haftung übernehmen. Die in diesem Werk wiedergegebenen Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. können auch ohne besondere Kennzeichnung Marken sein und als solche den gesetzlichen Bestimmungen unterliegen.
Widmung Meiner Frau: Isabelle, es hat lange gedauert, aber hier ist es nun endlich. Meinem Vater: Es tut weh, dass du dieses Buch niemals sehen wirst, aber im Laufe der Zeit verstehe ich, dass Erfahrung und Demut den Schlüssel zum Verständnis darstellen.
Inhalt Danksagung 17 1
Einführung in Windows DNA 19
1.1 1.1.1 1.1.2 1.1.3 1.1.4
Die Wichtigkeit des Kontextes 20 DOS – Die erste Ära 20 Windows 3.x – Die zweite Ära 21 32-Bit-Windows – Die dritte Ära 21 Windows DNA – Die vierte Ära 22
1.2 1.2.1
Die Windows DNA-Strategie 23 Die Grundlagen von Windows DNA
1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5
Die Windows DNA-Architektur 24 COM überall 25 Darstellungsschicht 27 Firewall 30 Schicht der Anwendungslogik 31 Datenschicht 37
1.4 1.4.1 1.4.2 1.4.3
Schreiben von Anwendungen mit Hilfe von Visual Studio Der Kontext einer willkürlichen Sprachauswahl 39 Programmiersprachen in Visual Studio 41 Funktionen der Enterprise Edition 44
1.5
Resümee
2
Alles über Muster
2.1
Einführung in Muster
2.2 2.2.1
Ein Beispiel für ein einfaches Muster Wo Muster versagen 48
2.3 2.3.1 2.3.2 2.3.3 2.3.4
Grundlegendes zu Mustern 49 Definieren eines guten Musters 49 Arten von Mustern 50 Vorlage zum Beschreiben von Mustern 51 Muster und deren Beziehung zu anderen Konzepte
2.4 2.4.1
Antimuster 54 Warum treten Antimuster auf?
2.5
Resümee
3
Entwickeln von Anwendungen 57
3.1 3.1.1 3.1.2 3.1.3 3.1.4
Entwickeln von Anwendungen 57 Der Anwendungsentwicklungszyklus 57 Das Wasserfallmodell 58 Gefahren bei der Implementierung einer Anwendung Die Lösung – Der iterative, kontextuelle Entwurf 61
24
39
45
47 47 48
n53
54
55
60
Inhalt
7
8
3.2 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5
Starten eines Projekts 68 Framework: ja oder nein? 68 Einige Hintergrundinformationen 68 Anwendungsanforderungen 71 Definieren der Anwendung 75 Definieren der Modelldetails 83
3.3
Resümee
4
Entwickeln eines Prototyps 89
4.1 4.1.1 4.1.2 4.1.3 4.1.4
Komponenten 89 Ein Stück Komponentengeschichte 89 Komponenten – Das Entwurfskonzept 93 Schnittstellen und Implementierungen 96 Einige Komponentenbeispiele 103
4.2 4.2.1 4.2.2 4.2.3 4.2.4
Entwickeln von Prototypen 104 Was kennzeichnet einen guten Prototypen? Zu implementierende Funktionen 105 Festlegen der ersten Codezeile 106 Codierungsstandards 107
4.3
Resümee
5
Erstellen des Thin Client
5.1 5.1.1 5.1.2
Die Webschnittstelle 113 Navigation 114 Websiteaktivitäten 116
5.2 5.2.1 5.2.2 5.2.3 5.2.4
Die Benutzerschnittstelle des Thin Client Definition von HTML 4.0 118 Webzugänglichkeit 119 Die erste HTML-Seit e119 Füllen der Webseite mit Inhalt 120
5.3 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7
Verarbeiten von HTML-Seiten für den Thin Client Überblick über ASP 123 Ein kurzes Skriptbeispiel125 ASP-Anwendungen 127 Verarbeiten von HTML-Formularen 132 Der CGI-Aufruf 134 Die Serverseite 135 Datenvalidierung 137
5.4
Resümee
Inhalt
87
104
111
139
113
117
123
6
Erstellen eines Rich Client
141
6.1 6.1.1 6.1.2
Warum ein HTML-Client? 141 Die Einfachheit von HTML 141 Die Dynamik von HTML 142
6.2 6.2.1 6.2.2 6.2.3
Strukturierung des Dokuments Die Elemente einer Seit e143 Dokumentlayout 144 Datenverständnis 152
6.3 6.3.1 6.3.2
Der Browsingclient 154 Einführung in XML 154 XSL 159
6.4 6.4.1 6.4.2 6.4.3
Der Workhorseclient 166 Arbeiten mit dem DOM-Modell Handhabung der Maus 171 Schreiben von Skriptcode 175
6.5
Resümee
7
Entwickeln einer Webanwendung 179
7.1 7.1.1 7.1.2 7.1.3
Erstellen der serverseitigen Anwendungslogik Cookies, ja oder nein? 179 Verschieben der Daten 180 Noch etwas zu ASP 180
7.2 7.2.1 7.2.2 7.2.3 7.2.4 7.2.5
Entwickeln der Webanwendung 181 Ermitteln der Browserfähigkeiten 182 Zurückgeben der richtigen Webseit e185 Sprachenunterstützung 189 Erstellen des Hauptteils 190 Die Skriptbibliothek von Visual InterDev 193
7.3 7.3.1 7.3.2
Umsetzung 204 Wo beginnen ?204 Entwerfen und Implementieren der einzelnen Abschnitte
7.4
Resümee
8
Entwerfen von COM-Schnittstellen 215
8.1 8.1.1
Schnittstellen 215 Entwickeln einer Architektur
8.2 8.2.1 8.2.2 8.2.3 8.2.4
Entwickeln einer COM-Komponente Funktionsweise von COM 218 IDL und seine Funktion 219 Ein COM-Paket 222 Die COM-Schnittstelle 224
143
167
178
179
205
213
215 218
Inhalt
9
10
8.3 8.3.1 8.3.2 8.3.3
Einige Entwurfsmethoden für die COM-Schnittstelle 229 Schnittstellen sind unveränderlich 229 Wann muss eine Schnittstelle erweitert werden? 232 Verwenden einer Programmiersprache zur Entwicklung von COMSchnittstellen 234
8.4
Resümee
9
Implementieren von COM-Schnittstellen 249
9.1 9.1.1
Der Vorgang der Komponentenimplementierung 249 Trennen von Datensammlung, Datenoperationen und Datenprüfung
9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.2.6
Ein Beispielsatz Schnittstellen 252 Erweitern der Schnittstellen mit mehreren Daten- und Operationsklassen Registrieren der Typbibliothek 255 Implementieren der Schnittstellen mit Visual Basic 255 Implementieren der Schnittstellen mit Visual J++ 258 Implementieren der Schnittstellen mit Visual C++ 261 Einige abschließende Anmerkungen 263
9.3 9.3.1 9.3.2
Schreiben von Implementierungen 263 Verwenden von UML 264 Schreiben von Komponenten mit Hilfe von UML
9.4 9.4.1 9.4.2 9.4.3
Verwenden der COM-Klassen 269 Einsatz von COM-Klassen mit Visual Basic 269 Einsatz von COM-Klassen mit Visual J++ 272 Einsatz von COM-Klassen mit Visual C++ 273
9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5
Fehlerbehandlung 280 COM-Fehler 281 Generieren von Fehlern 282 Fehlerbehandlung in Visual Basic 285 Fehlerbehandlung in Visual J++ 287 Fehlerbehandlung in Visual C++ 287
9.6
Resümee
10
Entwickeln von Transaktionskomponenten
10.1 10.1.1 10.1.2 10.1.3 10.1.4
Einführung in Transaktionen 291 ACID: Die vier Gebote der Transaktionsverarbeitung Arten von Transaktionen 295 Zweiphasiger Commit-Vorgang 297 COM+-Anwendungen 297
Schreiben von Transaktionskomponenten 309 Deklarieren eines COM+-Transaktionsobjekts 310 Mehrere Szenarien mit COM+-Objekttransaktionen Abrufen der Transaktionsschnittstelle 318 Ändern der Transaktionsergebnisse 319
10.4
Konvertieren von COM-Objekten in COM+-Objekte
10.5
Resümee
11
Entwickeln von Messaging-COM+-Objekten 325
11.1 11.1.1 11.1.2 11.1.3 11.1.4
Einführung in MSMQ 325 MSMQ-API und die MSMQ-ActiveX-Komponent e326 Vergleich von DCOM und MSMQ 326 Experimentieren mit MSMQ unter Verwendung des API-Testbeispiels Nachrichtenattribute 336
11.2 11.2.1 11.2.2 11.2.3
Schreiben einer Messaginganwendung 344 Eine Lösung für das Messaging 344 Schreiben einer Implementierung 346 Beispiel für eine verteilte Anwendung 355
11.3
Resümee
12
Asynchrone COM+-Dienste 367
12.1 12.1.1 12.1.2 12.1.3 12.1.4 12.1.5
In der Warteschlange befindliche COM+-Komponenten 367 Funktionsweise von in der Warteschlange befindlichen Komponenten 368 Implementieren einer warteschlangenfähigen COM+-Komponente 369 Instanziieren einer warteschlangenfähigen COM+-Komponente 374 Falls etwas schief geht 376 Erhalten einer Antwort 379
12.2 12.2.1 12.2.2 12.2.3 12.2.4
COM+-Ereignisdienste 379 Erstellen einer Verleger/Abonnent-Anwendung 380 Filtern von Ereignissen 388 Definieren eines vorübergehenden Abonnenten 395 Transaktionen und COM+-Ereignisse 397
12.3
Resümee
13
Weitere Informationen zur COM+-Dienstprogrammierung
13.1 13.1.1 13.1.2
Skalierbarkeit und Verwaltung der COM+-Lebensdauer Implementieren des Objektpoolings 399 Dynamischer Lastenausgleich 407
13.2 13.2.1 13.2.2 13.2.3 13.2.4
Weitere Schnittstellen zur Transaktionsverwaltung 408 IContextState 408 IObjectContextInfo 408 Festlegen von Erstellungsparametern 411 Erstellen eines Transaktionsstreams von Objekten 413
316
323
323
329
365
397
399
399
Inhalt
11
12
13.3 13.3.1 13.3.2 13.3.3 13.3.4
Entwicklung von ASP-Komponentenobjekten Eine einfache ASP-Seite 416 Integrieren eines COM+-Objekts 416 Ein ASP-COM+-Objekt 418 ASP-Seiten mit Transaktionen 423
13.4
Resümee
14
Erstellen des Hybridclients
14.1 14.1.1 14.1.2
Definieren des Hybridclients 427 Die Architektur des Hybridclients 428 Anwendungslogik auf dem Client 430
14.2 14.2.1 14.2.2
Entwickeln eines Unternehmensdesktop Erstellen der Shell 432 Erstellen des Inhalts 447
14.3
Resümee
15
Erstellen einer Ressource 467
15.1 15.1.1 15.1.2
SQL und Portabilität 467 Gründe für und gegen ein objektorientiertes Datenbankverwaltungssystem (OODBMS) 468 Die zu wählende Version von Microsoft SQL Server
15.2 15.2.1 15.2.2 15.2.3 15.2.4
SQL-Grundlagen 469 Tabellen 469 Gründe für die Verwendung gespeicherter Prozeduren Erstellen einfacher gespeicherter Prozeduren 481 Einfache Tabellenbearbeitung mit SQL 484
Weitere SQL-Techniken 487 Arbeiten mit Variablen 487 Suchen mit Hilfe von Datumsangaben 488 Suchen mit Hilfe von Platzhalterzeichen 489 Erweiterte Datenänderung 490 Programmieren in Transact SQL 494 Temporäre Informationen und Cursor 499 Zugreifen auf Systeminformationen 501 Erstellen und Verwenden von Sichten 501 Arbeiten mit Verknüpfungen 503
15.4
Resümee
16
Datenzugriff 511
16.1 16.1.1
Universeller Datenzugriff 511 Gründe für die Verwendung von UDA
16.2 16.2.1 16.2.2
Möglichkeiten des Datenzugriffs 514 Reine Leseoperationen 514 Operationen zur Datenbearbeitung 515
Inhalt
415
425
427
432
466
509
513
468
478
16.3 16.3.1 16.3.2 16.3.3
Einführung in ADO 516 Die Architektur von ADO 517 Ausführen von Verarbeitungsdatenoperationen Durchführen reiner Leseoperationen 525
16.4 16.4.1 16.4.2
»Record«- und »Stream«-Objekte Einträge 534 Streams 538
16.5 16.5.1 16.5.2 16.5.3 16.5.4 16.5.5 16.5.6
Datenshaping mit ADO 540 Verwenden des Providers für das Datenshaping 541 Programmieren eines SHAPE-Befehls 542 Durchsuchen der Daten 542 Ausführen von Berechnungen 543 Einbetten mehrerer SHAPE-Anweisungen 544 Schreiben komplexerer SHAPE-Befehle 546
16.6
Resümee
17
Optimieren der Datenzugriffsschicht
17.1 17.1.1 17.1.2 17.1.3
Klassen der OLE DB-Consumer Templates Verbindungsklassen 551 Ausführungsklassen 551 Ansichtsklassen 551
17.2 17.2.1 17.2.2 17.2.3
Beispiel für ein OLE DB-Consumer Template Erstellen der Templateklasse 552 Öffnen der Tabelle 554 Testen der Klassen 557
17.3 17.3.1 17.3.2 17.3.3
Aufrufen gespeicherter Prozeduren 559 Ausführen des Befehls 561 Verwenden von Mehrfachzugriffsroutinen 561 Weitere Informationen zu COLUMN_ENTRY 565
518
534
546
549 549
552
17.4
Arbeiten mit BLOB-Daten
17.5
Massenabruf von Datensätzen
570
17.6
Resümee
18
Verzeichnisdienste
18.1 18.1.1 18.1.2
Architektur von Microsoft Active Directory 575 Active Directory-Objekte 576 Ein praktisches Active Directory-Beispiel 580
18.2
Zugreifen auf den Benutzer mit ADSI 583
18.3 18.3.1
Erstellen benutzerdefinierter Objekte 589 Arbeiten mit einem benutzerdefinierten Active Directory-Objekt
572
572
575
594
Inhalt
13
14
18.4 18.4.1 18.4.2 18.4.3
Verwenden von OLE DB und ADO 595 Abfragen mit der LDAP-Notation 595 Abfragen mit der SQL-Notation 597 Verwenden von ADO 597
18.5
Resümee
19
Der Qualitätskontrollprozess 599
19.1 19.1.1 19.1.2
Definieren des Qualitätskontrollprozesses Definieren einer Metrik 600 Protokollieren von Fehlern 603
19.2 19.2.1 19.2.2 19.2.3
Testen der Anwendung 607 Definieren einer Teststrategie 608 Entwickeln eines Frameworks 613 Schreiben eines Treibers 616
19.3 19.3.1 19.3.2 19.3.3
Leistungstests 622 Testen der Hardware 622 Testergebnisse 632 Testen der Anwendung mit Visual Studio Analyzer
19.4
Resümee
20
Erstellen von Diensten 639
20.1 20.1.1 20.1.2
Ausführen eines 24x7-Prozesses Steuern von Diensten 640 Entwickeln von Diensten 642
20.2 20.2.1 20.2.2
Ausführen eines Stapelverarbeitungsprozesses Verwenden des Windows Scripting Host 653 Hinzufügen einer geplanten Aufgabe 655
Danksagung Ich möchte allen Personen danken, die mich bei der Realisierung dieses Buches unterstützt haben. Lon Fisher: Was soll ich sagen. Danke ... Richard Hale Shaw: Richard, danke für dein Verständnis in den Momenten, als alles über mir zusammenbrechen zu schien und du an mich geglaubt hast. Gary Cornell, Dan Appleman: Ihr wart tolle und verständige Verleger. Verleger, die sich von anderen deutlich absetzen, und ich habe einige kennen gelernt. Außerdem Verleger, die mir bis zum Schluss zur Seite standen. Andy Carroll: Ich habe nicht geglaubt, dass Development-Editoren so fantastisch sein können. Super. Mary Kirtland: Die Wunderfrau von Microsoft. Wahrscheinlich die einzige Frau, die tatsächlich alle Technologien kennt, die Microsoft je entwickelt hat. Eines Tages werde ich eine Frage finden, die du nicht beantworten kannst. Mathias Heffner und Arne Steingräber: Danke Jungs, dass ihr nicht gelacht habt, wenn ich zu sagen pflegte: »Vertraut mir, es ist fertig«. Und danke für die großartige Unterstützung. Dem Visual C++-Team und einigen besonderen Teammitgliedern – Chris Hargarten, Jeff Ressler, Dean Rowe, Christian Beaumont, Mike Blaszack: Ich sage nur danke, danke, danke. Ihr habt mich wie ein vollwertiges Teammitglied aufgenommen, und diese Freundschaft werde ich immer zu schätzen wissen. Ihr habt mich zu der Einsicht gebracht, dass es den »Microsoft-Gorilla« gar nicht gibt, sondern dass ihr einfach Spitzenprogrammierer seid, die coole Codes schreiben möchten. Meiner Frau Isabelle: Mein technischer Editor und diejenige, die sich nicht davor scheute, mir die Meinung zu sagen. Die Person, die mir stets zur Seite stand. Und meinen zwei englischen Bulldoggen: Patches und Bossy, die mich durch dick und dünn begleiteten und mir immer wieder ein Lächeln aufs Gesicht zauberten. Der größte Dank gilt meinen Kunden. Ohne ihre Probleme, Lösungen, Erfolge und Teilerfolge wäre ich niemals in der Lage gewesen, dieses Buch zu schreiben.
17
1 Einführung in Windows DNA Wenn Sie dieses Buch lesen, sind Sie wahrscheinlich ein Softwareentwickler, der Informationen zu Windows 2000 und Windows DNA benötigt. Sie suchen nach einer Lösung, die sämtliche Dienste und COMObjekte (Component Object Model) beinhaltet, die für Ihre Arbeit erforderlich sind. Und genau dies werden Sie in diesem Buch finden. Sie werden in die Entwicklung verteilter Windows 2000-Anwendungen eingeführt. Und Sie lernen die Bedeutung von Windows DNA kennen. Windows DNA steht für Windows Distributed Internet Applications Architecture, also die Architektur für verteilte Windows-Internetanwendungen. Die Marketingstrategie von Microsoft hat sich zwar geändert, aber Windows DNA ist weiterhin das, was Microsoft ursprünglich folgendermaßen beschrieben hat: Ein Lösungsansatz, mit dem Unternehmensentwickler und unabhängige Softwarehändler unter Verwendung grundlegender WindowsTechnologien verteilte Unternehmensanwendungen entwerfen und entwickeln können. Dies bedeutet, dass Microsoft momentan und in Zukunft versucht, die besten Ansätze zur Lösungsentwicklung mit den Microsoft-Technologien zu definieren. Der Grund, aus dem Windows DNA- und Windows 2000-Lösungen sich gleichen, ist der, dass Windows 2000 sämtliche Dienste bereitstellt, die in einer vollständigen Windows DNA-Anwendung benötigt werden. Ich werde das DNA-Modell besprechen, darüber hinaus jedoch auch die Verwendung der Technologien erläutern, die am besten zur Lösung spezieller Kundenprobleme geeignet sind. Ich habe über zehn Jahre lang in einer Unternehmensumgebung mit den Microsoft-Technologien gearbeitet, und das Wichtigste, was ich dabei gelernt habe, ist Folgendes: Die Arbeit mit den Microsoft-Technologien kann zu guten Ergebnissen führen. Die Windows DNA, von der ich hier spreche, eignet sich optimal zur Lösung von Kundenproblemen. Viele Konzepte der Windows DNA stehen auch auf anderen Plattformen zur Verfügung, beispielsweise mit Java oder CORBA (Common Object Request Broker Architecture). Ich denke, es wird Ihnen gefallen.
19
1.1 Die Wichtigkeit des Kontextes Wenn ich ein Problem angehe, versuche ich stets, den Kontext des Problems zu erfassen. Mit Kontext ist hierbei nicht nur die gegenwärtige Situation gemeint, sondern auch die Umstände, die zur aktuellen Situation geführt haben. Im Umgang mit vertikalen Anwendungen ist es häufig so, dass Entscheidungen nicht nur aufgrund technischer Aspekte, sondern auch auf der Grundlage persönlicher oder anderer Gründe getroffen werden. Und diese persönlichen Entscheidungen sind schwer einschätzbar. Manchmal sind sie logisch, manchmal jedoch auch nicht. Die Entscheidung sollte auf dem Kontext des Problems basieren. Im Hinblick auf das vorliegende Buch gehört zu diesem Kontext u.a., ein Problem mit Hilfe von Windows DNA zu lösen. Lassen Sie uns zusammen den Kontext erarbeiten, der Windows 2000 zu dem Betriebssystem gemacht hat, das es heute ist. Nehmen Sie folgende Aussage: Windows 2000 ist stabil. Haben Sie gelacht? Ich möchte wetten, dass viele von Ihnen gelacht haben. Wir haben alle die Horrorstories vom BSOD (Blue Screen of Death) von Windows NT gehört. Sie denken vielleicht, dass Windows 2000 aufgrund seiner Ablaufverfolgung niemals stabil sein kann. Wenn Sie sich letztere jedoch genauer ansehen und sich klarmachen, was Microsoft damit erreichen wollte, werden Sie erkennen, warum einige Dinge genau so umgesetzt wurden, wie sie umgesetzt wurden. Und dann werden Sie auch verstehen, wie es zu Windows DNA gekommen ist. Windows und Microsoft gibt es schon einige Jahre. DOS war erfolgreich, aber erst mit Windows 3 kam der Durchbruch für Microsoft. Also lassen Sie uns betrachten, wie Windows zu dem wurde, was es heute ist.
1.1.1 DOS – Die erste Ära Als DOS herausgebracht wurde, war der größte Vorteil die Tatsache, dass ein kostengünstiger Rechner bereitgestellt wurde, der durch eine Einzelperson programmiert werden konnte. Es waren weder Expertenteams noch das umfangreiche Budget großer Unternehmen erforderlich. DOS (Disk Operating System) konnte auf einem Gerät ausgeführt werden, der als Personal Computer (PC) bezeichnet wurde. Keine dieser Innovationen war jedoch wirklich neu. Apple entwickelte einen eigenen PC sowie das zugehörige Betriebssystem, und auch CP/M von Digital war eine Art Festplattenbetriebssystem. Die Neuheit war, dass DOS auf kostengünstiger, generischer Hardware ausgeführt werden konnte, und das DOS selbst ebenfalls nicht teuer war.
20
Einführung in Windows DNA
1.1.2 Windows 3.x – Die zweite Ära Als die erste Ära PCs den Markt eroberte, wurde schnell klar, wo die Probleme lagen. Hardware gab es im Überfluss, generische Software mit den geeigneten Treibern dagegen war rar. Die Computerhersteller verbesserten ständig die Rechnergeschwindigkeit und statteten die PCs mit mehr Festplatten- und Arbeitsspeicher aus, aber um diese Hardwareinnovationen nutzen zu können, benötigten die Anwendungen benutzerdefinierte Treiber. Jede Anwendung hatte eigene Treiber, die in den meisten Fällen nicht mit anderen Anwendungen kompatibel waren. So bestand der Kontext in einer größer werdenden Menge proprietärer Software, die keine ausreichende Kompatibilität mit anderen Produkten aufwies. Die Lösung für dieses Problem war Windows. Windows abstrahierte das Konzept eines Gerätetreibers, sodass das Betriebssystem dieses verstehen konnte. Auf diese Weise konnte der Benutzer Software unabhängiger Hersteller einsetzen. Jeder konnte beliebige Softwarekomponenten einsetzen, immer vorausgesetzt, es war ein entsprechender Windows-Treiber vorhanden. Die Hardwarehersteller brauchten lediglich einen Treiber zu entwickeln, der für alle Anwendungen verwendet werden konnte.
1.1.3 32-Bit-Windows – Die dritte Ära Als die zweite Computer-Ära fortschritt, förderte dies neue Probleme zutage. Die zweite Computer-Ära basierte auf den technologischen Konzepten der ersten Ära. So verwendeten Anwendungen 16-Bit-Prozessorbefehle, es gab kein Multitasking und es gab keinen Anwendungsfehlerschutz. Anwendungsfehler verursachten für einen Anwendungsabstürze, die wiederum häufig zu einem Absturz des gesamten Windows-Betriebssystems führten. Dieses Problem resultierte aus dem Treibermodell, das in der zweiten Ära eingeführt wurde. In der ersten Ära erstellte jeder Entwickler eigene Systeme. Diese übernahmen die vollständige Steuerung von Prozessor, Festplatte und Bildschirmanzeige. In der zweiten Ära mussten die Anwendungen diese Ressourcen gemeinsam nutzen, aber viele Entwickler schrieben keine Anwendungen oder Treiber, die dieses Konzept auch wirklich umsetzten. Selbst Windows legte die Schnittstellen nicht richtig offen, daher führte die gemeinsame Ressourcennutzung zu Fehlern. Das Entwickeln von Systemen für die gemeinsame Nutzung ist schwierig, da es ein gewisses Vertrauen in die anderen Anwendungen und deren ordnungsgemäße Funktion erfordert. Die Antwort auf diesen Kontext der gemeinsamen Ressourcennutzung war die Einführung des Multitasking und der Schutz des Anwendungsbereichs. Der Prozessor gaukelte den Anwendungen vor, dass sie sich weiterhin in der ersten Ära
Die Wichtigkeit des Kontextes
21
befanden und somit die exklusive Steuerung der Umgebung innehatten. Der Prozessor setzte diese gemeinsame Nutzung auf Chipebene um. Diese geschützte gemeinsame Ressourcennutzung machte das Betriebssystem stabil und gleichzeitig schneller und effektiver.
1.1.4 Windows DNA – Die vierte Ära Damit befinden wir uns nun in der vierten Generation. Anwendungen können sicher in einem eigenen, geschützten Bereich ausgeführt werden. Was weiterhin benötigt wird, ist eine Möglichkeit, mit der Anwendungen einen gemeinsamen Knotenpunkt nutzen können, ohne dass dies zu Konflikten führt. Die Lösung findet sich in der gemeinsamen Nutzung von Diensten. Die gemeinsame Nutzung mit anderen Systemkomponenten ist extrem schwierig zu steuern, da bei einem so weit verbreiteten System wie Windows nicht jeder die gleiche Vorstellung davon hat, wie die Dinge funktionieren sollten. Aus diesem Grund entwickelte jeder eine eigene Version der verschiedenen Dienste. Einige dieser Dienste basierten auf Datenbanken. Andere basierten auf dem Web. Wieder andere basierten auf Transaktionen. Und jeder dieser Dienste verfügte über eine eigene Schnittstelle. Sobald ein Benutzer seine gemeinsam genutzten Dienste aktualisierte, führte dies zu Konflikten und Systemabstürzen. Die Benutzer denken häufig, dass Windows instabil ist, weil es schlecht geschrieben ist. Tatsächlich kann Windows jedoch häufig aufgrund der Unmenge an Anwendungen instabil werden, die gemeinsam das Betriebssystem nutzen und hierbei gewisse Kompromisse erforderlich machen. Solange die gemeinsame Nutzung von Komponenten ordnungsgemäß erfolgt, gibt es auch keinerlei Probleme. Zur Lösung dieses Problems sieht die Windows DNA-Strategie u.a. die Offenlegung eines gemeinsamen Satzes an Diensten und Hilfsprogrammen vor, durch die der zuvor genannte Knotenpunkt implementiert wird. Diesen Knotenpunkt stellen die Kerndienste dar, beispielsweise Transaktions- und Nachrichtendienste sowie weitere Komponenten, die in verschiedenen Anwendungen wieder verwendet werden können. So wird das Konzept der gemeinsamen Nutzung vereinfacht, denn es ist nur ein Dienstesatz vorhanden. Das Ziel von Windows DNA ist die Bereitstellung einer generischen Lösung mit den folgenden Leistungsmerkmalen: 왘 Interaktion mit dem Benutzer über Tastatur, Bildschirm oder Maus 왘 Mögliche Verwendung andere Eingabegeräte, beispielsweise Touchscreen, Pen
oder Sprachaktivierung 왘 Verbindung zu weiteren Rechnern mit Hilfe von Netzwerkkonzepten
22
Einführung in Windows DNA
왘 Zuverlässige und konsistente Datenfreigabe für mehrere Computer 왘 Eigene Verwaltung sowie einfache Umgebungsverwaltung
Diese Art Lösung erfordert verschiedene Voraussetzungen, die aus der Ära des Windows-APIs (Application Programming Interface) stammen. Bis vor einiger Zeit konzentrierten sich die Hersteller auf ihre eigenen, spezifischen Anwendungen. Dieser allgemeine Trend hat sich jedoch gewandelt, da der Computer zu einem Teil des täglichen Lebens geworden ist und nicht mehr nur Aufgaben lösen, sondern auch mit anderen Geräten und Umgebungen interagieren muss. Der Verbraucher möchte, dass sein Computer genauso einfach zu bedienen ist wie beispielsweise der Toaster oder das Radio.
1.2 Die Windows DNA-Strategie In Anlehnung an die Microsoft-Literatur kann die Windows DNA folgendermaßen definiert werden: Die Kombination aus einem Betriebssystem mit konsistentem Satz wieder verwendbarer Dienste und einem logischen Entwurfsprozess zur Entwicklung von Anwendungen Diese Definition umfasst drei wichtige Aspekte. Der erste Aspekt ist das Betriebssystem. Das Betriebssystem ist ein fundamentaler Aspekt des Computersystems, da mit diesem die Grundlage für das gesamte Framework bereitgestellt wird. Dieses Betriebssystem sollte schnell, stabil und skalierbar sein. Der zweite Aspekt ist der, dass das Betriebssystem über eine Reihe von Diensten und Hilfsprogrammen verfügt. Diese Dienste ermöglichen die Ausführung bestimmter Aufgaben, beispielsweise Transaktionsverarbeitung oder Messaging. Und als drittes sollten Betriebssystem und Dienste so miteinander kombiniert werden, dass Anwendungen logisch entwickelt werden können und gleichzeitig den größten Nutzen aus den Diensten und Hilfsprogrammen ziehen. Eine Windows DNA-Lösung wird typischerweise von Organisationen eingesetzt, die Anwendungen zur Lösung spezieller Unternehmensprobleme bereitstellen. Microsoft stellt den Knotenpunkt bereits zur Verfügung. Die Größe des Unternehmens, die diese Lösung einsetzt, spielt keine Rolle – kleine und große Unternehmen haben häufig die gleichen Probleme, diese unterscheiden sich lediglich in der Dimension.
Die Windows DNA-Strategie
23
1.2.1 Die Grundlagen von Window sDNA Windows DNA weist die folgenden Hauptmerkmale auf: 왘 Internetfähigkeit: Internettechnologien, wie beispielsweise TCP/IP, HTTP, FTP
usw., sind in das Betriebssystem integriert. Auf diese Weise können Anwendungen die Internetfähigkeit voraussetzen. 왘 Kurze Entwicklungszeiten: Integrierte Dienste und Hilfsprogramme ermögli-
chen dem Entwickler, sich auf das Schreiben der Anwendung statt auf die Bereitstellung des bereits genannten »Knotenpunktes« zu konzentrieren, wodurch die Entwicklungszeiten erheblich verkürzt werden. 왘 Interoperabilität: Offene Protokolle und Standards ermöglichen die leichte
Kommunikation und Interaktion von Anwendungen auf verschiedenen Plattformen, sodass eine Lösung mit Produkten verschiedener Hersteller eingesetzt werden kann. 왘 Geringere Komplexität: Gemeinsam genutzte Dienste und Hilfsprogramme
sind in das Betriebssystem integriert, neue Versionen werden mit den Betriebssystemaktualisierungen bereitgestellt. Auf diese Weise wird vermieden, dass durch die Setup-Programme der verschiedenen Anwendungen inkompatible Versionen derselben Dienste installiert werden. Gleichzeitig erleichtert dies den Installationsvorgang für Betriebssystem und Programme. 왘 Sprachunabhängigkeit: Eine Stärke der Windows-Plattform ist die Möglich-
keit, dass Entwickler von Fremdanbietern die bevorzugte Entwicklungsumgebung verwenden können. Dies bedeutet, dass Entwickler die Programmiersprache wählen können, die zur Lösung des gegebenen Problems eingesetzt wird. 왘 Senkung der Gesamtbetriebskosten: Die Anwendung sollte einfach bereitge-
stellt, verwaltet und erneut verwendet werden können. Dies kann durch eine Vereinfachung der Installation und Prozesse zur Versionssteuerung erreicht werden.
1.3 Die Windows DNA-Architektur Windows DNA verfügt über eine Schichtenarchitektur. Eine Schicht ist eine logische Abtrennung einer funktionellen Komponente von den weiteren funktionellen Bestandteilen. Die Schicht kann ein abstraktes Konzept darstellen oder die tatsächlichen Komponenten eines Projekts reflektieren. Abbildung 1.1 zeigt die Windows DNA-Architektur, die zur Entwicklung mehrschichtiger Anwendungen eingesetzt wird. Es sind drei Schichten vorhanden, die Darstellungsschicht, die Schicht mit der Anwendungslogik und die Datenschicht. Zwischen der Darstellungsschicht und der Schicht der Anwendungslogik befindet
24
Einführung in Windows DNA
sich eine Firewall. Diese Architektur stellt keine physische Architektur, sondern ein abstraktes Konzept dar. Innerhalb einer der abstrakten Schichten können mehrere physische Schichten enthalten sein.
Abbildung 1.1 Die Windows DNA-Architektur
Damit die gesamte Architektur einer Windows DNA-Anwendung funktioniert, müssen die verschiedenen Schichten, aus denen sich die Anwendung zusammensetzt, miteinander kommunizieren können, ohne die individuellen Implementierungsdetails zu kennen. Diese Kommunikation findet anhand der COM-Technologie statt.
1.3.1 COM überall Die Grundlage von COM ist ein binärer Standard, über den zwei Codeabschnitte Informationen austauschen können. Abbildung 1.2 zeigt eine so genannte VTable (Virtual Functions Table). Die VTable umfasst eine Reihe von Funktionszeigern auf Codeimplementierungen, die VTable-Definition kann auf einer beliebigen Plattform vorliegen, da sie plattformunabhängig generiert wird. Da die VTable generisch ist, wird sie als Schnittstelle bezeichnet. Durch diese Tabelle werden spezifische Intentionen definiert; anschließend verweist die Schnittstelle auf Implementierungen, mit deren Hilfe die beabsichtigten Aktionen umgesetzt werden.
Die Windows DNA-Architektur
25
Abbildung 1.2 COM-Schnittstelle und Implementierungsverfahren
Die Implementierung erfolgte traditionell binär und plattformspezifisch, mit Visual J++ wird diese Einschränkung jedoch aufgehoben. Visual J++ erzeugt JavaCode, der auf einer beliebigen Plattform ausgeführt werden kann. Die einzige Bedingung ist, dass die COM-Laufzeitumgebung auf der Zielplattform vorhanden sein muss. Bei der Entwicklung von Windows DNA-Anwendungen untersuchen Sie zunächst den Problemkontext. Anschließend formulieren Sie die für die Anwendung zu erreichenden Ziele. Mit anderen Worten, Sie definieren eine Reihe von Schnittstellen. Die Implementierungen können die angestrebten Ziele einer oder mehrerer Kontexte umsetzen. Mit Hilfe von COM wird abgefragt, ob eine Implementierung ein zu erreichendes Ziel in einem spezifischen Kontext unterstützt. Windows 2000 im Kontext von Windows DNA erfüllt den gleichen Zweck. Das Betriebssystem legt eine Reihe von Diensten, z.B. Sicherheit, Transaktionsverarbeitung, Messaging und Hardwareunterstützung, als einen Satz COM-Objekte offen. Diese Tatsache steht im Gegensatz zu früheren Windows NT-Versionen, bei denen die Mehrzahl der Dienste als APIs offen gelegt wurde. Warum also COM? Neben technischen Gründen stellt COM die derzeit umfangreichste Komponentenstrategie dar. Durch COM erhält Windows DNA mehr Kompatibilität. Bei Verwendung von COM können Softwarekomponenten entwickelt werden, die auf einer beliebigen Schicht ausgeführt und bereitgestellt werden können. Die COM-Laufzeitumgebung bietet Unterstützung für das Packen und Partitionieren sowie für weitere Aspekte mehrschichtiger Anwendungen.
26
Einführung in Windows DNA
1.3.2 Darstellungsschicht Die Darstellungsschicht ist die Schicht, in der die Anwendungen mit dem Benutzer interagieren. Dieser Schritt erfolgt, um die Daten in einer Form zu präsentieren, die von uns verstanden werden kann. Aktuell basiert die Darstellungsschicht weitestgehend auf Tastatur, Bildschirm und Maus, in Zukunft muss sich dies jedoch ändern, um beispielsweise der Kommunikation per Spracheingabe oder über Bewegungen gerecht zu werden. In der aktuellen Version von Windows DNA liegt das Hauptziel der Darstellungsschicht darin, überall und jederzeit jede Art von Zugriff zu bieten. Dies wird durch die Internetintegration erreicht, einschließlich Browser, Einwahl- und Netzwerkkomponenten. Der Entwurf dieser Schicht stellt eine Herausforderung dar, da die verschiedenen Clienttypen sich stark voneinander unterscheiden. In der Windows DNA-Architektur ist der Client nicht notwendigerweise Windows-basiert, kann jedoch UNIX-, Mac OS- und weitere Betriebssysteme umfassen. Zur Unterstützung dieser verschiedenen Plattformen reicht die Darstellungsschicht von einem Rich Client bis zu einem Thin Client. Sie dürfen sich einen solchen Thin Client jedoch nicht wie im Rahmen der Downloadmenge in Byte vorstellen. Ein Thin Client kann mehrere Hundert Megabyte umfassen. Der Unterschied besteht im Funktionsumfang, der auf der Clientseite ausgeführt werden kann.
Abbildung 1.3 Die Darstellungsschicht im Detail
Die Windows DNA-Architektur
27
Abbildung 1.3 ist eine Abstraktion der Art und Weise, mit der die Darstellungsschicht erstellt wird. Auf der rechten Seite, dargestellt durch die Zahnräder, befinden sich die verschiedenen Technologien. Unter Verwendung dieser Technologien können zwei Arten von Clients erstellt werden, EXE-basierte und webseitenbasierte Clients. Diese Clienttypen können in vier weitere Typen unterteilt werden, die vom Thin Client bis zum Rich Client reichen. Webseitenbasierte Lösungen Der Thin Client ist eine Form des webseitenbasierten Clients. Dies bedeutet, dass der Clientcomputer an das Netzwerk angeschlossen ist und nach Bedarf Inhalte herunterlädt. Die Clientanwendung dient lediglich der Anzeige und dem Abruf von Informationen. Beim Herunterladen spezieller Inhalte werden diese als statische Informationen angezeigt. Sie können anschließend in einigen Textfeldern Eingaben vornehmen oder ein paar Kontrollkästchen aktivieren und die Informationen zur Verarbeitung weiterleiten. Dieser Typ Funktionalität ähnelt der Stapelverarbeitung in Mainframesystemen. Der einzige Unterschied besteht darin, dass sich das Terminal an beliebiger Stelle im Internet befinden kann. Typischerweise handelt es sich bei diesem Clienttyp um einen Webbrowser, und diese Art Anwendung wird als browserneutrale Anwendung bezeichnet. Ein statischer Webbrowseransatz stellt sicher, dass Ihre Anwendung die größtmögliche Unterstützung aufweist. Die Funktionalität auf Clientseite wird durch die Leistungsmerkmale der Laufzeitumgebung auf dem Clientcomputer bestimmt. Es gibt einfache Webbrowser, und es gibt leistungsstärkere Webbrowser. Der Lösungsansatz ist jedoch derselbe, denn in beiden Fällen wird die Anwendung heruntergeladen. In Abbildung 1.3 wird der erste Rich Client gezeigt, der browsererweiterte Client. Der Unterschied zwischen diesem Client und dem browserneutralen Client besteht darin, dass ein Teil der Verarbeitung auf dem Clientcomputer stattfindet. Das Verwenden eines browsererweiterten Clients macht die Anwendung interaktiv, der Nachteil hierbei ist jedoch, dass weniger Webbrowser den Inhalt decodieren können. Es ist beispielsweise möglich, Inhalte zu entwickeln, die nur mit dem Internet Explorer oder ausschließlich mit dem Netscape Navigator verwendet werden können. Übliche Anwendungen sind webbrowserbasierte Intranet- oder Extranetanwendungen. In diesen Fällen ist der verwendete Browser häufig bekannt. EXE-basierte Lösungen Der zweite Lösungstyp erfordert mehr Funktionalität auf der Clientseite; es ist die Implementierung einer Fat Client-Anwendung erforderlich. Diese Clients basie-
28
Einführung in Windows DNA
ren auf ausführbaren Anwendungen, die auf dem Clientcomputer installiert und ausgeführt werden, daher können diese als Rich Clients bezeichnet werden. Die erste Form des Rich Client ist eine internetbasierte Anwendung, die aus Gründen der Funktionalität das zugrunde liegende Betriebssystem und aus Gründen des Inhalts das Netzwerk miteinander kombiniert. Ein Beispiel für diesen Anwendungstyp ist eine Data Warehouse-Anwendung. Beim Sichten eines Data Warehouses erzeugen Sie üblicherweise komplexe Pivottabellen und benötigen einen Server zur Durchführung einiger Aufgaben, da das Verschieben aller Daten auf den Client nicht möglich ist. Zur Anzeige der Ergebnisse in einem Diagramm benötigen Sie jedoch einen Client, der ein grafisches Teilsystem umfasst. Daher benötigt die Anwendung ein dediziertes Netzwerk und umfangreiche Anzeigefunktionen. Der vierte Typ Rich Client ist eine interneterweiterte Anwendung. Dieser Clienttyp ähnelt dem vorherigen, stellt jedoch immer dann eine Netzwerkverbindung her, wenn Informationen gemeinsam genutzt werden müssen. Nehmen Sie beispielsweise ein Textverarbeitungsprogramm. Die meisten Funktionen, die Sie benötigen, können auf der Clientseite ausgeführt werden. Das Netzwerk spielt sekundär ebenfalls eine Rolle, da über dieses Dokumente mit anderen Textverarbeitungsverwendern genutzt werden können. Dieser Clienttyp bringt auch die neue Hybridclientanwendung hervor, bei der bei Bedarf das Internet genutzt wird, dies jedoch unsichtbar für den Benutzer. Ein Beispiel für diese Art Anwendung ist Microsoft Money. Diese Anwendung wird auf dem Clientcomputer installiert und hauptsächlich isoliert ausgeführt. Wenn Sie jedoch Ihre Kontoauszüge anzeigen möchten, müssen Sie online gehen. Hierbei werden Sie über einen eingebetteten Browser direkt mit Ihrer Bank verbunden. Anschließend werden Ihre Kontoauszüge abgerufen und angezeigt. Während dieses gesamten Vorgangs muss der Benutzer weder eine URL (Uniform Resource Locator) noch zusätzliche Netzwerkbefehle zur Verbindungsherstellung zum Internet eingeben. Mit anderen Worten, der Hybridclient stellt eine vollständig integrierte Form der Internetnutzung dar. Die Technologien Zum Verständnis der einzelnen Clienttypen müssen die verwendeten Kerntechnologien umrissen werden: 왘 HTML: Abkürzung für Hypertext Markup Language, der Websprache für Be-
nutzerschnittstellen. Der Begriff HTML bezieht sich hier üblicherweise auf Version 3.2 oder Version 4.0 der HTML-Spezifikationen. Bei beiden Versionen handelt es sich um plain-vanilla HTML, durch das die formulargestützte Verar-
Die Windows DNA-Architektur
29
beitung unterstützt wird. Von allen Technologie verfügt diese über die größte Bandbreite und wird in der Regel für den Thin Client verwendet. 왘 DHTML: Dynamic HTML ist die erweiterte Form der Benutzerschnittstelle für
Webbrowser. DHTML unterscheidet sich von HTML in der Unterstützung eines vollständigen Objektmodells. Jeder Bestandteil der Benutzerschnittstelle kann dynamisch adressiert, entfernt oder hinzugefügt werden. Auf diese Weise wird die Schnittstelle zu einer äußerst leistungsstarken Komponente. Diese Funktion wird jedoch nur von wenigen Browsern unterstützt. 왘 Scripting: Innerhalb des DHTML-Objektmodells wird eine Scriptingschnitt-
stelle definiert. Das Scripting wird als separate Technologie betrachtet, da mit dieser Komponente jede Art von Client gesteuert werden kann. Beispielsweise kann mit Hilfe von Skripts Microsoft Excel zur Durchführung bestimmter Aufgaben angewiesen werden. Der Hauptvorteil der Skriptverwendung ist der, dass eine Anwendung angepasst werden kann, ohne dass hierzu umfangreiche Programmierkenntnisse erforderlich sind. 왘 Komponenten: Komponenten sind kompilierte Codeabschnitte, über die
Funktionalität bereitgestellt wird. DHTML und Scriptingdienste stellen beispielsweise Komponenten dar. Unter Verwendung von COM können derartige Umgebungen miteinander kommunizieren. 왘 Win32: Das Win32-API stellt eine Möglichkeit zur Interaktion mit dem Win-
dows-Betriebssystem dar. Diese Technologie eignet sich für Clients, die besonderen grafischen oder höheren Leistungsanforderungen gerecht werden müssen. Die Win32-API ist Windows-spezifisch. Es steht für HTML oder DHTML kein vergleichbares Programmiermodell zur Verfügung.
1.3.3 Firewall Bei der Firewall handelt es sich zwar nicht um eine Schicht, dennoch muss sie erwähnt werden. Die Firewall befindet sich zwischen der Darstellungsschicht und der Schicht der Anwendungslogik. Eine Firewall bezeichnet eine Sicherheitsbarriere, mit der die Daten innerhalb der Firewall geschützt werden. In einer allgemeinen Architektur ist eine Firewall nur ein Dienst innerhalb des gesamten Frameworks, dies ist bei Windows DNA jedoch nicht der Fall. Windows DNA unterscheidet sich von der Mehrzahl der Betriebssysteme dadurch, dass die Sicherheit in die Schicht der Anwendungslogik verlagert wird. Dies bringt verschiedene Vorteile mit sich. Zunächst wird eine konsistente Sicherheitsprüfung etabliert. Stellen Sie sich Ihr privates Netzwerk als eine Burg vor. Bei den derzeit verwendeten Sicherheitsmechanismen könnte ein Reiter anonym in Ihre Burg gelangen. Anschließend, abhängig von den angeforderten Diensten, fin-
30
Einführung in Windows DNA
det eine Sicherheitsprüfung statt. Das Problem hierbei ist, dass sich der anonyme Reiter auf seinem Weg durch die Burg wiederholt einer ID-Prüfung unterziehen muss. Außerdem könnte ein Reiter mit bösen Absichten Schaden anrichten, wenn ein Dienst nicht ausreichend geschützt wurde. Darüber hinaus ist es nicht einfach, eine konsistente Sicherheitsrichtlinie aufrechtzuerhalten, wenn jeder Dienst eine eigene Sicherheitsprüfung durchführt. Sobald der Reiter aus einer Bar in der Burg hinausgeworfen wird, sollten auch alle weiteren Bars für diesen Reiter geschlossen bleiben. Ohne eine zentrale Sicherheitsautorität ist dies nicht zu erreichen. Durch das Errichten einer Firewall einer zentralen Sicherheitsautorität ist jeder Zugriff auf jeden Dienst bekannt. Den Nutzern werden spezifische Rechte und Privilegien erteilt. Bei Missbrauch dieser Rechte können diese auf einfache und konsistente Weise wieder entzogen werden. Letztendlich führt dies zu einem schnelleren und einfacher strukturierten Sicherheitssystem.
1.3.4 Schicht der Anwendungslogik Die Schicht der Anwendungslogik stellt das Herz Ihrer Anwendung dar. Hier werden die Daten verarbeitet und bearbeitet. Hierfür sind Geschäftsprozessregeln und Workflowprozeduren verantwortlich. Diese Schicht erfordert die meiste Programmierarbeit. In dieser Schicht werden die Verarbeitung großer Datenmengen, Transaktionsunterstützung, Bereitstellungen großen Umfangs, Messaging und ggf. die Webschnittstelle definiert. Daher ist die Schicht der Anwendungslogik äußerst komplex. Wenn Sie Windows 2000 als Server für die Anwendungslogik einsetzen, stehen drei Hauptdienste zur Verfügung, COM+, MSMQ (Microsoft Message Queue) und IIS (Internet-Informationsdienste). Siehe hierzu Abbildung 1.4. Es stehen darüber hinaus weitere Dienste zur Verfügung, beispielsweise Microsoft Exchange und Microsoft SQL Server (Structured Query Language), aber diese stellen keinen Bestandteil des standardmäßigen Windows 2000-Pakets dar. Diese Komponenten müssen separat erworben werden. Jeder Dienst führt eine spezielle Aufgabe aus. COM+ ist für die COM-Grundlagen und die Transaktionsfunktionalität verantwortlich. IIS sorgt für die Internetdienste wie SMTP (Simple Mail Transfer Protocol), FTP (File Transfer Protocol) und HTTP (Hypertext Transfer Protocol). MSMQ übernimmt das Messaging. Jeder dieser Dienste kann andere Dienste zur Erstellung neuer Dienste verwenden. In die Warteschlange gestellte Komponenten beispielsweise verwenden sowohl COM+ als auch MSMQ zur Erstellung neuer Dienste.
Die Windows DNA-Architektur
31
Abbildung 1.4 Aufbau der Schicht der Anwendungslogik
COM+ Die nächste Generation von COM ist COM+. Durch das Hinzufügen von Diensten werden die Einsatzmöglichkeiten von COM im Unternehmen erweitert. COM bietet nur das Framework für die Komponenten, COM+ fügt darüber hinaus Transaktionsfunktionen hinzu. COM+ und Transaktionsdienste wurden erstmalig in SQL Server 6.5 vorgestellt, bei dem ein Dienst namens DTC (Distributed Transaction Coordinator) eingesetzt wurde. Dieser Dienst sorgte für die Verwaltung von SQL Server-Transaktionen. Anschließend wurde DTC mit einer einfacheren Programmierungs- und Bereitstellungsumgebung namens MTS (Microsoft Transaction Server) neu herausgebracht. MTS ermöglichte das Schreiben von COM-Objekten, durch die Geschäftsprozesse ausgeführt und Transaktionen gesteuert werden konnten. COM+ übernimmt diesen Transaktionsaspekt und integriert ihn in die COM-Laufzeit sowie das Betriebssystem. MTS wurde in das Betriebssystem eingebettet, aber ein MTS-Objekt konnte MTS umgehen. Mit COM+ wird nicht nur dieses Hintertürchen geschlossen, COM+ ist auch konsistenter und eleganter. Aus der Sicht des Programmierers können mit COM+ alle serverseitigen COM-Objekte die Vorteile der Transaktionsverarbeitung nutzen und dazu beitragen, Anwendungen zuverlässiger zu machen. Die Schicht der Anwendungslogik enthält mit Abstand die größte Anzahl an COM-Objekten, über die spezifische Dienste bereitgestellt werden. Diese Flexibilität ist zur Entwicklung qualitativ hochwertiger Unternehmensanwendungen er-
32
Einführung in Windows DNA
forderlich, über die Daten von der Datenschicht empfangen, verarbeitet und zurückgesendet werden können. Verschiedene Erweiterungen basieren auf COM und COM+, beispielsweise die TIP-Integration (Transaction Internet Protocol), die erweiterte Sicherheit und der dynamische Lastenausgleich. Diese Erweiterungen werden in den folgenden Abschnitten beschrieben. TIP-Integration COM+ verwendet OLE-Transaktionen (Objekt Linking and Embedding) zur Verwaltung der Umgebung für die Transaktionsverarbeitung. Dies gilt jedoch nur für die Windows 2000-Plattform. TIP ermöglicht COM-Objekten die Teilnahme an anderen TP-Umgebungen (Transaction-Processing). Diese Transaktionen können durch das TP-System verwaltet werden. Das TP-System unterscheidet sich von der vorherigen Version in MTS. In der damaligen Version mussten Transaktionen durch MTS und DTC verwaltet werden. Erweiterte Sicherheit Im vorangegangenen Abschnitt zur Firewall wurde erläutert, dass Windows 2000 das Sicherheitsmodell erweitert. Ein Teil dieser Erweiterung erfolgt durch die COM+-Umgebung. In COM+ wird die Sicherheit entweder durch ein rollenbasiertes Sicherheitssystem oder durch Berechtigungen für den Prozesszugriff erreicht. Die rollenbasierte Sicherheit ist ein Verfahren, bei dem einer Rolle ein Satz Berechtigungen zugewiesen wird, die zur Durchführung einer bestimmten Aufgabe benötigt werden. Diese Rolle wird anschließend den Benutzern zugeordnet, die diese Aufgabe ausführen müssen. Diese Rollen können beispielsweise Manager, Angestellte, Bestellungsverarbeitung usw. sein. Das Verwenden von Rollen vereinfacht das domänenunabhängige Zuweisen verschiedener Sicherheitsprivilegien für Benutzer. Ein Angestellter der Buchhaltung kann beispielsweise über viele Zugriffsberechtigungen zu den Buchhaltungsprozessen verfügen, jedoch nur wenige Zugriffsrechte auf die Produktionsprozesse besitzen. Eine Erweiterung der rollenbasierten Sicherheit in COM+ stellt die Fähigkeit dar, Sicherheitsrichtlinien auf der Methodenebene von COM-Objekten anzuwenden. Zentrale Verwaltung In der vorangegangenen Version von Windows NT wurden COM-Objekte unter Verwendung des MTS Explorers verwaltet. Zur Verwaltung der DCOM (Distributed Component Object Model)-Einstellungen wurde das Dienstprogramm DCOMCNFG eingesetzt. Unter Windows 2000 werden beide Tools durch den Komponentendienst ersetzt. Durch diesen wird die Verwaltung der COM+-Objekte kombiniert und erweitert. Zu den administrativen Aufgaben gehören Bereitstellung, Verwaltung und Überwachung der COM+-Anwendung. In Warteschlange gestellte Komponenten DCOM ermöglicht den Aufruf von COM-Objekten auf unterschiedlichen Computern. DCOM ist ein synchrones Protokoll, d.h. wenn ein Aufruf durch einen Computer erfolgt, muss der Empfänger verfügbar sein. Steht der Empfänger nicht zur Verfügung, schlägt der Aufruf fehl, und jede zugehörige Operation schlägt ebenfalls fehl. Wurde diese Opera-
Die Windows DNA-Architektur
33
tion innerhalb des Kontextes einer Transaktion ausgeführt, schlägt auch die Transaktion fehl. Sie müssen demnach ein Verfahren zum erneuten Versuch nach einem Fehlschlag implementieren. Unter Windows 2000 wird die einfachere Lösung der Verwendung in Warteschlange gestellter Komponenten angeboten. In Warteschlange befindliche Komponenten ermöglichen das asynchrone Aufrufen anderer Komponenten auf weiteren Computern. Die in Warteschlange befindlichen Komponenten verwenden als zugrunde liegende Messagingarchitektur MSMQ. Wenn daher die Verbindung oder der Aufruf fehlschlagen, kann so lange ein erneuter Versuch unternommen werden, bis die Operation erfolgreich ist. Ereignisdienste Unter Windows NT 4.x war es nicht einfach, Ereignisse mit Komponenten zu verbinden. Es war eine Technologie vorhanden, die so genannten COM-Verbindungspunkte, aber diese wiesen viele Einschränkungen auf. COM+-Ereignisse dagegen ermöglichen die lockere Anordnung von Ereignissen in Paaren, wobei der Ereignisempfänger aktiv oder inaktiv sein kann. Bei Verwendung von COM+-Ereignissen können Unicast- oder Multicastaufrufe durchgeführt werden. Unicast bedeutet hierbei 1–1, Multicast 1–n. Der Empfänger des Ereignisses stellt mit Hilfe eines Abonnementverfahrens eine Verbindung zum Sender her. Das Veröffentlichen und Abonnieren ermöglicht dem Verleger, den COM+-Ereignisdienst darüber zu informieren, dass er Informationen verteilen möchte. Ein Empfänger, der diese Informationen erhalten möchte, teilt dies dem COM+-Ereignisdienst durch ein Abonnement mit. Sobald auf dem Verleger Informationen geändert werden, werden diese Änderungen über den COM+-Ereignisdienst an die einzelnen Abonnenten weitergegeben. Dynamischer Lastenausgleich Mit Windows 2000 ist es möglich geworden, einen dynamischen Lastenausgleich für COM+-Objekte bereitzustellen. Beim dynamischen Lastenausgleich wird die Prozessorlast gleichmäßig über mehrere Computer verteilt. Der Lastenausgleich stellt einen serverorientierten Prozess dar und erfolgt gegenüber dem Client, der den Serveraufruf initiiert, transparent. Internetdienste Über IIS werden verschiedene Internetdienste bereitgestellt. 왘 HTTP: Das Protokoll des Web. Mit HTTP werden Webseiten und jeglicher In-
halt abgerufen, der über den Webbrowser angefordert wird. 왘 FTP: Das Protokoll zur Dateiübertragung im Internet. Es ist speziell auf die Da-
teiübertragung ausgelegt und daher optimal für diese Aufgabe geeignet.
34
Einführung in Windows DNA
왘 SMTP: Das Protokoll, das zur Übertragung von E-Mail-Nachrichten von einem
Computer zu einem anderen eingesetzt wird. Es kann zusammen mit POP verwendet werden. 왘 Gopher: Dieses Protokoll wurde in früheren Versionen von IIS verwendet,
dann aber durch HTTP ersetzt. Im Vergleich zu anderen Internetservern ist ein besonderes Merkmal von IIS die Transaktionsintegration. Bei Durchführung einer Anforderung aus IIS handelt es sich um eine Transaktionsanforderung. Durch die Verwendung von Transaktionen als Grundlage für die Anforderungsverarbeitung können alle Vorteile von COM+ genutzt werden. Gleichzeitig ist IIS so beim dynamischen Lastenausgleich oder bei der Optimierung sehr flexibel. Zur Erstellung von IIS-Inhalten kann ISAPI (Internet Server API) oder – häufiger verwendet – ASP (Active Server Pages) herangezogen werden. ASP ist eine Skriptumgebung, bei der der Programmcode über Skripts ausgeführt wird. Die Skriptsprache ist neutral und voll dynamisch. Zusammen mit dem COM+-Objekt eingesetzt, können voll funktionsfähige Unternehmensanwendungen erstellt werden. Mit Hilfe von ASP kann DHTML oder XML (Extensible Markup Language) generiert werden. Über Microsoft Visual InterDev können ASP-, DHTML- und XMLAnwendungen erstellt werden (diese Funktion steht unter Windows 2000 nicht zur Verfügung). Messagingdienste Das asynchrone Messaging wird von MSMQ gehandhabt. Das Messaging und das Schreiben von Code unterscheiden sich sehr stark voneinander. Beim Messaging wird angenommen, dass die Verbindung zwischen Client und Server nicht vorhanden ist, die Operation aber dennoch ausgeführt wird. Das Eventualitätsmodell ermöglicht das Schreiben von Anwendungen, die auch in unzuverlässigen Netzwerken oder unter unzuverlässigen Bedingungen funktionieren. MSMQ ist eine ereignisgesteuerte Architektur. Die Daten werden auf den Server gelegt, anschließend muss der Server auf das aufgerufene Ereignis reagieren. Das ereignisgesteuerte Modell ist nicht neu, denn Windows selbst ist ebenfalls ereignisgesteuert. Dennoch bedeutet dies, dass die Anwendungsprogrammierung von der allgemeinen Norm abweichen muss, da eine sofortige Antwort nicht zu erwarten ist.
Die Windows DNA-Architektur
35
Interoperabilität mit vorhandenen Systemen, Anwendungen und Daten Windows 2000 internetfähig zu machen war eine Sache. Eine mindestens ebenso wichtige Aufgabe ist jedoch, eine Integration mit älteren Systemen zu ermöglichen, den so genannten Legacysystemen. Legacysysteme funktionieren in der Regel und reichen zur Ausführung der ihnen zugedachten Aufgabe vollständig aus. Daher stellt das Ersetzen eines solchen Systems keine Option dar. Stattdessen müssen derartige Systeme in die Gesamtarchitektur integriert werden. Windows 2000 bietet verschiedene Optionen zur Integration unterschiedlicher Legacytechnologien: 왘 MSMQ-Integration: Die Messagingintegration für MSMQ wird für verschie-
dene Produkte angeboten. Zur Verbindung der Windows-Plattform mit MQSeries kann ein SNA Server eingesetzt werden. Die Verbindung von anderen Plattformen (UNIX, MVS, OS/2 usw.) zu MSMQ kann über FalconMQ-Clients ab Level8 erfolgen. 왘 COM-Transaktionsintegration: Mit Hilfe dieser Schnittstelle können CICS-
und IMS-Transaktionen (Messaging- und Transaktionsprodukte von IBM) zu COM-Objekten erweitert werden. Mit der COM-Transaktionsintegration stehen eine Reihe von Entwicklungstools und eine Anwendungslogik zur Verfügung, mit der die IBM-Mainframetransaktion gekapselt werden kann. Die Kommunikation mit einer IBM-Transaktion wird durch Windows SNA Server ermöglicht. 왘 UDA (Universal Data Access): Diese Schicht wird für den Datenbankzugriff
verwendet. Durch Verwendung systemeigener Treiber kann eine Verbindung zu Ressourcen hergestellt werden, die sich auf einer anderen Plattform befinden. 왘 DCOM auf UNIX und Mainframe: Eine Technologie, mit der COM-Objekte
nicht nur auf einer Windows-Plattform geschrieben werden können. Die Software AG brachte den ersten Anschluss für Solaris heraus. Dieser Anschluss wurde zum Einschluss der Mainframeplattform erweitert und wird derzeit auf eine Gesamtarchitektur names EntireX iNTegrator ausgeweitet. 왘 TIP/XA-Transaktionsintegration: Es sind verschiedene Standards für Transakti-
onsprotokolle vorhanden. TIP wurde bereits beschrieben, ein weiterer Standard ist das XA-Protokoll. Die Integration dieses Protokolls wird durch einen Ressourcenverteiler/-manager erzielt.
36
Einführung in Windows DNA
1.3.5 Datenschicht Die dritte Schicht ist die Datenschicht. Hier werden die Ressourcen verwaltet. Im Gegensatz zur Schicht der Anwendungslogik werden auf dieser Schicht keine Daten verarbeitet. Stattdessen wird auf dieser Schicht die Verwaltung der riesigen Datenmengen definiert. Datenbanken und Ressourcen wachsen in Ihrer Größe täglich, und die Aufgabe, diese Daten zu verwalten, gestaltet sich immer schwieriger. Einige der Daten werden in Tabellen gespeichert, andere in Textverarbeitungsdokumenten, der Großteil befindet sich in relationalen Datenbanken. Es gibt zwei Methoden zur Speicherung dieser Vielfalt und Menge an Daten. Bei der ersten Methode erstellen Sie einen Speicher (Repository), in dem alle diese Datentypen gespeichert werden können. In der Praxis werden für diese Form der Datenspeicherung so genannte Blobs verwendet (Binary Large Objects). Anhand von Blobs werden Daten jedoch nur als binäre Abbilder gespeichert. Die Richtigkeit der relationalen Informationen wird jedoch nicht gewährleistet. Wenn beispielsweise ein gespeicherter Blob auf ein anderes Dokument verweist, wird ein ungültiger Verweis nicht erfasst. In der Datenschicht haben diese ungültigen Verweise keine Bedeutung, in der Schicht der Anwendungslogik jedoch schon. Durch derartige Verweise könnten eine oder mehrere Transaktionen abgebrochen werden. Sie können natürlich dafür sorgen, dass in der Schicht der Anwendungslogik richtige Blobs geschrieben werden. Dies erfordert jedoch zusätzliche Programmierschritte, die Teil der Datenschicht sein sollten, denn die Verweisverwaltung ist eine Datenoperation. Zur Bereitstellung der Verweisverwaltung müssen Sie dem Datenrepository ein externes Modul hinzufügen. Dies stellt jedoch nicht die optimale Lösung dar. Besser ist es, wenn alle Ressourcen ihre eigenen Datenformate verwalten. Jede Ressource muss hierbei einem üblichen Objektmodell entsprechen. Auf diese Weise können Daten formatunabhängig bearbeitet, kopiert und referenziert werden. Genau diese Lösung wird mit der UDA-Strategie (Universal Data Access) geboten. Das UDA-Objektmodell wird auch als OLE DB-Objektmodell bezeichnet (siehe Abbildung 1.5). Es handelt sich um ein Lowlevel-Format, das eine Spezifikation dazu enthält, wie Transaktionen und Rowsets definiert werden. Das OLE DB-Objekt ist ein schnelles Lowlevel-Objekt. Dies bedeutet, dass das Schreiben von OLE DB-Objekten etwas Arbeit erfordert. Im Gegensatz zu Windows 2000 bietet Visual C++ eine kleine, überlagerte Schicht mit Vorlagen, die so genannten xxxxxxx
Die Windows DNA-Architektur
37
Abbildung 1.5 Universeller Datenzugriff (UDA, Universal Data Access)
OLE DB-Consumer Templates. Mit dieser Schicht wird OLE DB vereinfacht, ohne es langsamer oder weniger effizient zu machen. Das OLE DB-Objektmodell hat den Vorteil, dass dieses weder durch eine bestimmte Form noch durch ein Format beschränkt wird. Im relationalen Modus beispielsweise muss das einzig mögliche Resultset eine bestimmte Anzahl Zeilen und Spalten umfassen. Für OLE DB muss eine kleine Anzahl COM-Schnittstellen implementiert werden, alles Weitere liegt beim OLE DB-Provider. Außer der Bedingung, dass es sich um ein COM-Objekt handeln muss, gelten keine weiteren Einschränkungen. OLE DB ist eine ausgeprägte Lowlevel-Technologie und kann nur in compilerbasierten Entwicklungsumgebungen, z.B. Visual C++ oder Delphi, genutzt werden. Dem Visual Basic-Programmierer oder dem Skriptschreiber wird ein einfacheres Objektmodell namens ADO (Active Data Objects) zur Verfügung gestellt. ADO ist ebenso leistungsstark wie OLE DB, da es den Zugriff auf die meisten OLE DBFunktionen ermöglicht. Eine Frage muss noch gestellt werden: Wie legt OLE DB die Funktionalität der relationalen Datenbankjobs offen? Die Antwort ist – mit Hilfe von COM-Objekten. Aus diesem Grund ist es nicht erforderlich, dass mit ADO sämtliche der verschiedenen Datenformate verarbeitet werden können. Es liegt in der Verantwortung des Providers, die als Schnittstelle zwischen Daten und ADO-Aufrufen benötigten COM-Objekte zu generieren.
38
Einführung in Windows DNA
1.4 Schreiben von Anwendungen mit Hilfe von Visual Studio Die Strategie von Windows DNA richtet sich nicht nur auf Dienste und Hilfsprogramme in Windows 2000. Sie umfasst auch das Erstellen von Anwendungen mit Hilfe dieser Dienste und Hilfsprogramme. Dies ist das Thema, das in diesem Buch behandelt werden soll. Zum Entwickeln von Anwendungen müssen verschiedene Tools eingesetzt werden. Microsoft Visual Studio ist ein Paket, das sämtliche der erforderlichen Tools enthält. Es stehen verschiedene Versionen von Microsoft Visual Studio zur Verfügung. Interessant für Sie ist die Visual Studio Enterprise Edition, die auch Visual Studio DNA-Edition genannt wird. Dies rührt daher, dass diese Edition genau die Tools enthält, die zum Entwickeln einer Windows DNA-Anwendung benötigt werden. Ein Problem von Visual Studio ist, dass es unterschiedliche Programmiersprachen enthält. Dies kann zu Problemen führen, da viele Entwickler eine Programmiersprache favorisieren. Eine Strategie bei der Implementierung von Windows DNA liegt jedoch darin, die beste Sprache für eine bestimmte Aufgabe zu wählen. Auf diese Weise wird die Diskussion um die beste Sprache an sich in die Frage »Wann ist welche Sprache die beste?« gewandelt. Im vorliegenden Buch habe ich einen willkürlichen Ansatz gewählt und jeweils die Sprache zur Lösung einer Aufgabe gewählt, die mir als die geeignetste erschien. Bevor Sie mich also als verrückt abtun, sollten Sie daran denken, dass Sie ein Windows DNA-Ingenieur sind und daher immer auch den Kontext und die Vorgeschichte einer Entscheidung berücksichtigen sollten.
1.4.1 Der Kontext einer willkürlichen Sprachauswahl Ein Freund von mir arbeitet als Consultant und entwickelt vertikale Anwendungen, die anschließend den jeweiligen Kundenwünschen angepasst werden. Dies stellt eine klassische Windows DNA-Anwendungsentwicklung dar. Die von ihm entwickelten Anwendungen weisen drei Merkmale auf: 왘 Die Anwendungen umfassen einen vollständigen Entwicklungszyklus (Entwurf,
Test, Dokumentation usw.) 왘 Die Anwendungen weisen einen vollständigen Lebenszyklus auf (Bug Fixes,
Versionsnummern) 왘 Jeder Schritt im Produktentwicklungszyklus wird nach Möglichkeit auf die ef-
fektivste Weise umgesetzt
Schreiben von Anwendungen mit Hilfe von Visual Studio
39
Mein Freund musste sich bei der Entwicklung seiner Anwendungen für eine Programmiersprache entscheiden. Für die erste Anwendungsversion verwendete er C++, da einige Legacyanwendungen vorhanden waren. Um jedoch ein optimales Kosten-Nutzen-Verhältnis zu gewährleisten, führte sein Team eine interne Studie durch, die sich auf den gesamten Produktentwicklungszyklus erstreckte und bei der verschiedene Projekte berücksichtigt wurden, in denen unterschiedliche Sprachen verwendet worden waren. Und zu welchem Schluss führte diese Studie? Sie kam zu dem Ergebnis, dass die verwendete Sprache keine Rolle spielt. Worauf es ankommt, ist die Leistungsfähigkeit Ihres Teams, die Erfahrung der einzelnen Entwickler. Im Einzelnen konnten der Studie jedoch einige interessante Anhaltspunkte in Bezug auf die jeweiligen Sprachen entnommen werden. Da wäre zunächst die Produktivität des Programmierers. Visual Basic ist eine Umgebung, in der eine Anwendung schnell implementiert werden kann. Visual C++ ist langsamer, da die Entwicklung einiger Bibliotheken und Objekte mehr Zeit erfordert. Ein weiteres Maß der Produktivität ist der Faktor, wie schnell ein Programmierer eine Sprache erlernen kann. Auch hier ist Visual Basic die beste Wahl, da diese Sprache leicht erlernt werden kann. Visual C++ dagegen ist schwieriger zu erlernen und zu programmieren. Ein drittes Produktivitätsmaß stellt die Anzahl der im geschriebenen Code erzeugten Bugs dar. In dieser Hinsicht schneidet Visual Basic sehr schlecht ab. Auf jeden Bug in Visual C++ kommen drei Bugs in Visual Basic. Welche Sprache ist also die produktivste? Anscheinend Visual Basic. Obwohl hier mehr Bugs erzeugt werden, erfolgt das Beheben dieser Bugs schneller, da die Produktivität an sich sehr viel höher liegt. Unglücklicherweise ist diese Antwort falsch. Die richtige Antwort lautet, dass Visual Basic eine genauso schwierige Sprache ist wie Visual C++. Und hierfür sorgt die Art der Bugs in Visual Basic. Die Bugs in Visual Basic sind logisch, in Visual C++ stellen Bugs ungültige Zeiger oder beschädigten Speicher dar. Logische Bugs sind schlecht für den Produktentwicklungszyklus, da sie Änderungen in der Dokumentation, im Design usw. verlangen. Solche Änderungen erfordern mehr Zeit und erzeugen zusätzliche Kosten. Bugs in Visual C++ dagegen sind einfacher zu beheben, da sie nur Änderungen am Code erfordern, nicht jedoch Änderungen an der Codelogik. Warum dies so ist? Visual C++ ist eine schwierige Sprache, die einen erheblichen Lernaufwand erfordert. Sie müssen lernen, einen Punkt auf das i und einen Strich
40
Einführung in Windows DNA
durch das t zu machen, um es einmal so auszudrücken. Das heißt, ein Visual C++Anfänger hat mehr Erfahrung als ein Visual Basic-Programmierer. Diese Erfahrung eines Visual C++-Programmierers umfasst beispielsweise die Fähigkeit, Klassen und Systeme richtig zu entwerfen. Zu welcher Schlussfolgerung gelangte also mein Freund? Besorgen Sie sich einen geeigneten Programmierer! Beim Einsatz erfahrener Visual Basic-Programmierer sank die Anzahl der logischen Bugs dramatisch. Deshalb gibt es nicht die richtige Sprache für eine Aufgabe, es gibt nur den richtigen Entwickler für eine Aufgabe. Deshalb werden für die Beispiele in diesem Buch auch verschiedene Sprachen eingesetzt. Natürlich verwende ich keine Stapelverarbeitungsskripts zur Erstellung logischer Komponenten. Aber ich verwende gelegentlich Visual Basic oder Visual J++ oder sogar Visual C++. Hierbei wird jedoch auch immer das richtige Verfahren zum Einsatz einer bestimmten Technologie aufgezeigt. Diese Vorgehensweise stellt einen Bestandteil der Windows DNA-Anwendungsentwicklung mit Hilfe von Windows 2000 dar.
1.4.2 Programmiersprachen in Visual Studio Die Basisedition von Visual Studio beinhaltet fünf Sprachprodukte für die Entwicklung. Visual J++ Visual J++ ist ein Entwicklungstool, das auf der Java-Sprache basiert. Java ist eine Sprache, bei der Quellcode temporär in Bytecode kompiliert wird. Der Bytecode wird anschließend durch eine VM (Virtual Machine) interpretiert. Mit Hilfe dieses Ansatzes ist es möglich, Quellen zu kompilieren und anschließend auf einem beliebigen Rechner mit Java-VM auszuführen. Sun Microsystems entwickelte die ursprüngliche Java-Sprache und definierte ebenfalls die Standardbibliotheken von Java. Visual J++ unterstützt die meisten Sun Java-Bibliotheksaufrufe, ist jedoch auf die Windows-Entwicklung zugeschnitten. Daher ist Visual J++ in gewisser Weise inkompatibel mit den Java-Standardbibliotheken. Vorteile Java ist einfacher zu handhaben als C++, jedoch komplizierter als Visual Basic. Es handelt sich um eine rein objektorientierte Sprache, wodurch der Entwickler zur Verwendung von Objekten gezwungen ist. Java erkennt die in C++ verwendeten Zeiger nicht, die diese Sprache so komplex machen. Das Schnittstellenkonzept ist in die Sprache integriert, daher stellt die Komponentenentwicklung eine logische Schlussfolgerung dar. Die derzeit in der Visual Studio-Umge-
Schreiben von Anwendungen mit Hilfe von Visual Studio
41
bung unterstützte Visual J++-Entwicklungsumgebung stellt gleichzeitig die produktivste dar. Hierbei wird die Dokumentation in das Produkt integriert. Nachteile Der größte Nachteil sind Geschwindigkeit und Flexibilität. Java kann als einfach und doch leistungsstark oder als komplex und einschränkend betrachtet werden. Dies richtet sich ganz nach Ihrer Haltung gegenüber Java. Die aktuelle Implementierung von Visual J++ unterstützt den Bibliothekssatz von Sun Java, hinsichtlich der Produktivität ist es jedoch nicht mit Sun Java vergleichbar. Während plattformübergreifende Fähigkeiten eine der besten Funktionen von Java darstellen, werden genau diese im Visual J++-Kontext nicht besonders gut unterstützt. Visual C++ Visual C++ ist ein Entwicklungstool, dass als Entwicklungssprache C++ verwendet. Visual C++ wird häufig für die Entwicklung von Systemen und Softwareanwendungen eingesetzt. Diese Sprache besitzt die Fähigkeit, C und COM miteinander zu kombinieren, und dies ohne Einschränkungen. Vorteile C++ ist bei weitem die populärste Sprache zur Entwicklung von System- oder Softwareanwendungen. Es werden alle Aspekte der objektorientierten Entwicklung unterstützt, und gleichzeitig wird die Fähigkeit zur Integration von CLegacyquellcode bereitgestellt. C++ ist eine Sprache, mit der beliebige Aufgaben ausgeführt werden können und stellt daher die beste Wahl zur Entwicklung von Bibliotheken, Objekten und Komponenten dar. Angewendet durch einen erfahrenen Entwickler, tendiert der C++-Code dazu, zuverlässiger zu sein, da Debugbibliotheken und -objekte eingebunden werden können. Diese Objekte können dazu eingesetzt werden, insbesondere Inkonsistenzen bei Laufzeit und Kompilierungszeit zu ermitteln. Nachteile C++ weist zwar sehr viele Vorteile auf, besitzt jedoch auch einige Nachteile. Mit C++ werden dem Entwickler bei Entwicklung und Entwurf zwar viel Freiheit und Flexibilität gewährt, Programmierfehler können jedoch katastrophale Folgen haben. Darüber hinaus ist C++ schwer zu erlernen. Aufgrund der zahlreichen Leistungsmerkmale entwickeln viele C++-Entwickler eigene Entwicklungsstrategien, was die Teamarbeit negativ beeinflussen kann. Visual Basic Visual Basic ist eines der populärsten Entwicklungstools auf der Windows-Plattform. Es handelt sich um eine Sprache, die auf einer Microsoft-spezifischen Version von Basic basiert. Visual Basic 6.0 unterstützt objektorientierte Konstruktionen, wie beispielsweise Objekte, jedoch keine Vererbung. Die Komplexität des Betriebssystems wird als eine Reihe von COM-Objekten maskiert.
42
Einführung in Windows DNA
Vorteile Der größte Vorteil von Visual Basic ist seine Popularität. Es bietet hohe Produktivität, da Komponenten und Anwendungen leicht entwickelt werden können. Sprache und Umgebung sind einfach und leicht zu erlernen. Nachteile Der größte Nachteil von Visual Basic ist seine Einfachheit. Diese führt zu Anwendungen, die praktisch »im Vorbeigehen entworfen« und geschrieben werden. Obwohl diese Art der Anwendungen sehr wahrscheinlich nicht so viele Entwicklungsfehler aufweist wie Visual C++ (Zeigerfehler), neigt Visual Basic dazu, mehr logische Fehler zu erzeugen. Diese Art Fehler kann aufwendig zu beheben sein, da logische Fehler eine Umgestalung des Logikdesigns erfordern. Da in Visual Basic das Betriebssystem sowie die Dienste als eine Reihe von COM-Objekten abstrahiert werden, ist die Verteilung einer Visual Basic-Anwendung aufgrund der Größe der Endversion einer Anwendung komplizierter. Visual FoxPro In den vorangegangenen Erläuterungen der Entwicklungsumgebungen stellte jedes der Tools ein Szenario zur Codierung und Kompilierung dar. Visual FoxPro unterscheidet sich in diesem Punkt, da eine Entwicklungsumgebung in eine Datenbankumgebung integriert wird. Auf diese Weise wird die Entwicklung von Datenbankanwendungen erheblich erleichtert. Visual FoxPro ist objektorientiert und extrem schnell beim Datenbankzugriff. Vorteile Visual FoxPro-Entwickler sind häufig sehr talentiert und kennen ihr Entwicklungstool besonders gut. Das heißt, eine Visual FoxPro-Anwendung ist im Allgemeinen gut entworfen und entwickelt. Visual FoxPro ist eine so genannte All-In-One-Umgebung mit umfassenden objektorientierten Fähigkeiten. Nachteile Der Entwicklerpool, obgleich konstant, ist im Vergleich zu den anderen Tools kleiner. FoxPro ist ein klassisches Client/Server-Entwicklungstool. Es stellt jedoch für die Komponentenentwicklung nicht das ideale Tool dar. Visual InterDev Das fünfte Entwicklungstool in der Visual Studio-Entwicklungsumgebung ist das DHTML- und ASP-Tool Visual InterDev. Visual InterDev beansprucht für sich nicht, ein All-In-One-Tool zu sein. Visual InterDev ist eine Tool zur Erstellung von Webanwendungen, bei dem auch Komponenten eingesetzt werden. Während DHTML (Dynamic HTML) einen Standard darstellt, ist Microsoft Internet Explorer der einzige Browser, von dem Visual InterDev unterstützt wird. Vorteile Visual InterDev ist das Entwicklungstool für Dynamic HTML und ASP. Es eignet sich besonders für die Entwicklung vollständig skalierbarer Webanwendungen.
Schreiben von Anwendungen mit Hilfe von Visual Studio
43
Nachteile Visual InterDev neigt dazu, einige Dinge zu verkomplizieren, die eigentlich einfacher ausgedrückt werden könnten. Während es sich gut dazu eignet, einen klassischen Programmierer an eine Webumgebung heranzuführen, wird es jedoch den Ansprüchen der Entwickler, die mit dem Web aufgewachsen sind, in Bezug auf Leistungsstärke und Vielfältigkeit nicht gerecht. Es kann zur Erstellung von Webseiten für Netscape Navigator oder andere Browser verwendet werden, bei der Erstellung clientseitiger Skripts ist es jedoch eher für den Microsoft Internet Explorer geeignet.
1.4.3 Funktionen der Enterprise Edition In der Enterprise Edition von Visual Studio 6.0 stehen verschiedene Tools für die Unternehmensentwicklung zur Verfügung. Diese Tools vereinfachen die Entwicklung von Windows DNA-Anwendungen. Es handelt sich zwar nicht um objektorientierte Tools, aber es werden andere Aspekte des Entwicklungsprozesses unterstützt, beispielsweise Anwendungsversionen sowie Leistungsaspekte. Visual Source Safe Bei Visual Source Safe (VSS) handelt es sich um ein System zur Versionssteuerung für alle Entwicklungsaufgaben. Es ist in sämtliche Entwicklungstools der Microsoft-Plattform integriert. Mit diesem Tool wird die Arbeit mit Quellcode, Dokumentation und anderen Entwicklungstools im Team ermöglicht. Application Performance Explorer Wenn Sie eine Anwendung entwickeln, ist es häufig erforderlich, das beste Kosten-Nutzen-Verhältnis zu ermitteln. Ist es beispielsweise besser, einen Server mit 4 Prozessoren und 1 GB Speicher zu verwenden, oder ist es kosteneffektiver, zwei Systeme mit jeweils 2 Prozessoren und 512 MB RAM zu kaufen? Die Antwort auf diese Frage fällt nicht leicht, da zu einer Beantwortung u.a. Netzwerkverbindung, Festplattentyp, Bus- und Prozessorgeschwindigkeit berücksichtigt werden müssen. Die Anzahl der Hardwareparameter ist enorm. Des Weiteren können die Dienste des Betriebssystems so optimiert werden, dass die jeweiligen Hardwarevorteile besser genutzt werden, sodass noch mehr Parameter zum Tragen kommen. Auf diese Weise wird das Errechnen des tatsächlichen Kosten-Nutzen-Wertes nahezu unmöglich. Eine Methode zur einfachen Berechnung des Kosten-Nutzen-Verhältnisses stellt der Application Performance Explorer dar. Es handelt sich hierbei um ein Tool, mit dem verschiedene Szenarien verteilter Anwendungen bereitgestellt werden, die zum Testen der Hardwareeffizienz verwendet werden können. Diese Szenarien können dann von einem Computer auf einen anderen verlagert und erneut aus-
44
Einführung in Windows DNA
geführt werden. Basierend auf den Ergebnissen für die Szenarien können mathematische Berechnungen zum besten Kosten-Nutzen-Wert durchgeführt werden. Visual Studio Analyzer Beim Testen verteilter Anwendungen ist es erforderlich, die jeweils zu erwartenden Engpässe zu kennen. Dies ist nicht mit der Profilerstellung vergleichbar, da diese Programme sprachspezifisch sind und einen Quellcodezugriff erfordern. Stattdessen wird ein Test des Laufzeitsystems mit Hilfe von Visual Studio Analyzer durchgeführt, bei dem kein solcher Eingriff erforderlich ist. Es können verschiedene Datenquellen eingerichtet werden, mit denen die Engpässe des gesamten Systems ermittelt werden. Visual Modeler Jede Architektur erfordert eine Entwurfsmethode für das gesamte System. Visual Modeler ist ein Tool, bei dem das System mit Hilfe von UML (Unified Modeling Language) entworfen wird. UML ist eine Notation, die zur Definition von Klassen, Methoden und Parametern eingesetzt wird. UML ist keine Sprache und dient ausschließlich zu Entwurfszwecken. Visual Modeler besitzt die Fähigkeit, das Modell aus dem Quellcode zu generieren. Anschließend kann das Modell in der Implementierungsphase des Produktzyklus verwendet werden. Component Manager Bei der Entwicklung vieler Systeme mit Komponenten ist es vorteilhaft, die Komponenten wieder zu verwenden. Diese Komponenten können Quellcode, Entwurfskonzepte oder binäre Komponenten sein. Der Component Manager unterstützt Sie bei diesem Prozess. Mit diesem Tool können Sie Informationen veröffentlichen, die durch eine andere Komponente wieder verwendet wird. Der Component Manager verwendet das Repositoryframework von Microsoft. Mit dem Microsoft Repository können Sie, basierend auf Funktionalität und Funktionssätzen, Abfragen nach spezifischen Komponenten durchführen. Das Repository kann als Datenbank betrachtet werden, tatsächlich ist es jedoch mehr als das, da es auf die Anforderungen des Entwicklungszyklus zugeschnitten ist.
1.5 Resümee Windows DNA stellt eine Form der Anwendungsentwicklung dar, die verschiedenste Funktionen unterstützt, beispielsweise Transaktionen, das Messaging und Datendienste. Windows DNA und Windows 2000 greifen hierbei ineinander. Windows DNA basiert auf den Diensten, die durch Windows 2000 bereitgestellt werden. Aus der Sicht von Microsoft ist Windows DNA ein Marketingtool, das
Resümee
45
eine Möglichkeit bietet, Anwendungen zu entwickeln. Ich bin der Meinung, dass mit Windows DNA eine Vielzahl von Methoden und Verfahren für die Anwendungsentwicklung zur Verfügung gestellt wird. Windows DNA kann als Modell zur Förderung effizienter Programmiertechniken verstanden werden, und hierbei stellt die Definition des Problemkontextes das erste effektive Verfahren dar. Es wurde zunächst kurz der Kontext von Windows umrissen, um einen Eindruck der Windows-Plattformhintergründe zu vermitteln. Das Anwenden des gleichen Verfahrens auf die eigene Problemsituation hilft Ihnen dabei, die Hintergründe des Problems zu erkennen. Basierend auf diesem Wissen können Sie einen Lösungsansatz erarbeiten, der am ehesten zum gewünschten Ziel führt.
46
Einführung in Windows DNA
2 Alles über Muster In diesem Kapitel soll die Anwendungsentwicklung eingehender betrachtet werden. Einen Ansatz zur Anwendungsentwicklung stellt die Verwendung von Mustern dar. In diesem Buch werden viele solcher Muster eingesetzt. Sie werden im Laufe Ihrer Karriere als Softwareentwickler feststellen, dass viele Probleme wiederholt auftreten. In diesen Situationen ist es sinnvoll, eine Lösung bzw. ein Muster anzuwenden, dessen Effektivität bereits erprobt wurde.
2.1 Einführung in Muster Was ist ein Muster? Es stellt eine Lösung zu einem Problem in einem bestimmten Kontext dar. Zusätzlich zeichnet sich ein Muster dadurch aus, dass es wiederholt eingesetzt werden kann. Eine Lösung kann nur dann tatsächlich als Muster bezeichnet werden, wenn dieses in mindestens drei ähnlichen Situationen zur Problemlösung eingesetzt wurde. Aus dieser Definition ergibt sich gleichzeitig, dass sich ein Muster nicht ausschließlich auf den Bereich der Computeranwendungsentwicklung beschränkt. Der Architekt Christopher Alexander definierte das Muster (pattern) folgendermaßen: »Jedes Muster ist eine aus drei Elementen bestehende Regel, mit der eine Relation zwischen einem bestimmten Kontext, einem Problem und einer Lösung ausgedrückt wird.« Das Muster verknüpft ein Problem und eine Lösung mit einem Kontext. Mit dieser Aussage wird an die Kontextdefinition von Kapitel 1 angeknüpft. Ein klar definierter Kontext stellt die Basis für ein gutes Muster dar. Wenn der Kontext nicht eindeutig klar ist, kann das Problemverständnis vielleicht unvollständig sein und dazu führen, dass die bereitgestellte Lösung dem Problem nicht angemessen ist. Angenommen, es liegt eine Problemsituation vor, bei der der Kontext definiert und eine Lösung gefunden wurde. Bedeutet dies automatisch, dass die Lösung ausgereift ist? Unglücklicherweise nicht, da ein Muster ausschließlich zur Lösung eines spezifischen Problems eingesetzt werden kann, und eine Lösung wirkt sich immer auch auf die weiteren Bestandteile des Systems aus. Als Ergebnis erhält man einen neuen Kontext mit neuen Problemen. Es gibt nie eine absolut richtige Antwort, durch die das System nicht in irgendeiner Weise beeinflusst wird. Sie können immer einen neuen Kontext anwenden, der wieder andere Muster und Lösungen erfordert. Die Auswirkungen der Verwendung eines Musters sind genauso wichtig wie der Kontext, in dem das Muster verwendet wird. Wenn Problem, Kontext und Muster
Einführung in Muster
47
klar definiert sind, können Sie Ihre Muster wie kleine Puzzleteilchen zusammenfügen und so die beste Lösung für die Anwendung erarbeiten.
2.2 Ein Beispiel für ein einfaches Muster Das folgende Beispiel hat nichts mit Computern zu tun, sagt aber trotzdem eine Menge über das Konzept von Mustern aus. Das Problem besteht darin, ein Haus zu errichten. Der Kontext lautet, dass das Haus in den Wäldern von Kalifornien erbaut werden soll. Daraus ergibt sich die einzukalkulierende Gefahr eines Waldbrandes, d.h., für das Haus ist ein optimaler Brandschutz erforderlich. Dennoch ist die zu bewältigende Aufgabe nicht neu, unter diesen Voraussetzungen wurden bereits viele Häuser erbaut. Sie können sich beispielsweise erkundigen, wie bei anderen Häusern vorgegangen wurde, die in einem Wald erbaut wurden, und was zu deren Schutz vor einem Waldbrand getan wurde. Sie würden schnell feststellen, dass die Erbauung dieser Häuser stets einem bestimmten Muster folgt; z.B. werden im näheren Umkreis des Hauses die vorhandenen Bäume gefällt, und es wird nach Möglichkeit mit nicht entflammbaren Materialien gebaut. Wenn Sie also bei der Errichtung des Hauses diesem Muster folgen, minimieren Sie die Gefahr, Ihr Haus durch einen Brand zu verlieren. Um ein Muster zu erkennen und in der richtigen Situation anzuwenden, müssen Sie Kontext, Problem und Lösung verstehen. Im vorangegangenen Beispiel besteht der Problemkontext in der Lage des Hauses. Als Nächstes wird das Problem definiert: Die Region, in der Sie das Haus errichten möchten, ist der Gefahr eines Waldbrandes ausgesetzt. Dies bedeutet, dass Ihr Haus abbrennen könnte. Die Lösung für das Problem besteht darin, die Bäume um das Haus zu entfernen und beim Hausbau nicht entflammbare Materialien zu verwenden. Auf diese Weise erhöhen Sie die Chance, bei einem Waldbrand ungeschoren davonzukommen.
2.2.1 Wo Muster versagen Muster stellen nicht immer den perfekten Lösungsansatz dar. Das häufigste Problem besteht darin, dass der Kontext nicht voll erfasst wird und bei der Verwendung von Mustern falsche Tatsachen vorausgesetzt werden. Nehmen wir wieder das Beispiel des Hauses, das in einem waldigen, trockenen Gebiet erbaut werden soll. Die Lösung bestand darin, die Bäume im näheren Umkreis des Hauses zu fällen und nicht entflammbares Material einzusetzen. Dabei geht man von der Annahme aus, dass für den Hausbau ein beliebiges Material eingesetzt werden kann, und dass es möglich ist, die Bäume um das Haus zu fällen.
48
Alles über Muster
Nehmen Sie aber einmal an, dass Sie Ihr Haus in einer Region mit strengen Bauvorschriften im Hinblick auf das Aussehen des Hauses erbauen möchten, dass sein Aussehen in Einklang mit der Natur stehen muss. Darüber hinaus stehen einige der Bäume im Umkreis Ihres Hauses unter Naturschutz und dürfen nicht gefällt werden. Das gefundene Muster greift hier also nicht. Im obigen Muster wurde davon ausgegangen, dass Sie auf Ihrem Grundstück und bezüglich des Aussehens Ihres Hauses freie Hand haben. Dies ist eine falsche Voraussetzung, das vorgeschlagene Muster würde zu einem Fehlschlagen des Projekts führen. Ist deswegen das Muster falsch? Nein – das Muster ist richtig, aber nur in den Fällen, in denen die umliegenden Bäume nicht unter Naturschutz stehen und ein beliebiges Material für den Hausbau eingesetzt werden kann. Das Muster und der Kontext müssen lediglich klar definiert und aufeinander abgestimmt werden. Dies verdeutlicht, dass der Kontext sorgfältig und umsichtig definiert werden muss.
2.3 Grundlegendes zu Mustern Der Hauptgrund für die Verwendung von Mustern stellt die Möglichkeit zur Entwicklung einer gemeinsamen Sprache dar. Mit einer solchen ist die Kommunikation mit anderen Systementwicklern ohne die Erläuterung des Kontextes möglich. Ein Muster ermöglicht Ihnen das Konvertieren eines Satzes aus Kontext/Problem/ Lösung in Spezifikationen, die leicht verständlich sind.
2.3.1 Definieren eines guten Musters Ein gutes Muster führt zur Lösung eines Problems, ist nicht linear und beruht auf Erfahrungswerten. Es werden drei Anforderungen erfüllt: 왘 Das Muster führt zur Problemlösung: Über das Muster werden Lösung und
Problem definiert. Die Definition muss hierbei eindeutig sein, da viele Systeme sich nach dieser Kombination aus Lösung/Problem richten. Darüber hinaus müssen Problem und Lösung wiederholt anwendbar sein, da es sich ansonsten nicht um ein Muster handeln würde. 왘 Die Lösung ist nicht linear: Muster sind niemals ganz logisch oder rational.
Wenn das Problem logisch wäre, wäre auch die Lösung einfach und jeder könnte zu der gleichen Antwort gelangen. Die Mathematik ist beispielsweise logisch. Wenn zwei Zahlen addiert werden, kann es nur ein richtiges Ergebnis geben. Das Addieren von Zahlen führt immer zum gleichen Ergebnis, egal, ob Sie im Wald stehen oder nicht. Es gibt keine Kontextabhängigkeit, die Definition eines Musters ist daher nicht erforderlich. 왘 Erfahrungswerte dienen als Grundlage: Da Muster nicht linear und zum Teil
irrational sind, hängen Sie stark vom jeweiligen Erfahrungsschatz desjenigen
Grundlegendes zu Mustern
49
ab, der die Lösung bereitstellt. Ein Muster kann gut oder schlecht sein. Daher sollten Muster auf verschiedenen Systemen basieren. Einige Referenzen erfordern mindestens drei Systeme, hierbei gilt: je mehr, desto besser. Mit einem guten Muster sammeln Sie Erfahrung, ohne zu experimentieren. Im Fachjargon wird dies als »intelligent guess« bezeichnet. »Guess«, da es keine richtige oder falsche Antwort gibt, »intelligent«, weil es sich trotzdem nicht um einen willkürlichen Versuch handelt – Sie setzen eine erprobte Methode zur Lösung des Problems ein.
2.3.2 Arten von Mustern Der Prozess der Produktentwicklung umfasst verschiedene Schritte und unterschiedliche Arten von Mustern für die einzelnen Schritte. Es sind drei Arten von Softwaremustern vorhanden: 왘 Konzeptuelle Muster: In der anfänglichen Produktentwicklung werden diese
Muster zur Beschreibung des Systems als Gruppe von Einheiten verwendet. Wenn Sie beispielsweise über die anfängliche Architektur nachdenken, tauchen bestimmte Probleme auf. Diese Probleme sind sehr abstrakt und beschäftigen sich mit dem Anwendungskonzept selbst. Es werden keine technischen Aspekte berücksichtigt, es werden ausschließlich Konzeptfragen geklärt. 왘 Entwurfsmuster: Bei der Implementierung des Anwendungsdomänenmodells
stehen verschiedene allgemeine Softwaremodelle zur Verfügung. Ein Modell kann beispielsweise den besten Zugriff auf eine Datenbank definieren und dynamische Objekte generieren. Die auf dieser Ebene definierten Muster sind häufig abstrakte Konzepte, die über die Software in Form von Schnittstellen, Objekten usw. angewendet werden. Entwurfsmuster enthalten in der Regel keine Implementierungsdetails. 왘 Programmierungsmuster: Bei der Implementierung der Anwendungsschnitt-
stellen und -entwürfe gibt es eine spezielle Methode zum Schreiben einiger der Codesegmente. Diese Muster sind sehr sprach-, plattform- und technologiespezifisch. Mit diesen Mustern wird versucht, unter Verwendung der verfügbaren Technologie ein optimales Programmsegment zu schreiben, beispielsweise einen Smartpointer (intelligenter Zeiger) in C++. Die drei verschiedenen Muster dienen unterschiedlichen Zwecken. Es besteht eine klare Staffelung von der konzeptionellen Ebene bis hin zur Implementierung. Die Muster bleiben in den unterschiedlichen Phasen jedoch abstrakt und umreißen ein Grundprinzip. Daher stellen Muster kein Patentrezept zur Implementierung eines Algorithmus oder eines UML-Diagramms (Unified Modeling Language) dar.
50
Alles über Muster
Wann ist ein Muster kein Muster? Die Welt besteht nicht nur aus Mustern. Es gibt fundamentale Richtlinien oder Regeln, die nicht kontextabhängig sind, beispielsweise die Regeln der Mathematik, und diese stellen keine Muster dar. Erweist sich ein Konzept in allen Fällen als richtig, handelt es sich nicht um ein Muster. Muster können nicht geplant werden. Es ist nicht möglich, ein Projekt mit der Absicht anzugehen, ein Muster zur Lösung des Problems zu entwerfen. Muster entstehen im Verlaufe eines Prozesses aus Fehlern und Erfolgen. Durch Fehler lernen Sie die Dynamik eines Vorgangs kennen und können sich so ein Muster erarbeiten. Daher ist es nicht möglich, ein Muster im Vorfeld zu planen. Bei der Erarbeitung eines Musters ist konstruktive Kritik gefragt. Durch konstruktive Kritik, beispielsweise durch Vergleiche, kann die Anwendungsfähigkeit eines Musters eingeschätzt werden.
2.3.3 Vorlage zum Beschreiben von Mustern Das Anwenden von Mustern erfordert gleichzeitig das Verständnis von Mustern. Leider ist die Lektüre der Musterdefinitionen wenig spannend und gleicht dem Durcharbeiten eines Standardlehrbuches. Dennoch können Sie sich Informationen zu verschiedenen Mustern aneignen und anschließend Probleme mit Hilfe der erlernten Muster lösen. Oder Sie versuchen, Problem und Kontext zu definieren und nach einem geeigneten Muster zu suchen. Ich neige dazu, Probleme nach dem zweiten Prinzip anzugehen. Der Nachteil hierbei ist, dass Sie zunächst eine Vielzahl von Büchern nach einem geeigneten Muster durchsuchen, bevor Sie sich ein umfassendes Wissen über häufig verwendeten Muster angeeignet haben. Bei der Suche nach einem Muster müssen Sie in der Lage sein, dieses zu kategorisieren und mit dem eigenen Problem und Kontext zu vergleichen. Dies kann schwierig sein, da unterschiedliche Musterbücher unterschiedliche Notationen verwenden. Viele dieser Notationen verwenden jedoch eine gemeinsame Technologie. Einige dieser Begriffe werden nachstehend beschrieben. Muster – allgemeine Begriffe Ein Muster wird durch seinen Namen identifiziert. Der Name kann aus einem kurzen Satz oder einem einzigen Wort bestehen. In beiden Fällen sollte der Name eindeutig und intuitiv sein. Es ist hilfreich, wenn der Name eines Musters dessen Funktion widerspiegelt. In vielen Musterdefinitionen bezeichnet das nächste Element das Problem, auch Intention genannt. Hier werden die Ziele für das Muster definiert. Diese Beschreibung umfasst häufig einen kurzen Absatz.
Grundlegendes zu Mustern
51
Der nächste Schritt besteht in der Definition des Zwecks für das Entwurfsmuster. Diese Komponente wird auch Motivation oder Beweggrund genannt und beschreibt, aus welchem Grund das Muster erstellt wird. In vielen Fällen wird hier ein Szenario definiert. Falls möglich, sollte bei der Definition des Musters auch das philosophische Argument dargelegt werden, damit der Leser die Gedankengänge des Entwicklers während der Dokumentation des Musters nachvollziehen kann. Ein Verständnis der Denkweise des Entwicklers ermöglicht dem Leser, seine eigene Situation mit der durch das Muster beschriebenen Situation in Beziehung zu bringen. Die unterschiedlichen Beweggründe können Richtigkeit, Ressourcen, Struktur, Aufbau oder Verwendung sein. Die Anwendbarkeit eines Musters definiert dessen Grenzen. Durch das Betrachten von Intention und Anwendbarkeit kann leicht bestimmt werden, ob sich das Muster für eine Situation eignet. Es wird eine Art Prüfliste definiert, die verwendet werden sollte, wenn die Intention sich als übereinstimmend erweist. Der letzte Punkt definiert die Auswirkungen oder Konsequenzen des Musters. Nehmen Sie an, ein Entwickler implementiert ein Muster und muss dann feststellen, dass dieses mit einem anderen Muster inkompatibel ist. Der Entwickler wird nicht gerade erfreut sein, denn das entworfene Muster sollte ja eigentlich eine Erleichtung darstellen. Durch das Beschreiben der Auswirkungen eines Musters ist es dem Entwickler möglich, eventuell bei der Implementierung auftretende Konflikte mit anderen Zielen einzuschätzen. Ruft die Implementierung Konflikte mit anderen Mustern hervor, ist das Muster nicht anwendbar. Ein weiterer Aspekt bei den Auswirkungen eines Musters ist der, die positiven Eigenschaften des Musters hervorzuheben. Angenommen, ein Entwickler muss die Verwendung seines Musters rechtfertigen. Die positiven Auswirkungen des Musters können den Entwickler bei der Entscheidung unterstützen, das Muster im Entwurf zu verwenden oder nicht. Auch wenn es sich nicht empfiehlt, können hier Schlagworte wie Skalierbarkeit, Stabilität, Erweiterbarkeit usw. eingesetzt werden. Abschließend benötigt jedes Muster eine Implementierung. Die Implementierung ist einfach, da über sie die Verwendung des Musters definiert wird. Hier können Code oder UML-Diagramme eingefügt werden, je nach beschriebenem Mustertyp. Lesen der Muster in diesem Buch Nachdem nun die Begrifflichkeit erläutert wurde, sind Sie in der Lage, ein Muster zu lesen. Aber wie wenden Sie ein Muster auf ein Problem an? Hierzu vergleichen Sie Ihren Kontext mit den Beweggründen für das Muster. Nachdem Sie ein Mus-
52
Alles über Muster
ter zur Verfügung haben, sollten Sie sicherstellen, dass in anderen Teilen der Anwendung keine Probleme hervorgerufen werden. Dieses Buch wendet Muster an, wenn es das Szenario erfordert. Bei der Beschreibung der Probleme, die durch COM (Component Object Model) gelöst werden, wird beispielsweise das Bridge-Muster verwendet. Innerhalb eines Kapitels, in dem COM beschrieben wird, wird demnach das Bridge-Muster referenziert und erläutert. In Anhang A finden Sie einfache Definitionen der Muster; diese werden mit den folgenden Begriffen beschrieben: 왘 Name: Hierbei handelt es sich um den Namen des Entwurfsmusters. 왘 Intention: Entspricht der zuvor definierten allgemeinen Zielsetzung. 왘 Start- und Endkontext: Das Verwenden von Begriffen wie Anwendbarkeit, In-
tention usw. kompliziert die Musterdefinition. Stattdessen kann ein Start- und ein Endkontext definiert werden. Dies ist einfacher, da nur zwei Elemente definiert werden müssen. Der Startkontext definiert das zu bewältigende Problem. Der Endkontext definiert die zu erreichenden Ziele oder Anforderungen an das System mit einfachen Worten. Sie können diesen Vorgang damit vergleichen, dass Sie den Startkontext in eine Blackbox geben und als Ergebnis den Endkontext erhalten. 왘 Lösung: Eine Implementierung der Blackbox, bei der als Eingabe der Startkon-
text und als Ergebnis der Endkontext verwendet wird. 왘 Auswirkungen: Alle Lösungen besitzen Auswirkungen, die kompliziert oder
einfach sein können. Diese müssen definiert werden, da durch sie der Endkontext etabliert wird. Diese Musterdefinition ist sehr einfach, eignet sich jedoch auch für eine Komponentenarchitektur. Komponenten sind wie Computerchips – es sind einige Drähte vorhanden, die verbunden und aktiviert werden. Eine Komponentenmethodik und eine Musterdefinition, um die dieser Ansatz zur Softwareentwicklung ergänzt wird, machen die Dinge verständlicher. Komponenten werden in den nächsten Kapiteln eingehender erläutert.
2.3.4 Muster und deren Beziehung zu anderen Konzepten Neben den Mustern gibt es Frameworks, Handbücher, empfohlene Vorgehensweisen usw. Worin unterscheidet sich ein Muster von diesen Dingen? Zunächst ist ein Muster ein generisches Konzept. Mit einem Muster kann eine Implementierung definiert werden, aber in erster Linie ist es ein Konzept. Frameworks und Handbücher definieren Lösungen für spezifische Probleme. Diese Konzepte weisen keinen Kontext auf. Stattdessen werden Probleme auf allgemeine Weise gelöst. Beispiele hierfür ist das MFC-Framework (Microsoft Foundation Library, ver-
Grundlegendes zu Mustern
53
wendet in C++), das zur Erstellung einer objektorientierten Benutzerschnittstelle verwendet wird. MFC ist kein Entwurfsmuster. Empfohlene Vorgehensweisen und Muster ähneln sich. Bei empfohlenen Vorgehensweisen handelt es sich um nicht validierte Muster – das Warum und der Kontext einer empfohlenen Vorgehensweise wurden nicht definiert oder ausreichend überprüft. Muster nehmen diese empfohlenen Vorgehensweisen auf und strukturieren diese. Eine empfohlene Vorgehensweise kann beispielsweise lauten: »Fasse nicht mit der Hand in kochendes Wasser.« Diese Empfehlung klingt sinnvoll. Tatsächlich ist sie aber nicht immer sinnvoll. Was geschieht, wenn die angesprochene Person Schutzhandschuhe trägt? Mit einer empfohlenen Vorgehensweise wird nicht der genaue Kontext definiert, ein Muster dagegen definiert einen Kontext und eine Lösung.
2.4 Antimuster Muster werden zum Erreichen positiver Ziele entworfen. Einige Projekte schlagen jedoch fehl, da Fehler in der Programmierungslogik oder bei den Programmiermethoden gemacht werden. Auch in diesen Fällen kann das Konzept der Muster angewendet werden, denn Fehler wiederholen sich häufig. Dennoch kann diese Art der Analyse nicht als Muster bezeichnet werden und wird daher Antimuster genannt.
2.4.1 Warum treten Antimuster auf? Ein Antimuster ist ein Muster, das sich wiederholt und negative Auswirkungen hervorruft. Eine offensichtliche Frage im Hinblick auf Antimuster ist, warum diese nicht von vornherein vermieden werden. Dies wäre die logische Konsequenz. Die Antwort ist, dass Menschen Fehler machen und bei der Anwendungsentwicklung nicht immer logische Entscheidungen treffen. Denken Sie an den Kontext – aufgrund der Geschäftspolitik, der Unternehmensprozesse, technischer Unerfahrenheit oder unzureichender technologischer Ressourcen usw. müssen Kompromisse gefunden werden. Bei der Anwendungsentwicklung für ein Unternehmen sehen wir uns häufig einer Art isolierter Umgebung gegenüber. Entscheidungen zur Anwendungsentwicklung werden durch die Unternehmensrichtlinien diktiert, was nicht immer zu einem optimalen Entwurfskonzept führt. Der Entwurf wird außerdem durch persönliche Vorlieben der Entwickler, die Unternehmenspolitik und durch die persönlichen Ziele der beteiligten Personen beeinflusst. Nicht zu vergessen die engen Deadlines, die um jeden Preis eingehalten werden müssen. In diesem Kontext ist es nicht überraschend, das in einer Anwendung Antimuster auftauchen.
54
Alles über Muster
Ein weiterer Grund für das Auftreten von Antimustern sind Unternehmen, die an einem traditionellen Geschäftsprozess festhalten. Dies ist an sich eine gute Sache, da sich ein Unternehmen so von anderen abhebt. Mit dieser Einstellung gewinnt das Unternehmen einen Vorteil im allgemeinen Konkurrenzkampf. Da ein solcher Geschäftsprozess sich jedoch nicht über Nacht entwickelt, treten häufig Probleme auf. Es müssen Kompromisse eingegangen werden, die häufig unlogisch sind. Das Softwaresystem wiederum muss diesen unlogischen Entwurf widerspiegeln, was dann zu schlechten Softwaresystemen führt. Das Gegenstück zu einem schlechten Geschäftsprozess ist schlechte Software. Ein Unternehmen verfügt häufig über einen Geschäftsprozess, dem die Software nicht gewachsen ist, daher muss sich das Unternehmen an die Software anpassen. Ein Beispiel hierfür ist das sehr umfangreiche ERP-Programm (Enterprise Resource Planner), bei dem genau dieser Umstand auftritt. Die Unternehmen fügen sich diesem Prozess der Softwareintegration, da dies als vorteilhaft für das Unternehmen angesehen wird. Dies ist jedoch nicht immer richtig. Eine weitere Ursache für Antimuster ist die Unerfahrenheit von Softwareentwicklern. Das Entwickeln eines konkurrenzfähigen Produkts erfordert umfangreiche Forschungs- und Entwicklungsarbeit, die dann zur Entwicklung eines neuen Produkts führen sollte. Es treten Fehler auf, und mit der Zeit entwickeln sich Muster negativer Auswirkungen. Daher treten selbst bei größter Anstrengung immer Fehler und daher auch immer Antimuster auf. Ihre Aufgabe besteht darin, die Zahl der Antimuster in einem Projekt zu minimieren und, falls möglich, vorhandene Antimuster zu beseitigen. Es wird immer Projekte geben, die nicht so funktionieren, wie sie sollten. Dies schmerzt, aber aus einer negativen Erfahrung kann man auch immer etwas lernen.
2.5 Resümee Die Definition eines Musters bleibt weiterhin etwas nebulös, da nicht jeder in gleicher Weise über ein Muster denkt. Dennoch glaube ich an das Konzept der Muster. Muster stellen eine praktische Methode zum Verknüpfen von Kontext und Problem sowie der anschließenden Definition einer Lösung dar. Versuchen Sie nicht, Muster als eine Art Patentrezept für die Problemlösung zu betrachten. Stattdessen sollten Sie Muster als eine Möglichkeit zum Angehen eines Problems begreifen. Der Vorteil bei der Verwendung von Mustern liegt darin, dass Sie nicht den Prozess durchlaufen müssen, den andere bereits getestet und erfolgreich erprobt haben. Stattdessen führen Sie den Entwicklungsprozess weiter fort, indem Sie einige Grundkonzepte anwenden und eigene Verfahren hinzufügen. Durch das Doku-
Resümee
55
mentieren dieser eigenen Entwurfsverfahren unter Verwendung eines Musterentwicklungszyklus können Sie den Entwicklungsprozess erweitern. Dies führt letztendlich zu einer stabileren und verständlicheren Software, selbst wenn diese sehr komplex ist.
56
Alles über Muster
3 Entwickeln von Anwendungen In diesem Kapitel wird der Prozess der Anwendungsentwicklung definiert. Zunächst soll der Anwendungszyklus definiert werden, anschließend wird eine praktische Entwicklungsstrategie erläutert, die als iterative, kontextuelle Entwicklung bezeichnet wird. Als Nächstes werden die einzelnen Schritte der Anwendungsdefinition unter Verwendung des Anwendungsentwicklungszyklus untersucht.
3.1 Entwickeln von Anwendungen Die Anwendungsentwicklung stellte nie eine einfache Aufgabe dar. Die Definition eines Modells zur Anwendungsentwicklung ähnelt manchmal dem Klinkenputzen – es wird viel versprochen, aber nicht alles gehalten. Meine Erfahrung hat mich gelehrt, dass jeder ein Modell definieren würde, wenn diese Aufgabe tatsächlich so einfach wäre. Es gibt jedoch keine einfache Antwort, da der Prozess der Anwendungsentwicklung sehr komplex ist. Es müssen viele Abstriche gemacht, Kompromisse eingegangen und Anpassungen vorgenommen werden. Bedeutet dies, dass alle Modelle zur Anwendungsentwicklung nutzlos sind? Nein. Es ist möglich, verschiedene generische Prozesse zu definieren und diese anschließend logisch zu optimieren. Bei der Entwicklung einer Anwendung beispielsweise wird immer ein bestimmter Zyklus durchlaufen, der so genannte Anwendungsentwicklungszyklus (Application Development Cycle, ADC). Es handelt sich um einen Zyklus, da Sie sich am Ende des Prozesses wieder dort befinden, wo Sie begonnen haben.
3.1.1 Der Anwendungsentwicklungszyklus Der Anwendungsentwicklungszyklus folgt dem nachstehenden allgemeinen Rhythmus: 왘 Definieren des Prozesses: Das erste Konzept der Anwendung wird erarbeitet.
Die Kundenwünsche werden zusammengetragen. In dieser Phase kommen Kunde und Anwendungsentwickler zusammen und treffen eine Übereinkunft hinsichtlich des idealen Geschäftsprozesses. 왘 Modellentwurf: Die technischen Details von Anwendungskonzept und Ge-
schäftsprozess werden definiert, beispielsweise die Einzelheiten des Komponentenmodells, die Bearbeitungsweise der Transaktionen sowie die zu verwendende Datenbank. Darüber hinaus werden in dieser Phase Begrifflichkeiten und Konzepte definiert. Diese Terminologie wird im gesamten Zyklus zur Refe-
Entwickeln von Anwendungen
57
renzierung spezifischer Kontexte eingesetzt. Zu diesem Zeitpunkt beginnt auch die Dokumentation. 왘 Entwickeln eines Prototyps: Basierend auf dem Entwurfsmodell wird der erste
Prototyp entwickelt. Durch dieses Modell werden die Kernfunktionen implementiert und bestimmte Annahmen getestet. 왘 Implementieren des Geschäftsprozesses: Basierend auf Entwurfsmodell und
Prototyp wird eine detaillierte Implementierung des Geschäftsprozesses vorgenommen. 왘 Testen der Implementierung: Die Implementierung muss getestet werden, da-
mit eine richtige und konsistente Implementierung des Geschäftsprozesses gewährleistet ist. 왘 Abschließende Dokumentation von Code und Anwendung: Nach dem Test-
lauf müssen Quellcode und Anwendung dokumentiert werden. Die Dokumentation zielt darauf ab, das Vorgehen und die Beweggründe der Anwendungsentwicklung zu erläutern. 왘 Herausgeben der Anwendung: Abschließend wird das System an den Kunden
ausgeliefert. Der Kunde kann dann das System zur Vereinfachung seines Geschäftsbetriebes einsetzen. Zu diesem Zeitpunkt beginnt der gesamte Prozess von vorn, um die nächste Version der Anwendung zu entwickeln. 왘 Beheben von Bugs: Da kein System fehlerfrei ist, ist das Beheben von Bugs so-
wie das erneute Verteilen der verbesserten Anwendung erforderlich. Der Anwendungsentwicklungszyklus definiert die allgemeine Reihenfolge der verschiedenen Schritte, es wird jedoch nicht definiert, wie diese Schritte umgesetzt werden. Müssen die Schritte beispielsweise seriell erfolgen, oder ist eine parallele Ausführung möglich? Das Wasserfallmodell stellt eine Form dar, diesen Zyklus näher zu verdeutlichen.
3.1.2 Das Wasserfallmodell Die klassische Implementierung des Anwendungsentwicklungszyklus (ADC) ist das Wasserfallmodell, bei dem die vorzunehmenden Schritte nacheinander durchgeführt werden. Jeder Schritt muss abgeschlossen sein, bevor der nächste Schritt in Angriff genommen wird. Abbildung 3.1 zeigt dieses Modell. In diesem Schaubild muss jeder der Behälter gefüllt werden, bevor der nächste Behälter gefüllt werden kann, genau wie im Entwicklungszyklus. Jeder Schritt muss abgeschlossen sein, bevor der nächste beginnen kann. Diese Form der Entwicklung erleichtert die Steuerung des Projekts, da jeder Schritt als Prüfpunkt dient, bei dem Faktoren wie Kosten, Zeit u.a. berücksichtigt werden können.
58
Entwickeln von Anwendungen
Abbildung 3.1 Wasserfallmodell für die Anwendungsentwicklung
Da jedoch keine optimale Ressourcennutzung gegeben ist, stellt diese Methode nicht das beste Modell dar. Ein typisches Entwicklerteam besteht aus Experten, die sich auf bestimmten Gebieten spezialisiert haben. Wenn der Programmierer beispielsweise auf das Entwurfsmodell warten muss, führt dies zu einer Leerlaufzeit für diesen Programmierer. Ein weiteres Problem beim Wasserfallmodell ist die klare Trennung zwischen Designer, Entwickler und Programmierer. Ein Programmierer oder Entwickler verfügt häufig über einen Einblick, den der Designer nicht besitzt, was bei unzureichender Kommunikation zu Mehrkosten durch Entwurfsänderungen führen kann. Stellen Sie sich beispielsweise vor, dass eine bereits getroffene Entscheidung rückgängig gemacht werden muss. Abbildung 3.2 veranschaulicht die hierbei entstehenden Kosten. Das obige Schaubild zeigt, dass sich bei der Zurücknahme einer Entscheidung die Kosten mit der Anzahl der neu durchzuführenden Schritte erhöhen. Nehmen Sie an, dass ein Fehler bei der Herausgabe der Anwendung dazu führt, dass der Zyklus in der Entwurfsphase neu begonnen werden muss. Dies bedeutet, dass fünf Schritte wiederholt werden müssen, und die Kosten für dieses Vorgehen sind enorm, da alle beteiligten Personen erneut in die Entwurfsphase eintreten müssen. Das Wasserfallmodell ist bei der Entwicklung umfangreicher Anwendungen nicht sehr effektiv und wird daher in den meisten Fällen nicht verwendet.
Entwickeln von Anwendungen
59
Abbildung 3.2 Mehrkosten durch das Zurücknehmen einer zuvor getroffenen Entscheidung
3.1.3 Gefahren bei der Implementierung einer Anwendung Ein Merkmal der Computerindustrie ist ihre Unbeständigkeit. Vor weniger als zehn Jahren wurden DOS-Anwendungen noch als Nonplusultra betrachtet. Heute würde niemand auch nur daran denken, zur Entwicklung einer Schnittstelle DOS-Interrupts zu verwenden. Die Industrie unterliegt einem ständigen Wandel. Die einzige konstante Größe ist die, dass es keine konstante Größe gibt. Viele Entwickler versuchen jedoch, diese permanenten Veränderungen zu ignorieren und langfristig einsetzbare Anwendungen zu entwickeln. Frameworks Ein klassischer Ansatz bei der Anwendungsentwicklung ist die Erstellung einer isolierten Schicht in der Anwendung. Wenn eine Änderung auftritt, kann diese Isolationsschicht an die aufgetretenen Änderungen angepasst werden. Diese Isolationsschicht wird häufig als Framework bezeichnet. Ein Framework wird als der Versuch definiert, einen Satz funktioneller Dienstprogramme zu entwickeln, die der Erleichterung spezieller Probleme dienen. Bei diesen Dienstprogrammen kann es sich um Tools, Bibliotheken oder um eine Kombination aus beidem handeln. Im Extremfall können Frameworks so allumfassend sein, dass der Eindruck entsteht, dass mit diesen die Entwicklung der Isolationsschicht erfolgt. So wird die gesamte Lösung in einen vollständigen Zyklus überführt. Die Lösung wird zu einem Problem, das anschließend zu einer Lösung wird. Dies ergibt nicht immer Sinn.
60
Entwickeln von Anwendungen
Der Schlüssel bei Entwurf und Entwicklung eines sinnvollen Frameworks besteht in dem Verständnis, dass über das Framework verschiedene Problemen gelöst werden müssen. Bei einer geschäftlichen Konferenz besteht beispielsweise das Problem darin, eine oder mehrere Konferenzen und deren Teilnehmer zu koordinieren, und genau dieses Problem muss mit einem Framework gelöst werden. Herstellerabhängigkeit Bei der Anwendungsentwicklung müssen verschiedene Technologien eingesetzt werden. Der Faktor des Herstellers kommt zum Tragen, wenn bei der Anwendungsentwicklung Technologien verwendet werden, die nur über einen Hersteller bezogen werden können. Je größer das Projekt, desto schwerer wiegt dieser Faktor und wird zu einem Problem, wenn der Geschäftsprozess vom Wohlwollen des Produktherstellers abhängt. Dennoch wird ein gewisser Grad der Herstellerabhängigkeit häufig in Kauf genommen, um ein Projekt abschließen zu können. Diese Situation tritt natürlich nicht nur bei der Entwicklung von Computerlösungen auf. Wenn Sie beispielsweise einen Mercedes fahren, lassen Sie diesen nicht in einer BMW-Werkstatt reparieren. Der BMW-Händler verfügt nicht über die Ausstattung zur Reparatur eines Mercedes. Daher können Sie nur hoffen, dass sich Ihre Investition gelohnt hat und dass der Mercedes-Händler weiterhin den nötigen Service bereitstellt. Falls dies nicht der Fall ist, können Sie nur auf eine andere (bessere) Lösung warten. Der Punkt ist, dass Sie einem Hersteller nicht blind vertrauen sollten, sondern dass Sie Ihre Hausaufgaben machen und herausfinden, welche Lösung zum gegebenen Zeitpunkt und in naher Zukunft die beste Option darstellt.
3.1.4 Die Lösung – Der iterative, kontextuelle Entwurf Derzeit stellt die beste Methode zur Implementierung des Anwendungsentwicklungszyklus das iterative, kontextuelle Entwurfsmuster dar. Dieses Muster wird nicht im Anhang erläutert, sondern nachstehend beschrieben. Das iterative, kontextuelle Entwurfsmuster umfasst zwei Hauptkonzepte: 왘 Iterativ: Das schrittweise Lösen eines Problems. Gute Systeme werden unter
Verwendung evolutionärer Verfahren entwickelt. 왘 Kontextuell: Zielt darauf ab, den Kundenwünschen besondere Aufmerksam-
keit zu schenken und hierzu die Kommunikation zwischen allen Beteiligten Parteien zu verbessern.
Entwickeln von Anwendungen
61
Iterativer Entwurf Ein iterativer Ansatz ist der Prozess, eine Anwendung auf evolutionäre Weise zu entwickeln. Der Vorteil eines schrittweisen Vorgehens ist der, dass nicht alle Probleme gleichzeitig gelöst bzw. alle Ziele gleichzeitig erreicht werden müssen. Beim iterativen Entwurf können neue Systeme aus alten Systemen entwickelt werden, indem das alte System aufgerüstet wird. Der iterative Entwurf sieht vor, Lösungen immer wieder auf die aktuellen Probleme zu untersuchen. Während der Lösungsimplementierung werden bestimmte Feststellungen gemacht – einige Dinge sind optimal, andere nicht. Diese Inkonsistenzen werden notiert und in zukünftigen Softwareversionen berücksichtigt. Die beste Methode, den iterativen Entwurf zu verstehen, besteht darin, sich die zu entwickelnde Anwendung als eine Reihe von Inseln vorzustellen, die miteinander kommunizieren. Siehe hierzu Abbildung 3.3.
Abbildung 3.3 Technologieinseln
Jede Insel repräsentiert einen Teil Ihrer Anwendung. Eine Insel stellt eine Gruppe von Funktionen dar, die von den anderen Inseln isoliert ist. Einige Insel sind rein technologisch (beispielsweise ein Datenbank- oder Dateiserver), einige Inseln basieren auf einer gewissen Logik (beispielsweise ein Verwaltungssystem oder Produktionsstatistiken). Einige dieser Inseln können eine ähnliche Logik besitzen, jedoch eine andere Technologie verwenden, und umgekehrt. Zur Übertragung von Daten von einer Insel zu einer anderen wird ein Protokoll eingesetzt. Das Protokoll stellt das gemeinsame Kommunikationsmittel zwischen zwei Inseln dar. Dieser Vorgang lässt sich mit zwei Personen vergleichen, die sich auf die Verständigung in einer bestimmten Sprache geeinigt haben. Beispiele für die verwendeten Protokolle sind HTTP (Hypertext Transfer Protocol), DCOM
62
Entwickeln von Anwendungen
(Distributed Component Object Model) und Kommunikationsprotokolle für den Datenbankzugriff. Der iterative Ansatz Beim iterativen Ansatz werden die Inseln Schritt für Schritt erstellt. Inseln können hinzugefügt, entfernt oder aktualisiert und in Form von »Inselketten« angeordnet werden, durch die beispielsweise ein Verwaltungssystem entsteht. Anschließend können mehrere Inselketten zusammen eine Anwendung bilden. Das Entwicklen der Anwendung ähnelt dem Erstellen einer eigenen Welt mit Hilfe von Inseln. Beim iterativen Ansatz beginnen Sie mit einigen wenigen Inseln und erstellen nach Bedarf weitere Inseln. Zur Strukturierung der Anwendung können Sie einen Plan erstellen, in dem Sie festhalten, welche Funktionen die einzelnen Inseln und Inselketten übernehmen sollen. Die zentralen Inseln sollten die Kernfunktionalität bereitstellen. Lassen Sie uns dieses Entwurfsmuster auf ein praktisches Beispiel anwenden. Angenommen, Sie benötigen ein Konferenzanmeldungssystem, mit dem ein Teilnehmer die Anmeldung an einer Konferenz selbst vornehmen kann. Der erste Schritt besteht in der Definition der drei Hauptinselketten (Datenbank, Anwendungslogik und Webseiten). Die Datenbankinselkette ist verantwortlich für die Datenbankfunktionalität. Sie enthält die Datenbank, gespeicherte Prozeduren und Datenbanktabellen. Die Schicht der Anwendungslogik enthält die verschiedenen Geschäftsobjekte, die in den einzelnen Prozessen implementiert wurden. Die Inselkette mit den Webseiten definiert die verschiedenen Benutzerschnittstellenkomponenten. Bei näherem Hinsehen enthält die Inselkette der Anwendungslogik drei Inseln – die Anmeldung, die Benutzerverwaltung und die Anwendungslogik für die Konferenzanmeldung, wie dargestellt in Abbildung 3.4.
Abbildung 3.4 Plan der Inseln und Inselketten
Entwickeln von Anwendungen
63
Beim iterativen Ansatz werden zunächst einige wenige Inseln erstellt. Die Idee sieht vor, das Konzept der Anwendung basierend auf den funktionellen Komponenten zu verstehen. Anschließend, basierend auf diesen Inseln, wird die Kommunikation zwischen den Inseln definiert. So werden die Abhängigkeiten der einzelnen Anwendungselemente eingerichtet. In dieser Phase werden einige Inseln implementiert, damit das Konzept teilweise erprobt werden kann. Wenn einige der Konzepte sich als falsch erweisen, können die Inseln neu geplant und strukturiert werden. Dies führt zu anderen Abhängigkeiten, und wieder müssen einige der Inseln implementiert werden, um die neuen Konzepte auf ihre Richtigkeit zu prüfen. Dieser Vorgang wird solange wiederholt, bis die Anwendung fertiggestellt ist. Ein Nachteil dieses Ansatzes liegt darin, dass er langsam ist und nach dem Trialand-Error-Prinzip verfährt. Der Grund für diese Kritik ist die Tatsache, dass ständig Verbesserungen und Aktualisierungen vorgenommen werden, d.h., es erfolgt eine immer neue Iteration. Man könnte dies mit einem Hauseigentümer vergleichen, der ständig Reparaturen an seinem Haus vornimmt. Das Haus unterliegt einem ständigen Wandel. Sie müssen in diesem Fall in der Lage sein, die Dinge so zu belassen, wie sie momentan sind und für einen späteren Zeitpunkt eine Reparatur anberaumen. Es gibt immer eine andere Version. Stellen Sie sich nun die Situation vor, in der Sie einen Plan erstellt haben und jetzt eine neue Technologie oder einen neuen Geschäftsprozess einführen möchten. Das Problem besteht darin, dass neue Inseln nicht ohne weiteres hinzugefügt werden – es sind Legacykomponenten vorhanden. Und in einigen Fällen werden aufgrund der knappen Zeitbemessung für die Inselanpassung so genannte Hacks oder Shortcuts verwendet. (Tatsächlich werden immer Hacks und Shortcuts verwendet.) Beim Inselansatz kommt es darauf an, dass die Designer in der Entwurfsphase eine gute Entwurfsspezifikation vorlegen. Die Erfahrung zeigt, dass die Schnittstelle mit einer guten Entwurfsspezifikation viele Entwurfsiterationen überstehen und leichter an ein Legacyszenario angepasst werden kann. Einen großer Vorteil des Inselansatzes stellt die Möglichkeit dar, individuelle Systeme zu entwickeln und zu testen und anschließend zusammenzuführen. Die Granularität des Systems richtet sich in diesem Fall nach den verschiedenen Teilsystemen. Die Granularität stellt die Codemenge einer einzelnen Insel dar. Ist der Code umfangreich, ist die Granularität hoch, ist der Code nicht umfangreich, liegt die Granularität niedrig und ist damit gut. Was Sie erreichen möchten, ist eine mittlere Granularität. Eine hohe Granularität erschwert das Testen, da zu viele Parameter geprüft werden müssen. Eine niedrige Granularität ist leichter zu testen, die Verfolgung aller Teilsysteme wird jedoch zu schwierig. Die exakte Definition einer mittleren Granularität wird in den folgenden Kapiteln definiert.
64
Entwickeln von Anwendungen
Der Cathedral-Ansatz Sie können Ihr System auch nach dem CathedralAnsatz gestalten. Bei diesem Ansatz wird ein System entwickelt und anschließend für eine bestimmte Zeit beibehalten. Die verwendete Technologie basiert auf den zum Entwicklungszeitpunkt verfügbaren Technologien, auch Technologiesnapshot genannt. Eine Technologie, die nach diesem Snapshot herausgebracht wird, wird ignoriert, da ansonsten der Entwicklungszyklus gestört würde. Der Zeitraum, für den eine Anwendung beibehalten wird, richtet sich nach dem Investitionsumfang. Häufig behalten Designer und Entwickler einen Lebenszyklus für n Jahre bei, und nach Ablauf dieser Zeitspanne wird ein nahezu komplett neues System entwickelt. Einige der Legacykomponenten werden hierbei beibehalten, dies jedoch lediglich aus Zeit- und Kostengründen. Das Ersetzen einer Technologie wird im Allgemeinen durch veraltete Technologien erforderlich. Das neue System muss funktionieren, und dies bedeutet einen zusätzlichen Stressfaktor für das Entwicklungteam. Beim Cathedral-Ansatz stellt sich das Problem, dass es nicht möglich ist, ein System basierend auf einem Technologiesnapshot zu entwickeln. Ein typisches Projekt nach dem Cathedral-Ansatz beinhaltet eine Definition der technologischen Anforderungen und benennt die technologischen Voraussetzungen. Das Projekt dauert etwa ein Jahr, und während dieses Jahres gibt es Änderungen im Technologiebereich. Im besten Fall handelt es sich lediglich um minimale Änderungen, im schlimmsten Fall umfassen die Änderungen grundlegende Neuerungen, beispielsweise die Einführung des Web. Auf diese Weise ist die ursprüngliche Technologie veraltet, bevor das Projekt abgeschlossen wurde. Eine neue technologische Definition ist erforderlich. Dies führt zu einem Mehraufwand bezüglich Zeit und Kosten. Ein weiterer Nachteil des Cathedral-Ansatzes liegt darin, dass Sie nicht garantieren können, dass eine Anwendung auch in Zukunft effektiv einsetzbar ist. Wenn die Anwendung beispielsweise eine Lebensspanne von fünf Jahren aufweist, müssen verschiedene neue technologische Komponenten implementiert werden, um die Anwendung dauerhaft einsetzbar zu machen. Die neue Technologie ist jedoch noch nicht vollständig erprobt, d.h. es gibt keine Muster, die angewendet werden können, da es sich um eine völlig neue Domäne handelt. Daraus folgt, dass Entwurfsfehler entstehen, die wiederum zu einem erhöhtem Zeitaufwand, höheren Kosten usw. führen. Iterativer und Cathedral-Ansatz im Vergleich Der iterative Ansatz beinhaltet ständige Änderungen, und ein solcher ständiger Wandel kann sehr kostenintensiv sein. Ein schlecht verwaltetes Projekt kommt nie zu einem richtigen Ende. Stattdessen wird es ständig weitergeführt. Daher wird häufig der CathedralAnsatz bevorzugt. Beim Cathedral-Ansatz gibt es eine Reihe von Zielen, die den
Entwickeln von Anwendungen
65
Anfang und das Ende eines Projekts markieren. Diese Ziele stellen jedoch gleichzeitig die Falle dar, denn sie ändern sich stetig. Obwohl ich den Cathedral-Ansatz nicht befürworte, kann dieser gelegentlich nicht umgangen werden. Es gibt beispielsweise alte Systeme, die in einer eigenen Welt existieren, und diese Systeme bedürfen einer beständigen Justierung. Das Einbringen dieser Systeme in neue Entwicklungen stellt hierbei keine Option dar. Der einfachere Ansatz ist deshalb der, Ressourcen in das alte System fließen zu lassen und gleichzeitig ein ganz neues System zu entwickeln, mit dem die gleichen Ziele erreicht werden. Die Lösungen für das Jahr 2000-Problem waren Beispiele für dieses Vorgehen. Nur wenige Unternehmen integrierten hier alte und neue Technologien. Stattdessen wurde der zweigleisige Ansatz gewählt, der einfacher und weniger risikoreich ist. Kontextueller Entwurf Der zweite Teil des iterativen, kontextuellen Entwurfs besteht in der Definition des Kontextes. Bei Projekten mit iterativem Entwurf wird häufig zunächst ein Plan erstellt, aber verschiedene Teams verfahren unterschiedlich, sodass Inseln entwickelt werden, die nicht definiert wurden. Manchmal sind diese Änderungen richtig, doch häufig sind diese Zusätze das Ergebnis aus Lücken im anfänglichen Entwurf, die Bestandteil des gesamten Anwendungsentwurfs hätten sein müssen. Diese fehlenden Details erzeugen Inkonsistenzen, die wiederum zu Testproblemen führen. Zur Einbringung dieser fehlenden Details in die Inselerstellung müssen zwischen den Projektteams Kommunikationskanäle eingerichtet werden. Die Kommunikation ist äußerst wichtig, da der iterative Entwurf ständige Änderungen beinhaltet. Alle Teams müssen über Änderungen informiert werden, sonst können Teilsysteme nicht richtig funktionieren. Der kontextuelle Entwurf umfasst das Projektmanagement, dessen Erläuterung jedoch nicht im Rahmen dieses Buches erfolgen kann. Empfehlenswert ist hierzu das Buch Contextual Design (siehe Anhang), in dem mit aller Ausführlichkeit auf die zugrunde liegenden Konzepte eingegangen wird. Resümee Der Vorteil einer iterativen, auf Inseln basierenden Entwicklung ist der, dass Sie neue Technologien einbringen können, ohne das gesamte System umgestalten zu müssen. Sie können die Auswirkungen einer neu hinzugekommenen Insel auf die ganze Inselkette erfassen. Basierend auf diesen Auswirkungen kann die Insel erweitert oder abgelehnt werden.
66
Entwickeln von Anwendungen
Die sich ergebenden Auswirkungen sind wichtig, da Sie anzeigen, welche Fähigkeiten Ihre Anwendungen aufweist. Es gibt zwei Arten von Auswirkungen, technologische Auswirkungen und die Folgen der Interaktion von Technologien. Die technologischen Auswirkungen müssen abgeschätzt werden, denn normalerweise ist die Technologie entweder noch neu und unerprobt oder wurde zumindest noch nicht zur Lösung des betreffenden Problems eingesetzt. Das Definieren der Auswirkungen ist schwierig, da keine direkten Erfahrungswerte herangezogen werden. Es gibt jedoch häufig indirekte Erfahrungswerte, die extrapoliert werden können. Die Berücksichtigung von Auswirkungen ist besonders wichtig. Eine leistungsstarke Technologie, die allein betrachtet gut funktioniert, kann in der vorliegenden Situation möglicherweise nicht in andere Komponenten integriert werden. In diesem Fall kann die Technologie nicht eingesetzt werden. Betrachten Sie beispielsweise die Auswirkungen bei der Adaptation von HTML (Hypertext Markup Language) als Benutzerschnittstelle. HTML unterscheidet sich stark von jedem anderen Benutzerschnittstellentyp, daher erscheint die Einschätzung der Folgen nahezu unmöglich. HTML stellt jedoch keine Neuentwicklung, sondern eine Weiterentwicklung von SGML (Standard Generalized Markup Language) dar. Daher sollten Sie bei der Betrachtung von HTML die Erfahrungen mit SGML berücksichtigen. Eine Überlegung ist, dass SGML zu komplex ist; HTML wurde aus diesem Grund vereinfacht. SGML ist ein Standard, der plattformübergreifend ist, und HTML weist diesen Vorteil ebenfalls auf. SGML ist jedoch dokumentbasiert, d.h., es weist keine vielfältige Funktionalität auf. In seiner reinen Form weist auch HTML diese Beschränkung auf. Daher ist als Auswirkungen der Verwendung von HTML eine plattformübergreifende Benutzerschnittstelle zu erwarten, die jedoch in ihrer Funktionalität eingeschränkt ist. Stellen Sie sich vor, Sie kombinieren die HTML-Schnittstelle mit serverseitigen Daten. Hierzu stehen verschiedene Methoden zur Verfügung. Eine Methode wäre, alle HTML-Seiten mit Hilfe eines Stapelverarbeitungsprozesses zu generieren, der zu bestimmten Zeitpunkten ausgeführt wird. Die Auswirkung hierbei ist, dass die Aktualität der Anwendungsdaten davon abhängt, wann der Stapelverarbeitungsprozess das letzte Mal ausgeführt wurde. Eine weitere Methode besteht darin, einige Komponenten zu schreiben, mit denen die Seite dynamisch erzeugt wird. Dies führt jedoch zu der Frage, welche Technologie verwendet wird, und es sind weitere Folgen zu berücksichtigen. Sollte ASP (Active Server Pages) oder ISAPI (Internet Server Application Programming Interface) verwendet werden? Der verbleibende Teil des Buches richtet sich auf die Anwendung der Technologie und darauf, die Auswirkungen der jeweiligen Technologie richtig einzuschätzen.
Entwickeln von Anwendungen
67
3.2 Starten eines Projekts Jetzt, nachdem die Konzepte definiert sind, wollen wir uns der Definition eines Projekts und dessen Umsetzung zuwenden. Das verwendete Projekt wurde tatsächlich entwickelt und für Microsoft Deutschland implementiert. Auf Teile dieses Projekts wird im gesamten Buch verwiesen. Da durch ein Projekt jedoch niemals alle Funktionen implementiert werden können, werden darüber hinaus auch andere Implementierungsszenarien betrachtet.
3.2.1 Framework: ja oder nein? Bei der Entwicklung einer Anwendung müssen Sie auch entscheiden, ob Sie zur Vereinfachung der Entwicklung weiterer Anwendungen ein Framework einsetzen oder nicht. Die Antwort lautet, dass Sie entsprechend den Anwendungszielen unter Verwendung des iterativen Entwurfs entwickeln. Wenn das Ziel der Anwendung in der Entwicklung eines Frameworks besteht, dann sollten Sie dieses Ziel schrittweise umsetzen. Versuchen Sie nicht, den Cathedral-Ansatz zu verwenden, da dies lediglich zu Problemen führt. Stellt die Entwicklung eines Frameworks kein Ziel dar, sollten Sie auch keines entwickeln – das Entwickeln von Frameworks ist schwierig und erfordert umfassende Kenntnisse zu Domäne und verwendeter Technologie.
3.2.2 Einige Hintergrundinformationen Beim Kombinieren von Anwendungsentwicklungszyklus, iterativem, kontextuellem Entwurf und Kontext stellt der ersten Schritt das Hinterfragen des Hintergrunds eines Projekts dar – Warum ist das Projekt erforderlich? Dieser Vorgang wird als Erarbeitung des Projektkontextes bezeichnet. In unserem Beispiel lautet die Antwort auf diese Frage, dass Microsoft zu einem sehr großen Unternehmen geworden ist, und sich daher – wie bei allen großen Unternehmen – einige Prozesse schneller ändern als andere. Microsoft Deutschland benötigte ein System für die Konferenzanmeldung, und obwohl Microsoft ein solches System besitzt, genügte dieses System nicht den speziellen Anforderungen der deutschen Firmenniederlassung. Microsoft führte zur Lösung des Problems eine Untersuchung bei allen Zweigniederlassungen durch, die jedoch Zeit beanspruchte. Deshalb entschied sich Microsoft Deutschland zur Entwicklung einer Zwischenlösung, die die vorhandene Lücke schließen sollte, bis das unternehmensweite System verfügbar wurde.
68
Entwickeln von Anwendungen
Die Hintergrundinformationen zu diesem Projekt lauteten folgendermaßen: 왘 Kurzfristigkeit: Es handelte sich um ein zeitlich begrenztes Projekt. Es bestand
keinerlei Notwendigkeit zur Entwicklung eines evolutionären Systems, da das System ein Jahr später verworfen werden sollte. 왘 Legacydaten: Die Anwendung würde Legacydaten erzeugen, die in das neue
Unternehmenssystem von Microsoft eingebettet werden müssten. Darüber hinaus war eine Interaktion der vorhandenen Daten und Anwendungen anderer Microsoft-Zulieferer erforderlich. 왘 Erfahrungswert: Es musste geeignete Technologie eingesetzt werden, aber die
Implementierung bestand auch im Testen anderer Microsoft-Technologien, um Erfahrung zu sammeln. Zum vollständigen Verständnis der Entwicklung des großen Unternehmenssystems mussten Erfahrungswerte durch kleine Systeme zusammengetragen werden, diese Erfahrung sollte anschließend als Muster für das große System verwendet werden. Diese Hintergrundinformationen vermitteln den Kontext der Anwendung – Microsoft beabsichtigte, wenig zu investieren, aber möglichst viel Erfahrung zu sammeln. Erarbeiten des Kontextes Die Erarbeitung des Kontextes für das Konferenzanmeldungssystem war nicht einfach, da diese Lösung nur temporär das eigentliche System ersetzen sollte. Diese Situation macht das Entwickeln einer Anwendung besonders interessant, da Sie sowohl die Produkteinführungszeit als auch die Verwendungsdauer des Produkts berücksichtigen müssen. Der Kontext wird aus den Informationen erarbeitet, die während der Meetings und im Gespräch mit den beteiligten Personen zusammengetragen werden können. Hierbei werden keine Projekteinzelheiten, sondern Hoffnungen und Erwartungen besprochen. In dieser Phase erfahren Sie, auf welche Punkte Wert gelegt wird und welche Punkte dem Kunden weniger wichtig sind. Beim Konferenzanmeldungssystem beispielsweise war ein sauberer, wieder verwendbarer Entwurf nicht wichtig, die Produkteinführungszeit und die Stabilität stellten die Hauptziele dar. Dies ist wichtig, da ein sauberer Entwurf Zeit und Kosten erfordert. Dies bedeutet nicht, dass ein Projekt keinen Entwurf benötigt. Es heißt lediglich, dass sich der Entwurf auf den Erhalt eines lauffähigen Systems unter Verwendung neuer Technologie konzentriert. Das Verständnis der grundlegenden Ziele ermöglicht das Implementieren der Funktionen, mit denen der Kunde unter geringstmöglichem Zeit- und Kostenaufwand zufrieden gestellt werden kann. Aber genau dieser Punkt wird von vielen
Starten eines Projekts
69
Softwareentwicklern falsch verstanden. Softwareentwickler möchten häufig möglichst »reine« Anwendungen entwickeln, hierzu ist jedoch viel Zeit und Geld erforderlich. Worauf es ankommt, sind jedoch die Kundenwünsche. Üblicherweise sollten Sie mit den Endbenutzern sprechen. Verlassen Sie sich nicht auf die gedruckte Dokumentation, da in dieser weder Emotionen noch Hoffnungen ausgedrückt werden, es werden hier ausschließlich die Unternehmensvorstellungen dargestellt. Emotionen spielen im Prozess der Anwendungsentwicklung eine sehr große Rolle. Missfällt dem Kunden eine Kleinigkeit, kann dies dem Gesamtbild schaden. Fällt dem Kunden jedoch eine Kleinigkeit positiv auf, kann dies dem Gesamtbild förderlich sein. Durch das Gespräch mit den Endbenutzern wird außerdem sichergestellt, daß die Wünsche des Managements mit denen der Endbenutzer übereinstimmen. Besteht in diesem Punkt ein Konflikt, muss dieser umgehend gelöst werden. Dieser Schritt in der Phase der Kontexterarbeitung kann darüber entscheiden, ob die Anwendung akzeptiert wird oder nicht. Nach meiner Erfahrung wird bei Unstimmigkeiten zwischen den Erwartungen von Endbenutzer und Management häufig der Entwickler der Anwendung für daraus resultierende Probleme verantwortlich gemacht. Häufig wird in diesem Fall die Anwendung als inakzeptabel betrachtet, und da der Vertrag scheinbar nicht eingehalten wurde, kommt es vor, dass der Lieferant nicht voll bezahlt wird. Wie viele Hintergrundinformationen sind erforderlich? Das Problem beim Zusammentragen der Informationen besteht darin, dass Sie mit Informationen überschwemmt werden. Wann liegen zu wenig Informationen, wann zu viele Informationen vor? Erfolgreiche Projekte folgen der nachstehenden Faustregel: Wenn Sie für eine Insel einer Anwendung verantwortlich sind, dann tragen Sie die nötigen Informationen für diese Insel zusammen. Alle weiteren Informationen sind in der Regel irrelevant. Die einzige Ausnahme für diese Regel besteht darin, dass jeder ein Gesamtbild davon haben muss, welche Funktion die Anwendung übernimmt, damit das gemeinsame Ziel erreicht werden kann. Dies ist ein scheuklappenartiger Ansatz, aber er ist erfolgreich. Wenn Sie nicht diesem Ansatz folgen, wird es häufig zu Problemen kommen, die eigentlich nicht vorhanden sind oder nicht in Ihrem Verantwortungsbereich liegen, was wiederum zu einem höheren Zeit- und Kostenaufwand führt.
70
Entwickeln von Anwendungen
Beispielsweise war einer der Punkte, den wir im Rahmen des Kontextes festgelegt haben, dass eine Insel dann richtig entworfen und implementiert wurde, wenn dies im Interesse des Kunden des Konferenzanmeldungssystem erfolgt. Im Beispiel mussten Entwurf und Implementierung lediglich »gut genug« sein. Wieder war dies ein Ergebnis der Tatsache, dass es sich bei dem Projekt um eine Zwischenlösung handelte. Eine Lösung nach dem Prinzip »gut genug« mag für einige Designer und Entwickler ein harter Brocken sein, aber die Situation war die, dass einige Bestandteile des Systems in absehbarer Zeit ausgemustert werden würden.
3.2.3 Anwendungsanforderungen Nachdem der Kontext definiert ist, muss der erste Schritt im Anwendungsentwicklungszyklus implementiert werden. Hierbei wird der Prozess definiert. Bei diesem Schritt wird der allgemeine Entwurf der Insel und Inselketten geplant. In vielen Büchern wird dieser Vorgang als Entwurf der grundlegenden Architektur bezeichnet. Bei diesem Schritt wird die Dynamik der Anwendung beschrieben und die Intention der Anwendung definiert. Die Kombination dieser zwei Aspekte soll Sie dabei unterstützen, die Systemparameter für die Optimierung zu ermitteln. Die Optimierung bezeichnet das Ändern von Teilen eines Systems und das Ermitteln der Auswirkungen, die sich hieraus für die weiteren Bestandteile des Systems ergeben. Bei der Definition der grundlegenden Architektur werden die Ziele der Anwendung zeitweise ignoriert. Sie überlegen, wie die verschiedenen Technologien und Ideen zur Lösung Ihres Problems eingesetzt werden könnten. In dieser Phase sollten Sie verschiedene Ideen und Konzepte ausprobieren. Sie möchten sicherstellen, dass die implementierte auch die optimale Lösung darstellt. Das Testen von Technologien zu einem späteren Zeitpunkt führt zu einer Störung des Entwicklungsprozesses. Zur Definition des Prozesses müssen folgende Dinge vorliegen: 왘 Ein Domänenmodell, mit dem die Anwendung in einfachen Worten definiert
wird 왘 Eine definierte Technologieplattform, die zur Umsetzung der Anwendung ein-
gesetzt wird 왘 Eine Liste mit Anwendungsfällen (Use Cases)
Die Technologieplattform wird nicht behandelt, da vorausgesetzt wird, dass Sie Windows DNA verwenden. Wie die Dienste von Windows DNA angewendet werden, wird im weiteren Verlauf des Buches erläutert.
Starten eines Projekts
71
Domänenmodell Das Domänenmodell dient der Anwendungsbeschreibung in Worten, die allgemein verständlich sind. Es stellt die Grundlage Ihrer Anwendung dar. Idealerweise handelt es sich bei dem Domänenmodell um einen geschriebenen Text. Der Text sollte präzise und interessant sein. Das Problem bei einem langweiligen Domänenmodelltext liegt darin, dass die Aufmerksamkeit der Leser nachlässt und ihnen deshalb wichtige Punkte entgehen. Das Domänenmodell enthält die Begriffe, die im verwendeten Kontext ausführlich beschrieben werden. Das Wort »Titel« beispielsweise hat mehrere Bedeutungen, es kann den Titel einer Person oder den Titel eines Buches bezeichnen. In einem Domänenmodell wird explizit definiert, was für eine Bedeutung Begriffe wie »Titel« besitzen. Ein guter Domänenmodelltext weist folgende Merkmale auf: 왘 Es handelt sich nicht lediglich um eine Aufzählung, da Informationen in Form
einer Auflistung nicht richtig vermittelt werden können. Das Schreiben einer guten und erklärenden Liste ist schwierig. Eine schlechte Liste deutet entweder auf Faulheit oder Zeitmangel hin. 왘 Der Text enthält nicht zu viele Schaubilder. Üblicherweise werden Schaubilder
eingefügt, um dem Dokument ein wichtigeres Aussehen zu verleihen. Schaubilder unterbrechen jedoch den Lesefluss, und wenn das Diagramm nicht ordnungsgemäß platziert ist, lenkt es den Leser vom Thema ab oder, noch schlimmer, bringt den Leser dazu, nur die Diagramme zu lesen. Der Text wird ignoriert und wichtige Details werden übersehen, was bei der späteren Implementierung zu Problemen führen kann. 왘 Ein guter Domänenmodelltext ist nicht zu lang. Es sollte nicht mehr als Domä-
nenmodell und Anwendung erläutert werden. Obwohl es eindrucksvoller erscheinen mag, mit einer 30seitigen Erklärung aufzuwarten, ist der Text deswegen nicht besser. Der Schlüssel liegt darin, präzise zu sein. Schreiben eines Domänenmodelltextes Wie schreibt man einen Domänenmodelltext? Die Antwort ist, mit Übung und Geduld. In technischen Hochschulen und Computerschulen wird dem Berichteschreiben häufig keine besondere Aufmerksamkeit geschenkt. Die Hochschulen (und Sie vielleicht auch) setzen voraus, dass Ihnen dieses Wissen in der Schulzeit vermittelt wurde. Es besteht jedoch ein großer Unterschied zwischen der Fähigkeit, etwas in eigene Worte zu fassen, und der Fähigkeit, diese Gedanken präzise und strukturiert in einer Präsentation darzulegen, die allgemein verständlich ist. Richtig oder falsch, häufig wird die Qualität einer schriftlichen Präsentation als Indikator für die Qualität ei-
72
Entwickeln von Anwendungen
nes gesamten Projekts herangezogen. Die Fähigkeit, sich stilistisch gut und gleichzeitig klar auszudrücken, stellt einen unschätzbaren Wert dar. Ich weiß dies nur zu gut, da Deutsch für mich immer das schlimmste Lehrfach darstellte. In der Universität musste ich beinahe ein Semester wiederholen, da mein Bericht nicht lesbar war. Mein damaliger Professor besaß jedoch die Freundlichkeit, mir zu erläutern, aus welchen Gründen der Bericht ihm nicht akzeptabel erschien. Daher konnte ich drei Berichtsversionen später das Semester doch noch erfolgreich abschließen. Für meine Karriere als technischer Autor hat sich diese Lektion jedoch als sehr nützlich erwiesen, wie Sie sich vielleicht vorstellen können. Der Domänenmodelltext sollte in einem journalistischen Stil geschrieben werden, eher in der Form eines technischen Artikels als in Form eines Buches. Das Schreiben eines Buches und das Verfassen eines Artikels sind zwei völlig verschiedene Dinge. In einem Buch erzählen Sie eine Geschichte, die am Anfang des Buches beginnt und auf der letzten Seite des Buches endet. Das vorliegende Buch ist eine Geschichte über Windows DNA. In einem Buch wiederholen Sie sich in der Regel nicht, obwohl Sie vielleicht von Zeit zu Zeit einzelne Passagen wieder aufgreifen. Das Schreiben eines Buches führt zu einer detaillierten Geschichte, bei der eine Zusammenfassung nicht auf allen Ebenen erfolgt, die der Leser vielleicht erwartet. Ein Manager, der nur eine kurze Synopsis von Windows DNA benötigt, wird diese im vorliegenden Buch nicht finden. Dieser Manager sollte sich besser nach einer anderen Informationsquelle umsehen. Technische Artikel beschäftigen sich häufig mit einem spezifischen Themenbereich und umfassen keine Geschichte, die im ersten Absatz beginnt und mit dem letzten endet. Stattdessen enthalten technische Artikel eine Reihe von Informationsschleifen, wie in der folgenden Abbildung dargestellt. Im Schaubild ist ein kleiner Kreis dargestellt. Dieser repräsentiert den ersten Absatz des Artikels, in dem der Themenschwerpunkt erläutert wird. Der zweite Kreis ist größer – der zweite Absatz und vielleicht der dritte, in dem die Informationen des ersten Absatzes ausgeweitet werden. In diesen Absätzen werden die Informationen des ersten Absatzes erneut aufgegriffen und näher ausgeführt. Dieser Prozess setzt sich innerhalb des Artikels immer weiter fort. Warum ist dies besser? Es ist einfach, den Artikel an einem bestimmten Punkt enden zu lassen. Enthielt der ursprüngliche Artikel beispielsweise 5.000 Wörter und drei Schleifen, so können Sie durch Auslassen der letzten Schleife einen kürzeren Artikel für eine höhere Managementebene erstellen, die ein weniger detailliertes
Starten eines Projekts
73
Gesamtbild wünschen. Der Artikel enthält hierbei die gleichen Informationen, jedoch mit weniger Details.
Abbildung 3.5 Informationsschleifen in technischen Artikeln
Mit diesem Ansatz können Sie ein einzelnes Dokument auf die verschiedenen Ebenen von Management, Programmierern, Entwicklern usw. anpassen. Das Management der höchsten Ebene wird nur die einführende Zusammenfassung lesen, die Programmierer interessieren sich für alle sie betreffenden Details. Es ist möglich, bestimmte Bereiche zu kürzen und dennoch das Gesamtkonzept zu verstehen. Beispiel für einen Domänenmodelltext Als Beispiel möchte ich den Domänenmodelltext anführen, der für das Konferenzanmeldungsprojekt verwendet wurde: Es ist erforderlich, eine webbasierte Anwendung zur Konferenzanmeldung zu entwickeln. Dies ist die erste Schleife erläuternder Informationen. Hier wird gesagt, was erreicht werden soll. In diesem Fall die Entwicklung eines webbasierten Konferenzanmeldungssystems. Kommen wir zu den Details: Das Konferenzanmeldesystem ist eine webbasierte Anwendung. Unter Verwendung dieses Systems kann der Benutzer nähere Informationen zu einer Konferenz anzeigen. Ist die Konferenz von Interesse für den Benutzer, kann er sich durch Drücken auf eine Schaltfläche für diese Konferenz anmelden. Diese Anmeldung wird in Echtzeit ausgeführt, d.h., die Konferenzveranstalter verfügen stets über aktuelle Anmeldeinformationen.
74
Entwickeln von Anwendungen
Dieser zweite Absatz beginnt mit der Erläuterung der Anwendungsdetails. Sie müssen den ersten Absatz nicht gelesen haben, da im zweiten Absatz die genannten Informationen wiederholt werden. Es werden Informationen zur Systemdynamik bereitgestellt. Es gibt einen Benutzer – einen potenziellen Konferenzteilnehmer. Der Benutzer könnte durch ein Objekt (Insel) in der Anwendung repräsentiert werden. Es ist wichtig, das Objektnamen und Domänenmodelltext zueinander in Beziehung stehen. Andernfalls wird der Leser verwirrt. Des Weiteren werden bestimmte Prozesse offensichtlich. Diese Prozesse stellen die Funktionen der Anwendung dar. Der Domänenmodelltext sollte so viele Funktionen wie möglich definieren. Abschließend sollten die Anwendungsanforderungen genannt werden. Die Anmeldung soll in Echtzeit erfolgen, damit den Konferenzorganisatoren stets aktuelle Daten vorliegen. An diesem Punkt ist es interessant, zu hinterfragen, warum diese Echtzeitfunktion nicht bereits früher implementiert wurde. Das Sammeln von Hintergrundinformationen ist ein wichtiger Aspekt bei der Projektarbeit. Häufig wurden bestimmte Aufgaben nicht ausgeführt, da beispielsweise die entstehenden Kosten zu hoch lagen. Nach einiger Zeit geraten diese Gründe in Vergessenheit, ein zweiter Versuch wird gestartet und möglicherweise kommt es erneut zu einem Misserfolg. Wenn Sie herausfinden, warum die Echtzeitfunktion nicht bereits früher implementiert wurde, können Sie ermitteln, ob immer noch etwas gegen eine solche Implementierung spricht. Ist dies der Fall, können Sie diese Anforderung aus dem Domänenmodelltext streichen.
3.2.4 Definieren der Anwendung Nachdem die Anwendung untersucht und beschrieben wurde und die zu implementierenden Funktionen festgelegt wurden, beginnt nun die Entwicklung der Anwendung. In der Begrifflichkeit des Anwendungsentwicklungszyklus handelt es sich hierbei um den Schritt des Modellentwurfs. Die beste Methode zur Entwurfsbeschreibung stellt die UML (Unified Modeling Language) dar. UML ermöglicht das Entwerfen der Anwendung, ohne dass das Schreiben von Code erforderlich ist. Bei UML handelt es sich um ein abstraktes Modellierungsverfahren, bei dem Elemente verwendet werden, die durch Pfeile und Linien miteinander verbunden werden. Wie steht es mit CASE? CASE (Computer Aided Software Engineering) war ein Schlagwort, das mit der Vision einer höheren Produktivität und einer gleichzeitigen Vereinfachung der An-
Starten eines Projekts
75
wendungsentwicklung verknüpft wurde. Der Gedanke war, mit Hilfe einer Art Notation eine vollständige Anwendung entwerfen zu können, und UML war ein Beispiel für dieses Verfahren. Sie klicken auf die Schaltfläche zum Generieren, und das CASE-Tool gibt eine fertige Anwendung aus. Ja, dies ist möglich; aber, nein, Sie sollten diese Methode nicht einsetzen. Die Probleme von CASE können folgendermaßen zusammengefasst werden: 왘 Aufgeblähter Code: Es ist einfach, schöne Grafiken zu erstellen und anschlie-
ßend durch das CASE-Tool automatisch die Klassen generieren zu lassen. Diese Art der Vorgehensweise führt jedoch zu einer Bequemlichkeit, bei der der Designer ein Diagramm nach dem anderen entwirft. Ich habe einmal an einem solchen Projekt mitgearbeitet – das Projekt umfasste 350 Geschäftsobjekte, was zu 15.000 Java-Klassendateien führte. 왘 Kleinster gemeinsamer Nenner: Die meisten Notationen unterstützen ledig-
lich ein einfaches, objektorientiertes Framework. Einfache Konzepte, beispielsweise Vererbung und Verknüpfung, werden unterstützt, erweiterte Programmierungskonzepte, beispielsweise C++-Vorlagen, dagegen nicht. Daher muss bei Verwendung von CASE einfacher Code geschrieben werden. Dies ist an sich nicht schlimm, kann aber zu einem Problem werden, wenn viele Strukturen hinzugefügt werden, um den einfachen Code mit den Funktionen eines eleganteren, komplexen Codes zu versehen. Ist die Verwendung von CASE demnach nicht zu empfehlen? Nein, es bedeutet lediglich, dass CASE nur in einem angemessenen Kontext eingesetzt werden sollte. Der Grund für die Entwicklung von CASE ist der, dass einige Programmierungsprojekte einfach zu umfangreich und komplex sind, um mit Hilfe einer gesprochenen Sprache wie beispielsweise Englisch umschrieben zu werden. Mit Hilfe von CASE können die Konzepte einiger höherwertiger Strukturen vereinfacht werden, da Sie zur Beschreibung des Softwareentwurfs eine Notation verwenden. Sie sollten CASE als Hilfe zur Strukturierung Ihrer Gedanken einsetzen. UML und die Tools zur Anwendungsmodellierung ermöglichen Ihnen das Umsetzen von Konzepten und Ideen. Diese Konzepte und Ideen ermöglichen Ihnen ein Verständnis der Dynamik sowie der möglichen Problembereiche einer Anwendung. Das Verwenden eines solchen Tools gleicht dem Erstellen einer Miniaturversion Ihrer Anwendung. Es werden nicht alle Details gezeigt, aber Sie erhalten einen Eindruck davon, was Sie erwartet. Steht der Nutzen eines solchen Diagramms nicht im Verhältnis zum erforderlichen Arbeitsaufwand, sollten Sie keines erstellen. In diesem Fall verschwenden Sie lediglich Ihre Zeit. Einige Anwendungen benötigen aufgrund Ihrer Einfachheit
76
Entwickeln von Anwendungen
keine solchen Modelltools. Andere Anwendungen erfordern erhebliche Arbeit für den Modellentwurf, da sie sehr umfangreich oder komplex sind. Dies hängt ganz von der jeweiligen Anwendung ab. Der Anhang enthält eine Liste mit weiterführender Literatur zu diesem Thema. Abstriche bei der Funktionsimplementierung Beim Modellentwurf möchten Sie üblicherweise alle der möglichen Funktionen in das Modell einarbeiten. Einige der angestrebten Funktionen sind hierbei nützlicher und wichtiger als andere. Üblicherweise gibt es einige Funktionen, deren Implementierung unbedingt erforderlich ist. Dies stellt kein Problem dar, da es sich um die Grundlage der Anwendung handelt. Problematischer dagegen ist es, herauszufinden, welche der (vielen) gewünschten Extrafunktionen implementiert und welche Funktionen erst in der nächsten Anwendungsversion umgesetzt werden können. Sie können zu diesem Zweck eine Gleichung aufstellen, bei der die folgenden Attribute berücksichtigt werden: 왘 Zeit: Wie viel Zeit wird zur Implementierung einer bestimmten Funktion benö-
tigt? Dieser Faktor kann in Zeiteinheiten gemessen werden. 왘 Vorteil: Welche direkten Vorteile hat das Hinzufügen dieser Funktion? Der
Vorteil einer Funktion kann in Begriffen verbesserter Verwendbarkeit, einfacherer Codeverwaltung oder reduziertem Testaufwand ausgedrückt werden. Je mehr Vorteile, desto höher die Wertung. 왘 Eindruck: Ist die Funktion »Zucker fürs Auge«? Etwas, das den Benutzer zum
Lächeln bringt? Es kann sich um eine vollkommen triviale Funktion handeln, die nicht Teil der gesamten Anwendung ist, deren Implementierung jedoch die Benutzer freuen würde. Häufig sind die Argumente, die für Funktion dieser Art sprechen, irrational und hängen mit einer bestimmten Vorliebe zusammen, beispielsweise für Java, Client/Server, Middleware usw. Je besser der gewonnene Eindruck, desto höher die Wertung. 왘 Auswirkungen: Wie stark wirkt sich das Hinzufügen der Funktion auf das Sys-
tem aus? Die Auswirkungen stehen mit dem Testaufwand in Verbindung, der aufgebracht werden muss, bis eine gewisse Stabilität erreicht werden kann. Je höher die Auswirkungen auf die Anwendung, desto höher die Wertung. 왘 Priorität: Die Priorität, die den einzelnen Funktionen der Anwendung beige-
messen wird. Einige Funktionen müssen implementiert werden, da sie zur Hauptfunktionalität des Systems gehören. Diese Funktionen werden von der Auswahl der zu implementierenden Funktionen ausgenommen. Andere Funk-
Starten eines Projekts
77
tionen weisen unterschiedliche Prioritäten auf. Je höher die Priorität, desto höher die Wertung. Die Wertung sollte anhand einer gemeinsamen Skala für alle Attribute erfolgen. Die Skala kann von 1 bis 10 oder von 1 bis 100 reichen. Schließen Sie die 0 nicht ein, da diese zu mathematischen Fehlern führen kann. Schließen Sie den Zeitfaktor ein, indem Sie die Zeit für die Implementierung nehmen und durch die verbleibende Gesamtzeit des Projekts dividieren. Wenden Sie dann folgende Formel an: (Vorteil x Eindruck x Priorität) / (Zeit x Auswirkungen) Das Ergebnis stellt keinen besonderen Wert dar. Es dient lediglich als Vergleichswert gegenüber anderen Funktionen. Je höher der Ergebniswert, desto wichtiger ist eine Funktion. Ein mögliches Problem bei dieser Gleichung ist, dass den einzelnen Bewertungsfaktoren gleiche Bedeutung eingeräumt wird. Es kann jedoch vorkommen, dass bei einem Projekt ein Faktor wichtiger ist als beispielsweise der Eindruck oder die Priorität. In diesem Fall müssen Sie jedem der Attribute einen Gewichtungsfaktor zuordnen. Der einfachste Weg, eine Gewichtung vorzunehmen, stellt das Verwenden von prozentualen Werten dar, wobei sichergestellt werden muss, dass alle Faktoren zusammen den Wert 100 ergeben. Ist die Zeit der wichtigste Faktor, können Sie die folgende Gleichung verwenden: ((0,1 x Vorteil) x (0,1 x Eindruck) x (0,1 x Priorität)) / ((0,6 x Zeit) x (0,1 x Auswirkungen)) In Ihrer speziellen Entwicklungssituation können natürlich auch andere Attribute erforderlich sein. Zum Einschluss dieser Faktoren müssen Sie den Attributwert lediglich über oder unter dem Divisor einfügen. Ist ein Attribut erwünscht, wird der zugehörige Wert über dem Divisor platziert. Ist ein Attribut unerwünscht, wird der zugehörige Wert unter dem Divisor platziert. Sie sollten nicht zu viele Attribute verwenden, da so das Ergebnis weniger klar ausfällt. Ein Beispiel hierfür ist der Kostenfaktor. Die Kosten scheinen einen eigenen Faktor darzustellen, tatsächlich handelt es sich jedoch um eine Auswirkung eines anderen Attributs. Höhere Kosten resultieren beispielsweise aus einem erhöhten Zeitaufwand, stärkeren Auswirkungen oder geringeren Vorteilen. Das Erhöhen des Zeitfaktors für eine Funktion führt automatisch zu einer Erhöhung der Kosten, und wenn die Kosten ebenfalls in die Gleichung aufgenommen werden und nicht anderweitig ausgeglichen werden, führt die Gleichung zu irreführenden Ergebnissen. Die Mathematik kann Ihnen ein beliebiges Ergebnis liefern, wichtig ist, wie Sie das Ergebnis interpretieren.
78
Entwickeln von Anwendungen
Wenn Sie sich unter Verwendung von Gleichungen für oder gegen die Implementierung einer Funktion entscheiden, bedeutet dies, dass diese Entscheidung von den Gesamtvorteilen für die Anwendung abhängig gemacht wird. Funktionen werden nicht auf der Grundlage eines einzelnen Attributs implementiert. Anwendungsfälle und Sequenzdiagramme Basierend auf der Einschätzung des Für und Wider einer Funktionsimplementierung wird ein Satz Funktionen ausgewählt, der anschließend entworfen werden muss. Funktionen werden unter Verwendung von UML-Anwendungsfällen (so genannte Use Cases) implementiert. In der UML-Terminologie bezeichnet ein Anwendungsfall eine Beschreibung der unterschiedlichen Anwendungsverwendung durch verschiedene Benutzer. Der Anwendungsfall (Use Case) beschreibt eine Interaktion zwischen Benutzer und Anwendung und vermittelt Ihnen einen Eindruck davon, welche Möglichkeiten dem Benutzer bei der Anwendungsverwendung zur Verfügung stehen. Der Domänenmodelltext bietet ein Gesamtbild sowie ein Verständnis der gesamten Anwendung, die Anwendungsfälle zeigen verschiedene Aspekte der Anwendung auf. Nachdem Sie sämtliche Anwendungsfälle für eine Anwendung kennen, können Sie ermitteln, welche Anwendungsfälle wieder verwendet werden können, und Sie können die Abhängigkeiten zwischen den verschiedenen Anwendungsfällen einrichten. In UML stellen die Anwendungsfälle eine einfache Struktur dar, die jedoch nicht sehr selbsterklärend ist. Üblicherweise möchten Sie einen Anwendungsfall mit einem Sequenzdiagramm kombinieren. Ein Sequenzdiagramm verdeutlicht die verschiedenen Schritte, die zur Ausführung einer bestimmten Aufgabe erforderlich sind. Ein Sequenzdiagramm zeigt typischerweise die Implementierung eines Anwendungsfalles (Use Case). Ein Anwendungsfall kann mit mehreren Sequenzdiagrammen verknüpft sein. Ein einfacher Anwendungsfall Lassen Sie uns zunächst den Anwendungsfall für einen Benutzer darstellen, der die Konferenzwebsite durchsucht. Als Tool zur Darstellung von Anwendungsfällen verwende ich Rational Rose, das von der Rational Software Corp. entwickelt wurde. Ich verwende dieses Tool, da ich es bereits häufig eingesetzt habe. Sie können natürlich auch ein anderes Tool verwenden. Das verwendete Tool spielt keine Rolle, da lediglich die Anwendungsfälle veranschaulicht werden sollen – hierzu kann auch Paint oder PowerPoint eingesetzt werden. Der zum Lieferumfang der Enterprise Edition von Visual Studio gehörende Visual Modeler stellt einen Teilsatz von Rational Rose dar, durch den
Starten eines Projekts
79
keine Anwendungsfälle oder Sequenzdiagramme unterstützt werden. Sie können Visual Modeler dennoch genau wie Paint oder PowerPoint einsetzen. Das Anwendungsfalldiagramm für einen Benutzer, der die Konferenzsite durchsucht, wird in Abbildung 3.6 dargestellt. Beispiel für einen Benutzer, der die Konferenzsite durchsucht. In diesem Feld könnte man einige wichtige beschreibende Informationen eingeben.
Durchsucht Site Benutzer
Abbildung 3.6 Einfacher Anwendungsfall für einen Benutzer, der eine Konferenzsite durchsucht
In diesem Diagramm sind drei Informationen enthalten. Es sind ein kleines Strichmännchen, ein Pfeil und ein Oval sichtbar. Das Strichmännchen wird als Akteur bezeichnet und repräsentiert eine Einzelperson mit einer spezifischen Rolle. Das Oval repräsentiert einen Anwendungsfall. Wenn der Zeiger (Pfeil) vom Akteur zum Anwendungsfall zeigt, wird dies als Interaktion bezeichnet. Der Akteur Der Akteur ist eine Einheit, die extern zur Anwendung vorliegt und auf bestimmte Weise mit dem System interagiert. Ereignisse werden durch einen Akteur instanziiert, und der Akteur kann seiner Rolle entsprechend beschrieben werden. Im Beispiel der Konferenzanmeldungsanwendung gehörten zu den Akteuren u.a.: 왘 Benutzer: Ein Akteur, der die Konferenzsite nach Informationen durchsucht,
die ihm bei der Entscheidung helfen, an welcher Konferenz er teilnehmen möchte (oder nicht). Die Identität des Benutzers ist unbekannt. 왘 Verwender: Ein Verwender ist mehr als ein Benutzer. Der Verwender ist eine
Person, die sich an der Konferenzsite angemeldet hat und vielleicht (oder auch nicht) an der Konferenz teilnimmt. Die Identität des Verwenders ist bekannt. 왘 Konferenzveranstalter: Der Konferenzveranstalter ist für den Inhalt der Konfe-
renzwebsite verantwortlich. Der Konferenzveranstalter prüft in regelmäßigen Abständen die Datenbank, um die Konferenzstatistik anzuzeigen. Sämtliche Akteure im Konferenzanmeldungssystem sind Personen, dies ist jedoch nicht immer der Fall. Ein Akteur kann eine anwendungsinterne oder -externe Einheit darstellen, beispielsweise eine andere Anwendung.
80
Entwickeln von Anwendungen
Der Konferenzanmeldungsanwendung wurde folgender Akteur hinzugefügt: 왘 System-Manager: Dieser prüft die Datenbank regelmäßig auf Ihre Datenkonsis-
tenz. Der System-Manager ist ein besonderer Akteur. Es handelt sich um einen TaskManager, mit dem regelmäßig durchgeführte Aufgaben wie das Bereinigen von E-Mail-Adressen durchgeführt werden. Warum ist ein System-Manager erforderlich? Viele Verwender geben bei der Anmeldung eine falsche E-Mail-Adresse an (in 25% der Fälle). Die Fehler waren sind besonders schwer wiegend (beispielsweise das Verwechseln von Bindestrichen und Unterstrichen), aber die Fehler verursachen E-Mail-Fehler. Der System-Manager durchsucht die Datenbank auf derartige Fehler. In komplizierteren Fällen können mehrere Akteure an einem Anwendungsfall beteiligt sein. Der initiierende Akteur startet das erste Ereignis. Die weiteren Akteure werden als teilnehmende Akteure bezeichnet. Der Anwendungsfall Der Anwendungsfall (Use Case) wird durch ein einfaches Oval dargestellt. Um dem Ganzen Sinn zu verleihen, muss eine Interaktion stattfinden. Ein guter Anwendungsfall beschreibt sämtliche Interaktionen zwischen Benutzer und Anwendung, es wird üblicherweise jedoch auf keine spezielle Technologie verwiesen. Im Beispiel der Konferenzanmeldungsanwendung stellt ein Anwendungsfall die Anmeldung des Verwenders an einer Konferenz dar. Der Vorgang, bei dem der Verwender das Anmeldeformular ausfüllt, stellt hierbei keinen eigenen Anwendungsfall dar – dieser Vorgang ist Bestandteil des Anwendungsfalls »Anmeldung«. Die Granularität der Anwendungsfälle ist äußerst wichtig. Sie sollten alle der möglichen Szenarien beschreiben, diese jedoch nicht bis ins Detail ausführen. Das Entwickeln der verschiedenen Anwendungsfälle stellt einen iterativen Prozess dar, und die Anzahl der Anwendungsfälle sollte mit der Anzahl der Iterationen steigen. In einer iterativen Lösung besteht das Ziel darin, sich bis zu einer spezifischen Lösung vorzuarbeiten. Ein komplexerer Anwendungsfall Das Konferenzanmeldungssystem weist neben dem Durchsuchen der Site weitere Anwendungsfälle auf. Weitere Anwendungsfälle umfassen das Verwalten der Konferenzinformationen, das Anmelden an der Site sowie das Teilnehmen an einer Konferenz. Abbildung 3.7 zeigt ein etwas komplexeres Anwendungsfalldiagramm.
Starten eines Projekts
81
Abbildung 3.7 Anwendungsfalldiagramm zur Konferenzanmeldung
In diesem Diagramm sind vier Anwendungsfälle enthalten, das Durchsuchen der Site, das Teilnehmen an einer Konferenz, das Anmelden an der Site sowie das Verwalten der Konferenzinformationen. Im ursprünglichen Entwurf wurde kein Unterschied zwischen der Anmeldung an einer Konferenz und der Anmeldung an der Site gemacht. Beim Verfassen des Domänenmodelltextes konnte jedoch festgestellt werden, dass zwei Formen der Registrierung eingerichtet werden müssen, da die Anmeldung zu einer Konferenz einen umfangreicheren Anwendungsfall darstellt als die Siteanmeldung. Interaktion In Abbildung 3.7 findet eine Interaktion zwischen den Akteuren und den Anwendungsfällen statt. Ein einfacher Pfeil repräsentiert die Benutzeraktion. Für eine Interaktion ist jedoch nicht zwangsläufig eine Benutzeraktion erforderlich. Bei den Akteuren könnte es sich auch um Programme auf Systemebene handeln. Beim Konferenzanmeldungssystem werden die Datenbanken auf Stapelverarbeitungsbasis durch eine Access-Datenbank zusammengeführt. Wie wird diese Interaktion modelliert? Die einfachste Methode besteht darin, das Legacysystem zu einem Akteur zu machen. Der Legacysystemakteur wird Systemakteur genannt. Anwendungsfälle und Erweiterungen Meldet sich ein Verwender für eine Konferenz an, erfordert das deutsche Recht, dass der Verwender die betreffende Website durchsucht, damit er weiß, was er kauft. Andernfalls könnte die über das Web durchgeführte Anmeldung als nicht bindend betrachtet werden. Daher muss
82
Entwickeln von Anwendungen
der Anwendungsfall »Teilnahme an einer Konferenz« die Funktionalität »Durchsuchen der Site« einschließen. Effizienter wäre es jedoch, wenn der Anwendungsfall »Teilnahme an einer Konferenz« die Funktionalität des Anwendungsfalls »Durchsuchen der Site« einschließen würde. Im Anwendungsfalldiagramm für die Konferenzanmeldung (Abbildung 3.7) wird der Anwendungsfall »Durchsuchen der Site« durch die UML-Notation eines Pfeils mit einer weißen Spitze und dem Wort verwendet dargestellt. So wird angezeigt, das ein Anwendungsfall die Funktionalität eines anderen Anwendungsfalls verwendet. Eine weitere Möglichkeit zur Wiederverwendung von Code ist die Erweiterung der Funktionalität. Im Anwendungsfalldiagramm für die Konferenzanmeldung (Abbildung 3.7) muss der Konferenzveranstalter in der Lage sein, weitergehende Aufgaben für die Site durchzuführen. Aber wie der Verwender muss sich der Konferenzveranstalter an der Website anmelden, um sich selbst als Konferenzveranstalter zu identifizieren. Mit Hilfe des gleichen Pfeils, diesmal jedoch mit dem Schlüsselwort erweitert, wird die Funktionalität des Anwendungsfalls »Anmeldung« durch den Anwendungsfall »Konferenzveranstalter« erweitert. Es ist möglich, über eine der beiden genannten Methoden Abhängigkeiten zwischen den verschiedenen Anwendungsfällen zu erzeugen, und so zu wieder verwendbarem Code zu gelangen.
3.2.5 Definieren der Modelldetails Nachdem Kontext, Domänenmodelltext und Anwendungsfälle definiert sind, sollten Sie sich einen Überblick verschafft haben, was mit der Anwendung erreicht werden soll. Bis zu diesem Punkt gestaltet sich das Verständnis der Anwendung jedoch sehr abstrakt. Das Spezifizieren der Details hängt von den zuvor durchgeführten Operationen ab. Wenn beispielsweise Anwendungsfälle, Domänenmodell oder Kontext falsch sind, sind die darauf basierenden Details ebenfalls falsch. Dies bedeutet, dass vor der Definition der Modelldetails sichergestellt werden muss, dass Kontext, Domänenmodelltext und Anwendungsfälle richtig sind. Sie sollten diese Phase nicht überspringen. Es gibt vier Möglichkeiten, den Anwendungsentwurf weiter zu verfeinern: 왘 Sequenzdiagramm: Die Definition eines Anwendungsfalls in Form einer
Schrittfolge 왘 Kollaborationsdiagramm: Ein dem Sequenzdiagramm ähnliches Diagramm, in
diesem können jedoch auch Verknüpfungen zwischen Objekten definiert werden
Starten eines Projekts
83
왘 Statusdiagramm: Ein Diagramm, mit dem die verschiedenen Statuszustände
eines Objekts dargestellt werden, die ein Objekt in verschiedenen Anwendungsfällen aufweisen kann 왘 Klassendiagramm: Ein Diagramm, in dem die Klassen der Anwendung und de-
ren Beziehungen untereinander dargestellt werden Bei diesen Diagrammen ist es wichtig, sich auf einige wenige Elemente zu beschränken. Obwohl es vier verschiedene Modelle gibt, ist es nicht nötig, für jedes ein Entwurfsmodell zu implementieren. Halten Sie die Anzahl der Modelle den Anforderungen entsprechend gering. Das Erstellen zu vieler Modell kompliziert den iterativen Prozess und führt zu einer Codeaufblähung. Ein Klassendiagramm ist erforderlich, da mit diesem das Gesamtobjektmodell der Architektur definiert wird. Dieser Aspekt des Entwurfsprozesses wird in Kapitel 9 behandelt, »Implementieren von COM-Objekten«. Das Statusdiagramm ist in den meisten Systemen nicht erforderlich. Bei der Verwendung von Transaktionen kann es jedoch hilfreich sein. Sequenz- oder Kollaborationsdiagramme (siehe nächster Abschnitt) sind für den Entwurfsprozess ebenfalls nützlich. Es ist nicht notwendig, beide Diagrammtypen einzusetzen, da Sie auf verschiedene Weise die gleiche Funktion erfüllen. Sequenzdiagramme Durch das Sequenzdiagramm werden die Schritte definiert, die für einen Anwendungsfall erforderlich sind. Es können mehrere Sequenzdiagramme mit einem Anwendungsfall verknüpft werden. Abbildung 3.8 zeigt das Sequenzdiagramm für den Anwendungsfall »Teilnahme an einer Konferenz«. In dieser Abbildung befinden sich im oberen Diagrammbereich ein Akteur und vier Felder, die Objekte. Unter jedem Objekt befinden sich verschiedene vertikale Linien. Beginnend beim Akteur »Verwender« ist eine horizontaler Pfeil eingezeichnet, der auf das zweite Objekt von links zeigt. Dies verdeutlicht das Senden einer Meldung vom Verwender zum Account-Objekt. Bei der gesendeten Meldung handelt es sich um die Anmeldung. Von der Anmeldungsnachricht gehen zwei schmale, vertikale Felder ab. Diese Felder definieren den Steuerungsfokus. Wird ein Methodenaufruf durch einen anderen Methodenaufruf aufgerufen, wechselt der Steuerungsfokus vom ersten zum zweiten Methodenaufruf. In einem Sequenzdiagramm geschieht dasselbe. Innerhalb jedes Feldes im Diagramm verbleibt der Steuerungsfokus bei der Operation. Wenn ein Pfeil auf ein anderes Feld oder auf sich selbst zeigt, wechselt der Steuerungsfokus auf das Element, auf das der Pfeil zeigt.
84
Entwickeln von Anwendungen
Abbildung 3.8 Sequenzdiagramm für den Anwendungsfall »Teilnahme an einer Konferenz«
Die Nachricht zur Anmeldung ist auf der rechten Seite mit einem Steuerungsfokus verbunden, der drei Get-Meldungen umfasst. Dies weist darauf hin, dass die Nachricht zur Anmeldung so lange warten muss, bis drei Get-Meldungen durchgeführt wurden. In der Programmierungsterminologie gleicht dies dem Aufruf einer Funktion, durch die eine weitere Funktion aufgerufen wird. Meldungen in Sequenzdiagrammen werden üblicherweise als einfach betrachtet. Die Mehrzahl der UML-Tools ermöglicht die Definition anderer Meldungstypen, beispielsweise asynchrone Meldungen, Blockierung mit Zeitüberschreitung oder sogar periodische Meldungen. Nachdem die Anmeldung abgeschlossen ist, wird die Meldung attend-Conference( ) gesendet. Auf diese Weise wird der Verwender am ConferenceDetails-Objekt angemeldet. Im Sequenzdiagramm verläuft der Meldungsfluss von links nach rechts. Es ist auch möglich, einen Meldungsaufruf von rechts nach links zu definieren, dies würde ei-
Starten eines Projekts
85
nen Rückruf anzeigen. Wie in Abbildung 3.9 gezeigt wird, ist es einem Objekt darüber hinaus möglich, ein Objekt an sich selbst zu senden.
EinObjekt: : Client Rückruf
Selbstmeldung
Abbildung 3.9 Sequenzdiagramm mit Rückruf und Selbstmeldung (Self-Message)
Beim Lesen eines Sequenzdiagramms sollten Sie den Steuerungsfokus betrachten, um zu sehen, welche Operationen in einem Kontext ausgeführt werden. Beachten Sie außerdem, dass die Aufrufsequenz oben im Diagramm beginnt und nach unten verläuft. Kollaborationsdiagramm Wie das Sequenzdiagramm zeigt auch das Kollaborationsdiagramm eine Schrittfolge. Es verfügt jedoch über die zusätzliche Fähigkeit, dass mit Hilfe von Objektverknüpfungen Objektinteraktionen dargestellt werden können. Der Anwendungsfall »Teilnahme an einer Konferenz« wird in Abbildung 3.10 als Kollaborationsdiagramm dargestellt. In Abbildung 3.10 sind die verschiedenen Objekte (Akteure und Felder) über das Diagramm verstreut. Die Linien, mit denen die Objekte miteinander verbunden werden, sind Verknüpfungen und stellen eine programmatische Form der Verknüpfung zweier Objekte dar. Die Verknüpfung kann global, lokal, ein Parameter oder ein einfaches Feld sein. Wie beim Sequenzdiagramm deuten die Pfeile auf Meldungen hin, die von einem Objekt zu einem anderen gesendet werden. Um jedoch die Reihenfolge zu verstehen, in der die Schritte ausgeführt werden, ist ein numerischer Bezeichner mit der Meldung verknüpft.
86
Entwickeln von Anwendungen
Abbildung 3.10 Kollaborationsdiagramm zum Anwendungsfall »Teilnahme an einer Konferenz«
Sequenzdiagramme ähneln den Kollaborationsdiagrammen sehr stark. In einem Sequenzdiagramm ist die Anordnung der Operationsschritte übersichtlicher, da die Schritte geordnet dargestellt werden. In einem Kollaborationsdiagramm können die Objekte nach Belieben angeordnet werden. Schlussendlich liegt die Auswahl des zu verwendenden Diagramms bei Ihnen.
3.3 Resümee In diesem Kapitel wurde Ihnen ein grundlegendes Verständnis zur Definition einer Anwendung vermittelt. Durch das bloße Erstellen einiger Klassen und das willkürliche Zusammenfügen dieser Klassen wird noch keine Anwendung definiert. Stattdessen muss der Kontext einer Anwendung erarbeitet und ein Domänenmodelltext verfasst werden, mit dem die Anwendung definiert wird. Anschließend können Anwendungsfälle und Sequenz- oder Kollaborationsdiagramme erstellt werden, die dem Verständnis der Anwendung dienen und Ihnen dabei helfen, die Auswirkungen von möglicherweise erforderlichen Änderungen einzuschätzen. Wozu all diese Schritte? Es mag so erscheinen, als wolle ich Ihnen zusätzliche Arbeit auferlegen oder eine Strukturierung um der Strukturierung willen durchführen. Meine Erfahrung hat jedoch gezeigt, dass Projekte mit umfassender vorheriger Planung sich als die besseren Projekte herausstellen. Geht diese Planungsphase mit einem iterativen Entwicklungsansatz einher, können die verschiedenen
Resümee
87
Faktoren optimal aufeinander abgestimmt werden, und es ist die Optimierung, auf die es bei einer Anwendung ankommt. Jeder kann eine Anwendung entwickeln. Wenn Sie jedoch eine stabile, skalierbare und langfristig einsetzbare Anwendung entwickeln möchten, erfordert dies einige Vorarbeit. Software und Technologie ändern sich, und obwohl eine Planung erforderlich ist, kann nichts »überplant« werden, denn niemand kann in die Zukunft sehen.
88
Entwickeln von Anwendungen
4 Entwickeln eines Prototyps Der Anwendungsentwicklungszyklus (Application Development Cycle, ADC) erfordert nach Fertigstellung des Entwurfs das Entwickeln eines Prototyps. Zur Prototypentwicklung ist das Schreiben von Code erforderlich. Zum Verständnis der Entwicklung eines Prototyps muss zunächst das Konzept der Komponenten erläutert werden. Komponenten stellen wieder verwendbare Module dar, die dem Programmierer als Black Box erscheinen. Ein Hauptmerkmal der Komponenten besteht darin, die Schnittstelle von der Implementierung zu trennen.
4.1 Komponenten Im Zusammenhang mit Komponenten denkt man üblicherweise an COM (Component Object Model), CORBA (Common Object Request Broker Architecture) oder Java Beans. Diese Beschreibung von Komponenten schließt jedoch den direkten Verweis auf eine spezifische Technologie ein. Eine angemessenere Beschreibung von Komponenten lautet, dass es sich um eine generische Entwicklungsstrategie handelt. Diese Beschreibung ist treffender, da Komponenten eine Möglichkeit darstellen, logische Abschnitte zu kapseln und anschließend mit einem bestimmten Protokolltyp offen zu legen. Dies bedeutet, dass Komponenten den im vorherigen Kapitel erwähnten Inseln entsprechen.
4.1.1 Ein Stück Komponentengeschichte Zum Verständnis der Komponenten kann ein Vergleich mit der objektorientierten Entwicklung angestellt werden. Eine objektorientierte Sprache, beispielsweise C++ oder Smalltalk, ist eine Sprache, die auf Klassen basiert. Wenn Sie mit Hilfe einer dieser Sprachen eine Anwendung entwickeln, erstellen Sie ein Objekt, um ein spezifisches Problem zu lösen. Das klassische Beispiel hierfür ist die Shape-Klasse. Mit dieser werden generische Operatoren zum Zeichnen und Beschreiben der Form (Shape) offen gelegt. Wenn Sie anschließend eine bestimmte Shape-Klasse, z.B. ein Rechteck, erstellen möchten, müssen Sie eine neue Klasse erstellen und diese von Shape ableiten. Die neue Rechteckklasse weist in diesem Fall zusätzliche Methoden für die zusätzliche Funktionalität auf. Wenn die Rechteckklasse jedoch generisch verwendet wird, kann nur die Shape-Klasse eingesetzt werden. Komponenten unterscheiden sich hiervon dadurch, dass sie mit Schnittstellen und Implementierungen arbeiten. Bei den Komponenten sind die Schnittstellen generisch, die Implementierung ist spezifisch. Eine ausführlichere Erläuterung
Komponenten
89
wird in den folgenden Abschnitten bereitgestellt, zunächst müssen jedoch noch einige Konzepte der objektorientierten Entwicklung definiert werden. Kapselung Die Grundidee bei der Kapselung besteht darin, Codeabschnitte mit einer bestimmten Funktion zu verbergen, sodass nur über einen Methodenaufruf auf diesen zugegriffen werden kann. Hierbei handelt es sich um einen Black Box-Ansatz, bei dem der Benutzer die Implementierungsdetails nicht kennen muss. Der Benutzer muss lediglich wissen, wie der Methodenaufruf funktioniert. Das einfachste Beispiel der objektorientierten Entwicklung ist die foo-Klasse, die zur Ausgabe »Hello world« führt: class foo { public: void sayHello() { printf(“Hello world“); } };
In diesem Beispiel weist die Klasse foo die Methode sayHello auf, mit der die CBibliotheksfunktion printf aufgerufen wird. Die Funktion printf sendet den Text “Hello world“ an die Standardausgabe. Die Methode sayHello ist öffentlich und kann durch jeden Benutzer aufgerufen werden, der diese Klasse instanziiert: int main() { foo ex; ex.sayHello(); }
Der Konsument, der so genannte Client, muss nicht wissen, wie die Begrüßung implementiert wird. Der Programmierer muss lediglich wissen, dass mit der Methode sayHello eine Begrüßung ausgegeben wird. In dieser Version von sayHello wird über die Methode ein statischer Text ausgegeben. Erweitern wir dieses Beispiel und definieren die Methode setRepitition, mit der festgelegt wird, wie häufig die Begrüßung sayHello erfolgt. Die erweiterte fooKlasse lautet folgendermaßen:
Die foo-Klase wurde von Version 1 nach Version 2 lediglich überarbeitet. Auch wenn die Benutzer von Version 1 keine Wiederholung einstellen möchten, funktioniert der Code weiterhin – der Constructor von foo setzt m_repitition auf den Wert 1. So wird sichergestellt, dass die Benutzer, die foo::sayHello aufrufen, nicht von der Änderung betroffen sind. Benutzer der zweiten Version von foo besitzen die Fähigkeit, den Wert für die Wiederholung einzustellen. Auch hier muss der Benutzer nicht wissen, wie die Wiederholung implementiert wird. Der Benutzer kennt lediglich den Unterschied zwischen den beiden Methodenaufrufen. Vererbung und Polymorphismus Die Mehrzahl der objektorientierten Sprachen weist die Fähigkeit zur Vererbungsimplementierung auf. Bei der Vererbung können neue Klassen basierend auf bereits vorhandenen Klassen erstellt werden. Die Vererbung bietet die Fähigkeit, die Funktionalität einer Klasse zu ändern, die vorhandene Basisfunktionalität jedoch beizubehalten. Im Fall der foo-Klasse könnte beispielsweise die Begrüßung auch in anderen Sprachen erfolgen, z.B. in französisch. Die neue Klasse würde folgendermaßen lauten: class frenchFoo : public foo{ public: void sayHello() { printf(“Salut tout le monde“); } };
Komponenten
91
Die Klasse frenchFoo wurde mit Hilfe der C++-Notation in der ersten Codezeile von der Klasse foo abgeleitet. Beachten Sie, dass frenchFoo die Methode sayHello aufweist, deren Methodensignatur mit der von foo::sayHello identisch ist. Dies bedeutet, dass bei der Instanziierung einer Klasse von frenchFoo alle Aufrufe für sayHello in frenchFoo::sayHello geändert werden. Sehen Sie sich den folgenden Quellcode an: int main() { foo ex; frenchFoo fex; fex.sayHello(); ex.sayHello(); }
Dieses Beispiel ergibt folgende Ausgabe: Salut tout le monde Hello world
Bei Instanziierung der Klasse foo führt das gleiche sayHello zu der Begrüßung, die wir im vorherigen Abschnitt gesehen haben. Die Vererbung befähigt zur Erstellung von Klassen, die sich ähneln. Wenn beispielsweise die Klasse frenchFoo an ein Objekt übergeben wird, das den Typ foo erwartet, erzwingt der Compiler einen Typecast. Dies bedeutet, dass Benutzer der Klasse foo denken, dass sie über foo verfügen, tatsächlich wird jedoch frenchFoo verwendet. Als Ergebnis werden Benutzer, die über die foo-Klasse sayHello aufrufen, an frenchFoo::sayHello umgeleitet. Das Vorliegen einer Klasse, die einer anderen Klasse ähnelt, wird Polymorphismus genannt. Wenn Sie beispielsweise eine generische Begrüßung erstellen möchten, kann eine b-Klasse als Basis verwendet werden. Von dieser Klasse ausgehend, würde eine beliebige Klasse zur Ausgabe einer Begrüßung die sayHelloMethode ableiten und implementieren. Verschiedene Sprachen verfügen über Techniken, mit denen Sie gezwungen werden, spezifische Methoden zu implementieren, wenn diese von einer Klasse abgeleitet werden. In C++ wird diese Technik als rein virtuell bezeichnet. Die Probleme der objektorientierten Entwicklung Die objektorientierte Entwicklung ist ein Fortschritt für den Entwicklungsprozess. Hierbei treten jedoch auch Probleme auf: 왘 Verwendung einer einzelnen Sprache erforderlich: Der vorangegangene Code
wurde in C++ geschrieben, und dieser Code kann nicht mit Visual Basic ver-
92
Entwickeln eines Prototyps
mischt werden. Der Compiler versteht nur eine Sprache, das Verwenden zweier oder mehrerer Sprachen führt zu Kompilierungsfehlern. 왘 Vollständige Klassenbeschreibungen erforderlich: Verwendet man eine Spra-
che wie C++, muss die foo-Klasse gegenüber dem Compiler vollständig beschrieben werden. Andernfalls weiß der Compiler nicht, welche Funktion das Objekt besitzt oder wie das Objekt arbeitet. Müssen Vererbung und Polymorphismus aufgelöst werden, muss der Compiler die Definitionen aller involvierten Klassen kennen, um das Layout der Klasse zu bestimmen oder zu definieren. Dies ist problematisch, denn wenn Sie binären Code gemeinsam verwenden möchten, müssen Sie zusätzliche Informationen zu einer Klasse bereitstellen. Und da nicht alle Compiler gleich sind, muss vielleicht der gesamte Quellcode freigegeben werden. 왘 Entwurf nicht einfach: Das Entwerfen einer guten objektorientierten Anwen-
dung ist nicht einfach. Da der objektorientierte Entwurf komplexer ist und große Anwendungen, die durch mehrere Personen geschrieben wurden, häufig verschiedene Programmierstile aufweisen, gestalten sich Integration, Test und Wartung schwieriger.
4.1.2 Komponenten – Das Entwurfskonzept Bei der Entwicklung eines Computers wird die Hauptplatine mit einer Reihe von Chips ausgestattet. Diese Chips werden verdrahtet und mit Hilfe von Elektrizität funktionstüchtig gemacht. Die Entwickler einer solchen Hauptplatine kümmern sich nicht um die internen Details dieser Computerchips. Sie müssen lediglich die externen Verbindungen und deren Funktion kennen. Der Chip ist für die Entwickler der Hauptplatine eine Blac kBox. Die Komponentenentwicklung folgt der gleichen Vorstellung. Die Komponenten stellen Teile eines Programmcodes dar, dessen Funktionen mit Hilfe einer Art Signatur offen gelegt wird. Die Schnittstellen, Methoden innerhalb der Schnittstellen, und Parameter innerhalb der Methode definieren eine Signatur. Unter Zuhilfenahme des Inselkonzeptes, das im vorangegangenen Kapitel besprochen wurde, stellen die Inseln individuelle Komponenten dar. Es soll versucht werden, die Begriffe objektorientierte Programmierung oder Modulprogrammierung zu vermeiden. Dies geschieht aus einem wichtigen Grund. Die Programmierung unterlag in den letzten zwanzig Jahren erheblichen Änderungen. Vor zwanzig Jahren wurden BASIC (Beginner’s All-Purpose Symbolic Instruction Code), COBOL (Common Business-Oriented Language) und FORTRAN (Formula Translation) als Nonplusultra betrachtet. Heute gibt es CORBA, COM, C++, Java usw. Das Beschränken der Komponenten auf eine Technologie ist kein guter Ansatz, da so auch das eigentliche Konzept eingeschränkt wird.
Komponenten
93
Im Laufe der Jahre war die alles bestimmende Idee die Entwicklung von wieder verwendbarer Software. Codeabschnitte mit dieser Funktionalität wurden zunächst als Bibliotheken, dann als Module und schließlich als Komponenten bezeichnet. Ziel all dieser Techniken war es, die Funktionalität in einer Form zu »verpacken«, dass die Details des Pakets in einer Black Box verborgen wurden. Der Benutzer des Pakets brauchte lediglich die Schnittstelle zu verstehen, bei der es sich um Strukturen, Methoden oder Objekte handeln konnte. Bei den Komponenten ist das Ziel dasselbe, das Konzept von Schnittstelle und Implementierung ist jedoch sehr explizit. Eine Komponente erfordert eine Schnittstelle. Dem Konsumenten gegenüber stellt die Komponente einen statischen Block dar. Die Implementierung ist weder direkt zugänglich, noch kann der Konsument diese bearbeiten. Durch das Verwenden von Schnittstellen arbeitet der Konsument eine Abstraktion ein, die später zur Definition neuer Funktionalität ohne Änderungen für den Konsumenten verwendet werden kann. Komponenten lassen sich am besten wie folgt zusammenfassen: Eine Softwarekomponente ist eine Kompositionseinheit, die nur aus vertraglich spezifizierten Schnittstellen und expliziten Kontextabhängigkeiten besteht. Eine Softwarekomponente kann unabhängig bereitgestellt werden und unterliegt der Gestaltung von Drittanbietern. Diese Definition wurde zuerst auf der ECOOP '96 formuliert (European Conference on Object-Oriented Programming). Middleware und Komponenten Möchten ein Konsument und ein Server kommunizieren, ist die Überbrückung technologischer Unterschiede erforderlich. Diese Brücken (Bridges) werden als Middleware bezeichnet. Die Middleware ähnelt einem Übersetzungsdienst, über den zwei Personen unterschiedlicher Sprache miteinander kommunizieren können. COM ist ein Beispiel für die Middleware. COM ermöglicht einem Visual Basic-Objekt die Kommunikation mit einem Visual C++-Objekt. Die COM-Middleware ist wie in Abbildung 4.1 aufgebaut. In der Architektur der Middleware sind Konsument und Server zwei separate Einheiten, auf der abstrakten Ebene scheinen diese jedoch direkt miteinander verbunden zu sein. Über die Middleware wird dem Konsumenten ein Ghost des Servers und dem Provider ein Ghost des Benutzers zur Verfügung gestellt. Dieser Ghostingeffekt vermittelt jeder Partei den Eindruck, sie verfüge über eine direkte Verbindung. Tatsächlich findet jedoch bei der Kommunikation zwischen Konsument und Server eine Kommunikation zwischen Konsument und Proxy statt, wo-
94
Entwickeln eines Prototyps
bei der Proxy die Informationen sammelt und in einer Art Paket zusammenfasst. COM verwaltet das Paket und sendet es an den Stub. Anschließend wird das Paket durch den Stub entpackt, und es wird ein Aufruf an den Server ausgeführt.
Abbildung 4.1 Die Architektur der COM-Middleware
Der Vorteil der Verwendung von Middleware besteht darin, dass Konsument und Server nichts voneinander wissen müssen. Der Konsument muss lediglich die Serverschnittstelle kennen. Auswirkungen von Standards auf Komponenten Der Grund für die Einrichtung von Standards ist die Vereinfachung des Informationsaustausches zwischen unterschiedlichen Parteien. Standards sind ein notwendiger Bestandteil unseres täglichen Lebens – ohne Standards würde die Welt im Chaos versinken. Die Softwareindustrie hat es jedoch wiederholt versäumt, derartige Standards einzusetzen. So sind einige Missverständnisse entstanden, die es zu beseitigen gilt: 왘 »Das Etablieren von Standards erfolgt nur langsam.«
Dies ist nicht länger richtig. Das ETSI (European Telecommunications Standards Institute) räumt ein, dass es in der Vergangenheit oft Jahre dauerte, einen Standard zu etablieren. Mit Einführung des Webs können Standards jedoch in weniger als einem Jahr definiert werden. 왘 »Ein Standard zwingt Produkte und Funktionen auf den kleinsten gemeinsa-
men Nenner.« Dies ist keineswegs richtig, zu sehen beispielsweise am Internet. Die Protokolle des Internets stellen einen Standard dar und werden von Millionen von Menschen eingesetzt. Es wurden unter Verwendung dieser Standards viele innovative Produkte entwickelt. Die tatsächliche Innovation liegt darin, die Standards den eigenen Anforderungen gemäß anzupassen. Obwohl das vorliegende Buch ein an Microsoft orientiertes Buch ist, in dem spezifische Microsoft-Technologien zum Einsatz kommen, können Sie ohne weiteres universelle Standards zum Erstellen von Anwendungen einsetzen. In diesem Buch
Komponenten
95
werden nach Möglichkeit derartige Standards verwendet, da diese Ihnen die Option zur Verwendung mehrerer Tools, Betriebssysteme und Umgebungen geben. Wie sieht es bei COM mit Standards aus? Microsoft entwickelte COM, aber im Laufe der Zeit wurde COM auf verschiedenen Plattformen verfügbar. Die MozillaGruppe (offener Netscape-Browser) unternimmt sogar den Versuch, eine offene, plattformübergreifende COM-Version mit Namen XPCOM zu entwickeln. Dies bedeutet, dass die COM-Technologie auch in den nächsten Jahren noch zum Einsatz kommen wird.
4.1.3 Schnittstellen und Implementierungen Nachfolgend soll die Funktionsweise von Schnittstellen und Implementierungen eingehender betrachtet werden. Die Schnittstelle definiert ein zu erreichendes Ziel; mit der Implementierung wird dieses angestrebte Ziel umgesetzt. Der Konsument interagiert ausschließlich mit der Schnittstelle. Werden ein Konsument und ein Server zusammengebracht, bilden sie einen »Vertrag«, der nicht durch eine der beiden Parteien gebrochen werden darf. Der Vorteil des Schnittstellen/Implementierungs-Ansatzes besteht darin, das Bridge-Muster umzusetzen. Ein Bridge-Muster ist ein komponentenartiges Muster. Durch Verwenden des Bridge-Musters kann der Server seine Implementierung ändern, ohne dass eine Änderung oder Neukompilierung des Konsumentencodes erforderlich wird. Die Implementierung kann außerdem mehrere Schnittstellen unterstützen. Muss beispielsweise eine Schnittstelle geändert werden, kann eine neue Schnittstelle erstellt werden. Der neue Konsument verwendet diese Schnittstellte, die alte Schnittstelle bleibt jedoch zur Unterstützung der alten Konsumenten weiterhin verfügbar. Granularität Mit den Komponenten soll das Inselkonzept umgesetzt werden, das im vorherigen Kapitel definiert wurde. Die Granularität definiert die in einer Komponente implementierte Codemenge. Ist die Granularität niedrig, ist die Menge des enthaltenen Code ebenfalls niedrig. Liegt die Granularität hoch, ist der Umfang der in der Komponente enthaltenen Codemenge ebenfalls hoch. Die Granularität spielt bei der Komponentenentwicklung eine wichtige Rolle. Betrachten Sie die Abbildungen 4.2 und 4.3.
96
Entwickeln eines Prototyps
Abbildung 4.2 Ein komplexeres Inseldiagramm mit vielen unterschiedlichen Verträgen
Abbildung 4.3 Ein einfacheres Inseldiagramm mit größeren Inseln, aber weniger Verträgen
Die Pfeile in den Abbildungen stellen die Verträge zwischen Konsument und Server dar. Aus diesen Diagrammen können zwei Beziehungen abgeleitet werden. Je höher die Granularität, desto weniger Verträge. Und je höher die Granularität, desto mehr Abhängigkeit von einer spezifischen Komponente. Abbildung 4.2 weist viele Komponenten, aber nur wenige Verträge auf, die mit einer spezifischen Komponente verknüpft sind. In diesem System mit geringer Granularität hat eine Änderung einer einzelnen Komponente keine besonders große Auswirkung auf die Anwendung. Da jedoch mehr Verträge vorhanden sind, kann die Verwaltung dieser Verträge schwieriger sein. Die Probleme wachsen mit der Anzahl der Versionen und Schnittstellen. Dies wiederum wirkt sich auf die Anwendungsstabilität aus. Abbildung 4.3 weist weniger Verträge zwischen den Komponenten auf, diese sind jedoch auch wichtiger. Es ist eine sehr große Komponente vorhanden, alle weiteren Komponenten besitzen Verträge mit dieser Komponente. Dies bedeutet, dass
Komponenten
97
die umfangreiche Komponente nicht ohne weiteres geändert werden kann, da von ihrer Funktionalität viele Konsumenten abhängen. Diese Komponente weist eine monolithische Architektur auf. Dennoch kann eine Anwendung so stabiler werden, da eine Versionsänderung ein umfangreicheres direktes Testen erfordert. Welche stellt die beste Lösung dar? Sehen Sie sich Abbildung4.4 an.
Abbildung 4.4 Eine Komponentenarchitektur mit Hybridgranularität
Bei der in Abbildung 4.4 gezeigten Architektur liegt die Komponentengranularität niedrig, es sind jedoch einige Schlüsselkomponenten vorhanden. Diese Schlüsselkomponenten enthalten mehr Funktionalität (in der Abbildung als große Kreise dargestellt). Der Grund für diesen Ansatz liegt in der Verringerung von Verständnisproblemen. Viele Verträge sind aus Sicht des Computers nicht negativ. Aus der Sicht der Person, die nur eine bestimmte Informationsmenge aufnehmen kann, ist das Verwenden vieler Verträge dagegen sehr wohl negativ. Wenn die Verträge in einer Hierarchie angeordnet werden, kann der Konsument jeden der vollzogenen Schritte verstehen. Eine weitere Möglichkeit zur Verringerung der Komplexität stellt das Erstellen spezifischer Komponenten dar, die als Vermittler dienen. Diese Komponenten verbergen eine Gruppe von Komponenten, durch die eine bestimmte Funktionalität bereitgestellt wird. Auf diese Weise muss der Konsument nur die Vermittler verstehen, nicht jedoch die gesamte Architektur. Ändert sich die Technologie, kann durch einen Vermittler diese Änderung besser verborgen werden als durch eine einzelne Komponente. Entwicklung in Schichten Üblicherweise befinden sich in einer Anwendung mehrere Komponenten, und einige Komponenten rufen weitere Komponenten auf. In einem entstehenden System erhöht sich die Anzahl der Verträge, und es ist wichtig, diese Verträge und die
98
Entwickeln eines Prototyps
Komponenten zu strukturieren. Eine Möglichkeit der Strukturierung stellt eine Schichtenarchitektur dar. In einer Schichtenarchitektur weisen Komponenten ausschließlich einseitige Abhängigkeiten auf. Eine einseitige Abhängigkeit tritt beispielsweise auf, wenn Komponente A von Komponente B abhängt, die wiederum von Komponente C abhängt; es bestehen jedoch keine weiteren Abhängigkeiten zwischen diesen Komponenten. Gegenseitige Abhängigkeiten zwischen Komponenten In einer Schichtenarchitektur werden gegenseitige Abhängigkeiten (siehe Abbildung 4.5) nach Möglichkeit vermieden.
Abbildung 4.5 Beispiel einer gegenseitigen Abhängigkeit zweier Komponenten
Das Problem bei gegenseitigen Abhängigkeiten besteht darin, dass diese nicht leicht kompiliert oder getestet werden können, da Komponente A Komponente B als Bestandteil ihres Zustands erfordert. In einer sequenziellen Kompilierung muss Komponente B zunächst definiert werden. Dies macht eine Kompilierung nahezu unmöglich. Viele Sprachen lösen dieses Problem durch vorwärtsgerichtete Definitionen. Das Testproblem gestaltet sich komplizierter. Nehmen Sie an, es soll sowohl Komponente A als auch Komponente B getestet werden, beginnend mit Komponente A. Da die Implementierung auf Komponente B beruht, können die Ergebnisse nicht als richtig betrachtet werden, denn Komponente B wurde noch nicht getestet und als richtig eingestuft. In einigen Fällen ist jedoch eine Vermeidung von gegenseitigen Abhängigkeiten nicht möglich. Dann muss ein Verfahren zum Testen der gegenseitigen Abhängigkeiten vorhanden sein. Beim Testen von Komponente A kann Komponente B durch eine künstliche Implementierung ersetzt werden. Über diese künstliche Komponente B werden die erforderlichen Entwurfsbeschränkungen implementiert. Unabhängig davon, wie die Implementierung von Komponente A Komponente B aufruft, wird immer ein richtiges Ergebnis erzielt. Dieser Ansatz kann unnötig sein, wenn die Implementierungen von Komponente A und B einfach sind. Sehr wichtig ist dieses Vorgehen, wenn die Implementierun-
Komponenten
99
gen von Komponente A und B weitere Komponenten einbeziehen, was zu einer komplexeren Implementierung führt. Gegenseitige Abhängigkeiten müssen nicht, wie hier beschrieben, direkt sein. Sie könnten mehr als das Entfernen einer Komponente umfassen. Das Hauptproblem liegt darin, dass eine Komponente von einer anderen Komponente abhängt, die wiederum von der ursprünglichen Komponente abhängt. Die verschiedenen Möglichkeiten bei der Schichtung Die Entwicklung einer Schichtenarchitektur zielt auf den Erhalt einer relativ strikt abgegrenzten Schichtenarchitektur ab. Das in Abbildung 4.6 gezeigte Schichtendiagramm besteht aus einem Satz Kernfunktionen, die durch die rechts dargestellten Komponenten definiert werden. Durch diese Komponenten wird die grundlegende Funktionalität des Systems bereitgestellt. In der nächsten Schicht, die sich links neben den Kernkomponenten befindet, wird eine weitere Funktionalitätsebene definiert. Links neben diesen Komponenten wiederum befindet sich eine weitere Schicht Komponenten. Die Verträge zwischen Konsument und Server verlaufen in eine Richtung von links nach rechts. Die Komponenten überspringen beim Aufruf einer weiteren Schicht keine der Schichten. Die Komponenten in der dritten Spalte von rechts beispielsweise rufen die Kernkomponenten nicht direkt auf. Dies ist ein Beispiel für eine strikte Komponentenschichtung.
Abbildung 4.6 Beispiel einer Schichten- und einer Zwiebelarchitektur
Abbildung 4.7 zeigt einen nicht strikten Schichtenansatz. In diesem Beispiel wird über die ursprünglich aufrufende Komponente, die erste Komponente links im Diagramm, die nächstliegende und die darunter befindliche Schicht aufgerufen. Eine Komponente in der nächstliegenden Schicht kann außerdem die Kernkomponentenschicht aufrufen.
100
Entwickeln eines Prototyps
Abbildung 4.7 Beispiel einer nicht strikten Schichtenarchitektur
Der Grund für die Verwendung einer strikten Schichtenarchitektur liegt darin, dass Sie die Auswirkungen von Änderungen innerhalb einer Schicht einschätzen können. Bei einem nicht strikten Schichtenmodell können Sie nicht einschätzen, welche Auswirkungen eine Änderung in einer bestimmten Schicht hervorruft. Selbst wenn Sie wüssten, welche Schichten von der Änderung betroffen sind, wird eine Verwaltung auf längere Sicht sehr kompliziert. Das Ziel bei der Schichtenarchitektur besteht in der Verringerung der Auswirkungen, die sich aus einer Änderung ergeben. Betrachten wir diese Situation nun in der Praxis. Ich habe bereits erwähnt, dass Sie eine relativ strikte Schichtenarchitektur anstreben sollten. Der Grund, warum ich »relativ strikt« sage, ist der, dass es gelegentlich nicht möglich ist, eine strikte Schichtenarchitektur zu implementieren. Oder es ist möglich, führt jedoch zu einer monolithischen Anwendung, die aus verschiedenen, stark voneinander abhängigen Komponenten besteht, bei der eine Ersetzung nicht mehr möglich ist. Der Schlüssel liegt in der Schichtung und dem Entwurf guter Schnittstellen. Dies kann gelegentlich bedeuten, andere Schichten zu umgehen, um bestimmte Kernfunktionen aufzurufen. Rückrufe und Ereignisse Die Schichtenarchitektur setzt voraus, dass eine Komponente der höheren Ebene eine Komponente niedrigerer Ebene aufruft – die Reihenfolge beim Aufruf ist sequenziell. Es gibt jedoch Situationen, in denen Sie einen Rückruf erzeugen möchten, bei dem ein Konsument einer Schnittstelle eine eigene Schnittstelle an den Provider weiterleitet. Ein Rückruf ähnelt einer gegenseitigen Abhängigkeit, der Unterschied liegt darin, dass die an den Konsumenten weitergeleitete Schnittstelle nicht Teil der Benutzerschnittstelle ist. Die Schnittstelle stellt eine separate Einheit dar, wie dargestellt in Abbildung 4.8.
Komponenten
101
Abbildung 4.8 Rückrufimplementierung zwischen zwei Komponenten
In Abbildung 4.8 sind zwei Komponenten sichtbar, Komponente A und Komponente B. Komponente A implementiert Schnittstelle AB und verfügt über eine Implementierung mit Namen AA. Komponente B implementiert Schnittstelle BB. Sobald Komponente A Schnittstelle BB aufruft, wird Schnittstelle AB ein Verweis auf Komponente B übermittelt. Jetzt verfügt Komponente B über die Fähigkeit, jederzeit Komponente A aufzurufen. Dieses Verhalten wird auch als asynchroner Aufruf oder als ereignisbasiertes Verhalten bezeichnet. Betrachtet man Komponente A genauer, wird deutlich, dass die Implementierungen der Schnittstellen AA und BB dieselbe Komponente, jedoch unterschiedliche Bereich der Komponente verwenden. Dies bedeutet, dass keine gegenseitige Abhängigkeit entsteht, da das Testen fortgesetzt werden kann. Es handelt sich hier jedoch um eine Gratwanderung, da die internen Strukturen der Komponente unter Umständen den gleichen Code verwenden, wodurch wiederum gegenseitige Abhängigkeiten im Code entstehen. Der Punkt ist, dass gelegentlich gegenseitige Abhängigkeiten implementiert werden müssen. Testen von Komponenten Testläufe werden häufig durchgeführt, in vielen Fällen erfolgt jedoch kein ordnungsgemäßer Test. Das Testen kann sehr langwierig sein, und wenn eine Sache langwierig wird, neigen viele Menschen dazu, sie entweder zu verschieben oder abzukürzen. In beiden Fällen führt dies dazu, dass kein richtiger Test erfolgt. Schlecht durchgeführte Tests führen zu schlechter Software. Das Testen von Komponenten unterscheidet sich vom Testen einer Anwendung, da keine Benutzerschnittstelle vorhanden ist. Eine Benutzerschnittstelle erschwert das Testen von Komponenten, da es schwierig sein kann, eine Benutzerschnittstelle als richtig oder falsch einzuschätzen. Bei Komponenten ist eine eindeutige Schnittstelle vorhanden, die zu messbaren Ergebnissen führt.
102
Entwickeln eines Prototyps
Komponenten können entweder anhand eines Programms oder eines Skripts getestet werden, mit der die Schnittstelle aufgerufen wird. Das Testen von Komponenten kann nicht mit dem Testen eines Bibliotheksmoduls verglichen werden, da eine Komponente, im Gegensatz zu einer Bibliothek, einen Statuswert enthält, der nur teilweise durch die zugehörigen Parameter definiert wird. Zum Testen einer Komponente müssen die Parameter und der Status innerhalb der Komponente validiert werden.
4.1.4 Einige Komponentenbeispiele Zum Verständnis der verschiedenen Vertragstypen müssen verschiedene Komponententypen beschrieben werden. Zu den Komponententypen gehören: 왘 COM: COM (Component Object Model) ist eine Technologie, mit der zwei se-
parate Codeabschnitte einer Anwendung miteinander kommunizieren können. COM basiert auf dem binären Konzept der VTable (Virtual Functions Table). VTables werden in Kapitel 8, »COM-Schnittstellen«, im Rahmen der Besprechung von COM näher erläutert. Im COM-Modell repräsentiert die VTable eine Schnittstellensignatur von Methoden, die durch den Anwendungscode implementiert werden. COM besitzt die Fähigkeit zur Kommunikation mit weiteren COM-Komponenten auf demselben Computer oder einem Remotecomputer. 왘 ADO/OLE DB: ADO (Active Data Objects) und OLE DB (Object Linking and
Embedding Database) sind Beispiele für COM-Schnittstellen mit spezifischen VTable-Signaturen. Diese Schnittstellen sind spezifisch für den jeweiligen Datenbankprovider. Mit Hilfe dieser vordefinierten Schnittstellen kann der Benutzer verschiedene Datenbanken auswählen, ohne dass der Code für eine spezielle Datenbank umgeschrieben werden muss. 왘 ODBC: ODBC (Open Database Connectivity) ähnelt ADO und OLE DB, bei
ODBC handelt es sich jedoch um eine DLL-Spezifikation (Dynamic Link Library). Es sind keine Objekte vorhanden, nur Funktionsaufrufe zu einer generischen Bibliothek. Auch ODBC (wie ADO und OLE DB) ermöglicht die Auswahl verschiedener Datenbanken, ohne dass der Client umgeschrieben werden muss. 왘 Gespeicherte SQL-Prozeduren: Eine gespeicherte SQL-Prozedur (Structured
Query Language) stellt eine Methode zur Interaktion mit der Datenbank dar, bei der keine Details zur Datenbank bekannt sein müssen. Eine gespeicherte Prozedur ähnelt einem Funktionsaufruf und ist in der Datenbank gespeichert. 왘 HTTP/CGI: Das HTTP-Protokoll (Hypertext Tranfer Protocol) wird zur Suche im
Web verwendet. Über dieses Protokoll wird die Datenübertragung zwischen Konsument und Provider definiert. HTTP ist eine Art standardisierte Middleware, die global anerkannt wird. Während HTTP keine Komponenten im Sinne
Komponenten
103
von COM unterstützt, wird CGI (Common Gateway Interface) unterstützt, eine Methode zum Aufrufen serverseitiger Funktionen. HTTP wird üblicherweise nicht als Komponententechnologie klassifiziert, fällt aber eigentlich unter diese Kategorie, da HTTP Middleware einsetzt und eine Methode zur Erstellung von Konsument und Provider darstellt. 왘 XML: XML (Extensible Markup Language) stellt den Datenaspekt einer Kompo-
nententechnologie dar. XML ist ein offener Standard zur Datenspeicherung sowie für den Datenaustausch zwischen Konsument und Provider. In Kombination mit HTTP/CGI stellt XML eine vollständige, vielseitige Komponententechnologie dar, die als Standard betrachtet werden kann. Im Verlauf dieses Buches wird diese Komponententechnologie zur Anwendungsentwicklung eingesetzt.
4.2 Entwickeln von Prototypen Beim UML-Entwurf für die Anwendung, der im letzten Kapitel besprochen wurde, lag das Hauptziel in der Erstellung einer Funktionsspezifikation ohne Berücksichtigung der involvierten Objekte. Jetzt besteht der nächste Schritt im Anwendungsentwicklungszyklus in der Entwicklung eines Prototyps. Der Prototyp stellt eine vorläufige Realisierung der Komponenten dar, die zusammen die vollständige Anwendung bilden. Das Entwickeln eines guten Prototyps ist nicht so einfach, wie dies zunächst erscheinen mag, da die mit einem Prototyp zu erreichenden Ziele häufig falsch ausgelegt werden.
4.2.1 Was kennzeichnet einen guten Prototypen? Wie der UML-Entwurf erfolgt der Prototypentwurf ohne besondere Berücksichtigung der Objekthierarchie. Dies ist wichtig, da Sie selten alle Details eines Projekts kennen werden. Sie haben vielleicht Kenntnisse über die Technologie, aber Sie kennen niemals alle Anwendungsdetails, selbst dann nicht, wenn Sie dies glauben. Das Ziel bei der Entwicklung eines Prototyps besteht darin, bestimmte Konzepte auszuprobieren. In der Phase der Anwendungsdefinition mussten bei der Implementierung einige Bereiche besonders berücksichtigt werden, z.B. hinsichtlich Geschwindigkeit, Verfügbarkeit, Kosten, Wiederverwendbarkeit oder ähnlicher Faktoren. Diese Faktoren machen Sie vielleicht nervös, da sie die Deadlines und die Funktionalität der Anwendung betreffen könnten. Bei der Entwicklung des Prototyps sollte dies nebensächlich sein. Im Vordergrund steht die Erarbeitung eines umfassenden Verständnisses der Anwendungsfunktionalität. Unter Zuhilfenahme des Prototyps möchten Sie die Auswirkungen der Anwendung einer Technologie und eines Verfahrens auf bestimmte Anwendungsprobleme ermitteln.
104
Entwickeln eines Prototyps
Was Sie nicht tun sollten Es gibt bestimmte Dinge, die Sie nicht tun sollten: 왘 Denken Sie nicht zuviel nach. Sie werden anfangen, über den Entwurf und die
beste Entwurfsmöglichkeit für die Objekte nachzudenken. Widerstehen Sie dieser Versuchung. Die Objekte ergeben sich bei der Entwicklung des Prototyps von allein. 왘 Verwenden Sie den Prototyp nicht wieder. Sie geraten vielleicht in die Versu-
chung, den Prototyp als Basis für die Anwendung zu verwenden. Obwohl dies eine zeitsparende Option zu sein scheint, ist sie es normalerweise nicht. Sie können einzelne Codeabschnitte wieder verwenden, nicht jedoch ganze Komponenten. Sie sollten die funktionierenden Muster des Prototyps wieder verwenden. 왘 Entwickeln Sie nicht die gesamte Benutzerschnittstelle. Über viele Proto-
typen wird lediglich die Benutzerschnittstelle entwickelt. Das liegt daran, dass die Benutzerschnittstelle einfach nachzuvollziehen und für Demonstrationen gut geeignet ist. Der Zweck eines Prototyps ist jedoch keine Demonstration, sondern die Verfeinerung des Anwendungsentwurfs. Dies soll nicht heißen, dass keine Benutzerschnittstelle entwickelt werden soll.
4.2.2 Zu implementierende Funktionen Wo sollten Sie beginnen? Der erste Schritt besteht darin, eine Liste der unterschiedlichen technischen Bestandteile der Anwendung aufzustellen. Hierbei geht es nicht darum, sämtliche der technischen Details zu verstehen. Nachfolgend sehen Sie die Liste für das in Kapitel 3 vorgestellte Konferenzanmeldungsprojekt: 왘 Webbasierte Benutzerschnittstelle 왘 Plattformübergreifende Benutzerschnittstelle 왘 COM-Komponenten ohne Benutzerschnittstellenattribute 왘 SQL Server-Datenbank 왘 Gespeicherte Prozeduren 왘 COM-Komponenten der mittleren Schicht 왘 Automatischer Import in Legacysysteme 왘 Faxerstellung
Einige der in der Liste aufgeführten Elemente wurden im Domänenmodelldokument nicht genannt. Dies geschah zur Vereinfachung des Domänenmodelldokuments. Meine Absicht lag darin, den Ansatz bei der Erstellung des Domänenmo-
Entwickeln von Prototypen
105
delltextes aufzuzeigen. Jetzt, bei der Entwicklung der Prototypkriterien, müssen jedoch alle Aspekte definiert werden. Gehen Sie mit dieser Liste zu Ihrem Kunden und erkundigen Sie sich, welche technischen Aspekte von Wichtigkeit sind. Die Schwierigkeit besteht darin, einen Kunden richtig zu verstehen. Im Beispiel verfügte der Kunde über ein abgeschlossenes Wirtschaftsstudium. Er kannte sich in Bezug auf Konferenzen und die Unterstützung der Entwicklergemeinschaft aus. Die Kenntnis der technischen Details dagegen war minimal. Dennoch war dem Kunden bekannt, welche Funktionen benötigt wurden. Ich umschrieb die technischen Details für den Kunden, und es entstand die folgende Liste: 왘 Jeder, der mit einem Webbrowser ausgestattet ist, soll auf die Site zugreifen
und diese verwalten können. Auf der Site sollen alle »coolen« DHTML-Funktionen (Dynamic HTML) enthalten sein. 왘 Einigen Benutzern steht möglicherweise kein E-Mail-Konto zur Verfügung, da-
her müssen zur Bestätigung der Konferenzanmeldungen auch Faxe eingesetzt werden. 왘 Die Website dient als Datencenter für alle Lieferanten von Drittanbieterfirmen.
Diese Liste wird mit der ursprünglichen technischen Liste verglichen, in der folgende Aspekte berücksichtigt wurden: Ein Gesichtspunkt war die Forderung, mit einem beliebigen Browser auf alle Funktionen zugreifen zu können, auf der anderen Seite stand die Notwendigkeit zur Verwendung von DHTML-Funktionen. Die Bestätigung per Fax stellte einen weiteren Problembereich dar, da Microsoft in vorherigen Versionen von Windows NT keine gute netzwerkbasierte Faxlösung bereitstellte. Der dritte Aspekt stellte die Forderung eines Datencenters dar, wofür sämtliche Drittanbieter über einen Internetzugang verfügen müssten. Dies war nicht der Fall. Einige Drittanbieter verwendeten weiterhin DOS-Anwendungen auf Windows 3.1-Arbeitsstationen. Anhand dieser Liste kann ein Prototyp entwickelt werden.
4.2.3 Festlegen der ersten Codezeile Basierend auf dem Listenvergleich mag es so erscheinen, als ob die gesamte Anwendung als Prototyp geschrieben werden muss. Dies ist jedoch nicht der Fall. Vom UML-Anwendungsfallmodell ausgehend, können Sie alle Anwendungsfälle betrachten und die minimale Anzahl Anwendungsfälle herauspicken, die alle Bereiche der Liste betreffen. So entsteht ein unvollständiger, minimaler Prototyp, aber mit diesem Prototyp wird die Anwendung optimal untersucht.
106
Entwickeln eines Prototyps
Der gewählte Anwendungsfall bestand in der Registrierung eines potenziellen Konferenzteilnehmers für eine Konferenz. Dieser Anwendungsfall führt zur Implementierung aller Kernfunktionen, die bei der Anwendung berücksichtigt werden müssen. Darüber hinaus erhalten Sie ein repräsentatives Bild zum Kompliziertheitsgrad der gesamten Anwendung. Verwenden Sie bei der Entwicklung des Prototyps keine UML-Tools, auf diese Weise vergeuden Sie lediglich Zeit. Sie möchten die Entwicklung des Prototyps so schnell wie möglich abschließen. Sie möchten aber auch keine technischen Shortcuts verwenden, darum entwickeln Sie den Prototyp. Sie möchten einige Theorien testen. Dies bedeutet, dass Sie Ihren gesunden Menschenverstand einsetzen und die Komponenten geschickt definieren. Sind die Komponenten nicht gut durchdacht, werden bestimmte Komponenteninteraktionen maskiert, die zu Problemen bei der Anwendungsimplementierung führen können.
4.2.4 Codierungsstandards Der Prototyp erfordert das Schreiben von Code. Dieser Code wird wahrscheinlich nicht wieder verwendet, die im Hinblick auf die Prototypentwicklung gemachten Erfahrungen dagegen schon. Zur Beschleunigung der Prototypentwicklung sollte eine Dokumentation vermieden werden. Bei fehlender Dokumentation muss jedoch unbedingt darauf geachtet werden, dass der Code leicht lesbar ist. Der Code selbst stellt hier die Dokumentation dar. Wenn das Team zusammenkommt und den Erfolg oder das Fehlschlagen eines Prototyps einschätzt, dient der Code als Grundlage für diese Einschätzung. Kommentierung Das Erstellen von Kommentaren ist eine Kunst und erfordert einige Vorüberlegungen. Der Hauptgrund für einen Kommentar ist die Erläuterung der Codeabsicht, falls diese aus dem Code selbst nicht klar hervorgeht. Kommentare werden nicht angefügt, wenn die Funktion klar ersichtlich ist. Es wird ein gewisser gesunder Menschenverstand vorausgesetzt. Beim Prototyp für Microsoft Deutschland lag der folgende, schlecht kommentierte Code vor. Public Function updateUser() Dim objUser As UserSite Dim status As Long Set objUser = CreateObject("VB_PDCRegistration.UserSite") objUser.user.id = user.id status = objUser.getStatus()
Entwickeln von Prototypen
107
If status = 1 Then 'Der Benutzer benötigt eine vollständige Adresse, daher leere Zeile einfügen Call addEmpty End If db.StoredProcedure "SQL_PDCRegistration", "clientUpdate200"
Der Code enthält nur einen Kommentar, »Der Benutzer benötigt eine vollständige Adresse, daher leere Zeile einfügen«. Was soll dieser Kommentar bedeuten? Bei Durchsicht des Codes können wir erkennen, was passiert, wenn status den Wert 1 erhält. Was wird erreicht, wenn status den Wert 1 annimmt? Jemand, der den Code nie zuvor gesehen hat, muss sich die Implementierung von objUser.getStatus ansehen, um den Zusammenhang zu verstehen. Der genannte Kommentar ist schlecht, da der Leser den Ablauf des Komponentenaufrufs nachvollziehen muss, um den Code zu verstehen. Was muss kommentiert werden? Für Anfänger muss erläutert werden, was erreicht werden soll. Der Funktionsname gibt an, dass Sie versuchen, einen Benutzer zu aktualisieren, aber im Kommentar wird einfach angegeben, dass beim Statuswert 1 eine leere Zeile eingefügt werden muss. Da diese Funktionalität nicht offensichtlich ist, muss ein Kommentar angefügt werden. Eine bessere Kommentierung des Code lautet folgendermaßen: Public Function updateUser() Dim objUser As UserSite Dim status As Long Set objUser = CreateObject("VB_PDCRegistration.UserSite") objUser.user.id = user.id ' Abrufen des Registrierungsstatus des Benutzers. Mögliche ' Werte sind "leer", "Site registriert" oder "Konferenz registriert" status = objUser.getStatus() If status = 1 Then ' Zur Einsparung von Funktionsaufrufen wird bei Benutzerstatus ' "leer" vorausgesetzt, dass ein gültiger, aber leerer Benutzereintrag ' hinzugefügt wird Call addEmpty End If
Beim Lesen des vorstehenden Kommentars wird die Funktionalität ersichtlich. Im ersten Kommentar wird erläutert, dass der Status die Art der Registrierung in der
108
Entwickeln eines Prototyps
Datenbank angibt. Im zweiten Kommentar wird, zur Einsparung von Funktionsaufrufen, ein leerer Eintrag hinzugefügt, wenn der Benutzer nicht vorhanden ist. Noch etwas zu den Kommentaren. Der erste Kommentar ist nicht erforderlich, da das Statusflag auf Komponentenmethodenebene in der Methode UserSite.getStatus definiert werden könnte. So wäre das Hinzufügen eines Kommentars nicht nötig. Hier wurde der Kommentar eingefügt, da anhand des Statuswertes eine wichtige Entscheidung getroffen wird – in diesem Fall das Hinzufügen eines Eintrags zur Datenbank. Würde der Status zur späteren Verwendung auf eine Variable übertragen, wäre ein Kommentar nicht erforderlich. Sie benötigen Kommentare, um dem Leser eine Codesequenz zu erläutern, ohne dass dieser blättern muss, denn dies ist störend, unnötig und zeitaufwendig. Benennungskonventionen Kommentare erläutern den Code, aber Benennungskonventionen ermöglichen Ihnen, den Code zu lesen. Code mit expliziter Benennung erfordert weniger Kommentare. Was zeichnet eine gute Benennungskonvention aus? Dies hängt von Ihrer Erfahrung ab. Bei einer guten Benennungskonvention muss zunächst sichergestellt werden, dass alle Teammitglieder die Benennungskonventionen verstehen. Hier einige Faustregeln: 왘 Verwenden Sie anstelle von kurzen Namen lange Namen. Der Name .getSta-
tus ist besser als .stat, denn der erste Bestandteil des Namens gibt an, dass Sie den Status des Objekts abrufen. Stat alleine ist nicht aussagekräftig genug. 왘 Verwenden Sie keine Akronyme, da diese regions- und sprachenabhängig vari-
ieren können. Akronyme können zu einer Fehlinterpretation des Codes führen. Wenn Sie Akronyme verwenden, sollten Sie sich an eine kurze Liste vordefinierter Akronyme halten, die von allen anderen Entwicklern im Team verwendet wird. Die Verwendung von Akronymen sollte konsistent erfolgen – wenn Sie anstelle von »table« »tbl« verwenden, sollten Sie immer die Kurzform verwenden. 왘 Verwenden Sie Begriffe aus dem Domänentextmodell der Anwendung. Diese
Begriffe werden im Text definiert, daher kann jeder, der dieses Dokument gelesen hat, den Code verstehen. 왘 Eine Variable oder Deklaration muss sich nicht nur aus einem Begriff zusam-
mensetzen. Verschiedene Begriffe können zu einem einzelnen, besser verständlichen Wort zusammengefasst werden. Bei der Verwendung zusammengesetzter Begriffe sollten Sie daran denken, dass diese das Lesen des Code erschweren. Verwenden Sie daher Großbuchstaben
Entwickeln von Prototypen
109
oder Unterstriche, um den Beginn eines neuen Wortes kenntlich zu machen. Sehen Sie sich die folgenden vier Beispiele an: SoMevAriAble SomeVariable someVariable some_variable
Das erste Beispiel zeigt, wie verwirrend ein Wort dem Leser erscheinen kann. Im zweiten Beispiel werden Großbuchstaben verwendet. Sie würden diesen Stil üblicherweise zur Kennzeichnung eines Daten- oder Objekttyps bzw. eines Funktionsaufrufs einsetzen. Das dritte Beispiel ist eine Variable, bei der es sich um eine Objektinstanz handeln kann. Das letzte Beispiel wird in Sprachen ohne Berücksichtigung der Groß- und Kleinschreibung verwendet, z.B. SQL. Die Groß- oder Kleinschreibung des ersten Buchstabens ist im Allgemeinen nicht definiert. In Java und Smalltalk beginnten Objektmethoden üblicherweise mit einem Kleinbuchstaben. In Visual C++ und Visual Basic beginnt der Methodenname mit einem Großbuchstaben. Jede dieser Konventionen ist akzeptabel, solange sie konsistent eingesetzt wird. Häufig verwendet wird auch die ungarische Benennungskonvention. Ein Zeichenfolgenwert wird hierbei z.B. durch strVar dargestellt. Der Datentyp wird in ein Akronym konvertiert und erhält als Präfix den Namen der Variablen. Auf diese Weise ist erkennbar, welchen Datentyp die Variable trägt, ohne dass die Deklaration bekannt ist. Einige Programmierer befürworten diese Notation, andere bemängeln, dass der erste Buchstabensatz eher verwirrend und schwer zu verstehen ist. Auch hier gilt: wenn Sie sich für eine Notation entschieden haben, sollten Sie diese konsistent einsetzen. Weitere Tipps für die Codierung Beim Schreiben von Anwendungscode sollten Sie niemals Shortcuts verwenden, da diese den Code zu kryptisch werden lassen. In C++ beispielsweise können Sie statt einer if-Anweisung die Notation ? : verwenden. Das Problem hierbei ist, dass diese Notation nicht sehr selbsterklärend ist. Sicher kennen die meisten C++-Programmierer die Bedeutung dieser Notation, aber mit einer if-Anweisung können auch Nicht-C++-Programmierer den Code problemlos lesen. Das if ist einfacher zu verstehen und führt zu einer besseren Trennung der verschiedenen Codeabschnitte.
110
Entwickeln eines Prototyps
4.3 Resümee Komponenten stellen die wichtigsten Elemente aller modernen, mehrschichtigen Anwendungssysteme dar. Komponenten sind keiner spezifischen Technologie zugeordnet – sie stellen die Grundidee beim Anwendungsentwurf dar. Komponenten erfordern zur erfolgreichen Anwendungsentwicklung eine Trennung von Schnittstelle und Implementierung. So kann eine Komponente aktualisiert werden, ohne dass alle Clients aktualisiert werden müssen, die von der Komponente abhängen. Bevor eine Anwendung geschrieben werden kann, muss der letzte Schritt im Anwendungsentwicklungszyklus durchgeführt werden, die Entwicklung eines Prototyps. Das Entwickeln eines Prototyps ist eine Herausforderung. Hierbei muss der Wunsch, möglichst alle Funktionen zu implementieren, zurückgestellt werden. Die Idee besteht darin, die wichtigsten Elemente zu implementieren und anschließend zu ermitteln, ob die Annahmen über die Anwendung sich als richtig erweisen. Fachleute nennen dies »einen Versuchsballon starten«. Im nächsten Kapitel soll mit der Codierung begonnen werden. Dieser Eintritt in die Implementierungsphase des Anwendungsentwicklungszyklus kann beginnen, sobald die letzte Entwurfsphase des Anwendungsentwicklungszyklus abgeschlossen ist.
Resümee
111
5 Erstellen des Thin Client Die einfachste Art der Clientbenutzerschnittstelle stellt ein Thin Client dar. Ein Thin Client ist ein Client, der mit Hilfe von HTML 4.0 programmiert wurde, der einfachsten Form von HTML (Hypertext Markup Language). Diese Version verfügt gleichzeitig über die geringste Funktionalität auf Clientseite. Die interaktive Funktionalität wird über die Programmierung der Serverseite bereitgestellt. Der Thin Client weist den Vorteil auf, dass mit ihm die größte Bandbreite an Geräten eingesetzt werden kann. Im vorliegenden Kapitel wird die Entwicklung einer Thin Client-Anwendung erläutert. HTML 4.0 ist eine relativ einfache Technologie, daher soll nicht jedes Detail von HTML 4.0, sondern die Entwicklung von HTML-Seiten beschrieben werden, die durch andere Geräte gelesen werden können. Die von den verschiedenen Browsern unterstützten HTML-Versionen entsprechen jeweils den neuesten Standards. Da der Gerätetyp, der diese Seiten liest, stets an Funktionalität gewinnt, müssen die Seiten einfach und instruktiv sein, d.h., die HTML-Seite muss den Browser anweisen, wie zum Erreichen optimaler Ergebnisse bestimmte Textabschnitte übersetzt werden sollten. Auf der Serverseite werden zum Generieren einer HTML-Seite für eine Datenbankanfrage oder bei der Verarbeitung eines HTML-Formulars Active Server Pages (ASP) verwendet.
5.1 Die Webschnittstelle Was macht die Webschnittstelle einzigartig? Sie ist dynamisch und einfach. Mit dem Web können Inhalte sofort aktualisiert und damit an das gesamten Netzwerk weitergegeben werden. Die Webarchitektur wird in Abbildung 5.1 veranschaulicht.
Webbrowser
Netzwerk
Computer
HTTPServer
HTMLSeiten
Abbildung 5.1 Standardmäßige Webarchitektur
Die Webschnittstelle
113
Zur Verwendung des Webs benötigen Sie einen Browser, einen HTTP-Server (Hypertext Transfer Protocol) sowie ein Netzwerk, das diese beiden Komponenten miteinander verbindet. Eine Webseite enthält eine Reihe von Tags, die in Benutzerschnittstellenelemente übersetzt werden. Der Browser kann die standardmäßigen HTML-Tags auf beliebige Weise anzeigen, da HTML jedoch einen Standard darstellt, ähneln sich die Benutzerschnittstellen der verschiedenen Browser auf den unterschiedlichen Plattformen. Der Browser ist für das Senden eines URL-Befehls (Uniform Resource Locator) an den HTML-Server verantwortlich. Ein URL-Befehl sollte folgende Syntax aufweisen: http://www.infoworld.com/info.html
Die URL ist eine Zeichenfolge, mit der unter Verwendung des DNS-Internetstandards (Domain Name Service) der Ressourcenstandort definiert wird. DNS ermöglicht das Auffinden des Servers im Internet. In diesem Beispiel lautet der Ressourcentyp HTTP. Dies bedeutet gleichzeitig, dass das HTTP-Protokoll für die Kommunikation zwischen Client und Server eingesetzt wird. Nach den zwei Schrägstrichen (//) werden Servername und Inhalt angegeben, in diesem Fall /info.html. Der Inhalt kann in virtuellen Unterverzeichnissen enthalten sein. Virtuell deshalb, weil der HTTP-Server das virtuelle Verzeichnis einem Verzeichnis auf der Festplatte oder einer anderen Anwendung zuordnet, die auf der Serverseite ausgeführt werden kann. Der Inhalt wird zusammen mit einem Tag zur Angabe des MIME-Typs (Multipurpose Internet Mail Extension) an den Browser weitergeleitet. Der MIME-Typ wird zur Identifikation der Behandlungsroutine eingesetzt, mit der die Daten verarbeitet werden. Handelt es sich um HTML-Inhalt, lautet der MIME-Typ text/html. Ist keine Behandlungsroutine vorhanden, wird der Benutzer durch den Browser gefragt, wie vorzugehen ist.
5.1.1 Navigation Eine gute Website ähnelt einem englischen Garten – ein ordentlicher und sauberer Platz, der Ihnen das Auffinden interessanter Dinge leicht macht. Eine Website ohne interessante Elemente gleicht einer Wüste. Andere Websites wiederum ähneln einem Dschungel, durch den es sich zu kämpfen gilt, möchte man etwas Interessantes entdecken. Webdokumente sind durch Hyperlinks miteinander verknüpft. Ein Hyperlink ist ein Zeiger, der, sobald aktiviert, den Browser anweist, an der angegebenen URL weitere Inhalte abzurufen. Ein gutes Navigationslayout erleichtert das Auffinden
114
Erstellen des Thin Client
von Informationen. Das standardmäßige Navigationslayout wird in Abbildung 5.2 gezeigt.
Abbildung 5.2 Beispiel einer Webanwendung
Eine Webseite ist in drei Abschnitte gegliedert: 왘 Banner: Bezeichnet den oberen Teil der Webseite. Das Banner ist der Ab-
schnitt, der zwar keine wichtigen Informationen zur derzeit ausgeführten Aufgabe anzeigt, jedoch nützliche Infos enthalten kann. Banner können Werbeanzeigen oder informative Verweise auf andere Bereiche der Website enthalten. 왘 Navigationsbereich: Dieser Abschnitt befindet sich üblicherweise im linken
Bereich der Website. Hier steht dem Benutzer eine Liste mit Hyperlinks zur Verfügung, die zu anderen Abschnitten der Website führen. Dieser Bereich kann ein hierarchisches Navigationsschema, Grafiken oder ein Textfeld für die Suche umfassen. Ein guter Navigationsbereich enthält nicht zu viele Informationen – ist dieser Abschnitt zu überladen, enthält die Website möglicherweise zu viele unterschiedliche Bereiche. In diesem Fall sollte die Site vielleicht in
Die Webschnittstelle
115
mehrere Websites unterteilt werden. Als Beispiel kann hier die Aufteilung der Microsoft-Website in Microsoft und MSDN genannt werden. 왘 Hauptbereich: Der verbleibende Anzeigebereich der Website. Hier sind die
Hauptinhalte und -informationen enthalten, die für den Benutzer von Interesse sind. Die einzelnen Webseitenbereiche können mit Hilfe von Webframes oder einer einzigen Seite erstellt werden. Die Seitenerstellung kann dynamisch oder auf Basis einer Stapelverarbeitung erfolgen, es kann sich jedoch auch um eine statische Seite handeln. Das verwendete Verfahren richtet sich nach dem Typ der Aktivität. Bedeutet dies, dass Ihre Webseite diesem Format entsprechen muss? Nein, aber mit diesem Seitenaufbau werden die drei Hauptaspekte einer Webseite definiert. Bei der tatsächlichen Gestaltung der Webseite können Sie Ihrer Kreativität freien Lauf lassen.
5.1.2 Websiteaktivitäten Zweck und Funktionen einer Website richten sich nach dem mit der Website verknüpften Geschäftsprozess. Der Inhalt einiger Seiten ist beispielsweise statisch und für jedermann zugänglich, auf anderen Seiten wird der Browser mit einem Token (Cookie) versehen, damit bei einem erneuten Besuch der Site der Benutzer als früherer Besucher identifiziert werden kann. Die unterschiedlichen Websiteaktivitäten können folgendermaßen klassifiziert werden: 왘 Bereitstellen von Informationen: Die einfachste Art der Aktivität. Es handelt
sich um eine reine Leseoperation, die entweder für kennwortgeschützte, datenbankgesteuerte oder für statische Seiten ausgeführt werden kann. Diese Aktivität dient nur der Bereitstellung von Informationen, da der globale Schlüssel (oder Cookie) keinen Einfluss darauf hat, wie die Daten erzeugt oder angezeigt werden. Die Informationen können direkt über ASP oder durch Aufruf einer Komponente generiert werden. Dieser Aufgabentyp folgt einem 1-n-Bereitstellungsmechanismus. Ein Beispiel ist die Anzeige von Nachrichten oder Unternehmensseiten. 왘 Interaktives Bereitstellen von Informationen: Es handelt sich ebenfalls um
eine reine Leseoperation, diese richtet sich jedoch nach dem globalen Schlüssel. Beim Generieren der Seiteninhalte werden diese an die Benutzerspezifikationen angepasst. Der globale Schlüssel verknüpft anschließend den Benutzer mit dem spezifischen Inhalt und kann den Inhalt zur »Wiedererkennung« des Benutzers bei dessen erneutem Besuch der Seite einsetzen. Ein Beispiel hierfür ist ein Gehaltsscheck, dem eine Person eindeutig zugeordnet werden kann.
116
Erstellen des Thin Client
왘 Datenänderung: Eine Eingabe-Ausgabe-Aktivität. Diese richtet sich nach dem
globalen Schlüssel und führt zur Anzeige von Informationen, die in direktem Zusammenhang mit dem globalen Schlüssel stehen. Bei der Eingabe von Daten ist der globale Schlüssel erforderlich, damit die zu speichernden Daten mit dem Benutzer verknüpft werden können. Ein Hauptmerkmal dieser Aufgaben ist die Verwendung serverseitiger Komponenten für die Eingabe-Ausgabe-Operationen. Ein Beispiel ist der An- und Verkauf von Aktien im Internet. 왘 Anonyme Datenänderung: Eine Eingabe-Ausgabe-Aktivität, die keinen globa-
len Schlüssel erfordert. Der Unterschied zwischen dieser Operation und der Anwendungsaufgabe liegt darin, dass kein globaler Schlüssel vorhanden ist. Eingabe und Ausgabe sind generische Operationen, die durch einen beliebigen Benutzer ausgeführt werden können. Ein Beispiel für diese Aktivität ist die Anmeldung an einer Konferenz, bei der die erforderlichen Angaben einmalig gemacht werden und anschließend nicht mehr auf der Website sichtbar sind. 왘 Zusätzliche Aktivitäten: Es gibt viele Aktivitäten, die keiner der zuvor genann-
ten Kategorien zugeordnet werden können. Diese Aktivitäten erfordern zum Teil einen globalen Schlüssel, zum Teil jedoch auch nicht. Diese Aufgaben müssen nicht einmal webbasiert sein. Es kann sich um Aufgaben handeln, die nicht Teil des Hauptgeschäftsprozesses sind. Eventuell muss Code geschrieben werden, einige Bestandteile anderer Aufgaben können wieder verwendet werden, stellen jedoch dennoch separate Funktionen dar. Als Beispiele können Datensicherungen, Operationen zum Füllen von Datenbanken und die Systemwartung genannt werden. Das Partitionieren einer Website ist nicht einfach. Durch das Definieren der verschiedenen Aktivitäten kann eine Webanwendung partitioniert werden. Nach der Partitionierung einer Website kann diese einfacher verwaltet und erweitert werden. Ein weiterer Grund für das Partitionieren einer Website besteht darin, dass für die verschiedenen Aktivitäten verschiedene Websitestrukturen eingesetzt werden. Informationsorientierte Sites können Tausende von Seiten enthalten, anwendungsorientierte Websites dagegen enthalten nur einige wenige Seiten. Zur Verwaltung von Tausenden von Seiten sind andere Tools erforderlich als zur Erstellung einer Website mit Datenbearbeitung. Der Typ Website, auf den sich dieses Buch richtet, sind Websites mit Funktionen zur Datenbearbeitung sowie weiteren Funktionen.
5.2 Die Benutzerschnittstelle des Thin Client Die Anwendung für die Konferenzanmeldung erfordert sowohl eine Thin Clientals auch eine Rich Client-Benutzerschnittstelle. In diesem Abschnitt soll die Be-
Die Benutzerschnittstelle des Thin Client
117
nutzerschnittstelle des Thin Client erläutert werden. Bei Projektbeginn war der HTML-Standard für den Thin Client Version 3.2. Zuvor gab es einen Unterschied zwischem dem HTML-Standard für Rich Client und Thin Client – es gab DHTML (Dynamic HTML) und HTML 3.2. Jetzt gibt es HTML 4.0.
5.2.1 Definition von HTML 4.0 HTML 4.0 unterscheidet sich von anderen HTML-Versionen dahingehend, dass kein vollständiger Arbeitssatz beschrieben wird. Stattdessen werden standardmäßige Teilsätze der HTML-Funktionalität definiert. Unter Verwendung von HTML 4.0 können beliebige Gerätetypen angesprochen werden. HTML 4.0 weist folgende Funktionen auf: 왘 Internationalisierung: Das Web ist enorm groß, und Englisch ist eine sehr po-
puläre Sprache. Dennoch gibt es noch weitere Sprachen. Zur Unterstützung dieser Sprachen bietet HTML 4.0 eine verbesserte Indizierung sowie eine bessere Unterstützung von internationalen Zeichen, Textausrichtung und Funktionen zur Umwandlung von Text in Sprache. 왘 Zugänglichkeit: Die Zugänglichkeit ermöglicht das Prüfen des Dokuments un-
ter verschiedenen Bedingungen, die keinen allgemein einsetzbaren Browser einschließen. Ein spezieller Browser für ein Auto erfordert beispielsweise Spracherkennungsfähigkeiten und weist einen kleineren Arbeitsbereich auf. 왘 Tabellen: Tabellen gibt es schon lange, aber mit HTML 4.0 kann das Tabellen-
layout besser gestaltet werden. Ein Beispiel hierfür stellt das inkrementelle Rendering dar. 왘 Verbunddokumente: In der Vergangenheit boten HTML-Seiten Unterstützung
für Grafiken. Anschließend wurde mit Einführung des -Tags die Unterstützung von Java-Applets vorgestellt. Das neue -Tag ermöglicht das generische Einbetten von Elementen in die HTML-Seite. 왘 Dokumentvorlagen: In früheren Browserversionen war es nicht immer leicht,
das Layout eines Dokuments zu steuern. Es wurden besondere Tags eingeführt, z.B. die so genannten Dokumentvorlagen, mit denen Struktur und Aussehen voneinander getrennt und damit bessere HTML-Seiten entwickelt werden konnten. 왘 Scripting: Über das Scripting kann der clientseitige Programmcode entwickelt
werden. Auf diese Weise werden die Serveranforderungen verringert und Navigation sowie Formularverarbeitung verbessert. 왘 Drucken: Ein großes Problem stellte in der Vergangenheit das Drucken von
Webseiten dar. Dieses Problem wurde mit HTML 4.0 größtenteils beseitigt.
118
Erstellen des Thin Client
5.2.2 Webzugänglichkeit Mit HTML 4.0 ist die Webzugänglichkeit sehr wichtig geworden. Früher wurde dies häufig ignoriert. Das Web ist allgegenwärtig und steht mittlerweile fast jedem zur Verfügung. Allerdings bieten Browser und Betriebssystem nur begrenzte Möglichkeiten. Bei der Erstellung der HTML-Inhalte müssen Sie sicherstellen, dass Ihre Website jedermann zugänglich ist. Hierzu müssen folgende Aspekte berücksichtigt werden: 왘 Ein Benutzer ist vielleicht nicht in der Lage, bestimmte Informationen zu lesen,
zu hören, zu verschieben oder zu verarbeiten. 왘 Ein Benutzer kann Schwierigkeiten haben, den Text zu lesen oder zu verstehen. 왘 Der Benutzer spricht möglicherweise die Sprache nicht fließend, in der die
HTML-Seite geschrieben wurde. 왘 Der Benutzer befindet sich möglicherweise in einer Situation, in der Augen,
Ohren oder Hände nicht anderweitig einsetzbar sind (z.B. beim Steuern eines Fahrzeugs oder dem Bedienen einer Maschine). 왘 Das Suchgerät (Browser) verfügt möglicherweise nicht über eine Maus oder
eine Tastatur. 왘 Das Suchgerät besitzt möglicherweise einen kleinen Bildschirm, einen Monitor,
auf dem nur Text angezeigt werden kann, einen Schwarz-Weiß-Bildschirm oder eine langsame Internetverbindung.
5.2.3 Die erste HTML-Seite Jetzt kann die Codierung des Thin Client beginnen. Bei der Codierung liegt das Hauptaugenmerk auf der Erstellung von HTML-Seiten, die über verschiedene Browser angezeigt werden können. Eine einfache »Hello world«-HTML-Seite könnte folgendermaßen codiert werden. Hello world
Die Benutzerschnittstelle des Thin Client
119
Dieses Dokument enthält drei Abschnitte. Der erste Abschnitt ist ein DOCTYPEVerweis, der angibt, um welche Art Dokument es sich handelt. Dieser Abschnitt ist nicht erforderlich, sollte jedoch hinzugefügt werden, damit über den Analysealgorithmus (Parser) ermittelt werden kann, ob eine Verarbeitung des Dokuments möglich ist. Das nächste Tag, , kennzeichnet den Dokumentanfang. Innerhalb dieses Tags sind zwei Abschnitte enthalten, , mit dem Titel und Dokumentheader definiert werden, sowie , der den Hauptteil des Dokuments enthält. In diesem Hauptteil wird der Text »Hello world« angezeigt. Beispiele für Headerdefinitionen Im Headerbereich sollte das Tag verwendet werden, damit der Browser weiß, wie mit dem Dokument verfahren werden soll. Wenn Sie beispielsweise Schlüsselwörter für die Suche im Dokument definieren möchten, können Sie folgende Zeile verwenden.
Die Schlüsselwörter sind "HTML" und "Referenz". Wenn Sie angeben möchten, wann das Dokument neu geladen werden muss, würde das -Tag folgendermaßen aussehen:
Die Expires-Definition bezieht sich immer auf die Greenwicher Zeit (Greenwich Mean Time, GMT). Das Format der Datumsangabe muss wie im Beispiel definiert sein. Tag und Monat bestehen jeweils aus drei Buchstaben. Wenn Sie einen Inhalt erstellen, der möglicherweise einen anderen Zeichensatz verwendet, können Sie den Inhaltstyp (Content-Type) wie folgt definieren.
In diesem Beispiel ist CHARSET als EUC-JP definiert. Es ist nicht nötig, den Inhaltstyp hinzuzufügen, aber es unterstützt den Parser bei seiner Arbeit.
5.2.4 Füllen der Webseite mit Inhalt Der nächste Schritt besteht darin, die Seite mit Inhalt zu füllen. Sie sollten Inhalt nicht hinzufügen, indem Sie einfach einige HTML-Tags hinzufügen und diese im Web anzeigen. Wenn Sie so vorgehen, kann möglicherweise überhaupt nicht auf das Dokument zugegriffen werden. Sie sollten zunächst die Strukturierung planen und dann den Inhalt einfügen.
120
Erstellen des Thin Client
Trennen von Struktur und Präsentation Bei Verwendung von HTML 4.0 kann die Webseitenpräsentation mit Hilfe von Dokumentvorlagen definiert werden. Dokumentvorlagen ermöglichen das Zuordnen von Positionen und Formaten zu den verschiedenen Elementen einer HTML-Seite. Die Vorteile bei diesem Ansatz liegen darin, dass verschiedene Dokumentvorlagen definiert werden können, die in verschiedenen Situationen zum Einsatz kommen. Die Dokumentvorlage für Menschen mit Sehschwächen kann beispielsweise große Schriftarten enthalten. Das Verwenden von Dokumentvorlagen lässt Ihnen außerdem mehr Spielraum für zukünftige Änderungen am Sitedesign. Dokumentvorlagen werden im nächsten Kapitel eingehender besprochen. Richtiges Verwenden von Grafiken Einer der drei Bereiche einer HTML-Seite ist das Banner. Das Banner ist nicht so einfach, wie es zunächst vielleicht erscheint. Das Problem bei den meisten Internetbannern liegt darin, dass Sie nur für die Leute toll aussehen, die die Seite lesen und die betreffende Grafik anzeigen können. Für alle anderen stellt das Banner nur eine Platzverschwendung dar. Das Hauptelement eines Banners sind Grafiken. Grafiken sind jedoch in ihrer Handhabung etwas problematisch. Zunächst stellen Sie für viele Browser, Bots, Indizierer und Endbenutzer Black Boxes dar, da Grafiken nicht wie Text entschlüsselt werden können. Grafiken erfordern einige Interpretation, die zu einer richtigen oder aber zu einer falschen Darstellung führen kann. Bei der Verwendung von Grafiken sollten Sie zunächst überlegen, ob die Grafik notwendig ist. Was ist mit notwendig gemeint? In der Vergangenheit wurden Grafiken dazu eingesetzt, einige Beschränkungen beim HTML-Layout zu umgehen. Da beispielsweise keine Dokumentvorlagen vorhanden waren, wurden spezielle Schriftarten mit Hilfe von Grafiken erzeugt. Da es mittlerweile solche Dokumentvorlagen gibt, müssen diese Verfahren nicht weiter angewendet werden. Eine Grafik sollte nur dann eingefügt werden, wenn mit ihr Informationen bereitgestellt werden, die durch einen Text nicht ausgedrückt werden können. Ist eine Grafik erforderlich, sollten Sie eine Bildunterschrift bereitstellen, in der die Grafik für diejenigen beschrieben wird, die die Grafik nicht anzeigen können. Dies ist besonders wichtig, wenn die Grafik zur Bereitstellung von Informationen dient, die an keiner anderen Stelle auf der HTML-Seite explizit erläutert werden. Sie erreichen dies durch das alt-Attribut, wie im folgenden Beispiel verdeutlicht wird.
Die Benutzerschnittstelle des Thin Client
121
Das Tag bezieht sich hierbei auf eine Grafik, das alt-Attribut enthält eine Beschreibung. Das alt-Attribut kann auch auf die Tags und angewendet werden. HTML 4.0 bietet eine Möglichkeit zur Bereitstellung einer längeren Beschreibung, das so genannte longdesc-Attribut. Mit Hilfe von longdesc ist es möglich, auf eine andere Webseite zu verweisen, die detailliert beschreibt, was durch das Element aufgezeigt werden soll. Es ist auch möglich, andere Dokumente mit Hilfe des -Tags zu referenzieren. Dieses Tag kann im Abschnitt verwendet werden, um auf ein alternatives Dokument zu verweisen, das besser für das Endgerät geeignet ist. Im folgenden sehen Sie eine Beispielverknüpfung zu einem Textdokument: Willkommen im Virtuellen Supermarkt!
Dieses Beispiel zeigt, wie eine alternative Website für Benutzer erstellt werden kann, die die Grafiken nicht anzeigen können. Weitere Textoptionen Beim Lesen des Abschnitts über die Zugänglichkeit des Webinhalts wird deutlich, dass Entwickler bei der Texterstellung sorgfältiger vorgehen sollten. Sehen Sie sich den folgenden Satz an: In Deutschland sagt man Auto, in Frankreich sagt man voiture. Das Eingeben dieses Satzes in Microsoft Word führt zu einem (angeblichen) Rechtschreibfehler. Dies geschieht deshalb, da Word voraussetzt, dass ich die Spracheinstellung »Deutsch (Deutschland)« verwende. Word weiß nicht, dass voiture ein französisches Wort ist. Bei der HTML 4.0-Spezifikation kann der Parser sprachspezifisch sein. Bei der Seitenindizierung ist es u.U. hilfreich, die Indizes nach Sprache zu trennen. Sie geben die Sprache folgendermaßen an: In Deutschland sagt man Auto, in Frankreich sagt man voiture
Das -Tag wird zur Erstellung eines gesonderten Bereichs, das Attribut lang zur Angabe der Sprache eingesetzt. Ist das Hauptdokument in französischer Sprache verfasst, sollten Sie das lang-Attribut auf das -Tag anwenden.
122
Erstellen des Thin Client
Ein weiteres sprachbezogenes Problem stellen die Akronyme dar. Wenn Sie die Bedeutung eines Akronyms nicht kennen, ist diese häufig nur schwer zu erraten. In HTML kann die Bedeutung eines Akronyms angegeben werden. Das folgende Beispiel gibt an, dass WWW für »World Wide Web« steht. Willkommen im WWW
Die Akronymdefinition kann vom Endgerät zur Angabe der Akronymbedeutung eingesetzt werden. Das Verwenden von Tags zur Definition von Textabschnitten ist dagegen schwieriger. Dies ist jedoch wichtig, wenn sie von Indizierern oder speziellen Browsern analysiert werden. Das Akronym könnte beispielsweise zur Erstellung eines Glossars für eine Website oder ein Dokument eingesetzt werden.
5.3 Verarbeiten von HTML-Seiten für den Thin Client Eine standardmäßige HTML-Seite verursacht keine serverseitige Verarbeitung. In einem Windows DNA-Framework lösen Sie mit ASP eine serverseitige Verarbeitung aus. Eine ASP-Datei ähnelt einer HTML-Seite, unterscheidet sich jedoch durch die Dateierweiterung (.asp) sowie dadurch, dass Sie Skriptelemente enthält. Das ASP-Framework kombiniert ein Scriptingmodul und HTML-Code, um eine HTML-Seite zu generieren. ASP ist erweiterbar, da der Skriptcode durch Komponenten erweitert werden kann, mit der Geschäftsoperationen ausgeführt und eventuell weiterer HTML-Inhalt generiert wird.
5.3.1 Überblick über ASP Sendet der HTTP-Server eine HTML-Seite an den Client, wird diese als HTTP-Paket gesendet, aber das Paket wird zeitversetzt und in mehreren Blöcken gesendet. Diese Blöcke können auch als HTML-Stream bezeichnet werden. Bei der Verarbeitung einer ASP-Seite wird ebenfalls ein HTML-Stream erzeugt, hierbei werden die Blöcke sofort nach Verarbeitung des Skripts gesendet. Abbildung 5.3 zeigt die Architektur von ASP.
Verarbeiten von HTML-Seiten für den Thin Client
123
Computer Webbrowser
Netzwerk
HTMLSeiten
HTTPServer ASPSeiten
ASPSeiten
Abbildung 5.3 ASP-Architektur
ASP und IIS (Microsoft Internet Information Server) stellen eine Architektur dar und können am besten zusammen eingesetzt werden. (Die Firma ChiliSoft bietet Unterstützung für ASP auf weiteren Webservern und anderen Plattformen.) Wenn ein Browser den ASP-Dateityp abfragt, handelt es sich um eine ASP-Abfrage. Die ASP-Verarbeitung umfasst folgende Schritte: 1. Der Browser führt eine Abfrage an den Server aus, beispielsweise http://server/ something/default.asp. 2. Die Abfrage wird in eine ASP-Abfrage konvertiert. Die ASP-Seite wird in den Prozessor geladen, und die Seite wird analysiert. Die Analyse beginnt oben auf der Seite und wird bis zum Ende der Seite fortgesetzt. Zu Beginn dieses Vorgangs wird standardmäßig ein Rückgabepufferstream geöffnet. Der Stream wird in Schritt 3 definiert. 3. Während der Abfrageverarbeitung können Verweise auf die vordefinierten ASP-Objekte erfolgen. Diese Objekte enthalten Informationen zu Client, Server und Abfrage. Die vordefinierten Objekte sind Application, ASPError, ObjectContext, Request, Response, Server und Session. 4. Während der Abfrageverarbeitung können COM+-Anwendungsobjekte referenziert werden. Diese Objekte führen zur Durchführung einiger Geschäftsprozesse. Üblicherweise sind diese Objekte benutzerdefiniert. 5. Der durch die Skriptverarbeitung erzeugte Streaminhalt wird an den Client gesendet. Standardmäßig wird der Stream sofort nach Generierung gesendet. Es ist möglich, mit Hilfe des vordefinierten Response-Objekts die Zeitsteuerung zu übernehmen.
124
Erstellen des Thin Client
Denken Sie daran, dass ASP mit Hilfe eines Streamkonzepts arbeitet. Wenn beispielsweise ein Streambyte geschrieben wurde, ist die Rücknahme dieses Bytes nicht möglich. Der Stream verläuft seriell, d.h., Informationen müssen in einer bestimmten Reihenfolge gesendet werden. Es sind beispielsweise vordefinierte Objektmethoden vorhanden, mit denen HTTP-Header gesendet werden. Diese Methoden müssen aufgerufen werden, bevor der HTML-Inhalt gesendet wird. Wenn Sie versuchen, diese Methoden einzusetzen, nachdem HTML-Inhalt gesendet wurde, führt dies zu einem Fehler.
5.3.2 Ein kurzes Skriptbeispiel In einer ASP-Seite sind drei Arten von Skriptblöcken vorhanden. Zwei sind serverseitig, einer ist clientseitig. Da ein Thin Client erstellt werden soll, sind die clientseitigen Skripts hier nicht von Interesse. Die Ausführung des serverseitigen Skripts findet auf dem HTTP-Server statt. Betrachten Sie das folgende Skript für eine ASP-Seite: function clientFunc() { var sum2; sum2 = 1 + 1; } function serverFunc() { var sum3; sum3 = 1 + 1; }
Wenn diese Datei über das ASP-Framework analysiert wird, wird nach Tags gesucht, die zur serverseitigen Skriptausführung führen. Diese Tags werden als Escapesequenzen bezeichnet. In diesem Beispiel sind drei solcher Blöcke enthalten, die zu einer Skriptumschaltung führen. Die Zeichen kennzeichnen den ersten Block. Hierbei handelt es sich um Code, der auf der Serverseite ausgeführt wird. Der nächste Block wird von den Tags und umschlossen. Dieser Block wird auf der Clientseite verarbeitet. Der letzte Block wird
Verarbeiten von HTML-Seiten für den Thin Client
125
erneut von den Tags und umschlossen. Der Unterschied ist, dass ein Attribut runat="server" vorliegt, mit dem angegeben wird, dass der Skriptcode auf der Serverseite ausgeführt wird. Rückgabe einer Antwort Sie benötigen keine Escapesequenzen, um Inhalt für die HTML-Methode zu generieren. Es ist möglich, das vordefinierte ASP-Objekt Response zu verwenden, wie im folgenden Beispiel verdeutlicht wird:
Die Methode Response.Write kann in einen serverseitigen -Abschnitt oder Funktionsblock eingebettet werden. Wenn Sie jedoch so vorgehen, richtet sich der Ort der Datenausgabe danach, was bisher in den Stream geschrieben wurde. Im folgenden Beispiel wird "Hello world" mit Hilfe einer Schleife zehnmal in den HTTP-Stream geschrieben:
Eine weitere Möglichkeit, Inhalt in den Stream zu schreiben, besteht darin, diesen mit Hilfe des Zuordnungsoperators direkt zu schreiben. Beispiel:
Der einzige Haken hierbei ist, dass im Skriptblock nur ein Zuordnungsoperator enthalten sein darf. Jede weitere Skriptoperation würde einen Fehler bei der Skriptausführung erzeugen. Der folgende Code ist falsch:
Welche Skriptsprache? Immer dann, wenn eine ASP-Seite aufgerufen wird, kann die verwendete Skriptsprache definiert werden. In den meisten der in diesem Buch verwendeten Beispiele wird JScript oder JavaScript verwendet. Fügen Sie oben auf der Seite den folgenden ASP-Befehl ein, um dem ASP-Framework die verwendete Sprache mitzuteilen:
126
Erstellen des Thin Client
Dieser besondere Befehl muss sich oben auf der Seite befinden und gibt an, dass als Standardsprache JavaScript verwendet wird. Als weitere Sprachen können VBScript, PerlScript und Python sowie viele andere verwendet werden. Wenn Sie auf einer Seite mehrere Skriptsprachen verwenden möchten, muss das -Tag zur Angabe der verwendeten Sprache das language-Attribut aufweisen.
5.3.3 ASP-Anwendungen Eine ASP-Anwendung ist nicht dasselbe wie eine kompilierte Programmdatei. In ASP handelt es sich bei jedem virtuellen Verzeichnis um eine Anwendung. Beim Abrufen von ASP-Seiten aus dem gleichen virtuellen Verzeichnis wird derselbe ASP-Prozessbereich verwendet. Der HTTP-Server, in diesem Fall IIS, konfiguriert virtuelle Verzeichnisse. Es ist möglich, Unterverzeichnisse auf der Festplatte zu definieren, aber diese werden nicht als separate ASP-Anwendungen erkannt. Virtuelle Verzeichnisse werden am besten unter Verwendung von Visual InterDev erstellt. Visual InterDev führt die geeigneten serverseitigen Änderungen durch und lädt den richtigen anfänglichen ASP-Anwendungsstatus. Traditionelle Anwendungen folgen dem Konzept globaler Variablen, die in der Anwendung gemeinsam verwendet werden können. ASP erweitert dieses Konzept um benutzerspezifische bzw. für die ASP-Anwendung spezifische globale Variablen. Sie müssen bedenken, dass im Gegensatz zu traditionellen Anwendungen der ASP-Anwendungsstatus mit anderen Benutzern gemeinsam genutzt wird. Das HTTP-Protokoll ist standardmäßig statuslos. Wenn also ein Benutzer eine HTML-Seite anfordert und anschließend eine zweite, erkennt der Server nicht, dass die Anforderungen vom gleichen Benutzer stammen. Mit ASP kann der Status gesetzt und auf eine Seite, einen Satz aktiver Objekte oder Aufgaben ausgeweitet werden. Innerhalb von ASP sind die folgenden Bereiche möglich: 왘 Funktion: Wird eine Variable auf Funktionsebene deklariert, existiert sie nur
innerhalb dieser Funktion. Nachdem die Funktion ausgeführt wurde, geht der Variablenwert verloren. 왘 Seite: Beim Abruf einer ASP-Seite verläuft die Seitenverarbeitung von oben
nach unten. Wird während dieser Verarbeitung eine Variable deklariert, entspricht die Lebensdauer der Variablen der der Seite. 왘 Benutzer: Greift ein Benutzer auf eine ASP-Seite zu, wird diesem Benutzer ein
Token zugeordnet, und es wird ein Status definiert. Innerhalb dieses Status können Daten gespeichert werden. Die Lebensdauer des Status endet, wenn eine Zeitüberschreitung eintritt oder ein expliziter Methodenaufruf zur Statusbeendigung ausgeführt wird.
Verarbeiten von HTML-Seiten für den Thin Client
127
왘 Anwendung: Greift ein Benutzer auf eine ASP-Anwendung zu, wird ein Status
erzeugt. Dieser Status wird durch alle Benutzer gemeinsam genutzt und ist immer dann verfügbar, wenn eine Seite abgerufen wird. Seitenbereich Der Seitenbereich kann am ehesten durch einen Vergleich der ASP-Seite mit einem seriellen Stream erläutert werden. Die Verarbeitung beginnt oben auf der ASP-Seite, und durch jede Escapesequenz wird eine Verarbeitung verursacht. Angenommen, der folgende Code ist der erste Abschnitt eines JavaScript-Skripts:
Die Variable value1 ist auf Seitenebene deklariert. Anschließend wird der Variablen der Wert 2 zugewiesen. Der nächste Codeabschnitt lautet folgendermaßen: var value2; value2 = value1 + 3; function getValue2() { return value2; } function valueAdd() { value2 = value2 + value1 + 3; return value2; } function getValue1() { return value1; }
Hier wird die Skriptumschaltung durch das -Tag verursacht. Dieser Codeabschnitt enthält eine Deklaration der value2-Variable. Diese Variable wird auf den Wert value1 plus 3 gesetzt. Beim ersten Hinsehen würden Sie wahrscheinlich denken, dass der Wert von value2 5 lautet. Dies ist jedoch nicht richtig, da der Code innerhalb des Skriptblocks nicht ausgeführt wird. Er wird ignoriert.
128
Erstellen des Thin Client
Sehen Sie sich nun folgendes ASP-Codefragment an:
Value1
Value2
ValueAdd
Value1
Value2
Die Ausgabe für den Methodenaufruf valueAdd resultiert in dem Fehler -1.#IND. Dieser Fehler entsteht, da value2 keinen Wert besitzt. Dies zeigt sich beim ersten Aufruf von getValue2. Zur Problemlösung muss value2 auf den Wert 0 gesetzt werden. Wurde der Quellcode in VBScript geschrieben, tritt kein Fehler auf, da in VBScript ein Datentypecast vorgenommen wird, und ein Typecast ohne Wert entspricht 0. Zur Deklaration von Variablen auf Seitenebene oder Verarbeitungsskripts müssen diese innerhalb eines Serverskriptabschnitts anhand der Tags definiert werden. Mit Hilfe der Funktion typeof können Sie prüfen, ob die Variable tatsächlich deklariert wurde: typeof( value2)
Diese Funktion gibt eine der nachstehenden Zeichenfolgen zurück: "number", "string", "boolean", "object", "function" oder "undefined". Benutzerbereich ASP weist eine Reihe vordefinierter Objekte auf. Hierzu gehört u.a. das Objekt Session, das zur Speicherung von Informationen auf Benutzerbereichsebene eingesetzt wird. Zur Verwendung dieses Objekts weisen Sie Variablen als Verweise zu, wie im folgenden Beispiel verdeutlicht wird: Session("SomeData") = 3
Je nach Sprache, üblicherweise VBScript oder JavaScript, liegen keine Datentypen vor. Alle Daten entsprechen dem Typ variant. Dies bedeutet, dass bei der Entwicklung der Datentyp nicht bekannt ist, es sei denn, sie fragen diesen über typeof ab. Variablen werden im Objekt Session gespeichert, indem diese wie oben gezeigt zugewiesen werden. Zur Identifikation einer Variable wird ein Zeichenfolgenbezeichner mit der Zuordnung verknüpft. Im vorangegangenen Beispiel lautet die Textzeichenfolge SomeData. Zum Abrufen des Wertes kann der folgende Code verwendet werden: =Session("SomeData");
Verarbeiten von HTML-Seiten für den Thin Client
129
Damit Benutzerbereichsvariablen funktionieren, verwendet ASP Cookies zur Verknüpfung des Benutzers mit den Daten. Cookies stellen ein unverfängliches Verfahren in HTTP dar, mit dem ein Benutzer mit einem HTTP-Server verknüpft werden kann. Anwendungsbereich Das Application-Objekt ist ein vordefiniertes Objekt mit Anwendungsbereich. Jeder Client, der eine eindeutige Session einrichtet, kann dasselbe Application-Objekt sehen. Der Zweck des Application-Objekts liegt darin, das gemeinsame Verwenden von Daten und Informationen durch einzelne Clients zu ermöglichen. Sie können, wie beim Session-Objekt, eine Variable im Application-Objekt speichern. Üblicherweise wird hierbei folgendermaßen vorgegangen:
Im obigen Beispiel werden die Methoden Lock und Unlock des Application-Objekts aufgerufen, da gleichzeitig auf dasselbe Objekt zugegriffen wird. Diese Konkurrenz tritt auf, wenn mehrere Skripts zum gleichen Zeitpunkt versuchen, den Application("AValue")-Wert zu ändern. Die Lock-Methode synchronisiert den Zugriff und verhindert so, dass zwei Clients den Inhalt einer Variablen gleichzeitig ändern. Hierbei kann nur ein Client die Sperre aufrufen. Nach Abschluss der Operation muss Unlock aufgerufen werden. Geschieht dies nicht, erfolgt der Aufruf nach Verarbeitung der ASP-Seite automatisch. Sie sollten beim Aufruf der Sperrmethoden sehr vorsichtig sein, da diese einen Engpass darstellen und eine Verlangsamung der Webanwendung hervorrufen können. Setzen und Zurücksetzen des Status Werden Objekte auf Session- oder Application-Ebene verwendet, müssen diese initialisiert werden. Wird ein nicht initialisierter Wert gelesen, wird nichts zurückgegeben, und je nach verwendetem Wert kann dies zu Problemen führen. Initialisierung und Zerstörung werden durch die Datei global.asa gehandhabt. Es gibt vier Ereignisse, die folgendermaßen definiert sind: function Session_OnStart() { } function Session_OnEnd() { }
130
Erstellen des Thin Client
function Application_OnStart() { } function Application_OnEnd() { }
Nehmen Sie eine ASP-Anwendung, die nicht ausgeführt wird – kein Benutzer hat eine ASP-Seite abgerufen. Zu diesem Zeitpunkt wird die ASP-Anwendung als leer betrachtet. Ruft der erste Benutzer eine ASP-Seite von der ASP-Anwendung ab, löst der Benutzer eine Session-Instanziierung innerhalb des ASP-Frameworks aus. Ist das Session_OnStart-Ereignis innerhalb der Datei global.asa implementiert, wird dieses aufgerufen. Zu diesem Zeitpunkt können spezielle Session-Variablen initialisiert werden. Wird der Benutzer als Benutzer von der Website entfernt, wird das Ereignis Session_OnEnd ausgelöst. Hierbei können alle erstellten Objekte freigegeben oder Operationen bereinigt werden. Handelt es sich bei dem Benutzer, der die ASP-Seite aufruft, um den ersten Benutzer, der eine Seite aufruft, löst der Benutzer eine Application-Instanziierung aus. Ist das Application_OnStart-Ereignis innerhalb der Datei global.asa implementiert, wird dieses aufgerufen. Mit diesem Ereignis können Anwendungsbereichsobjekte festgelegt werden. Es ist erforderlich, die Methoden Application.Lock und Application.Unlock zu verwenden, da über das ASP-Framework sichergestellt wird, dass nur ein Aufruf erfolgt. Wird die Anwendung geschlossen, da kein Benutzer vorhanden ist oder der Webserver heruntergefahren wird, wird das Ereignis Application_OnEnd ausgelöst. Wie bei Session kann dieses Ereignis dazu eingesetzt werden, Objektverweise freizugeben oder Operationen zu bereinigen. Interaktion bei Zeitüberschreitung Ein Benutzer wird üblicherweise aus der Anwendung entfernt, wenn für den Session-Status eine Zeitüberschreitung eintritt. Ist die Zeitüberschreitung beispielsweise auf zehn Minuten eingestellt, muss der Benutzer innerhalb von zehn Minuten die nächste ASP-Seite aufrufen. Nach Ablauf der zehn Minuten tritt eine Zeitüberschreitung ein, und alle gesetzten Werte gehen verloren. Beim Schreiben von Code in der Datei global.asa kann es zu einem Zeitüberschreitungskonflikt kommen. Nehmen Sie beispielsweise an, dass die Zeitüberschreitung für eine Sitzung auf zehn Minuten eingestellt ist, und dass innerhalb eines Session-Objekts eine Datenbankabfrage gespeichert ist. Für die Abfrage gilt eine Zeitüberschreitung von fünf Minuten. Wartet der Benutzer sechs Minuten, steht der Session-Status weiterhin zur Verfügung, die Zeit für die Datenbankabfrage ist jedoch abgelaufen. Alle weiteren Datenbankoperationen führen zu einem Fehler. Dieses Problem kann gelöst werden, indem man die Session-Zeitüberschreitung stets als niedrigsten Wert definiert.
Verarbeiten von HTML-Seiten für den Thin Client
131
5.3.4 Verarbeiten von HTML-Formularen Die am häufigsten programmierte HTML-Operation ist die Formularverarbeitung. Eine Formularverarbeitung setzt sich aus zwei Vorgängen zusammen, der Clientseite, über die eine Datensammlung erfolgt, und die Serverseite, durch die die Daten verarbeitet werden. Die Clientseite Über das -Tag innerhalb einer HTML-Seite wird eine Formularoperation spezifiziert. Es wird ein Container erstellt, der der Erstellung eines Serverprozesses dient. Damit ein Formular funktioniert, müssen die Attribute action und method wie folgt spezifiziert werden: ...
Das Attribut action definiert, welche URL zur Formularverarbeitung aufgerufen wird. Eine Formularoperation ist eine spezielle Form der HTTP-Anforderung. Im Gegensatz zu einer regulären HTML-Seite werden hierbei zusätzliche Informationen an den Server gesendet. Diese Informationen werden zum Generieren eines HTML-Textstreams benötigt, der durch das Endgerät angezeigt wird. Bei diesem Stream handelt es sich normalerweise nicht um eine statische Seite. Im Beispiel wird eine ASP-Seite aufgerufen. Das -Tag muss keine ASP-Seite sein, es könnte sich auch um eine HTML-Seite handeln. Wenn jedoch bei der Anforderung der Formularseite ein Serverstatus erforderlich ist, muss die HTML-Seite die Erweiterung .asp aufweisen. Der Unterschied zwischen GET und POST Es gibt zwei Möglichkeiten, Informationen an den HTTP-Server zu senden. Eine Möglichkeit stellt die Verwendung von GET dar, die andere besteht in der Verwendung von POST. GET und POST werden im Methodenattribut des Tags angegeben. Der Unterschied zwischen GET und POST liegt in der Handhabung durch HTTP. Gibt der Client einen HTTP-Befehl GET aus, werden der Ziel-URL verschiedene Formularfelder angehängt. Die URL wird innerhalb des HTTP-Headers gesendet. In der POST-Variante werden die Formulardaten als verschlüsselte Informationen nach dem HTTP-Header gesendet. Im Zusammenhang mit Formularen rufen GET und POST dieselbe URL auf und leiten die gleichen Informationen weiter. Der Vorteil von POST liegt darin, dass unterschiedliche Datentypen und größere Datenmengen gesendet werden kön-
132
Erstellen des Thin Client
nen. Mit Hilfe von POST kann beispielsweise eine Datei gesendet werden, ohne dass dies zu einer langen und kryptischen URL führt. Erstellen eines einfachen Formulars Für das im vorherigen Beispiel verwendete Formular werden zwei weitere Elemente benötigt. Beide Elemente sind vom Typ . Sehen Sie sich die folgende Formularimplementierung an:
Das -Tag stellt das Grundformular dar. Dieses kann, je nach Attributtyp, verschiedene Dinge repräsentieren. In der ersten -Zeile des Beispiels lautet das type-Attribut text und bezeichnet damit ein Textfeld. In der zweiten -Zeile lautet das type-Attribut submit, d.h., es handelt sich um eine Schaltfläche. Die Schaltfläche stellt einen Sonderfall dar, denn beim Klicken auf diese wird die action-URL zusammen mit den Datenformularvariablen aufgerufen. Das mit der submit-Schaltfläche verknüpfte Attribut value stellt den Text dar, der auf der Schaltfläche angezeigt wird. Es ist wichtig, jedes Formularelement unter Verwendung des name-Attributs zu identifizieren. Werden die Daten mit der URL kombiniert und an den Server gesendet, darf es sich bei den Daten nicht um ein großes Array handeln. Stattdessen werden die Daten zu dem im -Tag enthaltenen Namen in Beziehung gesetzt. Diese Information wird anschließend auf der Serverseite verwendet. Hinzufügen weiterer Formulardetails Bei Formularelementen muss es sich nicht um Textfelder handeln. Sie können folgende Elemente einschließen: 왘 Password: Ein spezielles Feld, das einem Textfeld ähnelt, bei dem jedoch die
eingegebenen Zeichen durch ein Sonderzeichen maskiert werden. Beispiel: 왘 TextArea: Ein mehrzeiliges Textfeld. Die Größe des Textbereiches wird mit Hilfe
der Attribute rows (Anzahl der Reihen) und cols (Anzahl der Spalten) definiert. Beispiel: 왘 Checkbox: Ein Kontrollkästchen, das aktiviert oder deaktiviert werden kann.
Beispiel:
Verarbeiten von HTML-Seiten für den Thin Client
133
왘 Radio: Eine Optionsschaltfläche, kann Teil einer Gruppe mehrerer Options-
schaltflächen sein. Eine Gruppe wird durch das Zuweisen mehrerer Optionsschaltlfächen zum selben Namen definiert. Hierbei werden unterschiedliche Werte eingesetzt. Innerhalb der Gruppe kann nur eine Optionsschaltfläche ausgewählt werden. Beispiel: 왘 Reset: Durch Drücken auf diese Schaltfläche werden die verschiedenen
Formularelemente auf ihre Standardwerte zurückgesetzt. Beispiel: Bei Kombinations- und Listenfeld handelt es sich um etwas kompliziertere Formularelemente. Bei Verwendung dieser Feldtypen wird vorausgesetzt, dass diese Felder vordefinierte Werte enthalten, aus denen der Benutzer wählen kann. Beispiel: Option 2 Option 1
Das -Tag kann sowohl für ein Kombinations- als auch für ein Listenfeld verwendet werden. Lautet der Wert für size 1, handelt es sich bei dem Formularelement um ein Kombinationsfeld. Liegt der Wert höher als 1, handelt es sich um ein Listenfeld. Zur Spezifizierung von Daten innerhalb des Kombinations- oder Listenfeldes wird das -Tag verwendet. Der Text innerhalb des Tags wird hierbei angezeigt. Zur Auswahl eines Standardwertes weist das Attribut selected="true" auf. Sind mehrere selected="true"-Attribute vorhanden, stellt der letzte den ausgewählten Wert dar. Eine Ausnahme stellt der multipleAttributwert true dar.
5.3.5 Der CGI-Aufruf Beim Klicken auf die submit-Schaltfläche werden die Daten in einem CGI-Befehl (Common Gateway Interface) kombiniert und an den Server gesendet. CGI ist eine Methode zur Verschlüsselung der verschiedenen Parameter. Sehen Sie sich folgende einfache Seite an:
134
Erstellen des Thin Client
Lautet der Wert für text1 "Hallo Koeln" und für text2 "Leute", lautet die verschlüsselte CGI-Zeichenfolge: text1=Hallo+Koeln&text2=Leute%2C&submit1=Submit
Bei der Datenübertragung zwischen Browser und Server weisen viele Zeichen eine unterschiedliche Bedeutung auf. Zu diesen Zeichen zählen beispielsweise Absatzmarken, Leer- und Sonderzeichen, die in Sprachen wie Deutsch oder Französisch verwendet werden. Diese Sonderzeichen müssen durch den Browser in eine neutrale Form konvertiert und anschließend auf der Serverseite wieder entschlüsselt werden. In der verschlüsselten Zeichenfolge werden die Formularelemente in eine Reihe von Namenspaarwerten konvertiert. Die Notation lautet [Formularfeldname]=[beliebiger Wert]. Einzelne Formularfelder werden durch ein kaufmännisches Und-Zeichen (&) voneinander getrennt. Leerzeichen werden durch ein Pluszeichen (+) ersetzt. Sonderzeichen werden in ASCII-Zeichen oder in eine Kurzform konvertiert, der als Präfix das Prozentzeichen (%) vorangestellt wird. Das liegt nicht daran, dass Sie Code schreiben, der diese Konvertierung vornimmt, denn dieser Vorgang wird von ASP automatisch ausgeführt. In einigen Fällen wird jedoch auch unverarbeiteter HTML-Code gesendet. In diesen Fällen liegt die Verantwortung für die Verschlüsselung der Zeichenfolge bei Ihnen. In Kapitel 7 wird näher auf dieses Thema eingegangen.
5.3.6 Die Serverseite Wenn eine Anforderung ausgeführt wird und die Verarbeitung der ASP-Seite beginnt, wird das Request-Objekt instanziiert. Dieses Objekt ist für die Analyse der CGI-Zeichenfolge und deren Zerlegung in eine Reihe von Variablen zuständig. Für den Zugriff auf die Formularfelder wird entweder die Methode QueryString oder Form verwendet. Die folgende Beispielseite verwendet beide Methoden sowie eine dritte Variante, bei der unerheblich ist, wie die Informationen vom Client an den Server weitergegeben werden.
Verarbeiten von HTML-Seiten für den Thin Client
135
Form collection Field 1: Field 2:
Querystring collection Field 1: Field 2:
Full collection Field 1: Field 2:
Um auf den Wert eines Formularfeldes zuzugreifen, können Sie die Form- oder QueryString-Methode mit dem geeigneten name-Attribut aufrufen. Im Beispielformular wird die POST-Methode eingesetzt, sodass die Form-Methode die richtigen Werte zurückgibt. QueryString erwartet, dass die individuellen Werte der GET-Methode entsprechen, daher sind diese Werte leer. Die letzte Möglichkeit zum Abrufen der einzelnen Werte stellt das Aufrufen des Request-Objekts ohne Methodenangabe dar. Diese Operation führt dazu, dass das Formularfeld sowohl im POST- als auch im GET-Abschnitt durchsucht wird. Das Aufrufen der zwei Methodenaufrufe ermöglicht das Erstellen angepasster CGI-Zeichenfolgen dar, dieses Thema kann im Rahmen des vorliegenden Buches jedoch nicht besprochen werden. Abrufen unbekannter Felder Einzelne Formularfelder können entweder durch wiederholtes Durchlaufen der Form- oder QueryString-Auflistung abgerufen werden, wie im nachfolgenden VBScript deutlich wird: Request.QueryString( ) =
Das Verwenden von Schleifen ist VBScript-spezifisch. Der Variablen x wird ein Element der QueryString-Auflistung zugewiesen. Würde die Formularmethode
136
Erstellen des Thin Client
GET lauten, enthielte die Auflistung die einzelnen Formularelemente. Die Variable x enthält den Namen des Formularfeldes. Dieser Wert wird anschließend als Index zum Abrufen des Formularfeldwertes aus der Request.QueryString-Auflistung verwendet. Das Ausführen dieser Operation in JavaScript erfordert eine manuelle Iteration, um jedes Element einzeln abzurufen. Mehrere gleichnamige Formularfelder Gelegentlich gibt es zwei Formularfelder mit gleichem Namen. Bei der Analyse des Formulars handelt es sich bei diesem Element um ein mehrwertiges Formularelement. Nachfolgend ein Beispiel für das Durchlaufen eines mehrwertigen Formularelements in VBScript:
In ASP wird ein mehrwertiges Formularelement in eine Auflistung konvertiert. Diese Iteration ist mit dem vorangegangenen Auflistungsbeispiel identisch.
5.3.7 Datenvalidierung Die Formularverarbeitung hat den Nachteil, dass keine clientseitige Validierung stattfindet. Dies bedeutet, dass die Daten zur Validierung an den Server gesendet werden. Ist ein Fehler vorhanden, muss der Benutzer das Formular erneut ausfüllen. Dies kostet Zeit und Bandbreite. Es ist einfacher, die clientseitige Validierung zu verwenden, vorausgesetzt, der Client unterstützt das Scripting. Die Datenvalidierung ist keine Geschäftsprozessvalidierung. Mit der Validierung wird sichergestellt, dass die an den Server gesendeten Werte für die Formularfelder Sinn ergeben. Hierzu stehen verschiedene Möglichkeiten zur Verfügung. Die einfachste und effektivste Methode ist das für die Konferenzanmeldungswebsite verwendete Verfahren. In der verwendeten Lösung wurde ein Clientskript implementiert. Dies schien sinnig, da die Browser mit HTML 3.2-Unterstützung 3.x-Browser waren, und dieser Browsertyp unterstützt in begrenztem Maße das Scripting. Basierend auf dieser Skripterstellung wurde eine clientseitige Validierung implementiert. Der Vorteil ist, dass so die Anzahl der Neuversuche bei der Formularübertragung verringert werden konnte. Das -Tag des type-Attributs submit wurde in button ge-
Verarbeiten von HTML-Seiten für den Thin Client
137
ändert, damit die Formularübertragung manuell gesteuert werden konnte. Der Beispielquellcode lautet wie folgt:
Um das click-Ereignis für die Schaltfäche zu erfassen, wird über das onclick-Attribut eine Ereignisbehandlungsroutine mit der Schaltfläche verknüpft. (Weitere Informationen zu diesem Attribut finden Sie im folgenden Kapitel.) Sobald auf die Schaltfläche geklickt wird, wird die Methode button1_click aufgerufen. Im Folgenden ein Implementierungsbeispiel für das Ereignis: function button1_onclick( form) { var dispString = ""; if( form.optSex.value == "") { dispString += "\nMännlich oder weiblich"; } if( form.txtFirstName.value == "") { dispString += "\nVorname"; } if( form.txtLastName.value == "") { dispString += "\nNachname"; } if( form.txtPostCode.value == "") { dispString += "\nPostleitzahl"; } else { var tempString = new String( form.txtPostCode.value); if( tempString.length > 5 || tempString.length < 4) { dispString += "\nPostleitzahl ist ungültig"; } } if( form.txtCity.value == "") { dispString += "\nStadt"; } if( form.txtStreet.value == "") { dispString += "\nStraße"; } if( form.txtTelephone.value == "") { dispString += "\nTelefonnummer"; }
Die Ereignisbehandlungsroutine leitet das zu validierende Formular weiter. Die einzelnen Formularfelder werden dahingehend geprüft, ob leere Felder oder ungültige Werte vorhanden sind. Ist ein Fehler vorhanden, wird der Fehler an die Variable dispString angehängt. Nach der Validierung (falls dispString Einträge enthält) werden die aufgetretenen Fehler angezeigt. Ist dispString leer, wird form.submit( ) aufgerufen. Dieser Methodenaufruf hat die gleiche Wirkung wie das Klicken auf eine submit-Schaltfläche. Der Vorteil dieser Methode liegt darin, dass die Fehler verarbeitet und in einem Schritt angezeigt werden. Darüber hinaus ist sämtlicher Validierungscode in einer Funktion enthalten.
5.4 Resümee In diesem Kapitel wurde mit der Codierung begonnen. Es wurde ein Framework zur Erstellung von Webanwendungen erstellt. Das Hauptziel lag in der Entwicklung eines Thin Client, d.h. einem Client ohne umfassende Funktionalität – der Großteil der Informationsverarbeitung findet unter Verwendung von ASP auf der Serverseite statt. Dies bedeutet, dass die Verarbeitungslast auf dem Server sehr viel größer ist. Daraus folgt, dass größere Server benötigt werden, damit diese Last gehandhabt werden kann. Dieser Ansatz ist gelegentlich erforderlich, besonders dann, wenn der für das Laden des Inhalts verantwortliche Browser keine komplexen HTML-Elemente verarbeiten kann. Die für das Entwickeln von Webanwendungen aufgezeigten Verfahren gelten sowohl für Thin Clients als auch für Rich Clients. In beiden Fällen handelt es sich um eine Form der verteilten Verarbeitung. Zu den bisher unbeantworteten Fragen gehört u.a., wie eine Verbindungsherstellung zu einer Datenbank erfolgt, welche weiteren Komponenten benötigt werden und wie eine vielseitigere Benutzerschnittstelle bereitgestellt werden kann. Diese Themenbereiche bilden den Schwerpunkt der nächsten Kapitel.
Resümee
139
6 Erstellen eines Rich Client Ein Rich Client nutzt die durch den Clientbrowser bereitgestellte Funktionalität weit reichender als ein Thin Client. Es wird vorausgesetzt, dass der Clientbrowser über eine derartige Funktionalität verfügt; ein Browser, der diese Funktionalität nicht aufweist, zeigt die zusätzlichen Informationen als unlesbaren Text an. Zunächst sollen die Gründe für die Verwendung von HTML (Hypertext Markup Language) aufgezeigt werden, anschließend folgt ein Überblick über die Funktionen von HTML 4.0. Viele Programmierer entwickeln für das Internet unter Verwendung von HTML, für Intranetentwicklungen setzen sie jedoch Formulare ein. Es gibt verschiedene Gründe, HTML für alle Anwendungsarten einzusetzen, unabhängig von Ort und Typ. Nach einem Überblick über die HTMLFunktionen erfahren Sie, wie Sie mit Hilfe von Dokumentvorlagen eine HTML-Formatierung durchführen. Dokumentvorlagen sind eine große Erleichterung für den Webentwickler, da mit ihnen das Layout von HTML-Seiten gesteuert werden kann. Abschließend werden die zwei Arten von Browseranwendungen erläutert, Browsingclient und Workhorseclient. Innerhalb dieses Themenbereichs werden auch weiterführende HTML-Themen wie DOM (Document Object Model) und das Scripting angesprochen. Darüber hinaus wird eine Erläuterung von XML (Extensible Markup Language) und XSL (Extensible Style Language) bereitgestellt. XML definiert ein Datenformat, während über XSL festgelegt wird, wie XML-Daten angezeigt werden. Neben diesen Technologien werden auch deren verschiedene Einsatzmöglichkeiten genannt.
6.1 Warum ein HTML-Client? Warum sollten Sie einen HTML-Client verwenden? Warum können Sie nicht eine Sprache wie Java, Visual C++ oder Visual Basic einsetzen? Die Antwort lautet, dass diese Sprachen sich besser zur Lösung anderer Probleme eignen. HTML in all seinen Facetten hat zwei besonders leistungsstarke Merkmale – seine Einfachheit und seine Dynamik.
6.1.1 Die Einfachheit von HTML HTML ist einfach, da es sich um eine textbasierte Sprache handelt. Die Tags sind relativ einfach, und das Zusammenstellen einer HTML-Seite kann mit dem Editor oder anhand eines Entwicklungstools erfolgen. HTML verursacht sehr geringe
Warum ein HTML-Client?
141
Einstiegskosten und ist aufgrund seiner Einfachheit eher auf Designer und an die Verwendung einer Oberfläche gewöhnte Benutzer als auf Programmierer zugeschnitten. Beim Erstellen einer Benutzerschnittstelle mit einem Tool wie Visual C++ ist der Designer im Nachteil. Er kann die Benutzerschnittstelle zwar anzeigen, verfügt jedoch im Allgemeinen nicht über die technischen Fähigkeiten, diese ohne die Hilfe eines Programmierers zu optimieren oder zu justieren. Andere Entwicklungssprachen, z.B. Java und Visual Basic, erfordern ebenfalls einen Programmierer. HTML bietet einem Designer die Möglichkeit, Benutzerschnittstellen ohne die Hilfe eines Programmierers zu erstellen und zu bearbeiten.
6.1.2 Die Dynamik von HTML Bei der Erstellung einer Benutzerschnittstelle mit Hilfe einer traditionellen Sprache wurde die Schnittstelle entworfen, anschließend schrieb der Programmierer den Code für die Funktionalitätsimplementierung. Die Zeiten ändern sich, und genauso müssen sich auch die Schnittstellen ändern. Schnittstellenelemente werden hinzugefügt oder entfernt, und obwohl dies häufig mit einem einzigen Drag&Drop-Verfahren in einem Formular erreicht werden kann, kann die Verknüpfung dieser Elemente etwas Arbeit erfordern. Änderungen erfordern häufig, dass der Programmierer Teile des Codes umschreiben muss, d.h., ein Teil des Programmieraufwands wird wieder zunichte gemacht. In HTML dagegen können Elemente dynamisch hinzugefügt oder entfernt werden. Das Definieren von Vorlagen und deren Anwendung auf spezielle Situationen ermöglicht die Wiederverwendung von Code. Obwohl dies in anderen Sprachen auch möglich ist, erfordert es häufig einen aus Kompilierung, Verteilung und Test bestehenden Zyklus, der länger dauert und komplizierter ist. Und damit kommen wir zur stärksten Seite von HTML. Eine HTML-Seite kann verteilt werden, ohne dass die einzelnen Clients die Seite manuell neu installieren müssen. Die in diesem Kapitel genannten Technologien sind außerdem plattformübergreifend, sofern Sie die neuesten Versionen der gängigsten Browser verwenden (zum Zeitpunkt der Entstehung dieses Buches Internet Explorer 5.5 und Mozilla 5.0 [Netscape]).
142
Erstellen eines Rich Client
6.2 Strukturierung des Dokuments Bei der Entwicklung einer HTML-Seite erstellen Sie keine Benutzerschnittstellen, sondern dynamisch generierte Dokumente. Während im vorangegangenen Kapitel das Dokument auf dem Server generiert wurde, wird der Inhalt hier auf der Clientseite verwaltet. In einem bestimmten Kontext kann der Rich Client als Fat Client betrachtet werden. In der Zeit vor dem Web bezeichnete der Fat Client eine Clientanwendung, die viele Byte in Anspruch nahm und bei der Installation viele Spuren hinterließ, d.h. es wurden viele Dateien hinzugefügt und bearbeitet. In technischer Hinsicht enthielt der Fat Client die clientseitige Anwendungslogik. Mit Einführung des Webs begann eine neue Ära der serverbasierten Computertechnologie und das Konzept der Fat Clients trat in den Hintergrund. Das Problem eines Fat Client besteht darin, dass zu viel Anwendungslogik an einer Stelle konzentriert wird. Einige konkurrenzfähige Lösungen beinhalten einen Fat Server, der in meinen Augen genauso wenig empfehlenswert ist. Der Schlüssel zu einer guten Lösung liegt in der richtigen Aufgabenverteilung zwischen den beteiligten Rechnern – hierbei sollten die jeweiligen Rechnerfähigkeiten berücksichtigt werden. In diesem Kapitel werden auch die Clientverarbeitungsvorlagen XML und XSL besprochen. Beide dieser Vorlagen werden zur Erweiterung der Benutzerschnittstelle des Rich Client eingesetzt.
6.2.1 Die Elemente einer Seite Eine typische Rich Client-HTML-Seite weist folgende Elemente auf: 왘 DOM: DOM steht für Document Object Model. Es stellt die verschiedenen
Elemente auf der Webseite dar. Die Elemente können mit Hilfe einer Skriptsprache gesteuert werden. 왘 Dokumentvorlage: Eine Dokumentvorlage definiert das Aussehen eines be-
stimmten HTML-Elements. 왘 Komponente: Eine Komponente ermöglicht die Erweiterung der Funktionalität
einer HTML-Seite, indem Code eingebettet wird, der mit Hilfe von Skriptcode nicht einfach dargestellt werden kann. 왘 XML: HTML bietet keine Lösung für die Datenverwaltung an, sondern wird le-
diglich für die Benutzerschnittstelle eingesetzt. XML dagegen kann für die Datenverwaltung eingesetzt werden; eine Benutzerschnittstelle für XML ist nicht vorhanden. 왘 XSL: XSL gleicht einer Dokumentvorlage – XML wird mit Hilfe von XSL auf der
HTML-Seite dargestellt. Die genaue Definition von XSL müsste lauten, dass es
Strukturierung des Dokuments
143
sich um eine Reihe von HTML-Tags handelt, mit der XML-Daten ausgewählt, gefiltert und dargestellt werden.
6.2.2 Dokumentlayout Jedes Dokument besitzt ein Layout, unabhängig davon, welche weiteren Funktionen implementiert sind. Daher soll zunächst die Dokumentvorlage besprochen werden. Bei dem folgenden HTML-Codebeispiel handelt es sich um ein sehr einfaches Dokument. Sehen Sie sich die folgende erste HTML-Seite an:
Hello world
Diese Seite ist nicht kompliziert. Zur Erweiterung der Seite könnten Sie alle Elemente mit Hilfe des Tags und des Attributs color erweitern. Stattdessen sollen Seiteninhalt und dessen Formatierung mit Hilfe einer Dokumentvorlage voneinander getrennt werden. Dokumentvorlagen Dokumentvorlagen ermöglichen eine Trennung von Layout und Inhalt einer Webseite. Mit Hilfe einer Dokumentvorlage können Sie beispielsweise das Layout einer HTML-Seite aktualisieren, ohne dass der Inhalt der Seite ebenfalls aktualisiert werden muss. Dokumentvorlagen sind sehr leistungsstark, da sie klein und einfach sind, aber viele Möglichkeiten haben.Sie verbessern außerdem die Zugänglichkeit einer HTML-Seite, da deren Inhalt nicht geändert wird.Einige Webdesigner verwenden zur Erzeugung von Texteffekten Grafiken. Werden diese Grafiken durch Dokumentvorlagenelemente ersetzt, kann die Seite weiterhin verarbeitet werden.
144
Erstellen eines Rich Client
Referenzierung einer Dokumentvorlage Eine Dokumentvorlage kann auf verschiedene Weise referenziert werden. Die einfachste Möglichkeit ist die, keine Referenzierung vorzunehmen, sondern die Vorlage mit Hilfe des Tags in die Webseite zu integrieren: BODY { COLOR: mediumblue; FONT-FAMILY: Arial }
Dieses Vorgehen wird nicht empfohlen, da weder eine Trennung von Dokumentvorlage und -struktur noch das Wiederverwenden der Dokumentvorlage in anderen Dokumenten möglich ist. Eine bessere Methode stellt die Verwendung des -Tags dar:
Die Attribute rel und type geben an, dass der Link auf eine Dokumentvorlage verweist. Wenn Sie dieses Tag in Ihre HTML-Seite integrieren, verwenden Sie die gleiche Notation. Über das href-Attribut wird definiert, von wo aus die Dokumentvorlage geladen werden soll. Dieses Attribut hat das Format einer gültigen URL (Unifom Resource Locator). Definieren einer Dokumentvorlage Beim Laden der HTML-Datei werden die verschiedenen Verweise auf die Dokumentvorlage geladen und verarbeitet. Anschließend wird der Wiedergabeprozess, das so genannte Rendering, gestartet. Die Dokumentvorlage definiert die Renderingmerkmale eines HTML-Tags. Beim Analysieren der HTML-Datei ermittelt der Browser die einzelnen HTML-Tags und sucht in der Dokumentvorlage nach einer Definition. Ist eine Definition vorhanden, werden die Renderingeigenschaften der Dokumentvorlage verwendet. Andernfalls werden die Standardeinstellungen für den Browser verwendet.
Strukturierung des Dokuments
145
Bei der Definition einer Dokumentvorlage erstellen Sie einen Satz »Regeln« für den Renderingprozess. Sehen Sie sich folgende Regel an: BODY { COLOR: mediumblue; FONT-FAMILY: Arial }
Das erste Element in der Regel wird als Selektor bezeichnet. Das HTML-Tag wird gerendert. Die geschweiften Klammern umschließen die Deklarationen, die in der Form Attribut: Wert; gerendert wird. Lediglich die letzte Deklaration weist kein nachgestelltes Semikolon auf. Die Regel in diesem Beispiel besagt, dass das HTML-Tag unter Verwendung der Farbe mediumblue und der Schriftart Arial gerendert werden soll. Wenn Sie diese Dokumentvorlage auf die in Kapitel 5 erstellte HTML-Seite »Hello world« anwenden, würde der Text »Hello world« in mittelblau angezeigt werden, und die Schriftart würde in Arial geändert. Dokumentvorlagen unterstützen die Vererbung. Nehmen Sie an, der ursprünglichen HTML-Seite wird das folgende HTML-Tabellenfragment hinzugefügt.
Hello
Wenn Sie die geänderte HTML-Seite anzeigen, wird der Tabelleninhalt ebenfalls blau angezeigt. Warum? Das zuvor definierte Dokumentvorlagentag galt nur für das -Tag. Das -Tag stellt den Stamm für alle Tags im Renderingprozess dar. Da das Tag
keine eigene Regel für das Rendering aufweist, wird die übergeordnete Regel verwendet, nach der die Textfarbe blau und die Schriftart Arial ist. Mit der folgenden Regel wird die Tabellenfarbe in grün geändert, und es wird eine andere Schriftart sowie -größe verwendet: TABLE { COLOR: green; FONT-FAMILY: 'Palatino Linotype'; FONT-SIZE: 20pt }
146
Erstellen eines Rich Client
Wenn Sie nun die HTML-Seite anzeigen, ist der Seiteninhalt blau, der Tabelleninhalt wird jedoch grün angezeigt, und die hier verwendete Schriftart ist größer. Dokumentvorlagen haben des Weiteren die Fähigkeit, eine Regel auf einen bestimmten Kontext anzuwenden. Sehen Sie sich folgende Regel an: TABLE B { COLOR: blue; FONT-FAMILY: 'Palatino Linotype'; FONT-SIZE: 10pt }
In diesem Beispiel wird die Vorlage auf jedes -Tag angewendet, das sich innerhalb eines
-Tags befindet. Wenn Sie die Tags und
durch ein Komma trennen, würde die Regel angewandt, wenn eines der Elemente innerhalb der HTML-Seite vorhanden ist. Dokumentvorlagenklassen Es gibt Situationen, in denen Sie eine Dokumentvorlagenregel basierend auf einem HTML-Tag definieren möchten. Sie möchten eine generische Regel definieren, die auf ein HTML-Element angewendet werden kann. Bei einer Dokumentvorlage wird dies als Klasse bezeichnet. Die Klasse kann in der HTML-Seite oder auf der Dokumentvorlagenseite definiert werden. Eine Beispieldefinition lautet folgendermaßen: .exampleClass { COLOR: red; FONT-FAMILY: 'Palatino Linotype'; FONT-SIZE: 10pt }
Zur Referenzierung dieser Vorlagenregel muss das HTML-Tag das richtige classAttribut aufweisen. Sie können die Dokumentvorlage folgendermaßen auf eine Tabellenzelle anwenden:
Cost
Beim Rendering des
-Tags wird die Regel exampleClass verwendet. Es ist auch möglich, einen Kontext für die Verwendung bestimmter Vorlagenregeln zu definieren. Angenommen, Sie möchten eine Dokumentvorlagenklasse de-
Strukturierung des Dokuments
147
finieren, die nur angewendet werden kann, wenn ein
-Tag ermittelt wird. Die zugehörige Dokumentvorlagenklasse würde wie folgt definiert: TABLE.exampleClass { ... }
Einsatzmöglichkeiten für Dokumentvorlagen Die vorherigen Beispiele sind sehr einfach und zeigen nur, wie einige Texteffekte erzeugt werden. Das eigentliche Ziel besteht jedoch darin, etwas wirklich Interessantes mit einer Dokumentvorlage zu erreichen. Nachfolgend wird gezeigt, wie Sie eine HTML-Seite mit Dokumentvorlagen vielfältiger gestalten. Arbeiten mit Tabellen Vor Einführung von DHTML (Dynamic HTML) und Dokumentvorlagen wurden für das Layout einer HTML-Seite Tabellen eingesetzt. Mit einer Dokumentvorlage ist es möglich, manuell zu definieren, wo und wie Elemente positioniert werden. Dies heißt nicht, dass keine Tabellen mehr verwendet werden sollen; diese müssen nur weniger häufig eingesetzt werden. Zu einer Tabelle gehören drei Elemente: Deklaration, Zeile und Zelle innerhalb der Reihe, wie dargestellt im nachstehenden Code:
One row, one cell table
Das
-Tag erzeugt eine Tabelle, und jede Tabelle sollte mindestens eine Zeile
und eine Zelle
aufweisen. Ist dies nicht der Fall, handelt es sich nicht um eine gültige Tabelle, auch wenn die meisten Browser dennoch die Tabelle rendern. HTML-Tabellen unterscheiden sich von regulären Tabellen, da nicht jede Zeile über die gleiche Anzahl Spalten verfügen muss. Die Daten werden in die Zelle und nicht in die Tabelle geschrieben. Eine HTML-Tabelle ist dynamisch; Zeilen- und Zellenhöhe sowie -breite werden so angepasst, dass die zu rendernden Daten aufgenommen werden können. Entwerfen eines Banners Sie können ein Dokumentvorlagenattribut direkt in einem HTML-Tag definieren. Nehmen Sie an, Sie möchten auf einer HTMLSeite ein Banner definieren. Eine Methode besteht darin, eine Tabelle zu erstellen und diese auf die Seitenbreite zu erweitern. Zur Hervorhebung des Banners sollte eine andere Hintergrundfarbe gewählt werden. Hierzu können Sie die Dokumentvorlagenregel für die entsprechende Tabellenzelle ändern. Sehen Sie sich die folgende HTML-Bannerimplementierung an:
148
Erstellen eines Rich Client
Hello world"; }
Das ID-Attribut wird insertionPoint genannt und ist mit einem
-Tag oder einer Tabellenzelle verknüpft. In diesem Skriptabschnitt wurde der Code geändert, um das insertionPoint-Objekt zu verwenden. Dieses Objekt weist alle Methoden auf, die ein reguläres Tabellenzellenobjekt besitzt. (Weitere Informationen zu DOM finden Sie in Anhang C.) Die am häufigsten verwendeten Eigenschaften zum Hinzufügen, Entfernen oder Bearbeiten von Text sind innerHTML und innerText. Diese zwei Eigenschaften können die verschachtelten HTML-Tags oder Text innerhalb des HTML-Tags enthalten. Der Unterschied zwischen diesen zwei Eigenschaften besteht darin, dass insertionPoint.innerHTML das Analysieren von HTML-Tags erlaubt, innerText dagegen nicht. Im Skriptabschnitt wird insertionPoint.innerHTML zur Anzeige von “Hello world“ eingesetzt. Seien Sie gewarnt – falls insertionPoint.innerText oder insertionPoint.innerHTML Informationen enthalten, werden diese gelöscht. Statt gesamte HTML-Inhalte zu ersetzen, können Sie HTML-Inhalt an ein Element anhängen. Dies erreichen Sie mit den Methoden insertionPoint.insertAdjacentHTML und insertionPoint.insertAdjacentText. Über diese Methoden wird das Fragment an das Textende oder das Ende der HTML-Tags gesetzt, die sich innerhalb des Objekts befinden. Durch Verwenden der Eigenschaft insertionPoint.outerHTML können sowohl das innere Fragment als auch das HTML-Ele-
Der Workhorseclient
169
ment/Objekt ersetzt werden. Aber Vorsicht! Durch das Entfernen von Inhalt und anschließendes Ersetzen durch anderen Inhalt können die Elementverweise ungültig werden, die Bestandteil des ersetzten Inhalts waren. Diese Verweisen sind anschließend unbrauchbar und können bei Verwendung zu Fehlern führen. Positionieren von Elementen Dokumentvorlagen ermöglichen das statische Positionieren von HTML-Elementen, durch eine Kombination aus Scripting und Positionierung können HTML-Elemente jedoch dynamisch platziert werden. Bei der Arbeit mit DHTML (Dynamic HTML) werden die HTML-Tags und am häufigsten verwendet. Ein -Tag ist ein einfacher Container mit HTML-Renderingfunktionen. Der Container ähnelt einem Feldbereich. Das -Tag unterscheidet sich darin, dass es sich um den inneren Textbereich handelt. Der -Bereich wird in den Textfluss integriert. Das folgende Beispiel zeigt ein Feld, dass von links nach rechts (und umgekehrt) verschoben werden kann, indem man auf die Links- oder Rechts-Schaltfläche klickt. Dieses Beispiel veranschaulicht das Verknüpfen von Schaltflächenereignissen, das Definieren eines -Bereichs und dessen Verknüpfung mit einer ID, das Definieren der -Bereichs anhand der Dokumentvorlagenklasse zur Definition von Länge und Breite sowie der linken oberen Position. Dieses Beispiel wurde mit VBScript geschrieben, für die Auswahl dieser Sprache lag jedoch kein besonderer Grund vor. .Box { position: absolute; left: 100px; top: 100px; width: 200px; height: 80px; overflow: 'clip'; background: 00cc00; } Sub butMoveLeft_Click() currBox.style.pixelleft = currBox.style. pixelleft - 10 end sub Sub butMoveRight_Click() currBox.style. pixelleft = currBox.style. pixelleft + 10 end sub
170
Erstellen eines Rich Client
Hello from DHTML
Die Vorlagenklasse Box definiert Dimensionierung sowie Farbe des Bereichs. Es handelt sich um ein gutes Beispiel zur Trennung von Struktur und Dokument, da das Aussehen des Dokuments geändert werden kann, ohne das die enthaltene Funktionalität hiervon betroffen wäre. Das -Element besitzt ein class-Attribut, das der Vorlagenklasse Box entspricht. Nach dem -Element folgen zwei Schaltflächen, die das Verschieben des Feldes nach links oder rechts ermöglichen. Wo befinden sich beim Rendern des Dokuments die Schaltflächen in Relation zum Feld? Die kurze Antwort lautet, dass die Schaltflächen auf den Feldbereich folgen. Dies ist jedoch aufgrund der Art des Rendering für -Bereich und Schaltflächen nicht unbedingt richtig. In der Regel für die Vorlagenklasse Box besagt die Deklaration, dass absolute Koordinaten verwendet werden (position: absolute). Da mit den Eingabeschaltflächen jedoch kein Positionierungsmodus verknüpft ist, wird eine relative Position verwendet. Daher lautet die richtige Antwort, dass die Schaltflächen vor dem Feld gerendert werden. Die Klassenregel Box verwendet die Deklarationen left, right, top und height. Im Skriptabschnitt wird jedoch style.pixelLeft verwendet. Dies ist wichtig. In der Klassenregel Box sind die Einheiten textbasiert und weisen das Suffix px auf, womit angezeigt wird, dass die Einheiten in Pixeln angegeben werden. Bei Referenzierung dieser Eigenschaft erhält man Textergebnisse. Zum Hinzufügen von Werten benötigen Sie daher die Eigenschaften posLeft oder pixelLeft – beide Werte sind numerisch. Die gleiche Regel gilt für top, posTop, pixelTop, right, posRight, pixelRight, height, posHeight und pixelHeight.
6.4.2 Handhabung der Maus Sie wissen nun, wie Sie Ereignisse an spezielle Elemente binden können. Mit DOM können Ereignisse an ein beliebiges Element auf einer HTML-Seite gebunden werden. Damit sichergestellt ist, dass das Ereignis ausgelöst wird, muss das Objekt jedoch das eingebundene Ereignis unterstützen. Beim Rendern eines
Der Workhorseclient
171
HTML-Objekts belegt dieses einen bestimmten Bereich. In diesen speziellen Bereichen können Standardereignisse ausgelöst werden. Es sind folgende Ereignisgruppen definiert: 왘 Mausereignisse: Diese Ereignisse sind mit der Maus verknüpft. Hierzu zählt
beispielsweise das Klicken in oder auf einen Bereich. Ein leistungsstarker Ereignissatz gibt an, wann sich die Maus in dem durch das Objekt belegten Bereich befindet bzw. nicht mehr befindet. 왘 Tastaturereignisse: Die standardmäßigen Tastaturereignisse. Hierzu zählen Er-
eignisse, die ausgelöst werden, sobald eine Taste gedrückt und wieder losgelassen wird. 왘 Zellenereignisse: Diese Ereignisse sind mit Aktionen verknüpft, die als Ergeb-
nis einer Benutzeraktion in einer Zelle ausgeführt werden. Hierzu gehören Zellaktivierung, -deaktivierung und eine Änderung der Zellengröße. Wird eine Bildlaufleiste neu positioniert, wird ein Bildlaufereignis ausgelöst. 왘 Dokumentereignisse: Werden das Dokument, ein Applet, ein Steuerungsele-
ment oder eine Grafik geladen oder entladen, führt dies zur Auslösung spezieller Ereignisse. Diese Ereignisse sind nützlich, da es sich um globale Ereignisse handelt, die zum Einstellen von Variablen auf globaler Ebene eingesetzt werden können. Sie ermöglichen das Entwickeln zuverlässiger DHTML-Skripts, da der Status von Applets und Objekten vordefiniert wird. Ereignisbubbling Das Bubbling wird im DHTML-Ereignismodell eingesetzt, wodurch es sich von anderen Ereignismodellen unterscheidet. Das Ereignisbubbling löst Ereignisse aus, die zunächst das Ereignis des betroffenen HTML-Elements aufrufen. Wenn die Ereignisbehandlungsroutine diese Verarbeitung abgeschlossen hat, erhält das HTMLElement mit dem auslösenden HTML-Element das Ereignis. Das Ereignis wird solange weitergegeben, bis der oberste Objektcontainer erreicht ist. Hier ein kleines Beispiel: function baseCell_onclick() { window.alert( "Zelle angeklickt"); }
172
Erstellen eines Rich Client
function bodyCell_onclick() { window.alert( "Hauptteil angeklickt"); } Hier ist eine Zelle
Auf dieser Seite befinden sich zwei ID-Objekte, bodyCell und baseCell. Das baseCell-Objekt ist eine -Zelle mit verknüpfter onclick-Mausbehandlungsroutine. Das bodyCell-Objekt ist eine -Zelle und verfügt ebenfalls über eine verknüpfte onclick-Mausbehandlungsroutine. Der Bereich von bodyCell ist der gesamte Clientbereich, der die HTML-Seite anzeigt. Wenn Sie auf Hier ist eine Zelle klicken, werden zwei Meldungen angezeigt. Die erste ist Zelle angeklickt, die zweite lautet Hauptteil angeklickt. Die zweite Meldung ist ein Beispiel für das Ereignisbubbling vom - bis zum HTML-Tag. Wenn Sie in einen umliegenden Bereich klicken, wird nur eine Meldung ausgelöst – Hauptteil angeklickt –, da das -HTML-Tag nicht betroffen ist. Wozu kann das Ereignisbubbling eingesetzt werden? Über das Bubbling können globale Behandlungsroutinen erstellt werden. Sie können beispielsweise eine Routine erstellen, durch die die Schriftart des HTML-Elements sich ändert, wenn ein Benutzer darauf klickt. Statt jedes Element an die globale Routine zu binden, muss lediglich ein onclick-Ereignis an das -Element geknüpft werden. Abbrechen eines Ereignisses Es ist gelegentlich wünschenswert, ein Ereignis abbrechen zu können, damit das Bubbling nicht fortgesetzt wird. Hierzu muss das event-Objekt bearbeitet werden. Beim Auslösen eines Ereignisses instanziiert der Browser das event-Objekt. Dieses event-Objekt ist global und enthält das Ereignis, durch das ursprünglich das Ereignis ausgelöst wurde. Von diesem Objekt aus kann das Ereignis auch abgebrochen werden: function baseCell_onclick() { window.alert( event.srcElement.tagName); event.cancelBubble = true; }
Der Workhorseclient
173
Die Änderung der Eigenschaft event.cancelBubble = true stoppt den Bubblingprozess, es werden keine weiteren Ereignisbehandlungsroutinen mehr aufgerufen, wenn die aktuelle Ereignisbehandlungsroutine abgeschlossen ist. Das Ereignisbubbling tritt nicht ständig auf. Einige Ereignisse weisen kein Bubbling auf und lösen jediglich ein spezifisches Element aus. Ein Beispiel für ein solches Ereignis ohne Bubbling ist onHelp. Dies ist logisch, da Sie nur für das Element Hilfeinformationen anzeigen möchten, auf das Sie klicken, nicht jedoch für die übergeordneten Objekte. Welches Element stellt die Quelle der Ereignisbehandlungsroutine dar? Ein Problem des Ereignisbubbling ist, das nicht bekannt ist, welches Element das Ereignis ausgelöst hat. Im ersten Beispiel für das Ereignisbubbling wurde das Ereignis Hauptteil angeklickt immer aufgerufen, unabhängig davon, in welchen Bereich geklickt wurde. Die Schwierigkeit liegt darin, das aufrufende Element zu ermitteln. Der Kontext des Ereignisses kann hierbei hilfreich sein. Nehmen wir erneut den ersten Quellcode für das Ereignisbubbling. Ändern Sie beide window.alert-Methoden folgendermaßen ab: window.alert( event.srcElement.tagName);
Wenn Sie jetzt auf Hier ist die Zelle (das -Element) klicken, werden zwei Meldungen mit dem Text DIV angezeigt. Ein Klick in einen anderen Bereich der Seite führt zu einer Meldung mit dem Text BODY. So kann die Quelle des Ereignisses ermittelt werden. Die Verwendung von tagName zur Identifizierung kann jedoch unter Umständen nicht ausreichen. Auf der Seite befinden sich möglicherweise mehrere HTML-Elemente des gleichen Typs. Zur Ermittlung des genauen Aufrufers ist die Verwendung der srcElement-Eigenschaft erforderlich. Ändern Sie im ursprünglichen Bubblingquellcode bodyCell_onclick folgendermaßen ab: function bodyCell_onclick() { if( event.srcElement == baseCell) window.alert( "Sie haben auf die Basiszelle geklickt"); else window.alert( "Hauptteil angeklickt"); }
Es ist ein direkter Vergleich zwischen der Eigenschaft event.srcElement und dem Objekt baseCell möglich. Die Eigenschaft event.srcElement verweist auf das Objekt, durch das das Ereignis ausgelöst wurde. Eine Eigenschaft dieses Objekts ist tagName, es sind jedoch weitere Eigenschaften vorhanden, beispielsweise in-
174
Erstellen eines Rich Client
nerHTML, innerText sowie die Methoden insertAdjacentHTML und insertAdjacentText.
6.4.3 Schreiben von Skriptcode Üblicherweise wird ein Skript durch direktes Einfügen in eine HTML-Seite integriert. Dies bedeutet jedoch, dass Dokument und Struktur nicht voneinander getrennt werden, eine Wiederverwendbarkeit wird damit ausgeschlossen. Sie können ein Skript anhand der folgenden Syntax mit der HTML-Seite verknüpfen:
Auch hier wird wieder das script-Tag verwendet, doch statt das Skript direkt in die Seite zu schreiben, wird über das src-Attribut der Standort der Skriptdatei angegeben. Definieren skriptfähiger Objekte Wie bei anderen Programmiersprachen auch, können Sie mit Hilfe von JavaScript Objekte definieren. Im Gegensatz zu anderen Sprachen müssen JavaScript-Objekte jedoch miteinander verknüpft werden. Dieses Verknüpfen von JavaScriptObjekten kann durch ein wieder verwendbares JavaScript-Objekt zur dynamischen Tabellenerstellung verdeutlicht werden. Das Objekt heißt TableBuilder und weist die Methoden addTable und setContents auf. Hierbei dient addTable dem Hinzufügen einer Tabelle mit den angegebenen Zeilen und Spalten, mit setContents wird der Inhalt von Zellen und Spalten festgelegt. Der Quellcode für das TableBuilder-Objekt lautet folgendermaßen: function TableBuilder() { this.addTable = _TB_addTable; this.setContents = _TB_setContents; this.table = null; this.rowCount = 0; this.colCount = 0; }
Beim Deklarieren eines JavaScript-Objekts muss zunächst eine Superfunktion vorhanden sein. Diese Funktion wird zum Objektconstructor. In diesem Fall handelt es sich um function TableBuilder. In dieser Funktion sind verschiedene this-Verweise enthalten. Der this-Verweis gibt an, dass Speicherfunktionen und Variablen erstellt werden. Die ersten zwei, this.addTable und this.setContents, sind Funktionen. Ihnen wird _TB_addTable und _TB_setContents zugeordnet, bei denen
Der Workhorseclient
175
es sich ebenfalls um Funktionen handelt. Die Notation _TB_xxx ist eine Benennungskonvention zur Angabe einer Methode, die durch die TableBuilder-Klasse (TB) verwendet wird. Sie können eine beliebige Benennungskonvention verwenden, aber Sie müssen sicherstellen, dass der Code mit der gewählten Konvention übersichtlich bleibt. Die letzten drei this-Elemente sind Variablen, durch die der Objektstatus angegeben wird. Die Implementierung der _TB_addTable-Funktion lautet wie folgt: function _TB_addTable( refObject, nameTable, rowCount, colCount) { this.table = document.createElement( "table"); this.table.setAttribute( "id", nameTable, false); this.table.setAttribute( "border", 1, false); this.rowCount = rowCount; this.colCount = colCount; for( iRow = 0; iRow < rowCount; iRow ++) { this.table.insertRow( 0); for( iCol = 0; iCol < colCount; iCol++) { this.table.rows[ 0].insertCell( 0); this.table.rows[ 0].cells[ 0].innerHTML = "Nothing"; } } refObject.appendChild( this.table); }
Diese Funktion weist folgende Parameter auf: 왘 refObject: Ein DHTML-Objekt, das als Einfügepunkt für die neue Tabelle dient. 왘 nameTable: Der ID-basierte Name der neu erstellten Tabelle. 왘 rowCount und colCount: Die Anzahl der Zeilen und Spalten, die die neue Ta-
belle enthalten soll. Der erste Schritt in dieser Funktion besteht in der Erstellung eines neuen Tabellenobjekts. Im DOM-Modell können keine Table-Objekte erstellt werden, da diese nicht existieren. Ein Tabellenobjekt wird durch die Instanziierung mit Hilfe der document.createElement-Methode erstellt, wobei der Parameter dem HTMLElement gegenüber eine Zeichenfolge darstellt. Der Rückgabewert dieser Methode ist die Tabelle. Da eine Navigation in der Tabelle möglich ist, wenn das Tabellenobjekt referenziert wurde, verfügt das TableBuilder-Objekt mit this.table über einen Verweis auf die Tabelle.
176
Erstellen eines Rich Client
Die Tabelle, wenn neu instanziiert, ist eine unverarbeitete Tabelle ohne Zeilen oder Spalten. Anhand der Methode table.setAttribute kann das ID-Attribut auf den Tabellennamen gesetzt werden (nameTable). Das andere Attribut, border, wird zur Anzeige der Tabellenränder eingesetzt. Zur Vereinfachung werden Zeilen- und Spaltenanzahl dem Objektstatus zugewiesen. Anschließend müssen die einzelnen Zeilen und Spalten erstellt werden. Das Tabellenobjekt verfügt über die Methode insertRow, mit der am angegebenen Punkt eine Zeile eingefügt und die neu instanziierte Zeile zurückgegeben wird. Zur einfacheren Referenzierung wird über this.rows[0].insertCell eine einzelne Zelle eingefügt. Die Einfügeoperatoren für Zeilen und Zellen werden wiederholt, bis die Werte der Variablen rowCount und colCount erreicht sind. Der letzte Schritt besteht im Hinzufügen der Tabelle zum Einfügepunkt. Hierzu wird refObject.appendChild verwendet, die Tabelle selbst stellt hierbei den Parameter dar. Zum Instanziieren und Aufrufen des TableBuilder-Objekts können Sie den folgenden HTML-Text verwenden: function butAddTable_onclick() { var tb = new TableBuilder(); tb.addTable( insertionPoint, "meineTabelle", txtRowCount.value, txtColCount.value); } Row: Columns:
Der Workhorseclient
177
Oben im HTML-Text wird die Skriptdatei mit Hilfe des -Tags referenziert. Zur Vereinfachung sollten Sie die Verweise oben auf der Seite hinzufügen, noch bevor das Scripting beginnt. So bleibt die Seite strukturiert und übersichtlich. Anschließend wird das Skript mit dem Schaltflächenereignis butAddTable_onclick verknüpft. Bei diesem Ereignis wird zunächst das TableBuilder-Objekt instanziiert, anschließend wird die TableBuilder.AddTable-Methode aufgerufen. Der Einfügepunkt in diesem Beispiel ist eine leere -Zelle.
6.5 Resümee Das Web hat die Arbeit mit Daten mehr als revolutioniert. Einige Dinge sind einfacher geworden, und wir können Informationen anzeigen, deren Anzeige zuvor nicht möglich war. Durch diese neue Flexibilität und die vielen neuen Optionen sind jedoch die Benutzerschnittstellen sehr viel komplexer geworden. Dies ist nicht notwendigerweise ein Nachteil, es muss lediglich zwischen den nützlichen und den überflüssigen Funktionen unterschieden werden. In diesem Kapitel wurde der browserbasierte Client zur Bereitstellung von mehr Funktionalität erweitert. Die Fähigkeit zur Klassifizierung von Funktionen ist besonders bei diesem Clienttyp sehr wichtig. Beim Anzeigen und Lesen von Daten ist beispielsweise der Browsingclient nützlich. Zur Arbeit mit Daten eignet sich der Workhorseclient besser. Natürlich ist keine Lösung nur ausschließlich für eine Aufgabe einsetzbar. Die meisten Lösungen stellen Kombinationen beider Situationen dar. Dies ist in Ordnung, so lange Daten und Code granular gehalten werden. Hierdurch ist es möglich, Komponenten zu definieren, die unter Verwendung von Harddaten miteinander kommunizieren. Dies wiederum ermöglicht ein erneutes Durchlaufen des Anwendungsentwicklungszyklus, um den sich ändernden Geschäftsanforderungen gerecht zu werden. Der nächste Schritt besteht darin, einige nützliche Daten zu generieren. Hierzu wird über das im vorangegangenen Kapitel besprochene ASP-Framework eine Verbindung zwischen ASP und einer Datenbank hergestellt.
178
Erstellen eines Rich Client
7 Entwickeln einer Webanwendung In Kapitel 5 wurde der Thin Client vorgestellt, Kapitel 6 enthielt eine Einführung in den Rich Client. Im vorliegenden Kapitel soll der Prozess der Webanwendungsentwicklung erläutert werden. Das Hauptziel besteht hierbei darin, die Informationen aus Kapitel 5 und 6 mit einigen neuen Verfahren zu kombinieren. Das Entwickeln von Webanwendungen unterscheidet sich von der Entwicklung traditioneller Anwendungen dahingehend, dass bei einer Webanwendung eine Server- und eine Clientseite vorhanden ist. Die Serverseite ist nicht vom ausgeführten Clientbrowser abhängig – es können Inhalte für einen beliebigen HTTP-basierten Browser generiert werden.
7.1 Erstellen der serverseitigen Anwendungslogik Bei der einfachsten Methode der Webanwendungsentwicklung wird vorausgesetzt, dass die wichtigsten Verarbeitungsprozesse auf Serverseite stattfinden. Dies bedeutet, dass auf der Serverseite Harddaten gespeichert werden. Der Client arbeitet mit Softdaten. Im Falle des Thin Client können auf der Clientseite keine Softdaten gespeichert werden. Hier wird der Status schrittweise erstellt und temporär im ASP-Objekt Session gespeichert.
7.1.1 Cookies, ja oder nein? Session-Variablen werden mit Hilfe von Cookies implementiert, über die sich die Geister scheiden. Es gibt viele Endbenutzer, die keine Cookies akzeptieren, und es gibt Proxys, die Cookies herausfiltern. Der Grund für die Verwendung von Cookies ist einfach. Bei der Entwicklung des ursprünglichen HTTP-Protokolls wurde dies als statuslos betrachtet. Dies ist jedoch nur scheinbar eine gute Eigenschaft eines Transaktionsverarbeitungssystems. Das eigentliche Problem besteht darin, dass die Statuslosigkeit für jede einzelne Anforderung gilt. Beim Anfordern einer Seite wird diese aufgerufen, anschließend wird die Verbindung getrennt. D.h., obwohl die gesamte Transaktion in einer HTTP-Anforderung ausgeführt wird, wird der Vorgang sehr langwierig – es muss für jede Anforderung ein Kontext erstellt werden. Zur Umgehung dieser Einschränkung verwendet ASP (Active Server Pages) zur Identifizierung der Benutzer und für das Erstellen eines Session-Objekts Cookies. Ein Cookie ist ein einfaches Token zur Benutzeridentifizierung. Es ist textbasiert und führt zu keiner Codeausführung. Da ein Cookie nur zur Identifizierung des aktuellen Benutzers, nicht jedoch zur Ermittlung der Identität eines Benutzers eingesetzt wird, stellt es auch keinen schwerwiegenden Eingriff in die Privatsphäre dar. Wenn Ihnen Cookies
Erstellen der serverseitigen Anwendungslogik
179
nicht geheuer sind, leeren Sie den Cookiepuffer einfach täglich. Cookies sind harmlos!
7.1.2 Verschieben der Daten Als Nächstes müssen Sie ermitteln, wie Daten vom Client zum Server und umgekehrt verschoben werden. Es wurden bereits einige Verfahren hierzu vorgestellt, beispielsweise die Verwendung von CGI (Common Gateway Interface) und XML (Extensible Markup Language), die auch weiterhin verwendet werden sollen. Der Vorteil dieser Methoden liegt darin, dass sowohl CGI als auch XML offene Standards darstellen. Theoretisch könnte der Webserver also von Windows 2000/NT auf UNIX portiert werden und umgekehrt. Beim Verschieben der Daten wird vorausgesetzt, dass die Datenübertragung nicht immer fehlerfrei verläuft. Die Programme werden daher in einem »defensiven« Stil geschrieben.
7.1.3 Noch etwas zu ASP In Kapitel 5 wurden die zwei vordefinierten Objekte Application und Session erläutert. Es sind noch fünf weitere vordefinierte ASP-Objekte vorhanden. Mit diesen Objekten wird der Mechanismus zur Interaktion mit ASP bereitgestellt. In einem der vorgestellten Architekturdiagramme wurde beispielsweise gezeigt, dass ein Anforderungsstream und ein Antwortstream vorhanden sind. Dementsprechend stehen die zwei ASP-Objekte Request und Response zur Verfügung. Die Definitionen der verbleibenden fünf ASP-Objekte lauten folgendermaßen: 왘 ASPError: Ein neues Objekt, das mit IIS 5.0 (Internet Information Server) ein-
geführt wurde. Mit diesem Objekt können Fehler abgerufen werden, die innerhalb des ASP-Ausführungskontextes aufgetreten sind. 왘 ObjectContext: Dieses Objekt ermöglicht bei Ausführung der ASP-Seite die In-
teraktion mit dem Transaktionskontext. 왘 Request: Dieses Objekt repräsentiert den eingehenden Stream und enthält In-
formationen zur Anforderung. Zu diesen Informationen zählen beispielsweise Parameter, Cookies und clientseitige Zertifikate. 왘 Response: Dieses Objekt repräsentiert den ausgehenden Stream. Mit diesem
Objekt ist es möglich, HTTP-Header (Hypertext Transfer Protocol) zu definieren, Puffer für das verzögerte Senden zu erstellen sowie auf weitere HTTP-Elemente zuzugreifen. 왘 Server: Ein einfaches Dienstprogrammobjekt zur Instanziierung weiterer COM-
Objekte (Component Object Model). Diese COM-Objekte können zur Erweiterung der Funktionalität von ASP-Skripts eingesetzt werden.
180
Entwickeln einer Webanwendung
Verwenden von COM-Objekten Innerhalb einer HTML-(Hypertext Markup Language) oder ASP-Seite können COM-Komponenten verwendet werden. COM-Komponenten sind binäre Ausführungseinheiten, die Code enthalten und eine Schnittstelle zu ihrer Funktionalität offen legen. Die Schnittstelle wird innerhalb der Skriptumgebung verwendet. Ein COM-Objekt wird entweder mit Hilfe einer PROG-ID (Programm-ID) oder einer CLSID (Klassen-ID) instanziiert. Der Unterschied zwischen diesen beiden Methoden besteht darin, dass erstere ID von Menschen lesbar, die zweite ID maschinenlesbar ist. Nachfolgend ein Beispiel einer PROG-ID: COMSNAP.SnapinAboutImpl.1
Die PROG-ID setzt sich typischerweise aus drei Bestandteilen zusammen. Der erste Teil repräsentiert die COM-Komponente, in der das COM-Objekt gespeichert ist. Der zweite Teil stellt den COM-Objektverweis dar, beim dritten Teil handelt es sich um die Versionsnummer des COM-Objekts. Diese Unterteilung folgt einer Benennungskonvention und kann daher variieren. Die PROG-ID wird in eine Zahl aufgelöst, die als CLSID oder COM-Coklasse bezeichnet wird. Der Computer verwendet die CLSID zur Referenzierung eines COM-Objekts. Eine CLSID sieht folgendermaßen aus: {52938292-1D8B-11d3-955F-0080C700807A}
Diese Zahl muss nicht manuell generiert werden, sondern kann anhand der Microsoft Visual Studio-Tools erstellt werden.
7.2 Entwickeln der Webanwendung Kehren wir zum ursprünglichen Websiteprojekt für die Konferenzanmeldung zurück. Die Forderung lautete, eine ansprechende und gleichzeitig einfache Website zu erstellen. Aufgrund dieser Anforderung muss die Website in eine coole, schicke Website und eine funktionelle Website unterteilt werden, mit der die größtmögliche Anzahl Endbenutzer unterstützt wird. Im ursprünglichen Domänenmodelltext wurde angemerkt, dass der Benutzer eine Anmeldung auch über Fax vornehmen können soll, falls kein Internetzugang verfügbar ist. Dies ermöglichte das Aufstellen einiger Regeln und minimaler Browseranforderungen, da jeder Benutzer, der diese Anforderungen nicht erfüllt, die Anmeldung per Fax vornehmen kann. Eine dieser Regeln besagt, dass der Browser Cookies akzeptieren muss. Viele Computer verfügen jedoch nicht über diese Optionen. In diesem Fall besteht das Problem weiterhin darin, eine Website zu erstellen, die cool, schick und gleichzeitig funktionell ist. Hierzu müssen die verschiedenen Browser nach ihren
Entwickeln der Webanwendung
181
Fähigkeiten sortiert werden. Anhand dieser Fähigkeiten kann der Server entscheiden, welcher Inhalt gesendet wird.
7.2.1 Ermitteln der Browserfähigkeiten Fordert ein Browser eine Webseite an, identifiziert sich der Browser gegenüber dem Server. Dieses Token wird als Benutzer-Agent-ID bezeichnet und in den HTTP-Headern gespeichert. Schreiben Sie den folgenden Code, damit es auf dem Server abgerufen wird: Request.ServerVariables( "HTTP_USER_AGENT")
So wird der Browsertyp zurückgegeben, beispielsweise: Mozilla/4.0 (compatible; MSIE 4.01; Windows NT)
Die Textzeichenfolge beginnt mit der Definition der unterstützten Kompatibilitätsebene. In diesem Beispiel wird übermittelt, dass der Browser mit Mozilla 4.0 kompatibel ist (Mozilla ist der Spitzname des Netscape-Browsers). Da es sich bei dem tatsächlichen Browser um Microsoft Internet Explorer (MSIE) handelt, ist eine zusätzliche Identifizierung erforderlich. In diesem Fall wird MSIE 4.0 auf der Windows NT-Plattform ausgeführt. Unterstützt dieser Browser das Scripting? Werden Rahmen unterstützt? Dies kann anhand der obigen Beschreibung nicht ermittelt werden. Sie können diese Informationen jedoch im Internet abrufen und anschließend Skriptcode für MSIE 4.0 oder andere Browser erstellen. Was ist jedoch mit MSIE 4.0, MSIE 4.02, MSIE 3.x oder MSIE, wenn diese auf einem Windows CE-Gerät, unter Windows 95/98 oder auf einem Macintosh ausgeführt werden? Die Anzahl der möglichen Browser- und Plattformkombinationen für den Internet Explorer sind enorm, und das Schreiben einer if-Anweisung zur Handhabung sämtlicher Fälle würde sehr viel Zeit in Anspruch nehmen. Glücklicherweise kann dieses Problem einfacher gelöst werden. Die eigentliche Aufgabe bestand darin, die vom Browser unterstützten Funktionen zu ermitteln. Der Browsertyp könnte ein Schlüssel zu einer Datenbank mit Browserfähigkeiten sein. Das ASP-Framework enthält ein COM-Objekt, mit dem Browsertyp und zugehörige Fähigkeiten abgerufen werden können. Diese Komponente wird als Browserfähigkeitenkomponente bezeichnet. Mit ihr wird nicht offen gelegt, um welchen Browser es sich handelt, sondern welchen Funktionssatz der Browser unterstützt. Der Browser erstellt hierzu eine Reihe von Variablen, durch die die verschiedenen Browserfunktionen repräsentiert werden. Sehen Sie sich den folgenden ASP-Quellcode an:
182
Entwickeln einer Webanwendung
Browser
Version
Frames
Tables
BackgroundSounds
VBScript
JScript
Über diesen Code wird ein Objekt erstellt, mit dem versucht wird, eine Datei der Browserfähigkeiten zu laden. Im ersten Abschnitt des ASP-Code wird das Objekt mit Server.CreateObject und unter Verwendung der COM-PROG-ID MSWC.BrowserType instanziiert. Nach der Objektinstanziierung wird versucht, die Datei namens browscap.ini zu laden. Diese Datei befindet sich im selben Verzeichnis wie browscap.dll, der Browserfähigkeitenkomponente. Während der Initialisierung ruft das Objekt die Zeichenfolge für den Browsertyp ab und versucht dann, für diese Zeichenfolge einen Querverweis in der Datei browscap.ini zu finden. Die Datei browscap.ini ist eine .ini-Datei, bei der die Zeichenfolge für den Browsertyp mit einem Abschnitt in der Datei verknüpft ist. Ein INI-Abschnitt wird definiert, indem Text von eckigen Klammern umschlossen wird. Suchen wir nach dem zu Beginn des Kapitels erwähnten Benutzer-Agenten. [IE 4.0] browser=IE Version=4.0 majorver=4 minorver=0 frames=TRUE
In dieser Beispieldatei befinden sich zwei INI-Abschnitte, [IE 4.0] und [Mozilla/ 4.0 (compatible, MSIE 4.*9]. Bei keinem der beiden Abschnitte handelt es sich um eine genaue Übereinstimmung mit der ursprünglichen Zeichenfolge für den Browsertyp. Im zweiten Abschnitt folgt auf das 4. jedoch ein Sternchen (*). Dieses Sternchen steht für einen Platzhalter, d.h., alle 4.xx-Versionen des Browsers sind eingeschlossen, und hierzu zählt auch der zu Beginn des Abschnitts angegebene Browsertyp. Internet Explorer 4.0, 4.01 und 4.02 weisen im Grunde identische Funktionssätze auf. Der Unterschied zwischen den verschiedenen Versionen liegt in einigen Bug Fixes. Liegt eine Abschnittsübereinstimmung vor, sind verknüpfte INI-Werte vorhanden. Im Falle der zuvor erwähnten Übereinstimmung gibt es den Wert parent=IE 4.0. Dies ist ein Verweis auf einen weiteren Abschnitt, der alle Einstellungen für parent enthält. Und tatsächlich, es gibt einen Abschnitt mit der Überschrift [IE 4.0]. Unter dieser befinden sich verschiedene Werte. Sehen Sie sich die folgenden Werte an: Win16=False, cookies=TRUE. Diese Werte werden als COM-Eigenschaften für das erstellte Objekt der Browserfähigkeiten offen gelegt. Die Werte dieser Eigenschaften sind entweder unwahr oder zutreffend (false oder true). Daher handelt es sich nicht um einen Windows 16-Bit-Browser, der Browser akzeptiert jedoch Cookies. Hinzufügen benutzerdefinierter Eigenschaften Durch das Installieren eines ASP- oder HTTP-Servers wird ein grundlegender Funktionssatz für jeden Browser installiert. Diese Datei browsercap.ini kann er-
184
Entwickeln einer Webanwendung
weitert werden, d.h., die Browserfähigkeiten können auch erweitert werden. Diese Erweiterung wird durch das Definieren von Werten unter einer Abschnittsüberschrift erreicht. Im Beispiel der Anwendung für die Konferenzanmeldung benötigen wir ein Flag, mit dem angegeben wird, ob ein Browser einen bestimmten minimalen Funktionssatz unterstützt. Dieser Abschnittswert supportsMinimumFeatures wird der Datei browsercap.ini hinzugefügt, wie nachstehend gezeigt: [Mozilla/4.0 (compatible; MSIE 4.*)] supportsMinimumFeatures=true
Der neue Abschnittswert kann auf einen beliebigen Wert eingestellt werden. Der Wert muss nicht unbedingt true oder false lauten, es können auch Zeichenwerte oder numerische Werte verwendet werden. Zur Referenzierung der Eigenschaft supportsMinimumFeatures in der ASP-Seite wird folgender Code benötigt:
My property
Der Vorteil einer benutzerdefinierten Eigenschaft besteht darin, dass für den Abruf nur eine Codezeile erforderlich ist. Andere Lösungen erfordern mehrere Codezeilen, d.h., es können mehr Fehler entstehen und der Code ist komplexer.
7.2.2 Zurückgeben der richtigen Webseite Nach Ermittlung der Browserfähigkeiten kann es wünschenswert sein, den Endbenutzer an die geeignete HTML-Seite umzuleiten. Bei der Konferenzanmeldungsanwendung müssen die Benutzer eine Sicherheitsprüfung hinsichtlich Identität und Zugriffsebene durchlaufen. Versucht ein Benutzer, ohne Sicherheitsprüfung auf eine Seite zuzugreifen, muss der Benutzer an die Seite für die Sicherheitsprüfung umgeleitet werden. Für die Dokumentumleitung stehen verschiedene Methoden zur Verfügung. Automatische clientseitige Umleitung Die einfachste Methode zur Umleitung wird als clientseitige Umleitung bezeichnet. Der Name ist etwas irreführend, da die Umleitung tatsächlich durch den Server ausgelöst wird. Fordert der Client ein Dokument an, sendet der Server einen HTTP-Befehl zurück, mit dem angegeben wird, dass das Dokument seinen Standort geändert hat und dass der angegebene Inhalt geladen werden muss. Diese
Entwickeln der Webanwendung
185
Methode wurde bei der Webanwendung für die Konferenzanmeldung verwendet. Sehen Sie sich den folgenden Quellcode an:
In diesem Beispiel wird der Anmeldestatus in der Session-Variablen userId gespeichert. Lautet der Status –1, ist der Benutzer nicht angemeldet. In diesem Fall wird die Response.Redirect-Methode aufgerufen, um den Benutzer an die Seite »../Logon/Default.asp« umzuleiten, der HTML-Seite für die Sicherheitsprüfung. Automatische Umleitung innerhalb einer Seite Im zuvor genannten Beispiel funktioniert die Umleitung nur, wenn kein Text an den ASP-Stream gesendet wurde. Wie Sie wissen, kann der an den Browser gesendete Text nicht zurückgerufen werden; Text stellt hierbei alles dar, was sich nicht im Skriptcodeabschnitt befindet. Muss eine Umleitung in der Mitte oder am Ende einer Seite vorgenommen werden, muss der ASP-Stream gepuffert werden. Beim Puffern des ASPStreams wird dieser temporär lokal gespeichert und kann anschließend entweder gesendet oder gelöscht werden. Das vorangegangene Beispiel könnte für diese Methode folgendermaßen umgeschrieben werden:
Bevor Text in den Stream geschrieben wird, muss Response.Buffer auf true gesetzt werden. So wird der Puffer vorbereitet. Erfolgt diese Vorbereitung später, tritt ein Fehler auf, da kein Inhalt gepuffert werden kann, der bereits an den Client
186
Entwickeln einer Webanwendung
gesendet wurde. Nach der Pufferaktivierung kann Response.Redirect an beliebiger Stelle innerhalb der HTML-Seite aufgerufen werden. Vergessen Sie jedoch nicht, Response.Flush und Response.End hinzuzufügen – so wird verhindert, dass die vom Proxy gesendete Meldung angezeigt wird, dass das Objekt verschoben wurde. Gelegentlich kann es wünschenswert sein, eine gepufferte HTML-Seite neu zu erstellen, da der Inhalt geändert werden muss. Verwenden Sie hierzu die Methode Response.Clear. So wird der Puffer geleert und ein Neustart ermöglicht. Wenn eine große Menge Daten an den Client gesendet werden muss und hierzu eine erhebliche Verarbeitungszeit erforderlich ist, können Sie den Inhalt in einzelnen Blöcken senden. Über die Methode Response.Flush wird der ASP-Streaminhalt gesendet, der bis zu diesem Zeitpunkt erstellt wurde. Nach dem Senden des Inhalts kann Response.Redirect jedoch nicht mehr verwendet werden. Der letzte Schritt besteht darin, die Verarbeitung der ASP-Seite zu beenden und den Inhalt zu senden, der sich im Puffer befindet. Hierzu kann der Befehl Response.End eingesetzt werden. Seien Sie bei der Platzierung dieser Methode vorsichtig, da durch diesen Methodenaufruf die Verarbeitung der aktuellen Seite beendet wird. Automatische Aktualisierung Eine weitere Methode zur automatischen Umleitung einer Webseite stellt der HTTP-Befehl refresh dar: Document Title
Das -Tag weist das Attribut HTTP-EQUIV auf, mit dem angegeben wird, dass diese Seite nach dem Download aktualisiert wird. In diesem Fall wird die Zeit durch das CONTENT-Attribut repräsentiert und ist auf fünf Sekunden eingestellt. Das URL-Attribut (Uniform Resource Locator) definiert, welche Seite nach Ablauf der fünf Sekunden geladen wird. Probleme treten bei dieser Methode auf, wenn der Benutzer zum Zeitpunkt der automatischen Browserumleitung noch nicht sämtliche Informationen in ein Formular eingegeben hat. In diesem Fall werden sämtliche Informationen gelöscht,
Entwickeln der Webanwendung
187
die der Benutzer bis dato eingegeben hat. Bei einigen Browsern führt das erneute Laden einer Seite dazu, dass der Browser für kurze Zeit blockiert wird. Automatische serverseitige Umleitung Mit Einführung von Windows 2000 wurden IIS-integrierte Objekte auf die Unterstützung der serverseitigen Umleitung erweitert. Im Gegensatz zur clientseitigen Umleitung erfordert die serverseitige Umleitung keine neue clientseitige Verbindung. Die Server.Transfer-Methode ermöglicht Folgendes: 왘 Session- und Application-Informationen werden zwischen mehreren Weban-
wendungen übertragen. So kann ein Benutzer innerhalb verschiedener Webanwendungen verfolgt werden. 왘 Die Umleitung kann während der Skriptausführung erfolgen, ohne dass ein
Puffer erstellt werden muss. Das ursprüngliche Beispiel für die Konferenzanmeldung könnte unter Verwendung dieser Methode folgendermaßen abgeändert werden: Logon process
Einbetten einer Aktivität Das Problem bei der Umleitung besteht darin, dass es sich um eine Aktion mit dauerhaften Auswirkungen handelt. Ohne einigen Zusatzaufwand ist es nicht möglich, zur ursprünglichen Seite zurückzukehren. Durch Windows 2000 wird mit dem IIS-Objekt Server ein Verfahren bereitgestellt, durch das ein Aufruf einer separaten Webseite wie ein Funktionsaufruf ausgeführt werden kann. So kann eine HTML-Seite in Fragmenten erstellt werden, ähnlich der Verwendung von Rahmen in einer Einzelseite.
188
Entwickeln einer Webanwendung
Die hierzu verwendete Methode heißt Server.Execute. Beispiel: Example site
In diesem Beispiel wird die Methode Server.Execute zum Hinzufügen eines Banners zur aktuellen Webseite verwendet. Die Methode Server.Execute führt zur Seitenverarbeitung. Das Verwenden des serverseitigen include-Elements zur Erstellung einer Seite in Fragmenten beinhaltet keine Verarbeitung und kann schneller erfolgen. Include wird üblicherweise verwendet, wenn der einzuschließende Inhalt auf der Webseite als Verweis verwendet wird. Der Einschluss kann sowohl auf der Client- als auch auf der Serverseite erfolgen. Einsatzmöglichkeiten der einzelnen Umleitungsmethoden Die Frage ist, wann welche Umleitungsmethode eingesetzt werden sollte. Die Antwort richtet sich nach den Einschränkungen jeder Methode. Die Methode Response.Redirect ist sehr leistungsstark und leicht zu verwenden, aber viele Webbrowser und Proxys können die zugehörigen Meldungen nicht richtig interpretieren. Statt die clientseitige Umleitung automatisch auszuführen, wird eine Meldung angezeigt. Ein weiteres Problem bei dieser Methode besteht darin, dass die Browserschaltfläche Zurück nicht ordnungsgemäß funktioniert. Der Benutzer ist gezwungen, die Schaltfläche schnell zu klicken und darauf zu warten, dass die richtige Seite angezeigt wird. Die Methoden Server.Transfer und Server.Execute stellen die besten Methoden dar, wenn die Umleitung auf demselben Rechner erfolgt. Diese neuen Methoden können in der Website für die Konferenzanmeldung verwendet werden. Problematisch bei diesen Methoden ist, dass sie weder einen anderen physischen Rechner aufrufen noch an diesen übermitteln können.
7.2.3 Sprachenunterstützung Im Internet werden viele verschiedene Sprachen eingesetzt, und ebenso sind Websites mit mehreren Sprachen vorhanden. Je nach der im Browser angezeigten
Entwickeln der Webanwendung
189
Sprache möchten Sie vielleicht Textelemente hinzufügen. Mit wachsender Popularität des Webs steigt auch die Zahl der nicht englischsprachigen Webnutzer. Dies bedeutet, dass die Website lokalisiert werden muss, damit die Benutzer die Seiten in ihrer eigenen Sprache anzeigen können. Der traditionelle Ansatz mehrsprachiger Websites besteht darin, Nationalflaggen hinzuzufügen, mit denen auf Links zu den jeweiligen Sprachen verwiesen wird. Der Benutzer kann dann die gewünschte Version auswählen. Dies ist eine akzeptable Lösung, die jedoch noch erweitert werden kann. Bei einer optimalen Lösung wählt der Server – abhängig von den Informationen, die der Browser an den Server übermittelt – automatisch die richtige Sprache aus. Hierzu gehen Sie folgendermaßen vor: Request.ServerVariables("HTTP_ACCEPT_LANGUAGE");
Der Browser gibt eine Einstellung wie en-us für Englisch (USA) oder de für Deutsch (Deutschland) zurück. Sowohl der Internet Explorer von Microsoft als auch der Netscape Navigator unterstützen diese Funktion. Die Spracheinstellung richtet sich nicht nach dem installierten Browser, sondern nach der Einstellung für den Computer. Wenn demnach die Ländereinstellungen, die sich in der Systemsteuerung befinden, auf die Option Deutsch (Deutschland) eingestellt sind, sendet der Client immer die Option de an den Server, wenn eine HTTP-Anforderung durchgeführt wird. Der Server erkennt die Spracheinstellung und erstellt den geeigneten Inhalt bzw. leitet den Browser an die Seite um, die den Inhalt in der angegebenen Sprache enthält. Das Erstellen von Websites, die mehrere Sprachen, verschiedene Browser oder unterschiedliche Technologien unterstützen, ist nicht einfach. Der zusätzliche Aufwand ist jedoch gerechtfertigt, denn die Website erhält so ein professionelles Aussehen. Sie sollten bei der Entwicklung einer lokalisierten Site daran denken, dass Sie mit jemandem zusammenarbeiten, der die Zielsprache und -kultur kennt. Das Lokalisieren des Ausgangstextes umfasst nicht nur die sprachliche Übertragung in die Zielsprache, sondern auch das Übertragen von Bedeutungsinhalten, Farben oder Grafiken in eine andere Kultur.
7.2.4 Erstellen des Hauptteils Wie bereits in Kapitel 5 erwähnt, erfordert der Hauptteil der Webanwendung die meiste Entwicklungszeit. Sämtliche der bisher genannten Verfahren finden auch hier Anwendung. Für diese Aufgabe ist jedoch mehr Aufwand erforderlich, da eine Interaktion mit der Datenbank, den Geschäftsobjekten und den vom Client gesendeten Daten stattfinden muss.
190
Entwickeln einer Webanwendung
Bei der Entwicklung der serverseitigen Inhalte sieht sich der Programmierer einer neuen Form der Programmierung gegenüber, da der ASP-Code eine Mischung aus HTML und Skriptcode darstellt. Beim ersten Hinsehen scheint es sich um ein großes Durcheinander zu handeln. Haben Sie sich jedoch erst einmal an die Notation gewöhnt, werden Sie erkennen, dass der ASP-Code sich optimal für Webanwendungen eignet. Der Vorteil ist, dass Sie keine HTML-Tags in die Programmierumgebung einbetten müssen und damit das Lesen der Programmiersprache nicht erschwert wird. ASP ist eine Art Vorlagenansatz. Der Skriptcode soll anhand von JavaScript und VBScript erläutert werden. Sehen Sie sich folgendes Skript an: Example Loop Count
In diesem Codebeispiel ist eine Schleife (for...next) von 0 bis 10 enthalten. Innerhalb der Schleife kann Text auf zwei Arten an den Stream ausgegeben werden. Die erste Möglichkeit besteht darin, den Methodenaufruf Response.Write zu verwenden, mit dem Daten in kleinen Blöcken an den Stream gesendet werden. In diesem Beispiel wird der Text dynamisch erstellt. Die zweite Möglichkeit ist die, ohne explizite Methoden Text an den Stream zu senden. Zur Ausgabe einer spezifischen Variable wird die Notation verwendet. In dieser Situation kann innerhalb des Ausgabeblocks kein weiterer Verarbeitungscode eingefügt werden, dies würde zu einem Fehler führen. Beide Methoden führen zu einer Datenausgabe an den Stream. Welche Methode die bessere ist, lässt sich nicht ohne weiteres sagen, sondern richtet sich nach der jeweiligen Situation. Wenn Sie mit Hilfe des Response-Objekts Text an den Stream ausgeben, müssen Sie sicherstellen, dass keines der Elemente eine Formatierung benötigt. Es ist äußerst schwierig, auf diese Elemente eine Vorlage oder Formatierung anzuwenden. Ist dies erforderlich, muss die Codierung manuell er-
Entwickeln der Webanwendung
191
folgen. Dieses Szenario eignet sich besonders für die Entwicklung von Skriptobjekten. Wenn Sie den zweiten Ansatz verwenden, sind HTML- und Skriptcode fragmentierter, aber Sie können einen Editor einsetzen, um die einzelnen HTML-Tags zu bearbeiten. Eine herausragende Eigenschaft dieser Methode stellt das Erzeugen von Sonderzeichen dar; das Anzeigen von Anführungszeichen mit Hilfe von Response.Write ist beispielsweise sehr viel schwieriger als mit der Notation . Diese zweite Methode erleichtert die Feinabstimmung der HTML-Tags erheblich, ist jedoch auch besser für das direkte Bearbeiten der ASP-Seite geeignet. Bei der Kombination von HTML mit Skriptcode funktioniert folgende Formatierung meiner Meinung nach am besten: Square of number
Number
Square
Die Formatierung ist einfach. HTML-Tags und Code werden mit Hilfe von Tabulatoren eingerückt. Die Tabulatorensequenz im Codeabschnitt sowie in den HTMLTagabschnitten werden jedoch voneinander getrennt. Die Notationen für Variablenausgabe folgen der Tabulatorennotation des HTML-Tagabschnitts, so sieht die ASP-Seite wie eine kontinuierliche Seite aus.
192
Entwickeln einer Webanwendung
7.2.5 Die Skriptbibliothek von Visual InterDev In Kapitel 6 wurden die JavaScript-Objekte vorgestellt. Microsoft Visual InterDev verfügt über eine neue Bibliothek mit dem Namen Skriptobjektbibliothek. Diese arbeitet mit Entwurfszeitsteuerelementen zusammen, um die Entwicklung von Webanwendungen durch Verwenden objektorientierter Methoden zu vereinfachen. Auf der Clientseite befindet sich ein Ereignismodell. Die Skriptobjektbibliothek erstellt ebenfalls ein Ereignismodell auf Serverseite und verbirgt die Komplexität von Client und Server. So können auf einfache Weise Webanwendungen für Basisbrowser und für Browser erstellt werden, die das gesamte DOM-Modell (Document Object Model) unterstützen. Mit Hilfe der Skriptobjektbibliothek können Sie Methoden der Datenbindung für die Datennavigation nutzen. Die Skriptobjektbibliothek wird mit JavaScript geschrieben, der ASP-Skriptcode kann jedoch in VBScript vorliegen. Entwurfszeitsteuerelemente
Abbildung 7.1 DTC-Schaubild
Entwurfszeitsteuerelemente (Design-Time Controls, DTC) gibt es seit Visual InterDev 1.0. Ein DTC unterscheidet sich von einem regulären COM-Steuerelement
Entwickeln der Webanwendung
193
dadurch, dass es nur zur Entwurfszeit eingesetzt werden kann. Am einfachsten kann man sich diese Steuerelemente als wieder verwendbare Assistenten vorstellen, über die Code generiert wird. Das DTC weist eine Reihe von Parametern auf, die durch den Programmierer eingestellt werden, der den Code generiert. Der Code wird beim Speichern der HTML-Seite erzeugt. Das DTC ist das Element in der Mitte von Abbildung 7.1. Die Parameter lauten Connection (eine Datenverbindung), Database object (eine Tabelle, Sicht oder eine gespeicherte Prozedur) und Object name, der sich nach dem Datenbankobjekt richtet (SQL-Tabelle Sports). Beim Speichern der Webseite wird der folgende Code generiert: … function _Recordset1_dtor() { Recordset1._preserveState(); thisPage.setState('pb_Recordset1', Recordset1.getBookmark()); }
194
Entwickeln einer Webanwendung
Der DTC-Code wird innerhalb einer HTML-Kommentierung gespeichert,
Entwickeln der Webanwendung
195
function thisPage_onenter() { // Do something Response.Write("Starting the page"); } function thisPage_onexit() { Response.Write("Ending the page"); } Some page content
Abbildung 7.2 Einfache Skriptausgabe
Das Schaubild scheint die richtige Ausgabe anzuzeigen. Jeder Textbestandteil befindet sich dort, wo er sein sollte. Sehen Sie sich jedoch den folgenden HTMLQuellcode an, der durch die Funktion thisPage_onenter generiert wurde: Starting the page thisPage._location = "/SimpleWebSite/scriptinglib/simple.asp";
196
Entwickeln einer Webanwendung
Some page content Ending the page
Dieser HTML-Quellcode verletzt sämtliche HTML-Richtlinien. Normalerweise beginnt eine HTML-Seite mit dem -Tag, diese Seite beginnt jedoch mit dem -Tag. Warum? Weil hier das onenter-Ereignis aufgerufen wird, bevor Text in den Stream geschrieben wurde. Der Zweck dieses Ereignisses besteht darin, die Daten und Objekte auf der Seite zu initialisieren. Ein weiterer Fehler auf dieser Seite besteht darin, dass das -Tag dem -Tag vorangeht. Visual InterDev generiert den HTML-Code in dieser Form, wenn die Skriptobjektbibliothek auf der ASP-Seite aktiviert ist. Deaktivieren Sie (wie zuvor beschrieben) die abgeblendeten Bereiche und verschieben Sie das -Tag, um diesen Fehler zu beheben. Zur erneuten Aktivierung der abgeblendeten Bereiche muss das -Tag verschoben werden, durch das die abgeblendeten Bereiche deaktiviert werden. Der erste Schritt umfasst die Deklaration der verwendeten Programmiersprache. In diesem Fall wurde JavaScript verwendet. Die folgenden Zeilen sind wichtig, da mit ihnen die Skriptobjektbibliothek initialisiert wird. Fügen Sie diese Zeilen mit Hilfe der Visual InterDev-Umgebung ein, werden die Zeilen abgeblendet angezeigt und können nicht bearbeitet werden. Wenn Sie abgeblendete Zeilen bearbeiten möchten, müssen Sie lediglich in der zweiten Zeile der ASP-Seite Inhalt einfügen, wie nachfolgend gezeigt:
Speichern Sie die Seite, und laden Sie sie erneut. Es sind keine abgeblendeten Bereiche mehr vorhanden. Der Nachteil ist, dass Sie keine DTCs verwenden können, die die Skriptobjektbibliothek erfordern, da Visual InterDev davon ausgeht, dass
Entwickeln der Webanwendung
197
die Skriptobjektbibliothek nicht aktiviert ist. Obwohl dies nicht der Fall ist, muss der gesamte Code manuell eingegeben werden. Die Sensitive Content If you can see this then you have been logged on
Umsetzung
205
Auf diese Seite wurde das Framework der Skriptobjektbibliothek angewendet. Des Weiteren ist ein Kommentar vorhanden, der angibt, wo die Anmeldefunktionalität eingefügt werden sollte. Diese Funktionalität sollte implementiert werden, bevor vertraulicher Inhalt angezeigt werden kann. Als Verwender von C++ und Visual Basic war mein erster Gedanke, den folgenden Quellcode zu schreiben: Sensitive Content If you can see this then you have been logged on
Es ist ein Benutzerobjekt vorhanden, über das geprüft wird, ob der aktuelle Benutzer angemeldet ist (isLoggedOn). Wird der Wert false zurückgegeben, wird die aktuelle Seite an die Anmeldeseite umgeleitet. Andernfalls könnte der vertrauliche Inhalt angezeigt werden. Dieser Ansatz birgt viele Probleme: 왘 Warum ist user ein Objekt? In ASP verfügt jeder user über einen eigenen Ar-
beitsbereich. Es ist nicht erforderlich, ausdrücklich ein user-Objekt zu erstellen. Stattdessen ist es angemessener, ein security-Objekt zu erstellen. Die Sicherheit wird auf den Benutzer angewendet. 왘 Ist der Benutzer nicht angemeldet, wird die Seite umgeleitet. Auf diese Weise
wird der Anmeldevorgang gestartet, aber wie kehren Sie zur ursprünglich angeforderten Seite zurück? Diese Methode erfordert, dass der angemeldete Benutzer einen Extraklick ausführen muss. Die Webanwendung sollte »wissen«, welche Seite ursprünglich aufgerufen wurde. 왘 Der hier geschriebene Code ist nicht wieder verwendbar und erfordert einige
Tipparbeit. Es wäre einfacher, wenn die Lösung weniger Codezeilen umfassen würde. Damit würde auch die Wartung vereinfacht. Die Lösung besteht darin, über die Funktionsweise von HTML und Skriptobjekten nachzudenken. Beim Entwickeln einer Benutzerschnittstelle können JavaScriptObjekte erstellt werden, die sämtlichen Code für die Benutzerschnittstelle enthalten. Dies bringt uns jedoch zum ursprünglichen Problem nicht dynamischer Be-
206
Entwickeln einer Webanwendung
nutzerschnittstellen zurück. Eine Lösung sollte die Benutzerschnittstelle für das Formular von der Funktionalität trennen. Skizzieren der Lösung In der Lösung werden die Statusverwaltungsfunktionen der Skriptobjektbibliothek sowie die ASP-include-Anweisungen zur Durchführung einer Sicherheitsprüfung verwendet. Es ist nicht nötig, eine Umleitung zu einer anderen HTML-Seite durchzuführen, wodurch der zusätzliche Schritt der »Speicherung« der ursprünglichen Seite entfällt. Jede Seite mit vertraulichen Informationen umfasst zwei Elemente. Das erste Element ist die Sicherheit, das zweite ist der Inhalt selbst. In ASP wird zunächst die Sicherheit einer Seite verarbeitet. Besagen die Sicherheitsrichtlinien, dass eine Anmelderoutine ausgeführt werden muss, werden die entsprechenden HTMLElemente in die Seite eingefügt. Das zweite Element erkennt die vorgenommenen Sicherheitsmaßnahmen und reagiert entsprechend. Wurde eine Anmelderoutine hinzugefügt, wird kein Inhalt generiert. Dem Endbenutzer wird ein Formular angezeigt. Der Benutzer füllt das Formular aus und übermittelt es an den Server. Die URL ist immer noch die ursprünglich eingegebene, und der Prozess der Sicherheitsprüfung beginnt von vorn. Die Sicherheitsprüfung erkennt, dass die in das Anmeldeformular eingegebenen Informationen verarbeitet werden müssen. Verläuft die Verarbeitung der Formulardaten erfolgreich, werden über das Inhaltselement die HTML-Elemente in die Seite eingefügt. Scheitert die Verarbeitung, generiert das Sicherheitselement für einen weiteren Anmeldeversuch erneut die Anmelderoutine. Benutzerschnittstelle Wenden wir uns der Implementierung einer vertraulichen Seite zu. Es sollen globale, wieder verwendbare Informationen eingesetzt werden. Die globalen Informationen können auch zusätzliche Informationen enthalten, mit denen Workflowanwendungen umsetzbar sind oder ermittelt werden könnte, wann die Daten aktualisiert werden müssen. Nehmen Sie folgende Implementierung:
Umsetzung
207
Sensitive Content If you can see this then you have been logged on
Das Dokument ist eine einfache "Hello World"-HTML-Seite. Es enthält jedoch Informationen, die als vertraulich betrachtet werden und muss daher über Anmeldefähigkeiten verfügen. Die generateDocument-Variable ist eine globale Variable und kann durch ein beliebiges Teilsystem geändert werden, um anzuzeigen, dass der Inhalt nicht angezeigt werden sollte. Die Anmeldefunktionalität ist in der Datei logon.asp enthalten. Der Einschluss erfolgt über die function _page_ctor() { scrMgr.logon(); } function _inittxtUsername() { txtUsername.setStyle( TXT_TEXTBOX); txtUsername.setMaxLength(20); txtUsername.setColumnCount(20); } function _inittxtPassword() { txtPassword.setStyle( TXT_PASSWORD); txtPassword.setMaxLength(20);
208
Entwickeln einer Webanwendung
txtPassword.setColumnCount(20); } function _creators_ctor() { CreateTextbox( 'txtUsername', _inittxtUsername, null); CreateTextbox( 'txtPassword', _inittxtPassword, null); } function thisPage_onshow() { scrMgr.username = txtUsername.value; scrMgr.password = txtPassword.value; } function butSubmit_onclick( form) { form.submit(); }
Username
Password
Betrachten wir die einzelnen Codeabschnitte. Würde diese Seite in die vorangegangene ASP-Seite eingefügt, werden ggf. die HTML-Anmeldeelemente generiert. Die Seitenerstellungsfunktion page_ctor wird aufgerufen. Momentan ist diese
Umsetzung
209
Funktion leer, aber sie kann zur Definition eines beliebigen Status auf Seitenebene eingesetzt werden. Es ist eine weitere Erstellungsroutine auf dieser Seite vorhanden, _creators_ctor. Diese Erstellungsroutine dient der Erzeugung von zwei Textfeldern, txtUsername und txtPassword. Warum ein Skriptobjekt-Textfeld erstellen, wenn es einfacher sein könnte, das -Tag zu verwenden? Es ist zwar weniger aufwendig, das -Tag zu verwenden, das Formular muss jedoch verarbeitet werden. Durch das Verwenden eines Skriptobjekt-Textfeldes erfolgen Textgenerierung, Formularverarbeitung und Werteabruf in einem Arbeitsschritt. In einem traditionellen Formular ist dies nicht der Fall. Das Textfeld txtPassword ruft zur Definition eines Kennwortfeldes setStyle auf. Das Textfeld txtUsername ruft zur Definition eines normalen Textfeldes setStyle auf. Über beide Textfelder werden Länge und Spaltenanzahl festgelegt. Die Skriptobjektbibliothek definiert nicht nur serverseitige Schaltflächen und Textfelder. Es können auch clientseitige Schaltflächen und Textfelder verwendet werden. Ein clientseitiges Button-Skriptobjekt wurde nicht verwendet, da dies zu komplex wäre. Die Schaltflächen sind einfache -Tags mit verknüpften Ereignisbehandlungsroutinen, über die das Formular () übermittelt wird. In dieser Seite wird die Methode form.submit aufgerufen, obwohl die Seite kein -Tag enthält. Das Formular ist Teil der Skriptobjektbibliothek. Die ursprünglich in logon.asp eingeschlossene vertrauliche Seite weist eine Zeile mit folgendem -Tag auf.
Bei der Formularübermittlung wird auf das oben genannte Formular verwiesen. Es wird nicht empfohlen, eigene Formulare zu verwenden, da über diese die von der Skriptobjektbibliothek verwendeten verborgenen Textfelder nicht übertragen werden. Dies bedeutet, dass der in Textbox, txtUsername und txtPassword erworbene Status nicht weitergegeben wird. Der -Attributname referenziert jedoch thisForm. Was ist thisForm? Es stellt einen Teil der clientseitigen Skriptobjektbibliothek dar und definiert die aktuelle URL als thisForm. Daher wird beim Aufruf von form.submit die aktuelle URL aufgerufen. So wird das Problem gelöst, sich die ursprünglich angeforderte Seite zu »merken«. Nach Aufruf der Erstellungsroutine wird das Ereignis thisPage_onshow aufgerufen. Dieses Ereignis wird generiert, um zu kennzeichnen, dass die Seite angezeigt werden soll und dass Werte, die geändert werden müssen, jetzt geändert werden sollten. In diesem Fall werden die Werte scrMgr.username und scrMgr.password zugewiesen. Diese Zuweisung ist bei der Generierung einer Anmeldung nicht
210
Entwickeln einer Webanwendung
wichtig. Bedeutung erhält die Wertezuweisung erst bei der Verarbeitung des Anmeldeformulars. Abschließend wird die Methode Mgr.logon aufgerufen. Aber woher kommt scrMgr? Die Antwort finden Sie im nächsten Abschnitt. Für den Moment betrachten wird diese Methode lediglich als verfügbar. Das scrMgr-Objekt verwaltet die Sicherheitsattribute des aktuellen Endbenutzers. Anschließend wird scrMgr.isLoggedOn aufgerufen, um zu ermitteln, ob der Anmeldeversuch erfolgreich war. Ist dies der Fall, wird die Anmelderoutine generiert und das Flag generateDocument wird auf false gesetzt. So wird sichergestellt, dass der eingeschlossene Anmeldungsinhalt nicht generiert wird. Implementieren der Anwendungslogik Die Anwendungslogik ist in der Datei ../logon/securitymgr.asp enthalten. Der securityManager ist ein globales, lokales Objekt. Auch wenn sich dies widersprüchlich anhört, gibt es dieses Objekt. Beim Einschluss der Datei securitymgr.asp enthält diese eine Erstellungs- und eine Zerstörungsroutine. Diese Funktionen erstellen das scrMgr-Objekt. Dieses Objekt ist global im Sinne der Anwendung, aber gleichzeitig lokal, da nur der aktuelle Benutzer es sieht. Das Objekt wird immer dann instanziiert, wenn auf eine vertrauliche Seite zugegriffen wird. Die Objektdefinition lautet wie folgt: function SecurityMgr() { if (typeof(_SecurityMgr_Prototype_called) == 'undefined') _SecurityMgr_Prototype(); this.username = null; this.password = null; this.securityLevel = ""; this.operationStep = STP_NOTHING; } function _SecurityMgr_Prototype() { SecurityMgr.prototype.logon = _SM_logon; SecurityMgr.prototype.logoff = _SM_logoff; SecurityMgr.prototype.isLoggedOn = _SM_isLoggedOn; _SecurityMgr_Prototype_called = true; STP_NOTHING = 0; STP_LOGGING_ON = 1; STP_LOGGED = 2; }
Umsetzung
211
function _SM_logon() { // Do something to log on the user if( this.operationStep == STP_LOGGING_ON) { this.operationStep = STP_LOGGED; } } function _SM_logoff() { // Do something to log off the user } function _SM_isLoggedOn() { if( this.operationStep == STP_LOGGED) { return true; } else { return false; } } function _SM_ctor() { if (typeof( scrMgr) != 'object') scrMgr = new SecurityMgr; scrMgr.operationStep = thisPage.getState( "operationStep"); } function _SM_dtor() { thisPage.setState( "operationStep", scrMgr.operationStep); }
Die Funktion _SM_ctor weist ein neues SecurityMgr-Objekt zu. scrMgr.operationStep wird ein globaler Status zugewiesen, d.h., der momentane Schritt im Sicherheitsprozess wird angegeben. Zu Beginn weist operationStep den Wert STP_NOTHING auf. Beim Aufruf der ersten vertraulichen Seite und während der Verarbeitung des Anmeldeformulars erhält operationStep den Wert STP_LOGGING_ON. Bei erfolgreicher Anmeldung weist operationStep den Wert STP_LOGGED_ON auf. Beachten Sie, dass der Status von operationStep unter Verwendung von thisPage.setStatus und thisPage.getStatus verwaltet wird. Diese Funktionen gehen davon aus, dass sich nur ein -Tag auf der Seite befindet. Der Status könnte auch in der Session-Variablen verwaltet werden. Eine Session-Variable sendet keine Daten an den Client, sondern erfordert Cookies. Der Vorteil von
212
Entwickeln einer Webanwendung
thisPage liegt darin, dass zwar Daten an den Client gesendet werden, Cookies jedoch nicht erforderlich sind. Interessant ist auch die Verwendung der Funktion _SecurityMgr_Prototype, die in der Erstellungsroutine von SecurityManager aufgerufen wird. Beim Erstellen einer Methodentabelle in einem Objekt können Standardfunktionen vererbt werden. Hierzu verwenden Sie am besten das Schlüsselwort Prototype wie in der Funktion _SecurityMgr_Prototype.
7.4 Resümee Nach diesem Kapitel soll die HTML-Entwicklung nicht weiter behandelt werden, daher einige Tipps aus meiner praktischen Erfahrung als Websiteentwickler: 왘 Die Anzahl und Vielfältigkeit der verschiedenen Browser ist immens. Es gibt
einfache Browser und moderne, sehr leistungsstarke Browser. Das Problem besteht häufig darin, auch die Benutzer zu berücksichtigen, die über keinen leistungsstarken Browser verfügen. Die Website muss alle Browsertypen unterstützen. Lösung: Verwenden Sie die Visual InterDev-Skriptobjektbibliothek zum einfachen Schreiben von Komponenten, die auf einem beliebigen Browser eingesetzt werden können. 왘 Benutzer lesen häufig nicht, was sie eingeben. In früheren Websites wurden
20% der Fehler durch falsch eingegebene E-Mail-Adressen verursacht. Häufig mussten hierbei lediglich Bindestriche entfernt oder Punkte eingefügt werden. Lösung: In Kapitel 5 wurde die Erstellung einer Routine für die Formularprüfung erläutert. Verwenden Sie immer eine Routine für die Validierung. Fügen Sie ggf. ein Dialogfeld ein, über das der Benutzer aufgefordert wird, seine Eingaben erneut zu prüfen, da durch falsche Angaben unnötig CPU-Zeit verbraucht wird. 왘 Ist die Website zu langsam, klicken viele Benutzer ein zweites Mal auf eine
Schaltfläche, um ganz sicher zu gehen, dass der Befehl ausgeführt wird. Lösung: Es gibt leider keine Lösung für dieses Problem. 왘 Proxys sind ein Alptraum und können für viele Probleme verantwortlich sein.
Ein Proxy verbirgt darüber hinaus den internen Benutzer und macht die IP-Referenzierung sinnlos. Lösung: Verwenden Sie formularbasierte Variablen, die den Status zwischen Browser und Server weitergeben. 왘 Skript-, Java- und COM/ActiveX-Steuerelemente werden häufig herausgefil-
tert, dies kann sich nachteilig auf die Seitenfunktion auswirken. Lösung: Verwenden Sie möglichst viel serverseitigen ASP-Code.
Resümee
213
왘 Benutzer drucken gerne die Einzelheiten zu Themen aus, die sie interessant fin-
den. Lösung: Bieten Sie eine druckorientierte HTML-Seite an, bzw. nutzen Sie die browserspezifischen Funktionen. Das Entwickeln von Webanwendungen stellt keine einfache Aufgabe dar, besonders dann nicht, wenn Sie ein Anfänger sind. Denken Sie daran, dass die Webentwicklung eine völlig andere Welt darstellt. HTML weist eine Vielzahl von Vorteilen auf, die traditionelle Benutzerschnittstellen und Anwendungen nicht bieten. Wenn Sie also mit der Anwendungsentwicklung beginnen, sollten Sie klein anfangen und aus der Erfahrung lernen.
214
Entwickeln einer Webanwendung
8 Entwerfen von COM-Schnittstellen Das vorliegende Kapitel beschäftigt sich mit der Komponentenentwicklung. Wie bereits in vorangegangenen Kapiteln erwähnt, sollte die Entwicklung mit Hilfe von Schnittstellen und Implementierungen erfolgen. In diesem Kapitel werden die COM-Schnittstellen (Component Object Model) und die Auswirkungen ihrer Verwendung erläutert. Zunächst erhalten Sie eine Einführung in COM und das Schnittstellenkonzept, anschließend werden die technischen Details von COM und der COM-IDL (Interface Definition Language) besprochen. Des Weiteren werden Sie an die Probleme herangeführt, die bei der Verwendung von COM-Schnittstellen in Visual C++, Visual J++ Visual Basic sowie bei der Skripterstellung auftreten können. Hierbei wird vor allem die Interaktion zwischen den verschiedenen Umgebungen betrachtet.
8.1 Schnittstellen Das Entwerfen von Anwendungen mit COM-Schnittstellen unterscheidet sich ein wenig von der Erstellung regulärer Klassen in Visual Basic, Visual J++ oder Visual C++. Beim Entwerfen einer Klasse entwerfen Sie auch eine zugehörige Implementierung. Das klassische objektorientierte Beispiel ist die Implementierung von Formen. Bei der klassischen objektorientierten Programmierung (Object-Oriented Programming) wird die Basisklasse shape zur Funktionsimplementierung der Formen definiert, beispielsweise Kreise, Quadrate und Rechtecke. Die Basisklasse shape enthält eine Funktion, die in den Klassen square oder circle implementiert werden kann. Unter Verwendung dieser Methode muss ein Konsument von shape nichts über die jeweilige Funktionsweise von square oder circle wissen.
8.1.1 Entwickeln einer Architektur Die Verfahren der objektorientierten Programmierung gelten für Schnittstellen, jedoch in einem anderen Kontext. Bei der Entwicklung eines Systems ist es wichtig, zunächst die Architektur zu entwerfen. Die Trennung von Schnittstelle und Implementierung ermöglicht dem Designerteam die Betrachtung des Gesamtbildes ohne Berücksichtigung sämtlicher Implementierungsdetails. Sherlock Holmes sagte einmal: »Watson, ich lese keine Nachrichten, da mein Gedächtnis einem Speicher von begrenzter Größe gleicht. Je größer das Durcheinander, desto größer ist auch die Gefahr, dass etwas Wichtiges verloren geht.« Das Gleiche trifft auf
Schnittstellen
215
die Entwicklung großer Anwendungen zu. Es kann jeweils nur eine begrenzte Anzahl von Zielen umgesetzt werden. Alles Weitere geht leicht unter. Das Designerteam ist verantwortlich für das Entwerfen der Kernschnittstellen zur Repräsentation der verschiedenen Anwendungsfälle und Kollaborationsdiagramme in der Anwendung. Wenn Sie das Buch Kapitel für Kapitel gelesen haben, sollten Sie einen Domänenmodelltext verfasst sowie einen Prototyp entwickelt haben, mit dem die verschiedenen technischen Schwierigkeiten untersucht werden. Nun kommen wir zu der Phase, in der bestimmte Aspekte des Objekt- und Programmverhaltens extrahiert werden können. Gehen Sie pragmatisch vor. Lässt ein Anwendungsfall beispielsweise die Verwendung einer COM-Schnittstelle zu, dann wenden Sie diese an. Versuchen Sie nicht, Schnittstellen zu entwerfen, die für den Anwendungsfall sekundär sind. Obwohl Sie vielleicht denken, dass diese wichtig sind, geraten Sie bei der Implementierungsphase in Schwierigkeiten, da vielleicht noch eine weitere Schnittstelle implementiert werden muss, d.h. es ist noch mehr Arbeit erforderlich. Verfallen Sie jedoch genauso wenig in das entgegengesetzte Extrem, indem Sie fordern, dass jede Schnittstelle mit einem Anwendungsfall verknüpft sein muss. Das Ziel besteht darin, einen Kompromiss zu finden, bei dem die Haupt-COM-Schnittstellen in direktem Zusammenhang mit den verschiedenen Anwendungsfällen und Kollaborationsdiagrammen stehen, einschließlich einiger sekundärer COM-Schnittstellen. Der Entwicklungsprozess kann in das in Abbildung 8.1 gezeigte Diagramm übertragen werden.
Gruppentreffen der Designer
Schnittstellen X 1 Designer 6 Entwickler
Schnittstellen Y 1 Designer 6 Entwickler
Abbildung 8.1 Teamstruktur mit Designerteam im Mittelpunkt
In Abbildung 8.1 wird ein Entwurfsteam gezeigt, dass sich als Gruppe trifft. Dieses Team entwirft die Schnittstellen für die gesamte Anwendung. Die Schnittstellen
216
Entwerfen von COM-Schnittstellen
werden dokumentiert und den zwei Teams ausgehändigt. Die einzelnen Teams implementieren die Schnittstellen, und wenn das Designerteam gute Arbeit geleistet hat, passen die einzelnen Elemente zusammen. Komponenten und das Testen Bei der Entwicklung von Schnittstellen und Komponenten ist die Erstellung einer Testumgebung unerlässlich. Dieses Thema wird in Kapitel 19 ausführlich erläutert. Das ordnungsgemäßige Testen ist jedoch nicht der einzige Faktor bei der Anwendungsstabilität. In Abbildung 8.2 sehen Sie ein Entwurfsteam, das zwei Schnittstellen definiert hat, IAccount und IUser. Diese Schnittstellen werden durch zwei verschiedene Entwicklerteams implementiert. Da alle Teams Teil einer größeren Gruppe sind, kennen Sie einander und treffen sich gelegentlich. Während dieser Treffen spricht einer der Entwickler, der Schnittstelle IAccount implementiert hat, mit einem Mitglied des Teams, das Schnittstelle User implementiert. Sie stellen fest, dass beide Teams teilweise die gleiche Funktionalität implementiert haben. Zur Verringerung des Entwicklungsaufwands entscheiden beide Teams, eine Komponente MyCommonObject zu erstellen, mit denen die Probleme beider Teams behoben werden. Person A hat Schnittstellen entworfen IUser
IAccount
Person C implementiert IUser
Person B implementiert IAccount MyCommonObject
Abbildung 8.2 Entwicklungsdiagramm zur Implementierung der Schnittstellen
Obwohl dies ein guter Ansatz zu sein scheint, werden tatsächlich Codeabschnitte wieder verwendet, ohne dass die Auswirkungen dieses Vorgehens bekannt sind. Zunächst wissen nur die Teammmitglieder über die Codewiederverwendung Bescheid – Designer und Tester wurden nicht informiert. Beim Testen der Anwendung wird Schnittstelle IAccount unabhängig von Schnittstelle IUser getestet. Angenommen, Schnittstelle IAccount wird erfolgreich getestet, Schnittstelle IUser muss jedoch umgestaltet werden. Zur Behebung der Bugs wird die gemeinsame Komponente geändert. Die Tester führen lediglich einen erneuten Test für die Schnittstelle IUser durch, Schnittstelle IAccount muss jedoch auch neu getestet
Schnittstellen
217
werden. Demnach kann der zunächst als effektiv erscheinende Ansatz letztlich zu einem Misserfolg führen, und es muss ein Patch erstellt werden. Die Lösung umfasst keine Wiederverwendung von Komponenten; stattdessen wird Code ausgeschnitten und wieder eingefügt, um zwei Quellcodes zu erhalten. Steht genügend Zeit zur Verfügung, können die Designer die Komponente untersuchen und die Auswirkungen der Komponentenverwendung ermitteln. Anschließend kann die Komponente eventuell wieder verwendet werden. Das Schreiben guter Komponenten ist nicht einfach, und Stabilität ist ein Ergebnis umfassender Testläufe, bei denen die Grenzen der Komponente ermittelt werden.
8.2 Entwickeln einer COM-Komponente Die COM-Komponente setzt sich aus einer Reihe von Schnittstellen zusammen, die in Klassen und Bibliotheken gruppiert werden. Der Anspruch des Designerteams besteht darin, die Schnittstellen zu entwerfen. Hierzu gibt es keine einfache Lösung. Im Idealfall wird ein UML-Tool (Unified Modeling Language) verwendet, da jedoch die derzeitigen UML-Tools nur wenig Unterstützung für den COMSchnittstellenentwurf bieten, wird die Verwendung von UML an anderer Stelle erläutert. In einigen Projekten wurden Microsoft Word-Dokumente zur Dokumentation und Erstellung von CORBA- (Common Object Request Broker Architecture, eine vergleichbare Komponententechnologie) und COM-Schnittstellen eingesetzt.
8.2.1 Funktionsweise von COM Bei COM handelt es sich um eine binäre Technologie, die das Definieren von Komponenten ermöglicht. COM-Komponenten sind wieder verwendbare Black Boxes, die eine spezifische Funktionalität offen legen. COM ist eine leistungsstarke Komponentenarchitektur, da eine COM-Komponente sprachunabhängig arbeitet. Bei Verwendung von COM kann das Hauptaugenmerk auf die Schnittstellen und deren Verwendung gelegt werden. Die COM-Technologie ist in Ebenen strukturiert. Auf der untersten Ebene befindet sich eine COM-Schnittstelle. Die COM-Schnittstelle stellt eine VTable (Virtual Table) dar, mit der eine Reihe von Funktionszeigern gruppiert wird, die auf die Implementierung der Schnittstelle verweisen. Dieser Bestandteil von COM folgt dem Bridge-Muster (siehe Muster im Anhang), da Schnittstelle und Implementierung unabhängig voneinander definiert werden können. In der COM-Umgebung wird das COM-Objekt durch eine COM-Coklasse beschrieben, mit der die vom COM-Objekt unterstützten Schnittstellen angegeben
218
Entwerfen von COM-Schnittstellen
werden. Hierbei bezeichnet COM-Coklasse eine COM-Klasse, ein COM-Objekt ist eine Instanz einer COM-Klasse. Mehrere COM-Klassen können in einer COM-Bibliothek zusammengefasst werden. Dies bezeichne ich als COM-Komponente. Sie können einwenden, dass eine Komponente eine einzelne physische COM-Klasse darstellt, und genau hier gehen die theoretische und die praktische Betrachtungsweise auseinander. Microsoft tendiert dazu, eine einzelne DLL als COM-Komponente zu bezeichnen, die eine COM-Bibliothek darstellt. Jede COM-Schnittstelle, -Klasse und -Bibliothek muss eindeutig identifiziert werden. Dies wird mit einer GUID (Globally Unique Identifier, global eindeutige Kennung) erreicht. Es handelt sich hierbei um einen 128-Bit-Wert, der sich aus einer Gruppe von 8 Hexadezimalzeichen zusammensetzt, gefolgt von drei Gruppen á 4 Hexadezimalzeichen und 1 Gruppe aus 1–12 Hexadezimalzeichen. Eine Beispiel-GUID lautet folgendermaßen: 6B29FC40-CA47-1067-B31D-00DD010662DA
Diese Kennung wird verwendet, wenn der Konsument eine COM-Klasse, -Schnittstelle oder -Bibliothek aufruft.
8.2.2 IDL und seine Funktion Die Schnittstelle ist ein Kernelement der COM-Technologie. Schnittstellen werden über IDL (Interface Definition Language) definiert. In der Vergangenheit verwendete Microsoft zwei Methoden zur Schnittstellendefinition, ODL (Object Definition Language) und IDL (Interface Definition Language). Die erste diente der OLE-Automatisierung, zweitere für RPCs (Remote Procedure Calls, Remoteprozeduraufrufe). Eine ausführlichere Beschreibung der OLE-Automatisierung erhalten Sie zu einem späteren Zeitpunkt. Mit der Zeit wurde ODL überflüssig. Es wurde entschieden, IDL sowohl für RPC- als auch COM-Beschreibungen zu verwenden. ODL wird weiterhin unterstützt, aber IDL stellt die empfohlene Methode zur Beschreibung von Schnittstellen dar. IDL beinhaltet ein grundlegendes Konzept. Jedes bezeichnete Kennwort kann über Attribute verfügen und andere Schlüsselwörter enthalten. Im Folgenden ein Beispiel für die Notation: [attributes] keyword label { keyword member descriptions };
Entwickeln einer COM-Komponente
219
oder typedef [ type_attributes] type_keyword label { keyword member descriptions };
Das Schlüsselwort besteht aus einem einzelnen Befehl, beispielsweise interfaces, coclass oder library. In den eckigen Klammern befinden sich die mit dem Schlüsselwort verknüpften Attribute. Beispiele sind object, uuid und helpstring. Die Schlüsselwörter enthalten innerhalb der geschweiften Klammern untergeordnete Elemente. Ein einfaches Beispiel Bei der Entwicklung größerer Anwendungen mit vielen COM-Klassen kann man leicht durcheinander geraten. Diese Verwirrung ergibt sich nicht durch die Komplexität der COM-Klassen, sondern durch zu viele Informationen. Sie kombinieren vielleicht COM-Implementierungscode mit COM-Schnittstellenbeschreibungen; wir werden jedoch die COM-Schnittstellen von den Implementierungen trennen. Hierzu erstellen Sie eine IDL-Datei. Es gibt verschiedene Möglichkeiten zur Verwendung einer IDL-Datei, die gewählte Methode richtet sich nach der verwendeten Programmierumgebung. Visual C++ verfügt über die Fähigkeit, IDL-Dateien systemeigen zu verwenden und in ein Projekt zu integrieren, Visual Basic und Visual J++ dagegen erfordern eine Typbibliothek für die Integration. Zur Erstellung einer Typbibliothek verwenden Sie das MIDL-Programm (Microsoft Interface Definition Language) zur Kompilierung der COM-Bibliothek in eine COM-Typbibliothek. Eine COM-Typbibliothek ist eine kompilierte, binäre Darstellung der IDL-Datei. Sehen Sie sich die folgende IDL-Datei an: import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(8895EECD-0915-11D2-9C50-00A0247D759A), dual, helpstring("ISimpleInterface Interface"), pointer_default(unique) ]
Bei der Visual Studio Enterprise Edition muss die Datei nicht unter Verwendung von MIDL manuell in eine COM-Typbibliothek kompiliert werden. Die Visual Studio C++-Shell ermöglicht Ihnen das Erstellen eines Dienstprogrammprojekts und das Hinzufügen der IDL-Datei zu diesem Projekt. Bei der Kompilierung des Dienstprogrammprojekts wird zur Kompilierung der IDL-Datei MIDL automatisch aufgerufen. Für dieses Beispiel wurde nach der Erstellung des Dienstprogrammprojekts eine neue Datei mit dem Namen InterfaceComponent.idl erstellt. Fügen Sie den vorstehenden Quellcode hinzu, und führen Sie den Menübefehl Erstellen aus. Nach der Erstellung verfügen Sie über die Datei InterfaceComponent.tlb, bei der es sich um die Typbibliothek handelt. IDL mag zunächst umständlich und schwierig erscheinen, dieser Eindruck täuscht jedoch. Sie müssen sich lediglich an die zuvor dargestellte Strukturierung erinnern. Eine COM-Komponente ist ein Paket, das über das Schlüsselwort library definiert wird und das Label SERVERPACKAGELib aufweist. In dem Paket können
Entwickeln einer COM-Komponente
221
mehrere COM-Klassen enthalten sein, die mit Hilfe der Schlüsselwörter coclass defininiert werden und das Label SimpleComponent aufweisen. Anhand einer COM-Klasse können Sie unter Verwendung des Schlüsselwortes interface mehrere Schnittstellen definieren, diese erhalten das Label ISimpleInterface. Alle genannten Schlüsselwörter verfügen durch Verwenden des uuid-Attributs über eine verknüpfte GUID. Instanziieren einer COM-Schnittstelle Bei Kompilierung der vorgenannten IDL erhalten Sie als Ergebnis eine Typbibliothek. Im vorangegangenen Beispiel nimmt der Client zur Verwendung der ISimpleInterface-Implementierung keine Instanziierung der Schnittstelle selbst vor. Stattdessen instanziiert der Konsument eine COM-Klasse (coclass), bei der die Schnittstelle implementiert wurde. Nach Instanziierung der coclass verwendet COM eine QueryInterface zum Auffinden der ISimpleInterface. Schlägt QueryInterface fehl, gibt COM einen Fehler aus, nach dem die Schnittstelle nicht existiert. Ist ISimpleInterface vorhanden, wird sie an den Konsumenten zurückgegeben, der anschließend verschiedene Methoden aufrufen kann. Warum so kompliziert? Es mag unsinnig erscheinen, IDL zu verwenden. Warum nicht Daten in einer Struktur speichern und anschließend Daten zwischen Betriebssystem und Programmiersprache austauschen? Die Antwort ist, dass unabhängig davon, wie ein Betriebssystem oder eine Programmiersprache geschrieben wurden, eine gemeinsame Sprache erforderlich ist. Jede Sprache, jedes Betriebssystem und jede Hardwarekomponente unterscheidet sich in der Form der Kommunikation. Auf einer 16-Bit-Plattform weist ein integer-Wert beispielsweise 16 Bit auf. Auf einer 32Bit-Plattform umfasst ein integer-Wert 32 Bit. Wenn eine Struktur kompiliert und als Dateneinheit zwischen den zwei Plattformen ausgetauscht wird, werden die Werte möglicherweise nicht gleichartig gelesen. COM verwendet IDL, um eine neutrale Form bereitzustellen, mit der Größe und Ausrichtung der Daten angegeben werden. Das so genannte Marshaling und Unmarsheling sind Verfahren, bei denen die COM-Schicht die Daten von einem Betriebssystem oder einer Programmiersprache in COM und anschließend in das andere Betriebssystem bzw. die andere Programmiersprache zurück konvertiert.
8.2.3 Ein COM-Paket Bei der Entwicklung einer COM-Anwendung ist kein umfassendes Verständnis von IDL erforderlich, aber Sie müssen das Verhalten von IDL kennen. Ein Verständnis von IDL ermöglicht eine Problemdiagnose im Hinblick auf die Komponentenverwaltung.
222
Entwerfen von COM-Schnittstellen
Die COM-Komponente Die COM-Komponente wird durch das Schlüsselwort library definiert, wie im Beispielabschnitt zu sehen ist. Die Komponente enthält alle Klassen, die durch die COM-Komponente offen gelegt werden. Diese Attribute werden im Folgenden aufgeführt: 왘 uuid: Die ID zur Definition der Bibliothek. Dieser Parameter ist erforderlich. 왘 version: Gibt die Versionsnummer der Bibliothek an. 왘 helpstring: Ein Text zur Beschreibung der Bibliothek. Dieser ist hilfreich für Ob-
jektbrowser. 왘 lcid: Mit dieser ID wird angegeben, welche Sprache auf diese Bibliothek ange-
wendet wird. 왘 hidden: Verbirgt das Objekt vor einem Objektbrowser.
Die COM-Klasse Das Schlüsselwort coclass (Coklasse) wird zur Definition einer Implementierung in einer COM-Komponente eingesetzt. Beim Definieren einer IDL-Datei zur Kompilierung in eine Typbibliothek muss eine coclass definiert werden. Geschieht dies nicht, ist während der Kompilierungsphase in MIDL standardmäßig die Schnittstelle nicht enthalten. Die bei der Schnittstellendefinition verwendete coclass muss keine besondere Coklasse sein, da sie nicht zur Referenzierung einer Implementierung eingesetzt wird. Die typische Syntax für eine Coklasse lautet folgendermaßen: [attributes] coclas lass classname { [interface attributes] [i inte nterface ace | dispi spinterfac face] interfacename; };
In der Implementierungsphase ist die coclass dagegen sehr wohl wichtig. Die coclass weist folgende Attribute auf: 왘 uuid: Gibt die CLSID (Class ID) an, die zur Identifizierung dieses Objekts ein-
gesetzt wird. 왘 version: Gibt die Versionsnummer der Bibliothek an. 왘 helpstring: Ein Text zur Beschreibung der Klasse. Dieser ist hilfreich für Objekt-
browser.
Entwickeln einer COM-Komponente
223
왘 licensed: Teilt der Klasse mit, dass das Objekt lizenziert ist und geprüft werden
sollte. In diesem Fall sollte die Schnittstelle IClassFactory2 als Class Factory verwendet werden. 왘 hidden: Verbirgt das Objekt vor einem COM-Objektbrowser.
Innerhalb der coclass-Definition können die verschiedenen Schnittstellen diese Attribute aufweisen: 왘 source: Gibt an, dass die Schnittstelle eine Ereignisquelle darstellt und zusam-
men mit dem Container IConnectionPoint verwendet wird. 왘 default: Wird von Makroprogrammierern (VBScript) zur Definition der stan-
dardmäßig aufzurufenden Schnittstelle verwendet, wenn keine Schnittstelle angegeben ist. 왘 restricted: Verhindert, dass die Schnittstelle von Makroprogrammierern ver-
wendet wird.
8.2.4 Die COM-Schnittstelle Die meiste Zeit muss für das Entwickeln guter Schnittstellen aufgebracht werden. Die Schnittstelle wird in COM folgendermaßen definiert: [attributes] interf erface interfacename [:baseinterface] { functionlist };
Zu den gültigen Attributen gehören: 왘 dual: Gibt an, dass die definierte Schnittstelle sowohl benutzerdefinierte als
auch IDispatch-Schnittstellen unterstützt. 왘 object: Ein besonderes Tag, das den MIDL-Compiler anweist, COM-kompatib-
len Code zu generieren. Wird dieses Tag angegeben, muss es über eine verknüpfte uuid verfügen. Ohne dieses Tag wird die Schnittstelle als DCE RPCAufruf (Distributed Computing Environment) kompiliert. 왘 uuid: Gibt die CLSID an, mit der dieses Objekt identifiziert wird. 왘 helpstring: Ein Text zur Beschreibung der Schnittstelle. Dieser ist hilfreich für
Objektbrowser. 왘 pointer_default: Wird verwendet, wenn innerhalb einer Schnittstelle Zeiger als
Parameter verwendet werden. Dies schließt keine Zeiger der obersten Ebene ein, nur Elemente wie Doppelzeiger. 왘 oleautomation: Gibt an, dass die Schnittstelle nur Parameter unterstützt, die
als Standardautomatisierungstypen betrachtet werden.
224
Entwerfen von COM-Schnittstellen
In der Methodenparameterliste befinden sich weitere Attribute, die angeben, wie der Parameterspeicher zugewiesen wird und wie Informationen gesendet werden. Die Attribute werden in der Liste durch Kommata voneinander getrennt. Der »QueryInterface«-Prozess COM unterscheidet sich von allen anderen Komponententechnologien dadurch, dass es mit unbekannten Elementen umgehen kann. Nehmen Sie an, ein Konsument instanziiert eine COM-Klasse. Wie erhält die COM-Schicht die Information, dass die COM-Klasse die COM-Schnittstelle implementiert hat? Die COM-Schicht fragt die COM-Klasse mit Hilfe des QueryInterface-Prozesses ab, ob die Schnittstelle implementiert wurde. Sehen wir uns an, welche Schnittstellen und Methoden implementiert werden müssen. Jede COM-Klasse muss IUnknown implementieren, und jede COM-Schnittstelle muss von IUnknown erben. Die Schnittstellendefinition von IUnknown lautet folgendermaßen: [ local, object, uuid(00000000-0000-0000-C000-000000000046), pointer_default(unique) ] interface IUnknown { HRESULT QueryInterface( [in] REFIID riid, [out, iid_is(riid)] void **ppvObject); ULONG AddRef(); ULONG Release(); }
Es sind drei Methoden in der IUnknown-Definition vorhanden, QueryInterface, AddRef und Release. COM ruft die erste Methode, QueryInterface, bei Instanziierung der COM-Klasse auf. Der erste Parameter, riid, ist die GUI der angeforderten Schnittstelle. In der Implementierung wird dieser Parameter mit der Liste der implementierten Schnittstellen verglichen. Liegt eine Übereinstimmung vor, gibt die Implementierung über den Zeiger ppvObject einen vtable-Zeiger zurück, der die Schnittstelle repräsentiert. Zu diesem Zeitpunkt vertraut COM darauf, dass die vtable-Signatur der Schnittstellenimplementierung mit der vtable-Signatur der IDL-Schnittstelle übereinstimmt. Bei Verwendung der Sprachen Visual Basic, Visual C++ und Visual J++ stellt dies kein Problem dar, da eine Nichtübereinstimmung der vtable-Signaturen
Entwickeln einer COM-Komponente
225
zu einem Kompilierungsfehler führen würde. Bei anderen Sprachen ist dies in den meisten Fällen auch so, aber Sie sollten die COM-Implementierungsdetails der Sprache prüfen. IDL unterstützt das Konzept der so genannten Schnittstellenvererbung, und eine Schnittstelle, die von IUnkown erbt, weist folgende IDL auf: [ object, uuid(E05034D2-8EB8-11d2-86CB-0000B45FCBCB), helpstring("ISimpleInterface2 Interface"), pointer_default(unique) ] interface ISimpleInterface2: IUnknown { [helpstring("method1")] HRESULT method1(); };
Die vtable-Signatur für ISimpleInterface2 verfügt über vier Funktionszeiger in der folgenden Reihenfolge:QueryInterface, AddRef, Release und method1. Die ersten drei Funktionszeiger sind das Ergebnis der Schnittstellenvererbung. Verweiszählung für Implementierungen Die zwei weiteren Methoden der IUnknown-Schnittstelle sind AddRef und Release. Diese zwei Methoden sorgen für die Verwaltung der Verweiszählung. In Abbildung 8.3 instanziiert Konsument A die COM-Klasse SimpleComponent. Anschließend instanziiert Konsument B die COM-Klasse SimpleComponent. Die Instanz von SimpleComponent ist jedoch ein so genanntes Singleton und gibt einen Verweis auf sich selbst zurück. Ein Singleton ist ein besonderes Objekt, bei dem nur eine Instanz der Klasse im Speicher vorliegen kann. Konsument A beendet den Vorgang und löscht die Instanz SimpleComponent. Konsument B erfordert jedoch weiterhin die Ausführung der SimpleComponent-Instanz. Löscht Konsument A tatsächlich SimpleComponent? Die Antwort auf diese Frage liegt im Verweiszähler. Immer dann, wenn ein Konsument eine COM-Klasse instanziiert oder referenziert, wird der Verweiszähler mit Hilfe von AddRef erhöht. Benötigt der Konsument den Verweis nicht mehr, wird der Verweiszähler mit Hilfe von Release verringert. Nachdem die Verweiszählung den Wert Null erreicht hat, löscht die COM-Klasseninstanz sich selbst.
226
Entwerfen von COM-Schnittstellen
Konsument A
Konsument B
Abbildung 8.3 Zwei Konsumenten, die ein einzelnes COM-Objekt referenzieren
Dieses Vorgehen scheint geeignet, aber könnte es nicht zu Fehlzählungen kommen? Fehler treten in einer 32-Bit- oder 64-Bit-Multitaskingumgebung wie Windows 2000 nicht auf. Das Betriebssystem fängt Prozesse auf, die nicht nachvollziehbar gelöscht werden und weitere Probleme verursachen. Wurde das Programm für die Verweiszählung jedoch nicht geeignet geschrieben, kann eine falsche Verweiszählung erfolgen. Bei Sprachen wie Visual Basic oder Visual J++ wird die Zählung durch die Sprache verwaltet. Die Verweiszählung in Visual C++ wird durch die Helper-Klassen verwaltet. Benutzerdefinierte Schnittstellen und frühe Bindung Erbt eine Schnittstelle direkt von IUnknown, wird diese als benutzerdefinierte Schnittstelle bezeichnet, und benutzerdefinierte Schnittstellen nehmen eine frühe Bindung vor. Frühe Bindung bedeutet, dass die vtable-Signatur der Schnittstelle bei Konsumentenkompilierung bekannt ist. Bei der frühen Bindung besteht ein »Vertrag« zwischen Konsument und Implementierung, der durch die Typbibliothek definiert wird. Die Laufzeitgeschwindigkeit liegt bei Verwendung der frühen Bindung höher, da der Konsument weiß, wo die Implementierungsfunktionalität zu finden ist. Das Verwenden der frühen Bindung führt jedoch zu Problemen, wenn die Implementierung den Vertrag ändert, ohne die Konsumentenanwendung hierüber zu informieren – derartige Änderungen der vtable-Signatur können zu Anwendungsfehlern führen. Späte Bindung Gelegentlich erstellen Sie COM-Klassen, die ihre zugehörigen Methoden dynamisch festlegen. Nehmen Sie die Erstellung einer COM-Schnittstelle zu einer Datenbank. Sie möchten die Fähigkeit besitzen, dynamisch Methoden zu generieren, mit denen Datenbankfunktionen repräsentiert werden. Sie können keine Typbibliothek erstellen, da diese überholt wäre, sobald sie erstellt wurde. In dieser Situation oder in anderen Fällen, in denen Sie keine Typbibliothek offen legen möchten, gilt die späte Bindung.
Entwickeln einer COM-Komponente
227
Die späte Bindung wird auch als OLE-Automatisierung bezeichnet. Die späte Bindung ist interessant, da der Konsument mit Hilfe einer als Vermittler fungierenden allgemeinen Funktion die Methode oder Methodeneigenschaft aufrufen kann. Der Vermittler akzeptiert Struktur und Parameter und versucht anschließend zu ermitteln, welche Methode in der Anwendung aufgerufen wird. Der Vermittler ruft dann die Methode auf und gibt die Daten in Form einer weiteren Struktur mit Informationen zurück. Dieser Vermittler wird als IDispatch-Schnittstelle bezeichnet, einem Bestandteil der Implementierung. ASP verwendet diese Bindungsmethode beim Aufruf von COM-Klassen. Duale Schnittstellen Bei der Objekterstellung muss entweder die frühe oder die späte Bindung unterstützt werden. Die frühe Bindung ist schneller, jedoch weniger flexibel. Liegt eine andere Version des Servers vor, und wurden bestimmte Methoden geändert oder entfernt, stürzt die Anwendung ab, da keine ordnungsgemäße VTabelle vorhanden ist. Soll das Objekt flexibel bleiben und einen Softcrash oder einen wiederherstellbaren Absturz unterstützen, sollten Sie die späte Bindung einsetzen. Das Verwenden der späten Bindung geht auf Kosten der Geschwindigkeit. Bei der späten Bindung fragt der Konsument einer COM-Klasse, ob eine Methode mit spezifischer Signatur vorliegt. Die COM-Klasse durchsucht die Funktionstabelle und gibt eine ID zurück. Der Konsument erstellt anschließend eine Struktur mit allen Parametern und gibt diese an die COM-Klasse weiter, die die Struktur wiederum in einen Funktionsaufruf übersetzt. Da die Methoden allgemein sind und Parameter und Methoden in Arrays gespeichert werden, muss die Implementierung die Strukturen entschlüsseln. Aufgrund des Frage- und Antwortprozesses und der vorgenommenen Interpretation verlangsamt sich der Vorgang. Beide Methoden weisen Vor- und Nachteile auf. Wenn Sie möchten, dass das Objekte beide Methoden unterstützt, können Sie sowohl die frühe als auch die späte Bindung implementieren. Diese Implementierung wird als duale Schnittstelle bezeichnet. Die Implementierung ist etwas umfangreicher, so wird jedoch sichergestellt, dass alle Clients das Objekt verwenden können. Die duale Methode wird bei den meisten Sprachen innerhalb der Programmierumgebung verborgen. Welche Bindungsmethode ist besser? Viele Programmierer glauben, dass benutzerdefinierte Schnittstellen besser sind, da durch sie schnellere Anwendungen und eine bessere Versionsnummerierung bereitgestellt werden. Im Allgemeinen ist die frühe Bindung vorzuziehen, aber es gibt besondere Situationen, in denen die späte Bindung eingesetzt werden sollte.
228
Entwerfen von COM-Schnittstellen
Im vorangegangenen Kapitel beispielsweise verwendete die Visual InterDevSkriptobjektbibliothek die Datenumgebungs-COM-Komponente. Dieses Objekt übersetzt gespeicherte Prozeduraufrufe direkt in COM-Methodenaufrufe. Die Datenumgebung vollführt diesen Trick mit Hilfe der späten Bindung und durch das dynamische Offenlegen von Funktionen, die gespeicherte Prozeduraufrufe repräsentieren. Diese Technik erscheint auf den ersten Blick langsamer, aber sehen wir uns dies genauer an. Wird die Datenumgebung mit Hilfe von benutzerdefinierten Schnittstellen implementiert, müssen Methoden für das Einstellen der verschiedenen Parameter sowie zum Aufrufen der gespeicherten Prozeduren geschrieben werden. Dies bedeutet, dass die Implementierung die Parameter und gespeicherten Prozeduraufrufe in spezifische gespeicherte Prozeduren übersetzen muss. Mit anderen Worten, die Implementierung muss zunächst einiges an Verarbeitung leisten, bevor die eigentliche gespeicherte Prozedur aufgerufen werden kann. Das Data Environment-Objekt, auf der anderen Seite, ist ein Beispiel für ein Objekt zur Technologieüberbrückung. Es wird lediglich ein Methodenaufruf einer Technologie in eine andere (COM in SQL [Structured Query Language]) übersetzt. Das Verwenden der Datenumgebung ermöglicht eine einfachere Programmiersyntax und führt zu fast keinen Leistungseinbußen. Die Faustregel bei Auswahl der Bindungsmethode lautet, immer eine duale Schnittstelle zu verwenden, wenn möglich jedoch die späte Bindung einzusetzen.
8.3 Einige Entwurfsmethoden für die COM-Schnittstelle Nachdem Sie nun über ein Grundverständnis zu COM und Schnittstellen verfügen, müssen einige Entwurfsmethoden für die COM-Schnittstellen erläutert werden.
8.3.1 Schnittstellen sind unveränderlich Wenn eine Schnittstelle veröffentlicht und der Öffentlichkeit zugänglich gemacht wurde, wird sie als unveränderlich betrachtet und kann nicht weiter bearbeitet werden. Das Veröffentlichen einer Schnittstelle bedeutet nicht zwangsläufig, dass diese dem allgemeinen Konsumenten zugänglich gemacht wird, sondern dem Entwicklungsteam. Unveränderlichkeit einer Schnittstelle Sie sollten eine Schnittstelle erst nach reiflicher Überlegung als unveränderlich festlegen. Diese Überlegung ähnelt der Entscheidung, sich einen Welpen anzuschaffen. Nachdem der Welpe ins Haus gekommen ist, braucht er Aufmerksamkeit und kann nicht einfach beiseite gelegt werden. Ebenso ist das Festlegen der
Einige Entwurfsmethoden für die COM-Schnittstelle
229
Unveränderlichkeit einer Schnittstelle eine Entscheidung, die nur einmal gefällt wird. Wenn Sie später Änderungen vornehmen möchten, ist dies nicht mehr möglich. Die Philosophie, die hinter der Anwendungsentwicklung in diesem Buch steckt, ist das Verwenden iterativer Entwicklungsmethoden. Eine unveränderliche Schnittstelle kann jedoch nicht schrittweise weiterentwickelt werden. Im vorgestellten Entwicklungsprozess möchten wir mehrere COM-Schnittstellen einsetzen, um eine feine Granularität zu erzielen. Manchmal kann es aber auch zuviel des Guten sein. Eine feine Granularität kann die Dinge unnötig verkomplizieren. Mit dem Façade-Muster können die Dinge vereinfacht werden. Hierzu können Sie einen iterativen Schichtenansatz oder einen fließenden Ansatz verwenden. Wenden wir nun an, was in den vorherigen Abschnitten zum Entwurfsteam gesagt wurde, das sich die Arbeit teilte. Und nehmen Sie an, die Anwendung wird anhand des Iterationskonzepts entwickelt. Wieder entwickelt das Entwurfsteam eine Reihe von COM-Schnittstellen für die Anwendung. Anschließend werden jedoch nur einige der Schnittstellen implementiert. Die Faktoren bei Auswahl der zu implementierenden Schnittstellen richten sich nach dem Team und den gesetzten Prioritäten. Nehmen Sie das in Abbildung 8.4 gezeigte Beispiel der Implementierung einer gesamten Anwendung mit einigen COM-Schnittstellen.
Abbildung 8.4 Gesamte Anwendung
Die gefüllten Blöcke repräsentieren implementierte COM-Schnittstellen, die leeren Blöcke stellen lediglich COM-Schnittstellendefinitionen dar. Bei diesem Ansatz werden die wichtigsten COM-Schnittstellen entwickelt und implementiert. Bei der weiteren Implementierung beginnt das Testteam mit dem Testen der Implementierungen. Das Testen wird schrittweise durchgeführt. Sobald das Entwurfsteam den Schnittstellenentwurf vorläufig beendet hat, können die Testergebnisse verarbeitet werden. Einige Ergebnisse können auf Pro-
230
Entwerfen von COM-Schnittstellen
bleme hinweisen; das Entwurfsteam erkennt diese sofort und kann entsprechende Änderungen vornehmen. Diese Änderungen erfordern möglicherweise neue COM-Schnittstellen oder Änderungen an den Testspezifikationen. Diese inkrementellen Änderungen wirken sich zwar nicht auf die gesamte Anwendung aus, dennoch wird die gesamte Anwendung justiert. Wo mit der Implementierung beginnen? Der iterative Ansatz klingt gut, bis Sie zum Implementieren und Testen der Objekte kommen. Der Grund hierfür ist der, dass Sie nicht wissen, welche COM-Schnittstellen implementiert werden müssen. Die Implementierung der COM-Schnittstellen ähnelt der Tunnelerstellung, wobei die Schicht der Anwendungslogik den Tunnel darstellt, mit dem Darstellungs- und Datenschicht miteinander verbunden werden. Beginnen Sie mit der Tunnelerstellung an der Darstellungsschicht und arbeiten sich zur Datenschicht vor, oder starten Sie an der Datenschicht und arbeiten sich zur Darstellungsschicht vor, oder beginnen Sie an beiden Seiten gleichzeitig und hoffen darauf, dass sie sich die Tunnelabschnitte in der Mitte treffen? Die gewählte Methode richtet sich nach dem Entwicklerteam. Wenn Sie einen Ansatz wählen, bei dem der Tunnel von einer Schicht aus erstellt wird, entwickeln erstellen Sie einen Kernsatz, mit dem die verschiedenen Schichten nacheinander erstellt werden. Dies ist ein sequenzieller und iterativer Ansatz, da Planänderungen als Zwischenschritt vorgenommen werden. Das Problem besteht darin, dass die Schichten nicht signifikant geändert werden können, nachdem sie als unveränderlich festgelegt wurden – d.h. Änderungen erfordern ein erneutes Testen und Auswerten, was wiederum Zeit und Geld kostet. Dieser Ansatz ist jedoch einfacher umzusetzen als die gleichzeitige Tunnelerstellung von beiden Seiten. Der Ansatz der Tunnelerstellung von beiden Seiten aus ist weitaus flexibler, da Schnittstellen erst als unveränderlich festgelegt werden, wenn Sie in verschiedenen Szenarien eingesetzt und für effektiv befunden wurden. Eine unzureichend entworfene Schnittstelle wird geändert, und jeder Benutzer dieser Schnittstelle muss entsprechende Änderungen vornehmen. Dieser Ansatz bedeutet ebenfalls, dass mehrere Änderungen auftreten können, und wenn einige Teams Änderungen an der Schnittstelle vornehmen, ohne die anderen Beteiligten zu informieren, treten Fehler auf. Es liegt in der Verantwortung des Entwurfsteams, diese Änderungen zu verfolgen und alle Beteiligten über Änderungen zu informieren. Dieser Ansatz funktioniert nicht, wenn das Team nicht über die erforderliche Disziplin, Erfahrung in der Einschätzung von Implementierungsänderungen oder das Erstellen effektiver Kommunikationskanäle verfügt. Derartige Defizite führen zu Budgetüberschreitungen und Zeitverzögerungen und somit letztendlich zu einem Misserfolg bei der Anwendungsentwicklung. Die Erfahrung zeigt, dass der zweite
Einige Entwurfsmethoden für die COM-Schnittstelle
231
Ansatz besser einsetzbar ist. Es handelt sich um einen flexibleren Ansatz, der zu einer Anwendung führt, die eher den Wünschen des Endbenutzers entspricht.
8.3.2 Wann muss eine Schnittstelle erweitert werden? Unveränderlich heißt soviel wie »nicht änderbar«. Wie im richtigen Leben gibt es jedoch Grauzonen. Diese Grauzonen werden besonders in der Entwicklungsphase erkennbar. Nehmen Sie beispielsweise an, dass eine Entwurfsänderung erforderlich ist, die Schnittstelle jedoch bereits getestet und praktisch als unveränderlich festgelegt wurde. Statt eine weitere Schnittstelle hinzuzufügen, durch die ein Mehraufwand hinsichtlich Dokumentation, Entwurf und Wartung erforderlich würde, ist es einfacher, die praktisch bereits als nicht änderbar geltende Schnittstelle zu bearbeiten. Sie können sich auch vorstellen, dass ein Patch für einen kleinen Bug bereitgestellt werden muss. Das Hinzufügen einer weiteren Schnittstelle ist zu kosten- und zeitintensiv, wenn das Hauptziel in der Minimierung von Kosten und Systemausfällen besteht. Das Vornehmen derartiger Änderungen kann nicht als Entwurfsmethode bezeichnet werden. Gute Entwürfe und eine gute Planung können Situationen verhindern, in denen Änderungen an unveränderlichen Schnittstellen erforderlich werden. Erweitern der Schnittstelle Die erste Methode zur Bearbeitung der Schnittstelle stellt das einfache Hinzufügen der zusätzlichen Methode an das Ende der VTabelle dar, während die ursprüngliche VTabelle intakt bleibt. Diese Methode wird als Erweiterung bezeichnet. Sehen Sie sich die folgende unveränderliche Schnittstelle an: [ uuid(BE1FFD3B-E489-11D1-B44D-00A0247D759A), version(1.0), helpstring("Math 1.0 Type Library") ] interface IMath : IUnknown { [id(1)] HRESULT add(long param1, long param2, long *retvalue); };
Die obige Schnittstelle ist eine mathematische Schnittstelle mit der Fähigkeit, zwei Zahlen (param1 und param2) zu addieren und das Ergebnis in der Variablen retvalue zurückzugeben.
232
Entwerfen von COM-Schnittstellen
Das folgende Beispiel fügt eine zusätzliche subtract-Methode an das Ende der ursprünglichen VTabelle an. [ object, dual, uuid(BE1FFD3B-E489-11D1-B44D-00A0247D759A), version(2.0), helpstring("Math 2.0 Type Library") ] interface IMath : IDispatch { [id(1)] HRESULT add(long param1, long param2, long *retvalue); [id(2)] HRESULT subtract(long param1, long param2, long *retvalue); };
In dieser erweiterten Schnittstellendefinition weist die subtract-Methode eine abweichende Funktionalität bereit. Im Vergleich zur Erstellung einer neuen Schnittstelle und dem damit verbundenen Overhead stellt diese Methode die weniger aufwendige und einfachere Lösung dar. Beim Veröffentlichen dieser Schnittstelle müssen Sie sicherstellen, dass die version-Nummer der Schnittstellenattribute geändert wird. Auf diese Weise kann der Kunde zwischen den verschiedenen Versionen unterscheiden. Hinzufügen ergänzender Funktionalität Im vorangegangenen Beispiel wurde die subtract-Methode hinzugefügt. Beide Methoden sind lediglich in der Lage, Zahlen vom Typ integer zu addieren (bzw. zu subtrahieren). Angenommen, Sie müssen die Funktionalität so abändern, das auch reelle Zahlen addiert werden können. Die Erweiterung wird auch hier verwendet, die Methode wird jedoch add2 genannt, um die ähnliche Funktionalität zu verdeutlichen. Die Lösung lautet folgendermaßen: interface IMath : IDispatch { [id(1)] HRESULT add(long param1, long param2, long *retvalue); [id(2)] HRESULT add2(double param1, double param2, double *retvalue); };
Die Zahl am Ende der add2-Methode gibt die abweichende Version an, was üblicherweise andere Parameter erfordert. Diese Methode wird angewendet, wenn nur wenige ähnliche Methoden vorhanden sind. Es sollte unter keinen Umständen add100 verwendet werden, da dies bedeuten würde, dass Ihre Schnittstelle einhundert Mal geändert wurde.
Einige Entwurfsmethoden für die COM-Schnittstelle
233
Hinzufügen ergänzender Schnittstellen Nehmen Sie an, Ihre Anwendungen müssen radikal geändert werden. Angenommen, Sie erfahren, dass kein Kostenunterschied zwischen der Verwendung von long- und double-Werten für die Addition vorhanden ist, und dass in einigen Situationen die Verwendung von double-Werten schneller zum Ziel führt. Tritt eine solche radikale Änderung ein, muss die Schnittstelle aktualisiert werden. Eine radikale Änderung erfordert eine neue Schnittstellendefinition. Sehen Sie sich die folgende Schnittstellendefinition an: interface IMath2 : IDispatch { [id(1)] HRESULT add(double param1, double param2, double *retvalue); [id(2)] HRESULT subtract(double param1, double param2, double *retvalue); };
In dieser Lösung wird nicht der Methode, sondern der Schnittstelle (IMath2) ein inkrementeller Wert hinzugefügt. Die Schnittstelle könnte auch IMathDoubleVersion heißen, beachten Sie jedoch die Situation, in der die Schnittstelle long double-Werte implementiert. Die Schnittstelle hieße nun IMathLongDoubleVersion. Erweitern Sie dies auf 100 Schnittstellen, und die Schnittstellennamen würden schnell äußerst lang und unübersichtlich. Dies ist eine Frage der Semantik, aber im Rahmen einer umfangreichen Skalierung von Vorteil. Der Vorteil des inkrementellen Ansatzes besteht darin, dass ein beliebiger Entwickler, der die Schnittstelle verwendet, weiß, dass die Schnittstellen ergänzend sind. Wenn bekannt ist, welche Funktion eine der Schnittstellen besitzt, sollte die Funktion der weiteren Schnittstellen erraten werden können.
8.3.3 Verwenden einer Programmiersprache zur Entwicklung von COM-Schnittstellen Jetzt sollen COM-Schnittstellen definiert werden. Sie haben erfahren, dass IDL die Grundlage von COM darstellt, daher ist es sinnvoll, IDL einzusetzen. Da eine COM-kompatible Sprache COM-Typenbibliotheken generiert, sollte diese Sprache zur Definition der entsprechenden Schnittstellen eingesetzt werden. Dieser Vorgang wird als Erstellung einer leeren Implementierung bezeichnet. Verwenden wir diese Methode und ermitteln, welche Art von COM IDL generiert wird. Eine einfache Komponente in den verschiedenen Sprachen Zum Vergleich der generierten IDL verwenden wird die COM-Schnittstellendefinition aus dem Abschnitt »Ein einfaches Beispiel« weiter oben in diesem Kapitel. Hier versuchen wir vor allem, SimpleComponent zu definieren.
234
Entwerfen von COM-Schnittstellen
Definieren von »SimpleComponent« über Visual C++ Zur Entwicklung einer COM-Komponente, die als leere Implementierung verwendet wird, muss es sich bei dieser um eine ATL-Komponente (Active Template Library) handeln. Es ist möglich, MFCs (Microsoft Foundation Classes) zu verwenden, ATL ist jedoch für COM-Schnittstellen besser geeignet (sowohl ATL als auch MFC sind Bibliotheken, die den Prozess der Entwicklung von Windows-Anwendungen erleichtern). Bei der Erstellung der ATL-Komponente müssen Sie sicherstellen, dass es sich um eine DLL (Dynamic Link Library) handelt. Die Schnittstellen werden unter Verwendung des ATL-Objekt-Assistenten zum Hinzufügen eines neuen Einfachen Objekts entworfen. Es gibt zwei Dialogfelder, die sorgfältig ausgefüllt werden sollten, diese werden in den Abbildungen 8.5 und 8.6 gezeigt.
Abbildung 8.5 Registerkarte »Namen« im Eigenschaftenfenster des Assistenten für das Objekt »SimpleComponent«
Der kurze Name definiert den Komponentennamen, die COM-Coklasse trägt denselben Namen. Der Assistent erstellt nicht unabhängig von der COM-Schnittstelle eine COM-Coklasse – die zwei Elemente werden in einem Arbeitsschritt erstellt. Die Schnittstelle weist das Präfix »I« auf, um zu kennzeichnen, dass es sich um eine COM-Schnittstelle handelt. Die Programm-ID lautet SimpleComponentVC.SimpleComponent, eine Aneinanderreihung von COM-Bibliotheksname und COM-Coklasse. Abbildung 8.6 definiert die Attribute der hinzuzufügenden Schnittstelle. Das einzige Attribut von Interesse ist das Interface-Attribut, das die Eigenschaft Custom oder Dual aufweisen kann. Lautet die Eigenschaft Custom, wird ISimpleComponent von IUnknown abgeleitet. Dual bedeutet, dass die Schnittstelle auf IDispatch basiert. In diesem Fall wurde das Schnittstellenattribut Custom ausgewählt. Zur Vervollständigung der Entwicklung ist es erforderlich, mit Hilfe eines kleinen Assistenten method1 als Teil der Schnittstelle hinzuzufügen. Dies ist alles, was zur
Einige Entwurfsmethoden für die COM-Schnittstelle
235
Erstellung einer Typbibliothek erforderlich ist, die eine Reihe von Schnittstellen enthält, die in anderen Entwicklungsumgebungen implementiert werden können.
Abbildung 8.6 Registerkarte »Attribute« des Assistenten für das Objekt »SimpleComponent«
Was generiert der Assistent in der Typbibliothek? Dies muss nicht gezeigt werden, da die Ausgabe nahezu identisch mit der Anzeige in der ursprünglichen IDL-Datei ist. Der wichtige Punkt ist, dass Visual C++ die Fähigkeit zur Feinabstimmung der IDL bietet und die Erstellung einer wunschgemäßen Schnittstelle ermöglicht. Definieren von »SimpleComponent« über Visual Basic In Visual Basic kann, wie in Visual C++, eine Typbibliothek durch das Definieren leerer Visual BasicKlassen definiert werden. Erstellen Sie hierzu ein Visual Basic-ActiveX-DLL-Projekt (Dynamic Link Library). Nennen Sie das Projekt SimpleComponentVB, es wird anschließend eine einzige VB-Klasse mit Namen SimpleComponent enthalten. Die Definition der Klasse lautet: Public Sub method1(ByVal param1 As Long) End Sub
Nach der Kompilierung wird die Typbibliothek generiert. [ uuid(FC81A421-2A94-11D3-957D-0080C700807A), version(4.0) ] library SimpleComponentVB { importlib("STDOLE2.TLB");
Ein Teil dieser Typbibliotheksgenerierung ist nicht richtig, da die Typbibliothek mit Hilfe der OLE-Ansicht angezeigt wurde. Die OLE-Ansicht übersetzt einige der IDL-Schlüsselwörter in eine eigene Darstellung, die nicht immer der ursprünglichen IDL entspricht. Im Folgenden liegt das Hauptaugenmerk auf der Benennungskonvention und den Schnittstellentypen. Im Gegensatz zu Visual C++ wird in Visual Basic der Schnittstellenname nicht mit dem Präfix »I« versehen, stattdessen wird ein Unterstrich verwendet. Wie in Visual C++ wird in Visual Basic die COM-Coklasse (coclass) mit der COM-Schnittstelle (interface) verknüpft. Im Gegensatz zu Visual C++ kann Visual Basic jedoch nur eine Schnittstelle aus einer spezifischen Klasse generieren – die von Visual Basic erstellte Schnittstelle ist dual. Mit Visual Basic können keine benutzerdefinierten Schnittstellen definiert werden. Des Weiteren ist eine Beschränkung hinsichtlich der Parameter und Parametertypen vorhanden.
Einige Entwurfsmethoden für die COM-Schnittstelle
237
Definieren von »SimpleComponent« über Visual J++ Zur Erstellung einer COM-Schnittstelle mit Visual J++ erstellen Sie ein Visual J++-Projekt, das eine COM-DLL repräsentiert. Benennen Sie anschließend die hinzugefügte Java-Klassendatei in SimpleComponent um, und definieren Sie die Quellen folgendermaßen: /** * Diese Klasse wurde zur Paketerstellung im COM-DLL-Ausgabeformat entworfen. * Die Klasse verfügt mit Ausnahme der Erstellungsroutine über keine standardmäßigen Einsprungpunkte. * Öffentliche Methoden werden als Methoden der standardmäßigen COM-Schnittstelle offen gelegt. * @com.register ( clsid=4D559511-2A97-11D3-957D-0080C700807A, typelib=4D559512-2A97-11D3-957D-0080C700807A ) */ public class SimpleComponent { public void method1( long param1) { return; } }
Durch das Kompilieren der Klasse wird die Typbibliothek generiert. [ uuid(4D559512-2A97-11D3-957D-0080C700807A), version(1.0), helpstring("SimpleComponentVJ") ] library SimpleComponentVJ { // TLib : // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046} importlib("STDOLE2.TLB"); // Vorwärts gerichtete Deklaration aller in dieser typelib definierten Typen dispinterface SimpleComponent_Dispatch; [ uuid(72AA2BB7-2A97-11D3-957D-0080C700807A) ] dispinterface SimpleComponent_Dispatch { properties: methods: [id(0x00000064)] VARIANT wait(
Die generierte Ausgabe ist in einer Schnittstelle nicht erwünscht. Visual J++ fügt verschiedene Methoden hinzu, die Teil der Java-Objektklasse sind und betrachtet die Schnittstelle als dispinterface. Hierbei handelt es sich um eine COM-Spezifikation, nach der die Methoden der Schnittstelle nur mit Hilfe der späten Bindung ausgelöst werden können. Dies bedeutet, dass COM langsamer werden kann, und dies soll natürlich nicht passieren. Ein weiteres kleines Problem ist die Benennung der COM-Schnittstelle mit SimpleComponent_Dispatch. Wird dieser Client im Kontext eines Visual C++- oder Visual Basic-Clients verwendet, sind mehrere Benennungskonventionen vorhanden. Und die Lösung ist? Die einfachste Methode zur Entwicklung einer Schnittstelle ist die Verwendung von Visual C++ oder das Schreiben eines Dienstprogrammprojekts und eine anschließende Kompilierung der IDL-Datei mit Hilfe von MIDL. Bei der manuellen Bearbeitung der IDL-Datei haben Sie umfassende Steuerungsmöglichkeiten hinsichtlich der Definition und Feinabstimmung der Schnittstellen und Methoden. Die IDL-Datei kann sich ändern und das Neuschreiben erfordern. Das Konzept der Schnittstellen und Implementierungen ändert sich jedoch nicht.
Einige Entwurfsmethoden für die COM-Schnittstelle
239
Parameter innerhalb von Methoden COM bietet die Fähigkeit zur Definition von Parameterdatentypen. Sie können entweder allgemeine Datentypen oder feste Datentypen verwenden. Ein allgemeiner Datentyp ist beispielsweise VARIANT, der einen beliebigen Datentyp akzeptieren kann. Ein fester Datentyp, beispielsweise long oder double, kann nur einen bestimmten Wertetyp enthalten. Feste Parameter weisen folgende Vorteile auf: 왘 Durch den spezifische Datentype kann leicht ermittelt werden, welcher Daten-
typ weitergegeben werden muss. 왘 Bei der Konvertierung von einem Datentyp in einen anderen gehen keine Da-
ten verloren. 왘 Feste Datentypen sind schneller als VARIANTS.
Allgemeine Parameter weisen ebenfalls einige Vorteile auf: 왘 Implementierungen können leicht aktualisiert werden, ohne dass eine Schnitt-
stellenänderung erforderlich ist, da die Parameter jeden Datentyp akzeptieren können. 왘 Die Parameter können verschiedene Datentypen handhaben, ohne dass der
Client verschiedene Methoden oder Schnittstellen für die unterschiedlichen Datentypen aufrufen muss. 왘 Schnittstellen können einfacher entkoppelt werden. Mit allgemeinen Parame-
tern hängt eine spezifische Schnittstelle nicht von einer anderen Schnittstelle oder Funktionalität ab. 왘 Allgemeine Parameter erfordern keine Skriptsprachenfunktionsaufruf zur Da-
tenkonvertierung. Viele Skriptsprachen verwenden als Standarddatentyp stringWerte, was jedoch häufig eine falsche Annahme darstellt. Die Entscheidung für allgemeine oder spezifische Datentypen ist keine einfache Entscheidung, da jede Option Stärken und Schwächen aufweist. Es gibt für den Einsatz der jeweiligen Datentypen keine Faustregel. ADO-Objekte (Active Data Objects) beispielsweise, die in Visual Basic und bei der Skripterstellung verwendet werden, werden mit allgemeinen Datentypen geschrieben. Allgemeine Datentypen ermöglichen das Erstellen generischer Methoden für den Datenabruf. ADO arbeitet mit Recordsets von Datenbanken. Beim Werteabruf ist es unsinnig, Methoden wie beispielsweise getDouble, getLong, getString usw. zu schreiben. Die Schnittstelle würde zu lang und komplex. Es ist einfacher, mit Hilfe der so genannten getValue-Methode einen VARIANT-Wert
240
Entwerfen von COM-Schnittstellen
zurückzugeben. Und da der typische Datenprovider viele verschiedene Datentypen aufweist, ist die Verwendung von VARIANT angemessen. Datentypen der OLE-Automatisierung Es besteht ein Unterschied zwischen den Parametertypen, die für benutzerdefinierte und für duale Schnittstellen verwendet werden können. Dies rührt daher, dass eine duale Schnittstelle in einer beliebigen Programmiersprache implementiert werden kann, aber nicht alle Konsumenten alle Datentypen unterstützen. Es gibt eine Kategorie Datentypen, die als kompatibel mit der OLE-Automatisierung bezeichnet werden. Der Hauptdatentyp für die OLE-Automatisierung ist VARIANT, eine Struktur, die einen Referenzwert zum repräsentierten Datentyp enthält. Die folgenden Datentypen werden am häufigsten eingesetzt: 왘 long, BY TE, short, float, double, BOOL, DATE, char: Diese Datentypen wer-
den sehr oft bei der Darstellung von Zahlen eingesetzt. 왘 BSTR: Der string-Datentyp von COM. 왘 IDispatch: Ein Datentyp, der eine Referenz auf eine weitere COM-Schnittstelle
enthält. 왘 SafeArray: Ein Datentyp, der ein Elementarray enthält, das entweder einem
der obigen Typen oder VARIANT entspricht. Das Array ist »sicher« (safe), da es den Elementzähler, Start- und Endpunkt enthält. Außerhalb der Datentypen für die OLE-Automatisierung befinden sich alle weiteren Datentypen. Obwohl diese im jeweiligen Kontext sinnvoll sind, können sie in vielen Programmierumgebungen nicht eingesetzt werden. Im Kontext von Windows DNA und Anwendungsentwicklung sollte jeder Datentyp ignoriert werden, der nicht überall verwendet werden kann, es sei denn, ein triftiger Grund spricht für dessen Verwendung. Verwenden des Datentyps »Variant« Lassen Sie uns einen Schritt zurückgehen und untersuchen, wie der VARIANT-Datentyp in den verschiedenen Sprachen dargestellt wird. Sehen Sie sich die folgende IDL-Methodendeklaration an: [ id(1)] HRESULT method2([in] VARIANT newVal);
Dieser VARIANT-Wert wird in Visual C++ folgendermaßen dargestellt: STDMETHOD( method2)(/*[in]*/ VARIANT newVal);
Der VARIANT-Datentyp ist eine Rohstruktur, die Informationen zu den enthaltenen Daten aufweist:
Einige Entwurfsmethoden für die COM-Schnittstelle
241
In Visual J++ wird VARIANT wie folgt darstellt: public void method2 (Variant newVal);
Der Variant-Datentyp ist eine Java-Klasse, die die Datenstruktur VARIANT kapselt. Visual J++ verwendet den COM-VARIANT-Datentyp nicht direkt, da es sich bei COM um ein Lowlevel-Modell handelt. Zur Vereinfachung der COM-Programmierung können Bibliotheken oder Helper-Elemente erstellt werden. Visual J++ hat das Helferobjekt Variant erstellt, mit dem die Details dieses Datentyps gehandhabt werden können. In Visual Basic wird VARIANT wie folgt darstellt: Public Sub method1(ByVal newVal As Variant)
In Visual Basic stellt der Datentyp Variant einen systemeigenen Datentyp dar. Dieser kann mit einer Reihe von Funktionen bearbeitet werden, aber da es sich um einen Datentyp handelt, kann er wie integer, long oder double direkt verwendet werden. Unabhängig davon, welche Sprache der VARIANT-Datentyp verwendet, ist zusätzliche Arbeit erforderlich, und je nach Verwendung kann eine Verlangsamung die Folge sein. Dies rührt daher, dass die VARIANT-Datenstruktur einige Flags erfordert, damit die Struktur richtig erkannt wird. Warum gibt es einen VARIANT-Datentyp? Über VARIANT wird eine Pseudostruktur erstellt und es werden flexible Parameter bereitgestellt. Im ADO-Beispiel wird das Problem durch den VARIANT-Datentypen perfekt gelöst. Werden Schnittstellen hauptsächlich in Skriptumgebungen verwendet, stellt das Verwenden des VARIANT-Datentyps die einfachere Option dar. Verwenden des Objektdatentyps Sehen Sie sich die folgende Methodendeklaration an, die eine Objektreferenz enthält: [ id(1)] HRESULT method3([in] IDispatch* newVal);
Das Objekt stellt einen Verweis auf die IDispatch-Schnittstelle dar. Es mag logischer erscheinen, IUnknown als Objekt zu referenzieren, aber das Problem hierbei ist, dass der Konsument bei einer Schnitstelle IUnknown wissen muss, welche Schnittstelle verwendet wird. Dies ist in vielen Skriptsprachen jedoch nicht möglich. Visual Basic hat Probleme, die IUnknown-Schnittstelle zu erkennen und erwartet, dass alle Objektverweise auf IDispatch basieren. Die IDispatch-Referenz ermöglicht der Skriptsprache das Abfragen verschiedener Methoden. Visual C++ wird die Methode folgendermaßen deklariert: STDMETHOD(method3)(/*[in]*/ IDispatch* newVal);
242
Entwerfen von COM-Schnittstellen
Hierbei handelt es sich um einen Zeiger, der auf die Schnittstelle selbst verweist. Visual J++ wird die Methode folgendermaßen deklariert: public void method3(Object newVal);
Der Object-Datentyp ist ein Java-Stammobjekt. Sie verwenden dieses, um einen Typecast für die gewünschte Schnittstelle durchzuführen. Visual Basic wird die Methode folgendermaßen deklariert: Public Sub method3(ByVal newVal As Object)
Wie in Visual J++ handelt es sich beim Object-Datentyp um ein Visual BasicStammobjekt. Im Gegensatz zu Visual J++ muss jedoch für das Objekt kein Typecast durchgeführt werden, um es zu verwenden. Visual Basic verwendet die IDispatch-Schnittstelle, wenn Methodenaufrufe für den Object-Datentyp durchgeführt werden. Bei der Entwicklung der Schnittstelle kann es geschickter sein, feste Schnittstellendatentypen zu verwenden, wie nachfolgend gezeigt: [ id(1)] HRESULT method3([in] ISimpleComponent* newVal);
Beide Methoden sind akzeptabel. Die einzige Tücke bei Verwenden einer festen Schnittstelle ist die, dass ISimpleComponent von IDispatch abgeleitet werden muss. Und obwohl es einfacher erscheinen mag, direkt die Schnittstellenzeiger zu verwenden, ist dies nur zutreffend, wenn Server und Konsument in Visual C++ geschrieben wurden. Visual C++ weist die Fähigkeit auf, Schnittstellenzeiger direkt zu verwenden. In anderen Sprachen kann eine QueryInterface vorhanden sein, in anderen Umgebungen, beispielsweise beim Scripting, kann nur IDispatch verwendet werden. Verwenden des Datentyps »String« String-Werte stellen in einem C-Programmiersprachenkontext Puffer von Zeichen dar, die auf ein NULL-Zeichen enden. Die Zeichen können Einzel- oder Doppelbyte umfassen. Die Byteanzahl vor dem NULL-Zeichen definiert die Länge der Zeichenfolge. Bei COM und OLE-Automatisierung verwendet eine Zeichenfolge den BSTR-Datentyp. Ein BSTR unterscheidet sich von einer herkömmlichen Zeichenfolge darin, dass sie UNICODEDaten enthält und die Länge der Zeichenfolge gespeichert wird. Sehen Sie sich diese Methodendeklaration an: [helpstring("method method3")] HRESULT method3(BSTR param1);
In Visual C++ lautet die Deklaration: STDMETHOD(method3)(BSTR param1);
Einige Entwurfsmethoden für die COM-Schnittstelle
243
In Visual J++ lautet die Deklaration folgendermaßen: public void method3(String param1);
In Java handelt es sich bei dem String-Objekt um eine Java-Basisklasse. In Visual Basic lautet die Deklaration folgendermaßen: Public Sub method3( ByVal param1 as String)
Wie in Visual J++ stellt in Visual Basic das String-Objekt einen Bestandteil der Sprache dar. Das Zeichenfolgenobjekt stellt weder in Visual J++ noch in Visual Basic ein Problem dar, da eine direkte Zuordnung vorhanden ist. In Visual C++ dagegen ist die Sache etwas komplizierter, da aufgrund einiger Legacyelemente viele Wrapperklassen für Zeichenfolgen vorhanden sind. In Kapitel 9 wird aufgezeigt, wie in Visual C++ Zeichenfolgen behandelt werden können. Definieren von Rückgabewerten Bei der Deklaration von Funktionen möchten Sie üblicherweise Rückgabewerte für den Aufrufer deklarieren. In allen vorangegangenen IDL-Beispielen wiesen die Rückgabewerte die Form HRESULT auf. In COM-IDL ist es üblich, einen HRESULT-Wert zurückzugeben. HRESULT ist ein Behandlungsroutinenmechanismus. COM behandelt Ausnahmen basierend auf dem Rückgabewert der aufgerufenen Methode. Diese Methode wird verwendet, da Ausnahmen nicht über COMSchnittstellen hinweg erzeugt (throw-Anweisung) werden können. Hierbei handelt es sich um eine Entwurfsentscheidung für COM, die bereits früh getroffen wurde. Zu dieser Zeit war noch kein konsistenter Ausnahmemechanismus vorhanden, und nicht alle Sprachen unterstützten Ausnahmen. Daher müssen alle Methoden der COM-Schnittstellen einen Fehlercode zurückgeben, der angibt, ob der Methodenaufruf erfolgreich war. Obwohl mittlerweile fast alle Sprachen das Ausnahmekonzept unterstützen, kann in COM weiterhin keine Ausnahme erzeugt werden. Zur Definition eines Rückgabewertes in COM muss die Funktion mit Hilfe dieser IDL deklariert werden: [id(1)] HRESULT method2(long param1, [out,retval]long *retval);
In Visual Basic und Visual J++ benötigen Sie keinen Rückgabewert der Form HRESULT um anzugeben, ob eine Methode funktioniert, da die Laufzeit die Details von HRESULT maskiert. Hierbei wird die interne Fehlerbehandlung mit einem HRESULT-Wert verknüpft. Je nach Ergebnis wird ein Fehler oder HRESULT ausgegeben.
244
Entwerfen von COM-Schnittstellen
Die Implementierung der vorstehenden IDL-Methode in Visual Basic lautet folgendermaßen: public function method2( ByVal param1 as long) as long
In Visual J++ lautet die Methode: public int method2(int param1);
Da Visual C++ auf unterster Ebene arbeitet, ist die Methodendeklaration mit der IDL-Deklaration identisch und weist zwei Parameter auf. Richtungsparameter Wenn ein COM-Konsument eine Implementierung aufruft, werden Daten vom Konsumenten an die Implementierung übergeben, und die Daten erfordern ein Marshaling und anschließendes Unmarshaling. Das Problem hierbei ist, dass die Daten einer kompilierten Programmierumgebung an eine weitere kompilierte Programmierumgebung gesendet werden müssen. Die Umgebungen können sich auf demselben Rechner oder in zwei völlig verschiedenen Netzwerken in verschiedenen Erdteilen befinden, daher müssen die Daten unter Umständen auf dem Rechner oder im Netzwerk gesendet werden. COM löst dieses Problem, da COM standortunabhängig ist. Der Preis hierfür ist jedoch, dass die Schnittstelle die Richtung des Parameters deklarieren muss. Sehen Sie sich diese Funktionsdeklaration an: HRESULT someMethod( long value);
Diese Methodendeklaration besagt, dass der Parameter vom Konsumenten an die Implementierung gesendet wird. Die COM-Marshalingroutine erstellt einen Puffer zur Speicherung des long-Wertes. Nehmen Sie jedoch einmal an, dass eine Zeichenfolge 40.000 Zeichen umfasst – ein derartiger Puffer ist nicht unbedeutend. Beim Verschieben des Puffers vom Konsumenten zur Implementierung und zurück wird Bandbreite benötigt. Beim vorangegangenen Methodenaufruf ist der Puffer nur erforderlich, wenn die Daten vom Konsumenten an die Implementierung gesendet werden. Für den Rückweg wird kein Puffer benötigt. Im Idealfall würde der Puffer lediglich an den Server gesendet und auf dem Rückweg ignoriert. Sie können diese Optimierung erreichen, indem Sie die Richtung des Parameters angeben. Dies geschieht durch Anwenden der Richtungsattribute auf den Parameter. Der Standard lautet in, d.h. dass nur Daten vom Konsumenten zur Implementierung gesendet werden. Das Attribut out gibt an, dass der Datenfluss von der Implementierung zum Konsumenten verläuft. Das Kombinieren der Attribute in und out gibt an, dass die Daten vom Konsumenten zur Implementierung fließen, gelöscht und neu zugewiesen und anschließend von der Implementierung
Einige Entwurfsmethoden für die COM-Schnittstelle
245
zurück zum Konsumenten gesendet werden. Dieser Vorgang erscheint kompliziert, ist jedoch erforderlich, da COM nicht standortabhängig ist. Nachfolgend eine Beispielschnittstelle, mit der alle drei Varianten implementiert werden: interface ITest : IDispatch { [id(1)] HRESULT onlyInput(long param1); [id(2)] HRESULT onlyOutput([out]long *output); [id(3)] HRESULT inputOutput([in, out]long *value); };
Das Richtungsattribut ist bei der Implementierung einer Schnittstelle besonders wichtig, da die Sprachen die Schnittstelle anders behandeln, wenn ein Konsument die Schnittstelle beansprucht. In diesem Kapitel wird nur der Aspekt der Implementierung besprochen. Wird Visual C++ zur Implementierung der Schnittstelle eingesetzt, ist die Methodendeklaration identisch mit der IDL. Die einzige Ausnahme besteht darin, dass die Deklaration auf der C++-Syntax basiert. Bei der Implementierung mit Visual Basic sind jedoch Unterschiede vorhanden. Visual Basic kann die Methoden onlyInput und inputOutput problemlos verwenden, onlyOutput kann jedoch nicht eingesetzt werden. Wenn Sie versuchen, die ITest-Schnittstelle – so, wie sie ist – zu implementieren, erzeugt Visual Basic einen Kompilierungsfehler. Dieser Fehler entsteht, da keine output-Parameter verarbeitet werden können. Die Methode inputOutput weist in Visual Basic die folgende Signatur auf: public sub inputOutput( value as long)
Diese Visual Basic-Methodendeklaration enthält in der Parameterliste kein ByValue. Dies rührt daher, dass bei einem in/out-COM-Parameter die Visual Basic-Laufzeitumgebung die Speicherverwaltung für den COM-Parameter übernimmt. Visual J++ kann eine beliebige der in der Schnittstelle definierten Methoden implementieren. Es wird jedoch nicht zwischen out und in/out unterschieden. Die Methodendefinitionen lauten: public void onlyOutput( int[] output); public void inputOutput( int[] output);
Der Parameter wird als Array betrachtet. Auf diese Weise wird eine Referenz erzeugt, die praktisch einem Zeiger gleicht.
246
Entwerfen von COM-Schnittstellen
Eine spezielle Methode – die Eigenschaft Bis zu diesem Punkt drehte sich alles um die Implementierung von Methoden, aber in COM können auch Eigenschaften definiert werden. Eigenschaften unterscheiden sich von Methoden darin, dass Sie einer Eigenschaft (wie einer Variablen) Werte zuordnen können. Obwohl Eigenschaften sehr effektiv sind, können Sie in einigen Sprachen nicht implementiert werden (beispielsweise in Visual C++ und Visual J++), da kein äquivalentes Konzept vorhanden ist. In Visual Basic und bei der Skripterstellung können Eigenschaften jedoch verwendet werden. Sehen Sie sich folgenden JavaScript-Code an: var temp = myObject.simpleProperty1;
Diese Syntax wird als Property Get-Anweisung bezeichnet, da der Wert der Eigenschaft vom Objekt abgerufen wird. Das Pendant hierzu ist die Property PutAnweisung, die folgendermaßen lautet: myObject.simpleProperty1 = temp;
MIDL behandelt Eigenschaften wie Funktionen mit den Attributen propput und propget, wie nachfolgend gezeigt wird: [propget] HRESULT simpleProperty1([out, retval] VARIANT *pVal); [propput] HRESULT simpleProperty1([in] VARIANT newVal);
In Visual C++ lautet die Methodendeklaration: STDMETHOD(get_simpleProperty1)(/*[out, retval]*/ VARIANT *pVal); STDMETHOD(put_simpleProperty1)(/*[in]*/ VARIANT newVal);
Die Methodendeklaration erhält die Präfixe get_ und put_. Hierbei handelt es sich lediglich um ein Notationsdetail, das durch den MIDL-Compiler erzeugt wird. COM übersetzt die Eigenschaften für diese Methoden. In Visual Basic lautet die Funktionsdeklaration folgendermaßen: Public Property Let simpleProperty1(ByVal RHS As Variant) Public Property Get simpleProperty1() As Variant
Visual Basic unterstützt die Notation von Eigenschaften, indem das Property-Attribut der Methodendeklaration vorangestellt wird. Das Let-Eigenschaftenschlüsselwort ist äquivalent mit dem IDL-Schlüsselwort put. Das Get-Eigenschaftenschlüsselwort stimmt mit dem IDL-Schlüsselwort get überein. Visual Basic weist ein weiteres Eigenschaftenschlüsselwort auf, Set, das dem Let entspricht, der Funktionsparameter weist hier jedoch kein ByVal auf.
Einige Entwurfsmethoden für die COM-Schnittstelle
247
In Visual J++ lautet die Funktionsdeklaration folgendermaßen: public Variant getSimpleProperty1(); public void setSimpleProperty1(Variant pVal);
Der Schlüssel zur ordnungsgemäßen Funktion von Eigenschaften in Visual J++ liegt darin, den IDL-Methodennamen die Präfixe get und set voranzustellen. Zufälligerweise ist dies auch Bestandteil der Java Beans-Spezifikation. Untersucht man jedoch die Ergebnis-IDL, fällt etwas Interessantes auf: [id(0x0000006c)]void setSimpleProperty1([in, out] VARIANT* Parameter0); [id(0x0000006d)] VARIANT getSimpleProperty1(); [id(0x0000006e), propget] VARIANT simpleProperty1(); [id(0x0000006e), propput] void simpleProperty1([in] VARIANT rhs);
Der Visual J++-Compiler erzeugt zwei IDL-Deklarationen. Die erste (IDs 6C und 6D) behandelt jede der Funktionen, bei denen es sich um Eigenschaften handelt, wie Funktionen. Die zweite Deklaration (ID 6E) behandelt die Funktionen als Eigenschaften, indem die Attribute propget und propput hinzugefügt werden.
8.4 Resümee In diesem Kapitel haben Sie eine Einführung in die COM-Schnittstellen erhalten. Sie haben gelernt, wie diese Schnittstellen in den verschiedenen Sprachen entwickelt und programmiert werden. Während dieses Prozesses haben Sie auch die jeweiligen IDL-Äquivalente kennen gelernt. Am einfachsten können Schnittstellen in IDL geschrieben werden. Das Verwenden anderer Sprachen ist weniger empfehlenswert, da durch jede Sprache zusätzliche Informationen hinzugefügt werden. Visual C++ fügt die wenigsten Zusatzinformationen hinzu, Visual Basic einige, Visual J++ weist so viele Extrainformationen auf, dass von der Verwendung von Visual J++ zur COM-Schnittstellendefinition abgeraten wird. Im nächsten Kapitel soll die Implementierung der verschiedenen Schnittstellen unter Verwendung unterschiedlicher Sprachen betrachtet werden.
248
Entwerfen von COM-Schnittstellen
9 Implementieren von COM-Schnittstellen In diesem Kapitel werden die durch das Designerteam entworfenen Schnittstellen implementiert und anschließend in anderen Implementierungen eingesetzt. Das Kombinieren einer Implementierung mit einer Schnittstelle führt zu einer Komponente. Zunächst betrachten wir einen Beispielssatz Schnittstellen, der die Arbeit einer typischen Windows DNA-Anwendung repräsentiert, und es wird untersucht, warum die Schnittstellen genau so und wie sie entworfen wurden. Anschließend wenden wir uns der Frage zu, wie Schnittstellen unter Verwendung der Unified Modeling Language (UML) am besten implementiert werden können. UML wurde in der COM-Entwicklungsphase (Component Object Model) nicht eingesetzt, in der Implementierungsphase kommt UML jedoch teilweise zum Einsatz. Die Komponenten werden unter Verwendung von drei unterschiedlichen Programmiersprachen (Visual Basic, Visual C++ und Visual J++) implementiert. Der Schwerpunkt dieses Kapitels richtet sich darauf, die Komponenten möglichst effizient zu implementieren. Ein weiterer Aspekt der Implementierung ist die Art und Weise, mit der Schnittstellen verwendet werden. Dies ist wichtig, da die Anwendungsarchitektur Schichten umfasst. Wieder soll die Verwendung anhand der drei verschiedenen Programmiersprachen verdeutlicht werden. Obwohl nicht ausführlich auf die Skripterstellung eingegangen wird, enthält das gesamte Kapitel Anmerkungen zum Scripting.
9.1 Der Vorgang der Komponentenimplementierung Das Entwickeln einer Windows DNA-Anwendung umfasst im Wesentlichen zwei Operationen: das Sammeln von Daten sowie das Verarbeiten der gesammelten Daten. Dies scheint ein einfacher Ansatz zu sein, gelegentlich werden jedoch Daten nicht ordnungsgemäß gesammelt, und manchmal sind die auszuführenden Operationen schwierig. Werden diese Aufgaben nicht ordnungsgemäß ausgeführt, führt dies zu Fehlern oder Bugs. Zur Minimierung der Bugzahl können Sie die Anwendung testen. Verwenden Sie das Debuggen, um die Stabilität der Komponente sicherzustellen, nicht jedoch, um Schlamperei bei der Programmierung wieder auszugleichen. Selbst durch umfangreiche Testläufe können nicht alle Bugs aufgespürt werden, die durch nachlässiges Programmieren entstehen. Das Debuggen ist ein sekundärer Vorgang, der sehr zeitaufwendig sein kann. Wenn das Programm jedoch gut entworfen und implementiert wurde, kann dieser sekundäre Prozess minimiert werden.
Der Vorgang der Komponentenimplementierung
249
Eine Programmiertechnik zur Verbesserung der Codequalität stellt die Verifizierung eines Datensatzes dar. Diese wird in Operationen zur Eigenschaftenfestlegung auf Objektebene implementiert, wie nachfolgend gezeigt wird: Private mvarexampleProperty As Long 'lokale Kopie Public Property Let exampleProperty(ByVal vData As Long) If vData > 0 Then mvarexampleProperty = vData End If End Property
Der erste Schritt bei der Funktion zur Eigenschaftenfestlegung besteht darin, über die if-Anweisung sicherzustellen, dass das Datenmitglied einen gültigen Wert enthält. Handelt es sich um einen gültigen Wert, wird der vData-Parameterwert in das lokale Datenmitglied kopiert. Der Zweck dieser Funktion zur Maskierung von Eigenschaftenzuordnungen besteht darin, deren Wert zu prüfen. Hierbei handelt es sich um eine gute Programmiermethode. Selbst in Handbüchern zur objektorientierten Programmierung wird die Verwendung der Werteprüfung empfohlen. Dies geschieht sowohl zur Sicherstellung einer gültigen Datenstruktur als auch, um den Entwickler mit der Flexibilität zum Ändern der zugrunde liegenden Datenstruktur auszustatten. In der Praxis zeigt sich jedoch, dass dieser Ansatz nicht funktioniert. Tatsächlich erfordert das Ändern der Datenstruktur innerhalb des Objekts Änderungen an den Funktionen, die mit den Objekten interagieren, da Komponenten nicht isoliert vorliegen, sondern Daten anderer Komponenten einsetzen. Nur wenn die Komponente sehr groß ist und umfangreiche Funktionalität bereitstellt, kann die objektorientierte Programmiermethode die geeignete Option darstellen.
9.1.1 Trennen von Datensammlung, Datenoperationen und Datenprüfung Eine bessere Methode zur Implementierungsverwaltung stellt das Trennen der drei grundlegenden Operationen – Datensammlung, Datenoperationen und Datenprüfung – in drei separaten Objekten dar. Die Objekte können anschließend dynamisch aneinander gebunden werden, um die Operation auszuführen. Der Vorteil bei diesem Ansatz liegt in der erhöhten Granularität, d.h. die Architektur kann nach und nach angepasst werden, um neue Ressourcentechnologien oder Operationen widerzuspiegeln. Wie in vorherigen Kapiteln gezeigt wurde, soll eine feine Granularität erzielt werden, damit die jeweiligen Bestandteile einzeln geändert werden können.
250
Implementieren von COM-Schnittstellen
Das Datenabstraktionsmuster vervollständigt diesen Ansatz. Mit diesem Muster werden die Probleme beim Ändern von Datenzugriff und Datenressourcen gelöst. Die Daten werden beispielsweise in einer Datenbank gespeichert, wobei die Datenbank objektorientiert oder relational sein kann. Unabhängig von der verwendeten Datenbanktechnologie sollen die gleichen Daten gespeichert werden, beispielsweise Adressen. Das Einbetten des Datenzugriffs in einen Objektsatz führt jedoch dazu, dass Sie niemals wissen, wie der Zugriff funktioniert. ODBC (Open Database Connectivity), ADO (Active Data Objects) und OLE DB (Object Linking and Embedding Database) funktionieren jeweils unterschiedlich. Üblicherweise versuchen Bibliotheksentwickler Abstraktionsschichten zu schreiben, die ein neutrales Format bereitstellen. Dieser Ansatz ist sinnvoll, erfordert jedoch eine ständige Feinabstimmung. Das Datenabstraktionsmuster löst dieses Problem, indem das Problem in zwei Bestandteile unterteilt wird, von denen nur ein Bestandteil optimiert wird. Das Datenabstraktionsmuster verwendet Objekte zur Datenverarbeitung sowie neutrale Datenformatstrukturen. Dieses Muster wurde zur Aufnahme der Datenverifizierung erweitert, da sich die Datenkonsistenz häufig nach dem Kontext richtet. Ein ähnliches Problem trat im Konferenzanmeldungssystem auf – nach dem Ausfüllen der Datenabstraktionsobjekte war gelegentlich eine Adresse erforderlich, manchmal nicht. Werden die Adressverifizierungsroutinen direkt an die Datenabstraktionsobjekte gebunden, ist eine spezielle Kontextauswahl nötig, d.h. die Komplexität erhöht sich. Werden die drei Operationen in einzelne Objekte gesplittet, sieht ein typisches Konsumentenszenario folgendermaßen aus: 1. Die drei Objekte werden erstellt. 2. Die Datenabstraktionsstrukturen werden ausgefüllt. 3. Eine Operationsroutine wird ausgeführt. 4. Die Operationsroutine erstellt ein Verifizierungsobjekt. 5. Das Verifizierungsobjekt wird auf den Datensatz angewendet. 6. Ist die Verifizierung erfolgreich, wird die Operation ausgeführt. 7. (Optional; für effektiveres Debuggen) Nach Ausführung der Operation kann ein weiteres Verifizierungsobjekt angewendet werden, um sicherzustellen, dass der bearbeitete Status richtig ist.
Der Vorgang der Komponentenimplementierung
251
Die Vorteile dieser Trennung sind: 왘 Das Testteam kann externe Datenprüfobjekte zum Testen der Richtigkeit ein-
zelner Operationen einsetzen. 왘 Die Datenabstraktion ermöglicht das Verschieben von Daten zwischen Opera-
tionen, ohne dass eine Neuzuweisung oder ein Kopieren der Daten erforderlich ist. 왘 Die Datenabstraktion ermöglicht die Statusspeicherung im Stream, der an-
schließend in einem verteilten Verarbeitungsszenario verwendet werden kann. 왘 Die Operationsobjekte können einzeln entworfen und ausschließlich zur Da-
tenverarbeitung eingesetzt werden.
9.2 Ein Beispielsatz Schnittstellen Kehren wir zum Beispiel der Konferenzanmeldung zurück. Der nächste Schritt besteht in der Definition der Objekte für die Benutzerbearbeitung. Der Datenbankcode soll an dieser Stelle nicht aufgezeigt werden, dieser ist in den Kapiteln zum Thema Datenbanken enthalten (Kapitel 15 bis 17). Der Hauptschwerpunkt liegt in der Implementierung der verschiedenen Schnittstellen unter Verwendung verschiedener Sprachen. Es sind drei Schnittstellensätze vorhanden, die in einer IDLBasistypbibliothek (Interface Definition Language) definiert werden: 왘 ISimpleData repräsentiert einen einfachen Datensatz. 왘 ISimpleOperations repräsentiert die Operationen für die ISimpleData-COM-
Klasse. 왘 IVerification repräsentiert die Statusverifizierung der ISimpleData-COM-Klasse.
Für ISimpleData wird die COM-Schnittstelle mit folgendem IDL-Abschnitt definiert: interface ISimpleData : IDispatch { [propget, id(1)] HRESULT username([out, retval] BSTR *pVal); [propput, id(1)] HRESULT username([in] BSTR newVal); [propget, id(2)] HRESULT password([out, retval] BSTR *pVal); [propput, id(2)] HRESULT password([in] BSTR newVal); [propget, id(3)] HRESULT userId([out, retval] long *pVal); [propput, id(3)] HRESULT userId([in] long newVal); };
Diese Schnittstelle ist ein Datenobjekt; im Objekt sollten sich keine Methoden befinden.
Diese Schnittstelle weist drei Methoden (addUser, deleteUser, findUser) und eine Eigenschaft (simpleData) auf. Die Methoden werden für die ISimpleDataCOM-Klasse ausgeführt, der Eigenschaft simpleData. Wie durch den IDL-Abschnitt gezeigt, werden für die simpleData-Eigenschaft sowohl Lese- als auch Schreibvorgänge durchgeführt (propput und propget). Dies ist nötig, da der Datensatz in weiteren Operationsklassen verwendet werden könnte. Die Übergabe des ISimpleData-Schnittstellenzeigers an die verschiedenen Klassen ist effizienter als das Verschieben der Parameter. Über die addUser-Methode wird basierend auf den Daten in der simpleData-Eigenschaft ein Benutzer hinzugefügt. Wenn alles in Ordnung ist, wird als Rückgabewert die userId zurückgegeben. Warum wird die userId zurückgegeben? Zur Erhaltung der Einfachheit – das Extrahieren der userId aus der Struktur ist einfach, erfordert jedoch einen weiteren Programmierschritt. Die findUser-Methode unterscheidet sich von addUser darin, dass das Suchkriterium (userId) als Parameter, nicht als Teil der simpleData-Eigenschaft, übergeben wird. Auch dieser Schritt erfolgt im Rahmen einer vereinfachten Anwendung. Ist die Suche erfolgreich, wird BOOL TRUE zurückgegeben, andernfalls lautet der Rückgabewert BOOL FALSE. Die dritte Methode, deleteUser, löscht den Benutzer wie durch die Eigenschaft simpleData angegeben. Es ist wichtig zu verstehen, dass die deleteUser-Methode nicht nur dazu führt, dass der Schnittstellenzeiger ISimpleData gelöscht wird, sondern dass auch der Benutzer aus der Datenbank entfernt wird. Beachten Sie, dass diese Methode userId nicht als Parameter aufweist. Dies ergibt sich aus der Annahme, dass der zu löschende Benutzer bereits gefunden wurde. Wenn Sie über beide Optionen verfügen möchten, weisen Sie der deleteUserMethode einen optionalen Parameter zu. Wenn Sie wissen möchten, wie derartige optionale Parameter definiert werden, sehen Sie sich die IDL-Parameterattribute in Kapitel 8 an.
Ein Beispielsatz Schnittstellen
253
Die IVerification-Schnittstelle ist folgendermaßen definiert: interface IVerification : IDispatch { [id(1)] HRESULT doVerification(IDispatch *pDisp); [propget, id(2)] HRESULT isOk([out, retval] BOOL *pVal); [propget, id(3)] HRESULT context([out, retval] long *pVal); [propput, id(3)] HRESULT context([in] long newVal); };
Die Schnittstelle weist eine Methode auf, doVerification. Diese Methode erwartet als Parameter das Objekt, für das die Verifizierung durchgeführt wird. Im vorliegenden Fall handelt es sich bei diesem Objekt um eine Implementierung von ISimpleData. Über die Eigenschaft isOk wird geprüft, wie die Verifizierung durchgeführt wird. Lautet der Rückgabewert TRUE, ist alles in Ordnung. Die Eigenschaft context ist eine zusätzliche Eigenschaft, mit der der Verifizierungskontext definiert wird. Dieser Kontext gibt an, welche Prüfroutinen aktiviert werden sollten. Diese Eigenschaft ist optional und wird zu Demonstrationszwecken hinzugefügt. Gegebenenfalls können Sie Eigenschaften und Methoden hinzufügen, mit denen die verschiedenen Fehler extrahiert werden, wenn für isOk der Wert FALSE zurückgegeben wird.
9.2.1 Erweitern der Schnittstellen mit mehreren Datenund Operationsklassen Die hier aufgeführten Schnittstellen sind einfach. In Ihren Szenarios sind wahrscheinlich mehrere Daten-, Operations- und Verifizierungsobjekte vorhanden. Ich möchte hier einfache Szenarios zeigen, um Ihnen eine allgemeine Vorgehensweise an die Hand zu geben. Datenobjekte können Verweise auf andere Datenobjekte und Überordnungs/Unterordnungs-Beziehungen mit mehreren COM-Objekten aufweisen. Ein Datenobjekt enthält niemals Code, Objekte zur Verweisprüfung oder Operationsobjekte. Datenobjekte sollten einfach sein und die Daten Ihres System repräsentieren. Ein Operationsobjekt kann verschiedene Datenobjekte referenzieren. Im Kontext eines Methodenaufrufs dagegen sollte beim Aufruf der Methode nur ein Verifizierungsobjekt referenziert werden. Das Prüfobjekt wird dynamisch erstellt, ihm sollten alle erforderlichen Informationen übermittelt werden. Eine typische Verifizierungsobjektimplementierung referenziert weder ein Operationsobjekt noch werden intern Datenobjekte gespeichert.
254
Implementieren von COM-Schnittstellen
9.2.2 Registrieren der Typbibliothek Die definierte IDL kann in eine Typbibliothek kompiliert werden. Wenn die Schnittstelle jedoch in einer COM-Produktionsumgebung verwendet werden soll, würde sie nicht funktionieren, da die in der IDL definierten Schnittstellen noch nicht in der Registrierung registriert wurden. Hierzu muss die Typbibliothek registriert werden. Das Registrieren einer Typbibliothek ist nicht vergleichbar mit der Registrierung eines ausführbaren Programms oder einer DLL (Dynamic Link Library), da die Registrierungsroutinen in das ausführbare Programm oder die DLL eingebettet sind. Zur Registrierung einer Typbibliothek wird die Win32-API-Funktion (Application Programming Interface) LoadTypeLibEx verwendet. Es gibt ein kleines Dienstprogramm, mit dem eine Typbibliothek über die Befehlszeile und unter Verwendung der Funktion LoadTypeLibEx registriert werden kann. Sie finden dieses Dienstprogramm im Quellcode zu diesem Buch, im Verzeichnis RegInterface.
9.2.3 Implementieren der Schnittstellen mit Visual Basic Der nächste Schritt besteht darin, die zuvor definierten Schnittstellen zu implementieren. Wir verwenden zunächst Visual Basic als Programmiersprache. Starten Sie hierzu Visual Basic, erstellen Sie ein ActiveX-DLL-Projekt, und referenzieren Sie die Typbibliothek. Sie referenzieren eine Typbibliothek, indem Sie Projekt -> Verweise auswählen. Durchsuchen Sie anschließend die Liste nach COMPONENTLib 1.0 Type Library. Dieser Bibliotheksname wird der Typbibliothek in der IDL-Datei gegeben. Wählen Sie den Namen aus, und klicken Sie auf OK. Nach Referenzierung der Typbibliothek kann diese im Objektbrowser angezeigt werden, wie dargestellt in Abbildung 9.1. Erstellen Sie als Nächstes ein neues Klassenobjekt, und geben Sie diesem einen Namen, beispielsweise clsImplData. Es soll die COM-Datenschnittstelle ISimpleData implementiert werden.
Ein Beispielsatz Schnittstellen
255
Abbildung 9.1 Objektbrowser mit Komponentendefinition
Verwenden Sie zur Implementierung einer COM-Schnittstelle das Visual BasicSchlüsselwort implements. Das implements-Schlüsselwort kann zusammen mit einer beliebigen COM-Typbibliothek verwendet werden, die im Projekt referenziert wird. Fügen Sie oben in der Klassendatei der Visual Basic-Klasse die folgende Zeile ein: Implements DataComponent
Durch diese Zeile wird der Visual Basic-Compiler angewiesen, die DataComponent- oder ISimpleData-Schnittstelle zu implementieren. Der Name DataComponent ist die coclass, die in der IDL-Datei definiert wurde. Wenn Sie versuchen, zum jetzigen Zeitpunkt eine Kompilierung vorzunehmen, treten Fehler auf, da die ISimpleData-Eigenschaften noch nicht implementiert wurden. Die Implementierung lautet folgendermaßen: Private myPassword As String Private myUserId As Long Private myUsername As String Private Property Let DataComponent_password(ByVal RHS As String) myPassword = RHS End Property Private Property Get DataComponent_password() As String DataComponent_password = RHS End Property Private Property Let DataComponent_userId(ByVal RHS As Long) myUserId = RHS End Property
256
Implementieren von COM-Schnittstellen
Private Property Get DataComponent_userId() As Long DataComponent_userId = myUserId End Property Private Property Let DataComponent_username(ByVal RHS As String) myUsername = RHS End Property Private Property Get DataComponent_username() As String DataComponent_username = myUsername End Property
Es sind drei private Variablen vorhanden, myPassword, myUsername und myUserId. In diesen Variablen werden die Eigenschaftenwerte gespeichert. Die Eigenschaftenimplementierungen sorgen lediglich dafür, dass die Parameter an die privaten Variablen übermittelt werden und umgekehrt. Die Falle Die Implementierung sollte funktionieren, nach der Kompilierung wird in der IDL jedoch folgende Ausgabe generiert: coclass clsImplData { [default] interface _clsImplData; interface ISimpleData; };
Laut IDL lautet die standardmäßige IDispatch-Schnittstelle _clsImplData. Dies ist weniger gut, da die Schnittstelle keine Eigenschaften von ISimpleData enthält, d.h. jeder Client mit Automatisierung (späte Bindung) kann auf die Eigenschaften und Methoden nicht zugreifen. Zur Lösung dieses Problems fügen Sie eine Mitgliedsfunktion hinzu, mit der ein Schnittstellencasting durchgeführt wird. Die Beispielmethode lautet folgendermaßen: Public Function getOther() As DataComponent Set getOther = Me End Function
Auf diese Weise wird ein QueryInterface-Aufruf zum Abruf der DataComponentSchnittstelle erzwungen. Diese Schnittstelle kann anschließend unter Verwendung der IDispatch-Methode zum Aufrufen der verschiedenen Eigenschaften und Methoden eingesetzt werden. Diese Methode funktioniert jedoch nur in einigen Programmiersprachen. Zum Zeitpunkt der Entstehung dieses Buches konnte
Ein Beispielsatz Schnittstellen
257
dieses Verfahren mit dem Windows Scripting-Host nicht eingesetzt werden, mit Visual Basic für Anwendungen war die Verwendung möglich. Wenn Sie unabhängig von der Programmiersprache auf die Eigenschaften zugreifen möchten, müssen Sie Visual Basic-Eigenschaften hinzufügen, die sich wie die ISimpleData-Umgebung verhalten. Dies gilt ebenso für Methoden. Sehen Sie sich beispielsweise die folgenden ISimpleData-Eigenschaften an: Private Property Let DataComponent_userId(ByVal RHS As Long) myUserId = RHS End Property Private Property Get DataComponent_userId() As Long DataComponent_userId = myUserId End Property
Zur Offenlegung dieser Methoden als Eigenschaften muss der Datei clsImplData der folgende Code hinzugefügt werden: Public Property Let userId(ByVal RHS As Long) myUserId = RHS End Property Public Property Get userId() As Long DataComponent_userId = myUserId End Property
Dieses Vorgehen führt zur Problemlösung, da die Methoden als Teil der _clsImplData-Schnittstelle offen gelegt werden, und zu genau dieser Schnittstelle wird IDispatch geroutet. Dies bedeutet zusätzlichen Aufwand, und Sie fragen sich vielleicht, weshalb eine IDL-Datei geschrieben werden muss. Bei umfangreicheren Anwendungen kann die ausschließliche Verwendung von Visual Basic jedoch zu Problemen führen. Wenn die COM-Bibliotheken nur an einer Stelle nicht richtig verwaltet werden, muss jedes Mal eine Neukompilierung vorgenommen werden. IDL-Dateien sind dagegen »idiotensicher«.
9.2.4 Implementieren der Schnittstellen mit Visual J++ Zur Implementierung der Schnittstellen mit Visual J++ müssen Sie ein ActiveXDLL-Projekt erstellen. Hierbei wird ein Projekt mit einer Klasse erstellt, die in SimpleImplementation.java umbenannt werden muss. Wie in Visual Basic muss die Typbibliothek COMPONENTLib referenziert werden. Der Unterschied bei Visual J++ besteht darin, dass eine Reihe von Stubs und Wrappern erzeugt wird. Wählen Sie hierzu die Menüpunkte Projekt -> COM-Wrapper hinzufügen aus. Wählen Sie anschließend in der Liste COMPONENTLib 1.0 Type Lib. Die
258
Implementieren von COM-Schnittstellen
Stubs werden generiert und dem Projekt als Unterordner mit dem Namen der COM-Komponente hinzugefügt, wie dargestellt in Abbildung 9.2.
Abbildung 9.2 Visual J++-Projekt mit generierten Stubs
Im Stubverzeichnis componentdefinition befinden sich sechs Java-Klassendateien. Wenn Visual J++ die COM-Stubs erstellt, wird sowohl der coclass-Stub als auch der COM-Schnittstellenstub erzeugt. Es sind die Elemente DataComponent, die coclass-basierte Klasse und ISimpleData, die interface-basierte Schnittstelle, vorhanden. Welches dieser Elemente Sie einsetzen, richtet sich nach dem jeweiligen Szenario. Bei der Implementierung einer COM-Schnittstelle erbt die Java-Klassendatei von der ISimpleData-Java-Schnittstelle. Wenn Sie eine JavaKlasse schreiben, die ein COM-Objekt verwendet, deklarieren Sie eine JavaKlasse und instanziieren diese. Es ist möglich, für die instanziierte Klasse einen Typecast auf eine spezifische Schnittstelle durchzuführen. Da eine Implementierung geschrieben wird, sollte die SimpleImplementationVJKlasse folgendermaßen implementiert werden: import com.ms.com.*; import componentdefinition.ISimpleData; public class SimpleImplementationVJ implements IUnknown, componentdefinition.ISimpleData { … }
Bei der Implementierung der COM-Schnittstellen müssen die COM-Basisklassen aus den Java-basierten com.ms.*-Paketen importiert werden. Auf diese Weise
Ein Beispielsatz Schnittstellen
259
wird das COM-Framework bereitgestellt, das in der Implementierung verwendet wird. Es muss auch die Schnittstelle componentdefinition.ISimpleData importiert werden. Diese Schnittstelle soll implementiert werden. Der Deklaration der SimpleImplementationVJ-Klasse nach zu urteilen, müssen die COM-Schnittstellen IUnknown und ISimpleData über die Klasse SimpleImplementationVJ implementiert werden. Zur Implementierung der unterschiedlichen Methoden können zwei Verfahren eingesetzt werden. Entweder können die Methoden manuell geschrieben werden, oder Sie erstellen die Implementierung mit dem Visual J++-Assistenten automatisch. Klicken Sie zur Verwendung des Assistenten in der Klassenansicht mit der rechten Maustaste auf die zu implementierenden Schnittstellen, und wählen Sie Methode hinzufügen, wie dargestellt in Abbildung 9.3.
Abbildung 9.3 Visual J++-Kontextmenü zum Hinzufügen leerer Methoden
Nachdem die Methoden hinzugefügt wurden, kann die Klasse kompiliert werden. Die Methoden für IUnknown werden der Implementierung der Java-Klasse nicht hinzugefügt, da nach der Kompilierung der Java-Klassendatei über die Java-Laufzeitumgebung eine standardmäßige IUnknown-Implementierung bereitgestellt wird. Die Java-Laufzeitumgebung bietet Standardimplementierungen für IUnknown und IDispatch. Unterstützung für Scriptinghosts Die Klasse SimpleImplementationVJ unterstützt sowohl Scriptingkonsumenten als auch Konsumenten von benutzerdefinierten Schnittstellen. Sehen wir uns für ein besseres Verständnis die generierte IDL-Datei an:
Visual J++ generiert eine Reihe von IDL-Methoden und Eigenschaftendeklarationen, über die die Schnittstelle für IDispatch-basierte Konsumenten implementiert wird. Nehmen Sie jedoch den Fall eines Visual C++-basierten Konsumenten – mit Visual C++ kann ein QueryInterface-Aufruf für die ISimpleData-Schnittstelle ausgeführt werden. Es scheint so, als könnte mit Visual J++ das Problem der gleichzeitigen Unterstützung von Scriptingkonsumenten und Konsumenten benutzerdefinierter Schnittstellen gelöst werden. Wenn jedoch zwei implementierte Schnittstellen die gleiche Methode aufweisen, wird ein Konflikt in der IDL-Datei hervorgerufen.
9.2.5 Implementieren der Schnittstellen mit Visual C++ Erstellen Sie zur Implementierung der Schnittstellen in Visual C++ ein ATL-DLLProjekt (Active Template Library) mit dem Namen SimpleProjectVC. Fügen Sie dem Projekt anschließend ein ATL-Objekt vom Typ Einfaches Objekt hinzu. Geben Sie dem Objekt den Namen ImplData. Akzeptieren Sie für die Attribute alle Standardeinstellungen, indem Sie auf OK klicken. Der nächste Schritt stellt die Kompilierung des Projekts dar. Eine Kompilierung ist erforderlich, da die Typbibliothek generiert werden muss. Wählen Sie anschließend die Klasse CImplData aus, und klicken Sie auf die rechte Maustaste. Ein Kontextmenü wird eingeblendet, wie dargestellt in Abbildung 9.4. Wählen Sie den Menübefehl Schnittstelle implementieren. Ein Dialogfeld öffnet sich, und Ihnen wird mitgeteilt, dass keine Schnittstellen vorhanden sind. Klicken Sie auf die Schaltfläche Typbibl. hinzufügen. Ein weiteres Dialogfeld wird angezeigt, dass ein Listenfeld mit allen registrierten Komponenten enthält. Suchen Sie nach COMPONENTLib 1.0 Type Library. Wählen Sie den Namen aus, und klicken Sie auf OK. Jetzt stehen im ursprünglichen Dialogfeld drei Schnittstellen zur Verfügung, ISimpleData, ISimpleOperations und IVerification.
Ein Beispielsatz Schnittstellen
261
Abbildung 9.4 Kontextmenü zur Implementierung der »ISimpleData«-Schnittstelle
Wählen Sie ISimpleData aus, und klicken Sie auf OK. Auf diese Weise wird ein Codegenerator aktiviert, der die CImplData-Klasse bearbeitet, über die ISimpleData implementiert wird. Visual C++ verwendet den so genannten COM-Compiler zur Implementierung der Schnittstelle. Der COM-Compiler ist ein Dienstprogramm, mit dem ein COM-Stub generiert wird, der in der Visual C++-Umgebung einfacher zu verwenden ist als COM-Hilfskomponenten. Der nächste Schritt besteht darin, die Methoden zu generieren, die über die ISimpleData-Schnittstelle implementiert werden müssen. Es muss hierzu eine Änderung in der IDispatchZuordnung vorgenommen werden: BEGIN_COM_MAP(CImplData) COM_INTERFACE_ENTRY(IImplData) //DEL
Die Änderung liegt im Makro COM_INTERFACE_ENTRY2. Der Codegenerator setzt den zweiten Parameter auf IImplData. Da diese Schnittstelle keine der offen zu legenden Eigenschaften enthält, muss sie in ISimpleData geändert werden.
262
Implementieren von COM-Schnittstellen
Jetzt kann das Visual C++-COM-Objekt entweder von einem Scriptingkonsumenten oder einem Konsumenten einer benutzerdefinierten Schnittstelle verwendet werden.
9.2.6 Einige abschließende Anmerkungen Sie fragen sich wahrscheinlich, welches Tool sich am besten für die Implementierung der Schnittstellen eignet. Es kommt darauf an. Wenn es sich beim Konsumenten um einen Scriptingkonsumenten handelt, sollte entweder Visual C++ oder Visual J++ verwendet werden, Visual Basic weist hier IDispatch-Probleme auf. Ist der Konsument benutzerdefiniert, spielt die Wahl des Tools keine Rolle. Aus der Sicht des Programmierers bietet Visual C++ die umfangreicheren Steuerungsmöglichkeiten hinsichtlich der Schnittstellenimplementierung. Visual J++ bietet die beste Assistentenunterstützung und eine leichte Implementierungsprogrammierung. Visual Basic ist die einfachste der genannten Programmiersprachen. Die Auswahl der Programmiersprache sollte sich danach richten, mit welcher Sprache Sie am besten arbeiten können.
9.3 Schreiben von Implementierungen In der erarbeiteten Entwicklungsstrategie wurden durch das Designerteam verschiedene COM-Schnittstellen entworfen, und diese Schnittstellen wurden den einzelnen Implementierungsteams ausgehändigt. Als Nächstes muss entschieden werden, welche Klassen als COM-Schnittstellen offen gelegt und damit als öffentlich deklariert, und welche Klassen als privat eingestuft werden sollen. Der COM-Purist könnte sagen, dass alle Operationen als COM-Schnittstellen implementiert werden sollten, und dass nichts dafür spricht, andere Elemente als COM-Schnittstellen einzusetzen. Nehmen Sie jedoch an, Sie schreiben eine COM-Komponente mit Visual C++. In Visual C++ können Vorlagen dazu eingesetzt werden, einige Programmierprobleme äußerst elegant zu lösen. Unglücklicherweise wird dieses Vorlagenkonzept von COM nicht unterstützt, d.h. wenn Sie eine reine COM-Komponente schreiben, können keine Vorlagen verwendet werden. Damit erschweren Sie sich die Programmierung in C++, wenn Sie ausschließlich COM-Schnittstellen einsetzen. Da das Designerteam entscheidet, welche Schnittstellen öffentlich verwendet werden, ist es unerheblich, ob die Implementierung ausschließlich COM-Schnittstellen oder einige sprachenspezifische Erweiterungen für die privaten Klassen verwendet. Die privaten Schnittstellen werden nicht offen gelegt, und wenn Änderungen an den privaten Schnittstellen vorgenommen werden, wirkt sich dies nicht auf die weiteren COM-Schnittstellen aus.
Schreiben von Implementierungen
263
Sie möchten jedoch möglicherweise zu einem späteren Zeitpunkt einige der privaten Funktionen als öffentliche Schnittstelle offen legen. Wenn dies wahrscheinlich ist, wäre das anfängliche Implementieren der Klassen als COM-Schnittstellen sinnvoll, denn so können Sie ermitteln, wie effizient die COM-Schnittstellen sind. Wenn Sie sich für die Verwendung von COM-Schnittstellen entschieden haben, müssen Sie darauf achten, nicht »COM-verrückt« zu werden und alles und jeden in eine COM-Schnittstelle zu verwandeln. Denken Sie daran, dass der durch die Verwendung von COM entstehende Overhead immer zu einem größeren Zeitaufwand führt als das Verwenden von systemeigenem Programmcode.
9.3.1 Verwenden von UML Das Schreiben von COM-Schnittstellenimplementierungen erfordert einigen Programmieraufwand. Zusätzlich muss der erstellte Code verwaltet, geplant und strukturiert werden. Diese Aufgabe kann in Visual Studio zwar zufriedenstellend erledigt werden, es gibt jedoch bessere Alternativen. Eine dieser Alternativen stellt das UML-Tool (Unified Modeling Language) dar, mit dem Klassendiagramme erstellt werden können. In den vorangegangenen Kapiteln wurde UML zur Definition der Anwendungsfälle, der Sequenz- und Kollaborationsdiagramme eingesetzt. Jetzt verwenden Sie das UML-Tool zum Erstellen eines Klassendiagrammsatzes. Seien Sie jedoch gewarnt. Einige Programmierer setzen UML zur Entwicklung der gesamten Anwendung ein. Dies ist einfach, da mit dem UML-Tool der Code automatisch generiert wird. Andere Programmierer lieben es, viele Diagramme zu zeichnen, was mit UML ebenfalls sehr leicht fällt. Das direkte Konvertieren dieser UML-Klassendiagramme in Programmcode führt jedoch zu einer stark aufgeblähten Anwendung. UML sollte dazu dienen, eine Implementierung verständlich zu machen. In einem UML-Diagramm können die Kernpunkte des Entwurfs festgehalten und die Architektur der einzelnen Komponenten veranschaulicht werden. Es gibt verschiedene UML-Tools, die zusammen mit Visual Studio verwendet werden können. Wenn Sie eine Programmierung mit Visual Basic planen, kann die Visual Studio-Anwendung Visual Modeler ausreichen. Der Visual Modeler unterstützt keine Anwendungsfälle, Kollaborations- oder Sequenzdiagramme. Wenn Sie Komponenten anhand von Visual C++ schreiben möchten, können Visual Case von Rogue Wave oder Rational Rose die beste Lösung darstellen. Besonders leistungsfähig und multifunktional sind Rational Rose und MicroGold. Achten Sie bei einem UML-Tool auf Folgendes:
264
Implementieren von COM-Schnittstellen
왘 UML 1.2-Fähigkeiten (einschließlich Anwendungsfällen, Kollaborations- und
Sequenzdiagrammen) 왘 Roundtrip-Engineering, d.h. bei Erstellung von Klassen im UML- oder Entwick-
lungstool sollten diese Änderungen sowohl im UML- als auch im Entwicklungstool reflektiert werden. 왘 Integrierte Versionssteuerung zum leichteren Verfolgen von Änderungen. 왘 Unterstützung der Teamentwicklung, d.h. es kann in Teams mit mehreren Be-
nutzern gearbeitet werden. 왘 Dokumentationserstellung, d.h. auf Grundlage der angegebenen Beschreibung
sollte automatisch eine Dokumentation generiert werden können. Die Integration mit dem Web oder einem Textverarbeitungsprogramm ist vorteilhaft.
9.3.2 Schreiben von Komponenten mit Hilfe von UML Das Schreiben von Anwendungsimplementierungen erfordert das Definieren von COM-Schnittstellen durch das Designerteam. Hierzu sollte kein UML-Tool eingesetzt werden. Die derzeitige COM-Unterstützung ist (gelinde gesagt) nicht die beste. COM-Schnittstellen können auch ohne UML-Tool einfach implementiert werden, außerdem lernen Sie so Ihre Komponentenimplementierung besser kennen. Verwenden Sie die mit den Programmiertools bereitgestellten Assistenten, um die Basisimplementierungen für alle COM-Schnittstellen zu erstellen. Ich persönlich bevorzuge bei der Implementierung der COM-Schnittstellen eine Namenskonvention, die das vorangestellte I des Schnittstellennamens streicht und an das Ende des Namens das Suffix Impl anfügt. Das Impl am Ende des Namens steht hierbei für Implementierung. Wenn die COM-Schnittstelle beispielsweise ISimpleData lautet, erhält die COM-Implementierungskomponente den Namen SimpleDataImpl. Nachdem die COM-Basisimplementierungen definiert wurden, wird das Projekt anhand des Assistenten für das Reverse-Engineering in ein UML-Projekt konvertiert. Dieser Assistent steht bei den meisten UML-Tools zur Verfügung. Ein Beispiel wird in Abbildung 9.5 gezeigt. Es sind drei Klassen vorhanden, mit denen die einzelnen Implementierungsklassen repräsentiert werden. In UML wird eine Klasse durch ein Feld repräsentiert, der Begriff »Klasse« ist jedoch sehr allgemein gefasst. Sie können in UML die unterschiedlichen Klassentypen durch Stereotypen identifizieren. Ein Stereotyp ist eine Metaklassifizierung eines Elements. Alle UML-Tools bieten einige Standardstereotypen, Sie können jedoch auch eigene Stereotypen definieren. In Abbildung 9.5 gibt das >-Tag oben in der Klasse das Stereotyp an.
Schreiben von Implementierungen
265
Abbildung 9.5 Anfängliche Klassenstruktur nach dem Reverse-Engineering
Das Diagramm ist unvollständig, da durch die verschiedenen Klassen Schnittstellen implementiert werden. Mit UML kann eine Schnittstelle als ein anderer Stereotyp der Klasse definiert werden, die UML-Tools können jedoch nicht wirklich COM-Schnittstellen erstellen. Diese müssen manuell hinzugefügt werden. In Abbildung 9.6 wird als Beispiel das Hinzufügen der Schnittstelle DataComponent/ ISimpleData veranschaulicht.
Abbildung 9.6 Schnittstellendefinition und Generalisierung derselben in »clsDataImpl«
In Abbildung 9.6 werden die Schnittstelle DataComponent und die Klasse clsDataImpl durch einen Pfeil miteinander verbunden. Diese Pfeilbeziehung wird als Generalisierung bezeichnet. Eine Generalisierung kann auf eine Vererbung oder eine Implementierung hinweisen. Da es sich bei einer Klasse um eine Schnittstelle, bei der anderen um eine Implementierung handelt, wird die Generalisierung als Implementierungsgeneralisierung bezeichnet. Die Implementierungsgeneralisierung ist der einzige Generalisierungstyp, der in Visual Basic und COM
266
Implementieren von COM-Schnittstellen
eingesetzt werden kann. Visual C++ und Visual J++ erkennen auch Vererbungsgeneralisierungen. Die Operationsimplementierungen verweisen auf verschiedene Datenimplementierungen. Als jedoch das Reverse-Engineering für das Projekt durchgeführt wurde, wurden diese Verbindungen nicht berücksichtigt, da die verschiedenen Implementierungen ohne Inhalt sind und keine wichtigen Informationen enthalten. Zunächst muss eine Verknüpfung zwischen clsDataImpl und clsOperateImpl erstellt werden. Die clsOperationImpl-Implementierung legt die Eigenschaft simpleData offen, die von der Schnittstelle OperationsComponent/ISimpleOperations::simpleData stammt. Die Schnittstelle DataComponent/ISimpleData muss referenziert werden, denn darauf verweist die Eigenschaft simpleData. Das UML-Diagramm zu diesen Beziehungen ist in Abbildung 9.7 dargestellt.
Abbildung 9.7 UML-Verknüpfungen zwischen Implementierung und Schnittstelle
Die Verknüpfung zwischen clsOperateImpl und DataComponent wird als einseitige Verknüpfung bezeichnet, da die Navigation nur in eine Richtung erfolgt; Navigation bezeichnet die Fähigkeit zum Wechseln zwischen Objektreferenzen. Bei dieser Verknüpfung kann von clsOperateImpl zu DataComponent gewechselt werden, nicht jedoch von DataComponent zu clsOperateImpl. Eine zweiseitige Verknüpfung ermöglicht eine Navigation zwischen beiden Komponenten. Im Ver-
Schreiben von Implementierungen
267
gleich zu verknüpften Listen stellen einseitige Verknüpfungen einzeln verknüpfte Listen, zweiseitige Verknüpfungen doppelt verknüpfte Listen dar. In der Beispielverknüpfung ist eine private-Referenz zu der Schnittstelle innerhalb der clsOperateImpl-Klasse myData vorhanden. Sie können anhand des Minuszeichens vor der myData-Variable erkennen, dass es sich um eine private-Verknüpfung handelt. Stünde hier ein Pluszeichen, würde es sich um eine öffentliche Verknüpfung handeln. Eine Hashmarke gibt an, dass die Variable geschützt ist, ist keine Marke vorhanden, deutet dies auf eine Implementierung hin. Verknüpfungen sind Deklarationen auf Klassenebene, die durch alle Visual Studio-Programmiersprachen unterstützt werden. In der Implementierung der clsOperateImpl-Klasse wurde das Verifizierungsobjekt instanziiert. Es ist sinnvoll, eine Referenz zwischen Schnittstelle und clsOperateImpl-Klasse einzurichten. In der UML-Terminologie wird dies als Abhängigkeit bezeichnet und durch einen gestrichelten Pfeil gekennzeichnet, siehe Abbildung 9.8.
Abbildung 9.8 UML-Abhängigkeit zwischen »VerficationComponent« und »clsImplOperate«
Die Abhängigkeit ist notwendig, da so bei der Codegenerierung die geeigneten COM-Schnittstellenverweise oder include-Anweisungen eingefügt werden. Von diesem Zeitpunkt an kann das UML-Diagramm in Quellcode kompiliert werden, und Sie können Ihre COM-Komponenten implementieren.
268
Implementieren von COM-Schnittstellen
Sie verfügen nun über die benötigte UML-Basisdatei. Jedes UML-Tool weist eigene Fähigkeiten auf. Rational Rose beispielsweise besitzt die Fähigkeit, Eigenschaften aus Verknüpfungen zu erstellen. Dies ist ein gutes Feature, aber bedenken Sie, dass der Zweck von UML in der Bereitstellung einer Gesamtarchitektur für die COM-Komponente besteht, daher wird eine umfangreiche Unterstützung der Grundfunktionen benötigt. Darüber hinaus ist Leistungsfähigkeit im Hinblick auf die Codegenerierung und das Reverse-Engineering gefragt. Andernfalls kann der Code sehr unübersichtlich und hässlich werden. Bei der Entwicklung im Team kann es wünschenswert sein, die verschiedenen UML-Diagramme in einem größeren UML-Teamdiagramm zu kombinieren. UMLTools ermöglichen Ihnen dies durch das Definieren von Komponenten. Mit Komponenten können Sie Implementierungen referenzieren, die die Implementierungsdetails enthalten.
9.4 Verwenden der COM-Klassen Beim Entwickeln von Implementierungen setzen Sie andere COM-Schnittstellen ein, da Ihre Architektur in Schichten strukturiert ist und von der Implementierung anderer COM-Schnittstellen abhängt. Bei Verwendung anderer COM-Schnittstellen in einer Windows DNA-COM+-Anwendung müssen Sie nicht berücksichtigen, wie die verwendete COM-Schnittstelle implementiert wurde. Dies trifft nur auf Windows DNA-COM+-Anwendungen zu. Wenn Sie jedoch das Multithreading einsetzen, können bezüglich der Verwendung der COM-Schnittstelle einige Beschränkungen gelten. Jede Programmiersprache weist eigene Beschränkungen auf. Diese Einschränkungen im Einzelnen zu erläutern, würde den Rahmen dieses Buches sprengen, da hier nur Windows DNA-Anwendungen behandelt werden.
9.4.1 Einsatz von COM-Klassen mit Visual Basic Beim Schreiben einer Visual Basic-Implementierung erscheinen COM-Objekte wie Visual Basic-Objekte. Zwischen diesen beiden wird nicht unterschieden. Verwenden einer benutzerdefinierten Schnittstelle Wenn Sie in Visual Basic ein COM-Objekt referenzieren, referenzieren Sie die Schnittstelle des COM-Objekts. Sie referenzieren die tatsächliche Implementierung des COM-Objekts nicht. Dies ist ein wichtiger Unterschied. Ich erwähne dies, da viele COM-Entwickler die Definition der Schnittstelle und die Implementierung der Schnittstelle im selben Objekt platzieren.
Verwenden der COM-Klassen
269
Zum besseren Verständnis dieser Unterscheidung soll als Beispiel die Verwendung des Verifizierungsobjekts herangezogen werden. Bei jedem Methodenaufruf muss das Verifizierungsobjekt instanziiert und aufgerufen werden. Das Verifizierungsobjekt stellt sicher, dass alle Parameter gültig und richtig sind. Der Beispielquellcode der Operationsmethode lautet folgendermaßen: Private Function OperationsComponent_addUser() As Long Dim tempVerification As VerificationComponent Set tempVerification = New clsVerifyImpl tempVerification.context = 1 tempVerification.doVerification (myData) If tempVerification.isOk = False Then 'Error cannot proceed Else ' Do the operations that need to be done End If Set tempVerification = Nothing End Function
Das Objekt tempVerification wird unter Verwendung der Dim-Anweisung als VerificationComponent-Objekt deklariert. Dies ist ein Beispiel für das Deklarieren einer Schnittstelle, die nicht mit einer Objektimplementierung definiert wurde. Da eine spezifische Implementierung mit der Variablen verknüpft werden soll, kann in der Variablendeklaration kein neues Schlüsselwort verwendet werden. Zur Zuweisung einer spezifischen COM-Implementierung zur Schnittstelle wird über die folgende Anweisung, New clsVerifyImpl, das Objekt instanziiert, und es wird ein QueryInterface-Aufruf für die Schnittstelle VerificationComponent durchgeführt. Wurde über clsVerifyImpl nicht die VerificationComponentSchnittstelle implementiert, liegt clsVerifyImpl weiterhin instanziiert vor, würde jedoch zerstört, wenn der QueryInterface-Aufruf fehlschlägt. Als Ergebnis muss die Variable tempVerification auf nothing gesetzt werden. Nach Instanziierung des tempVerification-Objekts kann die Methode VerificationComponent aufgerufen werden. Der erste Schritt bei der Verifizierung besteht darin, den Kontext festzulegen, der hier den Wert 1 trägt. Anschließend wird die Methode doVerification aufgerufen. Zu diesem Zeitpunkt untersucht die Implementierung von VerificationComponent den Objektstatus und stellt sicher, dass alles in Ordnung ist. Hierzu wird die Eigenschaft isOk geprüft. Lautet der Wert für diese Eigenschaft false, ist ein Fehler aufgetreten, und die Operation kann nicht
270
Implementieren von COM-Schnittstellen
fortgesetzt werden. Der Wert true ermöglicht, dass die Operation fortgeführt wird. Verwenden von »IDispatch«-Schnittstellen Die zweite Möglichkeit zur Objektinstanziierung ist das Verwenden von CreateObject. Ein Beispiel zur Verwendung von CreateObject anstelle der New-Funktion sehen Sie nachstehend: Set tempVerification = CreateObject( "SimpleImplementationVB.clsImplVerify")
Durch CreateObject wird exakt das Gleiche erreicht wie mit New, es wird jedoch explizit COM verwendet. In der Vergangenheit wurde diese Methode häufig empfohlen, da durch das Verwenden von New bestimmte COM-Routinen umgangen werden können. Da es sich bei tempVerification jedoch weiterhin um eine spezifische Schnittstelle handelt, wird auch weiterhin ein QueryInterfaceAufruf für die Schnittstelle durchgeführt. Zur Verwendung der IDispatch-Schnittstelle muss die Deklaration des tempVerification-Objekts folgendermaßen aussehen: Dim tempVerification as Object
In diesem Fall lautet der Objekttyp Object, ein IDispatch-Objekt. Wenn die gleichen Methodenaufrufe ausgeführt werden, werden diese unter Verwendung der IDispatch-Schnittstelle geroutet. Arbeiten mit benutzerdefinierten und »IDispatch«-Schnittstellen Würde für das tempVerification-Objekt der Datentyp Object beibehalten, würden alle Aufrufe zu einem Fehler führen. Diese Fehler resultieren aus einem zuvor besprochenen Problem – Visual Basic-Objekte leiten die IDispatch-Schnittstelle von den öffentlich deklarierten Klassenmethoden und -eigenschaften ab. Es gibt in Visual Basic jedoch ein Verfahren zur Verwendung der IDispatch-Schnittstelle der gewünschten Schnittstelle. Sehen Sie sich folgenden Beispielcode an: Dim genericObject As Object Dim typeVerification As VerificationComponent Dim usableObject As Object Set genericObject = CreateObject("SimpleImplementationVB.clsVerifyImpl") Set typeVerification = genericObject Set usableObject = typeVerification usableObject.context = 1
Verwenden der COM-Klassen
271
Sehen Sie sich die Zeile an, in der das genericObject zugewiesen wird, nachdem das Objekt mit Hilfe von CreateObject instanziiert wurde. Da genericObject den Typ Object aufweist, stammt die enthaltene IDispatch-Schnittstelle von clsVerifyImpl. Der nächste Schritt besteht darin, anhand der typeVerification-Zuordnung einen QueryInterface-Aufruf für die Schnittstelle VerificationComponent durchzuführen. Nun enthält typeVerification einen Verweis auf die VerificationComponent-Schnittstelle. Bei Zuordnung von usableObject wird ein QueryInterface-Aufruf für die IDispatch-Schnittstelle durchgeführt. Hierbei stammt die IDispatch-Schnittstelle jedoch von VerificationComponent, da diese die letzte benutzerdefinierte Schnittstelle darstellt. Wenn Sie nun die Eigenschaft usableObject.context zuweisen, funktioniert das Ganze. Das von Visual Basic in einen QueryInterface-Aufruf umgewandelte Typecasting mag kompliziert erscheinen. Wenn Sie jedoch mit COM-Klassen arbeiten, die mehrere Schnittstellen implementieren, müssen Sie spezifische Schnittstellen extrahieren. Diese Methode ermöglicht das Implementieren eines Polymorphismus.
9.4.2 Einsatz von COM-Klassen mit Visual J++ Die einfachste und benutzerfreundlichste Sprache zum Verwenden von COMSchnittstellen stellt Visual J++ dar. Für den Einsatz einer COM-Schnittstelle müssen lediglich die verschiedenen COM-Wrapper der Komponenten COMPONENTLib und (in unserem Fall) der Visual Basic-Bibliothek SIMPLEIMPLEMENTATIONVB generiert werden. Der folgende Code ist eine Visual J++-Version des Verifizierungscode, der im Abschnitt zu Visual Basic aufgeführt wurde: import componentdefinition.*; import simpleimplementationvb.*; … IVerification tVerification; tVerification = new clsImplVerify(); tVerification.doVerification( objData); tVerification.setContext( 1); if( tVerification.getIsOk() != 0) { // Keine Aktion } else { // Fehlerbehandlung } }
272
Implementieren von COM-Schnittstellen
Der erste Schritt besteht darin, dass IVerification-Schnittstellenobjekt (tVerification) zu deklarieren. Zur Zuordnung einer COM-Objektinstanz wird das clsImplVerify-Objekt instanziiert. Da dieses jedoch tVerification zugeordnet wird, wird ein Typecast durchgeführt. Hierbei handelt es sich um ein Java-Standardverfahren. Hinzu kommt jedoch, dass die Microsoft Java-VM (Virtual Machine) erkennt, dass es sich um eine COM-Schnittstelle handelt, und es wird ein QueryInterfaceAufruf für IVerification ausgeführt. Von jetzt an kann die Schnittstelleninstanz tVerification verwendet werden. Verwenden von »IDispatch«-Schnittstellen In der Visual J++-Umgebung wird davon ausgegangen, dass alle Schnittstellen über verknüpfte Typbibliotheken verfügen. Wenn der COM-Wrapper ausgeführt wird, wird nach der Typbibliothek gesucht, damit der richtige Stub generiert werden kann. Ist keine Typbibliothek vorhanden und der Aufruf muss unter Verwendung von IDispatch ausgeführt werden, wird die Sache ein wenig hässlich. Im Visual J++-Paket ist eine Klasse namens com.ms.com.Dispatch vorhanden. Es handelt sich hierbei um eine Klasse mit vielen unterschiedlichen Methoden. Durch das Verwenden dieser Methoden können IDispatch-basierte Implementierungen aufgerufen werden. Es ist nicht erforderlich, eine Struktur zu füllen, es müssen jedoch verschiedene Methoden mit unterschiedlicher Parameterzahl aufgerufen werden. Dieses Verfahren funktioniert, führt jedoch zu sehr unübersichtlichem Code. Wenn es erforderlich ist, die Dispatch-Klasse zu verwenden, sollten Sie eine Stub-Klasse erstellen, mit der die Funktionalität gekapselt wird. So ist die Codeverwaltung sehr viel einfacher. Zur Verwendung von COM-Implementierungen sind keine weiteren Informationen erforderlich. Es handelt sich mit Abstand um den einfachsten und benutzerfreundlichsten Ansatz.
9.4.3 Einsatz von COM-Klassen mit Visual C++ Abschließend soll die Verwendung von COM-Klassen mit Visual C++ besprochen werden. Früher war das Verwenden von COM-Implementierungen mit Visual C++ nicht besonders erfreulich. Häufig führte dieses Vorgehen zu interessanten Debugsitzungen. Mit Visual C++ 5.0 und dem COM-Compiler ist dies jedoch anders geworden. Durch den COM-Compiler ist es möglich geworden, unter Verwendung einer einfachen Notation COM-Klassen zu referenzieren und zu verwenden. Der COMCompiler ähnelt einem Vorprozessor, der bei der Kompilierung von C++-Code ausgeführt wird. Durch das Angeben einer spezifischen Typbibliothek werden die enthaltenen COM-Klassen gelesen. Auf Grundlage der Typbibliothekinformatio-
Verwenden der COM-Klassen
273
nen wird eine Reihe von C++-Klassen generiert. Dieser Vorgang ähnelt dem von Visual J++, er ist lediglich auf Visual C++ zugeschnitten. Die Verwendung des COM-Compilers hat den Vorteil, dass eine umfassende Integration in C++ erfolgt, und dass der Programmcode nicht als COM-Hilfscode vorliegt. Es folgt ein Beispiel zur Verwendung des COM-Compilers: #import "file" attributes #import attributes
Die Verwendung von import mit Anführungszeichen oder Klammern entspricht den Regeln zum Einschluss einer Datei in C oder C++. Die angegebene Datei muss Typbibliothekinformationen enthalten. Eine Beispielimplementierung für die Verifizierungsroutine lautet folgendermaßen: COMPONENTLib::IVerificationPtr ptr( "SimpleImplementationVB.clsImplVerify"); ptr->doVerification( ptrData); if( ptr->GetisOk() == FALSE) { // Fehlerbehandlung } else { // Ausführen einer Aktion }
In diesem Codebeispiel stellt COMPONENTLib einen C++-Namespace dar, der die drei zuvor definierten Schnittstellen enthält. Für uns ist hierbei die Schnittstelle IVerification von Interesse. Die Schnittstelle IVerification kann jedoch nicht direkt verwendet werden; stattdessen muss die Klassendefinition mit angehängtem Ptr eingesetzt werden. (Hierzu später mehr.) Anschließend wird das ptr-Objekt deklariert. Als Teil der Erstellungsroutine wird die PROG-ID der Implementierung angegeben, über die IVerification implementiert wird. Diese Zeile instanziiert das COM-Objekt und führt dann die QueryInterface-Aufrufe für die IVerification-Schnittstelle aus. In der nächsten Zeile, ptr->doVerification, wird die COM-Dateninstanz an die Routine übergeben. Die Prüfung besteht aus dem Methodenaufruf GetisOk(), der IDL-COM-Eigenschaft isOk. Funktionsweise des COM-Compilers Der COM-Compiler ist ein interessantes Tool, da mit ihm verschiedene Aufgaben ausgeführt werden können. Hierzu zählen: 왘 Automatisches Verwalten der Verweiszählung für die COM-Schnittstelle 왘 Konvertieren der COM-Eigenschaften und Rückgabewerte als Funktionsrück-
gabewerte, wie in Visual Basic und Visual J++ 왘 Automatisches Abrufen der geeigneten Schnittstelle
274
Implementieren von COM-Schnittstellen
Der COM-Compiler erledigt sämtliche dieser Aufgaben durch das Verwenden von intelligenten Zeigern und durch Operatorüberladung in einigen benutzerdefiniert generierten Stubs. Wenn eine Datei mit #import kompiliert wird, generiert der COM-Compiler zwei Dateien, eine THL- (Typelib Header) und eine TLI-Datei (Typelib Implementation). Nachfolgend eine Beispielquelle: #include namespace COMPONENTLib { // Weiterleiten von Verweisen und Typdefinitionen struct /* coclass */ DataComponent; struct __declspec(uuid("88950010-0915-11d2-9c50-00a0247d759a")) /* dual interface */ ISimpleData; struct /* coclass */ OperationsComponent; struct __declspec(uuid("88950011-0915-11d2-9c50-00a0247d759a")) /* dual interface */ ISimpleOperations; struct /* coclass */ VerificationComponent; struct __declspec(uuid("88950012-0915-11d2-9c50-00a0247d759a")) /* dual interface */ IVerification; // Typdefinitionsdeklarationen für intelligente Zeiger _COM_SMARTPTR_TYPEDEF(ISimpleData, __uuidof(ISimpleData)); _COM_SMARTPTR_TYPEDEF(ISimpleOperations, __uuidof(ISimpleOperations)); _COM_SMARTPTR_TYPEDEF(IVerification, __uuidof(IVerification)); // Elemente der Typbibliothek struct __declspec(uuid("88950003-0915-11d2-9c50-00a0247d759a")) VerificationComponent;
// [ default ] Schnittstelle IVerification
struct __declspec(uuid("88950012-0915-11d2-9c50-00a0247d759a")) IVerification : IDispatch { __declspec(property(get=GetisOk)) long isOk; __declspec(property(get=Getcontext,put=Putcontext)) long context; HRESULT doVerification ( IDispatch * pDisp ); long GetisOk ( ); long Getcontext ( ); void Putcontext ( long pVal );
Es wird relativ viel Quellcode generiert, aber der COM-Compiler geht hierbei in besonderer Form vor. Der erste Schritt stellt die Deklaration eines C++-Namespace dar. Dieser ist identisch mit dem COM-IDL-Bibliotheksnamen. Anschließend werden innerhalb des Bibliotheksnamens die verschiedenen coclass- und Schnittstellen-UUIDs definiert. Diese werden für die COM-Bibliotheksaufrufe benötigt. Im Anschluss daran werden die verschiedenen Schnittstellen deklariert. Sämtliche Schnittstellendeklarationen, mit Ausnahme von IVerification, wurden aus Gründen der Übersichtlichkeit entfernt. Innerhalb der Schnittstellendeklaration sind zwei Methodendeklarationen für doVerification vorhanden. Die erste lautet doVerification, die zweite raw_doVerification. Wird die Klassendeklaration verwendet, kommt die doVerification-Methode zum Einsatz. Versucht der Compiler, die VTabellen der COM-Schnittstelle zuzuordnen, wird raw_doVerification verwendet. Es sind zwei Methoden vorhanden, damit eine benutzerfreundliche Version der Methode generiert werden kann. Benutzerfreundlich heißt hier, dass statt der Übergabe von Zeigern Rückgabewerte verwendet werden können, beispielsweise die Eigenschaft GetisOk. Intelligente Zeiger (Smartpointer) stellen eine Methode zur Emulation der Garbagecollection unter Verwendung der Verweiszählung dar. Die Idee dabei ist, keinen Zugriff auf den Schnittstellenzeiger zu besitzen, aber über eine Klasse zu verfügen, mit der der Schnittstellenzeiger im Namen des Benutzers verwaltet wird. Sehen Sie sich folgenden Code an: template < class managed> class smrtPtr { public: smrtPtr() { } ~smrtPtr() { ptr->Release(); } managed *operator->() { return ptr; } managed *ptr; };
276
Implementieren von COM-Schnittstellen
Dies ist eine Version eines Smartpointers zur Veranschaulichung der wichtigen Bits. Das verwaltende Element, die smrtPtr-Vorlage, enthält ein typisiertes Schnittstellenmitglied. Wird das Objekt zerstört, erfolgt über den Manager eine Verringerung des Wertes für die Verweiszählung durch Aufruf von IUnknown::Release. Zum Zugriff auf den verwalteten Zeiger ohne Verwendung der Notation smrtPtr::ptr::method wird der -> operator überladen. So kann die Notation smrtPtr->method für den Zugriff auf die durch den intelligenten Zeiger verwaltete Methode verwendet werden. Der Zugriff auf die verwaltete Methode macht es unmöglich, über -> operator Methoden innerhalb der smrtPointer-Klasse zu verwenden. Dies ist jedoch in Ordnung, da ein intelligenter Zeiger niemals mit Hilfe einer Speicherzuweisung zugeordnet wird. Werden Smartpointer und der durch den COM-Compiler generierte Stub kombiniert, erhält man eine einfach einzusetzende COM-Objektverwendungsmethode. Durch die Klasse der intelligenten Zeiger wird die Fähigkeit zum automatischen Konvertieren von einer Schnittstelle in eine andere bereitgestellt. Der folgende Code ist beispielsweise zulässig: SimpleImplementationVB::_clsImplDataPtr ptrData( "SimpleImplementationVB.clsImplData"); COMPONENTLib::ISimpleDataPtr ptrInterface; ptrInterface = ptrData;
Bei dem anfänglich zugeordneten Objekt ptrData handelt es sich um die Schnittstelle _clsImplData. Gewünscht wird die Schnittstelle ISimpleData. Es ist bekannt, dass durch _clsImplData ISimpleData implementiert wird. Zum Abruf wird ptrInterface der Zeiger ptrData zugeordnet. Der QueryInterface-Aufruf für die Schnittstelle ISimpleData findet automatisch statt. Der QueryInterface-Aufruf ist in der Klasse der Smartpointer eingebettet. Es wurde bereits zu einem früheren Zeitpunkt erwähnt, dass an den Schnittstellennamen das Suffix Ptr angehängt werden muss. Dies ergibt sich aus dem folgenden Makro: #define _COM_SMARTPTR_TYPEDEF(Interface, IID) \ typedef _COM_SMARTPTR Interface ## Ptr
Zur Einsparung von Zeit und zur Verringerung des Aufwands weisen alle generierten Schnittstellen im Stub eine typedef-Schnittstelle auf, die den intelligenten Zeiger einbettet.
Verwenden der COM-Klassen
277
Eine Tücke der intelligenten Zeiger Bei der Arbeit mit intelligenten Zeigern (Smartpointer) gibt es eine Situation, in der die Zeiger nicht funktionieren. In der Implementierung der Eigenschaft ISimpleOperations::simpleData muss ein Schnittstellenzeiger zurückgegeben werden. Wird einem Doppelzeiger eine durch den COM-Compiler generierte Klasse zugewiesen, wird der Wert für die Verweiszählung nicht erhöht. Dieser Schritt muss manuell erfolgen, wie im folgenden Beispielquellcode: STDMETHOD(get_simpleData)(IDispatch * * pVal) { if (pVal == NULL) return E_POINTER; *pVal = ptrData; (*pVal)->AddRef(); return S_OK; }
Der Wert pVal wird der COM-Compilerklasse ptrData zugeordnet. Nach erfolgter Zuordnung muss pVal->AddRef aufgerufen werden. Ausnutzen der Unterstützungsklassen für den COM-Compiler Des Weiteren bietet die COM-Compilerunterstützung eine zusätzliche Klasse, mit der die Schnittstellenzeiger und die nicht C++-basierten Typen verwaltet werden. Obwohl in ATL Pendants zu diesen Klassen vorhanden sind, sind diese ATL-spezifisch. Die durch den COM-Compiler verwalteten sind nicht bibliotheksspezifisch und können in COM-Komponenten verwendet werden, falls erforderlich. Obwohl ATL und MFC gute Bibliotheken darstellen, sind sie spezifisch und nicht so leicht zu verwenden wie die COM-Compilerunterstützung. Definieren eigener Smartpointer-basierter Klassen Es gibt Schnittstellen, in denen die Smartpointer nicht definiert sind. Sie können mit Hilfe der COM-Compilernotation eine eigene Klasse intelligenter Zeiger definieren: _COM_SMARTPTR_TYPEDEF(IMyInterface, __uuidof(IMyInterface));
Auf diese Weise wird eine Klasse intelligenter Zeiger namens IMyInterfacePtr erzeugt, die verwaltet ist. Seien Sie jedoch gewarnt – da der COM-Compiler keinen Stub generieren könnte, handelt es sich um COM-Hilfsmethoden. Arbeiten mit Daten: _variant_t Das Verwenden der VARIANT-Klasse _variant_t des COM-Compilers vereinfacht das Programmieren erheblich. Aufgrund seiner Einfachheit ist keine detaillierte Erläuterung nötig, sehen Sie sich einfach den folgenden Quellcode an, der zeigt, wie der Variantdatentyp eingesetzt wird:
278
Implementieren von COM-Schnittstellen
_variant_t var; _variant_t var2((long)12); var = (short)12; var = "Hello";
Dies erscheint vielleicht sehr simpel, aber im Hintergrund laufen einige interessante Dinge ab. Wird var ein Wert von 12 zugewiesen, muss ein Typ deklariert werden. Der Grund hierfür ist, dass es sich bei dem Wert 12 um einen int-, longoder short-Wert handeln könnte. In der Klasse wird die gesamte Funktionalität gekapselt, damit sichergestellt ist, dass der Variantwert ordnungsgemäß deklariert und zugewiesen wird. Wenn Sie jedoch einen VARIANT-Wert übergeben haben und diesen in einen _variant_t-Typ konvertieren möchten, nehmen Sie die folgende Zuordnung vor: VARIANT vtText; VariantInit( &vtText); V_VT( &vtText) = VT_BSTR; V_BSTR( &vtText) = SysAllocString( L"12"); testCopy = vtText;
Darüber hinaus muss der Variantwert von einem Typ in einen anderen geändert werden. Im vorstehenden Quellcode lautet der Varianttyp V T_BSTR. Die Zeichenfolge soll in eine Zahl konvertiert werden, da das Referenzieren einer Zahl effizienter ist als das Referenzieren einer Zahl als Zeichenfolge. Hierzu müssen lediglich die Typen geändert werden: void ChangeType( VARTYPE vartype, const _variant_t* pSrc = NULL );
Der erste Parameter, vartype, ist der Variantzieltyp. Dies entspricht exakt den Typen, die mit der OLE-Automatisierung eingeführt wurden, Beispiele sind V T_BSTR, V T_14 usw. Der zweite Parameter definiert einen Ursprung für die Konvertierung. Wenn pSrc demnach nicht NULL ist, werden die Inhalte von pScr in den Typ vartype konvertiert und anschließend zum referenzierten _variant_tObjekt kopiert. Jedes im aktuellen Objekt referenzierte Element geht verloren. Der Standard lautet NULL, d.h. das referenzierte Objekt wird in den gewünschten Typ konvertiert. Zum Schluß können Sie den Variantwert mit Hilfe der Methode Clear löschen. Arbeiten mit COM-Zeichenfolgen: _bstr_t Abschließend soll eine der wichtigsten Klassen besprochen werden. Diese Klasse ermöglicht das einfache Konvertieren von Zeichenfolgen des Typs bstr in den Typ wide char oder normal
Verwenden der COM-Klassen
279
char. Wie die Variantklasse kapselt die _bstr_t-Klasse die meisten Konvertierungen durch einen Satz überladener Operatoren. Typische Verwendungsszenarien sind: _bstr_t bstrVar( "Hello"); char *tempHello; tempHello = bstrVar;
In diesem Beispiel wird lediglich eine Zeichenfolge in eine _bstr_t-Variable der Erstellungsroutine konvertiert, und zusammen mit dem überladenen Zuordnungsoperator in einen normale char-Zeichenfolge zurückkonvertiert. Bei Verwendung dieser Klasse gibt es einen Haken. Nehmen Sie folgendes Beispiel: void func( BSTR input) { _bstr_t var( input, true); char *result; result = var; return; }
Der Grund dafür, dass der Eingabeparameter BSTR als _bstr_t-Erstellungsparameter verwendet wird, ist der, dass die Konvertierung einfacher ist. Das Problem hierbei besteht darin, dass die Ursprungsbyte bearbeitet werden und dass der Aufrufer der Klasse den übergebenen BSTR-Wert erstellt. Der Eingabeparameter BSTR muss daher in einen anderen Puffer kopiert werden. Geschieht dies nicht, wird bei der Selbstzerstörung der _bstr_t-Variablen der Versuch unternommen, BSTR freizugeben. Da dieser Parameter jedoch nicht durch die Funktion erstellt wurde, tritt ein Fehler auf.
9.5 Fehlerbehandlung In Visual Basic werden COM-Fehler als Laufzeitfehler bezeichnet. In Visual J++ heißen COM-Fehler Ausnahmen und müssen verwendet werden. In Visual C++ werden COM-Fehler auch Ausnahmen genannt, deren Verwendung ist jedoch optional. Im Allgemeinen verwenden Programmierer keine Ausnahmen. Hiervon ausgenommen sind Java-Programmierer, da die Ausnahmeverwendung durch Java vorgeschrieben wird. Von nun an enthalten alle Codebeispiele die Ausnahmebehandlung in den verschiedenen Sprachen. Die Ausnahmebehandlung ist nützlich, wenn sie richtig angewendet wird.
280
Implementieren von COM-Schnittstellen
9.5.1 COM-Fehler Bevor die Fehlerbehandlung näher untersucht wird, müssen wir einen Blick auf die COM-Fehler werfen. Wie Sie wissen, geben IDL-Methoden und Eigenschaftendeklarationen HRESULT-Werte zurück, da COM derzeit keine Ausnahmebehandlung unterstützt. Fehler werden mit Hilfe eines Methoderückgabewerts zurückgegeben, der als HRESULT bezeichnet wird. Decodieren von HRESULT HRESULT wird als 32-Bit-integer-Wert definiert und setzt sich folgendermaßen zusammen: 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 +-+-+-+-+-+---------------------+-------------------------------+ |S|R|C|N|r|
Die für den Entwickler wichtigsten Bestandteile von HRESULT sind Severity (Schweregrad), Facility (Gerät) und der Code selbst. Das S steht für Severity (Schweregrad), ein Einzelbit, durch das der Meldungstyp angegeben wird. Die Meldung kann entweder vom Typ Success (0) (Erfolg) oder Failure (1) (Fehler) sein. Die Facility (Gerät) gibt an, woher die Meldung ursprünglich stammt. Handelt es sich um eine Windows-, COM-System-Fehlermeldung oder um eine Meldung, die schnittstellenspezifisch ist? In den verbleibenden Bits wird der Code gespeichert. Durch Erstellen eines HRESULT-Werts, der sich aus diesen drei Bestandteilen zusammensetzt, kann nahezu jede Form der Meldung definiert werden. Wird ein HRESULT-Wert empfangen, muss dieser in seine Einzelteile zerlegt werden. Dies geschieht folgendermaßen: HRESULT hr; hr = ptrData->callMethod(); if( SUCCEEDED( hr)) { printf( "This is not an error, but Facility %ld, Code %ld\n", HRESULT_FACILITY( obj.Error()), HRESULT_CODE( obj.Error())); } else if( FAILED( obj.Error())) { printf( "This is an error, but Facilty %ld, Code %ld\n", HRESULT_FACILITY( obj.Error()), HRESULT_CODE( obj.Error())); }
Fehlerbehandlung
281
Das Gerät wird unter Verwendung des Makros HRESULT_FACILITY ermittelt. Als Geräte kommen in Frage: #define FACILITY_WINDOWS #define FACILITY_STORAGE
8 3
#define FACILITY_RPC
1
#define FACILITY_SSPI
9
#define FACILITY_WIN32
7
#define FACILITY_CONTROL
10
#define FACILITY_NULL
0
#define FACILITY_INTERNET
12
#define FACILITY_ITF
4
#define FACILITY_DISPATCH #define FACILITY_CERT
2 11
Die Liste stammt aus der Headerdatei winerror.h. Wird eine benutzerdefinierte Gerätemeldung gesendet, ist diese vom Typ FACILITY_ITF. Dies bedeutet, dass der Fehler schnittstellenspezifisch ist. Wird dieser Fehler ausgegeben, muss der Empfänger wissen, dass es sich um schnittstellenspezifischen Code handelt. Fehlercode 100 von der benutzerdefinierten Schnittstelle IInterface1 und Fehlercode 100 von der benutzerdefinierten Schnittstelle IIInterface2 sind nicht dasselbe. Hier taucht ein potenzielles Problem auf, denn wenn ein FACILITY_ITFFehler in der Methodenstruktur nach oben weitergeleitet wird, kann der Fehler fälschlicherweise für einen anderen Fehler gehalten werden. Daher muss eine lokale Fehlerbehandlung vorgenommen werden. Zum Abrufen des Fehlers aus HRESULT wird das Makro HRESULT_CODE verwendet. Ein gerätespezifischer Fehler wird zurückgegeben. Unter Verwendung der Makros SUCCEEDED und FAILURE kann ermittelt werden, ob der Fehlercode sich auf einen Erfolg oder einen Fehlschlag bezieht. Anhand der Makros FAILURE und SUCCEEDED wird ermittelt, was vorgefallen ist.
9.5.2 Generieren von Fehlern Das Generieren von Fehlern ist ein langwieriger Prozess. Die einfachste Methode stellt das Generieren benutzerdefinierter HRESULTs für ein Framework dar. Dieses Verfahren funktioniert, ruft jedoch lediglich die Beendigung einer Anwendung, eine allgemeine Schutzverletzung oder – noch schlimmer – die Anzeige eines Dialogfeldes hervor. Die Fehlerbehandlung für einfache Endbenutzeranwendungen ist nicht sehr komplex, bei verteilten Anwendungen mit Tausenden von Instanzen sieht es dagegen schon ganz anders aus. In diesen Szenarios ist das De-
282
Implementieren von COM-Schnittstellen
buggen nicht einfach, da die Fehlersituation häufig nicht reproduziert werden kann. Eine funktionierende Lösung stellt das Erstellen eines Fehlerobjekts dar, das zusammen mit der COM-Fehlerbehandlungsroutine zusammenarbeitet. Obwohl es unsinnig erscheinen mag, ein weiteres Fehlerobjekt zu erstellen, hat sich dieser Ansatz bei verteilten Anwendungen als sehr effizient erwiesen. Innerhalb eines benutzerdefinierten Fehlerobjekts können benutzerdefinierte Verfolgungsroutinen sowie eine Statusverwaltung integriert werden. Das Fehlerobjekt kann darüber hinaus nach seiner Erstellung in verschiedenen Projekten eingesetzt werden. Die Schnittstellendefinition der benutzerdefinierten COM-Klasse für die Fehlerbehandlung lautet: interface IErrorHandler : IDispatch { [id(1)] HRESULT setError(long code, BSTR module, long lineNumber); [propget, id(2)] HRESULT errorLevel([out, retval] long *pVal); };
Die erste Methode lautet setError, durch die ein Fehler generiert wird. In einer typischen Implementierung wird diese Methode im Ereignis- oder Ablaufverfolgungsprotokoll aufgezeichnet. Die zwei weiteren Parameter der Methode sind Dateistandortindikatoren. In C++ können diese in Code kompiliert werden. In anderen Sprachen ist eine manuelle Einstellung erforderlich. Es ist zwingend erforderlich, diese Informationen einzuschließen – bei der Codeausführung kann der Fehler exakt ermittelt werden. Ausgeben des Objekts Viele Sprachen unterstützen die Notation von Ausgabeobjekten als Fehler, daher macht es Sinn, die Implementierung von IErrorHandler auszugeben. Da COM anders funktioniert, kann diese Methode nicht verwendet werden. Das Fehlerobjekt kann nur als Objekt zum Aufzeichnen des Fehlers eingesetzt werden. Schreiben in das Ereignisprotokoll Windows 2000 verfügt über die Fähigkeit, Ereignisse im so genannten Ereignisprotokoll aufzuzeichnen. Das Schreiben in das Ereignisprotokoll ist nicht besonders schwierig. Im Gegenteil, Schreibvorgänge in das Windows 2000-Ereignisprotokoll sind sogar einfacher als das Schreiben in eine Datei. Stellen Sie sich ein internes Netzwerk vor. Ein Server wird nicht ordnungsgemäß ausgeführt, es treten Fehler auf. Mit Hilfe von Windows 2000 können Sie das Ereignisprotokoll von ei-
Fehlerbehandlung
283
nem zentralen Rechner aus einsehen. In einem Internetszenario kann die Verwendung einer Datei angebrachter sein. Eine Beispielimplementierung für das Schreiben in ein Ereignisprotokoll kann folgendermaßen aussehen: public void setError(int code, String strModule, int lineNumber) { EventLog log; log = new EventLog( "bacchus", null); log.reportEvent( EventLog.ERROR, lineNumber, 100, strModule); }
Visual J++ weist eine Klasse auf, mit der die Ereignisprotokollfunktionen gekapselt werden. In Visual Basic oder Visual C++ sind drei äquivalente Windows APIFunktionen (RegisterEventSource, ReportEvent und DeregisterEventSource) vorhanden. Der grundlegende Prozess der Ereignisprotokollierung ist zunächst das Registrieren einer Quelle. Dieser Vorgang ist identisch mit der EventLog-Erstellungsroutine, die zwei Zeichenfolgen aufnimmt, den Namen des Servers, auf dem ein Ereignis aufgezeichnet wird, sowie die Quelle, die die Ereignisprotokollierung durchführt. Als Quelle kann entweder Null oder ein Teilschlüssel unter dem Registrierungsschlüssel HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\EventLog verwendet werden. Der nächste Schritt umfasst das Melden eines Ereignisses. Hierbei kann ein Ereignis durch eine Zeichenfolge oder durch eine Zeichenfolge und binäre Informationen angegeben werden. Die binären Informationen könnten beispielsweise durch ein Analysetool zur Fehlerermittlung verwendet werden. Hier könnte auch der Status gespeichert werden. In der reportEvent-Methode sind vier Parameter vorhanden. Der erste Parameter gibt an, welche Art von Ereignis aufgezeichnet wird. Es kann sich um eine Warnung, eine Informations- oder (wie hier) um eine Fehlermeldung handeln. Der zweite Parameter definiert die Ereigniskategorie. Diese Zahl ist spezifisch für die Komponente, die das Ereignis generiert. Anhand dieser Nummer kann ein Analysetool die verschiedenen Ereignisse nach dem Teilsystemstandort kategorisieren. Der dritte Parameter definiert die Ereignis-ID; diese richtet sich nach der Quelle. Hierbei könnte es sich beispielsweise um eine interne Fehlercodesequenz handeln. Der vierte Parameter ist ein Zeichenfolge, deren Inhalt sich danach richtet, ob binäre Informationen zu einem Fehler gespeichert werden.
284
Implementieren von COM-Schnittstellen
9.5.3 Fehlerbehandlung in Visual Basic In Visual Basic wird über On Error Goto ein Framework für die Fehlerbehandlung bereitgestellt. Zur Veranschaulichung der Fehlerbehandlung habe ich ein COMObjekt namens BadCOM erstellt, durch das ständig ein Fehler ausgegeben wird. Zur Integration dieses COM-Objekts wurde folgender Visual Basic-Code geschrieben: Private Sub method2() On Error GoTo ErrorHandler: Dim tmpObj As BadCOM Set tmpObj = New BadCOM tmpObj.alwaysAnError Exit Sub ErrorHandler: Dim errObj As New ErrorComponent.implErrorHandler Call errObj.setError(Err.Number, Err.Description, 1) Err.Raise 2 + vbObjectError + 512 End Sub
Zunächst muss durch die Methode anhand von On Error Goto eine lokale Fehlerbehandlungsroutine deklariert werden. Die Fehlerbehandlungsroutine gilt nur für den lokalen Kontext der Methode. Wird während einer Methodenausführung ein Fehler generiert, ermittelt Visual Basic die letzte im aufrufenden Stack erstellte Routine. Nach Goto muss angegeben werden, wohin der Sprung erfolgen soll. In diesem Fall wird nach ErrorHandler gesprungen. Der nächste Schritt umfasst die Erstellung des BadCOM-Objekts sowie das anschließende Aufrufen der Methode alwaysAnError. Es ist sehr wichtig, die exit sub-Anweisung (exit function) vor der Fehlerbehandlungsroutine einzufügen. Andernfalls wird die Fehlerbehandlungsroutine auch dann aufgerufen, wenn kein Fehler aufgetreten ist. In der Fehlerbehandlungsroutine muss zunächst das Error-COM-Objekt erstellt werden. Schlägt dieser Schritt fehl, wird eine Ausnahme erzeugt, die durch die nächste verfügbare Behandlungsroutine oder durch die Anwendung selbst aufgefangen wird. Die Methode setError zur Fehlerbestimmung wird aufgerufen. Beachten Sie die Parameter von setError. Wenn Visual Basic einen Fehler generiert, wird das Err-Objekt erstellt. Das Err-Objekt ist ein Objekt, das sämtliche Fehlerinformationen enthält. Sie sollte vor allem die Nummer (err.number) und die Beschreibung (err.description) interessieren.
Fehlerbehandlung
285
Nachdem der Fehler behandelt wurde, kann die Verarbeitung fortgesetzt werden. Zuvor muss jedoch der Fehler mit Hilfe der Methode err.Clear gelöscht werden. Abschließend kann die resume-Anweisung aufgerufen werden, um die Verarbeitung der aktuellen Methode wieder aufzunehmen. Eine weitere Option besteht darin, die Methode zu beenden und mit der aufrufenden Methode fortzufahren. Treten wir nun einen Schritt zurück und betrachten die Visual Basic-Fehlerbehandlung von einem allgemeineren Gesichtspunkt aus. Tritt ein Fehler aufgrund eines fehlerhaften COM-Aufrufs oder eines falschen Wertes auf, wie kann dann die Funktion beendet werden? Verlassen Sie die Funktion, ohne einen Fehlerhinweis zurückzugeben? Nein – so könnte ein weiterer Fehler entstehen, da der Objektstatus nicht konsistent ist. Sie möchten den Aufrufer der Methode darüber informieren, dass ein Fehler aufgetreten ist. Hierzu erstellen Sie in Visual Basic einen anwendungsspezifischen Fehlercode mit folgender Syntax: [error number] + vbObjectError + 512
Der Fehler wird anschließend, wie zuvor erläutert, mit Err.Raise erzeugt. In diesem Framework wird ein Fehler durch den Fehlerbehandlungsmechanismus aufgefangen. Der Fehler wird aufgezeichnet, und es wird ein anwendungsspezifischer Fehler generiert. Der Aufrufer verfährt sehr wahrscheinlich ebenso und generiert ebenfalls einen Fehler. Dieser Prozess wird fortgesetzt, bis die Anwendung nicht länger fortgesetzt werden kann. Auf diese Weise kann eine Ablaufverfolgung für den Aufrufstack eingerichtet werden. Das Einsehen der Protokolldatei ermöglicht Ihnen das Ermitteln der Fehlerursache. Sie denken vielleicht, dass so ein beliebiger Fehler zum automatischen Abbruch der Aufgabe führt, und dass dies eine zu drastische Maßnahme darstellt. Es ist vielleicht möglich, das Problem zu lösen. Im Allgemeinen ist dies jedoch nicht der Fall. Bei Auftreten eines Fehlers handelt es sich meistens um einen schwer wiegenden Fehler, bei dem eine Wiederherstellung unmöglich ist. Darüber hinaus sollte der Einsatz von Ausnahmen überdacht werden. Ausnahmen werden auch nur in Ausnahmesituationen angewendet. Wenn Sie beispielsweise eine Datei öffnen, wird eine Ausnahme generiert, wenn die gelesene Datei beschädigt ist. Sie sollten keine Ausnahme generieren, wenn Probleme beim Öffnen einer Datei durch das Erfragen eines anderen Dateinamens gelöst werden können. Kann die Datei geöffnet werden, ist jedoch beschädigt, und kann das Problem nicht durch das Anfordern einer neuen Datei gelöst werden, muss eine Ausnahme generiert werden.
286
Implementieren von COM-Schnittstellen
9.5.4 Fehlerbehandlung in Visual J++ Die COM-Fehlerbehandlung in Visual J++ ähnelt dem Auffangen einer Java-Ausnahme. Der Unterschied besteht darin, dass das aufzufangende Objekt ComFailException heißt und Bestandteil des Pakets com.ms.com ist. Diese Klasse enthält den aufgetretenen Fehler. Als Gegenstück ist die Klasse ComSuccessException vorhanden, die angibt, das die aufgerufene COM-Methode erfolgreich ausgeführt wurde. Diesen Fehler möchten Sie üblicherweise nicht auffangen. Die folgende Implementierung funktioniert ebenso wie das vorangegangene Visual Basic-Beispiel: try { BadCOM tmpObj = new BadCOM(); tmpObj.alwaysAnError(); } catch( ComFailException e) { implErrorHandler errObj = new implErrorHandler(); errObj.setError( e.getHResult(), e.getMessage(), 1); throw new ComFailException(); }
Die Abschnitte try und catch definieren einen Bereich, in dem Ausnahmen aufgefangen werden. Für uns ist nur die Ausnahme ComFailException interessant. Zum Abrufen des Fehlercode wird die Methode e.getHResult aufgerufen. Die Fehlermeldung wird mit Hilfe von e.getMessage abgerufen. Zum Generieren einer Ausnahme wird die throw-Anweisung verwendet. In diesem Beispiel wird das ComFailException-Objekt instanziiert, ohne dass der Fehlerwert definiert wird. Dies ist in Ordnung, da die Klasse ComFailException den Standardwert E_FAIL verwendet.
9.5.5 Fehlerbehandlung in Visual C++ In C++ können Fehler mit Hilfe von Ausnahmen aufgefangen werden. Einige Methoden in C++ generieren Ausnahmen, andere jedoch nicht. Dies verkompliziert die Angelegenheit, denn Ausnahmen müssen aufgefangen werden. Bei Methoden ohne Ausnahmeerzeugung müssen die Ausnahmen in Methoden gekapselt werden, durch die Ausnahmen generiert werden. Wenn durch den COM-Compiler die Stubs generiert werden, ist die Ausnahmebehandlung in diesen Stubs enthalten. Die gleichen Routinen werden im Visual C++-Mechanismus zur Ausnahmebehandlung eingesetzt. Wenn Sie den Visual Basic-Code in äquivalenten Visual C++-Code konvertieren, erhalten Sie Folgendes:
Mit Hilfe von try und catch wird ein Ausnahmeblock eingerichtet. Wie in Visual J++ ist auch hier die Ausnahme von Interesse _com_error. Es handelt sich um eine COM-Compilerklasse, die in der Headerdatei definiert ist. Für den Zugriff auf den Fehlercode wird die Methode err.Error aufgerufen. Zum Zugreifen auf die Fehlermeldung wird die Methode err.ErrorMessage aufgerufen. Es wird ein _bstr_t-Wert zurückgegeben. Zum Generieren eigener Fehler verwenden Sie nicht throw, sondern die Funktion _com_raise_error mit dem COM-Fehlercode als Parameter. Erstellen eigener HRESULT-Werte In Visual C++ ist das Erzeugen eigener COM-Fehler etwas komplizierter. Es steht jedoch zur Vereinfachung ein Makro bereit. Sehen Sie sich den folgenden Quellcode an: HRESULT retCode = MAKE_HRESULT( SEVERITY_ERROR, FACILITY_ITF, 600);
Das Makro MAKE_RESULT maskiert alle Bitoperatoren. Der erste Parameter legt fest, ob es sich um einen Fehler (SEVERITY_ERROR) oder um eine Erfolgsmeldung (SEVERITY_SUCCESS) handelt. Der zweite Parameter gibt das Gerät an. Da es sich hier um eine benutzerdefinierte Fehlermeldung handelt, lautet der Parameter FACILITY_ITF. Die letzte Zahl bezeichnet den Fehlercode. Der Fehlercode darf nicht den Bereich zwischen 0 und 0x1FF umfassen, da dieser Bereich für COM-Gerätefehler reserviert ist. Auffangen nicht als Ausnahmen einzustufender Ereignisse Es ist wichtig, dass alle COM-Methoden Ausnahmen generieren, die mit den COM-Compilerklassen zusammenarbeiten. Hierzu steht die Helferfunktion CheckError zur Verfügung. Diese Funktion akzeptiert ein HRESULT als Parameter. Üblicherweise geben die meisten COM-Hilfsschnittstellen einen HRESULT-Wert zurück, der dann an die Helferfunktion übergeben werden kann. Eine Beispielimplementierung lautet folgendermaßen:
Wenn es sich bei HRESULT um einen Fehler handelt, wird ein COM-Fehlerobjekt _com_error ausgegeben. Handelt es sich bei HRESULT um eine Warnung, wird keine Ausnahme erzeugt.
9.6 Resümee In den vorangegangenen Kapiteln wurde erläutert, wie eine Anwendung mit Hilfe von Sequenz- und Kollaborationsdiagrammen sowie Anwendungsfällen entworfen werden kann. Im vorliegenden Kapitel wurden die Technologien vorgestellt, die zur Implementierung der COM-Objekte eingesetzt werden. Die hier behandelten Themen sind von besonderer Bedeutung, da wichtige technische Details zur Implementierung von COM-Objekten genannt werden. Das Generieren von Fehlern beispielsweise ist eine Fähigkeit, auf die oft nicht genügend Wert gelegt wird. Ich weiß aus meiner persönlichen Erfahrung, dass ein gutes Framework für die Fehlerbehandlung den Unterschied zwischen einem stabilen und einem instabilen Laufzeitprodukt ausmacht. Dies rührt daher, dass Laufzeitfehler schwer zu verfolgen sind, es jedoch besonders wichtig ist, die Quelle eines Fehlers zu ermitteln. Des Weiteren wurde die Implementierung von COM-Objekten in drei verschiedenen Programmiersprachen untersucht. Wenn Sie die Unterschiede zwischen verschiedenen Szenarien kennen, können Sie die Auswirkungen dieser Faktoren auf Ihr Projekt einschätzen. Im folgenden Teil des Buches werden die verschiedenen technischen Details zur Implementierung der Geschäftsobjekte erläutert, die bisher entworfen wurden.
Resümee
289
10 Entwickeln von Transaktionskomponenten Das Schreiben von COM-Komponenten und -Objekten (Component Object Model) ist nicht besonders schwierig. Das Schreiben stabiler, zuverlässiger und skalierbarer COM-Komponenten und -Objekte ist dagegen relativ komplex. Die Schwierigkeit beim Schreiben dieser Komponenten liegt darin, dass der Programmierer häufig serverseitigen Code erstellen muss, durch den Aspekte wie das Threading und der gleichzeitige Zugriff gesteuert werden müssen. Das Debuggen dieser Art von Code ist sehr komplex, da ein Laufzeitfehler in einer Debugsitzung möglicherweise nicht reproduziert werden kann. In diesem Kapitel wird untersucht, wie COM-Objekte erstellt werden, mit denen die Funktionen der COM+-Dienste von Windows 2000 genutzt werden können. Der COM+-Basisdienst ermöglicht das Registrieren eines COM-Objekts in einer COM+-Awnwendung. So wird das COM-Objekt zu einem COM+-Objekt und kann die Vorteile der COM+-Transaktionsdienste nutzen. Dieses Kapitel setzt bei der Bedeutung der COM+-Transaktionsdienste an. Teile des vorliegenden Kapitels sind theoretischer Natur, da die Verwendung des COM+Frameworks die Verwendung eines anderen Programmiermodells erfordert. Im Anschluss an den theoretischen Teil werden die praktischen Probleme beim Schreiben von COM+-Objekten beleuchtet.
10.1 Einführung in Transaktionen Eine Transaktion im Hinblick auf die Software bezeichnet die Ausführung eines Prozesses, an dem zwei Parteien oder Komponenten beteiligt sind. Im allgemeinen Sprachgebrauch bezeichnet eine Transaktion häufig einen Geschäftsprozess. Stellen Sie sich eine Bank vor. Sie gehen zu einem Geldautomaten, um ein wenig Geld von Ihrem Konto abzuheben. Hierzu gehen Sie folgendermaßen vor: 1. Sie führen Ihre Bankkarte in den Geldautomaten ein. 2. Sie geben Ihre Geheimzahl ein. 3. Sie wählen den Betrag aus, den Sie abheben möchten. 4. Sie drücken die Eingabetaste, um die Transaktion auszuführen. 5. Sie warten, bis Ihr Konto mit dem angeforderten Betrag belastet wurde. 6. Sie entnehmen dem Geldautomat Ihre Bankkarte. 7. Sie entnehmen das Geld.
Einführung in Transaktionen
291
Alle Schritte zusammen bilden eine Transaktion. Stellen Sie sich vor, dass die bankinterne Belastung des Kontos sich verzögert und letztendlich nicht vorgenommen wird. Sie haben jedoch das Geld über den Bankautomaten erhalten, und der Geldautomat kann nicht hinter Ihnen herlaufen, um das Geld zurückzuverlangen. Dies verdeutlicht einen wichtigen Aspekt im Hinblick auf Transaktionen. Nachdem eine Transaktion ausgeführt wurde, kann sie nicht mehr zurückgenommen werden. Nachdem das Geld ausgegeben wurde, ist eine Rückforderung unmöglich. Sie denken vielleicht, dass der größte Schaden hierbei darin besteht, dass die Bank Geld verliert, es gibt jedoch ein größeres Problem – es wird Geld erzeugt. Das durch den Automaten ausgegebene Geld muss verbucht werden. Wird nicht Ihr Konto mit der entsprechenden Summe belastet, wird das Geld dem System hinzugefügt. Mit anderen Worten: durch einen fehlerhaften Prozess wurde (nicht vorhandenes) Geld erzeugt. Dies ist illegal und sollte niemals passieren.
10.1.1 ACID: Die vier Gebote der Transaktionsverarbeitung ACID ist ein Akronym, das sich aus den Anfangsbuchstaben der Schlagworte zusammensetzt, die für ein Transaktionssystem gelten: Atomicity (Unteilbarkeit), Consistency (Konsistenz), Isolation (Isolierung) und Durability (Dauerhaftigkeit). Im Gegensatz zu anderen Systemen, bei denen für bestimmte Teile des Systems Ausnahmen gelten können, müssen in einem transaktionsbasierten System alle Elemente den aufgestellten Regeln entsprechen. Hierzu zählen das Transaktionssystem, die Server- und die Clientkomponenten. Es muss betont werden, dass niemals Shortcuts und Hacks verwendet werden sollten, da sie langfristig zu Problemen führen können. Ein Transaktionssystem ist mit einer Kette vergleichbar, deren Stärke durch das schwächste Glied bestimmt wird. Atomicity (Unteilbarkeit) Eine Transaktion ist ein Vorgang, der als Ganzes entweder ausgeführt wird oder scheitert. Wird eine Transaktion erfolgreich ausgeführt, spricht man davon, dass ein Commit ausgeführt wurde. In diesem Fall bleiben die vorgenommenen Änderungen in Kraft. Wird die Transaktion abgebrochen, spricht man von einem Rollback, d.h., die Änderungen werden wieder rückgängig gemacht. Im oben genannten Geldautomatenbeispiel bestand die Forderung darin, dass das Konto erfolgreich mit einer bestimmten Geldsumme belastet werden sollte. Wird diese Transaktion unterbrochen oder tritt ein Systemausfall ein, muss die Transaktion fehlschlagen. Schlägt die Transaktion fehl, muss der Bankkunde davon ausgehen können, dass der alte Kontostand beibehalten wird. Die Forderung nach Atomicity (Unteilbarkeit) besagt, dass entweder die gesamte Transaktion erfolgreich durchgeführt oder vollständig abgebrochen und rückgängig gemacht
292
Entwickeln von Transaktionskomponenten
wird; bei diesem Erfolg bzw. Misserfolg muss der richtige Status erhalten bleiben. Entweder bleibt alles beim Alten, oder sämtliche der vorgenommenen Änderungen treten in Kraft. Consistency (Konsistenz) Eine Transaktion steht für einen gültigen Status; die Unveränderlichkeit des Status wird gewahrt. Ein gültiger Status liegt vor, wenn ein Datenwert als gültiger Wert definiert wurde. Wird der Status geändert, liegt ein anfänglicher Wertesatz und ein abschließender Wertesatz vor. Damit der Status als gültig anerkannt wird, muss entweder der anfängliche oder der abschließende Wertesatz vorhanden sein. Jeder andere Wertesatz wird als ungültig eingestuft und damit als falscher Status betrachtet. Der Status kann verfälscht werden, da der Statusübergang nicht umgehend stattfindet. Es vergeht Zeit, und während dieser Zeitspanne können Probleme auftreten. Konsistenz bezeichnet kein Programm ohne Bugs. Konsistenz ist die Anwendung einer Konsistenzmethodik. Bei der Zinsberechnung können beispielsweise keine halben Pfennige gezahlt oder eingefordert werden. Der Betrag muss auf- oder abgerundet werden. Das Runden ist problematisch, da verschiedene Methoden verwendet werden können. Eine Methode stellt das mathematische Runden auf den nächsthöheren oder -niedrigeren Wert dar. Im Allgemeinen verfahren Banken genauso, da es sich um einen einfachen und konsistenten Ansatz handelt. Es wäre nicht richtig, wenn die Bank bei der Zinszahlung ab- und bei der Zinsforderung aufrunden würde, dieses Vorgehen wäre inkonsistent. Die Konsistenz stellt eines der anzustrebenden Ziele für einen Transaktionscode dar. Wird ein Konto mit einem bestimmten Geldbetrag belastet, sollte diese Belastung immer nach dem gleichen Verfahren erfolgen. Ein Beispiel für Inkonsistenz wäre es, ein Konto bei einer Buchung mit zwei Summen, bei einer anderen Buchung nur mit einer Einzelsumme zu belasten. Das Ergebnis ist dasselbe, aber die Art der Berechnung ist anders, und dieser Unterschied kann zu Problemen führen. Bei der Zinsberechnung sollte immer auf die gleiche Weise verfahren werden. Über die Konsistenz wird sichergestellt, dass eine Finanztransaktion immer zum gleichen Endergebnis führt. Ein Transaktionssystem kann keine Konsistenz erzwingen – die Konsistenz wird ausschließlich durch umfangreiche Testläufe und eine gute Programmiermethode erzielt. Eine effiziente Methode zur Konsistenzerzwingung stellt die Dokumentation von Regeln und Geschäftsprozessen dar. Isolation (Isolierung) Parallel ausgeführte Transaktionen sollten vom Ergebnis anderer Transaktionen isoliert werden, die möglicherweise noch unvollständig sind.
Einführung in Transaktionen
293
Während der Transaktionsausführung werden Daten geändert und befinden sich im Hinblick auf den Status in der Schwebe. Stellen Sie sich erneut eine Bank vor. Es soll Geld von einem Konto auf ein anderes transferiert werden. Ein Konto wird mit der Summe belastet, anschließend wird die Summe einem anderen Konto gutgeschrieben. Hierbei kann eine Zeitspanne auftreten, in der das eine Konto zwar schon belastet, dem anderen Konto die Summe aber noch nicht gutgeschrieben wurde. Diese Übergangsphase dauert ein wenig an. Fragt nun in dieser Zeit eine andere Transaktion das Konto ab, welcher Kontostand wird angezeigt? Wird der alte oder der neue Kontostand angezeigt? Transaktionssysteme gehen davon aus, dass die Daten nicht geändert wurden. Demnach werden der anfordernden Transaktion die alten Daten angezeigt. Die Aufgabe des Transaktionssystems ist noch nicht vollständig ausgeführt. Wenn durch die zweite Transaktion eine Datenänderung vorgenommen werden soll, müssen die beiden Transaktionen voneinander isoliert werden. Andernfalls könnte eine Aktualisierung an Daten erfolgen, die möglicherweise noch durch einen Rollback zurückgesetzt werden. Ein weiteres Problem stellen veraltete Daten dar. Transaktion A liest Daten ein. Transaktion B liest und aktualisiert dieselben Daten. Transaktion A aktualisiert die Daten. Die von Transaktion A gespeicherten Daten werden als veraltet eingestuft, da sie zwischenzeitlich von Transaktion B bearbeitet wurden. Einige Datenbanken lösen dieses Problem, in dem die Daten gesperrt werden, hierdurch entsteht jedoch ein neues Problem. Die Transaktionen beanspruchen sehr viel Zeit (dies ist nicht wünschenswert, wie zu einem späteren Zeitpunkt verdeutlicht werden wird). Eine bessere Lösung stellt das Schreiben von relativem Code oder Code mit Versionsprüfung dar. Relativer Code ist Code, bei dem der Wert innerhalb eines Eintrags bzw. Datensatzes keine Rolle spielt. Durch andere Werte wird der ursprüngliche Wert herabgesetzt oder erhöht, es wird jedoch kein neuer Wert bereitgestellt. Bei der Versionsprüfung wird die Version der Daten geprüft, bevor eine Aktualisierung vorgenommen wird. Durability (Dauerhaftigkeit) Nachdem ein Commit für eine Transaktion ausgeführt wurde, bleiben die Auswirkungen selbst nach Systemausfällen in Kraft. Das Konzept der Dauerhaftigkeit erfordert, dass auch bei einem Fehler die über die Transaktion bearbeiteten Daten weiterhin vorhanden sind. Dieses Konzept ist sehr wichtig, da es nicht nur für den Softwarebereich gilt. Kehren wir zum Beispiel mit dem Geldautomaten zurück. Angenommen, die Transaktion wurde beendet, das Geld wurde an den Kunden ausgegeben. Tritt jetzt ein Systemfehler auf, darf sich dies nicht auf das Ergebnis der Transaktion auswirken. Wird das System nach
294
Entwickeln von Transaktionskomponenten
dem Systemausfall wieder hochgefahren, ist der Status nach der Transaktion weiterhin vorhanden. Wurde der Systemabsturz durch eine Beschädigung an der Festplatte verursacht und ist ein Austausch der Festplatte erforderlich, geht die Transaktion verloren, wenn sie nur auf der Festplatte gespeichert wurde. Daher ist es unabdingbar, dass eine gewisse Redundanz bereitgestellt wird. Das Konzept der Dauerhaftigkeit erfordert, dass bei jedweder Problemursache eine erfolgreich abgeschlossene Transaktion weiterhin als abgeschlossen gilt. Es gibt keine Möglichkeit, die Transaktion rückgängig zu machen, zu ändern oder zu löschen. Die Dauerhaftigkeit innerhalb einer Transaktion ist nicht nur ein Softwareproblem – sie betrifft sowohl Hard- als auch Software. COM+ unterstützt nur den Softwareaspekt der Dauerhaftigkeit. Nachdem die Transaktion auf die Festplatte geschrieben wurde, wird sie als dauerhaft angesehen. Eine weitergehende Dauerhaftigkeit erfordert die Implementierung eines Serverclusters mit RAID-Funktion (Redundant Array of Independent Disks).
10.1.2 Arten von Transaktionen Eine Transaktion besteht aus einer Reihe von Geschäftsprozessen, die zusammen zu einem bestimmten Ergebnis führen. Zur Vervollständigung eines Transaktionssystems ist eine Koordinierung der Transaktionen erforderlich. Diese Koordinierung ermöglicht das Durchführen von Rollbacks oder Commits für eine Transaktion. Es gibt verschiedene Arten von Transaktionen, aber die COM+-Dienste unterstützen nur den einfachsten Transaktionstyp, die flache Transaktion. Flache Transaktionen Eine flache Transaktion liegt vor, wenn die beteiligten Geschäftsprozesse in sequenzieller Reihenfolge ausgeführt werden. Es gibt einen Start- und einen Endpunkt. Tritt zwischen diesen beiden Punkten ein Fehler auf, wird ein Rollback ausgeführt, und eventuelle Änderungen an den verschiedenen Ressourcen werden rückgängig gemacht. Im vorherigen Transaktionsbeispiel findet die erste Transaktion statt, wenn das Konto mit einem Geldbetrag belastet wird und die Datenbank, in der das Konto enthalten ist, aktualisiert wird. In diesem Fall stellt die Datenbank die Ressource dar. Zum Abbuchen des Geldes wird die folgende transaktionale Anwendungslogik ausgeführt: 1. Über den Transaktionsmonitor wird ein BEGIN-Transaktionsbefehl an die Ressource ausgegeben. So wird der Ressource mitgeteilt, dass eine Aufgabe eingeleitet wurde und dass ein Transaktionskontext mit dem Systembenutzer ver-
Einführung in Transaktionen
295
knüpft werden muss. Werden derzeit weitere Transaktionen ausgeführt, erfolgt keine Verknüpfung mit der neuen Transaktion. Es wird ein neuer Kontext erstellt. 2. Die Kunden-ID wird von der Ressource abgerufen und innerhalb der Logik gespeichert. Der Transaktionskontext verwaltet sämtliche Verweise auf die Ressource. Die Kunden-ID wird zur Bearbeitung des Kundenkontos eingesetzt. 3. Der letzte Schritt in der Logik besteht in der Ausgabe des Transaktionsbefehls COMMIT. Sämtliche der vorgenommenen Änderungen werden dauerhaft gespeichert, wenn der zweiphasige Commit-Vorgang erfolgreich verläuft. Dieser Vorgang wird noch genauer erläutert, im Moment kann er als Methode betrachtet werden, mit der die Daten Dauerhaftigkeit erlangen. Tritt ein Fehler auf, wird ein ROLLBACK-Befehl ausgegeben, der dazu führt, dass sämtliche Änderungen, die seit Ausgabe des BEGIN-Transaktionsbefehls an der Ressource vorgenommen wurden, wieder rückgängig gemacht werden. Eine flache Transaktion wird als flach bezeichnet, da sie nur einen BEGIN-Transaktionsbefehl und einen COMMIT-Transaktionsbefehl zur dauerhaften Speicherung der Daten enthalten kann. Nachdem der BEGIN-Befehl ausgegeben wurde, wird durch einen weiteren BEGIN-Befehl eine weitere, unabhängige Transaktion gestartet. Bei flachen Transaktionen ist die zeitliche Koordination wichtig, da je nach Zeitspanne, die zwischen einem BEGIN- und einem COMMIT- oder ROLLBACK-Befehl verstreicht, das System entweder skalierbar oder langsam ist. Ein skalierbares System kann eine große Anzahl Anforderungen verarbeiten, ein langsames System weist lange Wartezeiten für den Kunden auf. Eine Transaktion, die viel Zeit beansprucht, führt zu einer Ressourcensperre, und jeder ROLLBACK-Vorgang erfordert eine Reihe weiterer Schritte. Eine Gruppe von Transaktionen, die jeweils kurze Zeitspannen für die Verarbeitung benötigen, führen zu einer komplizierten Verwaltung des Ressourcenstatus. Es wurden lange und kurze Verarbeitungszeiten genannt, und es ist nicht möglich, diese Zeitspannen genau zu definieren. Die Definition von lang oder kurz richtet sich nach dem Geschäftsprozess und danach, was der Benutzer als einen langen oder kurzen Zeitraum empfindet. Eine Zeitspanne von beispielweise zwei Stunden ist jedoch definitiv ein langer Zeitraum, d.h., solche Zeitspannen sollten vermieden werden. Im Abschnitt »Aktivitäten« weiter unten in diesem Kapitel wird der Aspekt kurzer/langer Verarbeitungszeiten noch weiter ausgeführt.
296
Entwickeln von Transaktionskomponenten
10.1.3 Zweiphasiger Commit-Vorgang Sollen die durch eine Transaktion bearbeiteten Daten dauerhaft gespeichert werden, wird ein zweiphasiger Commit aufgerufen. Durch solch einen zweiphasigen Commit-Vorgang wird das Verzögerungsproblem bei der Koordination zahlreicher Ressourcen mit den Einheiten gelöst, durch die die Ressourcen bearbeitet werden. Der zweiphasige Commit-Vorgang ähnelt einer traditionellen Hochzeitszeremonie. Bei einer klassischen Hochzeit kommen die zwei Brautleute zusammen und halten sich an den Händen. Der Pastor fragt den Bräutigam, ob er gewillt ist, die Braut zur Frau zu nehmen. Der Bräutigam beantwortet die Frage mit »Ja«. Anschließend fragt der Pastor die Braut, ob Sie den Bräutigam zum Ehemann nehmen möchte. Die Braut antwortet ebenfalls mit »Ja«. Daraufhin erklärt der Pfarrer die beiden zu Mann und Frau. In einem zweiphasigen Commit entsprechen Braut und Bräutigam den Ressourcen, der Pastor dem Transaktionsmonitor. Wenden Sie nun das obige Beispiel auf ein Computerszenario an, ergeben sich für einen zweiphasigen Commit die folgenden Schritte: 1. Lokale Vorbereitung: Jede lokale Ressource bereitet sich auf den Commit vor. 2. Verteilte Vorbereitung: Senden einer Vorbereitungsanforderung an jede ausgehende Sitzung der Transaktion. 3. Entscheidung: Stimmen alle Ressourcen einem Commit für die Transaktion zu, wird ein Commit ausgegeben. 4. Commit: Auslösen jeder Ressource, Informieren der Ressourcen zum Ausgang der Entscheidung über die Commit-Ausführung und Senden der Commit-Meldung an die ausgehende Transaktionssitzung. 5. Abschluss: Haben alle Ressourcen die Commit-Meldung bestätigt, wird ein vollständiger Commit-Eintrag geschrieben. Ist die Meldung dauerhaft, werden die für die Transaktion benötigten Ressourcen wieder freigegeben. Es ist wichtig zu verstehen, dass die Transaktionen in der mittleren Schicht platziert werden. Der Transaktionskontext kann nicht auf der Darstellungsschicht gespeichert werden. Eine Ausnahme bildet hierbei der Windows 2000-Clientdesktop. Hier wird der Transaktionsdienst standardmäßig installiert und kann in einer verteilten Transaktion verwendet werden.
10.1.4 COM+-Anwendungen In Windows 2000 wurden MTS (Microsoft Transaction Server) und COM+ integriert und zu einer Komponente zusammengeführt. Diese Integration geht über einen einfachen Transaktionskontext hinaus, es handelt sich vielmehr um ein In-
Einführung in Transaktionen
297
frastrukturkonzept. Ein GUI-loses COM-Objekt (Graphical User Interface) kann ohne Weiteres eingesetzt werden. Wird es jedoch in eine so genannte COM+-Anwendung eingebunden, wird es zu einem COM+-Objekt. Ein COM+-Objekt weist die Fähigkeit zur Interaktion mit verschiedenen COM+-Diensten auf. Warum sollte eine COM+-Anwendung eingesetzt werden? Eine COM+-Anwendung basiert auf Komponenten. Innerhalb einer COM+-Anwendung sind verschiedene spezifische Elemente vorhanden, beispielsweise Transaktionen und Datenbankverbindungen. Durch das Gruppieren von COM+-Objekten kann über den COM+-Anwendungs-Manager der Cache optimiert werden, was zu besser skalierbaren und stabileren Anwendungen führt. Funktionsweise einer COM+-Anwendung Das Entwickeln von COM+-Anwendungen erfordert ein Umdenken. In einer klassischen Anwendung wird eine Hauptfunktion ausgeführt, die für die Ausführung verschiedener Aktionen verantwortlich ist, beispielsweise das Anzeigen einer Benutzeroberfläche und das Ausführen der Datenbanklogik. Bei COM+ ist keine solche Hauptschleife vorhanden. Es gibt lediglich COM-Schnittstellen, die innerhalb eines COM+-Kontextes ausgeführt werden. Die COM-Schnittstellen sind vergleichbar mit Diensten, die der Ausführung spezieller Aufgaben dienen. Alle Elemente werden als eine Reihe von COM+-Objekten offen gelegt. In Abbildung 10.1 wird die Funktionsweise einer COM+-Anwendung veranschaulicht. Innerhalb der COM+-Anwendung befinden sich eine Aktivierungs- und eine Abfangeinheit. Dieses Darstellung der COM+-Anwendungsarchitektur ist stark vereinfacht, reicht vom Standpunkt des COM+-Objektentwicklers jedoch völlig aus. Innerhalb der COM+-Anwendung befinden sich drei Elemente: Kontext, Attribute und COM-Objekt.
Abbildung 10.1 Aktivierungs- und Abfangarchitektur
298
Entwickeln von Transaktionskomponenten
Die COM+-Anwendung wurde früher als MTS-Paket bezeichnet. COM+-Anwendungen ersetzen MTS und beinhalten die verschiedenen COM-Objekte. Möchte der Client das COM-Objekt instanziieren, wird diese Instanziierung nicht direkt vorgenommen. Stattdessen wird die Instanziierung über eine so genannte Aktivierungseinheit vorgenommen. Diese Aktivierungseinheit ist dafür verantwortlich, die Instanziierung des COM-Objekts im richtigen Moment und mit dem richtigen Kontext vorzunehmen. Im Grunde handelt es sich um eine automatische Funktion. Wichtiger ist die Erstellung eines Abfangmodells. Wenn Sie einfach nur COM verwenden, hat der Konsument direkten Zugriff auf das COM-Objekt und die zugehörigen Schnittstellen. Das Problem hierbei ist, dass sich Konflikte oder Fehler direkt auf den Konsumenten auswirken. Durch das COM+-Abfangmodell wird ein gewisser Fehlerschutz bereitgestellt. Das Abfangmodell ermöglicht darüber hinaus eine dynamische Kontextabfrage. Der Kontext des COM-Objekts enthält Informationen zu der Umgebung, in der das COM-Objekt ausgeführt wird. Im Kontext werden Informationen zu Sicherheit, IIS-Verfügbarkeit (Internet Information Server) und Transaktionsfähigkeiten gespeichert. Benötigt das COM-Objekt Laufzeitinformationen, wird beim Kontext die geeignete COM-Schnittstelle angefordert. Verfügt der Kontext über diese Informationen, wird die entsprechende COM-Schnittstelle zurückgegeben. Im Vergleich zur API-Verwendung (Application Programming Interfaces) oder dem regulären COM-Modell stellt dies eine bessere Lösung dar, da der Kontext bestimmte Aktionen im Voraus erkennen und entsprechende Maßnahmen ergreifen kann. Nehmen Sie beispielsweise die Verbindungsherstellung zu einer Datenbank. Bei Verwendung eines Abfangmodells kann der Kontext Informationen zur Datenbankverbindung zwischenspeichern. Fragt der Client diese Informationen ab, kann das Offenlegen der COM-Schnittstelle sehr schnell erfolgen. In einem traditionellen API-Szenario erfordert die Verbindungsherstellung relativ viel Zeit, da nur wenige Informationen zwischengespeichert werden. Anhand der durch das COM-Objekt definierten Attribute kann der Kontext ermitteln, welche Dienste geladen werden müssen. Wenn beispielsweise das COMObjekt sagt: »Ich möchte Transaktionen«, können bei der Kontextaktivierung Informationen zum Transaktionskontext vorgeladen und verwaltet werden. Werden diese Informationen zwischengespeichert, können sie extrem schnell geladen werden.
Einführung in Transaktionen
299
Das Abfangmodell führt also zu einer allgemeinen Beschleunigung, da in der Infrastruktur ein Cache der kürzlich verwendeten Daten erstellt werden kann. Der Cache befindet sich üblicherweise im RAM und ist leichter zugänglich als die Originaldatenquelle. Dies wiederum macht die Anwendung schneller und besser skalierbar. Ein COM-Objekt, das die COM+-Dienste nutzt, muss sich jedoch an das COM+-Programmiermodell halten und den Kontext verwenden. Der Verteilte Transaktionskoordinator Der Kontext ist weder für das Verwalten der Transaktion noch für die Verwaltung des zweiphasigen Commit verantwortlich. Diese Verwaltung liegt in der Verantwortlichkeit des Verteilten Transaktionskoordinators (Distributed Transaction Coordinator, DTC). Abbildung 10.2 zeigt zwei Computer und zwei COM+-Anwendungen. Beim Laden des Kontextes besteht die Anforderung, dass dem Kontext eine Transaktion hinzugefügt wird. Der Kontext kommuniziert mit dem DTC, anschließend wird die Transaktion gestartet. Werden weitere COM+-Objekte referenziert, kommunizieren die DTCs auf den zwei Rechnern miteinander, und die Transaktion wird auf den zweiten Computer ausgeweitet. Jegliche Ressourcen, die mit der Transaktion verwendet oder verknüpft sind, werden von den zwei COM+-Objekten gemeinsam genutzt.
Abbildung 10.2 Architektur von DTC und COM+-Objekten
Die Transaktion wird nicht durch den Kontext verwaltet; der Kontext wird mit dem DTC verbunden. Der DTC stellt den Knotenpunkt zwischen den verschiedenen COM-Objekten und den Ressourcen dar. Wenn beispielsweise das neue
300
Entwickeln von Transaktionskomponenten
COM-Objekt auf eine weitere Ressource zugreifen oder eine vorhandene Ressource bearbeiten möchte, versucht der DTC, mit der neuen Einheit zu kommunizieren und sich selbst für die Transaktionsverwaltung zur Verfügung zu stellen. Der DTC bleibt hierbei passiv, da Ressourcenverteiler und Ressourcen-Manager die Kommunikation initiieren müssen. Ressourcenverteiler Stellen Sie sich vor, Sie sitzen in einem Restaurant und bestellen ein Gericht. Üblicherweise wird Ihre Bestellung durch einen Kellner aufgenommen und an die Küche weitergeleitet. Das Küchenpersonal bereitet das Gericht zu, und der Kellner bringt das Essen an Ihren Tisch, sobald es fertig ist. In gewisser Weise erhalten Sie durch den Kellner Zugriff auf die Küchenressource. Der Kellner stellt den Ressourcenverteiler dar, der Gast ist das COM-Objekt. Das COM-Objekt hat bei der Verbindungsherstellung zur Ressource keinerlei Informationen darüber, woher die Ressource stammt. Es werden lediglich Informationen empfangen, genauso, wie der Gast im Restaurant sein Essen erhält. Prinzipiell könnten die Lebensmittel von einem Standort aus versandt worden sein, der sich mehrere Zeitzonen entfernt befindet. Nach dem Empfang der Daten kann das COM-Objekt nach Belieben mit diesen Daten verfahren, genau wie es den Kellner nicht interessiert, ob Sie das Essen direkt im Restaurant verspeisen oder es draußen Ihrem Hund geben usw. Der Ressourcenverteiler sorgt lediglich für die Verwaltung der verschiedenen Verbindungen und versucht, den Zugriff auf die Ressourcen zu optimieren. Als Beispiele für Ressourcenverteiler können der Manager für den gemeinsamen Speicherzugriff und ODBC (Open Database Connectivity) genannt werden. Ressourcen-Manager Stellen Sie sich eine Situation vor, in der der Kellner alle Gerichte ausgegeben hat. Sobald der Gast seine Mahlzeit beendet hat, muss er für diese zahlen. Der Kellner kehrt mit einer Rechnung an den Tisch zurück, auf der alle Speisen und Getränke aufgeführt werden, die der Gast verzehrt hat. Diese Auflistung ist mit den Statusinformationen vergleichbar. Wo wurden die Statusinformationen gespeichert, als die ursprüngliche Bestellung erfolgte? Im Restaurant wurden diese Informationen auf einem Block notiert oder in einen Computer eingegeben, nicht jedoch lediglich im Gedächtnis des Kellners gespeichert, denn der Kellner hat viele Aufgaben zu erledigen. Im COM-Szenario werden die Statusinformationen zu den Daten durch den Ressourcen-Manager verwaltet. Der Ressourcen-Manager ist nicht an der Verteilung der Ressourcen beteiligt, diese Aufgabe wird durch den Ressourcenverteiler erledigt. Ein typischer Ressourcen-Manager ist beispielsweise SQL Server (Structured Query Language). Das Schreiben eines Ressourcen-Managers ist sehr viel komplizierter als das Entwickeln eines Ressourcenverteilers, da durch den Ressourcen-Manager das Sta-
Einführung in Transaktionen
301
tusproblem gelöst werden muss. Eine effiziente Statusverwaltung ist nicht leicht zu erzielen. Der Manager für den gemeinsamen Speicherzugriff ist ein Ressourcenverteiler, auch wenn er die Aufgaben eines Ressourcen-Managers zu übernehmen scheint. Es handelt sich nicht um einen Ressourcen-Manager, da bei einem Absturz die Statusinformationen nicht beibehalten werden. Kehren wir zum Restaurantbeispiel zurück. Der Kellner erinnert sich vielleicht an bestimmte Dinge – beispielsweise daran, wie viele Personen an einem Tisch sitzen, oder daran, wie viele Suppenlöffel benötigt werden. Der Kellner wird sich jedoch nicht daran erinnern, wie viele Kartoffeln aus dem Vorratsraum verbraucht wurden. Für diese Art von Aufgaben ist der Ressourcen-Manager verantwortlich.
10.2 Gute COM+-Transaktionsobjekte Ein gutes COM+-Transaktionsobjekt ist ein Objekt, das einen Kontext verwendet und Ressourcen nicht länger als nötig beansprucht. Ressourcen werden nach Bedarf verwaltet und umgehend freigegeben, wenn Sie nicht mehr benötigt werden. COM+-Objekte sind statusverwaltet, d.h., sie sind weder statuslos noch persistent. Die Erfahrung zeigt, dass sich dieser Ansatz optimal zur Entwicklung skalierbarer, stabiler Anwendungen eignet. Sehen wir uns zum besseren Verständnis dieses Ansatzes an, wie in der Vergangenheit verfahren wurde. Ein Stück Geschichte Stellen Sie sich vor, es gäbe keine COM+-Dienste. Es werden ausschließlich traditionelle Tools verwendet, von denen einige von unterschiedlichen Herstellern stammen. Abbildung 10.3 zeigt ein Objekt namens Client. Das Objekt Benutzer repräsentiert einige Daten, die bei der Anmeldung von Client an der Website verwendet werden. In seiner einfachsten Form enthält Benutzer nur Informationen wie Name, E-Mail-Adresse und Kennwort. Das Objekt Client dagegen kann weitere Daten enthalten, beispielsweise Adresse, Konferenzteilnehmer usw. Die Frage ist, wie das Benutzer-Objekt bei der Instanziierung von Client instanziiert wird.
Client
Client
Benutzer Datenbank
Client
Abbildung 10.3 Beispielwebarchitektur
302
Entwickeln von Transaktionskomponenten
Benutzer
In einem rein objektorientierten Kontext lautet die Antwort, dass die Daten von einer objektorientierten Datenbank abgerufen und anschließend in den Speicher verschoben werden. Während die verschiedenen Operationen durch die unterschiedlichen Clients ausgeführt werden, verbleibt das Benutzer-Objekt im Speicher. Dies scheint in Ordnung zu sein, da bei Verwenden von SQL die Daten vom SQLDatenbankformat in ein systemeigenes Format konvertiert werden müssen. Bei einer objektorientierten Datenbank ist dies nicht notwendig, daher ist alles etwas einfacher. Bei beiden Ansätzen werden die Daten in den Speicher verschoben und dort beibehalten. Dieses Vorgehen stellt einen Fehler im Programmentwurf dar. Befinden sich Daten im Speicher, sind zwei Speicherkopien vorhanden. So entstehen Probleme hinsichtlich des Cache und des parallelen Zugriffs. Was geschieht beispielsweise, wenn ein gleichzeitiger Zugriff erfolgt? Wenn das erste Client-Objekt instanziiert wird, werden die Benutzer-Daten in den Speicher geladen. Wird ein zweites Client-Objekt instanziiert und referenziert die gleichen Benutzer-Daten, was geschieht mit der zweiten Client-Anforderung? Wird ein Handle auf die Benutzer-Instanz im Speicher ausgegeben, oder erfolgt eine neue Instanziierung des Benutzer-Objekts? Untersuchen wir beide Möglichkeiten. Im Falle der Ausgabe eines Handles für das im Speicher vorliegende Objekt treten Zugriffsprobleme auf, die durch den Programmierer der Client- und BenutzerObjekte gelöst werden müssen. Was geschieht, wenn Client 1 die Adresse ändert und Client 2 eine erneute Adressänderung vornimmt? Welche Adresse ist die richtige? Wenn eine neue Kopie der Benutzer-Daten erstellt wird, treten keine Probleme hinsichtlich des parallelen Zugriffs auf, da jeder Client über eine eigene Kopie der Daten verfügt. Für eine zusätzliche Kopie der Daten werden jedoch auch zusätzliche Ressourcen benötigt – das Instanziieren des Objekts erfordert zusätzliche Verarbeitungszeit. Wird das Objekt durch einen der Clients geändert, werden diese Änderungen von dem jeweils anderen Client nicht wahrgenommen, da jeder Client das Objekt für die Lebensdauer der Anwendung instanziiert. In beiden Szenarien werden die Objekte im Speicher beibehalten. Stellen Sie sich nun vor, dass der Computer abstürzt, nachdem das Objekt bearbeitet wurde. Vielleicht wurde die Ressource bereits aktualisiert, vielleicht jedoch auch nicht; der Absturz führt in jedem Fall dazu, dass alle Daten aus dem Speicher gelöscht werden. Wurde keine Speicherung vorgenommen, müssen die Schritte erneut ausgeführt werden.
Gute COM+-Transaktionsobjekte
303
Zur Lösung dieser Probleme könnte Code geschrieben werden, aber weder das Schreiben noch das Debuggen dieser Art von Code ist einfach. Diese Form der zusätzlichen Programmierung ist nicht zu empfehlen, da der Entwickler Systemcode schreibt, der nichts mit der Lösung zu tun hat, sondern lediglich als Framework für den Anwendungscode dient. Darüber hinaus sollte Systemcode besser von Unternehmen geschrieben werden, die sich auf derartige Produkte spezialisiert haben. In einem Windows DNA-Szenario besteht das Ziel beim Schreiben von Anwendungscode darin, einen vorgegebenen Geschäftsprozess zu lösen.
10.2.1 Statusverwaltete Objekte In einer COM+-Umgebung werden statusverwaltete Objekte geschrieben. Über den Kontext wird ein Cache verwaltet, der die Ressourcen kennt, die vom COM+Objekt benötigt werden. Ein statusverwaltetes Objekt führt spezifische Operationen aus und entfernt sich dann selbst vollständig. Mit anderen Worten, es werden beständig COM+-Objekte instanziiert und zerstört. Dies stellt kein Problem dar, da in COM+ sämtliche Informationen zwischengespeichert werden. Die Instanziierungszeit richtet sich daher stark nach der Initialisierungszeit für die COM+-Objekte. Wird hierzu wenig Zeit benötigt, liegt ein schnelles System vor. Für COM+ ist ein gewisser Overhead erforderlich, dieser ist jedoch im Vergleich zu der Zeit, die zur Ausführung der Geschäftsoperationen erforderlich ist, minimal. In einer statusverwalteten Anwendung muss das Client-COM-Objekt einige Statusinformationen an das COM+-Objekt weiterleiten. Diese wenigen Informationen werden zur Erstellung eines neuen Status für die neuen Geschäftsprozesse eingesetzt. Bei Verwendung des COM+-Kontextes gibt es jedoch einen Haken. Ist die bearbeitete Ressource nicht in der Lage, den COM+-Kontext oder COM+-Transaktionen zu verwenden, und werden die zugehörigen Informationen nicht zwischengespeichert, wird die COM+-Anwendung nicht schneller. Sie kann sogar langsamer werden. JIT-Aktivierung (Just-in-Time) Kommuniziert ein Konsument mit einem COM-Objekt, muss zwischen den beiden Elementen eine COM-Laufzeitumgebung erstellt werden. Sie benötigen beispielsweise einen Proxy und einen Stub sowie einige Kommunikationsinformationen, um eine COM-Kommunikationsinfrastruktur zu erstellen, wie dargestellt in Abbildung 10.4.
304
Entwickeln von Transaktionskomponenten
Konsument
Proxy
COM+-Objekt
COM
Stub
Abbildung 10.4 COM-Kommunikationsinfrastruktur
In COM+ werden weiterhin die Proxy- und Stubinformationen verwendet, jedoch zwischengespeichert, wodurch die Verweise auf COM+-Objekte schneller erfolgen. Diese Zwischenspeicherung der COM+-Kommunikationsinfrastruktur wird JIT-Aktivierung (Just-in-Time) genannt. Beim Starten einer Transaktion werden über die COM+-Infrastruktur ein Proxy, ein Stub und die COM+-Objektimplementierung erstellt. Nach Beendigung der Transaktion wird das COM+-Objekt zerstört, Proxy und Stub dagegen werden zwischengespeichert. Der Konsument nimmt an, dass er weiterhin über einen Verweis auf das COM+-Objekt verfügt. Wird dieser Verweis verwendet, wird eine neue Transaktion gestartet und ein neues COM+-Objekt instanziiert. Der Konsument weiß hierbei nicht, was im Hintergrund geschieht. Das COM+-Objekt wird immer dann instanziiert und verwendet, wenn es benötigt wird. Die Länge der Transaktion definiert die Lebensdauer des jeweiligen COM+-Objekts. Da die gleiche Kommunikationsinfrastruktur wieder verwendet wird, erfolgt die Instanziierung des COM+-Objekts erheblich schneller. Die Wahrheit über das Objektrecycling In der COM+-Infrastruktur findet standardmäßig keine Wiederverwendung der COM+-Objekte statt. Für jede Transaktion wird ein neues Objekt instanziiert. Nach Abschluss einer Transaktion wird das verwendete Objekt zerstört. In der Vergangenheit ließ Microsoft verlauten, dass in zukünftigen MTS-Versionen die Wiederverwendung von Objekten eingeführt werden sollte. Diese Funktion wurde auch tatsächlich implementiert, jedoch in einer etwas anderen Form als erwartet. Der Zweck des COM+-Objektrecycling bestand darin, eine Anwendung schneller zu machen. Betrachten Sie jedoch das Modell, in dem die Wiederverwendung von COM+-Objekten implementiert wurde. In diesem Modell gibt es eine Objektinitialisierung und eine COM+-Initialisierung. Es entsteht ein komplexes Programmiermodell, da zwei Initialisierungsroutinen vorhanden sind. Die Auswahl
Gute COM+-Transaktionsobjekte
305
der zu verwendenden Initialisierungsroutine kann nicht leicht getroffen werden. Das gleiche Problem gilt für die Zerstörungsphase, da ebenfalls zwei Zerstörungsroutinen vorhanden sind. Die zu beantwortende Frage in diesem Recyclingmodell lautet: Ergibt sich aus dem Objektrecycling ein Vorteil hinsichtlich der Geschwindigkeit? Die Antwort lautet Ja, denn es muss keine COM-Kommunikationsinfrastruktur erstellt werden. Der Initialisierungsschritt ist weiterhin erforderlich, da bei der Wiederverwendung von Objekten die Objekte initialisiert werden müssen. Vergleichen wir dies mit einer Lösung, bei der die JIT-Aktivierung verwendet wird. Bei der Objektreferenzierung hat die COM+-Infrastruktur die benötigten Informationen bereits zwischengespeichert, beispielsweise Class Factory und Datenbankverbindungen. Es muss lediglich das Objekt neu erstellt werden. Auf den ersten Blick scheint das Objektrecycling die schnellere Lösung darzustellen. Die Wahrheit ist jedoch, dass beim Objektrecycling der Objektstatus neu eingestellt werden muss, und in den meisten Fällen erfordert dieser Vorgang genauso viel Zeit wie das Neuerstellen eines Objekts. Daher funktioniert das Objektrecycling zwar, eine intelligente Infrastruktur erfüllt jedoch den gleichen Zweck und ist einfacher zu programmieren. Der einzige Fall, in dem das Objektrecycling Vorteile bietet, ist bei der Verwendung von langsamen Ressourcen. In diesen Situationen erfordert die Initialisierung von COM+-Objekten sehr viel Zeit; das Objektrecycling stellt die schnellere Lösung dar. COM+ unterstützt das Objektrecycling, erfordert jedoch, dass das COM+-Objekt die Transaktion und die Verknüpfung zwischen Transaktion und Ressource verwaltet. Dies ist nicht schwierig, erfordert jedoch einen zusätzlichen Schritt. Kurz gesagt, das Objektrecycling bietet in den meisten Fällen keine Vorteile hinsichtlich der Leistung, und das Instanziieren und Zerstören einzelner Objekte führt zum einfacheren Programmiermodell. Aktivitäten Da Objekte zu Beginn einer Transaktion instanziiert und am Ende einer Transaktion zerstört werden, kann das Programmiermodell keinen dauerhaften Status voraussetzen. Das Objekt muss statusverwaltet sein, und die einfachste Methode hierzu stellt das Definieren von Aktivitäten dar. Eine Aktivität folgt der gleichen Regel wie das Schreiben eines Absatzes. Ein Absatz ist eine Gruppierung von Sätzen, die ein einzelnes Konzept behandeln. Das Besprechen eines neuen Konzepts geht mit dem Beginn eines neuen Absatzes
306
Entwickeln von Transaktionskomponenten
einher. Das Thema des neuen Absatzes wird nicht willkürlich gewählt, sondert baut auf den Gedanken auf, die im vorangegangenen Absatz erläutert wurden. Das gleiche gilt für Aktivitäten. Eine Aktivität ist die Ausführung eines Geschäftsprozesses, und mehrere Aktivitäten zusammen bilden eine Anwendung. Zwischen den verschiedenen Aktivitäten muss eine kleine Menge globaler Statusinformationen vorliegen, um einen Kontext für jede Aktivität zu erzeugen. Die Mehrzahl der Aktivitäten sollte nur wenig Zeit beanspruchen und nur Minuten oder Sekunden dauern. Sie sollten niemals Stunden in Anspruch nehmen, da so unnötige Ressourcensperren entstehen. Entwurf für Aktivitäten Das Entwickeln eines aktivitätsbasierten Objekts unterscheidet sich stark vom Entwerfen einer rein objektorientierten Anwendung. Mit Aktivitäten werden spezifische Aufgaben ausgeführt, d.h., im Mittelpunkt des Entwurfs stehen nicht Substantive, durch die Objekte repräsentiert werden. Stattdessen werden mit Hilfe von Verben Aufgaben beschrieben, die im Mittelpunkt des Objektentwurfs stehen. Betrachten wir das Konferenzanmeldungsprojekt im Hinblick darauf, dass dieses aktivitätsbasiert ist. Sehen Sie sich zunächst das in Abbildung 10.5 gezeigte UMLKlassenmodell (Unified Modeling Language) an, da ich dieses zur Erläuterung des Aktivitätskonzepts heranziehen werde. Das Objektmodell stammt aus Kapitel 9, in dem ein Satz Daten- und Operationsklassen erstellt wurde. Hierbei wurde aus Gründen der Einfachheit die Verifizierungsklasse weggelassen. Es sind drei Datenklassen vorhanden, dClient, dUser und dAddress. Diese Klassen enthalten sämtliche Daten, die in der Anwendung eingesetzt werden. Sie wurden mit Hilfe eines auf Substantiven basierenden, objektorientierten Entwurfs konzipiert. Zur Umsetzung der Aktivitäten müssen Operationsklassen entworfen werden. Das Modell weist vier Operationsklassen auf, Registration, tasks, SponsoredUser und UserSite. Diese Klassen basieren auf den vier Hauptaktivitäten, die für die Konferenzanmeldungsanwendung gefordert werden, Anmeldung, Aufgaben, Verwaltung gesponsorter Benutzer, Benutzerverwaltung. Die Klassennamen sind zwar ebenfalls Substantive, basieren jedoch auf Aktivitäten. Zur weiteren Verdeutlichung könnten die Klassen Register, AccomplishTasks, ManageSponsoredUser und ManageUser genannt werden. In jedem Fall handelt es sich um einen aktivitätsbasierten Entwurf. Sehen Sie sich die Klasse Registration genauer an. Diese Klasse enthält eine Reihe von Methoden, mit denen Operationen ausgeführt werden, beispielsweise das
Gute COM+-Transaktionsobjekte
307
Hinzufügen, Aktualisieren und Löschen. Diese Methoden sind Operationen, die in Transaktionen kombiniert werden können.
Abbildung 10.5 Objektmodell der Webanwendung für die Konferenzanmeldung
Beim Entwerfen eines aktivitätsbasierten Klassenmodells sollten Sie folgendermaßen vorgehen: 왘 Definieren Sie alle substantivischen Datenobjekte im Entwurf. Diese Informati-
onen können üblicherweise dem Domänenmodell und den Anwendungsfällen entnommen werden. 왘 Versuchen Sie, mehrere Sequenz- oder Kollaborationsdiagramme zu gruppie-
ren und Ähnlichkeiten zu ermitteln. Sie möchten eine Reihe von Operations-
308
Entwickeln von Transaktionskomponenten
klassen erstellen, mit denen die verschiedenen Operationen der Anwendung umgesetzt werden. 왘 Kombinieren Sie die verschiedenen Daten- und Operationsobjekte, und kon-
vertieren Sie diese in Transaktionen. Auf diese Weise können die Sequenz- und Kollaborationsdiagramme umgesetzt werden. Basierend auf diesen Transaktionen erhalten Sie einen Satz Statuswerte, die Ihre Verifizierungsobjekte repräsentieren. Beim Durchlaufen dieses Prozesses sollten Sie keine Shortcuts verwenden, d.h., weisen Sie nicht ein Operationsobjekt pro Sequenz- oder Kollaborationsdiagramm zu. So wird lediglich der Code aufgebläht, und Sie erhalten keine zufriedenstellende Anwendung. Setzen Sie stattdessen Ihren gesunden Menschenverstand und Ihr Urteilsvermögen ein. Jede Anwendung ist einmalig und erfordert einen individuellen Entwurf. Sollten Ihnen die vorgenannten Schritte nicht angemessen für Ihr Projekt erscheinen, dann verwenden Sie sie auch nicht. In meiner persönlichen Erfahrung haben sich diese Schritte jedoch als nützlich erwiesen.
10.3 Schreiben von Transaktionskomponenten Die hier geschriebenen COM+-Objekte dienen nicht dem Datenzugriff. Dieses Thema wird in einem späteren Kapitel behandelt. Die hier erstellen COM+-Objekte werden lediglich zur Datenbearbeitung eingesetzt. Dies wirft die Frage auf, ob die Transaktionsdienste überhaupt benötigt werden, wenn keine Ressource verwendet werden soll. Die Antwort lautet: wahrscheinlich nicht. Die in Windows 2000 enthaltenen Transaktionsdienste bieten mehr als nur Transaktionsfähigkeiten, es werden auch Stabilität und COM-Objektverwaltungsfunktionen bereitgestellt. Dies ist jedoch nicht so wichtig, wenn die COM-Objekte ordnungsgemäß geschrieben wurden (wenn sie einen Fehlerbehandlungsmechanismus aufweisen, eine Datenprüfung durchführen usw.). In dieser Situation ist es nicht unbedingt erforderlich, die COM+-Transaktionsdienste zu nutzen. Basierend auf den vorangegangenen Erläuterungen können einige Regeln hinsichtlich der Entwicklung von COM+-Transaktionsobjekten aufgestellt werden: 왘 Alle COM+-Objekte müssen statusverwaltet sein. Dies bedeutet, dass es sich
bei deren Aktivierung immer um neue Objekte handelt. 왘 Alle COM+-Objekte werden über das COM+-Framework oder eine COM+-
Anwendung initialisiert. Setzen Sie zum Zurücksetzen privater Objektvariablen nur die Erstellungsroutine oder die Objektinitialisierung ein.
Schreiben von Transaktionskomponenten
309
왘 Alle COM+-Objekte erhalten Ressourcen erst spät und geben diese umgehend
wieder frei, um Skalierbarkeit zu gewährleisten. Aufgrund des COM+-Caching und der Optimierung können Datenbankverbindungen auf Ebene eines Methodenaufrufs abgerufen werden. 왘 Alle COM+-Objekte setzen bei der Ausführung von Aufgaben Aktivitäten ein.
Die Aktivitäten stehen in direktem Zusammenhang mit Geschäftsoperationen.
10.3.1 Deklarieren eines COM+-Transaktionsobjekts Beim Schreiben eines COM+-Objekts muss dieses unbedingt als transaktional deklariert werden. Dies kann auf IDL-Ebene (Interface Definition Language) erreicht werden. Im Attributabschnitt einer Schnittstelle wird der Transaktionstyp definiert. Hierzu ein Beispiel: [ object, uuid(E0B99A70-324D-11D3-868C-0080C700807A), dual, helpstring("IExTransactionVC Interface"), pointer_default(unique), TRANSACTION_REQUIRED ] interface IExTransactionVC : IDispatch { [id(1), helpstring("method method1")] HRESULT method1(); };
In dieser Beispielschnittstelle wird das Attribut auf transaction_required gesetzt. Auf diese Weise wird in der Visual C++-Umgebung ein Transaktionsattribut angegeben. In der Visual J++-Programmierumgebung wird das Transaktionsattribut auf Klassenebene spezifiziert. Klicken Sie in der Klassenansicht mit der rechten Maustaste auf die betreffende Klasse, und klicken Sie auf die Schaltfäche für die Klasseneigenschaften. Ein Dialogfeld, ähnlich dem in Abbildung 10.6, wird geöffnet. Hier können Sie das erforderliche Transaktionsattribut auswählen. In Visual Basic und Visual J++ können die Transaktionsattribute auf Klassenebene gesetzt werden. Ist im Dialogfeld das erforderliche Transaktionsattribut nicht aktiviert, muss zunächst COM aktiviert werden.
310
Entwickeln von Transaktionskomponenten
Abbildung 10.6 Transaktionsattribute in Visual J++
In der Visual Basic-Programmierumgebung wird das Transaktionsattribut auf Klassenebene spezifiziert. Das Transaktionsattribut befindet sich im Listenfeld Eigenschaften, siehe auch Abbildung 10.7.
Abbildung 10.7 Transaktionsattribut in Visual Basic
Schreiben von Transaktionskomponenten
311
Die verschiedenen Transaktionstypen Es ist nicht erforderlich, das Transaktionsattribut in der Programmierumgebung zu deklarieren, dies kann auf administrativer Ebene geschehen. Beim Importieren eines COM+-Objekts in die Komponentendienste kann der Transaktionstyp deklariert werden. Siehe hierzu Abbildung 10.8.
Abbildung 10.8 Transaktionsattribute in den Komponentendiensten
Die vorangegangenen Abbildungen zeigen, dass verschiedene Arten von Transaktionsattributen vorhanden sind. Stellen Sie sich vor, ein COM+-Objekt wird durch einen Konsumenten referenziert – es wird eine Beziehung zwischen dem Konsumenten und dem COM+-Objekt eingerichtet. Bedeutet diese Beziehung, dass die gleiche Transaktion gemeinsam genutzt wird? Was geschieht, wenn derzeit für den Konsumenten keine Transaktion ausgeführt wird? Die Art der Beziehung bestimmt das Transaktionsattribut. Es gibt folgende Möglichkeiten: 왘 Deaktiviert: Es handelt sich um eine Ein/Aus-Option, die auf Aus eingestellt
ist, d.h., die COM+-Transaktion kann nicht verwendet werden. Es wird möglicherweise eine Transaktion verwendet, COM+ sollte diese jedoch weder beeinflussen noch eine automatische Anmeldung an einer Transaktion vornehmen. Die Transaktions-ID wird weiterhin gesendet und stellt einen Bestandteil des gesamten Transaktionsstreams dar. 왘 Nicht unterstützt: Das COM+-Objekt akzeptiert keinen Transaktionskontext.
312
Entwickeln von Transaktionskomponenten
왘 Erforderlich: Es ist ein Transaktionskontext erforderlich. Weist der Konsument
einen Transaktionskontext auf, nutzen der Konsument und das gerade instanziierte COM+-Objekt diesen Kontext gemeinsam. Andernfalls wird ein neuer Transaktionskontext gestartet. 왘 Erfordert neu: Eine neue Transaktion wird gestartet, unabhängig davon, ob be-
reits eine Transaktion vorhanden ist oder nicht. Verwechseln Sie dies nicht mit der Verschachtelung, da die neue Transaktion sich nicht auf die derzeit ausgeführten Transaktionen auswirkt. 왘 Unterstützt: Weist der Konsument einen Transaktionskontext auf, nutzen der
Konsument und das gerade instanziierte COM+-Objekt diesen Kontext gemeinsam. Andernfalls findet keine gemeinsame Nutzung des Transaktionskontextes statt. Es mag so erscheinen, als ob die Optionen Deaktiviert und Nicht unterstützt identisch seien, da in beiden Fällen das COM+-Objekt keine gemeinsame Nutzung des Transaktionskontextes vorsieht. Der Unterschied besteht darin, dass mit Deaktiviert die manuelle Justierung der Transaktion aktiviert wird. Es gibt einige wenige Situationen, in denen es wünschenswert sein kann, die DTC-Transaktion manuell zu steuern, beispielsweise bei der Optimierung des Ressourcenzugriffs. Durch Einstellen des Transaktionsattributs auf Deaktiviert ist es möglich, keine Transaktionen zu verwenden, jedoch weiterhin die Vorteile einer COM+-Anwendung zu nutzen, z.B. JIT. Synchronisierung Stellen Sie sich vor, Sie schreiben ein COM+-Objekt, das Bestandteil einer Transaktion ist. Nehmen Sie an, dieses COM+-Objekt instanziiert zwei weitere COM+Objekte, jedes dieser Objekte wird jedoch in einer separaten Transaktion ausgeführt. Daraus folgt, dass gleichzeitig drei verschiedene Transaktionen ausgeführt werden. In unserer Anwendung führen wir aus Gründen der Skalierbarkeit mehrere Threads aus. Wie wirkt sich diese Tatsache aus, wenn sich die drei COM+Objekte gegenseitig aufrufen? Die Antwort hängt vom verwendeten COM-Threadingmodell ab. Da das COMThreading im Rahmen dieses Buches nicht besprochen werden kann, soll nur erwähnt werden, dass über das Threadingmodell definiert wird, wie COM-Objekte mit Multithreadinganwendungen interagieren. Werden die COM+-Objekte in einem Apartmentmodell mit freien Threads ausgeführt, rufen sie einander ohne Einschränkung direkt auf. Dies ist nicht wünschenswert, da das aufgerufene COM+-Objekt mit einer Prozessausführung beschäftigt sein könnte und daher nicht unterbrochen werden sollte. Die Lösung für dieses Problem stellt das Schreiben von Synchronisierungscode dar.
Schreiben von Transaktionskomponenten
313
Wenn Sie keinen Synchronisierungscode schreiben möchten, können die COM+Objekte auch im Apartmentmodell ausgeführt werden, einem COM-Threadingmodell, bei dem eine Aufrufsynchronisierung erfolgt. Hierbei werden die Aufrufe für das COM+-Objekt in Warteschlange gestellt. Dieses Vorgehen ist akzeptabel, jedoch nicht optimal. Es wird zu viel Verarbeitungszeit benötigt, da das COM+Objekt einen Threadkontextwechsel ausführen muss. Eine mögliche Lösung für diesen Kontextwechsel stellt NA dar (neutrales Apartmentmodell), ein weiteres COM-Threadingmodell. Wird ein COM+-Objekt innerhalb eines neutralen Apartments ausgeführt, findet keine Ausführung innerhalb eines spezifischen Threads statt. Der für die Ausführung des Objekts zuständige Thread wird zum Zeitpunkt des Aufrufs bestimmt. Zurück zum Beispiel mit den drei verschiedenen COM+-Objekten. Beachten Sie, dass diese in drei verschiedenen Transaktionskontexten ausgeführt werden. Versucht eines der Objekte, ein anderes COM+-Objekt zu bearbeiten, wird die betreffende Transaktion beeinflusst. Ein COM+-Objekt könnte das Objekt anweisen, die Transaktion zu übergeben (einen Commit auszuführen), das andere Objekt könnte das COM+-Objekt anweisen, die Transaktion abzubrechen (Rollback). Dies bedeutet, dass erneut Code für die parallele Nutzung geschrieben werden muss, wenn COM+ nicht eine einfachere Lösung bereitstellen würde. Bei der COM+-Lösung kommt die Synchronisierung zum Einsatz. Bei der Synchronisierung wird ein COM+-Objekt in einem Apartment gesperrt, jedoch über das Appartment hinaus zum Einschluss der Aktivität erweitert, im vorliegenden Fall die Transaktion. Die Transaktion wiederum kann sich auf weitere Computer ausdehnen. So können aufrufende Objekte und Aktionen »ausgesperrt« werden, die zu einem Fehler in der Transaktion führen könnten. Abbildung 10.9 zeigt das Dialogfeld im COM+-Explorer (Komponentendienste), mit dem Sie die verschiedenen Synchronisierungeinstellungen anpassen können. COM+ weist fünf Synchronisierungseinstellungen auf: 왘 Deaktiviert: Bei der Instanziierung des COM+-Objekts werden die Synchroni-
sierungsattribute ignoriert. Dies bedeutet, dass das COM+-Objekt in einem eigenen Kontext ausgeführt wird. Es ist wichtig, dass diese Objekttypen keine Ressourcen verwenden. 왘 Nicht unterstützt: Die instanziierten COM+-Objekte nehmen niemals an der
Synchronisierung teil. 왘 Unterstützt: Ist ein Synchronisierungskontext vorgesehen, wird dieser verwen-
det. Ist kein Synchronisierungskontext vorhanden, wird auch keiner verwendet.
314
Entwickeln von Transaktionskomponenten
왘 Erforderlich: Es ist ein Synchronisierungskontext erforderlich. Ist kein Synchro-
nisierungskontext vorhanden, wird einer erstellt. Bei Verwendung in einem Transaktionskontext mit JIT stellt diese Einstellung den Standardwert dar. 왘 Erfordert neu: Bei der Instanziierung des COM+-Objekts wird ein neuer Syn-
chronisierungskontext erstellt.
Abbildung 10.9 COM+-Synchronisierungsattribute
Die Synchronisierungseinstellungen richten sich nach dem Transaktionsattribut. Lautet das Transaktionsattribut beispielsweise Unterstützt, Erforderlich, kann das Synchronisierungsattribut nur Erforderlich lauten. Lautet das Transaktionsattribut Neuer Kontext erforderlich, kann das Synchronisierungsattribut nur Erforderlich oder Erfordert neu lauten. Daraus folgt, dass bei der Erstellung von COM+-Transaktionsobjekten keine Optionen für die Synchronisierung vorhanden sind. Die Synchronisierung kann nur angepasst werden, wenn ein Transaktionskontext vorhanden ist. Die Synchronisierung ist nützlich, da durch sie COM-Objekte geschrieben werden können, ohne dass Win32-Synchronisierungscode erforderlich ist. Diese Methode der Synchronisierung ist einfacher.
Schreiben von Transaktionskomponenten
315
10.3.2 Mehrere Szenarien mit COM+-Objekttransaktionen Es ist wichtig, die Auswirkungen bei der Kombination von COM+-Objekten zu berücksichtigen. Die Transaktionsattribute in Abbildung 10.10 lauten folgendermaßen: 왘 Erforderlich: COM+-Objekte A, D, F 왘 Neue Transaktion: COM+-Objekt B 왘 Unterstützt Transaktionen: COM+-Objekt C 왘 Keine Transaktionsunterstützung: COM+-Objekt E 왘 Deaktiviert: COM+-Objekt G
Objekt B
Konsument erstellt A A erstellt B und C B erstellt D und G C erstellt E und F
Objekt D
Objekt G Objekt A Objekt E Erfordert Transaktion (A,D,F)
Objekt C
Erfordert neue Transaktion (B) Unterstützt Transaktionen (C) Unterstützt keine Transaktionen (E)
Objekt F
Deaktiviert (G)
Abbildung 10.10 Beispielszenarion mit mehreren COM+-Transaktionen
Der Konsument erstellt COM+-Objekt A. Als Ergebnis wird ein neuer Transaktionsstream gestartet. COM+-Objekt A erfordert einen Transaktionskontext, und da der Konsument keinen Transaktionskontext aufweist, wird ein neuer Kontext erstellt. COM+-Objekt A ist ein besonders Objekt, da es sich im Stamm des Transaktionsstreams befindet. Als Nächstes instanziiert COM+-Objekt A die COM+-Objekte B und C. COM+Objekt B weist das Transaktionsattribut Neue Transaktion auf. Dies bedeutet, dass ein neuer Transaktionskontext gestartet wird, der zum Stamm des neuen Transaktionsstreams wird. COM+-Objekt C besitzt das Transaktionsattribut Unterstützt Transaktionen. Da COM+-Objekt A einen Transaktionskontext aufweist, nutzt COM+-Objekt C diesen Kontext gemeinsam mit COM+-Objekt A, COM+Objekt C wird dem Transaktionsstream von COM+-Objek tA hinzugefügt.
316
Entwickeln von Transaktionskomponenten
COM+-Objekt B instanziiert die Objekte D und G. COM+-Objekt D besitzt das Attribut Erforderlich, d.h., das Objekt wird dem Transaktionsstream von COM+Objekt B hinzugefügt. COM+-Objekt G weist das Attribut Deaktiviert auf, d.h., es wird nicht zum Bestandteil eines Transaktionsstreams. Theoretisch wäre dies zwar möglich, diese Entscheidung wird jedoch von Objekt G getroffen; COM+ entscheidet nicht, wie Kontext und Transaktionsstream verwaltet werden. COM+-Objekt C instanziiert die Objekte E und F. COM+-Objekt E besitzt das Transaktionsattribut Unterstützt keine Transaktionen, daher ist es kein Bestandteil eines Transaktionsstreams. COM+-Objekt F besitzt das Transaktionsattribut Erfordert Transaktionen. Da COM+-Objekt C einen Kontext aufweist, nutzt Objekt F diesen Kontext ebenfalls und stellt einen Teil des durch COM+-Objekt A erstellten Transaktionsstreams dar. Stamm des Transaktionsstreams Sowohl COM+-Objekt A als auch Objekt B stellen den Stamm des zugehörigen Transaktionsstreams dar. Dies ist wichtig, da über den Stamm die Gültigkeit des Streams und der darin enthaltenen Objekte sichergestellt wird. Nehmen Sie an, der Konsument speichert COM+-Objekt F als Verweis. Anschließend löscht der Konsument den Verweis auf COM+-Objekt A. Da es sich bei COM+-Objekt A um den Stamm des Transaktionsstreams handelt, werden alle COM+-Objekte im Stream ebenfalls deaktiviert. Dies bedeutet, dass bei einem späteren Referenzierungsversuch auf COM+-Objekt F ein Ausführungsfehler auftritt. Verwenden mehrerer Transaktionsstreams Bei der Verwendung mehrerer Transaktionsstreams können Probleme auftreten. Stellen Sie sich folgende Situation vor. Es sind zwei Transaktionsstreams vorhanden, jeder Stream bearbeitet einige Daten. Der erste Stream bearbeitet Datensatz A, durch den zweiten Stream wird Datensatz B geändert. Werden hierbei durch die Aktivität die Daten des jeweils anderen Streams aktualisiert, tritt ein so genannter Deadlock, eine Blockierung auf. Ein Deadlock tritt auf, wenn zwei Elemente Sperren für eine Ressource einrichten, auf die das andere Element ebenfalls zugreifen möchte. Da beide Elemente die zugehörige Sperre nicht aufheben, verbleiben beide Elemente im Wartezustand. Beide Elemente sind blockiert. Dieser Zustand tritt in COM+ üblicherweise nicht ein. Betrachten wir hierzu das Vorgehen zum Auflösen eines Deadlocks. Ein Deadlock kann auf zwei Arten aufgelöst werden. Die erste Möglichkeit besteht in einer ein-
Schreiben von Transaktionskomponenten
317
fachen Zeitüberschreitung. Jede Transaktion wartet einen bestimmten Zeitraum, um Zugriff auf die gesperrten Informationen zu erhalten. Läuft die vorgegebene Zeitspanne ab, tritt eine Zeitüberschreitung auf und die Transaktion wird abgebrochen. Hierbei wird eine Transaktion durchgeführt, die andere wird abgebrochen. Die zweite Lösung besteht in der Ermittlung solcher Deadlocks. Eine Ressource erkennt, welche Transaktion auf welche Informationen zugreift. Wird eine Deadlocksituation ermittelt, kann diese Situation gelöst werden, indem die betreffende Transaktion aufgelöst oder abgebrochen wird, je nach Ressource. Deadlocks können selbst dann vermieden werden, wenn Sie Code schreiben, mit dem Daten gesperrt werden, die in anderen Transaktionen verwendet werden. Der Hauptfaktor liegt im Bereich der Daten. Ein Benutzername oder die Clientadresse weisen nur einen beschränkten Bereich auf. Das Risiko, dass zwei Anwendungen zur gleichen Zeit dieselben Daten verwenden, ist in diesem Fall gering. Daraus folgt, dass eine Transaktion, die Daten mit eingeschränktem Bereich bearbeitet und abfragt, länger ausgeführt werden können. Ein Datenobjekt dagegen, das einen weniger eingeschränkten Bereich aufweist oder durch viele Anwendungen verwendet wird, darf nicht für längere Zeiträume gesperrt werden, da dies zu einer Verlangsamung der Anwendung führen würde. Durch das Beachten dieser Richtlinien kann das Risiko von Deadlocks reduziert werden, dies bedeutet jedoch nicht, dass Transaktionen ausgeführt werden sollten, die lange Ausführungszeiten aufweisen. Selbst bei einem optimalen Aktivitätsentwurf treten gelegentlich Probleme hinsichtlich der gemeinsamen Ressourcennutzung auf. Microsoft SQL Server 6.5 beispielsweise verwendet Page Locking (Seitensperren). Hierbei kann zwischen zwei Transaktionen ein Deadlock entstehen, wenn auf die gemeinsam genutzten Daten einer Page (Seite) zugegriffen wird. In Microsoft SQL Server 7.0 wird Record Locking (Datensatzsperren) verwendet, daher tritt dieses Problem nicht auf.
10.3.3 Abrufen der Transaktionsschnittstelle Bei der Aktivierung eines COM+-Objekts weist dieses die Fähigkeit zur Verwaltung des Transaktionskontextes auf. Hierbei wird jedoch nicht direkt auf den Transaktionskontext, sondern auf die Transaktionskontextschnittstelle namens IObjectContext zugegriffen. Mit Hilfe dieser Schnittstelle kann Einfluss auf die Transaktionsausgabe genommen werden, es können weitere COM+-Transaktionsobjekte erstellt oder spezielle Laufzeiteigenschaften geprüft werden.
318
Entwickeln von Transaktionskomponenten
Der Transaktionskontext ist ein Shadow-Objekt, das mit Hilfe des Methodenaufrufs GetObjectContext abgerufen wird. In Visual J++ wird dies folgendermaßen erreicht: import com.ms.mtx.*; import com.ms.com.*; IObjectContext objContext = MTx.GetObjectContext();
Die Transaktionsschnittstellen werden im Paket com.ms.mtx gespeichert. In Visual Basic wird der Transaktionskontext folgendermaßen abgerufen: Dim objContext As ObjectContext Set objContext = GetObjectContext()
Da Visual Basic den Aspekt der Schnittstellenverwendung nicht unterstützt, wird die Coklasse ObjectContext als IObjectContext-Schnittstelle verwendet. Sämtliche Transaktionsschnittstellen werden in der Referenztypbibliothek COM+Services Type Library definiert. In der Visual C++-Umgebung wird der Transaktionskontext auf folgende Weise abgerufen: #include _COM_SMARTPTR_TYPEDEF(IObjectContext, __uuidof( IObjectContext)) IObjectContextPtr objContext; _com_util::CheckError( GetObjectContext( &objContext));
Die Transaktionsschnittstellen werden in der Headerdatei der COM+Dienste definiert. Zur einfacheren Verwendung der IObjectContext-Schnittstelle wird über _COM_SMARTPTR_TYPEDEF ein intelligenter Zeiger (Smartpointer) definiert. Anschließend wird zum Abruf des Schnittstellenzeigers GetObjectContext verwendet, hierbei jedoch in der Fehlerprüfroutine _com_util::CheckError gekapselt. Die Verwendung des Operators & ist möglich, da der Operator zum Abruf des richtigen Schnittstellenzeigers durch die COM-Compilerklassen überladen wurde.
10.3.4 Ändern der Transaktionsergebnisse Es gibt vier Methoden zur Beeinflussung der Transaktionsergebnisse. Diese werden im vorliegenden Beispiel durch IDL dargestellt, da IDL durch alle COM-fähigen Sprachen verwendet werden kann. HRESULT SetComplete(); HRESULT SetAbort(); HRESULT EnableCommit();
Schreiben von Transaktionskomponenten
319
HRESULT DisableCommit();
Diese vier Funktionen können in zwei Typen unterteilt werden, Transaktionsbeendigung und Transaktionsverzögerung. Methoden zur Transaktionsbeendigung In einer Transaktion kann angezeigt werden, ob die Transaktion abgeschlossen wurde und wie die Ausführung verlaufen ist. Hierbei war die Ausführung entweder fehlerfrei, oder es sind Probleme aufgetreten. Eine fehlerfreie Ausführung wird folgendermaßen angezeigt: objContext.SetComplete();
Durch diese Syntax wird angegeben, dass die Transaktion fehlerfrei beendet wurde. Als Ergebnis wird ein zweiphasiger Commit gestartet. Wenn alle Transaktionen erfolgreich waren, werden die Datenänderungen für die Ressource dauerhaft gespeichert. Die zweite Möglichkeit besteht darin, dass ein Fehler aufgetreten ist und die Transaktion abgebrochen werden sollte. Diese Möglichkeit wird durch den folgenden Code umgesetzt: objContext.SetAbort();
Es wird kein zweiphasiger Commit gestartet, alle Datenänderungen an der Ressource werden rückgängig gemacht. Ein beiden Fällen wird der Transaktionsstream freigegeben. Alle derzeit im Stream aktiven COM+-Objekte und Ressourcen sowie Sperren werden freigegeben bzw. aufgehoben. Es ist wichtig, dass alle COM-Verweise auf den Transaktionsstream auf NULL gesetzt werden, da die Verweise ab sofort als ungültig betrachtet werden. Methoden zur Transaktionsverzögerung Beim Methoden- oder COM+-Objektaufruf wird das COM+-Objekt aktiviert. Nach Ausführung des Methodenaufrufs wird das COM+-Objekt deaktiviert, sobald eine der Methoden zur Transaktionsbeendigung aufgerufen wird. In Geschäftsprozessen werden häufig mehrere Methodenaufrufe für COM+-Objekte verwendet, daher kann es hilfreich sein, die Beendigung einer Transaktion zu verzögern, um das Rückgängigmachen von Datenänderungen zu vereinfachen. Daten, die bereits aktualisiert wurden, sind schwieriger wieder in ihren anfänglichen Status zurückzuführen.
320
Entwickeln von Transaktionskomponenten
Zur Verzögerung einer Transaktionsbeendigung ist das Aufrufen weiterer Methoden für die Schnittstelle IObjectContext erforderlich. Zur Verzögerung des Transaktionsausgang und zum Abbrechen bei Start eines zweiphasigen Commits verwenden Sie den folgenden Code: objContext.DisableCommit();
Zur Verzögerung des Transaktionsausgang und zur Übergabeaktivierung beim Starten eines zweiphasigen Commits verwenden Sie den folgenden Code: objContext.EnableCommit();
Zur Integration dieser Methoden in die Konferenzanmeldungsanwendung werden die Operationsklassen erweitert. Zur Erweiterung der Methoden können zwei Verfahren eingesetzt werden. Bei der Anwendung für die Konferenzanmeldung wurde der Transaktionsausgang für jede Methode verzögert. Zum Abbrechen oder Übergeben einer Transaktion werden explizite Methoden aufgerufen. In der Konferenzanmeldungsanwendung wird die Transaktion durch Aufruf der Registration.reset-Methode abgebrochen. Das Verwenden expliziter Methoden zum Abbrechen oder Übergeben einer Transaktion ist nicht erforderlich. Es handelt sich um eine Entwurfsentscheidung, die sich nach dem ausgeführten Geschäftsprozess richtet. Das Verwenden dieser expliziten Methoden kann zu langen Transaktionen führen, da die mit der Transaktion verknüpfte Aktivität möglicherweise viel Zeit beansprucht. Beeinflussen des Transaktionsausgangs Bei den Methoden SetComplete, SetAbort, EnableCommit und DisableCommit handelt es sich um Methoden mit so genanntem »Bit-Toggling«, einer Werteumkehrung. Im Kontext eines Methodenaufrufs können diese Methoden in beliebiger Reihenfolge aufgerufen werden. Es wird jedoch die Methode angewendet, die im Methodenkontext als letzte aufgerufen wird. Wie Sie wissen, wechseln diese Methoden zwischen den Optionen Done und Consistent. Ist die Option Done auf ON eingestellt, können die durch eine Transaktion verwendeten Ressourcen freigegeben werden. Ist die Option Consistent auf ON eingestellt, sind die Daten zulässig. Bei einer Kombination mit der DoneEinstellung ON wird ein zweiphasiger Commitvorgang gestartet. Die folgende Tabelle verdeutlicht die Beziehung zwischen diesen Methoden.
Schreiben von Transaktionskomponenten
321
Methode
Option: Done
Option: Consistent
DisableCommit
OFF
OFF
EnableCommit
OFF
ON
SetAbort
ON
OFF
SetComplete
ON
ON
Während des Methodenaufrufs werden die Optionen umgekehrt. Am Ende des Methodenaufrufs entscheidet der Status der Optionen darüber, wie mit dem Transaktionsstream verfahren wird. Gelegentlich müssen Sie wissen, ob ein COM+-Objekt innerhalb eines Transaktionskontextes ausgeführt wird. Oder Sie möchten wissen, welcher Benutzer derzeit das COM+-Objekt ausführt. Diese Methoden fallen in die Kategorie der Transaktionskontextabfrage. Eine Erläuterung der Transaktionen ist wenig hilfreich, wenn Sie nicht wissen, ob das Objekt Teil einer Transaktion ist oder nicht. Mit der Methode IsInTransaction wird geprüft, ob ein Objekt an einer Transaktion beteiligt ist. BOOL IObjectContext::IsInTransaction ( );
Wird durch die Methode der Wert TRUE zurückgegeben, wird das Objekt in einer Transaktion ausgeführt. Der Wert FALSE zeigt an, dass das Objekt nicht in einer Transaktion ausgeführt wird. Über diese Methode kann auch geprüft werden, ob das Objekt im COM+-Dienstekatalog ordnungsgemäß konfiguriert ist. Wenn beispielsweise durch eine Prüfung ermittelt wird, dass das Objekt nicht an einer Transaktion beteiligt ist, jedoch in einem Paket registriert ist, wird die Transaktionseigenschaft so eingestellt, dass keine Transaktionen unterstützt werden. Ist dies nicht akzeptabel, kann das Objekt die Ausführung der aktuellen Methode stoppen und einen Fehler an den Aufrufer zurückgeben. Instanziierungsmethoden für COM+-Objekte In früheren Editionen von MTS (Microsoft Transaction Server) war es nötig, zur Instanziierung neuer COM+Objekte eine besondere Methode einzusetzen, IObjectControl::CreateInstance. Darüber hinaus musste die Methode SafeRef aufgerufen werden, wenn COM+Objektverweise zwischen verschiedenen COM+-Objekten weitergeleitet wurden. Diese Vorgehensweisen sind nicht mehr erforderlich. Die Methoden werden weiterhin unterstützt, eine Verwendung ist jedoch nicht nötig. In COM+ müssen lediglich die üblichen COM-Instanziierungsaufrufe durchgeführt und die Objektverweise weitergeleitet werden.
322
Entwickeln von Transaktionskomponenten
10.4 Konvertieren von COM-Objekten in COM+-Objekte Wenn ein COM-Objekt instanziiert wird und dieses anschließend COM+-Dienste verwendet, muss es Bestandteil einer COM+-Anwendung sein. Ist dies nicht der Fall, tritt ein Anwendungsfehler auf, da kein Kontext vorhanden ist. Das COM+Objekt muss dem COM+-Objektkatalog hinzugefügt werden, indem es mit einer COM+-Anwendung registriert wird. Es gibt zwei Methoden zur Bearbeitung des COM+-Katalogs. Die erste und einfachere Methode besteht in der Verwendung der Komponentendienste. Durch einfaches Zeigen und Klicken wird eine COM+-Anwendung erstellt. Bei der Erstellung einer COM+-Anwendung reichen die Standardeinstellungen in der Regel aus. Nach der Erstellung der COM+-Anwendung können Sie der Anwendung über einen einfachen Zeige- und Klickvorgang COM-Objekte hinzufügen. Nach der Installation der COM+-Objekte können Sie deren Eigenschaften bearbeiten, beispielsweise hinsichtlich Transaktions- und Synchronisierungsunterstützung. Die zweite Methode umfasst das Schreiben von Skriptcode, mit dem die administrativen COM+-Objekte bearbeitet werden. Diese Methode ist komplexer, bietet jedoch die Fähigkeit zur Verwaltung sämtlicher Eigenschaften der COM+-Anwendung sowie der zugehörigen Objekte. Des Weiteren ist diese Methode zur Erstellung von Installationsroutinen für größer angelegte Testumgebungen geeignet.
10.5 Resümee Das Schreiben von Transaktionscode umfasst nicht nur das Verwenden von Schnittstellen und das anschließende Ausführen oder Rückgängigmachen von Transaktionen. Zum Schreiben von Transaktionscode ist ein Verständnis der Transaktionsverarbeitung erforderlich. Dieses Verständnis können Sie sich am besten erarbeiten, indem Sie die Transaktionsverarbeitung mit Geschäftsprozessen vergleichen. Sie führen eine Reihe von Schritten aus und entscheiden anschließend, ob die Ergebnisse übernommen (Commit) oder rückgängig gemacht werden sollen (Rollback). Bei der Transaktionsverarbeitung ist der Statusaspekt von besonderer Bedeutung. Zur effektiven Statusverwaltung müssen umsichtig entworfene Aktivitäten vorliegen, und dies kann nur durch sorgfältig durchdachte Sequenz- und Kollaborationsdiagramme erreicht werden. Diese Diagramme beeinflussen den Entwurf der Operationsklassen. Eine guter aktivitätsbasierter Entwurf ermöglicht eine Transaktionsausführung in Paketform. An dieser Stelle wird deutlich, wie eng Entwurf und Implementierung miteinander verknüpft sind.
Konvertieren von COM-Objekten in COM+-Objekte
323
Für das Schreiben von Transaktionscode muss viel Zeit aufgebracht werden. Sie können beispielsweise in einer Anwendung das Messaging verwenden, mit dieser Komponente können jedoch keine Geschäftsprobleme gelöst werden. Das Messaging dient der Problemlösung hinsichtlich Infrastruktur und zeitlicher Koordination. Bei der Meldungsverarbeitung wird ein Geschäftsprozess ausgeführt, d.h., es wird eine Transaktion vollzogen. Der nächste Schritt besteht darin, eine Infrastruktur zur Unterstützung verteilter Anwendungen zu entwickeln.
324
Entwickeln von Transaktionskomponenten
11 Entwickeln von Messaging-COM+-Objekten Dieses Kapitel enthält eine Einführung in die Messagingdienste, die über Microsoft Message Queue (MSMQ) zur Verfügung stehen. Zunächst werden die Einsatzmöglichkeiten für das Messaging (oder MSMQ) beschrieben. Im weiteren Verlauf des Kapitels erhalten Sie einen Einblick in die Funktionsweise von MSMQ, indem Sie das MSMQ-API-Testprogramm (Anwendungsprogrammierschnittstelle) verwenden. Schließlich werden die Einzelheiten der Entwicklung verteilter Anwendungen besprochen, in denen Transaktionen und Messagingfunktionen eingesetzt werden.
11.1 Einführung in MSMQ Was ist Microsoft Message Queue (MSMQ), und wie wird es eingesetzt? Welchen Zweck erfüllt es? Stellen Sie sich eine Firma mit Sitz in Deutschland vor, die weltweit beliebige Gegenstände herstellt und das Kupfer für diese Gegenstände von einer Firma in Chile bezieht. Die Kommunikation mit der chilenischen Firma ist teuer und aufgrund der großen Entfernung zuweilen unzuverlässig, sodass die Firma das Kupfer von der chilenischen Firma über einen synchronen Aufruf über DCOM (Distributed Component Object Model) bestellt. Das Objekt in Deutschland instanziiert das Objekt in Chile. Eine Einschränkung dieses Systems besteht, abgesehen von der Zeit, die zum Instanziieren der Objekte und zur wechselseitigen Kommunikation erforderlich ist, darin, dass alle Bestellungen in Deutschland zur chilenischen Geschäftszeit eingegeben werden müssen, da andernfalls kein Mitarbeiter verfügbar ist, um die Bestellungen zu bestätigen. Stellen Sie sich nun vor, die Leitung bricht an einer beliebigen Stelle mitten im Vorgang zusammen. Bei COM+-Transaktionsdiensten werden alle Prozesse rückgängig gemacht, und die Bestellung muss erneut eingegeben werden. Wenn dies häufiger geschieht, wachsen sowohl die Kosten als auch der Ärger. Was kann man unternehmen, um ein besseres System einzurichten? Microsoft Message Queue ist die Antwort von Microsoft auf den Bedarf nach zuverlässigen asynchronen Kommunikationsmöglichkeiten zwischen Anwendungen. Dies ist bislang die einzige Software, die garantiert, dass eine Nachricht von einem Punkt zu einem anderen gelangt. Mit Hilfe von MSMQ können Anwendungen miteinander kommunizieren, ohne dass eine direkte Verbindung eingerichtet wird. Dies bedeutet, dass eine Anwendung eine Nachricht auch dann versenden kann, wenn die andere Anwendung nicht online ist, ohne dass die Gefahr besteht, dass die Nachricht nicht ankommt.
Einführung in MSMQ
325
MSMQ ist ein Netzwerk aus Warteschlangen und Nachrichten. Eine Warteschlange ist ein Ort, an dem Nachrichten gespeichert werden. Warteschlangen sind mit Mailadressen vergleichbar. Um eine Nachricht zu senden, müssen Sie sie lediglich an die richtige Adresse oder Warteschlange schicken. Bei Nachrichten kann es sich um jede Art von Daten handeln: Text, binäre Daten, Objekte usw. MSMQ setzt inhaltlich keine Grenzen. Es ist nur für das Versenden der Nachricht zuständig, nicht für deren Inhalt. Eine Anwendung interagiert mit einer Warteschlange über eine Reihe von APIs. Diese APIs sind einfacher Natur: Öffnen, Schließen, Senden und Empfangen. Sie bieten eine Zugriffsebene für den darunter liegenden MSMQ-Dienst, die man als Warteschlangen-Manager bezeichnet. Mit Hilfe des Warteschlangen-Managers haben Sie die Möglichkeit, Nachrichten in einer bestimmten Warteschlange abzulegen. Dabei spielt es keine Rolle, ob die Warteschlange sich auf einem lokalen Computer oder auf einem Remotecomputer befindet, da der WarteschlangenManager die Weiterleitung der Nachricht verwaltet.
11.1.1 MSMQ-API und die MSMQ-ActiveX-Komponente MSMQ stellt zwei Programmierschnittstellen bereit: zum einen die grundlegende MSMQ-API und zum anderen die MSMQ-COM-Komponente (Component Object Model). Ich persönlich ziehe die MSMQ-COM-Komponente vor, da sie sich in die Komponentenarchitektur einfügt und einfacher zu verwenden ist als die API. Die API müssen Sie eigentlich nur dann verwenden, wenn die MSMQ-COMKomponente Ihre Vorstellungen nicht erfüllen kann.
11.1.2 Vergleich von DCOM und MSMQ Die Technologien MSMQ und DCOM erscheinen Ihnen möglicherweise austauschbar, jede Technologie besitzt jedoch ihre eigenen Verwendungszwecke. DCOM ist eine verbindungsorientierte Technologie, wohingegen es sich bei MSMQ um eine Messaging-Technologie handelt. Um die Rolle beider Technologien vollständig nachzuvollziehen, betrachten Sie deren Möglichkeiten und Anforderungen. Zunächst ist für DCOM eine Verbindung erforderlich. Stellen Sie sich einen Basisclient vor, der eine Komponente auf einem anderen Computer aufruft. Wenn der Aufruf durchgeführt wird, muss die Komponente auf dem anderen Computer vorhanden und lauffähig sein. Daher muss eine Verbindung zwischen der Komponente und dem Basisclient bestehen. Für MSMQ ist keine Verbindung erforderlich: Wenn die Nachricht vom Basisclient zur Komponente gesendet wird, spielt es für den Basisclient keine Rolle, ob die Komponente vorhanden ist.
326
Entwickeln von Messaging-COM+-Objekten
Ebenso müssen für DCOM sowohl das Quellnetzwerk und der Quellcomputer als auch der Empfänger aktiv sein. Wenn eine dieser Komponenten nicht aktiv ist, kann der Methodenaufruf nicht erfolgreich durchgeführt werden. Bei MSMQ muss lediglich der lokale Computer eingeschaltet sein, der die Nachricht empfängt. Sobald die Nachricht von der Warteschlange angenommen wurde, ist sie ein Teil des Systems. Zu diesem Zeitpunkt gilt der Vorgang als abgeschlossen. Wenn ein DCOM-System einen Methodenaufruf oder einen Stapel von Methodenaufrufen startet, werden sie nach dem FIFO-Prinzip (First-in, First-out) ausgeführt. Der Grund hierfür liegt darin, dass es sich um einen seriellen Prozess handelt – der erste Aufruf muss als Erster verarbeitet werden. Andernfalls ist es nicht möglich, das Ergebnis der Methodenaufrufe festzulegen. In einem MSMQ-System basiert die Reihenfolge der Nachrichten auf einem Prioritätsmodell. Das bedeutet, dass die erste Nachricht nicht unbedingt als Erste verarbeitet wird. Dies ist jedoch nur scheinbar ein Problem, wie im weiteren Verlauf des Kapitels bei der Entwicklung von Anwendungen gezeigt wird. In einem DCOM-System werden Ressourcen häufig gesperrt. Stellen Sie sich den Aufruf einer Komponente in Frankfurt vor, die über New York und Tokyo eine Verbindung nach Santiago in Chile herstellt. Aufgrund der großen Entfernungen wird der Aufruf nicht direkt durchgeführt. Es gibt eine kleine zeitliche Verzögerung. Der Aufruf nach Santiago kann sich daher etwas aufwendiger gestalten. Während dieses Aufrufs werden die Komponenten gesperrt, da sie auf die Antwort auf den Komponentenmethodenaufruf warten. In einem auf MSMQ basierenden System findet der Aufrufmechanismus lokal statt. Wenn dieselbe Nachricht nach Santiago versendet würde, läge am jeweiligen Standort eine Sperre vor, der nächste und der vorherige Standort wäre jedoch für das Senden und Empfangen von Aufrufen an andere oder von anderen Standorten frei. Das Datenmodell des DCOM-Systems ist unmittelbar. Stellen Sie sich wiederum die Aufrufsequenz nach Santiago vor. Wenn an einem Standort eine Änderung vorgenommen wird, ist diese Änderung unmittelbar. Wenn der Urheber des ursprünglichen Aufrufs in Frankfurt zum Verarbeiten anderer Vorgänge frei ist, sind die Daten an allen Standorten aktuell und bekannt. Bei MSMQ ist dies nicht möglich, da nicht bekannt ist, wann der Empfänger die Nachricht empfängt und verarbeitet. Beim Messaging ist jedoch gewährleistet, dass die Nachricht schließlich zum Empfänger gelangt und dort verarbeitet wird. Die Datenintegrität wird daher als letztendlich betrachtet. Wenn ein Fehler auftritt, schlägt das DCOM-System fehl. Der Verbindungsversuch kann zwar wiederholt werden, jedoch nur manuell. Bei MSMQ wird der Wiederholungsversuch automatisch durchgeführt.
Einführung in MSMQ
327
Wählen zwischen DCOM und MSMQ Sollten also den zuvor genannten Argumenten entsprechend ausschließlich MSMQ-Komponenten verwendet werden? Die Lösung besteht darin, beide Technologien in bestimmten Situationen einzusetzen. Verwenden Sie MSMQ unter folgenden Bedingungen: 왘 Der Sender und der Empfänger kommunizieren zu unterschiedlichen Zeiten
miteinander. Dies ist beispielsweise bei einem Vertriebssystem der Fall, das kontinentübergreifend kommuniziert. In diesem Fall gibt es sehr wenig zeitliche Überschneidung zwischen den Vertriebspartnern, und es ist sicherer, eine Serie von Nachrichten zu senden. 왘 Die Kosten zum Versenden einer Nachricht, die möglicherweise nicht erfolg-
reich gesendet werden kann, sind sehr hoch. MSMQ reduziert die Kosten durch die automatische Wiederholungsfunktion, da nicht der gesamte Prozess unterbrochen wird. Auf diese Weise wird lediglich das Endergebnis verzögert. 왘 Der Sender möchte eine Nachricht versenden und anschließend andere Vor-
gänge verarbeiten. MSMQ bietet dem Sender in gewissem Umfang die Möglichkeit der parallelen Verarbeitung. 왘 Die Nachrichten müssen zur möglichen Überprüfung oder Wiederherstellung
protokolliert werden. 왘 Das Herstellen einer physischen Verbindung zwischen dem Sender und dem
Empfänger ist umständlich. Dies betrifft beispielsweise Anwendungen, die die Stapelverarbeitung einsetzen. Auch Notebookbenutzer mit einem mobilen Arbeitsplatz sind geeignete Nutzer des Messagingsystems, da sie ihre Arbeit erledigen und zu einem späteren Zeitpunkt zur Weiterverarbeitung versenden können. Verwenden Sie DCOM unter folgenden Umständen: 왘 Der Sender und der Empfänger verfügen über eine zuverlässige Verbindung
mit hoher Bandbreite. Das DCOM-Protokoll erfordert eine höhere Bandbreite, um zuverlässig und schnell zu reagieren. 왘 Der Sender kann nicht auf eine Antwort vom Empfänger warten. Wenn die
Kommunikation synchron verlaufen muss oder die asynchrone Gestaltung der Kommunikation keinen Vorteil bietet, ist die Verwendung von DCOM einfacher.
328
Entwickeln von Messaging-COM+-Objekten
11.1.3 Experimentieren mit MSMQ unter Verwendung des API-Testbeispiels Im Plattform-SDK (Software Developer’s Kit) (Beispielverzeichnis COM\MessageQueueingdirectory) finden Sie eine Anwendung, die im Folgenden als MSMQAPI-Testprogramm bezeichnet wird. Diese Anwendung veranschaulicht die Interaktion mit MSMQ über die MSMQ-APIs. Dieses Beispiel ist so strukturiert, dass die MSMQ-APIs als Methoden dargestellt werden. Wenn die MSMQ-Methode Parameter erfordert, werden diese über das angezeigte Dialogfeld definiert. Anhand des MSMQ-API-Testbeispiels wird gezeigt, wie Nachrichten gesendet und empfangen werden können. Damit Sie die Anwendung nicht selbst erstellen müssen, suchen Sie im Quellcode des Buches nach dem Verzeichnis util, starten die Anwendung MqAPITst.exe, und anschließend eine zweite Instanz derselben Anwendung. Ordnen Sie die beiden Instanzen, wie in Abbildung 11.1 gezeigt, übereinander an.
Abbildung 11.1 Anordnung der beiden MSMQ-Fenster
Einführung in MSMQ
329
Das obere Fenster stellt den Empfänger dar, das heißt, in diesem Fenster wird die Nachricht empfangen. Das untere Fenster übernimmt die Rolle des Senders und versendet die Nachricht. Das Ziel besteht darin, eine Nachricht vom unteren Fenster an das obere Fenster zu senden. In diesem Demo werden sowohl der Sender als auch der Empfänger gestartet. Der Empfänger könnte jedoch ebenso erst gestartet werden, nachdem die Nachricht gesendet wurde. Starten eines Empfängers Zum Empfangen einer Nachricht muss eine Überwachungsfunktion ausgeführt werden. Wenn MSMQ Nachrichten an eine Warteschlange liefert, wird keine Anwendung gestartet – der Empfänger muss diese Aufgabe übernehmen. Daher muss der Empfänger in diesem Demo gestartet sein, damit er die Warteschlange auf eingehende Nachrichten überwachen kann. Da Sie diese Anwendung zum ersten Mal ausführen, müssen Sie zunächst die Warteschlange erstellen, die anschließend überwacht werden soll. Wählen Sie hierzu im Menü API -> MQCreateQueue. Das in Abbildung 11.2 dargestellte Dialogfeld wird angezeigt.
Abbildung 11.2 Das Dialogfeld »MQCreateQueue«
Im oberen Textfeld werden der Name der Nachrichtenwarteschlange und der Computer definiert, auf dem die Warteschlange eingerichtet wird. Der Computer wird durch Eingabe eines Namens festgelegt oder über einen Punkt (.), um den lokalen Computer anzugeben. Für die Warteschlange kann ein beliebiger Name eingegeben werden. Beispielsweise könnten Sie den Namen COMPUTER\Warteschlange eingeben, wobei COMPUTER für den Namen des Computers in Ihrem lokalen Netzwerk steht. Im unteren Textfeld wird die Bezeichnung festgelegt, die der Warteschlange zugeordnet wird. Eine Bezeichnung ist mit einer Beschreibung vergleichbar, sie kann jedoch als Verweis auf eine bestimmte Warteschlange verwendet werden. Die Bezeichnung sollte leicht lesbar sein und den Zweck der Warteschlange vermitteln. Um den Warteschlangennamen näher zu erläutern, entfernen Sie den Standard-
330
Entwickeln von Messaging-COM+-Objekten
text, und geben Sie MQ-API-Test ein. Klicken Sie anschließend auf OK. Wenn die Warteschlange nicht bereits vorhanden war, wird jetzt eine Meldung im Fenster angezeigt, die auf eine erfolgreiche Erstellung der Warteschlange hinweist. Diese neue Warteschlange muss nun geöffnet werden. Wählen Sie im oberen Fenster im Menü API -> MQOpenQueue. Das in Abbildung 11.3 dargestellte Dialogfeld wird angezeigt.
Abbildung 11.3 Öffnen einer Warteschlange
Wurde eine Warteschlange erfolgreich erstellt, wird im Kombinationsfeld automatisch ein Warteschlangenpfad angezeigt (ähnlich dem im oben dargestellten Dialogfeld). Wenn ein Programm eine Warteschlange öffnet, können drei Arten von Messagingvorgängen durchgeführt werden: Senden, Empfangen und »Peeking« (Abrufen einer Nachricht, ohne dass diese aus der Warteschlange entfernt wird). Das Programm kann pro Verweis auf eine Warteschlange immer nur einen Vorgang durchführen. Vom Programm aus betrachtet, ist eine Warteschlange ein in einer Richtung verlaufender Vorgang. Um gleichzeitig an eine Warteschlange zu senden und Nachrichten von ihr zu empfangen, muss das Programm zwei Verweise auf die Warteschlange öffnen, bei denen es sich jeweils um in einer Richtung verlaufende Vorgänge handelt. Da wir derzeit die Empfängerseite bearbeiten, aktivieren Sie das Kontrollkästchen MQ_RECEIVE_ACCESS. Die Option MQ_PEEK_ACCESS, die für einen Empfänger gilt, wird abgeblendet dargestellt. Klicken Sie zum Abschluss auf OK. Eine Meldung im Clientbereich weist darauf hin, dass das Warteschlangenhandle erfolgreich geöffnet wurde.
Einführung in MSMQ
331
Der letzte Schritt zum Einrichten der Empfangsanwendung besteht darin, den Überwachungsvorgang zu starten, um in der Warteschlange eingehende Nachrichten zu erkennen. Die Anwendung kann die Warteschlange entweder synchron oder asynchron auf Nachrichten überwachen. Die synchrone Überwachung ist vergleichbar mit dem Ausgeben eines Methodenaufrufs und dem Warten auf eine Antwort mit der Ausnahme, dass eine Zeitüberschreitung angegeben werden kann, damit der Prozess nicht gesperrt wird. Die asynchrone Überwachung funktioniert wie ein Rückruf (Callback) unter Windows. Das Schreiben von Messaginganwendungen mit Hilfe von MSMQ ist eine Form der asynchronen Programmierung. Im vorherigen Absatz wurde jedoch erwähnt, dass die Möglichkeit besteht, Nachrichten entweder synchron oder asynchron abzurufen. Da MSMQ asynchron funktioniert, wird oft angenommen, dass Nachrichten auch asynchron abgerufen werden – dies ist jedoch nicht unbedingt der Fall. MSMQ ermöglicht die Verarbeitung von Nachrichten zu einem beliebigen Zeitpunkt. Das heißt, dass Sie die Nachrichten entweder explizit abrufen oder auf das Eintreffen einer Nachricht warten können. Dies wird als »Polling« (Abfragen) der Warteschlange bezeichnet. Beim Polling handelt es sich um einen synchronen Vorgang, bei dem eine Methode in regelmäßigen Abständen aufgerufen wird. Wenn ein COM+-transaktionsfähiges Objekt mit MSMQ interagiert, muss diese Methode verwendet werden. Ein asynchroner MSMQ-Vorgang verläuft anders. Bei der Instanziierung eines COM-Objekts wird eine Verbindung mit MSMQ über ein MSMQ-API hergestellt. Das COM-Objekt teilt MSMQ anschließend mit, dass Nachrichten asynchron übermittelt werden sollen. MSMQ sendet die Nachrichten asynchron unter Verwendung von COM-Verbindungspunkten. COM-Verbindungspunkte erfordern jedoch, dass sowohl der Sender als auch der Empfänger von COM-Verbindungspunktereignissen gleichzeitig aktiv sind. Bei COM+-Komponenten ist dies nicht immer gewährleistet, da COM+-Transaktionsdienste die COM+-Komponente nach Abschluss der Transaktion deaktivieren und den COM-Verbindungspunkt entfernen. Um dieses Problem zu vermeiden, können Sie COM+ weiterhin verwenden, Sie sollten jedoch nicht die COM+-Transaktionsdienste einsetzen. Auf diese Weise bleibt ein aktiver Verweis auf das COM+-Objekt erhalten, das die MSMQ-COM-Verbindungspunktereignisse empfängt. Das MSMQ-API-Testbeispiel verwendet einen synchronen Methodenaufruf zum Empfangen von MSMQ-Nachrichten. Wählen Sie zum Starten der Überwachung im Menü API -> MQReceiveMessage. Das in Abbildung 11.4 dargestellte Dialogfeld wird angezeigt. In diesem Dialogfeld können Sie drei Optionen festlegen.
332
Entwickeln von Messaging-COM+-Objekten
Abbildung 11.4 Empfangen einer Nachricht
Das Kombinationsfeld Queue definiert, aus welcher Warteschlange Nachrichten empfangen werden. Wenn Sie die vorherigen Schritte ausgeführt haben, gibt der Warteschlangenname den Namen der von Ihnen erstellten Warteschlange wieder. Das Textfeld Timeout legt die Zeit in Millisekunden fest, die die MSMQ-Überwachungsmethode wartet, bevor eine Zeitüberschreitung eintritt. Geben Sie den Wert 20000 ein. Das Textfeld Body Length definiert die Länge des Puffers, der vom Empfänger an den Sender gesendet wird. Beachten Sie hierbei, dass zwischen dem Sender und dem Empfänger eine Vereinbarung besteht. Wenn der Sender eine bestimmte Bytezahl an Daten sendet, muss der Empfänger dieselbe Bytezahl an Daten lesen. Wird diese Vereinbarung gebrochen, können Probleme entstehen. Übernehmen Sie den Standardwert von 256, und klicken Sie auf OK. In einem Dialogfeld wird darauf hingewiesen, dass der Empfänger auf eine Nachricht wartet. Wird eine Nachricht gesendet, gibt die Methode die Nachricht im Clientbereich des Empfängerprozesses aus. Wenn 20.000 Millisekunden überschritten werden, tritt eine Zeitüberschreitung auf und die Methode gibt kein Ergebnis aus. Wenn Sie wissen, dass eine Nachricht vorliegt, rufen Sie das Dialogfeld MSMQ Receive Message erneut auf. Starten und Testen eines Senders Der Sender ist für das Senden einer Nachricht an eine Warteschlange zuständig. Hierzu muss der Sender die Warteschlange öffnen können. Eine Warteschlange kann zwar direkt geöffnet werden, dies ist jedoch aufgrund der Art, wie MSMQ Warteschlangen verwaltet, nicht immer sinnvoll. Ein MSMQ-Netzwerk kann viele Computer umfassen, auf denen sich jeweils mehrere Warteschlangen befinden
Einführung in MSMQ
333
können. Einige Warteschlangen auf verschiedenen Computern verfügen möglicherweise über denselben Namen. Sie müssen daher festlegen, welche Warteschlange geöffnet werden soll. Sie können einen Pfad mit einem Computernamen und einem Warteschlangennamen angeben. Auf diese Weise wird die Anwendung jedoch auf einen bestimmten Computer und eine bestimmte Warteschlange festgelegt. Um die Anwendung flexibler zu gestalten, können stattdessen Sie selbst die Warteschlange ausfindig machen, beispielsweise in einer Sammlung von Warteschlangen. Wählen Sie im Menü API -> MQLocate, um die Warteschlange auszuwählen. Das in Abbildung 11.5 dargestellte Dialogfeld wird angezeigt.
Abbildung 11.5 Auswählen einer Warteschlange
Dieses Dialogfeld enthält nur ein Textfeld Label (Bezeichnung). Bei der Bezeichnung einer Warteschlange handelt es sich um einen Bezeichner, der vom MSMQAPI-Testbeispiel zum Auffinden einer Warteschlange verwendet wird. Geben Sie MQ-API-Test ein – die Bezeichnung, die weiter oben im Kapitel beim Erstellen der Warteschlange verwendet wurde. Klicken Sie anschließend auf OK. Der Empfänger sollte die soeben erstellte Warteschlange finden und eine Meldung »locate queue operation completed successfully« (Warteschlange wurde gefunden) ausgeben. Dies ist nicht die einzige Möglichkeit zum Auswählen einer Warteschlange. Üblicherweise wird ein Universal Unique Identifier (UUID) verwendet, der mit einem COM-GUID (Globally Unique Identifier) vergleichbar sein kann. Der UUID-Ansatz wurde im MSMQ-API-Testbeispiel nicht eingesetzt, da die Eingabe eines UUIDBezeichners kompliziert und fehleranfällig ist. Sobald Sie die Warteschlange gefunden haben, kann diese zur Verwendung geöffnet werden. Das Öffnen einer Warteschlange für den Sender ist derselbe Vorgang wie das Öffnen einer Warteschlange für den Empfänger mit der Ausnahme, dass Sie im Dialogfeld das Kontrollkästchen MQ_SEND_ACCESS anstelle des Kontrollkästchens MQ_RECEIVE_ACCESS aktivieren müssen. Im Clientbereich wird die Meldung »queue was successfully opened« (Warteschlange wurde geöffnet) an-
334
Entwickeln von Messaging-COM+-Objekten
gezeigt. Nun können Sie eine Nachricht an die Warteschlange senden. Wählen Sie im Menü API -> MQSend. Das in Abbildung 11.6 dargestellte Dialogfeld wird angezeigt.
Abbildung 11.6 Eigenschaften beim Senden von Nachrichten
Dieses Dialogfeld ist wesentlich komplexer als die bislang in diesem Kapitel behandelten Dialogfelder. Im einfachsten Fall müssen Sie die Textfelder Label und Body ausfüllen und auf OK klicken. Unter Label wird eine Nachrichtenüberschrift oder eine Beschreibung angegeben, und das Textfeld Body enthält den eigentlichen Nachrichteninhalt. Im nächsten Abschnitt werden die einzelnen Optionen detailliert erläutert. Geben Sie zunächst an dieser Stelle nur Text in den Feldern Label und Body ein, und klicken Sie auf OK, um die Nachricht an den Empfänger zu senden. Der Empfänger sollte nun Ergebnisse anzeigen, die den in Abbildung 11.7 dargestellten ungefähr entsprechen.
Einführung in MSMQ
335
Abbildung 11.7 Empfänger mit empfangener Nachricht
11.1.4 Nachrichtenattribute Nachfolgend werden die in Abbildung 11.6 gezeigten Nachrichtenattribute und deren Auswirkungen auf die Nachricht erläutert. Nachrichtenübermittlungszeit Standardmäßig wird der Zeitraum, der zum Übermitteln einer Nachricht vom Sender zum Empfänger beansprucht wird, ignoriert. Einige Anwendungen verfügen jedoch über Nachrichten, deren Inhalt nach einem bestimmten Zeitraum als veraltet gilt. Wenn Sie beispielsweise einen Börsentickerwert drei Wochen nach seinem Auftreten in einer Anwendung für den Handel mit Aktien erhalten, ist diese Berechnung veraltet und nicht mehr relevant. In MSMQ können Nachrichten über zwei Kriterien als veraltet gekennzeichnet werden: über den Zeitraum bis zum Erreichen der Warteschlange und über den Zeitraum bis zur Ankunft beim Empfänge. Ist eine Nachricht veraltet, wird die Datei vom MSMQ-Netzwerk den Warteschlangen- und Nachrichteneigenschaften entsprechend verarbeitet. Wenn ein Sender eine Nachricht übermittelt, wird sie nur auf programmatischer Ebene als gesendet betrachtet. Tatsächlich wurde die Nachricht dem MSMQNetzwerk hinzugefügt und ist im Begriff, an die Warteschlange übermittelt zu werden. Die Warteschlange kann sich auf einem anderen Computer in einem anderen Land befinden, und der Sendevorgang kann einige Zeit in Anspruch nehmen. Der Zeitraum bis zum Erreichen der Warteschlange wird als Time To Reach Queue (Übermittlungszeit bis Warteschlange) bezeichnet. Wenn die Nachricht aus der Warteschlange abgerufen wird, nennt man den Zeitraum, der für den ge-
336
Entwickeln von Messaging-COM+-Objekten
samten Übermittlungsvorgang vom Sender bis zum Empfänger benötigt wurde, Time To Be Received (Übermittlungszeit bis Empfänger). Der Zeitraum bis zum Erreichen der Warteschlange ist immer geringer als die Übermittlungszeit bis zum Erreichen des Empfängers. Wenn Zeitüberschreitungen für beide Ereignisse definiert wurden, erhält der Zeitüberschreitungswert für die Übermittlungszeit bis zum Empfänger Vorrang. Das heißt, dass die Nachricht auch dann als veraltet gilt, wenn die Zeit bis zum Empfänger den Zeitüberschreitungswert übersteigt, die Zeit bis zum Erreichen der Warteschlange jedoch noch im zulässigen Rahmen liegt. Beim Senden einer Nachricht können Sie die Priorität der Nachricht festlegen. Wenn Sie zwei oder mehr Nachrichten über MSMQ senden, können Sie die Reihenfolge nicht angeben, in der die Nachrichten empfangen werden. Diese können in beliebiger Reihenfolge eintreffen, je nach Datenaufkommen und Verarbeitungsmöglichkeiten. Sie können die Reihenfolge jedoch beeinflussen, indem Sie die Priorität der Nachrichten einstellen. Bei hohem Datenaufkommen wird auf diese Weise sichergestellt, dass einige Nachrichten vor anderen gesendet werden. MSMQ verfügt über eine Nachrichteneigenschaft Priority; die Prioritäten liegen im Bereich zwischen 0 und 7. Je höher die Priorität, desto mehr Vorrang erhält die Nachricht vor den anderen Nachrichten im MSMQ-Netzwerk. Übermittlungsweise einer Nachricht Für die Übermittlung einer Nachricht stehen zwei Möglichkeiten zur Verfügung: EXPRESS (schnell) und RECOVERABLE (wiederherstellbar). Zwischen diesen beiden Übermittlungsarten gibt es einen wesentlichen Unterschied. Im schnellen Modus wird die Nachricht mit hoher Geschwindigkeit gesendet. Um die Geschwindigkeit zu optimieren, wird die Nachricht nicht auf die Festplatte geschrieben. Sie verbleibt stattdessen im RAM, und es wird kein Protokoll erstellt. Der Nachteil dieser Methode besteht darin, dass bei einem Systemausfall des Computers alle Spuren dieser Nachricht verloren gehen. Beim wiederherstellbaren Modus wird die Nachricht auf der Festplatte gespeichert. Wenn die MSMQ-Nachricht durch das Netzwerk wandert, wird an jedem Knoten eine Sicherungskopie der Nachricht erstellt. Auf diese Weise wird gewährleistet, dass die Nachricht auch bei einem Systemausfall des Computers noch gesendet wird. Die Nachricht hinterlässt demnach eine Spur. Der Nachteil dieses Ansatzes besteht darin, dass das Schreiben der Daten auf die Festplatte zusätzliche Verarbeitungszeit in Anspruch nimmt.
Einführung in MSMQ
337
Der Nachrichteninhalt In MSMQ besteht der Nachrichteninhalt aus einer Reihe von Bytes. Die MSMQActiveX-Komponente verfügt über eine zusätzliche Funktion. Handelt es sich beim Inhalt um ein COM-Objekt, kann MSMQ das Objekt über die COM-Schnittstelle IPersistStream serialisieren. Auf diese Weise wird das Schreiben und Lesen der Nachricht vereinfacht, da das MSMQ-ActiveX-COM-Objekt zusätzliche Aufgaben übernimmt. Feststellen des Übermittlungsstatus einer Nachricht Das Senden einer Nachricht ist ein in einer Richtung verlaufender Kommunikationsvorgang insofern, als es einen Sender und einen Empfänger gibt; es ist jedoch hilfreich zu wissen, was mit der Nachricht geschehen ist, zu diesem Zweck erhält man eine Empfangsbestätigung. Es gibt verschiedene Typen von Empfangsbestätigungen, sowohl positiver als auch negativer Art. Beide Arten der Bestätigung werden an eine administrative Warteschlange gesendet. Die administrative Warteschlange wird vom Benutzer angegeben. Unter MSMQ 1.0 gab es nur grundlegende Bestätigungen. Unter MSMQ 1.0 wurde die Bestätigung über die Eigenschaft MSMQMessage.Ack eingestellt. Unter MSMQ 2.0 ist die Eigenschaft MSMQMessage.Ack noch immer vertreten, sie hat jedoch an Bedeutung verloren. Unter MSMQ 2.0 werden Nachrichtentypen einer Nachrichtenklasse (MSMQMessage.Class) zugeordnet. Die verschiedenen MSMQMessage.Ack-Eigenschaftenwerte lauten folgendermaßen: 왘 MQMSG_ACKNOWLEDGMENT_FULL_REACH_QUEUE: Hinterlegt je nach-
dem, ob die Nachricht die Warteschlange erreicht oder nicht, eine positive oder negative Bestätigung. Eine negative Bestätigung wird abgelegt, wenn die Übermittlungszeit bis zum Erreichen der Warteschlange überschritten wird oder wenn die Nachricht nicht authentifiziert werden kann. 왘 MQMSG_ACKNOWLEDGMENT_NACK_REACH_QUEUE: Legt eine negative
Bestätigung ab, wenn die Nachricht die Warteschlange nicht erreichen kann. Eine negative Bestätigung wird abgelegt, wenn die Übermittlungszeit bis zum Erreichen der Warteschlange überschritten wird oder wenn die Nachricht nicht authentifiziert werden kann. 왘 MQMSG_ACKNOWLEDGMENT_FULL_RECEIVE:
Hinterlegt eine positive oder negative Bestätigung abhängig davon, ob die Nachricht aus der Warteschlange abgerufen wird, bevor die Übermittlungszeit bis zum Empfänger überschritten ist.
338
Entwickeln von Messaging-COM+-Objekten
왘 MQMSG_ACKNOWLEDGMENT_NACK_RECEIVE: Legt eine negative Bestä-
tigung ab, wenn ein Fehler auftritt und die Nachricht nicht aus der Warteschlange abgerufen werden kann, bevor die Übermittlungszeit bis zum Empfänger abgelaufen ist. 왘 MQMSG_ACKNOWLEDGMENT_NONE: Dies ist die Standardeinstellung. Es
werden keine Bestätigungsnachrichten (weder positiv noch negativ) abgelegt. Die Verwendung der verschiedenen MSMQ-Nachrichtenklassenwerte umfasst viele Einzelheiten, und die unterschiedlichen Bestätigungen können in Ankunftsbestätigungen und Lesebestätigungen zusammengefasst werden. In der Dokumentation zum Microsoft Plattform-SDK werden die Begriffe Ankunftsbestätigung und Lesebestätigung verwendet, um die mit dem Eintreffen der Nachricht in der Warteschlange und die mit dem Lesen der Nachricht aus der Warteschlange verbundenen Ereignisse zu definieren. (In diesem Kapitel wurden diese Ereignisse als das Senden der Nachricht an die Warteschlange und das Abrufen der Nachricht aus der Warteschlange bezeichnet.) Es gibt Bestätigungen für die Verschlüsselung, für das Schreiben der Nachricht in die Warteschlange, für das Überschreiten des Kontingents, für den Erfolg der Transaktion usw. Alle Bestätigungen werden an eine administrative Warteschlange gesendet. Transaktionen und MSMQ Es gibt vier verschiedene Typen von MSMQ-Transaktionen: 왘 MSMQ-Transaktionen: Diese Transaktionen finden innerhalb von MSMQ
statt. Sie betreffen nur MSMQ-Nachrichten und werden nicht an externe Ressourcen weitergegeben. 왘 DTC-Transaktionen: Bei diesen Transaktionen wird MSMQ explizit mit dem
DTC (Distributed Transaction Coordinator) kombiniert. Wenn Ressourcen durch den DTC verwaltet werden, wirkt sich die MSMQ-Transaktion auf diese Ressourcen aus. 왘 COM+-Transaktionen: Diese Transaktionen ähneln Transaktionen von MSMQ
mit DTC mit der Ausnahme, dass die Transaktionsverknüpfung automatisch vonstatten geht. Das Schreiben zusätzlicher Programmiercodes ist nicht erforderlich. 왘 XA-kompatible Transaktionen: Es ist möglich, eine Nachricht über Transaktio-
nen basierend auf einem Transaktionsmonitor mit Hilfe des XA-Protokolls zu senden. In diesem Szenario müssen Sie dennoch mit dem DTC interagieren, da er den Ressourcenmonitor für den MSMQ-Ressourcenanbieter darstellt. Die Kombination von Transaktionen und Messaging ändert die Funktionsweise des Messaging. Damit Transaktionen für MSMQ-Nachrichten eingesetzt werden kön-
Einführung in MSMQ
339
nen, muss die Warteschlange, an die die Nachrichten gesendet werden, transaktionsfähig sein. Dies erreichen Sie, indem Sie das Transaktionsattribut der Warteschlange aktivieren (auf true setzen). Anschließend muss die Nachricht Transaktionen verwenden und ermitteln, welche Transaktionen aus den vier oben definierten Modellen verwendet werden. Standardmäßig wird die COM+-Transaktionsmöglichkeit eingesetzt, da sie sich in die Windows-DNA-Architektur einfügt. Nun kommt der schwierigere Teil. Wenn eine Anwendung eine Nachricht im Kontext einer derzeit ausgeführten COM+-Transaktion sendet, wird die Nachricht nicht sofort übermittelt, sondern erst dann, wenn die Transaktion erfolgreich übergeben wurde. Wird die Transaktion abgebrochen, werden die Nachrichten gelöscht und nicht gesendet. Dies mag zunächst merkwürdig erscheinen, hat jedoch einen sehr wichtigen Grund. Stellen Sie sich vor, eine Transaktion sendet eine Nachricht und wird anschließend abgebrochen. Das MSMQ-Netzwerk müsste daraufhin die Nachricht zurückziehen. Wurde die Nachricht abgerufen und verarbeitet, müsste MSMQ auch die Transaktionen widerrufen, die durch das Verarbeiten der Nachricht ausgeführt wurden. Der einfachere Ansatz besteht darin, keine Nachrichten zu senden, bis die Transaktion vollständig abgeschlossen wurde. Dies hat jedoch zur Folge, dass Sie eine Nachricht weder senden noch auf eine Antwort warten können. Dadurch würde eine automatische Blockade (Deadlock) verursacht, da die Transaktion zuerst abgeschlossen werden muss, bevor eine Antwort auf die Nachricht generiert werden kann. Nehmen Sie nun an, Sie möchten eine Windows-DNA-Anwendung erstellen und Transaktionen für den Sender und den Empfänger ausführen. Es ist möglich, einen Transaktionskontext am Sender zu starten und auf den Empfänger auszuweiten; dies ist jedoch nicht empfehlenswert, da es eine lange Transaktion hervorrufen kann. Eine lange Transaktion ist zu vermeiden, da sie leicht zu Ressourcensperren führen kann und die Skalierbarkeit herabsetzt (wie in Kapitel 10 erläutert wurde). Stattdessen sollten Sie einen Transaktionskontext auf der Senderseite und eine separate Transaktion auf der Empfängerseite erstellen. Senden und Empfangen von Transaktionsnachrichten Wenn eine Transaktion verwendet wird, kann es sich bei der Nachricht um eine Transaktionsnachricht handeln oder auch nicht. Dasselbe gilt für die Warteschlange. Daher kann sich eine Situation ergeben, in der die Nachricht durch eine Transaktion verarbeitet wird, die Warteschlange jedoch nicht. Dieser Umstand wird weiterhin dadurch kompliziert, dass die Warteschlange sich entweder auf einem lokalen oder auf einem Remotecomputer befinden kann. Welche Kombinationen von Transaktionen und Warteschlangen sind also zulässig? Die folgende Tabelle zeigt, welche Sender Nachrichten an die Warteschlange senden dürfen.
340
Entwickeln von Messaging-COM+-Objekten
Lokale Warteschlange
Remotewarteschlange
Transaktion
Keine Transaktion
Transaktion
Keine Transaktion
Senden mit Transaktion
Ja
Nein
Ja
Nein
Senden ohne Transaktion
Nein
Ja
Nein
Ja
Beim Senden von Nachrichten muss die Nachricht durch Transaktionen verarbeitet werden, wenn die Warteschlange Transaktionen einsetzt. Der Speicherort der Warteschlange ist kein Kriterium dafür, ob eine Transaktionsnachricht gesendet werden kann. In folgender Tabelle wird die Situation eines Empfängers dargestellt, der eine Nachricht aus der Warteschlange abruft.
Transaktionsnachricht erhalten
Lokale Warteschlange
Remotewarteschlange
Transaktion
Keine Transaktion
Transaktion
Keine Transaktion
Ja
Nein
Nein
Nein
Ja
Ja
Ja
Nicht-Transaktions- Ja nachricht erhalten
In diesem Szenario kann eine Transaktionsnachricht nur im Kontext einer Transaktion abgerufen werden, wenn die Warteschlange lokal gespeichert ist. Wenn Sie eine Nachricht jedoch unbedingt abrufen möchten, ist dies in allen Situationen möglich. Die Frage lautet dann, ob mit der abgerufenen Nachricht ein Transaktionskontext verknüpft ist. MSMQ geht von einem Abrufmodell ohne Transaktionen aus, da nicht vorausgesetzt werden kann, dass der Empfänger über Transaktionsinhalt verfügt. Der Vorteil der Verwendung von Transaktionswarteschlangen liegt darin, dass Nachrichten nur einmal und in der richtigen Reihenfolge eingehen. Die Eingangsreihenfolge der Nachrichten entspricht der Reihenfolge, in der sie während der Ausführung der Transaktion gesendet wurden. Wenn jedoch in anderen Transaktionskontexten erstellte Nachrichten abgerufen werden, treffen diese nicht in dieser Reihenfolge ein.
Einführung in MSMQ
341
Die unterschiedlichen Warteschlangen Unter MSMQ gibt es verschiedene Typen von Warteschlangen: Nachrichtenwarteschlangen, Verwaltungswarteschlangen, Antwortwarteschlangen, Journalwarteschlangen, Warteschlangen für nicht übermittelbare Nachrichten und Berichtwarteschlangen. Nachrichtenwarteschlangen Die gängigsten Warteschlangen sind die Nachrichtenwarteschlangen. Diese werden von Anwendungen erstellt und sind für das Senden von Daten von einem Sender an einen Empfänger zuständig. Beim Erstellen einer Nachrichtenwarteschlange gibt es zwei Varianten: öffentlich und privat. Von beiden Typen kann eine beliebige Anzahl erstellt werden. Die öffentliche Warteschlange wird innerhalb des MSMQ-Frameworks gespeichert. Das hat zur Folge, dass sie gesucht und gefunden werden kann. Warteschlangen dieses Typs sind außerdem dauerhaft und können gesichert werden. Die öffentliche Warteschlange bietet grundlegende Funktionen, sodass sie in einer Unternehmensumgebung eingesetzt werden kann. Die private Warteschlange wird auf dem lokalen Computer gespeichert. Sie kann nicht gesucht werden. Eine private Warteschlange verursacht im Gegensatz zur öffentlichen Warteschlange keinen Overhead und ist daher wesentlich schneller. Verwaltungswarteschlangen Wenn eine Nachrichtenwarteschlange erstellt wird, kann der Weg der Nachrichten verfolgt werden. Dies kann von Bedeutung sein, wenn eine Nachricht vertrauliche Informationen enthält oder wenn die Informationen in einem bestimmten zeitlichen Rahmen gesendet werden müssen. In diesen Fällen kann es wichtig sein zu wissen, ob die Nachricht gesendet wurde und ob sie pünktlich übermittelt wurde. Außerdem ist manchmal wichtig, ob beim Senden der Nachricht ein Fehler aufgetreten ist. Wenn diese Eigenschaften der Nachricht angefordert werden, so werden diese Daten in Form einer Nachricht zurückgegeben. Damit diese Nachrichten nicht mit den eigentlichen Anwendungsnachrichten verwechselt werden, gibt es eine Verwaltungswarteschlange, die derartige Statusnachrichten enthält. Antwortwarteschlange Wenn ein Sender eine Nachricht an einen Empfänger sendet, kann der Sender oft eine Antwort anfordern. Diese Art der Antwortnachricht ähnelt dem zuvor genannten Nachrichtentyp. Es gibt jedoch einen wesentlichen Unterschied. Die typische administrative Nachricht enthält nur in beschränktem Umfang Details, und sie wird gesendet, wenn Fehler aufgetreten sind. Eine Antwortnachricht enthält Anwendungsdaten. Sie funktioniert nur dann, wenn die Nachricht übermittelt wurde. Wurde die Nachricht nicht übermittelt, wartet der Sender auf die Rückkehr der Nachricht. Bei Ausbleiben einer Antwort kann nicht festgestellt werden, ob ein Fehler aufgetreten ist.
342
Entwickeln von Messaging-COM+-Objekten
Journalwarteschlangen Die Journalwarteschlange wird erstellt, sobald einem MSMQ-Unternehmen ein Computer hinzugefügt wird. Der Zweck des Journals besteht darin, Nachrichten zu protokollieren. Die Nachrichten werden nur dann in das Journal aufgenommen, wenn dies in der Warteschlange festgelegt wurde. Damit Nachrichten nach einem Fehler erneut gesendet werden können, müssen sie im Journal aufgezeichnet werden. Ein Journal wird auf dem lokalen Computer verwaltet, da die Nachricht nach dem Aufzeichnen im Journal als Bestandteil des Systems betrachtet wird. Es gibt zwei Arten von Journalen: das Computerjournal, in dem alle Nachrichten vom jeweiligen Computer aufgezeichnet werden, und das Warteschlangenjournal, das eigentlich eine Warteschlange ist, die im Speicherort der Nachrichtenwarteschlange erstellt wird. Das Warteschlangenjournal zeichnet die Nachrichten auf, die aus der Hauptwarteschlange entfernt werden. Warteschlangen für nicht übermittelbare Nachrichten Die Warteschlange für nicht übermittelbare Nachrichten ist vergleichbar mit einer Journalwarteschlange, die je Computer angelegt wird. Darin werden die Nachrichten gespeichert, die nicht übermittelt werden konnten. Es gibt zwei Arten von Warteschlangen für nicht übermittelbare Nachrichten: eine für nicht auf Transaktionen basierende Nachrichten und eine für Transaktionsnachrichten. In diesen Warteschlangentypen werden die Nachrichten unterschiedlich verarbeitet. Eine nicht übermittelbare Nachricht, die nicht auf Transaktionen basiert, wird in der Warteschlange für nicht übermittelbare Nachrichten auf dem Computer gespeichert, der die Nachricht nicht übermitteln konnte. Der Grund für das Fehlschlagen der Übermittlung könnte durch einen Netzwerkausfall verursacht worden sein oder einfach dadurch, dass die Nachricht abgelaufen ist. Wenn eine Nachricht also im Journal aufgezeichnet wird und auf dem vorherigen Computer in die Journalwarteschlange aufgenommen wurde, verbleibt die Nachricht auf diesem Computer. Durch eine nicht übermittelbare Nachricht werden auf den Computern, die sie bereits durchlaufen hat, keine Änderungen vorgenommen. Wenn eine Nachricht im Transaktionsmodus gesendet und als nicht übermittelbar eingestuft wird, verschiebt der sendende Computer sie in das Journal für nicht übermittelbare Nachrichten. Alle im Netzwerk durchgeführten Aktionen werden gelöscht, und es ist keine Kopie der Nachricht mehr vorhanden. Berichtwarteschlangen Berichtwarteschlangen sind Warteschlangen, die den Verlauf der Nachrichtenübermittlung bis zum Ziel verfolgen. Berichtwarteschlangen sind hilfreich für den Administrator, der den Weg der Nachrichten nachvollziehen muss.
Einführung in MSMQ
343
11.2 Schreiben einer Messaginganwendung Das Schreiben von Messaginganwendungen ist nicht so leicht wie es scheint. Der Prozess des Versendens von Daten von einem Standort zu einem anderen ist relativ einfach. Komplexer wird der Vorgang durch die Vielfalt der Optionen und den Mangel an Festlegungen zur Datenverschlüsselung. Um Messaginganwendungen effektiv gestalten zu können, müssen zwei Probleme überwunden werden. Das erste Problem besteht in der Neuerstellung einer Datenstruktur. Eine typische Messaginglösung beinhaltet das Speichern einer Struktur von Daten als Nachricht. Die Nachricht wird anschließend an einen anderen Ort gesendet, an dem sie gelesen und in eine Struktur zurückkonvertiert wird. Die Lösung ist also aufgeteilt in eine Sender- und eine Empfängerseite, die möglicherweise in derselben Sprache geschrieben ist oder auch nicht. Sowohl der Sender als auch der Empfänger erfordern Verwaltungsaufwand, und zum Schreiben und Lesen der Daten müssen explizite Routinen erstellt werden. Das zweite Problem liegt im Einrichten eines effektiven Kommunikationsschemas. Wenn ein Nachrichtenempfänger eine Nachricht abruft, geschieht dies über eine bestimmte Warteschlange. Im Idealfall gibt der Empfänger eine Antwort zurück; er kann jedoch nicht dieselbe Warteschlange verwenden, aus der er die Nachricht empfangen hat, da er seine eigene Nachricht wieder abruft, sobald die Nachricht in der Warteschlange abgelegt ist. Aus diesem Grund müssen Sender und Empfänger Nachrichten aus ihren eigenen Warteschlangen lesen.
11.2.1 Eine Lösung für das Messaging Die Lösung für das Problem der Erstellung und Verwaltung separater Sender und Empfänger besteht darin, das Befehlsmuster einzusetzen. Das Befehlsmuster vereinfacht den Entwicklungsprozess, da es den Sender und den Empfänger in einem Objekt zusammenfasst. Der Sendeprozess sieht die Nachricht nur als Objekt, das im Zuge eines Geschäftsprozesses bearbeitet wird; der Empfangsprozess versteht die Nachricht als generisches Objekt, das eine Schnittstelle bereitstellt, die instanziiert wird. Das Objekt wird über den Serialisierungsprozess von einem Standort zu einem anderen gesendet. Der Senderprozess serialisiert das Objekt in eine MSMQNachricht. Der Empfängerprozess hebt die Serialisierung auf, instanziiert das Objekt und führt es aus. Die Ausführung ist eine Methode einer definierten Schnittstelle, die vom Objekt implementiert wurde. Diese Methode führt anschließend auf der Grundlage dessen, was der Sender im Objekt gespeichert hat, einige Geschäftsvorgänge durch.
344
Entwickeln von Messaging-COM+-Objekten
Bei Verwendung des Befehlsmusters müssen der Sender- und der Empfängerprozess die Einzelheiten der Implementierung nicht kennen. Sie müssen nur nach der Schnittstelle IPatternCommand suchen und die entsprechende Methode aufrufen. Das offizielle Befehlsmuster wird nur mit einer einzigen Methode definiert. Der IpatternCommand-Schnittstelle kann jedoch eine reply-Methode hinzugefügt werden. Die reply-Methode ermöglicht das Senden einer Nachricht, das Durchführen von Verarbeitungsfunktionen und das Senden einer Antwort. In der COM-IDL (Interface Definition Language) wird die IpatternCommandSchnittstelle folgendermaßen definiert: interface IPatternCommand : IDispatch { [id(1)] HRESULT execute(); [id(2)] HRESULT reply(); [propput, id(3)] HRESULT service([in] IDispatch* newVal); };
Der Eigenschaftsdienst wird zum Einstellen des Kontexts der aktuellen Ausführungsumgebung verwendet. Wenn ein COM-Objekt als MSMQ-COM-Objekt definiert werden soll, müssen die IpatternCommand-Schnittstelle und die IpersistStream-Schnittstelle implementiert werden. Bei der IpersistStream-Schnittstelle handelt es sich um eine standardmäßige COM-Schnittstelle, die zum Implementieren der Serialisierung verwendet wird. Wenn der Sendeprozess eine Nachricht sendet und der Empfangsprozess eine Nachricht abruft, muss der Name der Warteschlange, des Computers und des Pfads definiert werden. Diese Informationen sollten am besten flexibel gehalten werden. Es empfiehlt sich im Allgemeinen nicht, den Warteschlangen- oder den Computernamen hart zu codieren. Die Beschreibung könnte einer Visual C++Headerdatei oder einer globalen Visual Basic-Moduldatei hinzugefügt werden; dies bedeutet jedoch, dass ein Skriptclient keine Nachricht senden könnte. Die Lösung besteht darin, eine COM-Schnittstelle zu definieren, die Methoden festlegt, über die der richtige Computer- oder Warteschlangenname abgerufen wird. Die folgende IDL-Schnittstelle zeigt ein Beispiel: interface IMsgProp : IUnknown {
Die Eigenschaften sind schreibgeschützt, da sie von der Implementierung abhängig sind. Der Sender- und der Empfängerprozess instanziieren das COM-Objekt, das diese Schnittstelle implementiert, und senden bzw. empfangen die Nachricht über die Computernamen- oder Warteschlangennamen-Eigenschaft, die durch die Implementierung der IMsgProp-COM-Schnittstelle definiert wird. Der Vorteil dieses Ansatzes liegt darin, dass die Namen und Speicherorte der Warteschlangen aktualisiert werden können, ohne die Sender- oder Empfängerprozesse oder das COM-Objekt neu zu kompilieren, das die IPatternCommand-Schnittstelle implementiert.
11.2.2 Schreiben einer Implementierung Das Konzept der Implementierung der IPatternCommand-Schnittstelle wurde bereits erörtert, die Implementierung der IPersistStream-Schnittstelle jedoch noch nicht. Es ist möglich, diese COM-Schnittstelle wie jede andere COMSchnittstelle einzusetzen. Dies verursacht jedoch einen größeren Arbeitsaufwand, wenn Visual Basic oder Visual J++ verwendet wird. Diese Tools bieten integrierte Unterstützung für die Implementierung dieser Schnittstelle. Implementieren von IPersistStream in Visual Basic Betrachten Sie die Visual Basic-Klassendateieigenschaften in Abbildung 11.8.
Abbildung 11.8 Visual Basic-Klasseneigenschaften
Die Klasse clsLine besitzt eine Eigenschaft namens Persistable. Diese kann entweder den Wert Not Persistable oder, wie in der Abbildung dargestellt, Persistable annehmen. Diese Eigenschaft bewirkt die Implementierung von IPersistStream, IPersistStreamInit und IPersistStorage im daraus hervorgehenden COM-Objekt. Die Schnittstellen IPersistStreamInit und IPersistStorage wurden bislang noch nicht behandelt; gehen Sie bisweilen davon aus, dass ihre Funktion der von IPersistStream entspricht, außer dass sie die Daten auf andere Weise speichern. Nachdem die Klasse auf Persistable eingestellt wurde, muss die Visual Basic-Klasse die Class_ReadProperties- und Class_WriteProperties-Methoden implementieren; hierbei handelt es sich um Persistenzereignisse. Innerhalb der
346
Entwickeln von Messaging-COM+-Objekten
Persistenzereignisse müssen die Objekteigenschaftswerte mit Hilfe von PropertyBag geschrieben werden. In Visual Basic fließen die Daten in eine so genannte PropertyBag. Eine PropertyBag kann als ein Teil des Arbeitsspeichers betrachtet werden, der den Status eines COM-Objekts enthält, in diesem Fall die Visual Basic-Klasse. Die PropertyBag wird für Steuerelemente, für Active Documents (ein fortgeschrittenes COM-Dokumentkonzept, das über den Rahmen dieses Buches hinausgeht) und für MSMQ-Persistenz verwendet. Die PropertyBag enthält eine Momentaufnahme des COM-Objekt-Status. Indem ein Objekt initialisiert und mit dem in einer PropertyBag enthaltenen Status geladen wird, wird der bestehende Status des Objekts gelöscht. Wenn sich also der Status des Objekts aufgrund von Operationen ändert, die an dem Objekt durchgeführt werden, ist es in Visual Basic ratsam, die PropertyBag über die Änderungen zu informieren. Andernfalls erkennen einige PropertyBag-Implementierungen nicht, dass der Status geändert wurde, und speichern daher das COM-Objekt nicht. (Es ist nicht erforderlich, der PropertyBag Änderungen mitzuteilen, wenn die PropertyBag eine MSMQ-Nachricht darstellt.) Ziehen Sie nun in Betracht, ein COM-Objekt in Visual Basic zu implementieren, das Persistenz unterstützt. Dieses Visual Basic-COM-Objekt besitzt einen Status. Der Status wird normalerweise durch einige private Datenmitglieder definiert. Diese privaten Datenmitglieder werden durch eine Geschäftsoperation am COMObjekt geändert. Die einfachste Möglichkeit zum Ändern der privaten Datenmitglieder besteht darin, sie als eine Reihe von COM-Eigenschaften darzustellen. Betrachten Sie folgendes Beispiel zur Definition einer COM-Eigenschaft in Visual Basic. Public Property Let x1(ByVal vData As Long) mvarx1 = vData PropertyChanged "x1" End Property
Die COM-Eigenschaft wird dem privaten Wert mvarx1 zugeordnet; anschließend wird durch Aufrufen von PropertyChanged mit dem Namen der Eigenschaft in der PropertyBag eine Statusänderung angezeigt. Der Name x1 ist ein Schlüssel, der zum Verknüpfen der Eigenschaft mit einem bestimmten Wert verwendet wird. Ein PropertyBag-Eigenschaftsname und der Eigenschaftsname einer COMSchnittstelle müssen nicht unbedingt übereinstimmen. Sie können beliebig benannt werden und ein beliebiges Objekt darstellen. Eine PropertyBag-Eigenschaft ist ein Bezeichner für ein Schlüsselwertpaar.
Schreiben einer Messaginganwendung
347
Datenvariablen werden aus der PropertyBag im Class_readProperties-Ereignis gelesen. Eine Beispielimplementierung lautet folgendermaßen: Private Sub Class_ReadProperties(PropBag As PropertyBag) mvarx1 = PropBag.ReadProperty("x1", 0) … End Sub
Die Variable mvarx1 wird zugeordnet, indem sie aus PropertyBag über den Schlüssel x1 abgerufen wird. Der letzte Parameter von ReadProperty ist ein Standardwert, wenn der Schlüssel in PropertyBag nicht enthalten ist. Zum Schreiben des Eigenschaftswerts wird folgender Code verwendet: Private Sub Class_WriteProperties(PropBag As PropertyBag) PropBag.WriteProperty "x1", mvarx1, 0 … End Sub
Die Methode WriteProperty schreibt ein Schlüsselwertpaar in PropertyBag. Der letzte Parameter wird zum Definieren eines Standardwerts verwendet. Beim Lesen und Schreiben von Daten aus der bzw. in die PropertyBag werden die Daten als VARIANT gespeichert. Daher kann es sich bei den Daten um alles handeln, was Visual Basic verarbeiten kann. Wenn Sie beispielsweise ein Objekt eingeben, wird das Objekt serialisiert. Die Serialisierung von Auflistungen wird allerdings nicht unterstützt; Auflistungen müssen manuell serialisiert werden. Nonbyte-Arrays werden nicht unterstützt. Erstellen einer serialisierbaren Auflistungsklasse Da Auflistungsklassen nicht automatisch serialisiert werden, muss eine angepasste Implementierung einer Auflistungsklasse erstellt werden, die serialisiert werden kann. Diese kann manuell codiert oder mit Hilfe des Visual Basic-Klassengenerators generiert werden. Wenn das Fenster des Klassengenerators aktiv ist, kann eine neue Auflistung hinzugefügt werden. Die auf diese Weise generierte Klasse ist eine Auflistung, die eine Reihe von Linienobjekten enthält. Sie wird mit clsLines benannt. (Als dieses Buch geschrieben wurde, war das Visual Basic-Add-In, das die automatische Erstellung von Auflistungsklassen ermöglicht, noch nicht funktionsfähig. Die Auflistungsklasse wird daher an dieser Stelle manuell codiert.) Eine Auflistungsklasse ist eine Klasse mit einer Reihe bestimmter Methodenaufrufe. Folgende Methoden werden benötigt:
348
Entwickeln von Messaging-COM+-Objekten
왘 Add: Fügt der Auflistung von Zeilen eine Zeile hinzu. Die Parameterliste für die
Add-Methode basiert auf den Daten, die zum Definieren einer gültigen Zeile erforderlich sind. Bei der Implementierung dieser Methode wird ein clsLineObjekt instanziiert, dem die angegebenen Parameter zugewiesen werden. Das neu instanziierte Objekt wird anschließend als Rückgabewert zurückgegeben. Ein optionaler Parameter ist ein Schlüsselwert, der die Möglichkeit zum Indizieren des soeben instanziierten Objekts in der Auflistung über einen aussagekräftigen Bezeichner bietet. 왘 Item: Ruft das Element basierend auf dem angegebenen Index ab. Bei dem In-
dex kann es sich um einen numerischen oder einen alphanumerischen Wert handeln. 왘 Count: Ruft die Anzahl der Elemente ab, die in der Auflistung enthalten sind. 왘 Remove: Entfernt das Element, auf das im aktuellen Index verwiesen wird.
Damit die Auflistungsklasse über eine FOR EACH-Schleife in Visual Basic wiederholt durchlaufen werden kann, muss eine zusätzliche Methode hinzugefügt werden. Diese Methode ist für das Erstellen einer Kopie der Auflistung zuständig. Diese kopierte Auflistung repräsentiert nicht den Inhalt der lokalen Auflistung (mCol). Im Folgenden sehen Sie eine Beispielimplementierung: Public Property Get NewEnum() As IUnknown 'this property allows you to enumerate 'this collection with the For...Each syntax Set NewEnum = mCol.[_NewEnum] End Property
Bei der Eigenschaft mCol.[_NewEnum] handelt es sich um eine besondere Eigenschaft, die einen IEnumVARIANT-Wert aus der Visual Basic-Auflistung zurückgibt. Damit alles funktioniert, wird die Methode im letzten Schritt mit einer bestimmten ID versehen. Wählen Sie hierzu im Visual Basic-Menü Extras -> Prozedurattribute. Klicken Sie im daraufhin angezeigten Dialogfeld auf Weitere, um das Dialogfeld zu erweitern. Wählen Sie im Kombinationsfeld Name den Methodennamen NewEnum aus. Geben Sie im Kombinationsfeld Prozedur-ID den Wert –4 ein. Aktivieren Sie im Gruppenfeld Attribute die Option Dieses Mitglied ausblenden. Das Dialogfeld sollte etwa dem in Abbildung 11.9 dargestellten entsprechen. Der Grund dafür, dass die NewEnum-Methode ein besonderes Tag benötigt, liegt in der Funktionsweise der Visual Basic-Schleife FOR EACH. Wenn Sie eine Visual Basic-Schleife FOR EACH starten, sucht diese nach dem Auflistungsobjekt für eine COM-Methode mit einer dispID von –4. Ist dieses vorhanden, ruft sie die Methode auf und versucht, eine IEnumVARIANT-COM-Schnittstelle zu instanzi-
Schreiben einer Messaginganwendung
349
ieren und zurückzugeben. Die verschiedenen Elemente werden basierend auf der IEnumVARIANT-Schnittstelle geladen und einem Visual Basic-Objekt zugewiesen.
Abbildung 11.9 Dialogfeld »Prozedurattribute«
Diese neu erstellte Auflistungsklasse überdauert die einzelnen Elemente nicht. Der nächste Schritt besteht darin, sie dauerhaft zu gestalten, indem die Eigenschaft persistable auf persistable eingestellt wird. Auf diese Weise werden die Methoden InitProperties, ReadProperties und WriteProperties der Visual BasicKlasse hinzugefügt. Die Implementierungen für das Lesen aus der und Schreiben in die PropertyBag lauten folgendermaßen: Private Sub Class_ReadProperties(PropBag As PropertyBag) Dim tmp As Object Dim count As Long Dim c1 As Long count = PropBag.ReadProperty("CollectionCount", 1) For c1 = 1 To count tmp = PropBag.ReadProperty("Item" & c1, Null) mCol.Add tmp Next End Sub
350
Entwickeln von Messaging-COM+-Objekten
Private Sub Class_WriteProperties(PropBag As PropertyBag) Dim c1 As Long PropBag.WriteProperty "CollectionCount", mCol.count For c1 = 1 To mCol.count PropBag.WriteProperty "Item" & c1, mCol.Item(c1), Null Next End Sub
Wenn die Serialisierung der Auflistung in den Stream beginnt, ist es wichtig, die Gesamtanzahl der Elemente (mCol.Count) als erstes Element in den Stream zu schreiben. Dies ist erforderlich, da der Klasse beim Einlesen der Auflistung die Anzahl der Elemente nicht bekannt ist. Um daher anzugeben, wie viele Elemente gelesen werden sollen, muss die Klasse die Anzahl in der Auflistung speichern. Implementieren von IPersistStream in Visual J++ Die IPersistStream-Schnittstelle wird innerhalb der Java Virtual Machine (VM) implementiert. Dies ist für das Java-COM-Objekt jedoch nicht erforderlich. Stattdessen implementiert das Java-COM-Objekt die standardmäßige Java-Serialisierungsschnittstelle (java.io.Serializable). Durch Einsatz dieser Schnittstelle werden IPersistStream, IPersistStreamInit und IpersistStorage folgendermaßen implementiert: import java.io.Serializable; public class cLine implements Serializable { private long x1, x2, y1, y2; … }
Wenn die Serializable-Schnittstelle in Java implementiert wird, untersucht Java VM die Klassendeklaration und sucht nach Datenmitgliedern, die in einen Puffer geschrieben werden können. Wenn es sich bei den Datentypen um die Java-Datentypen oder andere Objekte handelt, die die Serializable-Schnittstelle implementiert haben, so funktioniert die Serialisierung. Lautet die Datendeklaration transient oder static, wird das Datenmitglied nicht serialisiert. Im Folgenden wird ein Beispiel für jede Datendeklaration aufgeführt: transient long var1; static long var2;
In den Fällen, in denen der Programmierer alle Variablen entweder als vorübergehend (transient) oder als Objektdeklarationen deklariert hat, müssen Sie die Methoden ReadProperties und WriteProperties implementieren, um diese Ele-
Schreiben einer Messaginganwendung
351
mente zu serialisieren. Ein Beispiel für die Konvertierung der einzelnen Werte der Java cLine-Klasse in vorübergehende Werte und für das Implementieren der Persistenzmethoden lautet folgendermaßen: transient private long x1, x2, y1, y2; private void writeObject(ObjectOutputStream s) throws IOException { s.writeLong( x1); s.writeLong( x2); s.writeLong( y1); s.writeLong( y2); } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { x1 = s.readLong(); x2 = s.readLong(); y1 = s.readLong(); y2 = s.readLong(); }
Die PropertyBag in Java wird als ObjectInputStream bezeichnet. Beide Begriffe haben dieselbe Bedeutung, im Gegensatz zu Visual Basic erfordert Java jedoch, dass der geschriebene Datentyp angegeben wird. Im vorhergehenden Code wurden die Methoden readLong und writeLong verwendet. Außerdem ist es möglich, Objekte (writeObject), Floatwerte (writeFloat) usw. zu schreiben. Implementieren von IPersistStream in Visual C++ Wenn Sie Visual C++ verwenden, um das Objekt dauerhaft zu gestalten, muss eine der COM-Schnittstellen (IPersistStream, IPersistStreamInit usw.) implementiert werden. Standardmäßig gibt es keine Helferklassen. Im Quellcode für dieses Buch ist eine Beispielklasse enthalten, die für die Standardimplementierung zuständig ist. Diese Klasse ist eine Vorlage namens MsgIPersistStreamImpl, die zu weiten Teilen wie die Java-Schnittstelle Serializable funktioniert. Sie muss einer Vererbungsliste eines ATL-COM-Objekts (Active Template Library) hinzugefügt werden. Anschließend muss die COM MAP extrahiert werden, um auf die Verfügbarkeit von IPersistStream hinzuweisen. Die serialisierten Datenmitglieder werden in der Struktur gespeichert. In dieser Implementierung
352
Entwickeln von Messaging-COM+-Objekten
der Serialisierung wird eine ganze Struktur geschrieben und anschließend aus dem Stream gelesen. Im Folgenden ist ein ATL-COM-Objekt mit den hinzugefügten Teilen in Kurzform aufgeführt: class ATL_NO_VTABLE CLine : public MsgIPersistStreamImpl< CLine >, { public: CLine () { clearMemory(); resetParameters(); } BEGIN_COM_MAP(CLine) COM_INTERFACE_ENTRY_IMPL_IID(IID_IPersistStream, MsgIPersistStreamImpl) END_COM_MAP() private: typedef struct { long x1; long x2; long y1; long y2; } structCLine; public: structCLine m_value; DWORD m_size; BOOL m_bRequiresSave; };
Die Vorlage MsgIPersistStreamImpl greift auf die öffentliche Darstellung des Datenmitglieds m_value zurück. Hierbei handelt es sich um eine Struktur, die alle Datenmitglieder enthält, die serialisiert werden sollen. Das Datenmitglied m_size entspricht der Größe der zu speichernden Struktur. Es wird von MsgIPersistStreamImpl verwendet, um zu ermitteln, wie viele Daten geschrieben oder gelesen werden sollen. Das Flag m_bRequiresSave ist ein nicht bereinigtes Flag. Wenn Eigenschaften geändert werden, sollten Sie es auf TRUE einstellen. Speichern von COM-Objekten Diese Serialisierungstechnik funktioniert problemlos, solange in der Struktur nur einfache Datentypen enthalten sind. Ist ein komplexer Datentyp vorhanden, beispielsweise ein Objekt, ist zusätzlicher Serialisierungsaufwand erforderlich. Die Vorlage MsgIPersistStreamImpl umfasst zwei
Schreiben einer Messaginganwendung
353
virtuelle Funktionen, die eine benutzerdefinierte Serialisierung ermöglichen. Die LocalLoad-Methode bietet zusätzliche Funktionen zum Lesen, die LocalSaveMethode zum Schreiben von Objekten. In jedem Fall gibt die Methode ein Äquivalent zur PropertyBag zurück, das eine Istream-Schnittstellenimplementierung darstellt. Die Lese- und Schreibmethoden setzen voraus, dass der IstreamSchnittstellenzeiger aus einem bzw. in einen Bytestream liest und schreibt. Dieser Bytestream kann nur einmal gelesen werden. Nachfolgend finden Sie ein Beispiel für die Implementierung von LocalLoad oder LocalSave: private: PRJBATCHEXLib::UserDataPtr ptrUser; public: void LocalLoad( LPSTREAM pIStream) { ptrUser.CreateInstance( "PrjBatchEx.UserData.1"); IPersistStreamPtr ptrStream = ptrUser; ptrStream->Load( pIStream); } void LocalSave( LPSTREAM pIStream, BOOL fClearDirty) { IPersistStreamPtr ptrStream = ptrUser; ptrStream->Save( pIStream, TRUE); }
Beim Objekt ptrUser handelt es sich um eine private Variable, in der IPersistStream implementiert wurde. Sie ist ein verschachteltes COM-Objekt. Wenn die Implementierungen von LocalSave die Daten speichern, wird die ptrUserSchnittstelle mit Hilfe von IUnknown::QueryInterface in eine IPersistStreamSchnittstelle konvertiert. Anschließend wird die Save-Methode aufgerufen, und der angegebene IStream wird an das Objekt weitergeleitet. Das Laden des verschachtelten Objekts geschieht auf ähnliche Weise (LocalLoad), damit es funktioniert, muss jedoch das übergeordnete Objekt zuerst instanziiert werden. Anschließend wird, wie beim Speichern des verschachtelten Objekts, mit QueryInterface in IPersistStream konvertiert. Dieses Mal wird Load mit dem angegebenen IStream-Schnittstellenzeiger aufgerufen. Serialisierung, Datentypen und Versionsnummern Die Serialisierung besitzt auch einen Nachteil. In dauerhaften Daten sind keine Versionsnummern enthalten. Das bedeutet, dass das serialisierte COM-Objekt nach einer Aktualisierung eventuell nicht mehr funktioniert, da zusätzliche Daten-
354
Entwickeln von Messaging-COM+-Objekten
felder möglicherweise hinzugefügt oder vorhandene entfernt wurden. Diese Situation kann in einem Messagingszenario auftreten, da es mit MSMQ möglich ist, einen Bestand an gesendeten Nachrichten zu erhalten. Die Lösung für dieses Versionsproblem besteht darin, der Klassendefinition ein Versionsfeld hinzuzufügen. Wenn die Versionsnummern nicht übereinstimmen, muss das serialisierte Objekt einen Vorgang einleiten. Stellen Sie sich vor, dass Visual Basic den serialisierten Puffer erstellt. Kann dieser Puffer anschließend mit Visual Basic oder Visual J++ gelesen werden? Die Antwort lautet: wahrscheinlich nicht. Der Grund hierfür liegt darin, dass IPersistStream das Format der gespeicherten Daten nicht definiert. Dieses Problem wird verstärkt, wenn die Daten auf einer Plattform erstellt wurden und auf einer anderen Plattform gelesen werden. Die Lösung des Problems besteht darin, den Stream mit demselben Tool zu lesen, mit dem er erstellt wurde.
11.2.3 Beispiel für eine verteilte Anwendung Eine ausgereifte verteilte Anwendung umfasst Messagingfunktionen. Bislang haben wir in diesem Buch alles behandelt, was zum Ausfüllen der Details der Darstellungsschicht und der Anwendungslogik erforderlich ist. Der Webclient ruft einen Webserver, d.h. Internet Information Server (IIS), auf. IIS ruft eine Active Server Page (ASP) auf, die wiederum ein COM+-Objekt aufruft. Dieses COM+-Objekt wird im Kontext einer Transaktion ausgeführt. Das COM+-Objekt kann im Kontext der Transaktion einige Verarbeitungsvorgänge durchführen. Ein Schritt dieser Verarbeitung besteht in der Erstellung einer Nachricht, die an die Warteschlange gesendet wird, wenn die Transaktion erfolgreich übergeben wurde. Ein Empfänger ruft die Nachricht ab, startet einen weiteren Transaktionskontext und führt die übrigen Aufgaben durch. In Kapitel 9 wurde erläutert, dass die COM-Objekte in unserer Anwendungslogik in drei Objekttypen unterteilt werden: Daten, Operationen und Überprüfung. Das Senden von Nachrichten ist in dieser Architektur nicht berücksichtigt. Zum Senden von Nachrichten wird das bereits genannte Befehlsmuster verwendet. In diesem Fall werden die Datenobjekte dem Befehlsmusterobjekt zugewiesen. Wenn das IPatternCommand-Objekt in eine Messagingnachricht serialisiert wird, werden auch die Datenobjekte serialisiert. Das bedeutet, dass die Datenobjekte so geändert werden müssen, dass IPersistStream implementiert wird, sodass sie in die MSMQ-Nachricht serialisiert werden können. Wenn der Empfänger die Nachricht aus der Warteschlange abruft, ist das IPatternCommand-Objekt auch für das Aufheben der Serialisierung der Datenobjekte sowie für deren Instanziierung zuständig.
Schreiben einer Messaginganwendung
355
Dies ist eine optimale Vorgehensweise beim Schreiben verteilter Anwendungen. Die Implementierung der Datenobjekte ist die einzige Änderung, die zum Senden des Status an einen anderen Ort erforderlich ist. Die Operations- und Überprüfungsobjekte werden nicht übermittelt und daher nicht durch Datenobjekte beeinträchtigt, die von einem anderen Standort aus gesendet werden. Für Operations- und Überprüfungsobjekte ist nur der Status des Datenobjekts relevant. Dadurch wird die Lösung sehr granular. Bei einer granularen Lösung wie dieser, ist es möglich, die einzelnen Objekte auf bestimmte Eigenschaften auszurichten. Beispielsweise befasst sich das Operationsobjekt nur mit der Bearbeitung eines Status. Das Datenobjekt ist für das Verwalten des Status zuständig, der in eine Nachricht serialisiert werden kann. Das Befehlsobjekt verschiebt Nachrichten von einem Standort zu einem anderen und startet anschließend einen Stapelverarbeitungsauftrag, um die Nachrichten zu verarbeiten. Im Großen und Ganzen finden hierbei nur wenige Datenkopiervorgänge statt. Beschreiben der Architektur in UML Stellen Sie sich nun eine vollständige Implementierung in Form eines UML-Diagramms vor (Unified Modeling Language). An dieser Stelle wird die Implementierung des Vorgangs einer Benutzerregistrierung erläutert, die anschließend an einen anderen Standort weitergeleitet wird. Die UML und der Programmierquellcode stehen im Verzeichnis appConference des Quellcodes für dieses Buch zur Verfügung. Zunächst wird ein UML-Diagramm besprochen, das die Highlevel-Objekte zeigt. Anschließend wird die Aktivität behandelt, die außerhalb des Messaging stattfindet. Diese Aktivität wird danach in zwei Vorgänge aufgeteilt: das Senden der Aktivität zu einer Nachricht und das Lesen der Aktivität aus einer Nachricht. Beginnen wir also mit dem UML-Klassendiagramm. Der Registrierungsvorgang in einem UML-Klassendiagramm wird in Abbildung 11.10 dargestellt. Es gibt zwei Datenobjekte: userData und addressData. Diese Datenobjekte enthalten Daten über einen Benutzer und die Adresse des Benutzers. Die Datenobjekte sind mit dem userOperations-Objekt verknüpft. Dieses Objekt besitzt Methoden wie Find (zum Suchen eines bestimmten Benutzers), Add und Delete. Zwischen dem userOperations-Objekt und dem fullVerification-Objekt besteht eine Abhängigkeit. Dies ist eine Folge der Methodenaufrufe des Operationsobjekts. Das cmdRmUser-Objekt ist ein Befehlsobjekt. Es besitzt eine Verknüpfung mit den Objekten userData und addressData. Die Abhängigkeit wird in der Execute-Methode des Befehlsobjekts hergestellt.
356
Entwickeln von Messaging-COM+-Objekten
Abbildung 11.10 Architektur in Bezug auf eine UML-Klasse
Eine Aktivität in einer verteilten Anwendung Das Klassendiagramm zeigt allerdings nicht die Reihenfolge, in der die Objekte bearbeitet und verändert werden. Das Aktivitätsdiagramm erweitert das Sequenzdiagramm (Abbildung 9.5), das in Kapitel 9 eingeführt wurde. Dieses Sequenzdiagramm zeigte die Interaktion zwischen den Daten-, Operations- und Überprüfungsobjekten. Das neue Sequenzdiagramm (Abbildung 11.11) zeigt die Aktivität der Benutzerregistrierung im Kontext einer Transaktion. Die Grundlage einer Aktivität ist ein genericConsumer-Objekt, das das userOperations-Objekt instanziiert. Dieses instanziiert wiederum die Objekte userData und addressData. Sie sollten festlegen, ob entweder das genericConsumer- oder das userOperations-Objekt die Datenobjekte erstellt. Ich ziehe es vor, die Anzahl der Methodenaufrufe, die vom Konsumenten getätigt werden müssen, möglichst gering zu halten und das Operationsobjekt die erforderlichen Datenobjekte erstellen zu lassen. An dieser Stelle können Sie auch eine Optimierung einsetzen. Einige Datenobjekte können sehr umfangreich sein und werden bei bestimmten Operationen möglicherweise nicht verwendet. Indem Sie die Erstellung des Objekts bis zu dem Zeitpunkt hinauszögern, an dem auf das Datenobjekt zum ersten Mal verwiesen wird, können Sie eine unnötige Ressourcenauslastung vermeiden. Dies hat sich
Schreiben einer Messaginganwendung
357
Abbildung 11.11 Sequenzdiagramm mit Details einer Aktivität
bei Transaktionen bewährt, weil das Ideal verfolgt wird, Ressourcen spät aufzurufen und früh wieder freizugeben. Nach der Instanziierung der verschiedenen Objekte muss der generische Konsument die Datenobjekte mit Daten füllen. Der nächste Schritt im Sequenzdiagramm besteht darin, die jeweilige Operation für das userOperations-Objekt aufzurufen. Durch diesen Vorgang wird das Überprüfungsobjekt fullVerification instanziiert. Die Daten werden in Form von Objektverweisen abgerufen, und die
358
Entwickeln von Messaging-COM+-Objekten
Überprüfung wird durchgeführt. Zum Schluss wird die Operation an dem Objekt ausgeführt. In Kapitel 10 wurde erläutert, dass bei Abschluss einer Transaktion alle von der Transaktion betroffenen Objekte deaktiviert und gelöscht werden. Das Sequenzdiagramm in Abbildung 11.11 zeigt, dass ein Problem vorliegt: Das userOperations-Objekt ist für die Erstellung der Datenobjekte zuständig, was bedeutet, dass die Datenobjekte ein Teil des Transaktionsstreams sein und gelöscht werden könnten, wenn die Transaktion abgeschlossen ist. Dies liegt jedoch nicht in unserer Absicht, da der in den Datenobjekten gespeicherte Status nicht verloren gehen soll. Möglicherweise sind Sie der Ansicht, dass dies in einer über den Status verwalteten Umgebung akzeptabel sei; beachten Sie jedoch, dass für diese Operationssequenz einige Statusangaben erforderlich sind. Stellen Sie daher beim Hinzufügen von Datenobjekten zu einer COM+-Anwendung sicher, dass sie keine Transaktionen verwenden und dass das Transaktionsattribut auf Transaktion nicht unterstützt eingestellt ist. Schreiben einer Nachricht in einer verteilten Anwendung Die zuvor definierte Benutzerregistrierungsaktivität muss nun in eine Messaginganwendung konvertiert werden, d.h., die Anwendung sendet Nachrichten, wenn eine bestimmte Aufgabe erfüllt werden soll. Das Sequenzdiagramm in Abbildung 11.12 zeigt den Vorgang zum Schreiben einer Nachricht. Beim Verfassen einer Nachricht gibt es zwei allgemeine Objekte: genericConsumer und BatchProcess. Das BatchProcess-Objekt ist ein generischer Prozess, der die Nachrichten verwaltet. (Der Stapelverarbeitungsprozess wird im letzten Kapitel dieses Buches behandelt.) Der genericConsumer instanziiert das Befehlsobjekt cmdRmUser mit der Operation new. Auf diese Weise werden wiederum die beiden Datenobjekte userData und addressData instanziiert. genericComponent ist außerdem für die Instanziierung des BatchProcess-Objekts zuständig, indem ein Verweis auf den derzeit ausgeführten BatchProcess erstellt wird. Wenn die verschiedenen Objekte instanziiert sind, weist genericConsumer die Datenobjekte userData und addressData zu. Bei diesem Vorgang ist keine Datenübertragung erforderlich; es könnte sich um eine Zuweisung bereits aktiver userData- oder addressData-Objekte handeln. Nun ist die Objekthierarchie vollständig. In dieser Messaginganwendung wird die Operation auf der Empfängerseite durchgeführt. Das cmdRmUser-Objekt stellt das Objekt dar, das die IPatternCommand-COM-Schnittstelle implementiert. Das BatchProcess-Objekt ist für das Senden der Nachricht an die Warteschlange zuständig. Daher muss das Objekt cmdRmUser dem BatchProcess-Objekt zugewiesen werden. genericConsumer weist das BatchProcess-Objekt an, die Nachricht zu senden.
Schreiben einer Messaginganwendung
359
Abbildung 11.12 Sequenzdiagramm zum Schreiben einer Nachricht
360
Entwickeln von Messaging-COM+-Objekten
In diesem Fall übernimmt das BatchProcess-Objekt die Steuerung und serialisiert die Objekthierarchie, auf die durch cmdRmUser verwiesen wird. Der Serialisierung zufolge, die in einem vorhergehenden Abschnitt dieses Kapitels beschrieben wurde, verursacht cmdRmUser die Serialisierung der Objekte userData und addressData. Der serialisierte Informationsstream wird in einem Puffer gespeichert, den das BatchProcess-Objekt als Inhalt für eine MSMQ-Nachricht verwendet. Sobald der Serialisierungsvorgang abgeschlossen ist, ruft das BatchProcessObjekt sich selbst auf und löst das Senden der Nachricht an die MSMQ-Warteschlange aus. Lesen und Ausführen einer Nachricht Der zweite Teil der Messagingaktivität umfasst das Lesen und Verarbeiten der Nachricht. Bei der Verarbeitung der Nachricht wird die Anwendungslogik ausgeführt. Nachrichten verursachen kein Ereignis auf der Empfängerseite; es muss daher einen aktiven Prozess geben, der entweder das Polling oder MSMQ-COM-Verbindungspunkte verwendet, um auf eine Nachricht zu warten. Im Sequenzdiagramm zum Empfangen einer Nachricht wird der aktive Prozess als BatchProcess bezeichnet. Dies ist identisch mit dem BatchProcess im Sequenzdiagramm zum Senden einer Nachricht. Es kann sich hierbei um dieselben Anwendungen oder um zwei verschiedene Anwendungen handeln. Der Begriff BatchProcess weist darauf hin, dass es sich um einen externen Prozess handelt, der zum Senden oder Empfangen von Nachrichten von MSMQ verwendet wird. Im Sequenzdiagramm zum Lesen der Nachricht (Abbildung 11.13) wird der Prozess zum Warten auf die Nachricht nicht gezeigt. Das Sequenzdiagramm setzt voraus, dass der BatchProcess die Nachricht empfangen hat, und beginnt mit der Verarbeitung der MSMQ-Nachricht. Wenn eine Nachricht eingetroffen ist, verfügt BatchProcess über einen Puffer mit einem serialisierten Objektstream. Daher muss das BatchProcess-Objekt das cmdRmUser-Objekt instanziieren. Auf diese Weise werden wiederum die beiden Objekte userData und addressData instanziiert. Nachdem der Objektstatus durch den Serialisierungsvorgang wiederhergestellt wurde, führt das BatchProcess-Objekt vom cmdRmUser-Objekt aus einen QueryInterface-Vorgang für die IPatternCommand-Schnittstelle durch. Anschließend wird, wie bereits beschrieben, die IPatternCommand::Execute-Methode vom BatchProcess-Objekt aufgerufen. Diese führt den Registrierungsvorgang aus, der zu Beginn dieses Abschnitts beschrieben wurde.
Schreiben einer Messaginganwendung
361
Abbildung 11.13 Sequenzdiagramm zum Lesen einer Nachricht
Informationen zum Stapelverarbeitungsprozess Das BatchProcess-Objekt ist ein generischer Stapelverarbeitungsprozess, der es ermöglicht, eine Aufgabe an jedem beliebigen Remotestandort auszuführen. Aufgrund des Stapelverarbeitungsprozesses wird die Transaktion jedoch in zwei Teile unterteilt. Eine Transaktion wird auf der Senderseite und eine andere auf der Empfängerseite durchgeführt. Der Sendeprozess geht davon aus, dass bei einem Erfolg der eigenen Transaktion auch die Empfängertransaktion erfolgreich verläuft.
362
Entwickeln von Messaging-COM+-Objekten
Was geschieht jedoch, wenn der Empfängerprozess abgebrochen wird? Das kann sehr kompliziert werden. Wenn auf der Senderseite Verarbeitungsvorgänge dauerhaft vorgenommen wurden, müssen diese wieder rückgängig gemacht werden. Theoretisches Rückgängigmachen einer Transaktion Das Rückgängigmachen einer Transaktion wird theoretisch nicht unterstützt. Wenn eine Transaktion abgeschlossen ist, wird jeder Status, der sich daraus ergibt, als korrekt betrachtet. In der Realität kann es jedoch vorkommen, dass Statusänderungen rückgängig gemacht werden müssen. Der Trick zum Rückgängigmachen einer Transaktion besteht darin, zusätzlich zur ersten Transaktion eine gegenläufige Transaktion durchzuführen. Beachten Sie, wie CAD-Software (Computer-Aided Design) Elemente eines Entwurfs rückgängig macht. In der CAD-Softwareumgebung ist das Rückgängigmachen von großer Bedeutung. Dieser Vorgang muss allerdings sehr ausgereift sein, da manche Dinge rückgängig gemacht werden sollen, andere jedoch nicht. Wenn Sie beispielsweise ein Haus entwerfen, sollte sich das Rückgängigmachen des Türentwurfs nicht auf die Fenster oder Wände auswirken. Wird die Tür aus der Zeichnung entfernt, sollte nur eine Wand ohne Tür zurückbleiben. Die CAD-Lösung für dieses Problem besteht in der Verwendung von Euler-Operatoren. Bei einem CAD-Entwurfsprozess sollte jede Operation ein genaues Gegenstück besitzen, das den Vorgang wieder rückgängig macht. Im Falle des Türbeispiels gibt es Operatoren zum Hinzufügen einer Tür und zum Entfernen einer Tür. Ein anderer Zusammenhang ist auch das Addieren und Subtrahieren von Zahlen. 1+2+3+4=10
Die gegenläufige Operation zum ersten Schritt lautet –1. Wenn man dies der mathematischen Gleichung hinzufügt, lautet sie folgendermaßen: 1+2+3+4-1=9
Das Ergebnis entspricht dem Ergebnis der Gleichung ohne den ersten Schritt: 2+3+4=9
In einem Geschäftsprozess muss jedes Objekt über gleiche und gegenläufige Methoden verfügen. Wenn Sie beispielsweise eine Methode zum Hinzufügen eines Benutzers verwenden, benötigen Sie auch eine Methode zum Löschen des Benutzers. Wichtig ist nun zu wissen, was gelöscht werden soll und wie die ursprünglichen Parameter lauteten. Um diese Informationen bereitzustellen, können Sie eine Protokollfunktion hinzufügen, über die die erfolgten Operationen aufgezeichnet
Schreiben einer Messaginganwendung
363
werden. Die Methode zum Rückgängigmachen ruft aus der Protokolldatei die Parameter ab, die im ursprünglichen Methodenaufruf verwendet wurden. Praktisches Rückgängigmachen einer Transaktion Beschäftigen wir uns nun damit, wie all diese Vorgänge praktisch umgesetzt werden können. Ein Teil der Lösung steht bereits in einer Technologie namens Queued Components (in Warteschlangen befindliche Komponenten) zur Verfügung. Queued Components werden detailliert in Kapitel 12 behandelt. Zum gegebenen Zeitpunkt ist nur relevant, dass Queued Components einen integrierten Mechanismus zum Rückgängigmachen besitzen. Dieser Mechanismus wird über eine COM-Schnittstelle namens IplayControl bereitgestellt, die folgendermaßen definiert wird: interface IPlaybackControl : IUnknown { HRESULT FinalClientRetry( void); HRESULT FinalServerRetry( void); };
Wenn eine Queued Component-Methode fehlschlägt, wird auf der Empfängerseite auf den Fehler hingewiesen, indem FinalServerRetry aufgerufen wird. Anschließend ruft die in der Warteschlange befindliche Komponente auf der Senderseite die FinalClientRetry-Methode auf, um die Auswirkungen der Nachricht rückgängig zu machen. Um dies in der Anwendung einzusetzen, implementieren die Operationsobjekte die IPlaybackControl-COM-Schnittstelle. Wenn eine der beiden Methoden der IPlaybackControl-COM-Schnittstelle aufgerufen wird, führt das Operationsobjekt die gegenläufige Aktion zu der Aktion durch, die normalerweise ausgeführt worden wäre. Den vorher dargestellten Sequenzdiagrammen zufolge würden die Objekte cmdRmUser und userOperations die IPlayBackControl-Schnittstelle implementieren. Anschließend würde BatchProcess die IPlayBackControl-Schnittstelle aufrufen, um auf einen Fehler hinzuweisen. BatchProcess würde dieselbe Aufrufsequenz durchlaufen, und jeder Methodenaufruf in der Aufrufsequenz würde die zuvor angewandten Operationen rückgängig machen. Das Wiedergabesteuerelement verfolgt denselben Ansatz wie die CAD-Euler-Operatoren. Der Unterschied liegt darin, dass es eine bestimmte COM-Schnittstelle gibt, die für das Einstellen des negativen Operationsflags zuständig ist. Diese Technik zum Rückgängigmachen einer Transaktion klingt vom technischen Standpunkt aus einfach, ist jedoch vom Anwendungsstandpunkt gesehen schwieriger. Wenn beispielsweise ein Geldautomat 100 DM ausgibt und anschließend einen Fehler feststellt, kann der Geldautomat nicht hinter der Person herlaufen
364
Entwickeln von Messaging-COM+-Objekten
und die 100 DM zurückverlangen. Stattdessen muss eine andere Maßnahme ergriffen werden, um das Geld sicherzustellen, und diese andere Maßnahme ist sicherlich keine einfache Lösung.
11.3 Resümee Durch das Messaging wurde der Grundton der Anwendung verändert. Messaging bietet die Möglichkeit, einen Status von einem Standort zu einem anderen zu senden. Der wichtigste Teil der gesamten Übung besteht darin nachzuvollziehen, dass das Verschieben des Status durch das Trennen der Datenobjekte von den Operationsobjekten wesentlich vereinfacht wird. Warum wird das Messaging also eingesetzt? Weil es Skalierbarkeit und Stabilität gewährleistet. In vielen Projekten hat sich das Messaging als stabilste Möglichkeit zum Schreiben funktionierender Anwendungen erwiesen. Das einzige Problem ist, dass das Programmiermodell geändert werden muss. Zu diesem Zweck wurde das Befehlsmuster eingeführt. Durch Verwenden dieses Musters ist es möglich, eine Aufgabe über verschiedene Standorte zu verteilen, ohne dass das Programmiermodell beeinträchtigt wird. Außerdem gibt es eine Technologie namens DCOM (Distributed COM), die es einem COM-Objekt ermöglicht, ein anderes COM-Objekt auf einem anderen Computer aufzurufen. Ich persönlich halte diese Technologie für weniger geeignet, da sie im Netzwerk sehr viel Datenverkehr verursacht und die Konfiguration eher kompliziert ist. Ich betrachte es als Hack, der in manchen Situationen eingesetzt werden kann. Messaging ist eine bessere Lösung, da ein Administrator alle Aspekte der Messaginganwendung steuern kann, beispielsweise welcher Server die Anforderung bearbeitet, oder dass Nachrichten bei einem Fehler erneut gesendet werden. Die Herausforderung liegt in der Bekehrung des Programmierers zum Schreiben ereignisgesteuerter serverseitiger Anwendungen.
Resümee
365
12 Asynchrone COM+-Dienste Dieses Kapitel enthält eine Einführung zu zwei neuen COM+-Diensten (Component Object Model): in der Warteschlange befindliche Komponenten (Queued Components) und Ereignisdienste. In der Warteschlange befindliche COM+-Komponenten sind DCOM-Komponenten (Distributed Component Object Model), die MSMQ (Microsoft Message Queue) verwenden. Der Vorteil von in der Warteschlange befindlichen COM+Komponenten liegt in der Fehlertoleranz. Derartige COM+-Komponenten bieten eine einfache Möglichkeit zur bedarfsabhängigen Verteilung von Verarbeitungsleistung. COM+-Ereignisse ermöglichen das Veröffentlichen und Abonnieren von Informationen. Wenn Sie in der Verwendung von COM erfahren sind, werden Sie feststellen, dass COM+-Ereignisse Verbindungspunkten ähneln mit der Ausnahme, dass COM+-Ereignisse stabiler und skalierbar sind. Wenn Sie COM+-Ereignisse verwenden, müssen Sie nicht wissen, wer die jeweiligen Informationen abonnieren möchte.
12.1 In der Warteschlange befindliche COM+-Komponenten Der Zweck von in der Warteschlange befindlichen COM+-Komponenten besteht darin, eine zuverlässige Kommunikation zwischen einem Client und einem Server zu gewährleisten. Am Ende von Kapitel 11 habe ich erwähnt, dass ich von DCOM nicht überzeugt bin. Dies liegt nicht am DCOM-Konzept, sondern an der Implementierung. DCOM ist ein Protokoll, das sehr viel Datenlast verursacht und dessen richtige Konfiguration kompliziert sein kann. Über eine weite Entfernung kann eine DCOM-Verbindung unzuverlässig sein. Erstrebenswert ist jedoch ein stabiles, zuverlässiges DCOM, und mit Windows 2000 gibt es eine robuste DCOM-ähnliche Technologie namens COM+ Queued Components (in der Warteschlange befindliche Komponenten). Mit COM+ Queued Components ist die Kommunikationsmethode zwischen zwei COM-Objekten mit der Verwendung von DCOM vergleichbar mit der Ausnahme, dass MSMQ als Transportmechanismus verwendet wird.
In der Warteschlange befindliche COM+-Komponenten
367
12.1.1 Funktionsweise von in der Warteschlange befindlichen Komponenten COM+ Queued Components ist eine Schicht, die zusätzlich zu MSMQ programmiert wurde. Das Problem, das in Kapitel 11 bei MSMQ aufkam, bestand darin, dass der Programmiervorgang durch das Senden unverarbeiteter Strukturen und Nachrichten erheblich erschwert wurde. Als Lösung wurde in Kapitel 11 das Befehlsmuster verwendet, um die Aufgabe vom Sender an den Empfänger zu verteilen. Der Vorgang wurde dadurch vereinfacht. In diesem Kapitel wird jedoch eine andere Lösung erläutert, die ein Problem behebt, das vom Befehlsmuster nicht gelöst wurde. In der Warteschlange befindliche COM+-Komponenten funktionieren durch Abfangen von Eigenschaftszuweisungen und Methodenaufrufen für ein COM+-Objekt, das als warteschlangenfähig definiert wurde. Diese Operationen werden von der COM+-Infrastruktur in einem Puffer aufgezeichnet. Dieser Puffer wird anschließend in eine MSMQ-Nachricht konvertiert, die an eine MSMQ-Warteschlange gesendet wird. Auf der Seite der Überwachungsfunktion gibt die COM+Infrastruktur die aufgezeichneten Operationen wieder. Das Ganze funktioniert wie ein COM-basierter Makrorecorder. Abbildung 12.1 zeigt die Funktionsweise von in Warteschlangen befindlichen COM+-Komponenten. Die Infrastruktur von COM+ Queued Components umfasst Geräte namens Recorder und Player. Instanziiert ein Client ein warteschlangenfähiges COM+-Objekt, so instanziiert die Infrastruktur der in der Warteschlange befindlichen COM+-Komponente eine Ghostschnittstelle. Die GhostCOM-Schnittstelle ist mit der COM-Schnittstelle des warteschlangenfähigen COM+-Objekts identisch und direkt mit dem Recorder verknüpft. Wenn also der Client einen Methodenaufruf oder eine Eigenschaftszuweisung vornimmt, wird diese automatisch aufgezeichnet. Nach Abschluss der Aufzeichnung werden die aufgezeichneten Informationen als Nachricht an die Überwachungsfunktion gesendet. Die Überwachungsfunktion öffnet die Nachricht und gibt die verschiedenen Methodenaufrufe und Eigenschaftszuweisungen wieder.
Abbildung 12.1 Infrastruktur einer in der Warteschlange befindlichen Komponente
368
Asynchrone COM+-Dienste
12.1.2 Implementieren einer warteschlangenfähigen COM+-Komponente Der Schnittstelle einer warteschlangenfähigen COM+-Komponente sind einige Einschränkungen vorgegeben. Diese Einschränkungen sind identisch mit den Einschränkungen, denen normale COM+-Objekte unterliegen, die MSMQ verwenden. Das heißt, die Kommunikation verläuft asynchron, sodass auf die Methodenaufrufe keine Antwort zu erwarten ist. Die Einschränkungen werden nicht auf das COM+-Objekt, sondern auf eine bestimmte COM-Schnittstelle angewendet, die das COM+-Objekt implementiert hat. Die COM-Schnittstelleneinschränkungen werden in folgenden Punkten zusammengefasst: 왘 Die in der Warteschlange befindliche Komponentennachricht muss Parameter
nach Wert enthalten. Es ist nicht möglich, Parameter nach Verweisen zu senden. Das bedeutet, dass Methodenparameter und -eigenschaften der warteschlangenfähigen COM-Schnittstelle nur mit dem IDL-Attribut (Interface Definition Language) [in] versehen werden dürfen. 왘 Wenn es sich bei dem Parameter um ein Objekt handelt, das die IPersist-
Stream-Schnittstelle unterstützt, wird er serialisiert. 왘 Die Kommunikation verläuft nur in eine Richtung, und es kann keine Antwort
gesendet werden. 왘 Der Recorder beendet die Aufzeichnung, bevor Nachrichten auf der Server-
seite wiedergegeben werden. Abgesehen von diesen Einschränkungen ist die Verwendung eines warteschlangenfähigen COM+-Objekts vergleichbar mit einem COM-Aufruf. Beispiel für eine warteschlangenfähige COM+-Schnittstelle Die am Ende des letzten Kapitels definierte Messagingaktivität wird im Folgenden angewendet. Es gab dort zwei Hauptobjekte in der verteilten Messaginganwendung: BatchProcess und cmdRmUser. Im letzten Kapitel wurde das BatchProcess-Objekt als einzelner Prozess oder als mehrere Prozesse definiert, die das Senden und Empfangen von MSMQ-Nachrichten übernehmen. Das cmdRmUser-Objekt implementierte die IPatternCommand-Schnittstelle. Die Hauptaufgabe des cmdRmUser-Objekts bestand in der Ausführung der Remoteaufgabe, wenn die IPatternCommand::Execute-Methode aufgerufen wurde. Zunächst erfolgt die Implementierung von BatchProcess. Dieses COM+-Objekt besitzt nur eine Schnittstelle mit einer einzigen Methode und einem einzigen Parameter. Wenn diese COM-Schnittstelle über Visual J++ oder Visual C++ definiert
In der Warteschlange befindliche COM+-Komponenten
369
wurde, ist es möglich, der IDL-Schnittstelle das Attribut »warteschlangenfähig« hinzuzufügen. In Visual Basic wird die Schnittstelle entweder manuell oder programmatisch als warteschlangenfähig registriert. Ein Beispiel für die Definition des Attributs »warteschlangenfähig« in IDL lautet folgendermaßen: [ helpstring("IBatchProcess "), uuid(CB34A1A4-28C2-11D2-B5B6-00C04FC340EE), object, dual, nonextensible, hidden, pointer_default(unique), QUEUEABLE ] interface IBatchProcess: IDispatch { [id(1)] HRESULT execute([in] IDispatch *obj); }
Der IBatchProcess ist ein Dienst, der von jedem beliebigen Client aufgerufen werden kann. Er enthält eine Methode, execute, mit einem Parameter. Der Parameter stellt das Remoteoperationsobjekt dar, das ausgeführt werden soll. In diesem Beispiel ist dies das Objekt cmdRmUser. Ein Beispiel für eine Visual Basic-Implementierung der IBatchProcess::execute-Methode lautet folgendermaßen: Public Sub execute(ByVal obj As Object) Dim tmp As PatternCommand Set tmp = obj tmp.execute End Sub
Das Objekt obj wird in die Variable tmp konvertiert, die COM-Schnittstelle IPatternCommand (PatternCommand). Anschließend wird die Methode IPatternCommand::Execute aufgerufen. Die Implementierung von BatchProcess ist damit abgeschlossen. Sie fragen sich vielleicht, wo der übrige Code ist. Die Antwort lautet, dass er im COM+ Queued Components-Framework enthalten ist. Warum wird dann das cmdRmUser-Objekt nicht als warteschlangenfähige Komponente offen gelegt? Die Antwort: COM-Schnittstelleneinschränkungen erschweren den Entwicklungsvorgang ein wenig. Wenn man dieses Objekt als warteschlangenfähig offen legt, muss auch jede COM-Schnittstelle als warteschlangenfähig offen gelegt werden. Durch Verwendung eines neutralen Objekts wie BatchProcess ist es nur notwendig, ein warteschlangenfähiges COM+-Objekt je Computer zu erstellen. Diese Architektur passt sich auch sehr elegant in die zuvor definierte Architektur ein. Die Einschränkung für das Senden von Objekten als Nachrichten bedeutet,
370
Asynchrone COM+-Dienste
dass sie die IPersistStream-Schnittstelle implementieren müssen. Dies ist jedoch einfach, da es bereits im letzten Kapitel durchgeführt wurde. Registrieren einer warteschlangenfähigen COM+-Komponente Ein warteschlangenfähiges COM+-Objekt muss einer COM+-Anwendung hinzugefügt werden, die COM+ Queued Component-Nachrichten akzeptiert. Beachten Sie, dass ein COM-Objekt zum COM+-Objekt wird, sobald es einer COM+Anwendung hinzugefügt wird. Im Kontext einer COM+-Anwendung ist es möglich, die Überwachungsfunktion für in der Warteschlange befindliche COM+Komponenten einzurichten. Erstellen Sie hierzu eine neue COM+-Anwendung. Klicken Sie anschließend mit der rechten Maustaste auf den Knoten der soeben erstellten COM+-Anwendung, und öffnen Sie das Eigenschaftendialogfeld für eine COM+-Anwendung. Wählen Sie die Registerkarte Warteschlange, die in Abbildung 12.2 dargestellt ist.
Abbildung 12.2 COM+-Warteschlangeneigenschaften
In diesem Beispiel wird die COM+-Anwendung als Test Application bezeichnet. Das Kontrollkästchen Einreihen ist aktiviert, sodass die Anwendung in der Warteschlange befindliche COM+-Komponentennachrichten empfangen kann. Das Kontrollkästchen Abfragen ist nicht aktiviert, d.h., dass in der Warteschlange befindliche COM+-Komponentennachrichten in der Warteschlange gespeichert, je-
In der Warteschlange befindliche COM+-Komponenten
371
doch nicht abgerufen werden. Wenn Sie das Kontrollkästchen Abfragen aktivieren, werden die Nachrichten automatisch abgerufen und verarbeitet. Wenn die COM+-Anwendung fertig ist, muss sie manuell gestartet werden. Dies kann entweder über den COM+-Explorer oder den COM+-Katalog-Manager geschehen. Informationen zu MSMQ-Warteschlangen Betrachten Sie, wie eine COM+ Queued Component-Anwendung mit einer MSMQ-Warteschlange arbeitet. Dies ist von Bedeutung, da hierdurch festgelegt wird, wo sich die Warteschlange befinden muss und welche Nachrichten an die Warteschlange gesendet werden können. Das COM+ Queued Component-Framework erstellt eine Warteschlange, die denselben Namen wie die COM+-Anwendung besitzt. Wenn Sie also MSMQ-Anwendungen den Anleitungen in Kapitel 11 entsprechend schreiben, können Sie keine Nachrichten an eine Warteschlange mit demselben Namen senden oder daraus abrufen. Dies kann und wird Konflikte mit der COM+ Queued Component-Infrastruktur hervorrufen.
Abbildung 12.3 MSMQ-Warteschlangeneigenschaften
Abbildung 12.3 zeigt, dass es sich um eine Transaktionswarteschlange handelt. Dies ist erforderlich, da die COM+-Anwendung Transaktionen verwendet. Wie im
372
Asynchrone COM+-Dienste
vorherigen Kapitel gezeigt, führen Fehler in MSMQ-Warteschlangen, die keine Transaktionen einsetzen, automatisch zum Verlust von Nachrichten. Das sollte unter allen Umständen vermieden werden. Jetzt greife ich ein wenig vor, da ich einige Nachrichten in der Warteschlange zeigen möchte. Abbildung 12.4 zeigt einfache Nachrichten. Wenn Sie eine davon öffnen und den Inhalt anzeigen würden, könnten Sie feststellen, dass es sich um Nachrichten mit binärem Inhalt handelt. Die Struktur des binären Inhalts ist nicht definiert. Es ist daher nicht möglich, die MSMQ-Nachrichten manuell über MSMQ-COM-Objektaufrufe zu bearbeiten. Dies könnte zur Irritation der COM+Anwendung und zu Fehlern führen.
Abbildung 12.4 Warteschlange mit Nachrichten
Registrieren der warteschlangenfähigen Komponente Sobald die COM+Anwendung für das Akzeptieren von COM+ Queued Component-Nachrichten eingerichtet ist, muss ein COM+-Objekt registriert und anschließend für die Annahme einzelner Nachrichten aktiviert werden. Wenn wir das vorherige Codebeispiel weiterführen, müsste das IBatchProcess-COM-Objekt der COM+-Anwendung hinzugefügt werden. Wenn IBatchProcess registriert ist, untersuchen Sie das COM+-Objekt und sehen sich die implementierten Schnittstellen an. Wählen Sie durch Klicken die COM-Schnittstelle aus, die die execute-Methode implementiert hat, klicken Sie mit der rechten Maustaste darauf, und wählen Sie im Kontextmenü den Befehl Warteschlange aus. Das in Abbildung 12.5 gezeigte Dialogfeld Eigenschaften wird angezeigt.
In der Warteschlange befindliche COM+-Komponenten
373
Abbildung 12.5 Als warteschlangenfähig aktivierte BatchProcess-COM-Schnittstelle
In dieser Abbildung ist das Kontrollkästchen Einreihen aktiviert, was darauf hinweist, dass diese Schnittstelle COM+ Queued Component-Nachrichten akzeptiert. Wenn diese COM-Schnittstelle in IDL definiert wäre und das Attribut QUEUEABLE besäße, würde sie automatisch eingestellt. Verletzt diese COMSchnittstelle eine der Einschränkungen für in der Warteschlange befindliche COM+-Komponenten, wird das Kontrollkästchen Einreihen abgeblendet angezeigt. Klicken Sie auf OK, um die Eigenschaften zu übernehmen. Die Anwendung wird dann für den Empfang von in der Warteschlange befindlichen COM+-Komponentennachrichten eingestellt.
12.1.3 Instanziieren einer warteschlangenfähigen COM+-Komponente Die Instanziierung einer warteschlangenfähigen COM+-Komponente unterscheidet sich von der Instanziierung eines normalen COM+-Objekts ein wenig. Der Unterschied liegt in der Instanziierungszeichenfolge. Bei normalen COM+-Objekten würden Sie CreateObject aufrufen und die CLSID (Klassen-ID) oder die PROG ID des gewünschten Objekts angeben. Bei in der Warteschlange befindlichen COM+-Komponenten verwendet die Zeichenfolge einen anderen Objekt-
374
Asynchrone COM+-Dienste
ersteller. Sehen Sie sich folgendes Beispiel an, über das ein warteschlangenfähiges COM+-Objekt instanziiert wird: Set userOp = GetObject("queue:/new:prjQueued.clsBatchProcess")
Die Funktion GetObject ruft ein vorhandenes Objekt aus einem Stream ab. Dieser Code ist in Visual Basic geschrieben, derselbe Funktionstyp wird jedoch auch in anderen Programmiersprachen verwendet. Ursprünglich wurde GetObject zum Instanziieren eines COM-Objekts aus einer Datei verwendet, da es COM-Bezeichnungen verwendet. Unter Windows 2000 gibt es zwei neue COM-Bezeichnungen: new und queue. Verwenden der Bezeichnung »New« Die Bezeichnung new wird zum Instanziieren eines beliebigen Typs von COMObjekten basierend auf dessen PROG ID oder CLSID verwendet. Im Folgenden wird je ein Beispiel aufgeführt: new:prjQueued.clsBatchProcess new:{812DF40E-BD88-11D0-8A6D-00C04FC340EE}
Um die Bezeichnung new einzusetzen, schreiben Sie den folgenden Visual BasicCode: set userOp = GetObject( "new:prjQueued.clsBatchProcess")
In Visual C++ würden Sie den Funktionsaufruf CoGetObject verwenden. Verwenden der Bezeichnung »Queue« Die Bezeichnung queue wird zum Instanziieren einer in der Warteschlange befindlichen COM+-Komponente eingesetzt. Sie wird in Verbindung mit der Bezeichnung new angewendet, um ein warteschlangenfähiges COM+-Objekt zu instanziieren. Der vorgenannte Code ist das einfachste Beispiel. Es ist außerdem möglich, MSMQ-Warteschlangenattribute innerhalb des Instanziierungstexts anzugeben. Um beispielsweise ein warteschlangenfähiges COM+-Objekt auf einem anderen Computer zu erstellen, würden Sie folgende Zeichenfolge verwenden: queue: ComputerName=cronos/new:prjQueued.clsBatchProcess
In diesem Beispiel wird die COM+ Queued Component-Nachricht an die Warteschlange auf dem Computer cronos weitergeleitet. Diese funktioniert die Nachricht in eine Art DCOM-Aufruf um, da der Aufruf sich über mehrere Computer erstreckt.
In der Warteschlange befindliche COM+-Komponenten
375
Es können auch weitere Optionen angegeben werden. In allen vorhergehenden Beispielen wurde angenommen, dass der Warteschlangenname mit dem Namen der COM+-Anwendung übereinstimmt. Betrachten Sie jedoch die Situation des Remoteclients. Die Infrastruktur der in der Warteschlange befindlichen COM+Komponente kennt den Namen der Remote-COM+-Anwendung nicht, sodass der Warteschlangenname als QueueName definiert werden muss. Es ist außerdem möglich, Attribute festzulegen, die die Versendungsart der Nachricht bestimmen. Zu den Beispielen gehören encryption, label und MaxTimeToReachQueue. Die Attribute werden der Instanziierungszeichenfolge des COM+Objekts hinzugefügt. Sie werden feststellen, dass sie den MSMQ-Nachrichtenund Warteschlangeneigenschaften direkt entsprechen. Der Grund dafür ist, dass COM+ Queued Components als Erweiterung zu MSMQ erstellt werden.
12.1.4 Falls etwas schief geht Bislang sind wir davon ausgegangen, dass alles gut geht. Was geschieht jedoch, wenn ein Fehler auftritt? Nehmen Sie beispielsweise an, das Netzwerk wird unterbrochen. Bei COM+ Queued Components werden Probleme verarbeitet und Nachrichten auf die eine oder andere Weise gesichert. Bei COM+ Queued Components gehen keine Nachrichten verloren. Dies ist der Vorteil von in der Warteschlange befindlichen Komponenten gegenüber einem Standard-DCOM-Aufruf. Stellen Sie sich die Situation eines Clients und eines Servers auf zwei verschiedenen Computern vor. Wenn Sie COM+ Queued Components verwenden, wird die Nachricht an den Server gesendet und anschließend von der COM+-Anwendung verarbeitet. Damit dieses Szenario auf dem Servercomputer funktioniert, muss ein COM+-Anwendungsproxy erstellt werden. Wählen Sie hierzu die COM+-Anwendung aus, und exportieren Sie sie als Anwendungsproxy. Installieren Sie diesen Proxy auf dem Clientcomputer. Eine mit der Server-COM+-Anwendung identische COM+-Anwendung wird erstellt. Wenn die Komponenten innerhalb der COM+-Anwendung auf dem Clientcomputer instanziiert werden, werden die Anforderungen jedoch an den Ursprungsserver gesendet. In diesem Client/Server-Szenario können viele verschiedene Fehler auftreten, die sich jedoch in zwei grundlegende Fehlertypen unterteilen lassen: Beim ersten Fehlertyp kann die Nachricht die Zielwarteschlange nicht erreichen, beim zweiten Fehlertyp ist die serverseitige Verarbeitung fehlgeschlagen. Warteschlange nicht erreichbar Der einfachste Fehlertyp besteht darin, dass die Nachricht die Zielwarteschlange nicht erreichen kann. Das kommt vor, wenn das Netzwerk nicht verfügbar ist oder
376
Asynchrone COM+-Dienste
wenn die Zielwarteschlange keine Nachrichten annimmt. Wenn die Nachricht nicht gesendet werden kann, wird sie in die Warteschlange für nicht übermittelbare Transaktionsnachrichten verschoben. Diese Warteschlange befindet sich unter der Überschrift System Queue im Message Queue-Explorer. Mit in der Warteschlange befindlichen COM+-Komponenten ist eine Warteschlangenprüffunktion verknüpft, die die Warteschlange für nicht übermittelbare Transaktionsnachrichten auf Nachrichten überprüft. Findet die Prüffunktion eine Nachricht vor und handelt es sich dabei um eine warteschlangenfähige COM+Objektnachricht, versucht die Funktion die mit dem warteschlangenfähigen COM+-Objekt verknüpfte Ausnahmeklasse zu instanziieren. Die Ausnahmeklasse ist ein COM+-Objekt, das instanziiert wird, wenn die Übermittlung einer COM+ Queued Component-Nachricht fehlschlägt. Der Zweck der Ausnahmeklasse besteht darin, eine Transaktion rückgängig zu machen, die aufgrund der ursprünglichen Nachricht erfolgt ist. Dieser Funktionalitätstyp wurde am Ende des letzten Kapitels erläutert, als ein Framework für den Undo-Vorgang erstellt wurde. Wählen Sie zum Angeben einer Ausnahmeklasse das COM+-Objekt in der COM+-Anwendung aus, klicken Sie mit der rechten Maustaste darauf, und wählen Sie den Befehl Eigenschaften, um die Eigenschaften des COM+-Objekts abzurufen. Die Registerkarte Erweitert des Dialogfelds wird in Abbildung 12.6 dargestellt.
Abbildung 12.6 Definition der Warteschlangen-Ausnahmeklasse
In der Warteschlange befindliche COM+-Komponenten
377
Die Warteschlangen-Ausnahmeklasse ist eine Klasse, die dieselbe Schnittstelle wie das fehlgeschlagene warteschlangenfähige COM+-Objekt und eine zusätzliche COM-Schnittstelle namens IPlaybackControl implementiert. Diese COMSchnittstelle wurde im letzten Kapitel behandelt. Ihr Zweck besteht darin, anzuzeigen, dass ein Fehler aufgetreten ist, indem die Methode IPlayBackControl::FinalClientRetry aufgerufen wird. Anschließend wird die endgültige Wiedergabe der fehlgeschlagenen Nachricht ausgeführt, und die Ausnahmeklasse versucht die durchgeführten Transaktionen rückgängig zu machen. Wenn dieser Vorgang gelingt, wird die fehlgeschlagene Nachricht aus der Warteschlange für nicht übermittelbare Transaktionsnachrichten entfernt. Andernfalls verbleibt die Nachricht in der Warteschlange für nicht übermittelbare Transaktionsnachrichten. Wenn die Nachricht in dieser Warteschlange enthalten ist, muss sie entweder über ein Verwaltungstool oder über ein Programm manuell gelöscht werden. Serverseitige Verarbeitung nicht möglich Das andere Fehlerszenario ist, dass eine Nachricht die Serverseite erreicht und das Wiedergabesteuerelement für COM+ Queued Components die COM+ Queued Component-Nachricht nicht erfolgreich durchführen kann. Diese Situation wird als Fehlschlag betrachtet. Zu diesem Zeitpunkt wird eine sehr lange Reihe von Versuchen ausgeführt. Ohne auf die Details einzugehen, verläuft dieser Vorgang folgendermaßen: 1. Die Verarbeitung der Nachricht wird erneut versucht. 2. Schlägt die Nachricht fehl, wird sie in die Warteschlange zurückgelegt. 3. Wenn die Anzahl fehlgeschlagener Nachrichten die Höchstzahl überschreitet, wird die Nachricht in eine andere Warteschlange verlegt. Der Name der Warteschlange lautet Application_X, wobei X für die Wiederholungsanzahl steht. Dieser Wert wird mit jedem fehlgeschlagenen Versuch um eins erhöht. 4. Wenn die Wiederholungsanzahl fünf überschreitet, wird die Nachricht in die Warteschlange Application_DeadQueue verschoben. Dort verbleibt die Nachricht, bis ein Administrator sie mit dem MSMQ-Explorer oder einem anderen Tool verschiebt. Beim letzten Versuch, bevor die Nachricht in die Warteschlange Application_ DeadQueue verschoben wird, wird die zugehörige Ausnahmeklasse mit dem fehlgeschlagenen warteschlangenfähigen COM+-Objekt abgerufen. Ist diese Ausnahmeklasse vorhanden, wird die IPlaybackControl-Schnittstelle abgerufen und die IPlaybackControll::FinalServerRetry-Methode aufgerufen. Anschließend werden die Methoden- und Eigenschaftszuweisungen wiedergegeben. Die Ausnahmeklasse ist für das Rückgängigmachen der Auswirkungen der Nachricht zuständig.
378
Asynchrone COM+-Dienste
Wenn dieser Vorgang gelingt, wird die Nachricht nicht in die Application_DeadQueue verschoben, sondern gelöscht. Sie erwarten möglicherweise, dass ein Hinweis zurückbleibt, wenn ein Fehler aufgetreten ist. Dies ist jedoch nicht erforderlich, da die Ausnahmeklasse alle vorgenommenen Operationen rückgängig macht. Wenn Sie dennoch eine Nachricht erhalten möchten, kann ein Ereignis in der Ausnahmeklasse protokolliert werden.
12.1.5 Erhalten einer Antwort Wie erhält die aufrufende Anwendung eine Antwort? Die Lösung besteht darin, das Befehlsmuster umgekehrt zu implementieren. Eine Möglichkeit hierzu ist, in der Implementierung des Befehlsmusters den Namen des Senders hinzuzufügen. Auf diese Weise enthält es die Adresse des Senders, falls der Aufrufer eine Antwort anfordert. Anschließend kann der Sender eine Nachricht erstellen, die an den Aufrufer gesendet wird. Dies ist eine unkomplizierte Lösung, die nur einen geringen Entwicklungsaufwand erfordert. Die beste Möglichkeit, dieses Problem anzugehen, ist, die einzelnen Computer nicht als Client und Server, sondern als gleichwertig zu betrachten. Eigentlich werden nur Nachrichten von einem Peer zu einem anderen gesendet.
12.2 COM+-Ereignisdienste COM+-Ereignisse sind ein anderer Typ der asynchronen Kommunikation. COM+Ereignisse implementieren einen abonnementartige asynchrone Kommunikation. Nehmen Sie folgendes Problem: Ein Benutzer hat eine Hypothek mit einem flexiblen Zinssatz aufgenommen und möchte diesen in einen feststehenden Zinssatz umwandeln, wenn er ein bestimmtes Maß übersteigt. Die klassische Möglichkeit, diese Funktionalität bereitzustellen, ist das Erstellen einer regelmäßig ausgeführten Anwendung, die den Zinssatz abfragt. Das Problem bei diesem Ansatz ist, dass selbst dann ein bestimmter Verarbeitungsaufwand erforderlich ist, wenn keine Änderung stattgefunden hat. Eine einfachere Lösung ist, die Zinssatzänderungen zu abonnieren und dann bei Bedarf zu reagieren. Diese Lösung erfordert eine geringere Bandbreite, funktioniert jedoch nur, wenn die entsprechende Infrastruktur bereitsteht. COM+-Ereignisse sind eine Implementierung des Verleger/Abonnent-Ereignismodells. In diesem Modell gibt es ein Ereignis, das durch eine COM-Schnittstelle beschrieben wird. Der Abonnent bekundet bei der Abonnementdatenbank für COM+-Ereignisse sein Interesse an den durch das Ereignis beschriebenen Informationen. Ein Verleger ist für das Erzeugen eines Ereignisses zuständig, bei dem die Ereignis-COM-Schnittstelle instanziiert wird. Zu diesem Zeitpunkt fängt der
COM+-Ereignisdienste
379
COM+-Ereignisdienst den Schnittstellenaufruf ab und überträgt alle Methodenaufrufe auf die einzelnen Abonnenten. COM+-Ereignisse lassen mehrere Abonnenten für dasselbe Ereignis zu. Weder der Verleger noch der Abonnent muss den jeweils anderen kennen. Dies wird als LCE (Loosely Coupled Events) bezeichnet. Die einzige Verbindung zwischen dem Verleger und dem Abonnenten ist die COM+-Ereignisschnittstelle. Abbildung 12.7 zeigt eine Implementierung von COM+-Ereignissen.
COM+-EREIGNIS Abonnent 1 Verleger
COM+-Ereignisse Abonnement Datenbank
Abonnent 1
Abbildung 12.7 COM+-Ereignisarchitektur
Es ist möglich, einen Dienst vom Typ COM+-Ereignis über COM-Verbindungspunkte zu erstellen. COM-Verbindungspunkte sind jedoch TCE (Tightly Coupled Events), d.h., der Abonnent und der Verleger kennen einander und sind voneinander abhängig. COM-Verbindungspunkte sind nicht zuverlässig und können keine Filterung durchführen.
12.2.1 Erstellen einer Verleger/Abonnent-Anwendung Erstellen Sie nun eine einfache COM+-Ereignisanwendung. Es gibt drei Codeteile, die implementiert werden müssen: die Ereignisklasse, ein Abonnent und ein Verleger. Die COM+-Ereignisanwendung basiert auf der Anwendung für die Konferenzanmeldung, die in den vorangegangenen Kapiteln bearbeitet wurde. Bei der derzeitigen Anwendung für die Konferenzanmeldung werden bei der Anmeldung eines Benutzers die entsprechenden Informationen nur in die Datenbank aufgenommen. Der Administrator der Anwendung für die Konferenzanmeldung hat keine Möglichkeit herauszufinden, welche Benutzer sich angemeldet haben. Um diese Situation zu verbessern, erstellen Sie ein COM+-Ereignis, das bei jeder Benutzeranmeldung ausgelöst wird.
380
Asynchrone COM+-Dienste
Definieren der Ereignisschnittstelle Bei der definierten Ereignisklasse handelt es sich nur um eine COM-Schnittstelle. Der COM+-Ereignisdienst verwendet diese COM-Schnittstelle als Verweis, wenn er die entsprechende Ghost-COM-Schnittstelle erstellt. Das bedeutet, dass jegliche von der COM-Schnittstelle implementierte Funktionalität ignoriert wird. Die einfachste Möglichkeit zur Implementierung dieser Schnittstelle besteht darin, Visual C++ zu verwenden und die COM-Schnittstelle mit Hilfe des ATL-Objekt-Assistenten zu definieren. Sie könnten auch Visual Basic oder Visual J++ einsetzen. Beachten Sie jedoch, dass bei Visual Basic oder Visual J++ möglicherweise keine richtig benannte Schnittstelle erzeugt wird, wie in Kapitel 8 erläutert wurde. Für die Beispielanwendung wird die Ereignisschnittstelle durch folgende IDL definiert: interface IVCEventRegistration : IDispatch { [id(1), helpstring("method newUser")] HRESULT newUser(long id); [id(2), helpstring("method updateUser")] HRESULT updateUser(long id); };
Nachdem die Ereignisbibliothek definiert wurde, muss die COM-Komponente kompiliert werden. Hinzufügen der Ereignisklasse zum COM+-Katalog Der nächste Schritt ist das Registrieren des COM-Objekts als Ereignisklasse. Gehen Sie hierzu folgendermaßen vor: 1. Starten Sie den Manager für die Komponentendienste. 2. Erstellen Sie hierzu eine neue COM+-Anwendung, oder verwenden Sie eine vorhandene. 3. Klicken Sie mit der rechten Maustaste auf den Ordnerknoten innerhalb des Kontexts einer COM+-Anwendung, und wählen Sie im Kontextmenü im Untermenü Neu den Befehl Komponente. 4. Das Dialogfeld Willkommen beim COM-Komponenteninstallations-Assistenten wird angezeigt. Klicken Sie auf Weiter. Das in Abbildung 12.8 dargestellte Dialogfeld wird angezeigt. 5. Klicken Sie auf Neue Ereignisklasse(n) installieren. Das Dialogfeld Öffnen wird angezeigt. 6. Wählen Sie die DLL (Dynamic Link Library) aus, die die Implementierung der Ereignisklasse (IVCEventRegistration) enthält. Klicken Sie auf OK. Das in Abbildung 12.9 dargestellte Dialogfeld wird mit den Informationen angezeigt, die in der COM-Komponente vorgefunden wurden.
COM+-Ereignisdienste
381
Abbildung 12.8 Ereignisklassenregistrierung
Abbildung 12.9 Anzeige der Ereignisklassenbeschreibung
7. Klicken Sie auf Weiter, und klicken Sie im letzten Dialogfeld auf Fertig stellen.
382
Asynchrone COM+-Dienste
Das soeben dem COM+-Katalog hinzugefügte COM-Objekt sieht wie jedes beliebige COM+-Objekt aus. Der Unterschied besteht darin, dass es als Ereignisklasse definiert wurde. Um dies zu überprüfen, klicken Sie mit der rechten Maustaste auf das COM-Objekt, wählen den Befehl Eigenschaften und klicken auf die Registerkarte Erweitert. Das Dialogfeld sollte etwa dem in Abbildung 12.10 dargestellten entsprechen.
Abbildung 12.10 Eigenschaften einer Klasse mit Anzeige des Ereignisstatus
Das Gruppenfeld LCE wird nur für COM+-Objekte angezeigt, die als Ereignisklasse definiert sind. Das Kontrollkästchen In-Process-Abonnenten zulassen sollte aktiviert sein, da COM+-Anwendungen In-Process COM-Objekte sind. Implementieren eines Abonnenten Zu diesem Zeitpunkt könnte ein Verleger mit dem Generieren von Ereignissen beginnen. Da es keine Abonnenten gibt, würden diese Ereignisse jedoch ins Nichts laufen. Es sollte also ein Abonnement implementiert werden. Der Abonnent ist ein weiteres COM+-Objekt, das entweder eine Methode der Ereignisklasse oder die gesamte Schnittstelle einer COM+-Ereignisschnittstelle implementiert.
COM+-Ereignisdienste
383
Im vorliegenden Beispiel wird die Ereignisschnittstelle als IVCEventRegistration bezeichnet. Da die Schritte zur Implementierung einer Schnittstelle bereits erläutert wurden, gehe ich im Folgenden davon aus, dass die COM-Schnittstelle implementiert wurde und dass die Implementierung in einer COM+-Anwendung registriert wurde. Um ein Abonnement mit dem COM+-Objekt zu verknüpfen, das im Manager für Komponentendienste implementiert wurde, öffnen Sie die COM+-Anwendung mit der Ereignisklasse IVCEventRegistration. Öffnen Sie anschließend die implementierte Ereignisklasse IVCEventRegistration. Dadurch werden die verschiedenen implementierten COM-Schnittstellen angezeigt, wie in Abbildung 12.11 dargestellt ist.
Abbildung 12.11 COM+-Objekt mit Ordner Abonnements
Das COM-Objekt prjVBEvents.clsEventRegSubscriber hat zwei COM-Schnittstellen implementiert: _clsEventRegSubscriber und IVCEventRegistration. Das Abonnement basiert auf der registrierten Ereignisklasse, zufälligerweise IVCEventRegistration. Gehen Sie folgendermaßen vor, um ein neues Abonnement zu erstellen: 1. Klicken Sie mit der rechten Maustaste auf den Ordner Abonnements, und wählen Sie Neu•Abonnement. Klicken Sie anschließend auf Weiter. 2. Der Assistent zum Erstellen von neuen COM-Abonnements zeigt alle Schnittstellen an. Wählen Sie, wie in Abbildung 12.12 gezeigt, die Schnittstelle aus, die ein Abonnement annimmt. 3. Sie können entweder eine ganze Schnittstelle oder auch eine bestimmte Methode einer Schnittstelle auswählen. Wählen Sie den IVCEventRegistrationNamen aus, sodass das Abonnement auf die gesamte Schnittstelle angewendet wird. Klicken Sie anschließend auf Weiter.
384
Asynchrone COM+-Dienste
4. Die ausgewählte Schnittstelle wird nun mit einer verfügbaren Ereignisklasse verknüpft; dieser Vorgang kann einige Zeit in Anspruch nehmen. Wenn er abgeschlossen ist, wird der in Abbildung 12.13 dargestellte Assistentenbildschirm angezeigt.
Abbildung 12.12 Auswählen eines Abonnements im Assistenten zum Erstellen von neuen COM-Abonnements
Abbildung 12.13 Auswählen einer Ereignisklasse im Assistenten zum Erstellen von neuen COM-Abonnements
COM+-Ereignisdienste
385
5. Wählen Sie die einzige Option aus, und klicken Sie anschließend auf Weiter. 6. Im letzten Bildschirm des Assistenten werden Sie aufgefordert, einen Namen für das Abonnement anzugeben. Geben Sie Erstes Abonnement ein, und aktivieren Sie das Kontrollkästchen Dieses Abonnement sofort aktivieren. Klicken Sie auf Weiter und schließlich auf Fertig stellen. Das Abonnement wird dem Ordner Abonnements hinzugefügt, und der Abonnent ist zur Annahme von Ereignissen bereit. Dies ist ein Beispiel für ein dauerhaftes Abonnement. Weitere Einzelheiten hierzu sind im weiteren Verlauf des Kapitels im Abschnitt »Definieren eines vorübergehenden Abonnenten« aufgeführt. Veröffentlichen von Informationen Jetzt muss ein Verleger erstellt werden. In diesem Fall wird ein Testprogramm zum Generieren eines neuen Benutzeranmeldungsereignisses verwendet. Der Verleger muss nur die COM+-Ereignisklasse (IVCEventRegistration) instanziieren und anschließend die Anmeldungsmethode (IVCEventRegistration::newUser) aufrufen. Ein Beispiel für eine entsprechende Visual Basic-Implementierung lautet folgendermaßen: Private Sub tstRegisterUser() Dim obj As New VCEventRegistration obj.newUser 13 End Sub
In diesem Code wird durch Instanziierung des COM-Objekts VCEventRegistration ein Beispielbenutzer erstellt, der mit der COM+-Ereignisklasse verknüpft wird. Anschließend wird die Methode newUser aufgerufen, und die Ereignisklasse erfasst diesen Aufruf. Die Ereignisklasse verbreitet den Methodenaufruf an die verschiedenen Abonnenten. Das bedeutet, dass der zuvor implementierte Beispielabonnent das Ereignis empfängt. Auslösen von Ereignissen Wenn der Verleger ein Ereignis erzeugt, gibt der COM+-Ereignisspeicher das Ereignis standardmäßig sequenziell weiter. Das heißt, die Abonnenten werden nacheinander über das Ereignis informiert. Die Auslösungsreihenfolge ist jedoch nicht vordefiniert, auch lässt sich kein Muster erkennen. Wenn bessere Leistung erforderlich ist, sollten Sie das Kontrollkästchen Parallel starten im Dialogfeld Eigenschaften des COM-Objekts aktivieren, das weiter oben in Abbildung 12.10 dargestellt wurde. Durch Aktivieren dieses Kontrollkästchens wird ein Multithreading-Ereignisübermittler instanziiert.
386
Asynchrone COM+-Dienste
Standardmäßig werden COM+-Ereignisse direkt ausgelöst, sobald der Verleger ein Ereignis auslöst. Es ist jedoch möglich, ein Ereignis unter Verwendung von in der Warteschlange befindlichen COM+-Komponenten auszulösen. Auf diese Weise wird ermöglicht, ein COM+-Ereignis auszulösen und es anschließend zu vergessen. Die in der Warteschlange befindliche COM+-Komponente kann zum asynchronen Veröffentlichen eines Ereignisses oder zum asynchronen Übermitteln eines Ereignisses verwendet werden. Abbildung 12.14 zeigt, wie dies definiert werden kann.
Verleger
Recorder Player
Com+-Ereignis
Recorder Player
Abonnent 1
Abbildung 12.14 COM+-Ereignisse mit in der Warteschlange befindlichen COM+-Komponenten
Beim Veröffentlichen von Ereignissen befinden sich Recorder und Player zwischen dem Verleger und dem COM+-Ereignisdienst. Praktisch wird dies implementiert, indem die Ereignisklasse als warteschlangenfähig definiert wird. Im aufgeführten Beispiel verweist der Verleger auf die COM-Schnittstelle IVCEventRegistration, die Bestandteil der Komponente VcIDLEvents.VCEventRegistration.1 ist. Die zweite Möglichkeit zum Hinzufügen der COM+ Queued Component-Funktionalität besteht darin, die Abonnentenereignisschnittstelle als warteschlangenfähig zu definieren, indem die Abonnementeigenschaften geändert werden. Wählen Sie im COM+-Explorer das Abonnement aus, das im vorherigen Beispiel erstellt wurde, klicken Sie mit der rechten Maustaste darauf, und wählen Sie Eigenschaften. Wählen Sie im Dialogfeld Eigenschaften die Registerkarte Optionen, die in Abbildung 12.15 dargestellt ist. Um dieses Abonnement in eine COM+ Queued Component-Nachricht umzuwandeln, aktivieren Sie das Kontrollkästchen Eingereiht. Auf diese Weise können
COM+-Ereignisdienste
387
COM+-Ereignisse remote ausgelöst und Nachrichten für mobile Benutzer gespeichert werden.
Abbildung 12.15 Abonnementeigenschaften
Durch Einstellen der Option Servername im Dialogfeld Eigenschaften (Abbildung 12.15) ist es möglich, ein COM+-Ereignis remote auszulösen. Dadurch wird der Abonnent aufgerufen, der sich an dem anderen Computer befindet. Dieser Ansatz funktioniert zwar, sollte jedoch besser nicht für Tausende von Benutzern eingesetzt werden.
12.2.2 Filtern von Ereignissen Bei COM+-Ereignissen ist es möglich, Ereignisse zu filtern, sodass der Abonnent nur die Informationen erhält, die für ihn von Interesse sind. Zwei Arten der Filterung sind zu unterscheiden: Die einfachste ist die Filterung auf Abonnentenseite. In diesem Fall definiert der Abonnent eine Filterzeichenfolge, die die Parameterwerte festlegt. Die kompliziertere Filteroption in Bezug auf die Programmierung wird als Filterung auf Verlegerseite bezeichnet. In diesem Szenario implementiert ein COM-Objekt eine bestimmte Schnittstelle, die aufgerufen wird, sobald ein Ereignis stattfindet. Durch den Filter auf Verlegerseite kann daraufhin festlegt werden, welchen Abonnenten dieses Ereignis angezeigt wird.
388
Asynchrone COM+-Dienste
Filterung auf Abonnentenseite Das Definieren eines Filters auf Abonnentenseite ist die einfachere der beiden Optionen, da er innerhalb des COM+-Explorers definiert werden kann. In Abbildung 12.15 sehen Sie auch ein Textfeld mit der Bezeichnung Filterkriterium. Die Filterzeichenfolge kann in diesem Textfeld definiert werden. Eine Filterzeichenfolge für die IVCEventRegistration-Schnittstelle könnte folgendermaßen lauten: id=10
Bei der Filterzeichenfolge handelt es sich um eine Reihe von Schlüsselwertpaaren, die als Vergleichszeichenfolge verwendet werden. Der Schlüssel ist der Name eines Parameters, der von dem Ereignis ausgelöst wird, und der Name des Parameters wird in der COM+-Ereignisklasse definiert. (In diesem Fall ist die Ereignisklasse die Schnittstelle IEventRegistration, wie in einem vorherigen Abschnitt dieses Kapitels bereits ausgeführt wurde.) Der Filter im oben genannten Beispiel legt daher fest, dass der id-Parameter für jedes ausgelöste Ereignis gleich 10 sein muss. Die Schlüsselwertpaare müssen nicht durch den Operator = (gleich) verbunden werden. Sie können die in der folgenden Tabelle genannten Operatoren verwenden. Operator
Beschreibung
=, ==
Gleich
!=, ~=,
Ungleich
&
Boolesches UND
|
Boolesches ODER
!,~
Boolesches NICHT
Verwenden Sie Klammern, um mehrere Schlüsselwertpaare miteinander zu kombinieren. Damit beispielsweise die id entweder gleich 10 oder gleich 20 ist, geben Sie Folgendes ein: (id=10) | (id=20)
Sie haben auch die Möglichkeit, Klammern zu verschachteln und viele verschiedene Kombinationen von Operatoren einzusetzen. Einzige Anforderung ist, dass der Schlüssel einem einfachen Datentyp entsprechen muss (numeric, string usw.). Es ist zum Beispiel nicht möglich, Objektdatentypen auszuwerten.
COM+-Ereignisdienste
389
Filterung auf Verlegerseite Die Filterung auf Verlegerseite findet statt, wenn das Ereignis von einem COMObjekt auf Verlegerseite vorverarbeitet wird, das die IMultiInterfacePublisherFilter-COM-Schnittstelle implementiert. Zum Zeitpunkt, als dieses Buch verfasst wurde, war es nicht möglich, diese Schnittstelle in Visual Basic zu implementieren, da die Visual Basic-OLE-Automatisierung (Object Linking and Embedding) eine der Methoden dieser Schnittstelle nicht unterstützt. Die Implementierungsdetails werden daher in Visual C++ durchgeführt. Bislang ist nicht einzuschätzen, ob Visual Basic in absehbarer Zukunft diese Möglichkeit bieten wird. In Abbildung 12.16 wird umrissen, was durch die Filterung auf Verlegerseite erreicht werden soll.
Abbildung 12.16 Architektur des COM+-Verlegerfilters
In dieser Abbildung interagiert der Verleger mit der IVCEventRegistration-COMSchnittstelle. Der COM+-Ereignisdienst übermittelt das Ereignis jedoch nicht direkt an die Abonnenten, sondern sendet es zunächst an den EventFilter. Der EventFilter erfüllt sowohl die Funktion, den Abonnentencache zu untersuchen als auch zu bestimmen, welche Abonnenten das Ereignis erhalten. Implementieren eines allgemeinen Filters Es gibt mehrere Möglichkeiten zur Implementierung der Funktionalität eines Filters auf Verlegerseite. Diese Möglichkeiten hängen davon ab, wie der Filter genau eingesetzt werden soll. Beginnen wir zunächst mit der allgemeinen Implementierung. Bei dieser Technik implementiert der Filter auf Verlegerseite nur IMultiInterfacePublisherFilter. Ruft der Verleger das Ereignis auf, so ruft der COM+-Ereignisdienst in diesem Fall die Methode IMultiInterfacePublisherFilter::Initialize auf. Der Verlegerfilter verwendet diese Methode zum Initialisieren des COM+-Objekts und zum Speichern der IMultiInterfaceEventControl-COM-Schnittstelle.
Betrachten Sie die IMultiInterfaceEventControl-Schnittstelle etwas genauer. Diese Schnittstelle bietet die Möglichkeit, die Filtereigenschaften der Ereignisklasse zu steuern. Beispielsweise können Sie IMultiInterfaceEventControl::SetPublisherFilter verwenden, um die Filterklasse des Verlegers programmatisch einzustellen. Wenn kein Verlegerfilter eingerichtet ist, können Sie ferner mit Hilfe von IMultiInterfaceEventControl::SetDefaultQuery eine Zeichenfolge ähnlich einem Abonnentenfilter festlegen. Es ist jedoch nicht sinnvoll, diese Art von Parametern einzustellen, während der Verlegerfilter den Filtervorgang durchläuft. Der für Sie wirklich interessante Methodenaufruf ist die IMultiInterfaceEventControl::GetSubscriptions-Methode, durch die alle derzeit verfügbaren Abonnements abgerufen werden, die mit dem Ereignis verknüpft sind. Wenn der Verleger ein Ereignis auslöst, wird das Filterereignis IMultiInterfacePublisherFilter::PrepareToFire aufgerufen. In der Implementierung dieses Ereignisses wird der Name der Methode des ausgelösten Ereignisses als Parameter weitergeleitet. Auf der Grundlage dieser Informationen kann der Filter bestimmen, welche Abonnements das Ereignis erhalten. Diese Ereignisimplementierung ist ein Beispiel für einen Durchgangsfilter. Das bedeutet, dass alle Abonnenten ein Ereignis erhalten. Die Implementierung lautet folgendermaßen: STDMETHODIMP CVCEventRegFiltImpl::PrepareToFire(REFIID iid, BSTR MethodName, IFiringControl * firingControl) { try { int error; IEventObjectCollectionPtr ptrEventCol; _com_util::CheckError( m_ptrEIC->GetSubscriptions( iid, MethodName, NULL, &error, &ptrEventCol));
In dieser Implementierung werden die verschiedenen Abonnements, die mit dem Ereignis verknüpft sind, über die Methode m_ptrEIC->GetSubscriptions geladen. Diese Methode gibt eine Auflistung von Objekten zurück, die jetzt durchlaufen werden soll, um die einzelnen Abonnements abzurufen. Hierzu wird eine neue Auflistung erstellt und über die IEnumEventObjectSchnittstelle (ptrEventCol->get_NewEnum) durchlaufen. Bei den einzelnen Elementen der Auflistung handelt es sich um COM-Schnittstellen des Typs IEventSubscription. Diese COM-Schnittstellen enthalten alle Einzelheiten in Bezug auf ein einzelnes Abonnement, wie den Abonnementnamen, die Ereignisklassen-ID, den Methodennamen und den Computernamen, der auf den Standort der Ereignisklasse hinweist. Anhand dieser Informationen ist es möglich zu ermitteln, ob der Abonnent das Ereignis erhalten soll. Diese Informationen können zudem in Verbindung mit einer externen Datenbank verwendet werden, um die Antwort zu verfassen. Wenn der Abonnent das Ereignis erhalten soll, wird die IFiringControl::FireSubscription-Methode aufgerufen. Das Abonnement, das ausgelöst werden soll, wird als Parameter angegeben. Implementieren eines auf Methoden basierenden Filters Der allgemeine Filter berücksichtigt nicht die Parameter der einzelnen Methoden. Wenn dies jedoch für die Entscheidung, welche Abonnenten das Ereignis erhalten, erforderlich ist, muss die Verlegerereignisklasse auch die COM-Schnittstelle der Ereignisklasse
392
Asynchrone COM+-Dienste
implementieren. Im vorliegenden Beispiel bedeutet dies, dass die Ereignisklasse die COM-Schnittstelle IVCEventRegistration implementieren muss. Während des Filtervorgangs wird die Ereignisklasse abgefragt, nachdem die IMultiInterfacePublisherFilter::PrepareToFire-Methode abgeschlossen wurde. Wie im Beispiel für den allgemeinen Filter wird ein einfacher Durchgangsfilter verwendet: STDMETHODIMP CVCEventRegFiltImpl::newUser(LONG id) { try { ULONG count; IEventSubscriptionPtr pes; m_ptrObjEvent->Reset(); while( m_ptrObjEvent->Next( 1, (IUnknown **)&pes, &count) == S_OK) { m_ptrFireCtrl->FireSubscription( pes); } } catch( _com_error err) { return err.Error(); } return S_OK; }
Ohne auf die Details einzugehen, können Sie erkennen, dass die Implementierung dieser Methode der Implementierung von IMultiInterfacePublisherFilter::PrepareToFire sehr ähnlich ist. In der vorherigen Methode wurden die Auflistung von Abonnementobjekten und die Auslösungssteuerung jedoch zur späteren Referenz in den Variablen m_ptrObjEvent und m_ptrFireCtrl gespeichert. In diesem Beispiel kann der Verlegerfilter den Wert des id-Parameters untersuchen und die Entscheidung treffen, welche Abonnements das Ereignis erhalten. In dieser Implementierung ruft der Verlegerfilter die FireSubscription-Methode zweimal für jedes Abonnement auf, d.h., dass das Ereignis zweimal aufgerufen wird. Zweimal daher, weil die Abonnements in den beiden Phasen des Filters ausgelöst wurden – dem allgemeinen Filter und dem Methodenfilter. Dies ist technisch gesehen zulässig; möglicherweise verletzt das zweimalige Aufrufen des Filters jedoch eine Geschäftsregel. Registrieren des Filters Nachdem Sie den Filter auf Verlegerseite implementiert haben, muss er registriert werden. Im COM+-Explorer gibt es jedoch keine Möglichkeit, einen Filter auf Verlegerseite zu definieren. Das Verfahren zum Registrieren eines Verlegerfilters besteht darin, den COM+-Explorer zu umgehen und die COM+-Verwaltungsobjekte stattdessen direkt zu programmieren. Durch
COM+-Ereignisdienste
393
Verwendung der COM+-Verwaltungsobjekte haben Sie die Möglichkeit, auf sämtliche Aspekte von COM+-Anwendungen Einfluss zu nehmen. Die COM+-Verwaltungsobjekte werden in einer Hierarchie angeordnet, d.h., dass im Stamm begonnen wird, danach die COM+-Anwendung und schließlich die Ereignisklasse gefunden wird. Von dieser Klasse aus wird der Verlegerfilter definiert. Die COM+-Verwaltungsobjekte werden durch Erstellen des COM-Objekts instanziiert, das von der PROG ID COMAdmin.COMAdminCatalog definiert wird. Eine Beispielimplementierung lautet folgendermaßen: Dim catalog As COMAdminCatalog Set catalog = CreateObject("COMAdmin.COMAdminCatalog")
Das zurückgegebene instanziierte Objekt stellt einen Stammknoten für alle COM+-Dienste und COM+-Anwendungen dar. Um die jeweilige COM+-Anwendung zu finden, die die Ereignisklasse enthält, ist die COM+-Anwendungsauflistung erforderlich. Diese kann folgendermaßen abgerufen werden: Set appcoll = catalog.GetCollection("Applications") appcoll.Populate
Beachten Sie in dieser Implementierung den Aufruf von appcoll.Populate. Diese Methode liest alle COM+-Anwendungsdaten aus dem COM+-Katalog ein. Nachdem diese Informationen verfügbar sind, muss die COM+-Anwendung mit der relevanten Ereignisklasse gefunden werden. Eine Beispielimplementierung lautet folgendermaßen: For i = 0 To appcoll.Count - 1 If appcoll.Item(i).Name = "Test Application" Then Set compcoll = appcoll.GetCollection("Components", appcoll.Item(i).Key) compcoll.Populate End If Next
Wenn die COM+-Anwendung gefunden ist, in diesem Beispiel Test Application, kann die Auflistung von Komponenten über die Methode appcoll.GetCollection abgerufen und über die Methode compcoll.Populate gefüllt werden. In der Komponentenauflistung werden die einzelnen Objekte über die PROG ID identifiziert. Da die Auflistung eine beliebige Anzahl von Komponenten enthalten kann (die größer ist als null), muss die Auflistung durchlaufen werden, um die Ereignisklasse
394
Asynchrone COM+-Dienste
zu finden, die in diesem Fall die PROG ID VcIDLEvents.VCEventRegistration.1 besitzt. Hierzu kann beispielsweise folgender Code verwendet werden: For i = 0 To compcoll.Count - 1 If compcoll.Item(i).Name = "VcIDLEvents.VCEventRegistration.1" Then Set comp = compcoll.Item(i) comp.Value("MultiInterfacePublisherFilterCLSID") = _ "VCEventFilter.VCEventRegFiltImpl.1" compcoll.SaveChanges End If Next
Nachdem die Ereignisklasse gefunden wurde, wird der Bezeichner der Ereignisklasse der PROG ID des Verlegerfilters zugeordnet, d.h. der Eigenschaft comp.Value. Es ist wichtig, die Änderungen zu speichern, die über die Methode compcoll.SaveChanges vorgenommen wurden. Nachdem diese Schritte abgeschlossen wurden, ist der Verlegerfilter vollständig und wird aufgerufen, sobald ein Verleger das Ereignis IVCEventRegistration aufruft.
12.2.3 Definieren eines vorübergehenden Abonnenten Es gibt zwei Arten von COM+-Abonnements: dauerhafte und vorübergehende. Von einem vorübergehenden Abonnement spricht man, wenn es sich beim Abonnenten um ein inaktives COM+-Objekt handelt; dieser Abonnementtyp wurde bislang erläutert. Bei einem dauerhaften Abonnement veröffentlicht ein Verleger Informationen, und das COM+-Ereignisobjekt wird instanziiert, wenn das COM+-Ereignis übermittelt wird. Im Gegensatz dazu handelt es sich um ein vorübergehendes Abonnement, wenn ein Abonnent derzeit im Kontext einer Anwendung ausgeführt wird. Jedes übermittelte Ereignis wird an den aktiven Abonnenten gesendet, das Abonnement gilt jedoch nur für den Zeitraum, in dem der Abonnent aktiv ist, oder für die Dauer der Computersitzung. Durch einen Neustart des Computers wird das Abonnement beendet. Um sicherzustellen, dass der Abonnent aktiv bleibt, fügt der COM+-Ereignisdienst dem COM-Objekt einen Verweiszähler hinzu. Ein vorübergehender Abonnent im Beispiel der Anwendung für eine Konferenzanmeldung müsste noch immer die IVCEventRegistration-Schnittstelle implementieren. Im Gegensatz zu einem dauerhaften Abonnement muss ein vorübergehendes Abonnement jedoch über die COM+-Verwaltungsobjekte hinzugefügt werden. Sie werden jetzt ein vorübergehendes Abonnement hinzufügen und anschließend entfernen.
COM+-Ereignisdienste
395
Hinzufügen eines vorübergehenden Abonnements Damit ein vorübergehendes Abonnement hinzugefügt wird, muss das COM+Verwaltungskatalogobjekt instanziiert werden, und die vorübergehenden Abonnements müssen abgerufen werden. Eine entsprechende Implementierung sieht folgendermaßen aus: Dim catalog As COMAdminCatalog Dim subColl As COMAdminCatalogCollection Set catalog = CreateObject("COMAdmin.COMAdminCatalog") Set subColl = catalog.GetCollection("TransientSubscriptions")
Das subColl-Objekt muss nicht gefüllt werden, da ein Abonnement der Auflistung hinzugefügt wird. Nach dieser Hinzufügung, müssen die verschiedenen Attribute des Abonnements definiert werden. Eine Beispielimplementierung lautet folgendermaßen: Set comp = subColl.Add comp.Value("EventCLSID") = "{BF72A012-3312-4C17-AA41-B8F98A243EBF}" comp.Value("Name") = "Sample Transient Subscription" comp.Value("SubscriberInterface") = objEvent comp.SaveChanges transId = com.Value("ID")
Es gibt drei Attribute, die definiert werden müssen. Das erste ist eine Definition, welche Ereignisklasse abonniert wird. In diesem Beispiel wird die Ereignisklasse durch die Eigenschaft EventCLSID definiert, in diesem Fall die CLSID der VCEventRegistration-COM-Klasse. Die Eigenschaft Name beschreibt den Namen des Abonnements; hierbei kann es sich um jede beliebige Zeichenfolge handeln. Die letzte Eigenschaft, SubscriberInterface, wird durch das derzeit aktive Objekt definiert, das als Abonnent fungieren soll. Nachdem diese Eigenschaften festgelegt wurden, müssen sie mit Hilfe der SaveChanges-Methode gespeichert werden. Damit das vorübergehende Abonnement leichter gefunden und entfernt werden kann, kann die Eigenschaft ID abgerufen und gespeichert werden. Diese Eigenschaft definiert die ID des vorübergehenden Abonnements und beschreibt das vorübergehende Abonnement eindeutig, welches der Auflistung hinzugefügt wurde. Entfernen eines vorübergehenden Abonnements Ein vorübergehendes Abonnement kann aus der Auflistung vorübergehender Abonnements entfernt werden, indem die Auflistung durchlaufen und das zu lö-
396
Asynchrone COM+-Dienste
schende Abonnement gefunden wird. In diesem Fall wurde die Abonnement-ID gespeichert. Sie kann daher als Schlüssel verwendet werden, um zu ermitteln, welches Abonnement gelöscht werden soll. Zum Löschen des Abonnements muss das COM+-Katalogobjekt instanziiert und die Auflistung vorübergehender Abonnements abgerufen werden. Da die einzelnen Elemente durchlaufen werden, muss die Populate-Methode aufgerufen werden, um die Auflistung zu füllen. Hierzu könnte folgender Quellcode eingesetzt werden: Set catalog = CreateObject("COMAdmin.COMAdminCatalog") Set subColl = catalog.GetCollection("TransientSubscriptions") subColl.Populate
Nun ist es möglich, die einzelnen Abonnementelemente zu durchlaufen und das Abonnement mit der ID zu löschen, die beim Hinzufügen des vorübergehenden Abonnements erstellt wurde. Die entsprechende Implementierung sieht folgendermaßen aus: For i = 0 To subColl.Count - 1 If subColl.Item(i).Value("ID") = transId Then subColl.Remove (i) subColl.SaveChanges End If Next
Um das Abonnement zu entfernen, wird die Remove-Methode mit dem Indexstandort als Parameter aufgerufen. Um die Änderungen dauerhaft vorzunehmen, wird die SaveChanges-Methode aufgerufen.
12.2.4 Transaktionen und COM+-Ereignisse Beim Schreiben von COM+-Ereignissen werden Transaktionen automatisch integriert. Es ist möglich, die Transaktionsattribute für die Ereignisklasse oder für einzelne Abonnenten festzulegen. Sie haben die Wahl. Bedenken Sie jedoch, dass alle Abonnenten möglicherweise in dieselbe Transaktion einbezogen werden, wenn Sie das Transaktionsattribut für die Ereignisklasse einstellen.
12.3 Resümee In diesem Kapitel wurde das Konzept der asynchronen Kommunikation eingeführt. Ich betrachte dieses Verfahren jedoch weniger als asynchrone Kommunikation, vielmehr als eine Art von Pushtechnologie. Dies bedeutet, dass der Konsument der Daten zu einem bestimmten Zeitpunkt Daten erwartet. Bei anderen
Resümee
397
Modellen ist Aktivität gefordert, und es müssen Objekte aufgerufen werden, um Daten zu verschieben. Der Unterschied zwischen den beiden in diesem Kapitel beschriebenen Modellen besteht darin, dass bei COM+ Queued Components die »gepushten« Daten automatisch empfangen werden. Bei COM+-Ereignissen erhalten Sie die Pushdaten nur, wenn Sie sie abonniert haben. Das Schreiben von Anwendungen für ein Pushmodell unterscheidet sich vom Schreiben einer normalen Anwendung, da man ein passiver Teilnehmer im Programmiermodell ist. Dies ist jedoch kein Problem, da das Windows-Programmiermodell auf Ereignissen basiert und es erfordert, dass auch die Anwendung sich passiv verhält. Der Unterschied beim serverseitigen Pushmodell liegt darin, dass man nicht an eine Ereignisarchitektur ohne grafische Benutzeroberfläche gewöhnt ist. Nachdem Sie dieses und das vorhergehende Kapitel gelesen haben, besitzen Sie eine klare Vorstellung, wie stabile Messaginganwendungen geschrieben werden. Wenn Sie mit dem Schreiben von Messaginganwendungen nicht viel Erfahrung besitzen, beginnen Sie mit einer kleinen Anwendung, um ein Gefühl für die einzelnen Vorgänge zu erhalten. Danach können Sie die Größe und Qualität Ihrer Messaginganwendungen fortlaufend steigern.
398
Asynchrone COM+-Dienste
13 Weitere Informationen zur COM+Dienstprogrammierung Dieses Kapitel behandelt die Steuerung bestimmter Aspekte von COM+Objekten (Component Object Model). Diese Funktionen werden nicht bei allen COM+-Objekten benötigt, in manchen Fällen sind jedoch zusätzliche Steuerungsmöglichkeiten wünschenswert. Der erste Teil des Kapitels befasst sich mit der Optimierung von COM+-Transaktionen. Im zweiten Teil liegt der Schwerpunkt auf der Integration eines COM+-Objekts in einer Active Server Page (ASP).
13.1 Skalierbarkeit und Verwaltung der COM+-Lebensdauer Bei der Entwicklung von COM+-Anwendungen für umfangreiche Umgebungen mit Tausenden von gleichzeitig arbeitenden Benutzern ist die Skalierbarkeit ein wichtiges Thema. Skalierbarkeit wurde unter Windows 2000 durch Hinzufügen verschiedener Dienste erreicht, beispielsweise effektiveres Caching und dynamischer Lastenausgleich. Wenn also eine COM+-Anwendung zu langsam ist, bietet es sich an, sie manuell zu optimieren.
13.1.1 Implementieren des Objektpoolings Das Objektpooling wird als Lösung für langsame Objektinstanziierung oder für die Verteilung von Legacyressourcen verwendet. Langsame Objektinstanziierung kann auftreten, wenn das COM+-Objekt für das Herstellen einer Verbindung mit einer bestimmten Ressource eine lange Zeit in Anspruch nimmt. Dies kann bei einer Legacyressource der Fall sein, die nicht für die Verwendung von COM+-Transaktionsdiensten optimiert ist. Außerdem besteht diese Möglichkeit, wenn das COM+-Objekt viele verschiedene Schritte ausführen muss, bevor es zum Ausführen von Anwendungslogik verwendet werden kann. In jedem dieser Fälle empfiehlt sich der Einsatz des COM+-Objektpoolings. Was versteht man unter Objektpooling? Zu Zeiten von MTS (Microsoft Transaction Server) stand das Objektpooling kurz vor seiner Einführung. Diese Funktion ermöglicht es, eine Instanz eines Objekts zu einem späteren Zeitpunkt wieder zu verwenden. Auf diese Weise wird die Leistung wesentlich verbessert, da das Objekt nicht erneut instanziiert werden muss. Das Objektpooling wird in COM+ implementiert, sein Zweck hat sich jedoch geändert. Nun besteht das Ziel nicht mehr in der allgemeinen Leistungsverbesserung, sondern in der Steigerung der Leistung von langsam instanziierenden Kom-
Skalierbarkeit und Verwaltung der COM+-Lebensdauer
399
ponenten. In Kapitel 10 wurde erläutert, dass das COM+-Objekt nicht wieder verwendet wird; die Informationen werden an irgendeinem Ort zwischengespeichert und von der Ressource und der COM+-Laufzeitumgebung verwaltet. Das System wird auf diese Weise nicht langsamer oder weniger stabil. Wenn das Caching jedoch umgebungsbedingt oder aufgrund unzulänglich geschriebener Ressourcen nicht optimal ist, wird das Implementieren des Objektpoolings erforderlich. Die einfachste Möglichkeit festzustellen, wann das Objektpooling eingesetzt werden soll, besteht darin, den Zeitaufwand zu erwägen, der für die Arbeit auf Systemebene im Vergleich zur Anwendungsebene beansprucht wird. Als Arbeit auf Systemebene gelten beispielsweise das Verbinden mit einer Ressource, das Herstellen einer Netzwerkverbindung usw. Ein Objekt sollte in den Pool aufgenommen werden, wenn der Zeitaufwand für die Arbeit auf Systemebene größer ist als der für die Arbeit auf Anwendungsebene. Durch ein im Pool enthaltenes Objekt kann die Instanziierungszeit herabgesetzt werden, indem es zwischengespeichert wird oder indem die Arbeit im Voraus geleistet wird. Anforderungen für das Pooling von Objekten Die Verwendung von Objektpoolingcode ist etwas komplizierter als die Verwendung von COM+-Objekten, da Sie Code der Systemebene schreiben müssen. Dieser kann entweder in Visual C++ oder in Visual J++ verfasst werden, nicht jedoch in Visual Basic aufgrund der Implementierungsweise von COM-Objekten in Visual Basic. Je nach Ressource kann das Schreiben dieses Code entweder sehr komplex oder einfach sein. Im Pool befindliche Objekte stellen folgende Anforderungen: 왘 Statuslos: Im Pool befindliche Objekte müssen statuslos sein. Sie können kei-
nen Status weitergeben, der von einer anderen Transaktion festgelegt wird. 왘 Keine Threadaffinität: Im Pool befindliche Objekte können nicht an einen be-
stimmten Thread gebunden sein. Dies bedeutet, dass lokale Threadspeicherung nicht verwendet werden kann. Ein im Pool befindliches Objekt sollte den Thread, von dem es ausgeführt wird, nicht verwalten; dies bedeutet aber nicht, dass es nicht andere im Pool befindliche Threads verwalten kann. 왘 Aggregierbar: Das im Pool befindliche Objekt muss COM-aggregierbar sein.
Wenn Sie ATL (Active Template Library) verwenden, muss die Option Aggregation aktiviert sein. 왘 Transaktional: Das im Pool befindliche Objekt muss Transaktionen unterstüt-
zen. Es besitzt die Option, an der Transaktion teilzunehmen oder nicht. Ein im
400
Weitere Informationen zur COM+-Dienstprogrammierung
Pool befindliches Objekt muss einen Transaktionskontext mit einer Ressource verknüpfen. 왘 Implementierung von IObjectControl: Im Pool befindliche Objekte sollten
diese COM-Schnittstelle implementieren, damit die Steuerung des Objekts wesentlich vereinfacht wird. Definieren der Objektpoolingschnittstelle Die Implementierung der Objektpoolingschnittstelle ist leicht. Auf Wunsch übernimmt der ATL-Assistent diese Aufgabe, wenn ein MTS-COM-Objekt erstellt wird. Die IObjectControl-Schnittstelle ist folgendermaßen definiert: interface IObjectControl : IUnknown { HRESULT Activate( void); HRESULT Deactivate( void); HRESULT CanBePooled( void); };
In dieser Schnittstelle sind drei Methoden enthalten. Die ersten beiden, Activate und Deactivate, legen zusammen die Lebensdauer des im Pool befindlichen Objekts fest. Betrachten Sie das Diagramm zur Lebensdauer des Objekts in Abbildung 13.1. Es gibt eine physische und eine transaktionale Lebensdauer, hinzu kommt, dass ein im Pool befindliches Objekt häufig von physisch zu transaktional und wieder zurück wechselt. Erstellungsroutine
Basisspeicher
Zerstörungsroutine
IOObjectControl::Activate
Initialisiert
Aktiviert
Methodenaufruf
IObjectControl::Deactivate
Abbildung 13.1 Lebensdauer von im Pool befindlichen Objekten
Die physische Lebensdauer beginnt, wenn das COM+-Objekt physisch instanziiert wird, d.h., wenn die Erstellungsroutine des C++-Objekts aufgerufen wird. Ein Objekt in einem physischen Status befindet sich nicht im Kontext einer Transaktion oder einer COM+-Anwendung. Stattdessen befindet es sich im Arbeitsspeicher und verrichtet keine speziellen Aufgaben. Wenn das COM-Objekt zerstört und aus dem Arbeitsspeicher entfernt wird, endet die physische Lebensdauer.
Skalierbarkeit und Verwaltung der COM+-Lebensdauer
401
Möchte ein Konsument ein COM+-Objekt verwenden, das sich derzeit in einem physischen Status befindet, wechselt COM+ die Lebensdauer in transaktional, und der Status des Objekts ändert sich in aktiv. Zu diesem Zweck wird die IObjectControl::Activate-Methode aufgerufen. Dadurch wird das COM+-Objekt aufgefordert, sich auf eine baldige Konsumierung vorzubereiten. Nach dem Aufrufen der Methode führt das COM+-Objekt eine bestimmte Art der Anwendungslogik durch. Die transaktionale Lebensdauer endet, sobald der Konsument die Transaktion abgeschlossen hat. Der aktive Status des COM+-Objekts wird durch Aufruf von IObjectControl::Deactivate beendet und bereinigt. Das COM+-Objekt befindet sich nun wieder in einem physischen Status. Es wird als im Pool befindliches Objekt bezeichnet, da es ständig aus einem physischen in einen aktiven Zustand und zurück wechselt. Die CanBePooled-Methode gibt der COM+-Laufzeitumgebung an, ob das Objekt auf diese Weise verwendet werden kann. Da das Objekt in den Pool aufgenommen werden soll, muss die Methode TRUE zurückgeben. Gäbe sie FALSE aus, könnte für das Objekt kein Pooling ausgeführt werden, die Methoden IObjectControl::Activate und IObjectControl::Deactivate würden jedoch nach wie vor aufgerufen. Funktionsweise des Objektpoolings Beim Objektpool handelt es sich um einen Pool aus aktiven COM+-Objekten. Es gibt eine Mindest- und eine Höchstanzahl von im Pool befindlichen Objekten. Die Mindestanzahl gibt die Anzahl aktiver COM+-Objekte an, die im Pool enthalten sein müssen, die Höchstanzahl gibt die maximale Zahl aktiver COM+-Objekte an, die sich im Pool befinden können. Stellen Sie sich vor, ein Konsument möchte ein im Pool befindliches COM+-Objekt instanziieren. Das Abfangsystem untersucht den Pool und ermittelt, ob ein Objekt verfügbar ist. Wenn keines vorhanden ist, und die maximale Anzahl von Poolobjekten noch nicht erreicht ist, wird ein Objekt instanziiert. Ist die Höchstzahl erreicht, wird eine Warteschlange wartender Objekte erstellt. Bei dieser Warteschlange handelt es sich um einen FIFO-Puffer (First In, First Out). Übersteigt eine Anforderung den von der COM+-Anwendung festgelegten Zeitüberschreitungswert, wird ein Fehler zurückgegeben. Einige Techniken des Objektpoolings Bei der Verwendung von im Pool befindlichen Objekten sind einige wichtige Aspekte zu beachten. Das im Pool befindliche Objekt kann eine sehr lange Zeit aktiv sein, möglicherweise sogar Jahre. Daher kann ein kleines Speicherleck zu einem Speicherverlust führen, sodass das System neu gestartet werden muss. Wenn die transaktionale Lebensdauer beendet ist, muss das Objekt bereinigt und in den Objektstatus zurückgeführt werden, in dem es sich vor Beginn der transaktionalen Lebensdauer befand. Objektspei-
402
Weitere Informationen zur COM+-Dienstprogrammierung
cherlecks entstehen beispielsweise dadurch, dass Schnittstellenzeiger nicht freigegeben werden, Speicher nicht gelöscht wird usw. Ein im Pool befindliches Objekt muss außerdem den Transaktionskontext mit der Ressource verbinden, sodass es die automatische Transaktionsanmeldung nicht verwenden kann. Angegeben wird dies, indem das transaktionale COM+-Attribut auf disabled eingestellt wird. Dies bedeutet, dass das im Pool befindliche Objekt noch immer Teil des Transaktionsstreams ist, die Methode jedoch entscheidet, ob die Ressource herangezogen wird oder nicht. Eine einfache Implementierung Nehmen Sie eine einfache Situation. Sie verwenden einen ODBC-Treiber (Open Database Connectivity), der DTC-Transaktionen (Distributed Transaction Coordinator), jedoch kein Verbindungspooling unterstützt. Dabei nimmt die Herstellung einer Verbindung viel Zeit in Anspruch. Wenn Sie daher ein im Pool befindliches Objekt erstellen, muss dieses Objekt die Verbindung und die Ressource verwalten. Beim Schreiben von Code auf Systemebene oder von Code, der sich häufig wiederholt, ist es von Vorteil, diesen Code in ein Helferobjekt umzuwandeln. Helferobjekte abstrahieren komplexe oder sich wiederholende Aufgaben in Objekte, deren Verwendung nur ein paar Zeilen erfordert. Helferobjekte wurden bereits im Zusammenhang mit im Pool befindlichen Objekten erwähnt. Die Aufgabe des im Pool befindlichen Objekts besteht darin, durch Objektpooling einen langsamen Vorgang zu beschleunigen. Nach genauerer Betrachtung werden Sie jedoch feststellen, dass von Code auf Systemebene die Rede ist. Code auf Systemebene steht mit der allgemeinen Anwendung nur insofern in Verbindung, als dass die Effizienz der Programmierung oder die Ausführungszeit optimiert wird. Helferobjekte sollten immer in diesem Zusammenhang verwendet werden. Diese Objekte reduzieren den Umfang des Programmiercode oder steigern die Effizienz der Anwendung. Es ist wichtig, Helferobjekte als separate Objekte zu verwalten, da sie in späteren Versionen der Anwendung ersetzt werden könnten. Ziehen Sie beispielsweise in Betracht, ein im Pool befindliches Objekt zu verwenden, um die Effizienz der Anwendung zu erhöhen. In einer neuen Version der Ressource gibt es möglicherweise einen optimierten COM+-Ressourcenverteiler, der das im Pool befindliche Objekt überflüssig macht. Wenn der Code des im Pool befindlichen Objekts über den gesamten Anwendungscode verteilt ist, wird das Vornehmen von Änderungen an der Anwendung erschwert. Da der Code jedoch in einem Helferobjekt zentralisiert ist, verläuft die Änderung relativ reibungslos.
Skalierbarkeit und Verwaltung der COM+-Lebensdauer
403
Erstellen eines physischen Status Bei der Implementierung eines im Pool befindlichen Objekts liegt die Hauptaufgabe in der Optimierung des Ressourcenerwerbs. Da Sie zum Implementieren des COM+-Objekts Visual C++ verwenden, werden drei Erstellungsroutinen ausgeführt. Bei der Instanziierung eines COM+-Objekts wird als Erstes die Erstellungsroutine für C++-Klassen aufgerufen. Diese Erstellungsroutine instanziiert nur private Klassenmitglieder, die mit COM nicht in Zusammenhang stehen. Der Grund hierfür liegt darin, dass die COM- und COM+-Dienste noch nicht verfügbar sind. Da Visual C++ und ATL verwendet werden, gibt es eine COM-Erstellungsroutine namens FinalConstruct. Wenn das COM-Objekt diese Methode implementiert, wird sie von der ATL-Bibliothek aufgerufen. Zu diesem Zeitpunkt ist das Aufrufen von COM möglich. Beispielsweise könnten Sie diese Methode zum Instanziieren weiterer COM-Objekte verwenden. Nach dem Aufrufen dieser Methode wird das COM+-Objekt als physisch aktiv angesehen. In dieser Implementierung von im Pool befindlichen Objekten wird die FinalConstruct-Methode zum Initialisieren der Datenbankverbindungen verwendet. Eine Beispielimplementierung lautet folgendermaßen: HRESULT FinalConstruct() { RETCODE rc; HRESULT hr= S_OK; rc = SQLAllocEnv(phenv); ASSERT_SQL_SUCCESS(rc); if (bUseObjectPool) { rc = SQLSetEnvAttr(*phenv, SQL_ATTR_CONNECTION_POOLING, (void*)SQL_CP_OFF, 0); ASSERT_SQL_SUCCESS(rc); } rc = SQLAllocConnect(*phenv, phdbc); … }
Die Datenbank, mit der eine Verbindung hergestellt werden soll, verfügt über einen ODBC-Treiber. In diesem Schritt werden daher die ODBC-Umgebung und das ODBC-Handle zugeordnet. Während der Initialisierungsphase empfiehlt es sich, keine der Variablen als statisch oder global zu speichern. Je nach Situation könnte hierzu das Schreiben von Synchronisierungscode erforderlich sein. Auf diese Weise entsteht ein Engpass, der das im Pool befindliche Objekt verlangsamt.
404
Weitere Informationen zur COM+-Dienstprogrammierung
Bei der Zerstörung eines Objekts wird die FinalRelease-Methode aufgerufen, wenn das Objekt vom aktiven in einen physischen Status wechselt. Bei dem im Pool befindlichen Beispielobjekt löscht diese Methode die ODBC-Umgebung und die ODBC-Verbindungen, die durch die FinalConstruct-Methode erstellt werden. Eine Beispielimplementierung lautet folgendermaßen: void FinalRelease() { if (m_hstmt) SQLFreeStmt(m_hstmt, SQL_DROP); if (m_hdbc) { SQLDisconnect(m_hdbc); SQLFreeConnect(m_hdbc); } … }
Anmelden der Transaktion Wenn ein im Pool befindliches Objekt im Kontext einer Transaktion verwendet wird, wechselt es von einem physischen in einen aktiven Status. Zu diesem Zeitpunkt muss das im Pool befindliche Objekt der Ressource mitteilen, dass eine Transaktion gestartet wird und dass sie daran teilnehmen soll. Das im Pool befindliche Objekt muss sich also manuell bei der Transaktion anmelden. Bei der Implementierung des im Pool befindlichen Objekts wird die Verbindung mit der Ressource über ODBC hergestellt. ODBC verfügt über die Möglichkeit, eine COM+-Transaktion mit der darunter liegenden Ressource zu verknüpfen, da ODBC als offizieller COM+-Ressourcenverteiler fungiert. Diese Transaktionsanmeldung ist jedoch nur dann nützlich, wenn die darunter liegende Ressource ODBC-Transaktionen unterstützt. Standardmäßig verwendet ODBC die automatische Transaktionsanmeldung, die im vorliegenden Beispiel deaktiviert werden muss. Eine Transaktion muss manuell mit der DTC-Transaktion verknüpft werden. Die Option der automatischen Transaktionsverknüpfung kann über die Methode SQLSetEnvAttr deaktiviert werden. Wenn das im Pool befindliche Objekt von einem physischen in einen aktiven Status wechselt, wird die IObjectControl::Activate-Methode aufgerufen. Der angegebene COM+-Kontext besitzt einen gültigen Transaktionskontext. Das im Pool befindliche Objekt ist für das Anmelden der Transaktion bei der Ressource zuständig. Der einzige Fall, in dem der Transaktionskontext nicht gültig ist, liegt vor, wenn das COM+-Objektattribut auf nicht unterstützt eingestellt ist.
Skalierbarkeit und Verwaltung der COM+-Lebensdauer
405
Eine Beispielimplementierung für die Anmeldung einer Transaktion lautet folgendermaßen: IObjectContextInfo
In einem traditionellen MTS COM-Objekt (Microsoft Transaction Server, Vorgänger der COM+-Transaktionsdienste) wird der Transaktionskontext über den Methodenaufruf GetObjectContext abgerufen. Bei COM+ ist es jedoch möglich, die CoGetObjectContext-Funktino aufzurufen. Durch diesen Funktionsaufruf werden folgende Schnittstellen abgerufen: IObjectContext, IObjectContextInfo, IObjectContextActivity und IContextState. Diese Schnittstellen werden zu einem späteren Zeitpunkt erläutert, im Moment sind sie noch nicht von Bedeutung. Wenn der pObjTx-Schnittstellenzeiger ungleich NULL ist, wird die DTC-Transaktionsschnittstelle ITransaction (pTx) geladen. Ist pTx ungleich NULL, wird es als Parameter an die ODBC-Funktion SQLSetConnectOption weitergegeben. Die ODBC-Funktion meldet anschließend die Ressource an, die durch die angegebene Transaktion bearbeitet wird. Nach Beenden der Transaktion wird die IObjectControl::Deactivate-Methode aufgerufen. Das im Pool befindliche Objekt wechselt nun von einem aktiven in einen physischen Status. Das im Pool befindliche Objekt ist für das Zurücksetzen der Datenbankverbindung zuständig. Da ODBC mit Transaktionsunterstützung verwendet wird, sind keine besonderen Schritte erforderlich. Der ODBC-Ressourcen-Manager verwaltet alle Einzelheiten automatisch. Allerdings müssen Sie auf Konsistenz mit den COM-Schnittstellenzeigern und dem Speicher achten. Wenn durch die FinalConstruct-Methode beispielsweise Speicher zugeordnet wurde, muss er über die FinalRelease-Methode wieder freigegeben werden. Wurde ein COM-Schnittstellenzeiger über die IObjectControl::Activate-Methode instanziiert und als Variable auf Klassenebene gespeichert,
406
Weitere Informationen zur COM+-Dienstprogrammierung
muss der entsprechende Verweis durch die IObjectControl::Deactivate-Methode wieder aufgehoben werden.
13.1.2 Dynamischer Lastenausgleich Eine Alternative zum Durchsetzen der Skalierbarkeit besteht im Lastenausgleich. Der Lastenausgleich ermöglicht das Verwalten einer Computergruppe als einzelnen Kommunikationspunkt. Hierbei handelt es sich nicht um Clustering, da das Clustering Failover gewährleistet. (Von Failover spricht man, wenn Server, Speicher und andere Komponenten auf verschiedenen Computern repliziert werden.) Clustering geht über den Rahmen dieses Buches hinaus. Beim Lastenausgleich gehen bei Ausfall eines Computers alle derzeit ausgeführten Transaktionen verloren. Es ist jedoch möglich, sofort wieder von vorn zu beginnen, da ein anderer Computer automatisch die zusätzliche Last übernimmt. Beim Failover gehen die Transaktionen bei Ausfall eines Computers nicht verloren, da ein anderer Computer über ein Spiegelbild des Status des ausgefallenen verfügt und die Transaktion automatisch fortsetzt. Die Systeme sind qualitativ gleichwertig, die Auswahl hängt von Ihren Anforderungen ab. Das Implementieren des Lastenausgleichs ist ein Verwaltungsaspekt, da keine besonderen Programmiertechniken gefordert werden. Wenn Sie jedoch Anwendungen schreiben, ist es wichtig, die Auswirkungen des Lastenausgleichs nachzuvollziehen. Stellen Sie sich eine Situation vor, in der ein Client zwei Server als einen einzigen Computer betrachtet. Das hat zur Folge, dass das Betriebssystem einen Lastenausgleich zwischen diesen beiden Computern vornimmt. Nehmen Sie nun an, ein Client greift über einen Webbrowser auf eine Anwendung zur Rechnungsstellung auf dem Server zu. Wenn der Client eine Anforderung ausgibt, leitet die Lastenausgleichsfunktion diese Anforderung an einen Server weiter, und der Client stellt einen bestimmten Status auf dem Server her. Während dessen geben andere Clients weitere Anforderungen aus. Der erste Client stellt eine andere Anforderung, und die Lastenausgleichsfunktion legt fest, dass die Anforderung an den anderen Server weitergeleitet werden sollte. Dadurch entsteht ein Problem. Der ursprüngliche Status, der zum Ausführen der Aufgabe erforderlich ist, befindet sich auf dem anderen Server. Die Anwendung setzt einen bestimmten Status voraus, und da dieser nicht vorhanden ist, stürzt die Anwendung auf der Serverseite ab. Es wurde kein Fehler begangen, das Computerprogramm war jedoch für dieses Problem nicht gerüstet. Wie sollte das Programm also vorgehen? Die Antwort liegt darin, dass das Programm den Status in einer Datenbank verwalten oder die Lastenausgleichsfunktion zwingen sollte, Benutzer anstelle von Objekten zwi-
Skalierbarkeit und Verwaltung der COM+-Lebensdauer
407
schen den Computern aufzuteilen. Der Lastenausgleich wurde als Dienst aus dem Windows 2000-Betriebssystem entfernt und einer neuen Anwendung namens Application Server hinzugefügt. Bei Verfassen dieses Buches waren zu wenige Einzelheiten über Application Server bekannt, sodass diese Anwendung an dieser Stelle nicht behandelt wird.
13.2 Weitere Schnittstellen zur Transaktionsverwaltung Die neuen COM-Schnittstellen, IObjectContextInfo und IContextState, verbessern die Möglichkeit zur Interaktion mit den COM+-Transaktionsdiensten.
13.2.1 IContextState Wenn IObjectContext::SetComplete, IObjectContext::SetAbort, IObjectContext::DisableCommit oder IObjectContext::EnableCommit aufgerufen wird, werden die Consistent- und Done-Bits vertauscht. (Diese beiden Bits wurden in Kapitel 10 erläutert.) Mit Hilfe der IContextState-Schnittstelle ist es möglich, jedes dieser Bits manuell zu steuern. Die Schnittstelle wird folgendermaßen definiert: interface IContextState : IUnknown { HRESULT _stdcall SetDeactivateOnReturn(VARIANT_BOOL bDeactivate); HRESULT _stdcall GetDeactivateOnReturn([out] VARIANT_BOOL* pbDeactivate); HRESULT _stdcall SetMyTransactionVote(tagTransactionVote txVote); HRESULT _stdcall GetMyTransactionVote([out] tagTransactionVote* ptxVote); };
Durch die Deactivate-Methoden wird der Abschluss, durch die Transaction-Methoden die Konsistenz der Transaktion definiert. Warum sollten diese Methoden verwendet werden? Wäre es nicht einfacher, die Methoden von IObjectContext zu verwenden? Dies ist im Allgemeinen richtig, die IObjectContext-Methoden stellen jedoch sowohl das Consistent- als auch das Done-Bit gleichzeitig ein. Es ist nicht möglich, die Bits einzeln festzulegen. Ebenso wenig ist es möglich festzustellen, welche Bits eingestellt sind. Wenn Sie die Transaktion manuell bearbeiten, können Sie mit Hilfe dieser Schnittstelle das Ergebnis der Transaktion optimieren.
13.2.2 IObjectContextInfo Wie im Beispiel der im Pool befindlichen Objekte bereits gezeigt wurde, ist es manchmal erforderlich, die zugrunde liegende DTC-Transaktion zu bearbeiten. Die COM-Schnittstelle IObjectContextInfo ruft Informationen zum Kontext ab, der mit dem COM+-Objekt verknüpft ist. Sie können diese Transaktion anfordern,
408
Weitere Informationen zur COM+-Dienstprogrammierung
wichtiger ist jedoch, dass Sie herausfinden können, ob Sie sich in einer Transaktion befinden und welche IDs mit der Transaktion, der Aktivität und dem Kontext verknüpft sind. Diese Schnittstelle dient nur zur Information. In IDL (Interface Definition Language) wird die Schnittstelle folgendermaßen definiert: interface IObjectContextInfo : IUnknown { long IsInTransaction(); HRESULT GetTransaction(IUnknown** pptrans); HRESULT GetTransactionId([out] GUID* pGUID); HRESULT GetActivityId([out] GUID* pGUID); HRESULT GetContextId([out] GUID* pGUID); };
Abrufen von IObjectContextInfo Im Beispiel des im Pool befindlichen Objekts wurde gezeigt, wie die COMSchnittstelle IObjectContextInfo mit Hilfe von CoGetObjectContext geladen wird. Nun wird anhand der GetObjectContext-Funktion die andere Technik vorgestellt. Durch Aufrufen dieser Funktion wird die COM-Schnittstelle IObjectContext zurückgegeben. Zum Abrufen von IObjectContextInfo wird QueryInterface für IObjectContext durchgeführt. Eine Beispielimplementierung lautet folgendermaßen: IObjectContextPtr pObjectContext; _com_util::CheckError( GetObjectContext(&pObjectContext)); IObjectContextInfoPtr pObjTx; pObjTx = pObjectContext //pObjectContext.QueryInterface(IID_IObjectContextInfo, (void **)&pObjTx);
In diesem Beispiel werden die COM-Compilerunterstützung und Smartpointer (intelligente Zeiger) verwendet, d.h., der gröbste Teil der Arbeit wird Ihnen abgenommen. Im unteren Teil des Codebeispiels habe ich eine Kommentarzeile hinzugefügt, um zu veranschaulichen, welche Aufgabe die Zuweisung des intelligenten Zeigers (pObjTx = pObjectContext) übernimmt. Die Codierung dieses Beispiels in Visual Basic verläuft ähnlich wie die Codierung für ein transaktionsfähiges COM+-Objekt. Zunächst müssen Sie das COM-Objekt ObjectContext abrufen. Anschließend wird aus dem COM-Objekt ObjectContext die COM-Schnittstelle IObjectContextInfo über die Eigenschaft ContextInfo abgerufen. Eine Beispielimplementierung lautet folgendermaßen: Dim ctxt As ObjectContext Dim ctxtInfo As ContextInfo
Weitere Schnittstellen zur Transaktionsverwaltung
409
Set ctxt = GetObjectContext() Set ctxtInfo = ctxt.ContextInfo
Arbeiten mit der nicht verarbeiteten Transaktion Im Beispiel des im Pool befindlichen Objekts musste die ODBC-Funktion direkt mit der unverarbeiteten DTC-Transaktion kommunizieren. Die IObjectContextInfo::GetTransaction-Methode gibt einen Schnittstellenzeiger des Typs ITransaction zurück. Diese Schnittstelle wird auch im OLE DB-Provider-Framework verwendet. Die Hauptfunktion dieser Schnittstelle besteht darin, die direkte Bearbeitung der Transaktion durch Aufruf von Commit oder Abort zu ermöglichen. Die Schnittstelle wird folgendermaßen definiert: interface ITransaction : IUnknown { HRESULT Commit( [in] long fRetaining, [in] unsigned long grfTC, [in] unsigned long grfRM); HRESULT Abort( [in] BOID* pboidReason, [in] long fRetaining, [in] long fAsync); HRESULT GetTransactionInfo([out] XACTTRANSINFO* pinfo); };
Die anderen COM-Schnittstellen, die die Transaktion übergeben oder abbrechen, sind viel einfacher strukturiert – sie erfordern keine Parameter. Die ITransactionSchnittstelle ist komplexer als IObjectContext::SetComplete und IObjectContext::SetAbort. Wenn Sie nämlich mit der unverarbeiteten Transaktion arbeiten, können Daten auf verschiedene Weise übergeben werden. Warum sollte man also die nicht verarbeitete Transaktion und diese komplexe Schnittstelle einsetzen? Die Antwort lautet, dass diese die Möglichkeit zur Arbeit mit der Transaktion bietet, ohne dass das COM+-Framework im Wege steht. Denken Sie an das Beispiel des im Pool befindlichen Objekts. Die Angabe einer COM+-Transaktion ist für die ODBC-Ressource nicht von Bedeutung, da sie sie nicht verarbeiten kann. Die Ressource kann jedoch eine DTC-Transaktion weiter verarbeiten. Die letzte Methode, GetTransactionInfo, ruft die Eigenschaften des derzeit ausgeführten Kontexts ab, beispielsweise die Isolationsebene der Transaktion. Dies ist ein Teil der derzeit ausgeführten Ebene der Ressourcensperrung.
410
Weitere Informationen zur COM+-Dienstprogrammierung
13.2.3 Festlegen von Erstellungsparametern Bislang wurden drei verschiedene C++-Erstellungsroutinen oder Methoden vorgestellt, die zum Initialisieren eines C++-COM-Objekts während der Instanziierung verwendet werden können. Übertragen Sie dies nun auf den Kontext eines generischen COM-Objekts, das in einer beliebigen Sprache geschrieben wurde. Das Problem bei der Initialisierung eines COM+-Objekts besteht darin, den richtigen Zeitpunkt zu bestimmen. Im Beispiel des im Pool befindlichen Objekts bestand die Lösung darin, zu warten, bis IObjectControl::Activate aufgerufen wurde. Dies gilt jedoch nur, wenn es sich um ein im Pool befindliches Objekt handelt. In COM+ wird durch die IObjectConstruct-COM-Schnittstelle eine weitere Erstellungsroutine implementiert. Diese COM-Schnittstelle ermöglicht die Initialisierung eines COM-Objekts, das in einer beliebigen Programmiersprache implementiert wurde. Bevor Sie darüber in Verwirrung geraten, welcher Code wo platziert werden sollte, sehen Sie sich die verschiedenen Initialisierungsroutinen an, die im Kontext einer Pseudoprogrammiersprache zu finden sind, und die Reihenfolge, in der sie aufgerufen werden: 왘 Language-Erstellungsroutine: Hierbei handelt es sich um eine von der Spra-
che bereitgestellte Erstellungsroutine oder Klasseninitialisierungsmethode. Der Speicher wird initialisiert. 왘 IObjectConstruct::Construct-Erstellungsroutine: Hierbei handelt es sich um
die neue Erstellungsroutine, die beim Instanziieren des Objekts aufgerufen wird. Sie wird in einer Konfigurationszeichenfolge angegeben, die im COM+Katalog definiert ist. 왘 IObjectControl:Activate-Erstellungsroutine: Hierbei handelt es sich um die
Erstellungsroutine der transaktionalen Lebensdauer, die darauf hinweist, dass eine Transaktion gestartet wurde. Diese wird in der Regel nur für im Pool befindliche Objekte verwendet. Die IObjectConstruct-COM-Schnittstelle ist folgendermaßen definiert: interface IObjectConstruct : IUnknown { HRESULT Construct([in] IDispatch* pCtorObj); };
Das Objekt, das als Parameter für die Construct-Methode angegeben wird (der Parameter pCtorObj), besitzt folgende IDL-Definition: interface IObjectConstructString : IDispatch { [id(0x00000001), propget] HRESULT ConstructString([out, retval] BSTR* pVal); };
Weitere Schnittstellen zur Transaktionsverwaltung
411
Betrachten Sie im Folgenden, wie diese COM-Schnittstelle implementiert werden könnte. Das COM+-Objekt ConstTest implementiert die COM-Schnittstelle IObjectConstruct, sodass ConstTest einer COM+-Anwendung hinzugefügt werden muss. Anschließend wird durch Auswahl der Registerkarte Aktivierung des Eigenschaftendialogfelds für das COM+-Objekt die Schnittstelle IObjectConstruct aktiviert. Das Dialogfeld Eigenschaften sollte etwa dem in Abbildung 13.2 dargestellten entsprechen.
Abbildung 13.2 Spezifikation zur Aktivierung des COM+-Objekts
Damit COM+ die IObjectConstruct-Schnittstelle aufruft, aktivieren Sie das Kontrollkästchen Objektkonstruktion aktivieren auf der Registerkarte Aktivierung. Wenn das ConstTest-Objekt instanziiert wird, ruft die COM+-Infrastruktur die IObjectConstruct::Construct-Methode auf. Eine Beispielimplementierung der Methode in Visual C++ lautet folgendermaßen: STDMETHODIMP CConstTest::Construct(IDispatch * pDisp) { try { IObjectConstructStringPtr pString = pUnk; if( pString != NULL) { BSTR szConstruct; pString->get_ConstructString(&szConstruct);
412
Weitere Informationen zur COM+-Dienstprogrammierung
// do some work SysFreeString(szConstruct); } } catch( _com_error err) { ; } return S_OK; }
Wie bereits erwähnt, handelt es sich beim pDisp-Parameter der IObjectConstruct::Construct-Methode um einen COM-Schnittstellenzeiger für die IObjectConstructString-Schnittstelle, die eine Eigenschaft enthält, die der im Textfeld Konstruktorzeichenfolge (Abbildung 13.2) angegebenen Laufzeitzeichenfolge entspricht. Dieselbe IObjectConstruct::Construct-Methode lautet in der Visual Basic-Implementierung folgendermaßen: Private Sub IObjectConstruct_Construct(ByVal pCtorObj As Object) Dim obj As IObjectConstructString Dim strRuntime As String Set obj = pCtorObj strRuntimw = obj.ConstructString End Sub
13.2.4 Erstellen eines Transaktionsstreams von Objekten Beim Entwickeln von auf Transaktionen basierenden COM+-Anwendungen muss das Problem gelöst werden, alle COM+-Objekte in denselben Transaktionsprozess aufzunehmen. Wie in Abbildung 13.3 gezeigt, werden bei allen bislang gezeigten Beispielen zwei Transaktionen erstellt, wenn der Konsument keinen Transaktionskontext besitzt und zwei Objekte instanziiert, die einen Transaktionskontext erfordern. Dies entspricht jedoch nicht dem angestrebten Ziel. Das Ziel bestand darin, beide COM+-Objekte in denselben Transaktionskontext aufzunehmen. In Abbildung 13.3 verfügen die COM+-Objekte 1 und 2 beide über das Transaktionsattribut Transaktionsunterstützung erforderlich. Wenn der Konsument das COM+-Objekt 1 instanziiert, wird ein Transaktionskontext gestartet, da der Ersteller des COM+-Objekts keinen Transaktionskontext besitzt. Dasselbe passiert,
Weitere Schnittstellen zur Transaktionsverwaltung
413
wenn der Konsument COM+-Objekt 2 instanziiert. Das bedeutet, dass zwei COM+-Objekte in zwei verschiedenen Transaktionskontexten vorliegen. COM+-Objekt 1
Konsument
COM+-Objekt 2
Abbildung 13.3 Beispiel für einen Konsumenten, der zwei COM+-Objekte instanziiert
Eine Lösung zum Ausführen beider COM+-Objekte im Kontext einer Transaktion besteht darin, ein Handoff-COM+-Objekt zu erstellen. Das Handoff-COM+-Objekt ist für die Erstellung der COM+-Objekte 1 und 2 zuständig. Die Verweise von diesen COM+-Objekten werden anschließend an den Konsumenten zurückgegeben. Dieses Szenario wird in Abbildung 13.4 dargestellt. Objekt wird ausgehändigt
COM+-Objekt 1
Konsument COM+-Objekt 2
Abbildung 13.4 Diagramm eines COM+-Objekts, das andere Objekte instanziiert
Der Ansatz des Handoff-COM+-Objekts funktioniert zuverlässig und ist einfach zu implementieren. Er verursacht jedoch unnötigen Arbeitsaufwand. In COM+ gibt es eine Schnittstelle namens ITransactionContextEx, die genau dieselbe Funktion erfüllt wie das Handoff-COM+-Objekt und die zusätzliche Fähigkeit besitzt, die Transaktion zu übergeben oder abzubrechen.
414
Weitere Informationen zur COM+-Dienstprogrammierung
Im Beispiel des Handoff-COM+-Objekts stellt das Handoff-COM+-Objekt den Stamm des Transaktionsstreams dar. Das bedeutet im Wesentlichen, dass das Handoffobjekt die Transaktion steuert. Die ITransactionContextEx-Schnittstelle funktioniert auf dieselbe Weise. Sie ist für die Transaktion zuständig und benötigt daher die Methoden zum Übergeben oder Abbrechen der Transaktion. In IDL ist ITransactionContextEx folgendermaßen definiert: interface ITransactionContextEx : IUnknown { HRESULT CreateInstance( [in] GUID* rclsid, [in] GUID* riid, [out, retval] void** pObject); HRESULT Commit(); HRESULT Abort(); };
Diese Schnittstelle lautet in Visual Basic ähnlich mit der Ausnahme, dass die CreateInstance-Methode PROGIDs verwendet. Trotz der offensichtlichen Vorteile dieser Schnittstelle werden Sie sie wahrscheinlich nicht einsetzen. Diese Schnittstelle ist nur nützlich, wenn Sie einen auf DCOM basierenden Client verwenden. Da zur Darstellung das Web genutzt wird, verfügen IIS und ASP bereits über integrierte Möglichkeiten zur Transaktionsverwaltung. Wenn Sie diese Schnittstelle verwenden müssen, überdenken Sie Ihren Geschäftsprozess noch einmal. Erfahrungsgemäß kann diese Schnittstelle durch Einsatz des Façade-Musters normalerweise vermieden werden. Das Façade-Muster ähnelt einem Handoffobjekt mit der Ausnahme, dass es Anwendungslogik enthält.
13.3 Entwicklung von ASP-Komponentenobjekten Es ist möglich, beinahe sämtlichen Anwendungscode in Skriptform oder unter Verwendung von JavaScript-Objekten zu verfassen. Manchmal ist es jedoch effizienter, Komponenten einzusetzen. Komponenten ermöglichen die Einkapselung von Programmcode und vereinfachen die Entwicklung von ASP-Seiten. Komponenten geben Webentwicklern außerdem die Möglichkeit, sich auf die Benutzerschnittstelle zu konzentrieren, und Komponentenentwickler können sich auf die Anwendungslogik beschränken. Um die Unterschiede bei der Entwicklung von ASP-Seiten aufzuzeigen, nehmen Sie die Aufgabe, von 1 bis 10 zu zählen, und verfolgen Sie dieses Beispiel durch die verschiedenen Phasen der COM+-Objektentwicklung. Zunächst wird ein reiner ASP-Ansatz verwendet, um das Zählproblem zu lösen. Anschließend wird ein
Entwicklung von ASP-Komponentenobjekten
415
Teil des Code durch ein COM+-Objekt ersetzt. Schließlich wird der größte Teil des HTML- und ASP-Skriptcode durch ein ASP-COM+-Objekt ausgetauscht. Beachten Sie, dass der Begriff COM+ häufiger als der Begriff COM verwendet wird. Wenn ich ein COM+-Objekt erwähne, beziehe ich mich in diesem Zusammenhang auch auf COM-Objekte ohne grafische Benutzeroberfläche (GUI).
13.3.1 Eine einfache ASP-Seite Eine Implementierung, durch die das Zählproblem in einer reinen ASP-Seite gelöst wird, sieht folgendermaßen aus:
In diesem Beispiel wird JavaScript verwendet, um von 0 bis 9 zu zählen, und ein Teil der Ausgabe wird in Form einer Tabelle erzeugt. Eine zusätzliche Berechnung wird durchgeführt, und die Ausgabe wird in der generierten Tabelle abgelegt.
13.3.2 Integrieren eines COM+-Objekts Beim Erstellen von Webanwendungen sollten Anwendungslogik und komplexe Berechnungen nicht in JavaScript auf einer ASP-Seite verfasst werden, da JavaScript nicht über so umfassende Funktionen verfügt wie eine Programmiersprache. Die Lösung besteht darin, ein COM+-Objekt zu erstellen, das die Anwendungslogik oder komplexe Berechnung enthält. Das Erstellen eines COM+-Objekts bedeutet auch, dass der Programmiercode zwischen einer ASP-Seite und
416
Weitere Informationen zur COM+-Dienstprogrammierung
einem COM+-Objekt verteilt werden muss. Es ist möglich, ein COM+-Objekt in einer ASP-Seite zu integrieren, indem die ASP-Seite das COM+-Objekt aufruft. Die Details zur Erstellung dieses COM+-Objekts werden an dieser Stelle nicht ausgeführt. Beschränken wir uns darauf, dass die COM-Objekt-IDL folgendermaßen lautet: interface IASPComponent : IDispatch { HRESULT reset(); HRESULT getValue( [out,retval]long *value); };
Diese Schnittstelle enthält zwei Methoden: reset, die den Zähler und den Wert zurücksetzt, und getValue, die den berechneten Wert abruft und den Zähler für den nächsten getValue-Aufruf erhöht. Anschließend kann das COM+-Objekt in die ASP-Seite integriert werden, um schrittweise dieselbe Tabelle zu erstellen wie im reinen ASP-Ansatz. Die geänderte ASP-Seite sieht folgendermaßen aus:
Entwicklung von ASP-Komponentenobjekten
417
In diesem Code besteht der Unterschied darin, dass das COM-Objekt auf der ASP-Seite instanziiert wird. Hierzu stehen zwei Verfahren zur Verfügung. Das erste besteht in der Verwendung des OBJECT-Tags, das zweite ist, die Server.CreateObject-Methode zu verwenden. Für das OBJECT-Tag müssen Sie drei Parameter angeben: 왘 RUNAT=server: Dies ist erforderlich, um dem ASP-Parser mitzuteilen, dass das
Objekt auf der Serverseite instanziiert wird. Wird dieser Parameter nicht hinzugefügt, wird das Objekt auf der Clientseite instanziiert. 왘 PROGID: Dies ist die COM-PROGID, die zur Instanziierung des Objekts ver-
wendet wird. Es ist möglich, die CLSID zu benutzen, diese ist jedoch abstrakter und schwieriger nachzuvollziehen. 왘 ID: Hierdurch wird der Objektbezeichner definiert, der von den Skripts für den
Verweis auf die instanziierten Objekte verwendet wird. Die Server.CreateObject-Methode erfordert als Parameter die PROGID des COM-Objekts. Wenn die Instanziierung des COM-Objekts erfolgreich verläuft, wird das Objekt zurückgegeben. Im vorhergehenden Code war diese Technik zu Referenzzwecken als Kommentar gekennzeichnet. Nachdem das Objekt über eine der Techniken instanziiert wurde, kann es wie die integrierten ASP-Objekte aufgerufen werden. In diesem Beispiel wird objCounter.reset aufgerufen, und in der Schleife wird die objCounter.getValue-Methode für den Zugriff auf den Wert verwendet.
13.3.3 Ein ASP-COM+-Objekt Die letzte Möglichkeit der Optimierung, die Sie vornehmen können, besteht darin, sowohl HTML als auch Skript im COM+-Objekt einzubetten. Diesen Vorgang bezeichnet man als das Erstellen eines ASP-COM+-Objekts. Der Verweis auf ein ASP-COM+-Objekt auf einer ASP-Seite erfolgt auf dieselbe Weise wie der Verweis auf das COM+-Objekt im vorherigen Beispiel. Das ASP-COM+-Objekt übersteigt jedoch die Möglichkeiten des COM+-Objekts insofern, als dass es auf das ASPObjektmodell im COM+-Objekt zugreifen kann. Der Vorteil dieses Ansatzes besteht darin, dass Skriptcode nicht in Eingabe- oder Ausgabe-HTML oder Skriptinhalt geschrieben werden muss. Ein kompaktes COM+-Objekt wird erstellt, das nur einige Parameter erfordert, um sehr umfassende Inhalte zu erzeugen. Aufgrund des COM+-Kontexts kann IIS das ASP-Objektmodell von einer Skriptumgebung an ein COM-Objekt weitergeben. Wenn ein COM+-Objekt innerhalb des Kontexts einer ASP-Seite instanziiert wird, ist bereits ein ASP-Kontext vorhanden. In einem normalen COM+-Objekt wird auf den ASP-Kontext nicht verwie-
418
Weitere Informationen zur COM+-Dienstprogrammierung
sen, in einem ASP-COM+-Objekt hingegen schon; aus diesem Grund wird es als ASP-COM+-Objekt bezeichnet. Nehmen Sie jedoch nicht an, dass das ASPCOM+-Objekt COM+-Transaktionsdienste einsetzen muss, um ordnungsgemäß zu funktionieren. Denken Sie daran, dass das Kontextprogrammiermodell dem COM+-Objekt ermöglicht, die benötigte Funktionalität zu wählen. Abrufen der ASP-Objekte Wie bereits erwähnt, wird innerhalb des COM+-Kontexts auf das ASP-Objektmodell verwiesen. Für den Zugriff auf das Objektmodell wird die GetObjectContextFunktion aufgerufen. Dadurch wird ein IObjectContext-COM-Schnittstellenzeiger abgerufen. Auf der Grundlage dieses COM-Schnittstellenzeigers ist es möglich, QueryInterface auszuführen und den IGetContextProperties-COM-Schnittstellenzeiger abzurufen. Von dieser COM-Schnittstelle aus können Sie die verschiedenen ASP-Objekte (Session, Application, Response usw.) als Eigenschaften laden. Die ASP-COM-Objekte sind in der Microsoft Active Server Pages-Objektbibliothek (ASP.TLB) definiert. Beginnen wir mit einer einfachen ASP-Objektbearbeitung. Ein Beispiel für das Laden des ASP-Objekts Response in Visual Basic lautet folgendermaßen: Dim objResponse As Response Dim objContext As ObjectContext Set objContext = GetObjectContext() Set objResponse = objContext("Response")
Nach diesem Visual Basic-Code zu urteilen, erscheint das Abrufen des ResponseObjekts nicht besonders schwierig. Der Grund hierfür liegt allerdings darin, dass Visual Basic dem Entwickler sehr viele komplexe Vorgänge vorenthält. Sehen Sie sich an, wie das ASP-Objekt Response mit Hilfe von Visual C++-Code geladen wird. IRequestPtr ptrRequest; IObjectContextPtr ptrCtxt; IGetContextPropertiesPtr pProps; GetObjectContext( &ptrCtxt); pProps = pObjContext; _variant_t vt; pProps->GetProperty( _bstr_t( "Response"), &vt);
Diese Code ist etwas komplexer. Es werden jedoch COM+ Smartpointers (intelligente Zeiger) verwendet. Dadurch werden Verweiszählung und der QueryInterface-Vorgang in den Programmiercode integriert. Der einzige Unterschied zwischen diesem Code und dem vorhergehenden Visual Basic-Code besteht darin, dass Sie die IGetContextProperties::GetProperty-Methode explizit aufrufen müssen. Diese Methode gibt einen VARIANT-Datentyp zurück, der das auf IDispatch basierende Response-COM-Objekt enthält. Optimieren von Verweisen auf ASP-Objekte in Visual C++ Im vorgenannten Visual C++-Beispiel kann das wiederholte Schreiben desselben Code für den Verweis auf das ASP-COM-Objekt ziemlich langweilig werden. Eine bessere Lösung ist, diese Funktionalität in einer anderen Klasse zusammenzufassen, die diesen Programmcode enthält. Im allgemeinen Verzeichnis mit dem Quellcode für dieses Buch befinden sich einige Headerdateien, und die Headerdatei implementation.h enthält eine Klasse ASPObjReference. In dieser Klasse werden alle oben definierten Schritte zusammengefasst. Diese Klasse wird normalerweise innerhalb einer anderen Klasse als Datenmitglied im Klassengeltungsbereich verwendet. Verwenden des ASP-Objektmodells Nachdem das ASP-Objektmodell abgerufen wurde, ist es an der Zeit, tatsächlich etwas damit anzufangen. Der einfachste Ansatz besteht darin, ein COM+-Objekt zu schreiben, das die Arbeit verrichtet und das ASP-Objektmodell bearbeitet. Bei dieser Art der Entwicklung gibt es jedoch ein Problem. Im gesamten Buch wurde nach Möglichkeit versucht, den Komponenten mehr Granularität zu verleihen. Durch Kombinieren des ASP-Objektmodells mit der Anwendungslogik wird die Granularität jedoch reduziert. Auch wenn ASP vielleicht in dieser Version Ihrer Anwendung »in« ist, ändern sich die Bedingungen möglicherweise bis zur Erstellung der nächsten Version. In diesem Fall müssen Sie eventuell einen größeren Teil umschreiben, um die Anwendungslogik aus dem Bearbeitungscode des ASP-Objektmodells zu extrahieren. Die an dieser Stelle verwendete Lösung ist, ein ASP-COM+-Objekt zu schreiben, das das IASPComponent-COM+-Objekt bearbeitet. Dieser Ansatz mag zu ausführlich erscheinen, da ein Stück Skript ein ASP-COM+-Objekt bearbeitet, das anschließend wiederum ein COM+-Objekt bearbeitet. Dieser Eindruck ist jedoch falsch, da das ASP-COM+-Objekt eine Einkapselung der ASP-Funktionalität dar-
420
Weitere Informationen zur COM+-Dienstprogrammierung
stellt. Das Schreiben von Skriptcode zum Bearbeiten des ASP-COM+-Objekts ist immer einfacher als das Schreiben von Skripts für die Aufgaben des ASP-COM+Objekts. Da das ASP-COM+-Objekt kompiliert wird, kann es nicht vorkommen, dass ein Skriptschreiber die falsche Skriptgeneration erstellt. Die einzige Ausnahme von dieser Regel ist, dass JavaScript-Objekte anstelle der ASP-COM+-Objekte implementiert werden könnten; das ASP-COM+-Objekt bietet jedoch noch immer den Vorteil der Geschwindigkeit und der Programmiervielfalt. Das ASP-COM+-Objekt kapselt die Iteration innerhalb der CASPWrapper::generate-Methode ein. Daher muss das ASP-Skript nur eine Methode aufrufen. Die Methode verlangt einen Parameter, der die Anzahl der ausgeführten Iterationen definiert. Diese Methode heißt generate und wird folgendermaßen implementiert: STDMETHODIMP CASPWrapper::generate(long iterations){ try { m_refASP.Init( ASPObjReference::get_Response); ASPCOMPONENTLib::ICounterPtr ptr( "ASPComponent.Counter.1"); long c1; char buffer[ 512]; ptr->reset(); m_refASP.Response()->Write( _variant_t( "
In diesem Beispiel besitzt die generate-Methode als Parameter die Anzahl der Iterationen, die erzeugt werden. Dieser Code verwendet außerdem das ASPObjReference-Objekt. Diese Helferklasse wird verwendet, indem die ASPObjReference::Init-Methode aufgerufen wird und die zu ladenden ASP-Objekte angegeben werden. In diesem Code sollte nur das Response-Objekt (get_Response) abgerufen werden.
Entwicklung von ASP-Komponentenobjekten
421
Nach dem Abrufen des Response-Objekts wird das COM-Objekt instanziiert, das die eigentliche Anwendungslogik durchführt (ptr). Anschließend wird wie im vorherigen ASP-Quellbeispiel die ptr->reset()-Methode aufgerufen, um den internen Zähler zurückzusetzen. Danach wird eine Tabelle erzeugt. Sehen Sie sich diesen Vorgang genauer an. Der Inhalt wird an den Client zurückgesendet, indem die Methode Response->Write aufgerufen wird. Dieser Vorgang wurde in ASP ebenso ausgeführt, besitzt jedoch eine bedeutsame Auswirkung: Dem ASP-COM+-Objekt ist nicht bekannt, wo der Inhalt auf der HTML-Seite abgelegt wird. Vom ASP-COM+-Objekt aus betrachtet handelt es sich nur um das Hinzufügen weiterer HTML-Inhalte zum Stream. Demzufolge ist es sehr wichtig, wo Sie das ASP-COM+-Objekt auf der ASP-Seite platzieren. Beispielsweise werden Sie das CASPWrapper-Objekt nicht zwischen die HTML-Tags und setzen. Stattdessen wird das CASPWrapper-Objekt zwischen den HTML-Tags und eingefügt. Beachten Sie, dass in diesem Codebeispiel das VARIANT-Tag verwendet wird. Als Visual C++-oder Visual J++-Nutzer werden Sie sich daran gewöhnen müssen. In Visual Basic ist dies ein fester Bestandteil der Programmiersprache. Betrachten Sie nun, wie dieses ASP-COM+-Objekt in die ASP-Seite integriert wird.
Beachten Sie, wie einfach diese ASP-Seite aufgebaut ist. Sie enthält nur einen Methodenaufruf, und die Tabelle wird automatisch erzeugt. Als Designer können Sie sich anschließend auf das Zusammenstellen der Anwendung und der Benutzeroberfläche konzentrieren. Der Nachteil ist, dass die vom COM-Objekt generierte Ausgabe nicht einfach bearbeitet werden kann. Wenn die Tabelle nicht Ihren Vorstellungen entspricht, müssen Sie möglicherweise das COM-Objekt ändern und erneut kompilieren. Da es sich beim ASP-COM-Objekt jedoch um einen Wrapper für das zugrunde liegende Objekt handelt, ist es auch möglich, das zugrunde liegende Objekt direkt zu verwenden und das ASP-Objekt somit zu umgehen.
422
Weitere Informationen zur COM+-Dienstprogrammierung
13.3.4 ASP-Seiten mit Transaktionen Eine Webanwendung verwendet DCOM nicht, wenn COM-Objekte auf der Serverseite über den Client bearbeitet werden sollen. Stattdessen wird eine Webanwendung in diesem Fall höchstwahrscheinlich IIS und eine ASP-Seite einsetzen. Das wirft jedoch die Frage auf, wie Transaktionen auf einer ASP-Seite verwaltet werden. Mit IIS 5.0 haben Sie die Möglichkeit, auf Transaktionen basierende Webseiten zu verwalten. Hier ist ein Beispiel für die Verwendung einer Transaktion innerhalb einer ASP-Seite:
Das Schlüsselwort transaction muss der ersten Zeile des ASP-Skripts hinzugefügt werden. Das Attribut transaction wird zum Definieren der unterstützten Transaktionstypen eingesetzt. In diesem Beispiel besitzt die Webseite das Transaktionsattribut supported. Weitere Transaktionsattribute sind required new, required und not supported. Sie sind bedeutungsgleich mit den COM+-Transaktionsattributen mit der Ausnahme, dass sie im Kontext einer ASP-Seite verwendet werden. ASP arbeitet mit Transaktionen auf Seitenebene. Das heißt, dass die Transaktion am Ende eines Seitenskripts übergeben oder abgebrochen wird. Eine von ASP instanziierte Transaktion kann nur für die Dauer der Seite anhalten. Wenn dies akzeptabel ist, bietet sich dieser Transaktionstyp an, da mehrere Komponenten innerhalb desselben Transaktionskontexts verwendet werden können. Auf diese Weise kann jede Aktion, die von einer der Komponenten ausgeführt wird, übergeben oder abgebrochen werden. Sehen Sie sich den folgenden Quellcode an: The transaction example Error is
In diesem Beispiel wird eine Transaktion gestartet, wenn die Seite geladen wird, und anschließend wird das ASP-Skript ausgeführt. Die Funktionen OnTransactionCommit und OnTransactionAbort werden jedoch nicht ausgeführt. Hierbei handelt es sich um Ereignisse, die von der ASP-Transaktionsunterstützung aufgerufen werden, um auf den Erfolg oder das Fehlschlagen der Transaktion auf der Seite hinzuweisen. Wenn die Ausführung einer Seite abgeschlossen ist, wird ein Commitvorgang in zwei Phasen gestartet. Verläuft dieser erfolgreich, wird das Ereignis OnTransactionCommit aufgerufen. Alles andere ist ein Fehler, der den Aufruf der Funktion OnTransactionAbort verursacht. Das andere Element, das auf dieser ASP-Seite zu beachten ist, ist die Verwendung von JavaScript-Ausnahmeblöcken. Durch Verwendung dieser Blöcke ist es möglich, dem Benutzer anstelle einer Fehlerseite mit kryptischem Fehlercode verständliches Feedback zu den aufgetretenen Fehlern zu geben. Integration von ASP-Transaktionen und COM+-Transaktionen Es ist möglich, das ASP-COM+-Objekt CASPWrapper mit einer ASP-Seite mit Transaktionen zu kombinieren. Das COM+-Objekt, das vom ASP-COM+-Objekt CASPWrapper bearbeitet wird, könnte COM+-Transaktionen verwenden. Die Transaktions-ID des COM+-Objekts und die ASP-Transaktions-ID stimmen jedoch nicht überein, da COM+-Transaktionen als flache Transaktionen gelten. (Weitere Informationen hierzu finden Sie in Kapitel 10.) Das bedeutet, dass es sich um zwei völlig separate Transaktionen handelt.
424
Weitere Informationen zur COM+-Dienstprogrammierung
Durch diesen Umstand wird die Transaktionsverwaltung erschwert, da festgelegt werden muss, welche Transaktion zu welchem Zeitpunkt und auf welche Weise ausgeführt wird. Hierzu gibt es einige einfache Regeln: 왘 Wenn ein COM+-Objekt im Kontext einer ASP-Seite mit Transaktionsunter-
stützung instanziiert wird, stimmen die Transaktions-ID des COM+-Objekts und die der ASP-Seite überein. Ein Aufruf zum Abbrechen der Transaktion von der ASP-Seite oder vom COM+-Objekt führt zum Fehlschlagen der Transaktion. 왘 Wenn das COM+-Objekt auf einer ASP-Seite instanziiert wird, die keine Trans-
aktionen unterstützt, wird eine separate Transaktion gestartet. Wird das Objekt durch eine ASP-Seite mit Transaktionsunterstützung bearbeitet, sind die Änderungen, die durch Skriptcode auf der ASP-Seite vorgenommen werden, kein Bestandteil der ASP-Transaktion. 왘 Wenn das COM+-Objekt einer Session-Variable zugeordnet wird, wird der
Objektverweis ungültig, sobald die Transaktion des COM+-Objekts beendet ist, und sollte nicht mehr verwendet werden. Stattdessen sollte der Wert auf NULL gesetzt werden.
13.4 Resümee Mit diesem Kapitel ist die Diskussion des Geschäftsobjekts der mittleren Schicht abgeschlossen. Es wurden einige fortgeschrittene Themen zur Verwendung von COM+-Transaktionen aufgezeigt. Dabei handelt es sich um spezielle Szenarios, die jedoch auftreten können. Beispielsweise solange es interessant ist, ganz neue Anwendungen mit ganz aktuellen Daten zu erstellen. Die Realität ist, dass Sie wahrscheinlich auch irgendwelche Legacydaten integrieren müssen. Die Konvertierung der Legacydaten ist nicht möglich, die Daten müssen jedoch in die COM+-Anwendung aufgenommen werden. Im Pool befindliche Objekte sind die Lösung. Sie können auch Ressourcenverteiler schreiben, dies ist jedoch wesentlich komplizierter. Der andere Teil dieses Kapitels beschäftigte sich mit dem Erstellen ASP-fähiger COM+-Objekte. Dies ist ein sehr spezielles Szenario, das Sie zu Entwicklungszwecken jedoch benötigen. Geschäftsobjekte auf der Serverseite müssen optimiert werden. ASP-Entwickler und HTML-Designer möchten ihre Zeit nicht mit dem Entwickeln großer Mengen von ASP-Code für die Anzeige einer Tabelle von Benutzern verschwenden. Ein ASP-COM+-Objekt optimiert diesen Vorgang. Es verringert die Flexibilität in der Formatierung nicht, da Dokumentvorlagen auf die gesamte HTML-Seite angewendet werden können, die sich auch auf den vom ASP-COM+-Objekt generierten HTML-Inhalt auswirken.
Resümee
425
14 Erstellen des Hybridclients Bislang wurden HTML (Hypertext Markup Language) und COM (Component Object Model) behandelt. In diesem Kapitel werden beide kombiniert, um den Hybridclient zu erstellen, der die Internettechnologie mit der Desktopfenstertechnologie verbindet. Die Kombination beider Technologien vereinfacht das Schreiben verteilter Anwendungen, die leicht gewartet werden können. In diesem Kapitel werden die verschiedenen Teile eines Hybridclients erläutert und wie diese kombiniert werden, um eine Hybridanwendung zu erstellen. Die verwendete Kerntechnologie ist Microsoft Internet Explorer, da der Internet Explorer nicht nur ein Webbrowser, sondern auch ein komponentenbasierter Dokumenten- und Internetbrowser ist.
14.1 Definieren des Hybridclients Der Thin Client wurde bereits als einfache Lösung für die weite Verteilung genannt. Der Thin Client löst jedoch nicht das Problem der Datenverwaltung. Ein Thin Client geht davon aus, dass alle Daten auf dem Server verbleiben – dies ist jedoch oft nicht möglich. Viele Anwendungen erfordern intensive Interaktion von der Clientseite, da eine umfassendere Schnittstelle manchmal beim Ausführen der Aufgabe hilfreich sein kann. In manchen Fällen genügt eine HTML-Benutzerschnittstelle nicht. Das Problem, dem wir in Zukunft gegenüberstehen werden, ist, dass die Prozessorgeschwindigkeit zum Verwalten großer Datenmengen nicht ausreicht. Ich besitze beispielsweise einen Apple Newton und mehrere Windows CE-Geräte, die ich zum Verteilen meiner Aufgaben verwende. Der Engpass entsteht beim Verschieben und Bearbeiten von Daten. Ich arbeite regelmäßig mit etwa 300 MB Daten und greife auf viele weitere MB archivierter Daten aus älteren Projekten zu. Im Allgemeinen übersteigt das Volumen an elektronischen Daten weitaus die verfügbare Netzwerkkapazität, und dieses Problem wird in der Zukunft nicht geringer, da Technologien wie XML (Extensible Markup Language) diese Anforderungen dramatisch steigern werden. Was demnach gebraucht wird, ist eine Computersoftwarelösung, die diesen Datenverkehr im Netzwerk optimiert. Und die einzige Lösung, die sich bislang ergeben hat, ist ein Gerät, das eine CPU, RAM und eine Festplatte besitzt (ein PC), auf dem ein Hybridclient ausgeführt wird. Was versteht man unter einem Hybridclient? Stellen Sie sich vor, Sie verwenden ein Textverarbeitungsprogramm zum Bearbeiten eines Dokuments. Ist es besser,
Definieren des Hybridclients
427
das Textverarbeitungsprogramm lokal zu installieren, anstatt die entsprechende Programmdatei von einem Remoteserver zu laden? Bei einer lokalen Installation wird das Textverarbeitungsprogramm schneller gestartet und es ist keine Netzwerkverbindung erforderlich. Sie könnten argumentieren, dass Netzwerke in Zukunft immer schneller werden, und Sie haben Recht. Es ist wahrscheinlich, dass Sie demnächst Software für täglich ausgeführte Aufgaben kaufen und lokal installieren und sich für gelegentliche Aufgaben mit der langsameren Netzwerkverbindung begnügen und eine Remoteanwendung zeitweise mieten. Wenden Sie nun das Konzept des Hybridclients auf die Aufgabe der Dokumentbearbeitung an. Ein intelligentes Textverarbeitungsprogramm ist ein Hybridclient. In nächster Zukunft werden Funktionen, die täglich benötigt werden, lokal und spezielle Funktionen nach Bedarf installiert. Dieser Prozess der Installation nach Bedarf wäre im Textverarbeitungsprogramm eingebettet. Interessanterweise ist diese Funktionalität in Microsoft Office 2000 integriert. Die einzige Funktion, die noch nicht implementiert wurde, ist die Möglichkeit, eine Gebühr für die einmalige Nutzung bestimmter Funktionen zu berechnen. Die nächste Frage ist, wo die im Textverarbeitungsprogramm bearbeiteten Dokumente gespeichert werden. Auch diese können lokal oder remote abgelegt werden. Benutzer müssen diesen Speicherort nicht definieren. Sie benötigen lediglich eine Reihe von Dokumenten, mit denen sie arbeiten können. Wenn Sie die erforderliche Funktionalität betrachten, wird deutlich, dass ein Hybridclient benötigt wird. Die Rolle des Hybridclients besteht darin, die Speicherung im Netzwerk und die lokale Speicherung in einer einzigen Domäne mit Dokumenten und Funktionen zusammenzufassen. Wenn diese Dokumente geändert werden, ermittelt der Hybridclient basierend auf dem Namen der Domäne, ob sie lokal oder remote gespeichert werden. Der Hybridclient verwaltet automatisch die Installation oder das Löschen von Funktionen. Dem Benutzer ist somit nie bewusst, ob es sich um eine lokale oder um eine Remotebearbeitung handelt.
14.1.1 Die Architektur des Hybridclients Der an dieser Stelle behandelte Hybridclient basiert auf Microsoft Internet Explorer. Es ist möglich, einen anderen Browser, beispielsweise Mozilla, zu verwenden. Die Erläuterung der einzelnen Browser ginge jedoch über den Rahmen dieses Buches hinaus. Microsoft Internet Explorer selbst ist eine Hybridclientanwendung, wenn auch eine einfache. Auf dem Windows 2000-Desktop stellt das Microsoft Internet Explorer-Symbol nur einen Verweis auf eine Programmdatei dar, die eine Anwendung instanziiert, in der das Webbrowser-COM-Steuerelement enthalten
428
Erstellen des Hybridclients
ist. Das Webbrowser-COM enthält die Funktionalität zum Durchsuchen eines beliebigen Dokumenttyps, einschließlich HTML. Abbildung 14.1 zeigt die Architektur von Microsoft Internet Explorer.
Abbildung 14.1 Architektur des Microsoft Internet Explorer
Die Microsoft Internet Explorer-Anwendung ist demzufolge ein Controller für das COM-Steuerelement, da das Webbrowser-COM-Steuerelement darin enthalten ist. Dieses Steuerelement befindet sich in der Bibliothek SHDOCVW.DLL. Es handelt sich um ein COM-Steuerelement, jedoch auch um einen Active Document Container. Ein Active Document Container verfügt über die Möglichkeit, einen Active Document-Server zu verwalten und zu enthalten. Ein Active Document-Server ist vergleichbar mit OLE (Object Linking and Embedding). OLE basiert auf der Möglichkeit zum Ausschneiden und Einfügen von Informationen aus einem bestimmten Dokumenttyp in einen anderen Dokumenttyp.
Definieren des Hybridclients
429
Die Implementierung dieser Art der Funktionalität ist sehr komplex. Ein Active Document-Server ist eine vereinfachte Version von OLE, da Sie zwar nicht ausschneiden und einfügen, jedoch ein generisches Dokumentframework erstellen können, das über den Inhalt der Dokumente gesteuert wird. Das Webbrowser-COM-Steuerelement ist insofern ein generisches Dokumentframework, als dass es Ihnen das Durchsuchen eines beliebigen Dokumenttyps ermöglicht, der einen Active Document-Server enthält. Natürlich werden Sie meistens HTML- und XML-Dokumente durchsuchen. Um jedoch eine komplexere Hybridclientanwendung zu erstellen, könnten Sie eine Microsoft Word- oder Microsoft Excel-Datei als Remotedokument integrieren. Wenn eine HTML-Seite in das Webbrowser-COM-Steuerelement geladen wird, wird der HTML-Active Document-Server, MSHTML.DLL, abgerufen. Diese DLL (Dynamic Link Library) enthält alle Dynamic HTML-COM-Objekte und dient als HTML-COM-Komponente. Das JavaScript, das das Dynamic HTML-Objektmodell auf der Clientseite bearbeitet, ändert das HTML-COM-Objektmodell.
14.1.2 Anwendungslogik auf dem Client Viele betrachten Fat Clients als ungünstig, da sie sowohl in der Bytezahl als auch in der Menge des Programmiercodes zu umfangreich sind. Der schlechte Ruf des Fat Clients ist jedoch nicht ganz berechtigt. Manchmal müssen wegen der zu erfüllenden Aufgaben Fat Clients eingesetzt werden. Allerdings möchte ich an dieser Stelle nicht versuchen, die Vorteile eines Fat Clients aufzuzeigen. Ein Hybridclient kann jedoch ein Fat Client sein, und ein Fat Client ist nur dann von Nachteil, wenn er viele Aufgaben durchführt, anstatt den Programmcode zu verteilen. Ein Hybridclient kann nie alle Aufgaben selbst ausführen. Einige Aufgaben müssen auf der Clientseite erfüllt werden, beispielsweise die Textverarbeitung. Eine Textverarbeitungsanwendung beschränkt sich auf die Aufgabe der Textverarbeitung. In manchen Fällen muss die Textverarbeitung durch Formularverwaltung und Textverarbeitungsinhalt erweitert werden. Dies ist typisch für eine Workflowanwendung. In diesem Fall agiert die Textverarbeitung als Shellanwendung, die den Inhalt bearbeitet. Im Gegensatz dazu verarbeitet ein serverseitiger Prozess den Inhalt auf der Serverseite. Nun stellt sich die Frage, wo der Programmcode abgelegt werden sollte. In den Kapiteln 6 und 7 wurden einige Techniken zur Erstellung und Bearbeitung von Daten besprochen. Besonders wurde das Konzept von Hard- und Softdaten untersucht. Mit der Einführung des Hybridclients verschwimmt der Unterschied zwischen Hard- und Softdaten, da einige Daten auf der Clientseite gespeichert wer-
430
Erstellen des Hybridclients
den. Der Schwerpunkt liegt nun auf der Lösung von Aufgaben und darauf, zu ermitteln, wo die Hard- und Softdaten gespeichert werden. Im Fall der Workflowanwendung wird der Inhalt anfangs als Softdaten eingestuft. Sobald der Inhalt jedoch auf der Serverseite verarbeitet wird, handelt es sich um Harddaten, die an den Client zurückgesendet werden. Zu diesem Zeitpunkt speichert der Client die Harddaten auf der lokalen Festplatte. Da es sich jedoch um Harddaten handelt, wird auf dem Server eine Kopie der Daten angefertigt und zu Referenzzwecken gespeichert. Der Client hat also praktisch die Harddaten auf der lokalen Festplatte zwischengespeichert, um die Zugriffszeit zu optimieren. Der Client betrachtet die Harddaten als schreibgeschützt. Sobald die Daten geändert werden, handelt es sich wieder um Softdaten, die an den Server gesendet und in Harddaten umgewandelt werden müssen. Diesen Vorgang nennt man Ressourcenoptimierung. Der Hybridclient ist gute eine Übung, um herauszufinden, wie Daten im Netzwerk gespeichert werden. Die Herausforderung liegt darin, die Daten in Harddaten umzuwandeln und sicherzustellen, dass anschließend jeder, der eine Kopie der Harddaten besitzt, über die richtigen Informationen verfügt. Im Rahmen dieses Buches gehe ich davon aus, dass das Konzept der COM+Transaktionen und COM+-Aktivitäten sich nicht auf die Clientseite erstreckt. Dass bei clientseitiger Verwendung von Windows 2000 auch COM+-Transaktionsdienste eine Rolle spielen, wird an dieser Stelle außer Acht gelassen. Der Grund hierfür liegt darin, dass die Ressourcen-Manager noch nicht vielseitig genug sind, um der Clientseite COM+-Funktionen zu verleihen. Es gibt zum Beispiel noch keine sichere Möglichkeit, clientseitige Transaktionen über das HTTP-Protokoll (Hypertext Transfer Protocol) oder XML mit dem Server in Bezug zu setzen. Dieses spezielle Beispiel ist von Bedeutung, da es sich auf den Speicherort auswirkt, an dem die Daten in Harddaten umgewandelt werden. Definieren der Shell Im Hybridclient wird das Konzept der Shell eingesetzt. Die Shell, die in diesem Buch verwendet wird, kapselt das Webbrowser-COM-Steuerelement ein. Die Hauptfunktion der Shell liegt in der nahtlosen Integration der Netzwerkfunktionalität. Das bedeutet, dass der Benutzer des Hybridclients zwischen der Verwendung von Netzwerkressourcen und der Verwendung lokaler Ressourcen keinen Unterschied feststellt. Das Remotesteuerungsmuster definiert den Funktionalitätstyp der Shell. Mit dem Remotesteuerungsmuster können Sie eine nahtlose Integration des Netzwerks gewährleisten, wobei es dennoch aktualisierbar bleibt. Dies wird nun in den Zusammenhang der Transaktionsanwendung übertragen. In diesem Szenario wird eine Anwendung unter Verwendung der klassischen Windows-APIs (Application
Definieren des Hybridclients
431
Programming Interfaces) mit einigen Steuerelementen geschrieben. Um diesen Client zu verteilen, wird ein Setupprogramm geschrieben, das auf den Computern ausgeführt wird, auf denen die Funktionalität benötigt wird. Dieser Verteilungsund Installationsvorgang kostet Zeit und Geld. Mit dem Remotesteuerungsmuster besteht die Lösung darin, eine Anwendung zur Datennavigation bereitzustellen; der Inhalt zur Datenbearbeitung wird jedoch von einer Ressource zur Verfügung gestellt, die einfacher zu verteilen ist. Im vorliegenden Beispiel werden die Datenänderungen über die in den Kapiteln 6 und 7 beschriebenen Techniken vorgenommen. Die Anwendung zur Datennavigation ist eine einfachere Anwendung, die keine ständige Aktualisierung zum Korrigieren von Fehlern erfordert. Für die Datenänderungen werden andererseits regelmäßige Patches und Aktualisierungen benötigt, die die Änderungen im Geschäftsprozess wiedergeben. Die Anwendung zur Datennavigation muss noch immer verteilt werden, jedoch weniger häufig als die aktualisierten Versionen anderer Anwendungen.
14.2 Entwickeln eines Unternehmensdesktop In jedem Unternehmen gibt es einen unternehmensspezifischen Computerdesktop. Der Desktop ist die Gruppierung von Unternehmensanwendungen, die zum Ausführen der täglichen Vorgänge verwendet werden. Im Falle einer Bank könnte es sich dabei beispielsweise um Anwendungen zur Verarbeitung von Hypotheken oder Sparguthaben oder zur Aktienverwaltung handeln. In einer Produktionsumgebung gibt es z.B. Anwendungen zur Produktivität, Rechnungsstellung und Kundenverwaltung. Nehmen Sie das folgende einfache Beispiel für die Erstellung einer Computerdesktopanwendung für eine typische Softwarefirma. Ein schnell verfasster Domänenmodelltext könnte folgendermaßen lauten: Der benutzerdefinierte Softwaredesktop wird zur Steuerung aller Aspekte des benutzerdefinierten Softwareentwicklungsprozesses eingesetzt. Der Desktop enthält Anwendungen zur Verwaltung von Projektdateien, Projekt-Zeiterfassungskarten, Softwarefehlern, Versionssteuerung und Verwaltungsprogrammen.
14.2.1 Erstellen der Shell Zunächst wird die Shell erstellt, die dem Remotesteuerungsmuster entspricht, das die Datennavigation der benutzerdefinierten Softwareanwendungen bereitstellt. Die Komponenten, die navigiert werden sollen, sind die Verwaltung von ProjektZeiterfassungskarten, die Verwaltung von Softwarefehlern und Nachrichtenanwendungen.
432
Erstellen des Hybridclients
Zur Darstellung und Navigation der verschiedenen Anwendungen in der Anwendungsschnittstelle der Shell könnten drei verschiedene Schaltflächen auf einer Symbolleiste verwendet werden. Das Problem ist jedoch, dass Schaltflächen auf einer Symbolleiste nicht dynamisch sind. Eine Datennavigationsshell muss sich anpassen können, wenn neue Anwendungen zur Datenbearbeitung verfügbar werden. Der Benutzer soll nicht die verschiedenen Anwendungen zur Datenbearbeitung selbst installieren müssen. Diese Art der Navigation der verschiedenen Anwendungen ist zwar falsch, dennoch ist sie die Navigationstechnik, die am häufigsten eingesetzt wird. Die Arbeit eines Computerbenutzers ist gemäß einer Reihe von Aufgaben organisiert, und die Desktopsoftware muss ähnlich organisiert werden. Die Anwendungen zur Datennavigation und zur Datenbearbeitung müssen den Aufgaben entsprechen, die der Benutzer durchführen möchte. Die Aufgaben selbst können in einzelne Portfolios (vergleichbar mit den Fächern einer Mappe) gegliedert werden, die sowohl auf der Server- als auch auf der Clientseite zum Strukturieren der Arbeit des Benutzers angewendet werden können. Eine weitere Funktion, die das Framework zur Datennavigation anbieten muss, ist die Möglichkeit zum Anlegen von Notizen. Notizen sind hilfreich, um spontane Gedanken festzuhalten. Diese Notizen sollten nicht auf dem Server gespeichert werden, da es sich um Softdaten handelt, die sich täglich ändern. Betrachten Sie nun die Optionen zum Erstellen einer Datennavigationsshell. Die erste Option besteht darin, die Hauptanwendung zur Datennavigation auf einen Server zu verschieben und die clientseitige Anwendung auf die Informationen auf dem Server verweisen zu lassen. Bei einer solchen Implementierung könnten HTML-Rahmen eingesetzt werden, mit einem Navigationsrahmen, einem Bannerrahmen usw. Das Problem bei dieser Lösung liegt jedoch darin, dass eine ständige Verbindung mit dem Server erforderlich ist, und HTML-Rahmen sind in ihrer Funktionalität einschränkend. Es ist beispielsweise nicht einfach, mehrere verschiedene Ansichten derselben Daten zu erstellen. Die zweite Option zum Erstellen einer Datennavigationsshell ist, eine MDI-Anwendung (Multiple Document Interface) zu erstellen und anschließend die verschiedenen Navigationsschritte durch die Datennavigationsshell überwachen zu lassen. Mit einer MDI-Shellanwendung ist es möglich, dieselben Dokumentdaten zweimal zu öffnen, jede Ansicht der Daten ist jedoch anders. Um diese Lösung umzusetzen, erstellen Sie eine Windows-MDI-Anwendung, die das WebbrowserCOM-Steuerelement in mehreren untergeordneten Fenstern verwaltet.
Entwickeln eines Unternehmensdesktop
433
Erstellen des Anwendungsmenüs Unabhängig von der Darstellung des Portfolios oder der Anwendungen zur Datenbearbeitung muss ein Anwendungsmenü definiert werden. Das Anwendungsmenü ist nicht mit einem Windows-Anwendungsmenü zu verwechseln. Das Anwendungsmenü ist eine HTML-Seite mit Verknüpfungen mit den verschiedenen Anwendungen zur Datenbearbeitung. Die einfachste Möglichkeit zur Erstellung dieser Anwendung besteht darin, dass eine XML-Datei heruntergeladen und analysiert wird, die eine Verknüpfung mit einer HTML-Datei enthält. Die XML-Datei ist ein Datenrepository, das andere Verweise als die HTML-Datei enthält. Zunächst ist jedoch nur die HTML-Datei interessant. Die HTML-Datei verweist auf einen oder mehrere Verweise auf Anwendungen zur Datenbearbeitung. Diese HTML-Datei wird heruntergeladen, für einen schnelleren Zugriff auf der lokalen Festplatte gespeichert und zu Anzeigezwecken in das Webbrowser-COM-Steuerelement geladen. Wenn auf eine der Verknüpfungen in der Verweis-HTML-Datei geklickt wird, wird ein weiteres untergeordnetes MDIFenster geöffnet und der Inhalt darin angezeigt. Die wichtigste Komponente innerhalb der Datennavigationsshell ist das Webbrowser-COM-Steuerelement selbst. Es stellt einen zentralen Bestandteil der Hybridclientanwendung dar. Die Einzelheiten zur Referenzierung des WebbrowserCOM-Steuerelements in der Programmierumgebung werden an dieser Stelle nicht näher behandelt. Die DLL SHDOCVW.DLL enthält alle nötigen Informationen zu COM-Typbibliotheken. In Verweisen auf die COM-Typbibliothek wird diese als Microsoft Internet Controls bezeichnet. Integrieren des Webbrowser-COM-Steuerelements Auch wenn es sich beim Webbrowser-COM-Steuerelement um einen Dokumentbrowser handelt, ist es nach wie vor ein COM-Steuerelement, und jedes COM-Steuerelement besitzt Methoden und Eigenschaften. Im Fall des Webbrowser-COM-Steuerelements gibt es Methoden, beispielsweise GoBack, GoForward, Search und Refresh2, die mit deren Browsergegenstücken identisch sind. Einige der Methoden ermöglichen die Interaktion zwischen der Hostanwendung des Webbrowser-COM-Steuerelements und dem Webbrowser-COM-Steuerelement. Es gibt außerdem andere Eigenschaften, beispielsweise WebBrowser.busy. Diese macht darauf aufmerksam, dass das Webbrowser-COM-Steuerelement mit anderen Aufgaben beschäftigt ist und nicht gestört werden sollte. Beim Webbrowser-COM-Steuerelement handelt es sich um ein COM-Steuerelement, bei dem alles abgefragt werden kann und sämtliche Statusinformationen geladen werden können. Das Webbrowser-COM-Steuerelement kann auch die
434
Erstellen des Hybridclients
Hostanwendung informieren, wenn sich ein Status geändert hat. Dies ist von Bedeutung, da das Laden eines bestimmten Dokuments einen unbestimmten Zeitraum beanspruchen kann. Es gibt ein Ereignis, das darauf hinweist, dass das Webbrowsersteuerelement im Begriff ist, Dokumente zu ändern (WebBrowser.BeforeNavigate). Außerdem gibt es ein Ereignis, das anzeigt, dass das Dokument heruntergeladen wurde (WebBrowser.NavigateComplete). Alle diese Ereignisse können an einzelne HTML-Rahmen oder generische Inhaltsdateien, z.B. ASCIIText, umgeleitet werden. Die Typbibliothek Microsoft Internet Controls enthält zwei Versionen des Webbrowser-COM-Steuerelements: WebBrowser und WebBrowser_V1. Der Unterschied besteht darin, dass WebBrowser_V1 eine ältere Implementierung mit weniger Ereignissen und Methoden ist. Das WebBrowser-Objekt ist die aktuelle Version, die in der Dokumentation zu Microsoft Internet Explorer beschrieben wird. In der Navigationsarchitektur des Hybridclients muss das XML heruntergeladen werden. Das Webbrowsersteuerelement wird in das Visual Basic-Formular platziert, und die XML-Datei wird anschließend von einem Webserver heruntergeladen. Dies ist mit Hilfe des folgenden Programmiercodes möglich: Private Sub Form_Load() WebBrowser1.Navigate2 "http://bacchus/desktop/applications.asp" End Sub
Wenn das Webbrowser-COM-Steuerelement in ein Visual Basic-Formular platziert wird, wird das erste Steuerelement dieses Typs Webbrowser1 genannt. Das Navigieren zu einem bestimmten Dokument auf einer lokalen Festplatte oder einem Remotenetzwerk wird durch Verwendung der Navigate2-Methode erzielt. In diesem Fall wird das einfachste Formular verwendet, das nur die URL-Adresse (Uniform Resource Locator) angibt. Beachten Sie die Verwendung von http:// in der URL-Adresse. Dies ist sehr wichtig. Internet Explorer erfordert keine explizite http://-Angabe (wenn die Angabe fehlt, wird das HTTP-Protokoll standardmäßig eingesetzt), es ist jedoch eine gute Programmiermethode, der URL-Adresse diesen Eintrag hinzuzufügen. Beachten Sie auch, dass durch die URL-Adresse eine ASP-Seite (Active Server Page) angegeben wird. Normalerweise enthält eine ASPSeite HTML-Befehle, sie kann jedoch beliebigen Text enthalten – in diesem Fall XML. In Visual C++ besitzt die Navigate2-Methode fünf Parameter statt einen. Die übrigen vier Parameter können in Visual Basic ignoriert werden, da sie als optional gelten. Die vollständige Definition der Webbrowser.Navigate2-Methode lautet folgendermaßen: Webbrowser.Navigate2(URL, [Flags], [TargetFrameName], [PostData], [Headers])
Entwickeln eines Unternehmensdesktop
435
Die zusätzlichen Parameter werden folgendermaßen definiert: 왘 Flags definiert, wie der neue Inhalt heruntergeladen wird. In Internet Explorer
5.0 kann Inhalt auf der lokalen Festplatte zwischengespeichert oder bei jeder Anforderung heruntergeladen werden. Eine Beispieleinstellung ist navNoWriteToCache. Dies bedeutet, dass der Inhalt heruntergeladen, das Ergebnis jedoch nicht in den Cache geschrieben wird. 왘 TargetFrameName wird verwendet, um festzulegen, in welchem Rahmen der
Inhalt angezeigt werden soll. Wenn keine Rahmen vorhanden sind oder der angegebene Rahmen nicht gefunden werden kann, wird ein neues Fenster geöffnet. 왘 PostData wird verwendet, um zusätzliche Informationen zur Anforderung be-
reitzustellen. Diese Informationen können in jeder beliebigen Form vorliegen, beispielsweise als binärer Bytestream oder formularcodierte Variablen. Wichtig ist, dass der MIME-Typ (Multipurpose Internet Mail Extensions) richtig eingestellt ist und dass der Server die jeweilige Art des Inhalts während der durchgeführten Anforderung erwartet. Wenn dieser Parameter verwendet wird, wird die HTTP-Anforderung automatisch von GET in POST konvertiert. 왘 Headers ermöglichen die Angabe zusätzlicher HTTP-Header bei einer HTTP-
Anforderung. Die letzten beiden Parameter der Navigate2-Methode setzen voraus, dass die Anforderung über das HTTP-Protokoll vorgenommen wird. Ist dies nicht der Fall, werden die Parameter ignoriert. Wenn die Anforderung beispielsweise ftp://ftp.myserver.com lautet, werden die letzten beiden Parameter ignoriert, da die URL-Adresse eine FTP-Anforderung darstellt. Warum wird diese eine Methode an dieser Stelle so ausführlich behandelt? Die Antwort ist, dass die Navigate2-Methode die wichtigste Methode ist, die Sie im Hybridclient verwenden. Vermeiden von Hartcodierung Im vorhergehenden Quellcodebeispiel verwendet die Navigate2-Methode eine hartcodierte URL-Adresse. Dies ist jedoch keine gängige Programmiertaktik. Sollte es jedenfalls nicht sein. Die URL-Adresse sollte entweder eine Konstante sein, die im Programm definiert wird, oder dynamisch in die Anwendung eingelesen werden. Möglicherweise sind Sie der Ansicht, dass das dynamische Einlesen der URLAdresse der bessere Ansatz ist, ich empfehle jedoch eher das Definieren der URLAdresse als Konstante im Programm. Es ist unwahrscheinlich, dass die URLAdresse sich ändert. Wie oft hat sich beispielsweise die URL-Adresse www.microsoft.com geändert? Nicht, seitdem sie erstmals eingeführt wurde. Die Server, die die URL-Adresse repräsentieren, haben sich möglicherweise geändert, die URL-
436
Erstellen des Hybridclients
Adresse ist jedoch nach wie vor dieselbe. Dies ist ja gerade das Besondere am Web. Demzufolge ist die beste Möglichkeit, eine Architektur für dynamische Datennavigation zu erstellen, eine unternehmensweite, zentrale URL-Adresse anzulegen, die über Internettechnologien aufgelöst wird. Der Server ist dann für dynamischen Inhalt zuständig, der angibt, wo die verschiedenen Ressourcen gespeichert werden. Der Vorteil dieses Ansatzes ist, dass die Hybridclientanwendung an einem beliebigen Speicherort installiert werden kann und sich automatisch basierend auf den Einstellungen des Netzwerkservers und der lokalen Umgebung selbst konfiguriert. Navigation Nun werden einige der erörterten Techniken auf die Shellanwendung angewendet. Die Hauptaufgabe liegt in der Anzeige der HTML-Navigationsseite für die Datenanwendungen. Diese Seite enthält eine Reihe von Verknüpfungen, und wenn jemand auf eine Verknüpfung klickt, wird ein anderes untergeordnetes MDIFenster geöffnet. Standardmäßig wird dieser Vorgang von HTML nicht durchgeführt. Diese Funktionalität wird in der Shellanwendung implementiert, indem das WebBrowser.BeforeNavigate2-Ereignis implementiert wird. Eine Beispielimplementierung lautet folgendermaßen: Private Sub WebBrowser1_BeforeNavigate2(ByVal pDisp As Object, URL As Variant, Flags As Variant, TargetFrameName As Variant, PostData As Variant, Headers As Variant, Cancel As Boolean) If redirect = True Then Dim tmpFrm As Object Set tmpFrm = New frmBrowser tmpFrm.Visible = True tmpFrm.brwWebBrowser.Navigate URL Cancel = True End If End Sub
Bevor Sie die Details dieser Implementierung untersuchen, sehen Sie sich zunächst die verschiedenen Parameter dieses Ereignisses an. 왘 pDisp stellt das Webbrowser-COM-Steuerelement dar, das derzeit bearbeitet
wird. Wenn ein Dokument geladen wird, das mehrere Rahmen enthält, wird das BeforeNavigate2-Ereignis für jeden Rahmen aufgerufen. Der pDisp-Para-
Entwickeln eines Unternehmensdesktop
437
meter steht für den lokalen Rahmen, bei dem es sich um ein WebbrowserCOM-Steuerelement handelt. 왘 URL definiert die URL-Adresse der Ressource, die geladen werden soll. 왘 Flags wurde noch nicht definiert und sollte ignoriert werden. 왘 TargetFrameName steht für den Zeichenfolgennamen des Rahmens, der der-
zeit geladen wird. Dieser wird nur angegeben, wenn das im WebbrowserCOM-Steuerelement geladene Dokument HTML-Rahmen enthält. 왘 PostData stellt den Inhalt dar, der mit einer POST-Anforderung gesendet wird.
Ein Beispiel hierfür ist das Ausfüllen der HTML-Formularelemente. 왘 Headers steht für die einzelnen Header, die mit der HTTP-Anforderung gesen-
det werden. 왘 Cancel ist ein Flag, das festlegt, ob die URL-Adresse abgerufen wird. Dies ist
der einzige Parameter, der als Verweis gesendet und tatsächlich vom Webbrowser-COM-Steuerelement verwendet wird. Wenn das Cancel-Flag auf TRUE eingestellt wird, wird das Dokument nicht abgerufen. Bei der Einstellung FALSE wird das Dokument heruntergeladen. Betrachten Sie nun die Implementierung des BeforeNavigate2-Ereignisses. Das redirect-Flag ist erforderlich, da andernfalls alle Dokumente, einschließlich der HTML-Navigationsseite für Datenanwendungen, umgeleitet werden. Das Problem lautet folgendermaßen: Nehmen Sie an, Sie verwenden ein Formular, in dem Sie das BeforeNavigate2-Ereignis implementiert haben. Jedes Mal, wenn das Ereignis aufgerufen wird, wird der Inhalt an ein anderes Dokument umgeleitet. In diesem Fall wird durch die Ereignisimplementierung ein untergeordnetes Fenster erstellt, was bedeutet, dass das Browserfenster, das die Umleitung vornimmt, nie mit einem HTML-Dokument geladen wird. Der Zweck des redirectFlags besteht darin, den ersten Ladevorgang der HTML-Navigationsseite für die Datenanwendungen zu ermöglichen. Wenn redirect auf True eingestellt ist, wird ein neues untergeordnetes MDI-Fenster erstellt. Dieses neue untergeordnete MDI-Fenster enthält eine Instanz des Webbrowser-COM-Steuerelements, und die URL-Adresse, die aufgerufen werden soll, wird der neuen Browserfensterinstanz zugeordnet (tmpFrm.brwWebBrowser.Navigate). Schließlich wird durch Einstellen des Cancel-Parameters auf True die Navigation im aktuellen Fenster beendet. Wenn Sie Ihre Anwendung in Visual Basic entwickeln und frmBrowser und frmNavigation im Hauptfenster über [form].visible = True gestartet werden, wird das Ereignis BeforeNavigate2 von den beiden Formularen gemeinsam verwendet. Dies ist jedoch nicht wünschenswert. Alle Formulare sollten über das Schlüsselwort new instanziiert werden, damit dieses Problem vermieden wird.
438
Erstellen des Hybridclients
Erstellen eines Portfolios Nachdem es nun möglich ist, zu den einzelnen Anwendungen zur Datenbearbeitung zu navigieren und mit diesen zu arbeiten, soll jetzt sichergestellt werden, dass Benutzer ihre Arbeit verfolgen können. Dies wird durch Verwalten eines lokal gespeicherten Portfolios mit den derzeit verwendeten Anwendungen erreicht. Die Herausforderung bei der Erstellung des Portfolios liegt nicht in der Verwaltung der einzelnen Datenteile und im Schreiben dieser Teile in eine Datei. Die Herausforderung besteht vielmehr darin, ein Dateiformat zu schreiben, das von jedem beliebigen Tool und jeder beliebigen Umgebung gemeinsam verwendet werden kann. An dieser Stelle wird nicht im Detail erläutert, wie die Schaltflächen zum Erstellen des Portfolios geschrieben werden. Diese Informationen finden Sie im beiliegenden Quellcode. Stattdessen wird der Mechanismus zum Speichern der Portfolioinformationen ausgeführt. Definieren der XML-Persistenz Die Aufgabe liegt darin, den Inhalt des Portfolios in einer XML-Datei dauerhaft zu speichern. Als erster Schritt wird das Objektmodell erstellt, das das Portfolio repräsentiert. Das UML-Klassendiagramm (Unified Modeling Language) wird in Abbildung 14.2 gezeigt. Das portfolio-Objekt ist das Stammobjekt. Es ist für die Verwaltung des gesamten Portfolios zuständig.
Abbildung 14.2 Portfolioklasse
Innerhalb eines Portfolios können Sie Ordner definieren, aus denen sich ein Desktop mit verschiedenen URL-Adressen zusammensetzt. Der Ordner ist das Objekt clsFolder. Da es innerhalb eines Portfolios mehrere clsFolder-Objekte gibt, werden diese in einer Auflistung clsFolders zusammengefasst. Das clsFolder-Objekt enthält mehrere URL-Adressen, die beim Laden des Ordners abgerufen werden. Eine URL-Adresse stellt ein Browserfenster dar, und es gibt verschiedene Attribute, beispielsweise Fensterabmessungen, die beachtet werden müssen. Diese Informationen werden in eine Klasse namens clsItem gruppiert. Die clsItemsAuflistung enthält clsItem-Objekte.
Entwickeln eines Unternehmensdesktop
439
Ein Beispiel für eine XML-Ausgabe lautet folgendermaßen:
In diesem Beispiel sehen Sie den Stammknoten namens portfolio. Darin enthalten ist ein XML-Knoten namens folders mit einem Attribut count, das auf die Anzahl der vorhandenen folder-XML-Unterknoten hinweist. Im XML-Knoten folder gibt es einen XML-Knoten items. Dieser dient demselben Zweck wie der Knoten folders, und auch er verfügt über ein count-Attribut. Schließlich ist der Knoten item aufgeführt. Bei diesem Objektmodell handelt es sich um eine direkte Kopie des UML-Objektmodells, und auf den ersten Blick sieht es nach einem guten Ansatz aus, da man normalerweise in einem Dateiformat nicht weiß, wie viele Elemente gespeichert werden. In XML ist es nicht nötig, Grenzen zu definieren, die explizit festlegen, wo ein Knoten endet und ein anderer beginnt. Der Grund hierfür ist, dass es sich bei XML um eine Ad-hoc-Auflistung verschiedener Elemente handeln kann. Die XML-Tags definieren die Grenzen der Auflistung. Daher ist es nicht erforderlich, die Knoten folders und items hinzuzufügen. Der Attributzähler geht verloren, und es ist nicht möglich festzustellen, wie viele folder- oder item-Knoten vorhanden sind. Dies bedeutet, dass die Laderoutine die Objekte dynamisch zuordnen muss. Das stellt jedoch kein Problem dar, da gute Programmierroutinen Objekte ohnehin dynamisch zuordnen. Das Einrichten von Grenzen ist keine gute Idee und kann Fehler verursachen. Wenn die XML-Tags folders und items aus dem vorherigen XML-Dokument entfernt werden, erhalten Sie folgendes Ergebnis:
440
Erstellen des Hybridclients
Arbeiten mit dem XML-DOM Das beste Verfahren zum Schreiben des Portfolios besteht in der Verwendung von XML. Durch Einsatz von XML wird sichergestellt, dass die Daten in verschiedenen Umgebungen gemeinsam verwendet werden können. Die Verwendung von XML für die Anzeige von Daten wurde in den Kapiteln 6 und 7 erläutert. XML kann jedoch auch als Dateiformat zum Speichern von Daten für eine Anwendung eingesetzt werden. Zum Bearbeiten des XML-Dokuments gibt es ein so genanntes XML-DOM (Document Object Model). Das XML-DOM basiert auf dem Knotenkonzept. Der Stamm des XML-Dokuments ist das XML-Dokumentobjekt. Innerhalb des XML-Dokumentobjekts gibt es XML-Knoten. Diese XML-Knoten können andere Auflistungen von XML-Knoten enthalten, die wiederum weitere XML-Knoten umfassen können. Die XML-Datenhierarchie gleicht einer Verzeichnisstruktur. Um eine bestimmte Information zu erreichen, durchlaufen Sie die XML-Datenhierarchie herauf und herunter. Das Problem bei diesem Ansatz liegt darin, dass das Navigieren in der XML-Datenhierarchie explizite Verweise auf verschiedene Knoten erfordert. Mit anderen Worten: Sie müssen jeden Knoten einzeln durchsuchen. Auf diese Weise wird das Finden einer bestimmten Information zu einer langwierigen Angelegenheit. Auch das Zuordnen der einzelnen XML-Knoten zu einem bestimmten COMObjekt ist sehr ermüdend und schwierig. Serialisieren von COM-Objekten in XML-Daten Die Herausforderung beim dauerhaften Speichern des Portfolios in einer XML-Datei besteht in der Zuordnung einer Reihe von COM-Objekten zu XML-Daten. Die beste Möglichkeit hierzu ist, Helferobjekte zu erstellen, die diese Funktionalität automatisch übernehmen. Warum reicht das XML-DOM nicht aus? Wenn Sie Informationen aus der XMLDOM-Struktur extrahieren, müssen Sie die XML-DOM-Struktur ständig nach oben und unten durchlaufen, um etwas zu finden. Die Helferobjekte können an den Stellen Ankerpunkte in der XML-DOM-Struktur definieren, an denen ein COM-Objekt die lokalen XML-Knoten nach dem Status durchsuchen würde. Dies bedeutet, dass in den meisten Fällen höchstens eine Ebene der Navigation in der XML-DOMStruktur erforderlich ist, sodass der Vorgang erheblich vereinfacht wird. Wie wird dies implementiert? Die Antwort lautet, dass es sich bei den Helferobjekten um COM-Objekte handelt, die COM-Schnittstellen definieren, die die dauerhaften Objekte implementieren müssen. Die Helferobjekte nehmen die automatische Serialisierung von COM in XML vor und rufen diese COM-Schnittstellen anschließend auf. Es ist jedoch wichtig, dass diese Helferobjekte die XMLDOM verwenden, da es sich um einen Standard handelt. Jede andere Umsetzung der Lösung hätte eine eigene Lösung zur Folge, die nicht in das Gesamtbild passt.
Entwickeln eines Unternehmensdesktop
441
Eine alternative Methode ist das Entwickeln eines Assistenten, der eine XML-Beschreibung abfragt und den Quellcode zur Bearbeitung des XML-DOM automatisch generiert. Hierbei handelt es sich zwar um eine edle Absicht, das Problem ist jedoch, dass der generierte Code für ein XML-Dokument beträchtlicher Größe sehr umfassend und schwierig zu lesen und zu verwenden ist. Das Beheben von Fehlern und das Vornehmen von Änderungen erfordert daher mehr Aufwand als nötig. Der bessere Ansatz besteht also wiederum in der Entwicklung einer Reihe von Helferobjekten, die den XML-DOM-Standard abstrahieren. In Kapitel 11 wurde die Persistenz von COM-Objekten durch IPersistStream beschrieben. Diese Objekte wurden jedoch in einem eigenen Format gespeichert, das nicht mit XML kompatibel ist. Zum dauerhaften Speichern von COM-Objekten in XML wird dieselbe Persistenztechnik verwendet, die auch in Kapitel 11 erläutert wurde. Diese Technik umfasst das Definieren einer COM-Schnittstelle für XML-Persistenz, die von jedem COM-Objekt implementiert werden muss. Das XML-COM-Persistenzframework fragt anschließend jedes COM-Objekt, das seinen Status im XML-Stream speichern soll, nach der COM-Schnittstelle für XML-Persistenz ab. Die COM-Schnittstelle für XML-Persistenz erfordert zwei Methoden: save und load. Bei der Implementierung der COM-Schnittstelle für XMLPersistenz stimmt der Prozess der Speicherung von Daten und untergeordneten Objekten mit dem in Kapitel 11 ausgeführten Prozess überein. Es gibt jedoch ein Problem mit dem Laden der verschiedenen COM-Objekte, die die COM-Schnittstelle für die XML-Persistenz implementiert haben. Der Speicherprozess speichert nicht die PROG ID oder die CLSID des COM-Objekts, wie dies beim Speicherprozess in Kapitel 11 der Fall war. Man weiß daher nicht, welche Objekte zu welchem Zeitpunkt geladen werden. Die XML-Tags bieten keinen Hinweis darauf, welches Objekt mit dem Tag verknüpft ist. Die Lösung besteht darin, Behandlungsroutinen für XML-Tags bei den Helferobjekten zu registrieren. Die Behandlungsroutine verknüpft sich selbst mit einem bestimmten Tag, und beim Laden des Dokuments prüfen die Helferobjekte ständig die Listen der Behandlungsroutinen, um Übereinstimmungen festzustellen. Wenn eine Übereinstimmung vorliegt, leitet die Behandlungsroutine den Prozess der XML-Knotenanalyse an die Behandlungsroutine für XML-Tags um. Die XML-Serialisierungsschnittstelle ist eine benutzerdefinierte COM-Schnittstelle, die folgendermaßen deklariert ist: interface XMLSerialize : IUnknown { HRESULT save([in] XMLService* service); HRESULT load([in] XMLService* service); };
442
Erstellen des Hybridclients
Die save-Methode wird zum Speichern des aktuellen Status des COM-Objekts im XML-Stream verwendet. Das Gegenteil ist die load-Methode, die zum Laden des Status des COM-Objekts aus dem XML-Stream eingesetzt wird. Bei beiden Methoden wird ein Schnittstellenzeiger des Typs XMLService angegeben. Hierbei handelt es sich um eine benutzerdefinierte Schnittstelle, die zur Verwaltung des Serialisierungsprozesses dient. Erstellen von XML-Dokumenten, -Knoten und -Attributen Die COMObjekte clsPortfolio, clsFolder und clsItem müssen die COM-Schnittstelle XMLSerialize implementieren. Nehmen Sie zum Beispiel den Speichervorgang des COM-Objekts clsPortfolio. Wenn die XMLSerialize::save-Methode aufgerufen wird, serialisiert sie die Auflistung der clsFolder-COM-Objekte, die wiederum die Auflistung der clsItem-COM-Objekte serialisiert. Die Serialisierung erfolgt rekursiv, da durch Serialisierung von clsPortfolio die verschiedenen untergeordneten Objekte serialisiert werden. Auf diese Weise entsteht eine XML-Hierarchie, die die Hierarchie der COM-Objekte widerspiegelt. Diese Rekursion wird durch die XMLService-Schnittstelle verwaltet. Die COM-Schnittstelle XMLService ist für die Verwaltung des Serialisierungsvorgangs zuständig. Die Schnittstelle wird folgendermaßen definiert: interface IPXMLService : IDispatch { [id(0)] HRESULT addHandler( BSTR handler, IDispatch *item); [id(1)] HRESULT removeHandler( BSTR handler); [id(2)] HRESULT load( BSTR url); [id(3)] HRESULT save( BSTR url, IDispatch *ptrVal); [propget, id(4)] HRESULT currNode([out, retval] IDispatch **pVal); [propput, id(4)] HRESULT currNode([in] IDispatch *newVal); [propget, id(5)] HRESULT XMLDocument([out, retval] IDispatch **pVal); [propput, id(5)] HRESULT XMLDocument([in] IDispatch *newVal); };
Um diese COM-Schnittstelle nachzuvollziehen, betrachten Sie zunächst die darin enthaltene Funktionalität. Nehmen Sie an, dass IPXMLService als COM-Objekt XMLService implementiert wurde. Das COM-Objekt XMLService wird zum Laden und Speichern des XML-Dokuments verwendet. Es ist für die Verwaltung des Persistenzvorgangs von COM zu XML zuständig. Die Liste der Behandlungsroutinen ist in der Implementierung enthalten. Das COM-Objekt XMLService ist für das Einstellen des lokalen XML-Ankerknotens verantwortlich, d.h. für die Eigenschaft IPXMLService::currNode.
Entwickeln eines Unternehmensdesktop
443
Wenn eine Anwendung eine Reihe von COM-Objekten in XML speichern möchte, muss die Anwendung XMLService instanziieren. Ein Beispiel für diesen Vorgang lautet folgendermaßen: Dim objXMLService As New XMLService objXMLService.save mvarfilename, rootObject
Um der XML-Hierarchie Inhalt hinzuzufügen, wird die objXMLService.save-Methode aufgerufen. Der erste Parameter steht für den Namen der XML-Datei, in der die Objekte dauerhaft gespeichert werden sollen. Beim zweiten Parameter handelt es sich um das COM-Objekt, das das COM-Stammobjekt darstellt, das wiederum als XML-Stammobjekt eingesetzt wird. Bei der Implementierung von XMLService.save wird das DOMDocument-COMObjekt instanziiert. Dieses Objekt ist Bestandteil der Microsoft XML-Bibliothek. Wenn es instanziiert wird, erstellt es ein leeres XML-Dokument. Da es sich um einen save-Serialisierungsprozess handelt, ist das rootObject für das Hinzufügen von XML-Knoten zum XML-Dokument zuständig. Die Eigenschaft XMLServer.XMLDocument steht für das DOMDocument, das in der XMLService.saveMethode instanziiert wurde. Anschließend wird die rootObject.save-Methode aufgerufen, um ihren Status dauerhaft zu speichern. Nachfolgend sehen Sie eine Beispielimplementierung einer IPXMLSerialize.saveMethode für ein COM-Objekt: Private Sub PXMLSerializeClass_save(ByVal service As Object) Dim pService As XMLService Dim pHelper As XMLHelper Dim pNode As IXMLDOMNode Set pService = service Set pHelper = pService.XMLHelper Set pNode = pHelper.setRootElement("folder ") Set pService.currNode = pNode Dim c1 As Long pHelper.addNodeTextCombo "CollectionCount",CStr( mCol.count) For c1 = 1 To mCol.count mCol.Item( c1).save service Next End Sub
444
Erstellen des Hybridclients
Bei dieser Implementierung beginnt der Code zunächst mit der Konvertierung des Dienstparameters in einen XMLService-Datentyp. Es ist möglich, den Parameter nicht zu konvertieren. In diesem Fall müssten Sie sich auf die COM-Schnittstelle IDispatch verlassen, wodurch der gesamte Persistenzvorgang verlangsamt wird. Die Variable pHelper steht für ein XML-Helferobjekt, das bestimmte Vorgänge einkapselt, beispielsweise das Hinzufügen untergeordneter XML-Knoten. Wenn ein leeres XML-Dokument erstellt wird, gibt es kein Stammelement. Daher ist es bei der Serialisierung des ersten COM-Objekts notwendig, ein XML-Stammelement anzulegen. Für die Erstellung des XML-Stammknotens ist das serialisierte COM-Objekt zuständig. Das XML-Helferobjekt besitzt die setRootElement-Methode zum Erstellen eines Stammknotens mit dem XML-Tag gemäß der Definition des folder-Parameters. Die mCol-Auflistung muss in den XML-Stream serialisiert werden. Sie ist mit der in Kapitel 11 beschriebenen Auflistung identisch, und die Serialisierung der Auflistung in einen XML-Stream stimmt mit der Serialisierung der Auflistung in einen MSMQ-Nachrichtenstream (Microsoft Message Queue) überein. Der einzige Unterschied besteht darin, dass anstelle der PropertyBag zum Schreiben in die MSMQ-Nachricht an dieser Stelle die XMLSerialize.Save-Methode aufgerufen wird. Die Instanziierung der XML-Knoten ist in der XML-Helferklasse eingekapselt. Im XML-DOM ist es nicht möglich, XML-Elemente durch Instanziieren des COMObjekts für XML-Knoten zu erstellen. Das DOMDocument verfügt über Methoden zur Erstellung der XML-Knoten, beispielsweise Kommentare, Attribute und Elemente. Für Sie ist die Erstellung von Knoten von Interesse. Dies erfolgt mit Hilfe der DOMDocument::createNode-Methode, wie im folgenden Beispiel verdeutlicht wird: Dim obj As IXMLDOMNode Set obj = mvarXMLDocument.createNode(NODE_ELEMENT, tagName, "")
Durch den ersten Parameter wird der Typ des erstellten Knotens festgelegt. In diesem Fall handelt es sich um ein NODE_ELEMENT, das in XML folgendermaßen lauten könnte:
Der Knoten könnte jedoch auch NODE_TEXT sein, der in XML folgendermaßen dargestellt wird: beliebiger Text
Entwickeln eines Unternehmensdesktop
445
Außerdem kann es sich bei dem Knoten um einen beliebigen anderen gültigen XML-Knotentyp handeln, beispielsweise Deklarationen von Einheiten, Kommentare, einen CDATA-Abschnitt usw. Der zweite Parameter definiert den Bezeichner des Tags, der mit der Eigenschaft nodeName übereinstimmt. Wenn der Wert von tagName ordner lautet, würde im vorhergehenden Beispiel Folgendes generiert:
Der letzte Parameter ist eine Textzeichenfolge, die den Namespace angibt, in dem der Bezeichner des Tags definiert ist. Dieser Parameter ist optional. Wenn er nicht verwendet wird, geht der Parser davon aus, dass der Bezeichner in der lokalen DTD (Document-Type Declaration) festgelegt ist, falls eine solche existiert. Nach einer erfolgreichen Erstellung wird der neue Code, eine COM-Schnittstelle von IXMLDOMNode, zurückgegeben. Bei der zurückgegebenen Variablen obj handelt es sich um einen IXMLDOMNode-COM-Schnittstellenzeiger. Sie ist nicht Teil des XML-Dokuments und muss diesem hinzugefügt werden. Sehen Sie sich nun das folgende XML-Fragment an: beliebiger Text
Um dieses Fragment zu erstellen, müssen Sie zwei XML-Knoten erstellen. Der erste ist der XML-Knoten, der das XML-Tag ELEMENT darstellt. Dieser wird im Folgenden als übergeordneter XML-Knoten bezeichnet. Das Attribut attribute= value ist ein Teil der XMLDOMNode.attributes-Auflistung. Dieses XML-Tag entspricht dem Typ NODE_ELEMENT. Um den XML-Inhalt beliebiger Text zu erstellen, muss ein weiterer XML-Knoten angelegt werden, diesmal vom Typ NODE_TEXT. Dieser XML-Knoten wird der Auflistung untergeordneter XML-Knoten des übergeordneten XML-Knotens hinzugefügt, der im vorherigen Absatz mit Hilfe der XMLDOMNode.appendChildMethode erstellt wurde. Mit Hilfe der insertBefore-Methode ist es außerdem möglich, einen untergeordneten Knoten an einem bestimmten Speicherort einzufügen. Diese Methode fügt einen Knoten vor einem bestimmten Knoten in der Auflistung untergeordneter Knoten ein. Attribute können einem XML-Knoten auf zwei Arten hinzugefügt werden. Das kompliziertere Verfahren besteht darin, DOMDocument::createAttribute aufzurufen und anschließend die verschiedenen Werte des Attributs einzustellen. Dieses Objekt wird anschließend der Attributauflistung des Knotens hinzugefügt.
446
Erstellen des Hybridclients
Eine einfachere Möglichkeit ist, einen QueryInterface-Vorgang für die IXMLDOMElement-Schnittstelle durchzuführen und anschließend die setAttributeMethode dem folgenden Beispiel entsprechend zu verwenden: Dim obj As IXMLDOMNode Dim obj2 As IXMLDOMElement … Set obj2 = obj obj.setAttribute "attribute", "value"
Sobald die Serialisierung abgeschlossen ist, speichert die XMLService.Save-Methode den Inhalt mit Hilfe der DOMDocument.save-Methode in einer Datei.
14.2.2 Erstellen des Inhalts Die HTML-Seiten zur Datenbearbeitung werden dynamisch auf Anforderung von einem Webserver heruntergeladen. Bis jetzt fanden die Vorgänge zur Datenbearbeitung auf der Serverseite statt, auch wenn einige clientseitige Techniken, beispielsweise JavaScript und XSL (Extensible Style Language), verwendet wurden. Jetzt soll jedoch eine sehr umfassende Funktionalität erzielt werden, die das Speichern von Statusinformationen in einem lokal gespeicherten Portfolio ermöglicht. Die entsprechende Vorgehensweise besteht darin, ein benutzerdefiniertes COMSteuerelement, auch bekannt als ActiveX-Steuerelement, zu erstellen. Erweitern des Datenbearbeitungsvorgangs ActiveX-Steuerelemente arbeiten mit Microsoft Internet Explorer zusammen und können in eine HTML-Seite eingefügt werden. Das ActiveX-Steuerelement wird, wie die HTML-Seite, nach Bedarf verteilt. Wenn der Benutzer nicht über das ActiveX-Steuerelement verfügt, wird es automatisch heruntergeladen und installiert. Die Installation eines ActiveX-Steuerelements im Kontext einer HTML-Seite ist ein einfacherer und kleinerer Installationsprozess als die Installation einer vollständigen Windows-Anwendung. Da etwas Programmiercode in das ActiveX-Steuerelement integriert werden kann, müssen Sie sich für Kriterien entscheiden, die zum Festlegen des hinzuzufügenden Programmiercodes verwendet werden sollen. Hierzu sollte das Muster zum Trennen von Format und Programmiercode eingesetzt werden. Dieses Muster beantwortet die Frage, wie der Inhalt auf der Clientseite strukturiert werden soll. Es müssen sehr granulare COM-Objekte erstellt werden, die dynamisch über clientseitige HTML-Skripts miteinander verknüpft werden können. Die verschiedenen COM-Objekte kennen die COM-Schnittstellen der anderen COM-Objekte, und
Entwickeln eines Unternehmensdesktop
447
die HTML-Skripts verbinden die verschiedenen COM-Implementierungen untereinander. Wenden Sie sich nun den physischen Eigenschaften von ActiveX-Steuerelementen zu. Sie ähneln COM-Objekten mit der Ausnahme, dass sie über eine GUI (Graphical User Interface), also eine grafische Benutzeroberfläche, verfügen. Sie unterstützen auch die Vorstellung von COM-Schnittstellen und COM-Implementierungen. Wenn Sie mit ActiveX-Steuerelementen arbeiten und sie mit Hilfe von Visual Basic oder Visual C++ entwickeln, stellen Sie sicher, dass sie auf dem richtigen Prozessor ausgeführt werden. Visual J++ ist anders, da hierfür nur erforderlich ist, dass die Microsoft Java Virtual Machine (VM) auf der Clientseite vorhanden ist. Es gibt auch Implementierungen der Microsoft VM auf UNIX- und Macintosh-Plattformen. Kompilierte ActiveX-Steuerelemente besitzen die Fähigkeit, dem Computer ernsthaft zu schaden, indem sie beispielsweise eine Festplatte formatieren. Möglicherweise sind Sie der Ansicht, dass ActiveX-Steuerelemente aufgrund des Mangels an Einschränkungen und an plattformübergreifender Funktionalität möglichst vermieden werden sollten. Viele würden Ihnen beipflichten. Es gibt jedoch geeignete Einsatzgebiete für ActiveX-Steuerelemente: 왘 Wenn eine bestimmte Funktionalität mit Hilfe von DHTML, XML und XSL nicht
erzielt werden kann, beispielsweise Kurvenzeichnung, mathematische Berechnungen und einige Typen von Programmiercode 왘 Wenn die Leistung von Skripts zu langsam ist 왘 Wenn Sie zum Verbessern der Wirkung auf den Endbenutzer besondere Ef-
fekte benötigen, die ActiveX-Steuerelemente verwenden 왘 Wenn die Netzwerkbandbreite effizienter genutzt werden soll, indem ein Teil
der serverseitigen Verarbeitung auf die Clientseite verlagert wird 왘 Wenn ein anderes Clientprotokoll in die Webanwendung integriert werden soll
In der Desktopanwendung muss kontrolliert werden, wie viel Zeit auf die verschiedenen Projekte verwendet wurde. Ein ActiveX-Steuerelement auf dem Client ist nützlich, da es ermöglicht, die Daten zu sammeln und sie zu einem späteren Zeitpunkt für den Persistenzvorgang an den Server zu senden. Um die verschiedenen Datenteile auf der Clientseite anzuzeigen, wird ein Recordset erstellt. Dieses Steuerelement führt zwei neue Konzepte ein: die Dynamic HTML-Datenbindung und die Interaktion mit dem Dynamic HTML-Objektmodell.
448
Erstellen des Hybridclients
Verwenden der Dynamic HTML-Datenbindung Mit der Einführung von Internet Explorer 5.0 ist es möglich, XML als Datenquelle zu definieren und es anschließend dynamisch an die HTML-Seite zu binden. Dies wird als Dynamic HTML-Datenbindung bezeichnet. Die DHTML-Datenbindung ermöglicht es, eine Reihe von Daten auf der Clientseite zu speichern und wie ein Recordset zu durchlaufen. Die DHTML-Datenbindung funktioniert folgendermaßen. Eine HTML-Seite wird heruntergeladen. Auf dieser Seite befindet sich ein DSO-Verweis (Data Source Object) auf ein Recordset. Dieser Verweis könnte eine URL-Adresse oder Daten auf der Seite sein. Wenn das DSO geladen wird, verbindet es die Daten mit sich selbst. Anschließend werden auf der HTML-Seite mit Hilfe von Skripts Verweise auf das DSO erstellt. Diese Verweise durchlaufen das Recordset oder zeigen die Daten an. Wichtig ist die Interaktion mit dem DSO-Recordset. Die Verwendung eines DSO ist eine sehr leistungsstarke Technik, da sie das Konzept von Daten in ein generisches Recordset abstrahiert. Da es sehr einfach ist, ein benutzerdefiniertes DSO zu schreiben, gibt es keine Lernkurve für HTML-Designer. Sie müssen lediglich das Konzept des Recordsets verstehen. Die Beispielanwendung, die nun erstellt wird, ist eine Zeiterfassungsanwendung. Die Zeiterfassungsanwendung ermöglicht das Anmelden und Abmelden bei einer Aufgabe, beispielsweise bei einem Objekt oder auch bei Urlaubstagen. Der Grund dafür, dass die Wahl auf eine Zeiterfassungsanwendung gefallen ist, liegt darin, dass sie sowohl serverseitige als auch clientseitige Verarbeitungsvorgänge umfasst. Außerdem besteht die effektivste Möglichkeit zum Anzeigen der verschiedenen Zeiterfassungskarten darin, ein DSO zu verwenden und die Karten wie Recordsets zu bearbeiten. Ein Beispiel für die Verwendung des XML-DSO Um nachzuvollziehen, wie ein DSO funktioniert, stellen Sie sich eine einfache Zeiterfassungsanwendung vor, die nur Daten anzeigt und durchsucht. Der hierzu erforderliche Quellcode lautet folgendermaßen:
Entwickeln eines Unternehmensdesktop
449
Root Project Other Project Another Project < Project >Some more Projects
Project name:
Im oben genannten HTML-Quellcode gibt es eine XML-Insel, die einige Daten repräsentiert. In diesem Beispiel werden die Daten direkt in die Webseite eingebettet, die durch das XML-Tag umschlossen wird. Internet Explorer 5.0 erkennt dies als XML-Datenfragment. Bei dem Datenfragment handelt es sich um eine sich wiederholende Reihe von Projektnamen (). Wenn Internet Explorer 5.0 das XML-Datenfragment lädt, wird das XML-DSO geladen, und das XML-DSO lädt anschließend die XML-Datenfragmente und konvertiert sie in ein Recordset. Für den Zugriff auf das Recordset müssen besondere HTML-Attribute verwendet werden. Dies sind DSO-Attribute, die die verschiedenen HTML-Elemente an das Recordset binden. Im vorangegangenen Quellcode bindet das HTML-Tag seinen Wert an das Recordset xmlSample und das Feld ID. Die DHTML-Datenbindungsattribute lauten folgendermaßen: 왘 DATASRC: Verweis auf das DSO, das die Daten bereitstellt 왘 DATAFLD: Die Spalte oder das Feld im DSO-Recordset, die bzw. das geladen
werden soll
450
Erstellen des Hybridclients
왘 DATAFORMATAS: Das Format, in dem DATAFLD dargestellt werden soll, falls
dieses Attribut verwendet wird 왘 DATAPAGESIZE: Die Anzahl von Einträgen, die auf der Webseite in einer Ta-
bellenanzeige dargestellt werden sollen Die Elemente, die auf diese Weise gebunden werden können, sind sämtliche Formularelemente, die Objektelemente sowie IMG, FRAME, IFRAME, DIV, SPAN und LABEL Um das Recordset zu durchlaufen, wird der Cursor durch Skripts vor oder zurück bewegt (beispielsweise durch xmlSample.recordset.movePrevious). Das durch das DSO dargestellte Objektmodell ist das ADO-Objektmodell (Active Data Objects). (Das DSO ist ein Beispiel für die Verwendung der OLE DB-Schnittstelle für einfache Provider.) Wenn das XML-DSO die Daten konsumiert, ist die Größe festgelegt. Es ist somit möglich, die Eigenschaft RecordCount zum Kennzeichnen des letzten Eintrags zu verwenden. Beim Vorwärts- und Rückwärtsnavigieren gibt die Eigenschaft AbsolutePosition an, welcher Eintrag derzeit angezeigt wird. Generieren der Daten Es ist möglich, mit Hilfe einer URL-Adresse auf die DSO-Daten zu verweisen. Der entsprechende Code lautet folgendermaßen:
Verweise auf XML-URLs in einer Dateninsel besitzen dieselben Abrufregeln wie Verweise auf HTML-Seiten. Im vorher genannten Beispiel bedeutet dies, dass die Datei projects.xml in dem Verzeichnis oder in dem virtuellen Verzeichnis vorhanden sein muss, von dem die HTML-Seite abgerufen wird. Es ist außerdem möglich, den XML-Inhalt mit Hilfe von ASP dynamisch zu generieren, indem projects.asp anstelle von projects.xml eingesetzt wird. Wenn Sie eine Datei projects.asp erstellen, muss diese einen Textstream in etwa folgender Form generieren: … …
Es ist möglich, komplexere XML-Dokumente mit weiteren Hierarchien zu erstellen, dies ginge jedoch über den Rahmen dieses Buches hinaus. Im Microsoft Plattform-SDK (Software Developer’s Kit) finden Sie in den Verzeichnissen mit XMLQuellcode einige Beispiele.
Entwickeln eines Unternehmensdesktop
451
Im XML-Dokumentfragment müssen die XML-Tagnamen ROOT und RECORD nicht unbedingt verwendet werden. Sie können durch eine Bezeichnung ersetzt werden, die eine bessere Beschreibung bietet. Anstelle des Tagnamens RECORD könnte der Tagname PROJECT verwendet werden, die erstellte Hierarchie muss jedoch beibehalten werden. Das Tag definiert den Anfang eines Recordsets. Innerhalb des Recordsets gibt es eine Reihe von Einträgen, und innerhalb eines Eintrags eine Reihe von Feldern. Der Name des Feldes ist sehr wichtig, da dieser Name in Verweisen durch das HTML-Tagattribut verwendet wird. Eine Beispiel implementierung von projects.asp in einer Datenbankabfrage lautet folgendermaßen:
452
Erstellen des Hybridclients
Diese Seite generiert dasselbe XML-Format, das auch das XML-Datenfragment besitzt. Beachten Sie jedoch, dass möglicherweise einige zusätzliche Tags generiert werden, wenn Sie die Visual InterDev-Skriptobjektbibliothek verwenden. In diesem Fall müssen Sie den Quellcode der Skriptobjektbibliothek trimmen. Normalerweise bedeutet dies, dass Sie DTC (Design-Time Controls) nicht verwenden können. Die vorherigen Beispiele zeigen, dass ASP keinen HTML-Text generieren muss. ASP kann XML-Text oder beliebigen anderen Text erzeugen, der den Vorstellungen des Entwicklers entsprechend formatiert ist. Anzeigen einer Seite mit Daten Es ist möglich, die Daten als Gruppe zu durchsuchen. Bei der Gruppe kann es sich um das gesamte Recordset oder um Datenseiten einer bestimmten Größe handeln. Indem auf das DSO über das HTML-Tag
verwiesen wird, werden die Daten als Gruppe durchlaufen. Die Tabelle ähnelt einer Schleife, die Zeilen der Definition gemäß generiert. Das folgende Beispiel zeigt zwei Dinge: eine seitenbezogene Tabellengröße und die Möglichkeit zum Sortieren von Einträgen. Paged example function forward() { tbl.nextPage(); } function reverse() { tbl.previousPage(); } function description_onclick() { xmlSample.Sort = "description"; xmlSample.Reset(); } function id_onclick() { xmlSample.Sort = "id"; xmlSample.Reset(); }
Entwickeln eines Unternehmensdesktop
453
Id
Description
Die Datenseite wird unter Verwendung einer Tabelle erstellt. Dem Tag
muss das Attribut DATASRC zugewiesen werden. Auf diese Weise wird Internet Explorer darauf hingewiesen, dass mehrere Zeilen generiert werden müssen. Um die Anzahl der Einträge anzugeben, wird auch das Attribut DATAPAGESIZE im Tag
zugewiesen. Wird dies nicht angegeben, werden alle Einträge angezeigt. Innerhalb des Tabellenhauptteils wird eine Vorlage mit einer Prototypzeile mit den gewünschten Feldern definiert. Diese Vorlage wird mit Daten aus dem DSO gefüllt, und dieser Vorgang wird für jeden DSO-Eintrag wiederholt. Die Vorlage wird so oft in die Tabelle eingefügt, wie durch das Attribut DATAPAGESIZE festgelegt wurde. Unterliegen Sie daher nicht dem Irrtum, dass das Attribut die Anzahl der Zeilen in einer Tabelle darstellt. In diesem Fall gibt es zwei -Tags mit DATAFLD-Attributen innerhalb einer Zeile (
). Dies ist nicht das einzig gültige Szenario. Es könnten auch zwei oder drei oder n Zeilen vorliegen. Um zwischen den verschiedenen Seiten von Einträgen in der Tabelle zu wechseln, stehen zusätzliche Methoden für die Navigation zur Verfügung: tbl.nextPage und
454
Erstellen des Hybridclients
tbl.previousPage. Die Neuerstellung der Tabelle wird automatisch von Internet Explorer übernommen. Für die DSOs, die dies unterstützen, können die Einträge mit Hilfe von xmlSample.Sort sortiert werden. Dies führt zu einem weiteren Aspekt von DSOs. Das XML-DSO verfügt nur über die Fähigkeit, schreibgeschützte Daten zu bearbeiten. Ein weiteres DSO namens RDS (Remote Data Service) ermöglicht die Aktualisierung von Daten. Das DSO, das an dieser Stelle entwickelt wird, besitzt die Möglichkeit zum Aktualisieren von Daten mit Hilfe von Transaktionen. Erstellen der clientseitigen Anwendungslogik Die Zeiterfassungsanwendung enthält in gewissem Umfang clientseitige Anwendungslogik. Sie sammelt die verschiedenen Informationen der Zeiterfassungskarten, zeigt diese an und sendet einige gegebenenfalls zur dauerhaften Speicherung an den Server. Diese clientseitige Anwendungslogik nutzt zwei Teile der COMTechnologie: die DHTML-Datenbindung und die DHTML-Integration, auf die später eingegangen wird. Das Erstellen clientseitiger Anwendungslogik ähnelt stark dem Erstellen serverseitiger Anwendungslogik. Schnittstellen, die die Anwendungslogik repräsentieren, müssen definiert werden. Im Anschluss daran müssen außerdem einige COM-Objekte definiert werden, die diese COM-Schnittstellen implementieren. Bei dem aufgeführten Quellcode handelt es sich um Visual C++, dasselbe Ergebnis kann jedoch mit Hilfe von Visual Basic oder Visual J++ erzielt werden. Im Microsoft Plattform-SDK im Verzeichnis \Samples\Web\Author\DataSrc\arraycontrol befindet sich ein Beispiel für ein Visual Basic-DSO. Definieren der Schnittstellen Wie lautet der Geschäftsprozess in diesem Projekt? Die Verwaltung der Zeit und der Kosten, die mit jedem einzelnen Projekt verbunden sind. Welche Vorgänge sind erforderlich? Die Anwendungsfälle weisen darauf hin, dass es sich hierbei zum einen um die Möglichkeit zum An- und Abmelden bei einem bestimmten Projekt handelt. Zum anderen sollte die Zeiterfassungskarte auch die Möglichkeit zum An- und Abmelden von Urlaubs- und Krankheitstagen bieten. Es gibt einen Controller, der COM-Schnittstellen bereitstellt, die einem Benutzer das Anmelden und Abmelden über die Zeiterfassungskarte ermöglichen. HTMLSkripts verknüpfen die verschiedenen Geschäftsoperationen mit dem Controller, und alle Objekte implementieren eine COM-Schnittstelle, die der Controller zur Datenverarbeitung einsetzt. Wenn daher ein Benutzer sich an- oder abmeldet, stellt der Controller die aktuellen Bedingungen, d.h. die Uhrzeit, fest und leitet diese Informationen an das Geschäftsoperationsobjekt weiter. Das Geschäftsoperationsobjekt verarbeitet anschließend diese Bedingungen und gibt eine Reihe von Daten an den Controller zurück, die dem Recordset des Controllers, also dem
Entwickeln eines Unternehmensdesktop
455
Recordset der Zeiterfassungskarten, hinzugefügt werden. Bei dem Controller handelt es sich um ein DSO; der Designer muss also nur die verschiedenen HTMLElemente mit dem Controller-DSO verknüpfen. Die COM-Schnittstelle, die das Geschäftsoperationsobjekt implementieren muss, wird durch folgende IDL (Interface Definition Language) definiert: interface ITimeCard : IUnknown { HRESULT PunchIn( BSTR time); HRESULT PunchOut( BSTR time); HRESULT SetService(IUnknown *serviceProvider); }
Der Controller ruft die PunchIn- und PunchOut-Methoden auf, wenn der Benutzer sich über die Zeiterfassungskarte an- oder abmelden möchte. Bei dieser Implementierung ist die aktuelle Uhrzeit die einzige Bedingung, die durch den Controller verwaltet wird. Für eine gute verteilte Anwendung ist es erforderlich, dass der Controller die Uhrzeit von einem zentralen Netzwerkzeitgeber bezieht. Die Methode SetService ist eine Verwaltungsmethode, die der Controller zum Bereitstellen einer funktionierenden Schnittstelle einsetzt, die von der Implementierung des Geschäftsoperationsobjekts verwendet wird. Erstellen des Controllers Die Controllerimplementierung steht mit den Geschäftsprozessen nur unwesentlich in Zusammenhang. Bei der Controllerimplementierung handelt es sich um ein COM-Objekt auf Systemebene. Sie übernimmt drei Funktionen: Sie verwaltet die verschiedenen Geschäftsoperationsobjekte, sie verwaltet das Recordset der Zeiterfassungskarten, und sie stellt eine COMSchnittstelle bereit, die von der HTML-Skriptumgebung aufgerufen wird. Der Controller ist das COM-Objekt CExDataProv. Er implementiert zwei Hauptschnittstellen: OLEDBSimpleProvider und IExDataProv. Die Schnittstelle IExDataProv stellt generische Methoden zum Anmelden und Abmelden bereit und wird folgendermaßen definiert: interface IExDataProv : IDispatch { [id(1)] HRESULT punchIn(); [id(2)] HRESULT punchOut(); [id(3)] HRESULT activeInterface(IUnknown *currInterface); [id(4)] HRESULT resetActiveInterface(); [restricted, id(-3900)] HRESULT msDataSourceObject(BSTR qualifier, IUnknown **ppUnk); [restricted, id(-3901)] HRESULT addDataSourceListener(IUnknown *pEvent); };
456
Erstellen des Hybridclients
Die IExDataProv-Schnittstelle enthält die PunchIn- und PunchOut-Vorgänge. Die ActiveInterface-Methode definiert, welche ITimeCard-Implementierung aktiv ist, wenn IExDataProv::PunchIn oder IExDataProv::PunchOut aufgerufen wird. Zum Zurücksetzen des aktuellen ITimeCard-Verweises in der IExDataProv-Implementierung wird die IExDataProv::ResetActiveInterface-Methode aufgerufen. Keine der bislang erläuterten Methoden verändert die Benutzeroberfläche, sie sind jedoch über Skript mit der Benutzeroberfläche verknüpft. Da der Controller als DSO definiert wurde, muss er die standardmäßig definierte COM-Schnittstelle OLEDBSimpleProvider implementieren. Wenn Microsoft Internet Explorer versucht, Daten an ein ActiveX-Steuerelement zu binden, wird ein QueryInterface-Vorgang für die COM-Schnittstelle OLEDBSimpleProvider durchgeführt. Diese COM-Schnittstelle wird zum Erstellen eines Recordsets verwendet. Damit die DHTML-Datenbindung funktioniert, muss die DSO-COM-Schnittstelle (IExDataProv) zwei Methoden mit bestimmten IDs hinzufügen (msDataSourceObject und addDataSourceListener). Diese Methoden setzen das DSO zurück und binden es an Internet Explorer. Speichern der Recordsetdaten Wenn Daten aus dem Browser in das DSO verschoben werden, wird der COM-Datentyp VARIANT verwendet. Auch wenn es möglich ist, den Datentyp intern zu konvertieren, sollte er idealerweise als VARIANT beibehalten werden. Auf diese Weise wird weniger Verarbeitungszeit mit dem Konvertieren von Daten von einem Typ in einen anderen vergeudet. Intern muss der Controller die Daten der Zeiterfassungskarten speichern, und dies wird durch die CRow-Klasse verwaltet. CRow speichert die Daten als ein Array von VARIANT-Daten. Die Definition der CRow-Klasse lautet folgendermaßen: class CRow { public: _variant_t getData( long iCol); void setData( long iCol, _variant_t var); CRow(); virtual ~CRow(); private: _variant_t m_arrData[ 5]; };
Die Methoden setData und getData stellen einzelne Werte des Arrays ein und rufen sie ab.
Entwickeln eines Unternehmensdesktop
457
Binden der Schnittstellen Wenn die Webseite geladen wird, veranlasst das HTML-Tag DSO den Internet Explorer zu prüfen, ob das Steuerelement die msDataSourceObject-Methode implementiert hat. Ist dies der Fall, muss der Controller einen Verweis auf das COM-Objekt zurückgeben, das die OLEDBSimpleProvider-Schnittstelle implementiert hat. Die OLEDBSimpleProvider-Schnittstelle kann über mehrere Datensätze verfügen, Internet Explorer kann jedoch nur einen abrufen. Die Implementierung von Microsoft Internet Explorer ruft die msDataSourceObject-Methode auf, um das COM-Objekt zu laden, das die COM-Schnittstelle OLEDBSimpleProvider implementiert hat. STDMETHODIMP CExDataProv::msDataSourceObject(BSTR qualifier, IUnknown **ppUnk){ if( ppUnk == NULL) { return E_FAIL; } *ppUnk = GetUnknown(); return S_OK; }
Der erste Parameter, qualifier, ist immer NULL und sollte ignoriert werden. Die COM-Schnittstelle OLEDBSimpleProvider wird im Parameter ppUnk zurückgegeben. Hierbei handelt es sich um einen IUnknown-COM-Schnittstellenzeiger. Das aktuelle Objekt soll im ppUnk-Parameter in Form eines IUnknown zurückgegeben werden. Dies wird erreicht, indem die lokale GetUnknown -Methode des COM-Objekts aufgerufen wird. Verläuft der Methodenaufruf erfolgreich, wird die addDataSourceListener-Methode aufgerufen. Diese Methode gibt eine DataSourceListener-COM-Schnittstelle an. Der Controller verwendet diesen COM-Schnittstellenzeiger, wenn sich Daten innerhalb des Recordsets des Controllers geändert haben. Auf diese Weise wird Microsoft Internet Explorer gezwungen, den auf der HTML-Seite angezeigten Inhalt zu aktualisieren. Im Folgenden sehen Sie eine Beispielimplementierung von addDataSourceListener: STDMETHODIMP CExDataProv::addDataSourceListener(IUnknown *pEvent) { DataSourceListener *temp = m_pListener.Detach(); if( temp != NULL) { temp->Release(); }
458
Erstellen des Hybridclients
m_pListener = pEvent; return S_OK; }
Diese DataSourceListener-COM-Schnittstelle besitzt die Fähigkeit, Internet Explorer durch Aufrufen von DataSourceListener::dataMemberChanged(NULL) über Änderungen in den Recordsetelementen Spaltendefinitionen, Eintragsgröße und Sortierreihenfolge zu informieren. Es gibt andere Methoden, die auf Änderungen in mehreren Datensätzen hinweisen; diese werden jedoch ignoriert, da Internet Explorer mehrere Einträge nicht unterstützt. Bindung zwischen Daten und Schnittstelle In HTML binden die DHTMLDatenbindungsattribute einen Feldnamen an ein Feld im Recordset. Das Problem liegt darin, dass die OLEDBSimpleProvider-Schnittstelle keine Methoden oder Eigenschaften zum Verweisen auf eine numerische Feld-ID oder einen Feldnamen bietet. Die OLEDBSimpleProvider-Schnittstelle nimmt alle Bindungen auf der Grundlage von Zeilen und Spalten vor. Die von der OLEDBSimpleProvider-COM-Schnittstelle verwendete Lösung besteht darin, die Informationen der Spaltenüberschrift an den Zeilenindex Null des Recordsets zu binden. Wenn der Inhalt von Zeile Null angefordert wird, sollte es sich dabei um die Feldnamen handeln. Durch das Durchlaufen aller Spalten verfügt Internet Explorer über die Fähigkeit, einen Querverweis zwischen einem Feldnamen (DATAFLD) und einer Feld-ID herzustellen. Wenn die in HTML angegebenen Felder nicht in Zeile Null des Recordsets vorhanden sind, werden diese Felder nicht gebunden. Eine weitere Besonderheit liegt vor, wenn entweder die Zeile oder die Spalte des Recordsets eine Index-ID von –1 besitzt. Dies bedeutet, dass alle Elemente in der Zeile oder Spalte ausgewählt werden. Beispielsweise bedeutet iRow = –1 und iCol = 2, dass die gesamte Spalte 2 markiert wird. Die Möglichkeit zum Auswählen aller Elemente über den speziellen Zeilen- und Spaltenindex ist optional und muss nicht unterstützt werden. Einige Datenmerkmale Im Beispiel des XML-DSO stammten die Daten von einer anderen Quelle und wurden über eine bestimmte URL-Adresse oder innerhalb der HTML-Seite abgerufen. Benutzerdefinierte DSOs können beliebige Aufgaben erfüllen. Ein benutzerdefiniertes DSO kann sich selbst an einen Informationsstream binden, der dem Client angeboten wird, oder an eine Datenbank auf dem lokalen Computer. Dies ist ein Implementierungsdetail des DSO. Nachdem das DSO abgerufen wurde und die HTML-Seite zum Anzeigen des Recordsets bereit ist, müssen einige Schritte ausgeführt werden. Der erste Schritt, den Microsoft Internet Explorer nach dem Laden des DSO durchführt, ist das Ab-
Entwickeln eines Unternehmensdesktop
459
rufen der Zeilen- und Spaltenanzahl mit Hilfe der Methoden OLEDBSimpleProvider::getRowCount und OLEDBSimpleProvider::getColumnCount des Recordsets. Für einen leeren Datensatz sollten die Werte für die Zeile und die Spalte jeweils null lauten. Ist der Datensatz nicht leer, ist jeder Wert akzeptabel, der höher ist als null. Der nächste Schritt hängt davon ab, ob sich Daten im Recordset befinden. Ist dies der Fall, wird der Status jedes einzelnen Datenelements abgerufen. Der Status definiert die Aktualisierungsmöglichkeit der einzelnen Datenelemente. Zwei gültige Statusflags sind das Flag für Schreibschutz und das Flag für Schreib-/Lesezugriff. Im Folgenden sehen Sie eine Beispielimplementierung eines Controllers, in dem die Daten schreibgeschützt sind. Eine Implementierung des Statusabrufs lautet folgendermaßen: STDMETHODIMP CExDataProv::getRWStatus(LONG iRow, LONG iCol, OSPRW *prwStatus) { *prwStatus = OSPRW_READONLY; return S_OK; }
In diesem Beispiel sind sämtliche Elemente aktualisierbar. Denken Sie jedoch daran, dass der Zeilen- und Spaltenindex auch 0 oder –1 lauten könnte. Wenn man die Implementierung der CExDataProv::getRWStatus-Methode in Betracht zieht, bedeutet dies, dass die Recordsetüberschriften oder eine ganze Spalte oder Zeile aktualisierbar sein könnten. Ändert sich der Datensatz, weil eine Zeile eingefügt oder gelöscht wird oder weil Elemente aktualisiert werden, muss der Internet Explorer-Konsument benachrichtigt werden. Dies geschieht über den Aufruf der OLEDBSimpleProvider::addOLEDBSimpleProviderListener-Methode. Eine OLEDBSimpleProviderListenerSchnittstelle wird in dieser Methode als Parameter angegeben. Diese Schnittstelle ist mit DataSourceListener vergleichbar mit der Ausnahme, dass sie sich mit einzelnen Zeilen und Zellen des Datensatzes befasst. Wenn Internet Explorer nicht am Empfang von Datensatzereignissen interessiert ist, wird OLEDBSimpleProvider::removeOLEDBSimpleProviderListener aufgerufen und die Schnittstelle angegeben, die aus der Liste der Parameter gestrichen werden soll. Arbeiten mit Zeilen Bei der Arbeit mit den einzelnen Zeilen ist es hilfreich, wenn man Einträge hinzufügen und löschen kann. Die OLEDBSimpleProviderSchnittstelle stellt die entsprechenden beiden Methoden bereit:
460
Erstellen des Hybridclients
OLEDBSimpleProvider:: deleteRows(LONG iRow, LONG cRows, LONG *pcRowsDeleted) OLEDBSimpleProvider:: insertRows(LONG iRow, LONG cRows, LONG *pcRowsInserted)
Wenn eine Zeile eingefügt oder gelöscht werden soll, gibt der erste Parameter, iRow, die Zeilennummer an. Der zweite Parameter, cRows, legt die Anzahl der Zeilen fest, die gelöscht oder eingefügt werden sollen. Der letzte Parameter, pcRowsxxx, steht für die Anzahl der Zeilen, die eingefügt oder gelöscht wurden. Wenn eine dieser Operationen ausgeführt wird, geschieht dies an der Zeilennummer. Wenn Sie beispielsweise einen Eintrag auf der zehnten Zeile einfügen, wird die aktuelle zehnte Zeile in die elfte Zeile verschoben. Wenn diese Methoden implementiert werden, müssen unbedingt Grenzüberprüfungen durchgeführt werden. Stellen Sie sicher, dass zu löschende Zeile wirklich vorhanden ist. Arbeiten mit den Daten Die Arbeit mit den Daten ist etwas komplizierter. Es gibt zwei mögliche Operationen: das Einstellen der Daten und das Abrufen der Daten. Die Definition der Methode zum Abrufen der Daten lautet folgendermaßen: OLEDBSimpleProvider::getVariant(LONG iRow, LONG iCol, OSPFORMAT format, VARIANT *pVar)
Die ersten beiden Parameter geben die Zeile, iRow, und die Spalte, iCol, der Datenzelle an, die abgerufen werden soll. Die Werte müssen innerhalb der Grenzen der zugrunde liegenden Daten liegen, mit einer Ausnahme. Diese Ausnahme gilt, wenn Internet Explorer Querverweise zwischen den DATAFLD-Werten und der Spalten-ID herstellt. In diesem Fall ist der Wert für die Zeile null. Der dritte Parameter definiert das Format der Daten, wenn diese abgerufen werden. Es gibt drei mögliche Werte: 왘 OSPFORMAT_RAW: Das ursprüngliche Datenformat des internen Datensatzes
kann beibehalten werden. Es ist keine Konvertierung erforderlich. 왘 OSPFORMAT_FORMATTED: Der interne Datensatzwert muss, unabhängig
vom Typ, in den Typ BSTR innerhalb des VARIANT-Typs konvertiert werden. 왘 OSPFORMAT_HTML: Der interne Datensatzwert muss, unabhängig vom Typ,
in den Typ BSTR innerhalb des VARIANT-Typs konvertiert werden, der HTMLTags enthalten kann. Der letzte Parameter ist der ausgegebene VARIANT-Datentyp. Bei der Weitergabe von VARIANTs vom internen Datensatz an Internet Explorer müssen die Daten mit Hilfe von VariantCopy kopiert werden. Visual Basic-Programmierer müssen sich hierüber keine Gedanken machen, da dieser Vorgang automatisch erfolgt.
Entwickeln eines Unternehmensdesktop
461
Um Daten zu speichern, muss es sich beim Zeilen-, Spalten- oder Zellenstatus um einen Schreib-/Lesestatus handeln. Der Status der Zeile, Spalte oder Zelle wurde eingestellt, als Microsoft Internet Explorer die OLEDBSimpleProvider::getRWStatus-Methode aufgerufen hat. Er wurde als OSPRW_READWRITE definiert, d.h., die Aktualisierungsfunktion wurde aktiviert. Eine Beispielimplementierung für das Einstellen der Daten lautet folgendermaßen: STDMETHODIMP CExDataProv::setVariant(LONG iRow, LONG iCol, OSPFORMAT format, VARIANT var) { if( iRow > m_rows.GetCount() || iCol > 5) { return E_FAIL; } try { _com_util::CheckError( Notify(CHANGECELL_ABOUTTODO, iRow, iCol, 1)); CRow *element = m_rows.GetAt( m_rows.FindIndex( iRow - 1)); element->setData( iCol, var); _com_util::CheckError( Notify(CHANGECELL_DIDEVENT, iRow, iCol, 1)); } catch( _com_error err) { return err.Error(); } return S_OK; }
Wie bei der getVariant-Methode werden die Daten unter Verwendung einer Zeile, iRow, und einer Spalte, iCol, eingestellt. Das Format ist in diesem Falle von nicht so großer Bedeutung, da sie in den internen Datensatz konvertiert werden können. Der letzte Parameter definiert die zu speichernden Daten. Bei der Implementierung dieser Methode ist es wichtig, Internet Explorer darauf hinzuweisen, was durch Aufrufen der Methoden der OLEDBSimpleProviderListener-Schnittstelle geschieht. Wenn der Wert eingestellt werden soll, wird beispielsweise die Methode aboutToChangeCell aufgerufen. Wenn die Zelle geändert wurde, wird die Methode cellChanged aufgerufen. Diese Methoden müssen aufgerufen werden, da Internet Explorer sie zum Aktualisieren der Benutzeroberfläche verwendet. Erstellen der Implementierung Der Controller interagiert mit den verschiedenen Implementierungen der Geschäftsoperationen von ITimeCard. Der
462
Erstellen des Hybridclients
Controller bindet die Benutzerschnittstelle nicht an die Implementierungen der Geschäftsoperationen; die Geschäftsoperationen sind eigenständig und führen diesen Vorgang selbst durch. Betrachten Sie nun ein Beispiel für eine Implementierung der Geschäftsoperation zum Anmelden und Abmelden bei einem Projekt. Die Benutzeroberfläche ist in zwei Teile gegliedert: das ausgewählte Projekt und die Beschreibung der ausgeführten Aufgabe. Die Implementierung ist ein COM-Objekt, das die IWorkingSchnittstelle implementiert. Die Werte könnten mit Hilfe von Skripts zwei Eigenschaften zugeordnet werden, das würde jedoch zusätzlichen Codieraufwand bedeuten. Ein anderer Ansatz besteht darin, dass die Implementierung der Geschäftsoperation auf die HTML-Seite zugreift und die Werte aus zwei HTML-Elementen lädt. Dieses Verfahren nutzt die DHTML-Integrationstechnologie, die über die Bibliothek MSHTML.DLL verfügbar ist. Sehen Sie sich das folgende HTML-Fragment an:
Project:
Project X Project Y Sink Manufacture Car Windshield 5 Liter Motor
Comment:
Die Implementierung der Geschäftsoperation muss die HTML-Seite nach den HTML-Elementen optProjects und txtWorkComment durchsuchen. Hierzu stehen zwei Verfahren zur Verfügung. Bei dem einen werden die HTML-Elemente bei der Instanziierung der Implementierung auf der HTML-Seite angegeben. Bei dem anderen sucht die Implementierung der Geschäftsoperation die Elemente auf der
Entwickeln eines Unternehmensdesktop
463
Seite. Beide Methoden sind akzeptabel. Der an dieser Stelle verwendete Ansatz ist die Angabe der HTML-Elemente. Mit diesem Ansatz ist es lediglich erforderlich, einen QueryInterface-Vorgang für die Schnittstelle IHTMLInputTextElement durchzuführen. COM-Kompiliererunterstützung ist hilfreich, da dadurch alle komplexeren Eigenschaften der verschiedenen Schnittstellen überdeckt werden. Sehen Sie sich die folgende Implementierung zum Einstellen des Kommentartextfelds an: #import "mshtml.dll" STDMETHODIMP CWorking::SetCommentElement(IUnknown * inpElement) { try { MSHTML::IHTMLInputTextElement *temp = m_comment.Detach(); if( temp != NULL) { temp->Release(); } m_comment = inpElement; } catch ( _com_error err) { return err.Error(); }; return S_OK; }
Die Variable m_comment ist ein direkter Verweis auf das HTML-Element auf der HTML-Seite. Alle Ereignisse oder Eigenschaften, die mit dieser Variable in Zusammenhang stehen, sind für die Implementierung verfügbar. Die Implementierung der Geschäftsoperation besitzt nun die Möglichkeit, das HTML-Element zu bearbeiten, ohne dass der Einsatz von HTML-Skripts erforderlich ist. Diese Lösung hat jedoch ihren Preis. In dieser Implementierung gibt es einen Verweis auf ein HTML-Tag. Nehmen Sie an, dieses Tag liegt innerhalb eines Rahmens und der Rahmen wird mit einer anderen Seite geladen. Das -Tag existiert nicht mehr, der Verweis innerhalb der Implementierung ist jedoch noch immer vorhanden. Wenn dieser Schnittstellenzeiger verwendet wird, sind die Ergebnisse nicht vorhersehbar und könnten zu einem Programmabsturz führen. Die Dynamik des Webs muss berücksichtigt werden. Sie macht den Unterschied zu herkömmlichen Anwendungen aus. Hinzufügen zum DSO-Recordset Wenn der Controller die punchIn-Methode aufruft, werden die Werte von der HTML-Seite abgerufen. Betrachten Sie hierzu den folgenden Quellcode: _variant_t strComment = m_comment->Getvalue(); _variant_t strProject = m_project->Getvalue();
464
Erstellen des Hybridclients
Auch in diesem Beispiel handelt es sich um VARIANT-Werte. Bei der Arbeit mit dem clientseitigen DHTML-Modell werden VARIANTs häufig eingesetzt. Nachdem die Werte abgerufen wurden, stellt die Implementierung die Werte im DSO ein. Der Controller stellt die DSO-Werte nicht selbst ein, da das DSO die Verarbeitungsweise der Daten nicht kennt. Das DSO stellt einen generischen Datensatz bereit. Die Implementierung erhält generische Informationen, führt einige Geschäftsoperationen durch und stellt die neuen Werte anschließend im generischen Datensatz ein. Ein Beispiel für das Einstellen der Daten lautet folgendermaßen: _com_util::CheckError( m_service->getRowCount( &m_currRow)); m_currRow ++; long actualInserted; _com_util::CheckError( m_service->insertRows( m_currRow, 1, &actualInserted)); _variant_t value( _bstr_t( "work")); m_service->setVariant( m_currRow, 1, OSPFORMAT_RAW, value);
Die Daten, die im Beispielquellcode hinzugefügt werden, stellen einen neuen Eintrag dar. Damit eine Zeile über die OLEDBSimpleProvider-Schnittstelle hinzugefügt wird, muss eine Position angegeben werden. Normalerweise wird eine Zeile am Ende hinzugefügt. Sie müssen daher die Eintragsanzahl mit Hilfe von getRowCount abrufen. Dieser Wert wird dann verwendet, um über insertRows eine Zeile hinzuzufügen. Schließlich werden die Zeilenwerte über setVariant eingestellt. Einige Hinweise zur clientseitigen Anwendungslogik Der gezeigte Quellcode ist Visual C++-Code; Visual Basic und Visual J++ können jedoch auch von dieser Technologie Gebrauch machen. Visual J++ wurde nicht behandelt, da es nicht genau dasselbe Objektmodell verwendet, sondern eine Bibliothek namens WFC (Windows Foundation Classes). Die Modelle sind fast identisch, und die Ausgabe ist genau dieselbe. Wenn Sie clientseitige Anwendungslogik erstellen, bietet sich die Verwendung von Visual Basic an, da die Komplexität des Sammelns von Ereignissen von der HTML-Seite verborgen wird. Wird beispielsweise auf eine Schaltfläche auf der HTML-Seite geklickt, so wird dieser Vorgang durch irgendein Skript aufgezeichnet. Bei Visual Basic muss lediglich das Schlüsselwort withEvents verwendet werden. DHTML-Ereignisse sind in Visual C++ möglich, die Codierung ist jedoch sehr
Entwickeln eines Unternehmensdesktop
465
komplex, da es keine standardmäßigen Implementierungen zur Erfassung von Ereignissen für die MSHTML.DLL gibt. Das Schreiben von Implementierungen mit Visual Basic bedeutet jedoch, dass die Visual Basic-Laufzeitumgebung auf der Clientseite vorhanden sein muss, und dies erfordert unter Umständen umfangreiche Ladevorgänge. In einem Intranetszenario ist die herunterzuladende Datenmenge möglicherweise kein Thema.
14.3 Resümee Dieses Kapitel bot eine Einführung in das Konzept des Hybridclients, eine erweiterte Version des ursprünglichen Windows 3.1-Clients. Das Besondere am Hybridclient ist, dass er das Internet nutzt und es nahtlos mit Hilfe von HTML und XML integriert. Durch Verwendung von Hybridclients kann die Verarbeitungsleistung zwischen den Clients und den Servern aufgeteilt werden. Das Ausführen von Anwendungslogik auf der Clientseite ist nicht unvorteilhaft – der Nutzen hängt von dem durchzuführenden Geschäftsprozess ab. Ein kurzer Hinweis zur Verwendung von Visual J++. Visual J++ ist eine tolle Umgebung; zu dem Zeitpunkt, als dieses Buch verfasst wurde, gab es jedoch zu viele offene politische Fragen, die die Frage, ob Visual J++ zum Schreiben clientseitiger Komponenten und Desktopshells verwendet werden sollte, zu einer heiklen Frage gemacht haben. Ich habe Visual J++ auf der Serverseite verwendet, da der Code dadurch, dass es keine grafische Benutzeroberfläche gibt, teilweise ohne größeren Aufwand in Java von Sun Microsystems portiert werden kann. Durch den Hybridclient wird die Erstellung von Benutzerschnittstellen wesentlich verändert. Einige Bestandteile sind derzeit zwar noch nicht plattformübergreifend, dies wird sich jedoch bald ändern. Zum Zeitpunkt der Entstehung dieses Buches ist mir über Transmeta (www.transmeta.com) noch nicht viel mehr bekannt, als dass eine flexible CPU entwickelt wird, die das Problem der verschiedenen Plattformen möglicherweise löst.
466
Erstellen des Hybridclients
15 Erstellen einer Ressource Bisher wurden die Darstellungsschicht und die mittlere Dienstschicht vorgestellt. Im Folgenden wollen wir uns mit der Datendienstschicht beschäftigen. Auf der Datendienstschicht befindet sich der Datenbestand. Auf dieser Schicht wird bestimmt, wie Daten zuverlässig, konsistent und unter Berücksichtigung der Skalierbarkeit gespeichert werden können. Das Speichern von Daten zur späteren Bearbeitung ist theoretisch sehr einfach, die entsprechende Implementierung jedoch sehr komplex. Ein Datendienst kann eine Datenbank oder ein Produkt wie Microsoft Exchange sein. Bei diesen beiden Beispielen werden Daten zur späteren Bearbeitung gespeichert, wobei das eine Produkt auf relationalen Daten und das andere auf Workflowdaten basiert. Die meisten Bücher, die sich mit Datenbanken beschäftigen, konzentrieren sich auf die administrativen Aspekte der Datenbank. Im Gegensatz dazu stellt dieses Buch das Schreiben von Code für die Datenbank in den Mittelpunkt. Dieses Kapitel beschäftigt sich mit dem Datendienst SQL Server und damit, wie mit SQL (Structured Query Language) Daten in einer relationalen Datenbank bearbeitet werden. In diesem Kapitel erfahren Sie insbesondere, wie Sie eine gespeicherte Prozedur schreiben. Eine gespeicherte Prozedur ist eine Funktion in einer Programmiersprache, die nur in einer Datenbank ausgeführt werden kann.
15.1 SQL und Portabilität SQL wurde als portierbare Standardsprache für die Datenabfrage entwickelt. Dieser nicht allumfassende Standard stellt die Grundoperationen zur Verfügung, die auf eine relationale Datenbank angewendet werden können. Zum Zeitpunkt der Entstehung dieses Buches war SQL 2 (SQL 92) der aktuelle Standard. Der Nachfolger SQL 3 (SQL 99) war jedoch bereits vollständig definiert. SQL 3 bietet zahlreiche neue Optionen und Möglichkeiten und ist als objektorientierte Sprache definiert, die die Grenzen der gespeicherten Prozedurarchitektur überwindet. Da wir jedoch mit den gegenwärtigen Technologien arbeiten, müssen wir uns weiter mit bestimmten Problemen auseinander setzen. Die vorgestellten gespeicherten Prozeduren sind spezifisch für Microsoft SQL Server und können deshalb z.B. in Oracle nicht eingesetzt werden. Glücklicherweise ist dies nicht wirklich ein Problem, da nur das Verschieben von Daten aus einer Datenbank in eine andere ein komplizierter und zeitraubender Vorgang ist. Das Übertragen gespeicherter Prozeduren in dieser Situation ist dagegen weniger kompliziert.
SQL und Portabilität
467
15.1.1 Gründe für und gegen ein objektorientiertes Datenbankverwaltungssystem (OODBMS) Ein OODBMS ist ein objektorientiertes Datenbankverwaltungssystem. Obgleich viele meinen, dass eine objektorientierte Datenbank auf Grund ihrer Eigenschaften die bessere Lösung sei, zeigt die Realität, dass OODBMS-Systeme den Erwartungen auf dem Windows DNA-Markt nicht gerecht geworden sind. Objektorientierte Datenbankverwaltungssysteme haben ihre Stärken in Spezialgebieten, wie z.B. Wissenschaft und Mathematik, da die Datenbank hier nur eine oder zwei Hauptanforderungen erfüllen muss. Im Windows DNA-Umfeld sind jedoch Sicherheit, Leistung, Skalierbarkeit und Erweiterbarkeit gleichermaßen wichtig. Denn obwohl SQL nicht objektorientiert ist, wird weiterhin auf diese durchdacht definierte und robuste Abfragesprache gebaut, die die folgenden Vorteile bietet: 왘 Unterstützung von Transaktionen mit großen Datenmengen 왘 Speicherung großer Datenmengen 왘 Unterstützung von Parallelverarbeitung, Lastausgleich und Indizierungstools 왘 Sicherungsmöglichkeit für große Datenmengen 왘 Sicherheitsimplementierungen für große Datenmengen und Benutzerzahlen 왘 Möglichkeiten der Datenbankwiederherstellung 왘 Unterstützung gleichzeitiger Benutzer
In einem SQL-System können also Daten rund um die Uhr gespeichert, verwendet und verwaltet werden. Mit der Weiterentwicklung von OODBMS-Systemen und der zunehmenden Beliebtheit von SQL 3 werden Datenbanken in Zukunft aus einer Kombination objektorientierter und relationaler Typen bestehen.
15.1.2 Die zu wählende Version von Microsoft SQL Server Zum Zeitpunkt der Entstehung dieses Buches war SQL Server 7.0 die aktuelle Implementierung von SQL Server. Im Allgemeinen ist es nicht empfehlenswert, auf eine neue Datenbank umzusteigen, doch in diesem Fall gibt es einen überzeugenden Grund. Microsoft SQL Server 7.0 ist die erste Version, in der Zeilensperren statt wie in den Vorversionen Seitensperren implementiert wurden. Der Seitensperrmechanismus kann zu massiven Skalierungsproblemen führen, wenn Datenbankzugriffschichten verwendet werden, die auf der dynamischen Abfrage der Datenbank basieren. Folgende Grundregel gilt: Weist die Datenbank mindestens zwei Transaktionen pro Sekunde auf, sollten Sie SQL Server 7.0 verwenden. SQL Server ist eine sehr schnelle Datenbank für Unternehmen.
468
Erstellen einer Ressource
15.2 SQL-Grundlagen Wenn Sie noch nie mit einer SQL-Datenbank gearbeitet haben, müssen Sie zuerst lernen, einen SQL-Befehl zu schreiben, der alle Einträge in einer Tabelle auswählt. Um eine relationale Datenbank verstehen zu können, müssen Sie die Funktionsweise einer Tabelle und die Verknüpfung von Daten in verschiedenen Tabellen verstehen.
15.2.1 Tabellen Tabellen bilden die Grundlage einer relationalen Datenbank. In ihnen werden Datenbankdaten gespeichert, weshalb Tabellen die erste Arbeitsgrundlage darstellen. Eine Tabelle hat ein rechteckiges Format, ihre Dimensionen werden von einer Anzahl Zeilen und Spalten bestimmt. Alle Zeilen einer Tabelle haben dieselbe Anzahl Spalten. Wird einer Tabelle eine Zeile hinzugefügt, verfügt sie über eine bestimmte Anzahl Spalten, die die einzelnen Felder der Tabelle repräsentieren. Die hinzugefügten Zeilen dürfen über leere Spaltenwerte oder Spalten mit NULLWerten verfügen. Eine Zeile in einer Datenbank wird Eintrag genannt. Wenn ein SQL-Befehl auf eine bestimmte Tabelle angewendet wird, werden u.U. einige Einträge in einem Resultset zurückgegeben. SQL legt jedoch nicht den Speicherort eines bestimmten Eintrags in der Tabelle fest. Diese Information ist nur für die Datenbank von Bedeutung. Für SQL ist der Speicherort der Daten uninteressant, da SQL mit Daten auf Resultset-Basis arbeitet. Arbeiten mit Datentypen Eine Tabelle besteht aus einer Folge von Spalten, die bestimmte Datentypen haben. Diese Datentypen sind auf die SQL-Umgebung beschränkt. Einige Datentypen sind Teil des SQL-Standards, andere werden dagegen spezifisch für eine Datenbank definiert. Zeichenfolgendatentypen Der weitaus geläufigste Datentyp ist die Zeichenfolge. Es gibt zwei Arten von Zeichenfolgen, char und varchar. char ist eine Zeichenfolge fester Länge, varchar hingegen eine Zeichenfolge variabler Länge. Das Zeichenfolgenformat muss mit Bedacht gewählt werden. Ein char-Feld der Größe x Bytes belegt x Bytes in der Datenbank, und zwar ungeachtet der enthaltenen Daten. Ein varchar-Feld der Größe x belegt maximale x Bytes, kann jedoch abhängig von der Größe der in diesem Feld gespeicherten Daten weniger Speicherplatz belegen. Dieser Unterschied ist von großer Bedeutung, wenn die Zeichenfolge von einer bestimmten Programmiersprache bearbeitet wird. In C und C++ wird eine Zeichenfolge mit NULL beendet. Ein char-Feld wird erst dann mit NULL beendet, wenn es das Ende des Puffers erreicht. Dies bedeutet, dass die C- und
SQL-Grundlagen
469
C++-Zeichenfolgenbearbeitungen das letzte Zeichen von Bedeutung suchen und dann ein NULL-Zeichen einfügen müssen. In Visual Basic ist diese Funktionalität integriert, obgleich es sich hier um eine Funktion handelt, die aufgerufen werden muss. In der Regel ist varchar die bessere Wahl. char-Zeichenfolgen bieten sich an, wenn es sich um eine feste Zeichenfolge handelt, wie z.B. eine internationale Währungsabkürzung mit drei Buchstaben (z.B. USD, DEM, SFR). Dabei muss jedoch Folgendes bedacht werden. Wie wir wissen, wurde das Jahr 2000-Problem durch das Verwenden zweistelliger Jahreszahlen zum Einsparen von Speicherplatz und Schreiben von effizienterem Code verursacht. Wenn Sie eine fest Länge verwenden möchten, bedenken Sie vor der Implementierung sorgfältig die Auswahl der richtigen Länge. In SQL Server 7.0 haben die Datentypen char und varchar eine maximale Datenlänge von 8000 Zeichen. In SQL Server 6.5x betrug die maximale Länge dagegen nur 256 Zeichen. Diese Vergrößerung ist von großer Bedeutung, da char und varchar häufig in den Datentyp text konvertiert werden müssen. Für den Datentyp text gilt eine maximale Länge von 231 Zeichen. Der Nachteil dieses Datentyps besteht darin, dass die Verarbeitung auf Grund dieser großen Länge verlangsamt wird. Große Datentypen Für die Verarbeitung großer Datentypen, die nicht einfach ist, gibt es verschiedene Möglichkeiten. Die erste ist die Verwendung des Felds text. Für die Verarbeitung reiner Binärdaten gibt es das Feld image, das eine Länge von 231 Byte aufweisen darf. Numerische Datentypen Der am häufigsten falsch verwendete Datentyp ist integer. Der Datentyp integer wird durch zwei Attribute definiert, Vorzeichen und Länge. Das Vorzeichenbit ermöglicht die Verkleinerung des Wertebereichs und das Festlegen negativer Werte. Die Länge kann 16, 32 oder 64 Bits betragen, obgleich 64 Bits noch nicht umfassend unterstützt werden. Die Verwirrung um den Datentyp integer wird ferner durch die Tatsache gesteigert, dass integer häufiger als andere Datentypen, wie z.B. Behandlungsroutinen und Ressourcenbehandlungsroutinen, definiert und neu definiert wird. Die folgende Tabelle unterscheidet die 16- und 32-Bit-Datentypen: 16-Bit
short, WORD
32-Bit
int, long, DWORD, UINT, ULONG
In Microsoft SQL Server gibt es auch integer-Datentypen, die jedoch anders als in der Programmiersprache benannt sind. Eine 16-Bit-Ganzzahl wird in SQL Server dem 16-Bit-Datentyp smallint zugeordnet. smallint ist eine Ganzzahl mit Vorzei-
470
Erstellen einer Ressource
chen, was bedeutet, dass positive und negative Werte in der Datenbank gespeichert werden können. Der höchste bzw. der niedrigste Wert ist 32.767 bzw. – 32.768. Wenn das Programm diese Werte überschreitet, erfolgt ein automatischer Umbruch, der zu einer falschen Antwort führt. Oder wenn ein positiver Wert, der 32.768 überschreitet, in die Variable geschrieben wird, wird der Wert automatisch in einen negativen Wert umgewandelt. Der Wert ist zwar an sich nicht fehlerhaft, doch wird das Vorzeichenbit in einem Vorzeichenkontext als negativer Wert falsch interpretiert. In den Fällen, in denen die 16-Bit-Ganzzahl nicht groß genug ist, muss die Ganzzahlgröße auf 32 Bits erhöht werden. Der Datentyp für 32-Bit-Ganzzahl heißt int. Dieser int-Datentyp darf nicht mit dem int-Datentyp in C++ verwechselt werden. Dieser Wert hat ein Vorzeichen und deshalb einen oberen Grenzwert von 2.147.483.647 und einen unteren Grenzwert von –2.147.483.648. Wird ein größerer Wert als dieser eingegeben, wird dieser umgebrochen oder in einen negativen Wert umgewandelt. Ein weiterer numerischer Datentyp ist real. Der Unterschied zwischen real und integer besteht darin, dass real Dezimalkommas aufweist. Es gibt zwei Arten von Fließkommazahlen, den 4-Byte-Datentyp float und den 8-Byte-Datentyp double. Das Zuordnen dieser Datentypen zu den entsprechenden SQL-Datentypen ist einfach, wenngleich es dabei zu Namenskonflikten kommt. Ein SQL-float hat 8 Byte, ein C++float dagegen nur 4 Byte. Demzufolge wird ein C++-float einem SQL-real mit 8 Byte und ein C++-double einem SQL-float zugeordnet. Unicode-Zeichenfolgendatentypen Die aktuellste Datentyperweiterung in SQL Server ist die Unterstützung der Unicode-Datentypen nchar, nvarchar und ntext. Sie sind ihren Zeichenentsprechungen ähnlich, weisen jedoch 2 Bytes pro Zeichen auf. Aufgrund dieser Eigenschaft beträgt die maximale Feldgröße von ntext 230 Zeichen. Für nchar und nvarchar ist die maximale Zeichenzahl weiterhin 8.000. Der SQL-Datentyp »Numeric« Der SQL-Datentyp numeric ist ein besonderer Datentyp, der einer Zahl ähnlich ist, jedoch als Zeichenfolge definiert wird. Die Datentypen heißen decimal und numeric. Betrachten Sie die folgende Zahl: 123,456. Diese Zahl hat eine Präzision von sechs Stellen und drei Nachkommastellen. Die SQL-Begriffe Präzision und Nachkommastellen definieren die Genauigkeit der Zahl und die Anzahl der Stellen rechts vom Dezimalkomma. Um die Zahl 123,456 zu speichern, muss die Deklaration wie folgt lauten: decimal(6,3) oder numeric(6,3). Die Anzahl der Nachkommastellen darf nicht höher als die Präzision sein, und die Präzision hat den Höchstwert 38.
SQL-Grundlagen
471
Zeitdatentypen Ein weiterer Datentyp ist das Feld time, das eine Uhrzeit und ein Datum enthalten kann. Es gibt die Zeitfelder datetime und smalldatetime, die sich im Genauigkeitsgrad unterscheiden. Der Datentyp smalldatetime ist auf die Minute genau, der Datentyp datetime bis auf 3,33 Millisekunden genau. Der Datentyp »Cursor« Der Datentyp cursor wird im Kontext mit gespeicherten Prozeduren verwendet (die weiter unten erklärt werden) und ermöglicht das Durchlaufen einer Abfrage sowie das Untersuchen der einzelnen Einträge. Erstellen einer Tabelle Eine Tabelle wird erstellt, indem der Basistabellendefinition verschiedene Spaltendatentypen hinzugefügt werden. Dazu wird der SQL-Befehl CREATE TABLE verwendet. Es folgt ein SQL-Beispielcode: Create table myTable (field1 datatype, field2 datatype, …)
In der Website für die Konferenzanmeldung ist die Tabelle users die Basistabelle. Sie enthält alle Benutzer des Anmeldungssystems samt Vorname, Nachname, Kennwort und E-Mail-Adresse. Der SQL-Befehl zur Definition der Tabelle lautet: CREATE TABLE users ( first_name varchar (255) , last_name varchar (255) , password varchar (255), email varchar (255) )
Unbestimmtheit und Redundanz von Daten Beim Entwurf einer SQL-Tabelle müssen verschiedene Faktoren beachtet werden: 왘 Die Reihenfolge der Spalten muss nicht angegeben werden. Wenn Sie dem-
nach auf eine Spalte gemäß einem numerischen Index verweisen, kann dies zu Fehlern führen. Stattdessen müssen alle Verweise auf einer abstrakten Ebene erfolgen, z.B. über das Verweisen auf den Spaltennamen. 왘 Alle Einträge in der Tabelle müssen eindeutig sein. Die Eindeutigkeit eines Ein-
trags kann durch einen einzelnen Spaltenwert oder eine Kombination von Spaltenwerten erreicht werden. Das Arbeiten mit doppelten Einträgen in einer Datenbank birgt potenzielle Gefahren für Ihre Daten. Doppelte Einträge in einer Datenbank können zu Konsistenzproblemen führen und falsche Aktualisierungen und Abfragen verursachen. Deshalb kann ein Tabelle nicht vollständig durch die Funktion definiert werden, der sie dient, da sie teilweise durch die Informationen definiert wird, die sie ent-
472
Erstellen einer Ressource
halten soll. Demzufolge ist es wichtig, die Daten zu verstehen, die in der Datenbank gespeichert werden. Ein weiterer wichtiger Punkt bei Tabellen ist der Unterschied zwischen Datenduplizierung und Datenredundanz. Von Datenduplizierung ist die Rede, wenn Daten in einer Tabelle zweifach, jedoch mit verschiedenen Attributen vorhanden sind. Die folgende Tabelle zeigt ein einfaches Beispiel: Teil
Beschreibung
123
Prozessor
124
Arbeitsspeicher
125
Festplatte
126
Prozessor
Es hat den Anschein, als wären alle Einträge in dieser Tabelle eindeutig, was jedoch nicht stimmt. Stellen Sie sich vor, wie diese Tabelle in einer Anwendung verwendet würde. Ein Benutzer könnte eine Textsuche basierend auf der Beschreibung ausführen und dazu Prozessor eingeben. Das Ergebnis wären zwei Einträge mit zwei verschiedenen Teilnummer (123 und 126). Dann stellt sich die Frage, welcher Prozessor der gesuchte ist. Es gibt keine korrekte Antwort, da beide Produkte dieselbe Produktbeschreibung haben, d.h., die beiden Einträge sind Duplikate. Um das Problem zu lösen, müssen die beiden Prozessoren voneinander unterschieden werden. Es folgen mögliche Lösungen: 왘 Wählen Sie eine bessere Beschreibung, wie z.B. Großer Prozessor und Kleiner
Prozessor. 왘 Aktualisieren Sie das Feld mit der Prozessorbeschreibung in ein Typfeld, und
erstellen Sie ein Bezeichnerfeld. Lassen Sie uns nun eine Tabelle mit redundanten Informationen untersuchen: Hersteller
Teil
Beschreibung
1
123
Prozessor
2
124
Arbeitsspeicher
3
125
Festplatte
4
123
Prozessor
In diesem Beispiel gibt es für Teil 123 redundante Informationen. Die Hersteller 1 und 4 stellen beide dieses Teil her, einen Prozessor.
SQL-Grundlagen
473
Überlegen Sie, was passieren würde, wenn die Tabelle aktualisiert und die erste Zeile der Beschreibung in Schneller Prozessor geändert würde. Dann gäbe es eine Inkonsistenz, da Teil 123 zwei verschiedene Beschreibungen zweier Hersteller hätte. Die Daten wären inkonsistent. Um Inkonsistenzen zu vermeiden, müssen die Daten normalisiert werden. Der Normalisierungsprozess wird weiter unten erläutert. Verwenden von Indizes Durch Tabellenindizes wird die Suche nach Daten beschleunigt. Ein Index ordnet die Einträge in einer Tabelle oder Sicht, jedoch nicht die Einträge selbst an. Er erstellt eine geordnete Menge von Eintragsindizes, die intern im Datenbankmodul verwaltet werden. Wenn das SQL-Modul einen Index zum Suchen der gewünschten Informationen verwenden kann, erfolgt die Suche sehr schnell. Wenn für die Informationen kein Index vorhanden ist, muss das SQL-Modul alle Einträge durchsuchen, wodurch der Prozessor verstärkt beansprucht wird. Wenn der Datenbank Einträge hinzugefügt werden, entscheidet die Datenbank, wo diese abgelegt werden. Die Datenbank legt einen neuen Eintrag an der Stelle mit dem niedrigsten Verarbeitungsaufwand ab, der zur Laufzeit bestimmt wird. Anschließend wird für den Datenzugriff ein Index verwendet, als wären die Daten gemäß bestimmter Kriterien sortiert. Im Konferenzanmeldungsprojekt kann ein Benutzer z.B. gemäß dem Namen oder der E-Mail-Adresse gesucht werden. Sehen Sie sich die folgenden Beispielreferenzdaten an: Vorname
Je nachdem, wie der Index der Tabelle definiert ist, kann der Eintrag Joe Smith doppelt vorhanden sein. Gäbe es einen eindeutigen Index nach Vor- und Nachname, wäre es nicht möglich gewesen, die beiden Joe Smith-Einträge mit unterschiedlichen Kennwörtern und E-Mail-Adressen hinzuzufügen, ohne dass ein Fehler erfolgt wäre. Es gibt jedoch mehrere Joe Smiths, sodass der Index Duplikate zulassen oder auf einem anderen Kriterium als nur Vor- und Nachname basieren muss. Bei der Definition eines Index müssen Sie untersuchen, wie die Daten durchsucht werden. In der vorherigen Tabelle ist die E-Mail-Adresse das eindeutige Attribut. Eine E-
474
Erstellen einer Ressource
Mail-Adresse ist eindeutig, weshalb wir sie für unseren Hauptindex verwenden können. Doch eine Suche nach einer Person erfolgt in der Regel nicht über die E-MailAdresse, sondern über Vor- und Nachname. Demzufolge wäre es sinnvoll, einen Index zu erstellen, der Duplikate für den Vor- und Nachnamen zulässt. Normalisierung Die Normalisierung reduziert Redundanzen und Inkonsistenzen in Datenbankdaten, indem Tabellendaten in mehreren Tabellen abgelegt werden. Im Beispiel der Konferenzanmeldungsanwendung gehört zum Benutzer eine verknüpfte Adresse. Wo werden diese Informationen gespeichert? Wird eine neue Tabelle erstellt oder die Tabelle Users erweitert? Im Folgenden untersuchen wir, was passiert, wenn die Tabelle Users erweitert wird. Die neue Tabellendefinition sieht so aus: CREATE TABLE users ( first_name varchar( 255), last_name varchar( 255), password varchar(255), email varchar(255), street varchar(255), postcode varchar(32), city varchar(64), country varchar(64) )
Die Tabelle ist nun etwas größer, da Adressen mit Benutzern verknüpft werden. Da der eindeutige Index auf der E-Mail-Spalte basiert, sind alle Einträge eindeutig, weshalb es nicht zu Duplikaten oder Redundanzen kommt. Ein Problem gibt es dennoch. Einige Benutzer haben eine Rechnungsadresse, die sich von ihrer postalischen Adresse unterscheidet. Gegenwärtig gibt es keine Möglichkeit, diese Information wiederzugeben, weshalb die Tabelle nochmals erweitert werden muss. CREATE TABLE users ( first_name varchar( 255), last_name varchar( 255), password varchar(255), email varchar(255), street varchar(255), postcode varchar(32), city varchar(64), country varchar(64) bill_street varchar(255), bill_postcode varchar(32), bill_city varchar(64), bill_country varchar(64) )
Beim Erweitern von Tabellen müssen zwei Punkte beachtet werden. Erstens sind nachträgliche Änderungen kein Zeichen einer guten Planung. Wenn häufig Änderungen durchgeführt werden müssen, sollte die Planung überprüft werden. Zweitens sollten Sie das Erweitern von Tabellen generell vermeiden, da dadurch vor-
SQL-Grundlagen
475
handene Daten ggf. angepasst werden müssen, was mit großem Aufwand verbunden sein kann. Stattdessen können Sie Tabellen hinzufügen, die auf vorhandene Tabellen verweisen. Die vorhandenen Tabellen können ohne Veränderungen erhalten bleiben, wodurch die Stabilität des Systems erhöht wird. Bei der Normalisierung werden Felder aus einer Tabelle entfernt und zum Erstellen einer neuen Tabelle verwendet. Dabei werden der neuen Tabelle Referenzfelder hinzugefügt, die deren Daten mit den Einträgen in der Ausgangstabelle verbinden. Um den folgenden Tabellenentwurf zu erstellen, können wir die Daten in Users wie folgt normalisieren: CREATE TABLE users ( id int, first_name varchar( 255), last_name varchar( 255), password varchar(255), email varchar(255), ) CREATE TABLE address ( user_id int, address_type int, street varchar(255), postcode varchar(32), city varchar(64), country varchar(64) )
Die beiden Tabellen enthalten alle Daten der Tabelle Users, die wir zuvor erweitert haben. Um funktionieren zu können, musste jedoch noch eine Änderungen an der Ausgangstabelle Users vorgenommen werden. Wenn auf zwei Tabellen verwiesen wird, muss jede Tabelle über einen eindeutigen Bezeichner verfügen, der ohne Duplizierung indiziert werden kann. Dieser Bezeichner ist in der Regel ein sich automatisch erhöhender Wert. Dieser Bezeichner kann indiziert werden, um das Erstellen von Relationen zwischen zwei Tabellen zu beschleunigen. In einem tatsächlichen Datenbankszearium ist der Bezeichner normalerweise der Hauptindex. In diesem Beispiel besteht der Bezeichner aus den Feldern users.id und address.user_id. Das Feld users.id wurde hinzugefügt, da das Verwenden des Felds users.email als Bezeichner mehr Raum erfordern würde und weniger effizent wäre, wenn die Relationen zwischen Tabellen verarbeitet werden. In der Tabelle address darf address.user_id nicht der Hauptbezeichner sein, da ein einzel-
476
Erstellen einer Ressource
ner Benutzer mehrere Adressen haben kann (z.B. eine unterschiedliche Post- und Rechnungsadresse). Deshalb ist der Hauptindex der Tabelle address eine Kombination der Felder address.user_id und address.address_type. Das Feld address.address_type gibt den Typ der Adresse an, entweder Post- oder Rechnungsadresse. Eine weitere Möglichkeit für den Hauptindex besteht darin, ein Feld address.id zu erstellen, um die Adresse eindeutig zu bezeichnen. Dies würde zwar funktionieren, doch auf Grund der Art der Verwendung der Tabelle address wären diese Informationen nicht von Nutzen. Eine Adresse wird nämlich nie allein wegen der Adressinformationen abgerufen. Eine Adresse wird stets abgerufen, da sie im Kontext eines Benutzers bearbeitet wird. Aus diesem Grund benötigt der Adressindex einen user.id-Verweis. Um die SQL-Verknüpfungen zu verstehen, müssen sie umgekehrt betrachtet werden. Es wäre z.B. denkbar, dass ein Benutzer mit einer Adresse verknüpft ist, doch der Verweis von der Tabelle address erfolgt von der Tabelle address auf die Tabelle Users. Dies bedeutet, dass es ausgehend von der Tabelle Users nicht möglich ist herauszufinden, welche Adresse mit einem bestimmten Benutzer verknüpft ist. Möglich ist es dagegen, die Benutzer ausschließlich basierend auf der Tabelle address zu ermitteln. Der nächste denkbare logische Schritt wäre, Verweise auf die verschiedenen Adressen in der Tabelle users hinzuzufügen. Bei der objektorientierten Programmierung könnte diese Aufgabe in Form einer doppelt verknüpften Liste ausgeführt werden. Diese Methode ist jedoch in einem SQLKontext nicht sehr effizient. In einem SQL-Kontext sind die Ausführungen in diesem Absatz sinnvoller, denn wenn Sie neue Daten mit einem bestimmten Benutzer verknüpfen möchten, müssen Sie die Tabelle Users nicht ändern. Bei einem objektorientierten Datenbankszenarium wäre dies erforderlich. Bei der Normalisierung werden aus einer Tabelle mehrere Tabellen erstellt. Dank dieses Vorgangs gibt es keine Datenredundanzen, da die relevanten Daten in separate Tabellen verschoben werden. Die Verknüpfung zwischen den Tabellen wird SQL-Relation genannt. Durch das Aufgliedern einer großen Tabelle in mehrere Tabellen wird die gesamte Tabellenkomplexität vermindert, wobei die einzelnen Tabellen leichter verständlich werden und weniger Ressourcen belegen. Vor einer zu großen Normalisierung muss jedoch gewarnt werden. Ein erstes Anzeichen dafür ist, dass zu viele Relationen erforderlich sind, um bedeutsame Informationen aus der Datenbank abrufen. Ferner wird durch zu viele Relationen die Leistung der Datenbank beeinträchtigt. Allgemein werden Programmierer nicht besonders in den Datenbankentwurf einbezogen, weshalb diese Art der Planung in der Regel den Datenbankadministra-
SQL-Grundlagen
477
toren überlassen wird. Sollte sich jedoch ein Programmierer über die hier erläuterten Zusammenhänge im Klaren sein, wird dadurch die Zusammenarbeit mit dem Datenbankadministrator erleichtert.
15.2.2 Gründe für die Verwendung gespeicherter Prozeduren Gespeicherte Prozeduren in einer Datenbank entsprechen Funktionen in einer Programmiersprache. Mit Hilfe gespeicherter Prozeduren können Informationen, wie z.B. ein Funktionsaufruf, an eine Datenbank übergeben werden. Wenn Sie keine gespeicherten Prozeduren verwenden, werden diese Informationen der Datenbank in Form eines größeren SQL-Befehls hinzugefügt. Dieser ist größer, da er mehr SQL-Logik als eine vergleichbare gespeicherte Prozedur enthält. Der Unterschied bei einer gespeicherten Prozedur ist, dass die Befehle bereits auf dem Datenbankserver gespeichert sind. In der vorgeschlagenen Datenbankzugriffsarchitektur interagieren die COM+-Objekte (Component Object Model) auf der Schicht der Anwendungslogik mit der Datendienstschicht, indem gespeicherte SQL-Prozeduren ausgeführt werden. Ein Grund für die Verwendung gespeicherter Prozeduren ist deren Kapselung der Datenbankfunktionalität. Eine gespeicherte Prozedur ist eine SQL-Komponente, die Funktionsaufrufdefinition der gespeicherten Prozedur ist die Schnittstelle. Gespeicherte Prozeduren bieten vier Hauptvorteile: Einfachheit, Abstraktion, Sicherheit und Leistung. Grund 1: Einfachheit Ein COM+-Geschäftsobjekt enthält Anwendungslogik, wobei ein Teil dieser Anwendungslogik für den Zugriff auf Daten in der Datendienstschicht vorgesehen ist. Im Allgemeinen enthält der Datendienstcode der Anwendungslogik mehrere Zeilen mit SQL-Befehlen, die jedoch nicht mit Code einer Programmiersprache wie C++, Visual Basic oder J++ zu vergleichen sind. Dies bedeutet, dass Sie zwei verschiedene Programmierstile kombinieren müssen. Dies ist im Grunde kein Problem. Ein Problem ist jedoch, dass auf Grund der beiden Programmierstile die COM+-Anwendungsobjekte sehr groß und komplex werden. Dank einer gespeicherten Prozedur können die beiden Programmierstile getrennt und unabhängig sein. Der COM+-Code muss weiterhin ein wenig SQL-Code ausführen, jedoch weit weniger als beim vorherigen Ansatz, denn der Großteil des SQL-Codes wird in die gespeicherte Prozedur verschoben.
478
Erstellen einer Ressource
Grund 2: Abstraktion Die gespeicherte Prozedurarchitektur erstellt verschiedene Schnittstellen, die durch Funktionen definiert werden. Bei Verwendung einer Sicht oder gespeicherten Prozedurarchitektur kann sich die zugrunde liegende Datenstruktur ändern, sodass es möglich ist, Änderungen bei Bedarf vorzunehmen. Die Standardnormalisierung funktioniert z.B. bei Datenbanken mittlerer Größe, doch bei Datenbanken mit Terabytegröße muss die Normalisierung auf Grund des gesteigerten Aufwands anders erfolgen. Bei einer Datenbank mit Terabytegröße kann also eine Abfrage, die sich auf zwei Tabellen bezieht, weniger effizient als das Ausführen derselben Abfrage unter Verwendung einer Tabelle sein. Tabellenentwürfe können nicht ohne weiteres geändert werden, wenn die COM+-Objekte die Tabellen direkt verwenden. Eine gespeicherte Prozedur kann dagegen ihre Implementierung ohne Änderung der Schnittstelle verändern. Für das COM+-Objekt ist der Unterschied nicht erkennbar. Das bedeutet, dass Sie bei Verwendung von gespeicherten Prozeduren und Sichten Versionen zum Aktualisieren der Datenbank erstellen und die Abwärtskompatibilität weiter erhalten können. Wenn Sie ferner andere Datenbanken erstellen und weiter auf die vorhandenen Daten zugreifen möchten, können Sie diese mit den neuen Daten zusammenführen. Diese Faktoren sind beim Data Warehousing und Filtern von Daten von Bedeutung. Grund 3: Sicherheit In einer SQL-Datenbank basiert die Sicherheit auf den Tabellen und wird auf Einzelbenutzerbasis implementiert. Untersuchen Sie bei der Konferenzanmeldungsanwendung den Fall, bei dem einem Benutzer ein Windows 2000-Sicherheitstoken zugewiesen wird. Wenn der Benutzer die Datenbank ändern möchte, wird das COM+-Geschäftsobjekt mit dem Sicherheitstoken des Benutzers ausgeführt. Wenn also ein Benutzer in der Lage sein soll, seine Daten in der Datenbank zu aktualisieren, muss er über die Berechtigungen für die gesamte Tabelle verfügen. Folglich könnten Benutzer den Inhalt anderer Anmeldungsanwendungen anzeigen, wodurch eine Sicherheitslücke entstünde. Eine Möglichkeit zur Beseitigung dieser Lücke ist zu veranlassen, dass die Benutzer ein COM+-Objekt zum Austauschen von Daten mit der Datenbank verwenden. Obgleich dieser Schritt in den meisten Fällen funktionieren dürfte, führt er in die falsche Richtung. Wenn ein Benutzer mit Hilfe eines COM+-Objekts mit dem Server kommunizieren kann, dann kann der Benutzer mittels eines Datenbankabfrageprogramms unter Verwendung desselben Benutzernamens und Kennworts auch mit der Datenbank kommunizieren. Wiederum könnte der Benutzer alle Einträge anzeigen lassen, für die er über Sicherheitsberechtigungen verfügt.
SQL-Grundlagen
479
Benötigt wird eine Datenbank, die das Aktivieren von Sicherheitstoken auf der Eintragsebene ermöglicht. Leider bieten nur wenige Datenbanken dieses Funktionsmerkmal, das darüber hinaus sehr datenbankspezifisch implementiert wird. Microsoft SQL Server 7.0 bietet z.B. dieses Funktionsmerkmal nicht. Erfreulicherweise kann dieses Problem mit Hilfe gespeicherter Prozeduren gelöst werden. Dazu müssen alle Tabellen einer administrativen Berechtigung zum Anzeigen und Bearbeiten der Daten zugewiesen werden. Die zum Bearbeiten der Daten verwendete gespeicherte Prozedur kann einer Benutzerberechtigung zugewiesen werden. Innerhalb der gespeicherten Prozedur wird die Sicherheitsebene in die administrative Ebene geändert, wodurch die Tabellen angezeigt und bearbeitet werden können. Diese Lösung ermöglicht allen Benutzern den Zugriff auf die gespeicherte Prozedur, nicht jedoch auf die Tabelle. Folglich können die Benutzer die Datenbank weder beschädigen noch Informationen anzeigen, die sie nicht anzeigen sollen. Grund 4: Leistung Unter Grund 1: Einfachheit wurde der Code zur Vereinfachung in zwei Teile aufgeteilt. Ein weiterer Grund dafür ist der Leistungsaspekt, denn kompilierter SQLCode kann wesentlich schneller ausgeführt werden als neuer SQL-Code, der vor der Ausführung erst noch kompiliert werden muss. SQL Server kompiliert Elemente wie Parameter und Wertüberprüfungen. Ferner ermittelt das Programm die schnellste Möglichkeit zum Aufrufen einer Anweisung. Diese Schritte werden zwischengespeichert, wenn gespeicherte Prozeduren verwendet werden. Immer dann, wenn ein neuer SQL-Befehl zum Server gesendet wird, erfolgt eine erzwungene Neukompilierung, was mehr Zeit benötigt. Weitere Überlegungen Die gespeicherte Prozedurarchitektur schließt die Verwendung von Transaktionen nicht aus. Wenn jedoch die gespeicherte Prozedur Transaktionscode hinzufügt, übernimmt sie die Steuerung der Transaktion, was bei bestimmten Operationen wünschenswert ist. Eine Rechnungsnummer ist z.B. inkrementell. Nachdem sie einmal abgerufen wurde, kann sie nicht nochmals verwendet werden, da sich bei einer Überprüfung eine fehlende Nummer ergäbe. Ungültigen Rechnungsnummern müssen besondere Gründe zugewiesen werden. Durch das Verwenden gespeicherter Prozeduren wird die Portabilität zu einem bestimmten Grad eingeschränkt. Dieser Nachteil wird durch die zahlreichen Vorteile jedoch mehr als ausgeglichen.
480
Erstellen einer Ressource
15.2.3 Erstellen einfacher gespeicherter Prozeduren Bislang haben wir nur SQL-Code zum Erstellen einer Tabelle geschrieben. Nun wollen wir eine gespeicherte Prozedur schreiben. Dazu muss ein SQL-Befehl ausgeführt werden. Es folgt die einfachste gespeicherte Prozedur, die erstellt werden kann: Create Procedure simpleProcedure As return (0)
Diese gespeicherte Prozedur hat keine Parameter und gibt nach einem Aufruf lediglich den Wert 0 zurück. Im SQL-Kontext gibt eine gespeicherte Prozedur in der Regel keine Parameter, sondern ein Resultset zurück. Das Resultset bei dieser einfachen gespeicherten Prozedur ist eine Tabelle ohne Spalten und Zeilen. Ein Resultset kann darüber hinaus eine oder mehrere Tabellen mit mehreren Spalten und Zeilen enthalten. In einem Programmiersprachenkontext entspricht dies der Fähigkeit, mehrere zweidimensionale Arrays zurückgeben zu können. Beim Schreiben mehrerer gespeicherter Prozeduren ist es schwierig, sich deren Aufgaben zu merken. In Microsoft SQL Server können gespeicherte Prozeduren nicht gruppiert werden. Sie müssen diese jedoch gruppieren, damit Sie eine bestimmte gespeicherte Prozedur schnell suchen und diese verstehen können. Eine Möglichkeit der Gruppierung gespeicherter Prozeduren ist die Festlegung einer Benennungskonvention. Die von mir verwendete Benennungskonvention lautet: [Bereich][Funktion]
Es folgt ein Beispiel: userGetSimple
Der Begriff Bereich bezieht sich auf den Arbeitsbereich. In der Konferenzanmeldungsanwendung gibt es z.B. einen Arbeitsbereich, der der Bearbeitung von Benutzerdetails dient. Ein geeigneter Bereichstitel ist user. Der Begriff Funktion bestimmt die versuchte Operationen, wie z.B. das Abrufen eines bestimmten Benutzers. In der Konferenzanmeldungsanwendung gibt es einen Unterschied zwischen einfachen Benutzern (simple) und erweiterten Benutzern (full). Demzufolge muss der Name einer gespeicherten Prozedur zum Abrufen eines einfachen Benutzers userGetSimple lauten. Der Vorteil dieser Benennungskonvention besteht darin, dass die gespeicherten Prozeduren entsprechend dem Bereich im Listenfeld mit den gespeicherten Prozeduren gruppiert werden.
SQL-Grundlagen
481
Bestimmen der Versionen gespeicherter Prozeduren Wenn Sie eine gespeicherte Prozedur zur öffentlichen Nutzung freigegeben haben, ist der Code veraltet und muss überarbeitet werden. Wenn Sie eine neue Version der gespeicherten Prozedur erstellen, die wesentliche Unterschiede aufweist, können die Änderungen auf die ursprüngliche gespeicherte Prozedur nicht angewendet werden. Einige Nutzer der ursprünglichen gespeicherten Prozedur benötigen u.U. bestimmte Funktionsmerkmale oder verschiedene vorhandene Parameter. In diesem Fall muss die gespeicherte Prozedur umbenannt werden. Sie können z.B. der neuen Version von userGetSimple die Bezeichnung userGetSimple2 geben. Die alte Version kann weiterhin verwendet werden. Mit der Zeit wird die alte Version immer weniger genutzt und kann dann aus der Datenbank entfernt werden. Parameter Gespeicherte Prozeduren verwenden Parameter, die mittels eines Namens angegeben werden, der mit dem Symbol @ beginnt und auf den der Datentyp folgt. Das Zeichen @ ist wichtig, da damit in SQL Server lokale Variablen angegeben werden. Es wird im Befehl Create Procedure deklariert und hinter dem Namen der Prozedur hinzugefügt. Es folgt ein einfaches Beispiel: Create Procedure clientAdd @firstname varchar(255), @lastname varchar(255), @email varchar(255), @password varchar(64) as return @0
Wenn diese gespeicherte Prozedur aufgerufen wird, müssen alle Parameter explizite Werte aufweisen, die von der aufrufenden Prozedur angegeben werden. Andernfalls kommt es zu einem Funktionsaufruffehler. Um einen optionalen Parameter anzugeben, d.h. einen Parameter, der von der aufrufenden Prozedur nicht angegeben werden muss, können Sie dem Parameter einen Standardwert zuweisen. Die Zuweisung erfolgt in derselben Zeile wie die Deklaration der gespeicherten Prozedur (siehe das folgende Beispiel): Create Procedure clientAdd @firstname varchar(255), @lastname varchar(255), @email varchar(255), @password varchar(64) = "nothing" as return @0
482
Erstellen einer Ressource
In diesem Beispiel wurde dem Parameter @password ein Standardwert zugewiesen. In diesem Fall war der Datentyp eine Zeichenfolge, numerische Datentypen sind jedoch auch möglich. Alle angezeigten Parameter sind Eingabeparameter. In Kapitel 8 wurden die Richtungsparameter in COM IDL erläutert. Dieses Richtungsparameterkonzept kann auch auf SQL-Parameter angewendet werden. Ein SQL-Eingabeparameter ist ein Wert, der nur vom Client zum Server gesendet wird. Wird der Wert eines Eingabeparameters innerhalb der gespeicherten Prozedur geändert, wird die Änderung nicht an den Client zurückgegeben. Damit der Wert eines Parameters an die aufrufende Prozedur zurückgegeben wird, muss ein Richtungsattribut hinzugefügt werden. SQL bietet das Attribut output, das angibt, dass der Parameter nur einen Wert an die aufrufende Prozedur zurückgibt. Mit dem Attribut input wird dagegen angegeben, dass der Parameter von der aufrufenden Prozedur zur Datenbank und wieder zurück gesendet wird. Es folgt ein Beispiel für eine Deklaration des Parameters output: Create Procedure addTwoNumbers @param1 int, @param2 int, @result int output as Select @result = @param1 + @param2
In diesem Beispiel wird dem Parameter @result das Ergebnis der Addition von @param1 und @param2 zugewiesen. Für Zuweisungen muss das Schlüsselwort SELECT verwendet werden. Eine Erläuterung des SQL-Befehls SELECT folgt weiter unten. Rückgabewerte Eine gespeicherte Prozedur kann einen Wert zurückgeben, der den Erfolg der gespeicherten Prozedur angeben kann. Die aufrufende Prozedur nutzt diesen Wert, um den Erfolg oder Misserfolg des Aufrufs der gespeicherten Prozedur anzuzeigen. Rückgabewerte entsprechen nicht Parametern, die einen Wert an die aufrufende Prozedur zurückgeben, sondern werden von der Middleware zum Bestimmen eines Rückgabecodes verwendet. Es folgt ein einfaches Beispiel der Rückgabe eines Fehlerwerts, wenn die Suche nach einem bestimmten Client misslingt: Create Procedure clientDoesExist @usr_name varchar(255) as declare @var int
SQL-Grundlagen
483
select @var = count(*) from clients where usr_name = @usr_name if @var 0 return 0 else return -100
In diesem Fall wird die SELECT-Anweisung zum Zählen der Einträge (count(*)) in der Tabelle mit einem Benutzernamen (user_name) verwendet, die dem Eingabeparameter @user_name entsprechen. Ergibt die Zählung der Einträge 0, wissen wir, dass kein Client vorhanden ist, weshalb der Wert –100 zurückgegeben wird. Sind einer oder mehrere Einträge vorhanden, wird 0 (kein Fehler) zurückgegeben. Obgleich es möglich ist, die tatsächliche Anzahl der Einträge zurückzugeben, ist dieser Vorgang inkorrekt. SQL Server reserviert die Rückgabewerte 0 bis –99. Der Rückgabewert 0 zeigt an, dass die gespeicherte Prozedur erfolgreich war. Der Rückgabewert –100 gibt einen Suchfehler an. Verwenden Sie den Rückgabewert nur dann, wenn Sie einen Fehler zurückgeben möchten.
15.2.4 Einfache Tabellenbearbeitung mit SQL In der Regel erfolgen in gespeicherten Prozeduren die folgenden vier Operationen: Auswählen, Einfügen, Aktualisieren und Löschen von Daten. Dafür stehen die folgenden vier Befehle zur Verfügung: INSERT, SELECT, DELETE und UPDATE. Mit diesen vier Befehlen werden die meisten gespeicherten Prozedur geschrieben. Auswählen von Daten Um den Auswahlvorgang verstehen zu können, müssen Sie das relationale Modell verstehen. Eine Tabelle ist im Grunde eine Auswahl von Daten, die über eine Anzahl von Spalten und Zeilen verfügt. Wenn Sie den Befehl SELECT auf diese Daten anwenden, erstellen Sie eine temporäre Tabelle, die eine Untermenge der Ausgangstabelle darstellt. Wir wollen nun die Daten in der Tabelle Clients untersuchen. Um den gesamten Inhalt der Tabelle abzurufen, müssen Sie diesen Befehl aufrufen: SELECT * FROM clients
Dies ist die einfachste Form des Befehls SELECT. Das Sternchen (*) weist den SQLBefehlsinterpreter an, alle Spalten auszuwählen und im Resultset abzulegen. Das Wort clients gibt die Tabelle an, deren Daten ausgewählt werden sollen. Da keine Einschränkungen angegeben sind, werden alle Einträge in der Tabelle ausgewählt.
484
Erstellen einer Ressource
Um einen bestimmten Client zu suchen, müssen Sie einen SQL-Befehl wie den Folgenden aufrufen: select * from clients where id=123
Die Hinzufügen der Anweisung WHERE gibt an, dass der Befehlsinterpreter nach Zeilen mit dem ID-Spaltenwert 123 suchen soll. Das folgende Beispiel ist ein wenig komplexer: select * from clients where firstname="george" or firstname="mary"
Hier wird nach Zeilen gesucht, in denen firstname entweder george oder mary entspricht. Der Operator OR gibt an, dass die Zeile eines der beiden Kriterien erfüllen muss. Der Operator AND fordert, dass beide Kriterien erfüllt sein müssen. In beiden Beispielen ist es der Gleichheitsoperator, der auf die Spalte angewendet wird. Mit Hilfe der Vergleichsoperatoren (=, , = und ) können Sie die Suche auch auf Bereichen basieren lassen. Durch Verwenden des Operators BETWEEN kann die Auswahl auch auf einem Zeichenfolgenbereich basieren (siehe das folgende Beispiel): select * from clients where firstname between 'c' and 'r'
Hier werden alle Clients ausgewählt, bei denen firstname mit einem Buchstaben von c bis r beginnt. Die Vergleichsoperatoren hätte hier auch verwendet werden können, was jedoch eine komplexere Codierung erfordert hätte. Wird der Operator NOT BETWEEN verwendet, werden alle Elemente außerhalb des angegebenen Bereichs ausgewählt. Um nur die Einträge in einer bestimmten Liste auszuwählen, muss diese Syntax verwendet werden: select * from clients where first_name in ('c', 'r')
In diesem Beispiel werden alle Einträge ausgewählt, deren firstname entweder c oder r lautet. Um eine sinnvollere Rückgabe zu erhalten (wie z.B. Namen, die mit c oder r beginnen), muss die Suche durch Platzhalterzeichen erweitert werden. Dies wird weiter unten unter »Suchen mit Platzhalterzeichen« erläutert. In den obigen SELECT-Beispielen enthielten die Daten im Resultset alle Spalten. Es ist jedoch möglich, die Daten aufzuteilen, sodass nur die Spalten zurückgegeben werden, die Sie für die Datenverarbeitung benötigen. Das folgende Beispiel gibt einen Datensatz mit drei Spalten und eine unbestimmte Anzahl Zeilen zurück. select id, firstname, lastname from clients
SQL-Grundlagen
485
In den vorhergehenden Beispielen wurde das Sternchen (*) verwendet, um anzugeben, dass alle Spalten im Resultset enthalten sein sollten. Das Einsetzen des Sternchens birgt jedoch Tücken, weshalb seine Verwendung überwacht werden muss. Angenommen, die Definition einer Tabelle wird geändert. Wenn Sie eine SELECTAnweisung mit Sternchen verwenden, basiert das Resultset auf der neuen Anzahl der Spalten in der geänderten Tabelle. Dies kann bedeuten, dass einige COM+Geschäftsobjekte nicht mehr funktionieren, da die erwarteten Informationen nicht gefunden werden. Wenn Sie alternativ auf bestimmte Felder in einer SELECT-Anweisung verweisen, wird diese automatisch abgebrochen. Anschließend erhalten Sie eine Datenbankfehlermeldung, wenn Sie versuchen, die Tabelle zu ändern und die Änderungen zu speichern. Einfügen von Daten Ein Großteil der SQL-Entwicklung befasst sich mit dem Auswählen von Daten. Eine weitere wichtige Aufgabe ist das Hinzufügen von Daten zu einer Datenbank. Dieser Vorgang ist einfacher und bietet weit weniger Optionen (siehe das folgende Beispiel): insert into clients ( id, username, password) values (123, "cgross", "cgross")
Dieses Beispiel erstellt einen neuen Eintrag und speichert die Werte 123, cgross und cgross in den Spalten id, username und password. Da nicht alle Spalten angegeben waren, als der Eintrag erstellt wurde, enthalten die anderen Spalten NULL-Werte, es sei denn, für die Spalten wurden Standardwerte angegeben. Wenn die drei Spalten in der vorangehenden INSERT-Anweisung so eingeschränkt wären, dass keine NULL-Werte erlaubt wären, wäre dies die INSERTMindestanweisung, die zum Erstellen eines gültigen Eintrags verwendet werden könnte (obgleich sich die Werte ändern könnten). Ferner ist es möglich, Zeilen abhängig vom Ergebnis einer SELECT-Anweisung einzufügen (siehe das folgende Beispiel): insert into oldclients (id, username, password) select id, username, password from clients where username between "c" and "r"
In diesem Beispiel wird die Tabelle clients in die Sicherungstabelle oldclients, eine nahezu identische Kopie der ursprünglichen Tabelle clients, kopiert.
486
Erstellen einer Ressource
Löschen von Daten Um Einträge aus einer Datenbank zu löschen, verwenden Sie den Befehl DELETE. Der Befehl DELETE ähnelt dem Befehl SELECT, da beide nach bestimmten Zeilen suchen, auf die sie dann angewendet werden. Der Befehl DELETE gibt jedoch keine Resultsets zurück, sondern löscht die Zeilen aus der Tabelle. Es folgt ein Beispiel eines DELETE-Befehls: delete from clients where username='cgross'
Nach der Angabe einer Tabelle entsprechen alle Elemente hinter der Klausel WHERE denen eines SELECT-Befehls. Aktualisieren von Daten Die letzte wichtige Operation ist das Ändern vorhandener Einträge. Dies erfolgt mit dem Befehl UPDATE (siehe das folgende Beispiel): update clients set password='newpassword' where userid=1234
Der Befehl UPDATE entspricht den Befehlen DELETE und SELECT, d.h., ein Eintrag wird basierend auf dem Auswahlergebnis aktualisiert. Sie können entweder eine oder mehrere Zeilen aktualisieren. Der Befehl UPDATE kann auf eine oder mehrere Spalten angewendet werden. Im obigen Beispiel wird nur die Spalte (password) aktualisiert.
15.3 Weitere SQL-Techniken Sie haben nun die Grundlagen von SQL und das Arbeiten mit Resultsets kennen gelernt. Der nächste Schritt beschäftigt sich mit weiteren Operationen, die innerhalb einer gespeicherten Prozedur ausgeführt werden können.
15.3.1 Arbeiten mit Variablen Gespeicherte Prozeduren werden in Microsoft SQL Server in T-SQL (Transact SQL) geschrieben. Sie haben bisher bereits mit T-SQL, einer Erweiterung des SQL 92Standards gearbeitet, ohne sich dessen bewusst zu sein. In T-SQL werden alle Variablen mit dem Symbol @ davor und unter Verwendung der standardmäßigen SQL Server-Datentypen deklariert. Um Variablen im Kontext einer Prozedur zu deklarieren, verwenden Sie das folgende Format: Declare @myVar int
Da SQL als Gruppensatzsprache arbeitet, entspricht das Zuweisen eines Wertes zu einer Variablen nicht dem Zuweisen eines Wertes zu einer Variablen in einer normalen Programmierumgebung. Aus diesem Grund müssen Zuweisungen mit
Weitere SQL-Techniken
487
Hilfe einer SELECT-Anweisung erfolgen. Das grundsätzliche Zuweisungsformat sieht so aus: select @var = 123
In SQL Server 7.0 bietet eine neue Erweiterung die Möglichkeit, lokale Variablen mit der Anweisung SET wie folgt zuzuweisen: set @var = 123
Wie bereits erwähnt, geben SELECT-Anweisungen ein Resultset mit keinem, einem oder mehreren Einträgen zurück. Anschließend arbeiten Sie mit einem Resultset anstatt mit einem einzelnen Eintrag. Es gibt jedoch Situationen, in denen es wünschenswert ist, mit den einzelnen Einträgen in einem Resultset zu arbeiten. Dazu können Sie einen Cursor einsetzen. Beim Erstellen eines Resultset enthält die aufrufende Prozedur einen allgemeinen Verweis auf die Daten, nicht jedoch auf bestimmte Einträge. Ein Cursor verweist auf die einzelnen Einträge, die von der aufrufenden Prozedur bearbeitet werden, und bietet die Möglichkeit, die verschiedenen Wert im Resultset zu durchlaufen. Cursor sind für verschiedene Aufgaben geeignet, dennoch wird ihr Einsatz im Allgemeinen nicht empfohlen, da Cursor im Vergleich zu Programmiercode langsam sind. Cursor werden im weiteren Verlauf dieses Kapitels ausführlich besprochen. Wenn Ihnen bekannt ist, dass das Resultset nur einen Eintrag enthält, müssen Sie keinen Cursor öffnen. Stattdessen können die Variablen innerhalb der SELECTAnweisung wie folgt direkt zugewiesen werden: Select @var = username from clients where id=123
Diese SELECT-Anweisung gibt nur einen Eintrag zurück. Ist dies nicht der Fall, gibt die Variable @var den letzten Eintrag im Resultset zurück. Auf Grund der Funktionsweise von Indizes ist der letzte Eintrag ggf. nicht derjenige, den Sie suchen.
15.3.2 Suchen mit Hilfe von Datumsangaben Das Arbeiten mit Datumsangaben ist ein lästiger, aufwendiger Vorgang, da verschiedene Länder und Computersysteme eigene Datumsformate haben. Aus diesem Grund erstellen Entwickler häufig ein weiteres Datumsformat in einem Zeichenfolgendatenfeld. Obgleich dies die Programmierung erleichtern mag, ist es für Anwendungen nicht sinnvoll, da lediglich ein weiteres proprietäres Datumsformat erstellt wurde. Die einfachste Möglichkeit der Suche mit Datumsangaben ist nicht die Suche nach einem bestimmten Datum, sondern das Durchsuchen eines Bereichs. Die Suche nach einem bestimmten Datum misslingt mitunter, wenn die Suche nicht
488
Erstellen einer Ressource
sorgfältig genug angegeben wurde. Wenn Sie z.B. nach einem Eintrag suchen, der vor zwei Tagen vorgenommen wurde, gibt Microsoft SQL Server nur die Einträge zurück, die exakt vor zwei Tagen, null Minuten und null Sekunden vorgenommen wurden. Eine Bereichssuche jedoch, wie sie im folgenden Beispiel gezeigt wird, gibt das gewünschte Ergebnis zurück: select * from sampleDate where date >= convert( datetime, '1997-03-09') and date < convert( datetime, '1997-03-10')
Diese Methode der Datumssuche ist sehr zuverlässig und funktioniert unabhängig davon, ob das Datum den Spaltentyp datetime oder smalldatetime hat.
15.3.3 Suchen mit Hilfe von Platzhalterzeichen Das Verwenden von Platzhalterzeichen ist eine besonders gut geeignete Methode zum Durchsuchen einer Datenbank. Platzhalterzeichen sind nur dann nützlich, wenn der SELECT-Befehl mit LIKE kombiniert wird. Das Suchkriterium LIKE sucht nach allem, was dem Kriterium nahe kommt. Verglichen mit einem Gleichheitsoperator ist LIKE ein etwas ungenauerer Suchtyp, der sinnvoll für das Durchsuchen einer Spalte mit Zeichenfolgen ist. LIKE ist nur dann effektiv, wenn das Kriterium verschiedene Platzhalterzeichen enthält. Die drei möglichen Platzhalterzeichen werden in der folgenden Tabelle erläutert. Platzhalterzeichen
Beschreibung
%
Gleicht alle Zeichen in der Zeichenfolge ab
_
Gleicht ein einzelnes Zeichen in der Zeichenfolge ab
[]
Gibt einen Bereich gültiger Zeichen in der Zeichenfolge an
Platzhalterzeichen mögen sehr kryptisch erscheinen, liefern jedoch sehr überzeugende Ergebnisse. Es folgt eine Reihe von SELECT-Befehlen samt Ergebnissen: Select * from clients where first_name like "c%"
Gibt alle Vornamen zurück, die mit c beginnen. Select * from clients where first_name like "c%n"
Gibt alle Vornamen zurück, die mit c beginnen und mit n enden. Select * from clients where first_name like "c__"
Weitere SQL-Techniken
489
Gibt alle Vornamen zurück, die mit einem c beginnen, auf das zwei Buchstaben folgen. Select * from clients where first_name like "[Cc]%"
Gibt alle Vornamen, die mit c beginnen zurück, unabhängig von Groß- und Kleinschreibung. Select * from clients where first_name like "[CD]%"
Gibt alle Vornamen zurück, die mit einem großen C oder D beginnen. Select * from clients where first_name like "[c-f]%"
Gibt einen Bereich von Vornamen zurück, die mit c, d, e oder f beginnen. Select * from clients where first_name like "[^c]%"
Gibt alle Vornamen zurück, die nicht mit c beginnen.
15.3.4 Erweiterte Datenänderung Mitunter ist es erforderlich, zusätzliche Datenänderungsoperationen auszuführen, z.B. wenn Sie ein eindeutiges Bezeichnerfeld definieren oder gleichzeitige Aktualisierungen angeben möchten. Einfügen mit sich automatisch erhöhenden Feldwerten Unter »Erstellen einer Tabelle« weiter oben wurde das Festlegen des Hauptindexes der Tabelle Users unter Verwendung eines eindeutigen numerischen Bezeichners erläutert. Beim Hinzufügen von Daten zur Datenbank ist es relativ einfach, einen eindeutigen Bezeichner zu erstellen, indem Sie die Nummer der Einträge in der Tabelle überprüfen und anschließend den eindeutigen Bezeichner des soeben hinzugefügten Eintrag auf einen Wert höher als die Gesamtzahl der Einträge einstellen. Was passiert jedoch, wenn Einträge gelöscht werden? Die Formel zum Ermitteln des eindeutigen Bezeichners funktioniert nicht, was zu Duplikaten führt. Microsoft SQL Server schafft hier mit dem sich automatisch erhöhenden Datenfeld Abhilfe. Das sich automatisch erhöhende Datenfeld stellt sicher, dass alle Einträge einen eindeutigen Wert haben. Wenn Sie einer Tabelle einen neuen Eintrag hinzufügen, die ein sich automatisch erhöhendes Feld enthält, müssen Sie in der SQL-Anweisung INSERT keinen Wert für das Feld eingeben, da der Wert des sich automatisch erhöhenden Felds nicht manuell eingestellt werden kann. Woher sollen Sie also wissen, welchen Wert das Datenfeld hat? Dazu dient die globale Variable @@IDENTITY, mit der Sie den nächsten sich automatisch erhö-
490
Erstellen einer Ressource
henden Wert abrufen können. Sie müssen den Wert direkt nach dem Einfügen eines Eintrags abrufen, da er sich ansonsten ändern kann. Es folgt ein Beispiel der Verwendung von @@IDENTITY: insert into clients ( id, username, password) values (123, "cgross", "cgross") select @@IDENTITY as "Identity"
@@IDENTITY wird nicht als Ausgabeparameter, sondern als Datensatz mit einer Zeile und einer Spalte zurückgegeben. Bei einem Szenario mit mehreren Datenbanken stellt das Verwenden des sich automatisch erhöhenden Felds ein Problem dar. Angenommen, dass es zwei UsersTabellen in zwei verschiedenen Datenbanken gibt und dass sich die ID der beiden Tabellen automatisch erhöht. Wird ein Benutzer der Tabelle Users in der ersten Datenbank hinzugefügt, hat dieser Benutzer die ID 1. Falls in der anderen Datenbank ein anderer Benutzer hinzugefügt wird, so hat auch dieser die ID 1. Falls einer dieser Benutzereinträge in die andere Datenbank repliziert werden soll, hätte er eine andere ID als in seiner ursprünglichen Datenbank. Dies führt dazu, dass Verknüpfungen (JOINs), die mit diesen Benutzern ausgeführt werden, nicht korrekt sind. Hinzufügen von Text- und langen Binärdaten SQL Server unterstützt die Bearbeitung von Text und Grafiken. Beim Bearbeiten dieser in der Regel großen Objekte müssen jedoch bestimmte Faktoren beachtet werden. Das Erstellen einer Variable und das anschließende Speichern von 2 GB Daten muss auf Grund der großen Datenmenge eingehender untersucht werden. Bei diesen großen Objekten handelt es sich entweder um Binärdatenobjekte (Grafik) oder Textdatenobjekte (Text). Beim Lesen und Schreiben dieser Datenfeldtypen werden die Daten nicht wie bei anderen Datenfeldtypen direkt gelesen oder geschrieben. Stattdessen werden die Daten unter Verwendung einer Folge von Funktionen (writeText, readText, updateText) in Form mehrerer Datenblöcke gelesen oder geschrieben. In einem großen Datenfeld wird anstelle der tatsächlichen Daten ein Verweis auf die Daten gespeichert. Der Verweis zeigt wiederum auf den Speicherort in der Datenbank, an dem die eigentlichen Daten gespeichert sind. Dies ist ein Unterschied zu den anderen Datentypen, deren Daten im Eintrag gespeichert werden. Dies ist bei der Entwicklung von Relevanz, da Sie beim Lesen eines Eintrags, der ein großes Datenfeld enthält, nicht den Datenfeldinhalt selbst lesen. Sie müssen die Methoden writeText, readText und updateText verwenden, um Zugriff auf die Daten zu erhalten.
Weitere SQL-Techniken
491
Der Befehl writeText wird folgendermaßen verwendet: declare @ptrField varbinary(16) select @ ptrField = textptr( textField) from testText where id=3 writetext testText.textField, @ptrField, "Hey dude, how’s it going!"
Der Zeiger ist eine 16-Byte-Verweisvariable. Wenn der Eintrag mit Hilfe einer einzelnen SELECT-Anweisung ausgewählt wird, wird der Zeiger mit der Funktion textptr abgerufen. Die nächste Zeile enthält die Funktion writeText, die den Zeiger verwendet, um den Text in den Eintrag zu schreiben. Verhindern gleichzeitiger Aktualisierungen Häufig kommt es in Transaktionsverarbeitungsumgebungen zu Problemen, wenn zwei oder mehrere Personen dieselben Daten ändern. COM+-Transaktionsdienste verhindern zwar, dass zwei Benutzer die Daten zur gleichen Zeit ändern, sie halten sie allerdings nicht davon ab, vorhandene Daten abzurufen, um diese anschließend zu bearbeiten, ohne dass der andere Benutzer die Änderungen sieht. Um dieses Problem zu vermeiden, kann der Tabelle eine Zeitstempelspalte hinzugefügt werden. Ein Zeitstempel entspricht einer Versionskennung, d.h., wenn eine Zeile hinzugefügt oder aktualisiert wird, erfolgt eine entsprechende Aktualisierung der Zeitstempelspalte. Angenommen, Benutzer 1 fügt einen Eintrag mit dem Zeitstempel A hinzu, wobei A eine Datum-/Zeitangabe ist. Benutzer 2 möchte den gerade hinzugefügten Eintrag aktualisieren und führt dazu eine SELECT-Anweisung aus. Zeitgleich entschließt sich auch Benutzer 1, den Eintrag zu aktualisieren. Zu diesem Zeitpunkt verfügen beide Benutzer über einen Eintrag mit dem Zeitstempel A. Benutzer 2 ruft eine gespeicherte Prozedur zum Aktualisieren des Eintrags auf und fügt den Zeitstempel A ein. Die gespeicherte Prozedur erkennt, dass der Zeitstempel des Eintrags und der eingefügte Zeitstempel übereinstimmen, sodass der Eintrag aktualisiert wird und den neuen Zeitstempel B erhält. Eine Millisekunde, nachdem Benutzer 2 den Eintrag aktualisiert hat, versucht Benutzer 1 die Aktualisierung des Eintrags. Da jedoch Benutzer 1 die gespeicherte Prozedur kurz nach Benutzer 2 aufgerufen hat, lässt die Datenbank Benutzer 1 warten, da der gesamte Vorgang im Kontext einer Transaktion ausgeführt wird. Nachdem die Transaktion von Benutzer 2 abgeschlossen ist, gleicht die gespeicherte Prozedur die Zeitstempel ab und merkt, dass der ursprüngliche Benutzer den Zeitstempel A hat, während der Zeitstempel des Eintrags nun B ist. Die gespeicherte Prozedur erkennt, dass ein Problem vorliegt und gibt einen Fehler zurück. Es liegt nun am ursprünglichen Benutzer, den aktualisierten Datensatz abzurufen oder auf die Aktualisierung zu verzichten.
492
Erstellen einer Ressource
Praktisch könnte dies implementiert werden, indem die Tabelle Client mit dem Feld verStamp erweitert wird. Eine getroffene Auswahl sieht wie folgt aus: select id, username, password verStamp from clients where id = @id
Das Feld verStamp wird anschließend bei der Aktualisierung folgendermaßen verwendet: update clients set … where id = @id and tsequal( verStamp, @inpVerStamp)
Wenn nun ein Benutzer einen Eintrag aktualisieren möchte, den ein anderer Benutzer bearbeitet, schlägt dieser Aktualisierungsversuch fehl, und das Geschäftsobjekt kann derweil eine andere Aktion ausführen. Ferner besteht die Möglichkeit, die beiden Felder mod_when und mod_by hinzuzufügen. Diese beiden Felder geben nicht nur an, wann eine Änderung erfolgt ist, sondern auch, wer die Daten bearbeitet hat. Diese Überwachungs- und Sicherheitsfunktionen werden implementiert, um aufzuzeichnen, welche Einträge von welchen Benutzern geändert wurden. Dieser Ansatz ermöglicht darüber hinaus die Implementierung von Sicherheitsmerkmalen auf Zeilenebene. Masseneinfügungen Immer wieder ist es erforderlich, eine große Datenmenge einzufügen, die nicht aus einer relationalen Quelle stammt. Beispiele dafür sind Börsendaten oder von einem Lesegerät eingelesene Daten. In diesen Fällen liegen die Quelldaten häufig in Form einer Datei vor. Die Datei kann der Datenbank entweder unter Verwendung eines COM-Komponententyps oder des Befehls BULK INSERT hinzugefügt werden. Nehmen Sie folgende Anweisung: BULK INSERT Northwind.dbo [Order details] FROM "f:\orders\lineitem.tbl" with ( FIELDTERMINATOR = "|", ROWTERMINATOR = "|\n" )
In diesem Beispiel erfolgt die Masseneinfügung in die SQL Server-Datenbank Northwind. Die angegebene Tabelle heißt Order Details. Der Tabellenname steht in eckigen Klammern, da er ein Leerzeichen enthält, was den Interpreter verwirren würde. Die Angabe FROM gibt die zu ladende Datei an. Das Laden von Dateien ist nicht unproblematisch, da Dateien verschiedene Datentypen und Formate enthalten können. Die möglichen Datentypen sind z.B.
Weitere SQL-Techniken
493
char, native, wide char und wide native. char und wide char sind Textdateien, wobei wide char die Doppelbyte-Zeichenfolgennotation verwendet. Die Datentypen native und wide native sind Dateien im Datenformat native. long wird z.B. mit 4 Bytes gespeichert. Die einzige Tücke dieses Dateityps ist jedoch, das er nur mit dem Dienstprogramm Massenkopieren (BCP) von SQL Server 7.0 erstellt werden kann. Der nächste wichtige Punkt bei Textdateien ist das Festlegen von Trennzeichen. BULK INSERT muss beim Lesen von Dateien wissen, welche Werte Felder und welche Werte Zeilen darstellen. Mit Hilfe von Trennzeichen (festgelegte Zeichen, die Feldund Zeilenumbrüche angeben) ist es möglich, Felder und Zeilen zu bestimmen. Jede Textzeile einer Datei wird durch eine Zeilenendmarke getrennt, die standardmäßig eine neue Datenbankzeile angibt. Das Standardtrennzeichen für Zeilenfelder ist das TAB-Zeichen. Nach Wunsch können Sie jedoch andere Standardtrennzeichen wählen. Im obigen Beispiel werden Felder mit dem Verkettungszeichen (|) getrennt. Der nächste Eintrag wird mit Hilfe von (|) und der Zeilenendmarke (\n) angegeben.
15.3.5 Programmieren in Transact SQL Die bislang vorgestellten SQL Server-Befehle basieren auf Anweisungen. T-SQL hat dagegen weiterentwickelte Konzepte zu bieten. Bei Programmieraufgaben, wie z.B. der Ablaufsteuerung, ähnelt die Sprache BASIC. Ablaufsteuerung Eine sehr wichtige Voraussetzung für das Schreiben gespeicherter Prozeduren ist die Fähigkeit, den Programmablauf steuern zu können. Ablaufsteuerung beschreibt die Fähigkeit zu entscheiden, welche Anweisung basierend auf den Programmergebnissen ausgeführt werden soll. Sie umfasst ferner das Vermögen, die Programmschritte mehrere Male zu durchlaufen. Die einfachste Ablaufsteuerungsanweisung ist die Anweisung if. Mit Hilfe von if ist es möglich, eine Aktion basierend auf einer Entscheidung auszuführen. Nehmen Sie folgendes Beispiel: declare @var int set @var = 21 if @var < 10 print 'The value is less than 10' else if @var >::Open
Kommen wir nun zu den Einsatzmöglichkeiten der verschiedenen Öffnungsmethoden. Wenn Sie die Klasse Cdbousers nur einmal instanziieren und das Resultset nur einmal verwenden, reicht es aus, die Methode Cdbousers.Open aufzurufen. Möchten Sie das Resultset jedoch zu einem späteren Zeitpunkt erneut öffnen, ist es erforderlich, die Aufrufsequenz zu ändern. Beim ersten Öffnen des Resultsets müssen Sie die Methode Cdbousers.Open aufrufen. Schließen Sie das Resultset nach seiner Verwendung, indem Sie Cdbousers.Close aufrufen. Wenn Sie anschließend ein anderes Resultset öffnen möchten, müssen Sie die Methode Cdbousers.OpenRowset verwenden, da die Verbindung bereits geöffnet ist. Der Aufruf von Cdbousers.Open würde aufgrund der geöffneten Datenbank zu einem Verbindungsfehler führen.
17.2.3 Testen der Klassen Der folgende Clientimplementierungscode zeigt die Aufrufsequenz für die Verwendung von OLE DB-Consumer Templates: int main(int argc, char* argv[]) { CoInitialize( NULL); try { Cdbousers users; _com_util::CheckError( users.Open()); if( strcmp( argv[ 1], "-add") == 0) { // We want to add a record to the database strcpy( users.m_email, "[email protected]"); strcpy( users.m_firstName, "Christian"); strcpy( users.m_lastName, "Gross"); strcpy( users.m_password, "something"); users.m_groupId = 0; users.m_status = 1; users.m_id = 1; users.Insert( 0); }
Dieses Programm ruft zunächst user.Open auf, um die Ressource zu öffnen und eine Verbindung zu ihr herzustellen. Das Resultset ist zu diesem Zeitpunkt zwar geöffnet, es ist jedoch nicht an die Klasse CdbousersAccessor gebunden. Da wir einen neuen Datensatz hinzufügen möchten, ist eine Verknüpfung zum ersten Datensatz im Resultset nicht erforderlich. Zum Hinzufügen eines Datensatzes müssen die Daten definiert werden, indem den CdbousersAccessor-Datenmitgliedern Werte zugeordnet werden (m_email m_firstName, usw.). Nachdem die Werte festgelegt wurden, werden die Daten mit users.Insert in die Datenbank eingefügt. Der Parameter 0 in der Methode users.Insert gibt die verwendete Zugriffsroutine an. Dieser Parameter wird im Abschnitt »Verwenden von Mehrfachzugriffsroutinen« dieses Kapitels näher erläutert. Angenommen, Sie möchten der Datenbank keine Daten hinzufügen, sondern das Resultset anzeigen und lesen. Nachdem Sie die Methode users.Open aufgerufen haben, müssen die Daten mit CdboUsersAccessor verknüpft werden. Dies geschieht beim Aufruf der Methode users.MoveNext automatisch. Anschließend enthalten die Datenmitglieder von CdboUsersAccessor einen gültigen Datensatz aus dem Resultset. Im Folgenden wird eine Beispielimplementierung dieses Szenarios dargestellt, die dem vorherigen Codebeispiel eine zweite if-Anweisung hinzufügt: … } else if( strcmp( argv[ 1], "-read") == 0) { while( users.MoveNext() == S_OK) { printf( "Record is %s %s email: %s password: %s\n", users.m_firstName, users.m_lastName, users.m_email, users.m_password); } } …
558
Optimieren der Datenzugriffsschicht
17.3 Aufrufen gespeicherter Prozeduren Im Folgenden wird erläutert, wie Code für OLE DB-Consumer Templates mit Hilfe von gespeicherten Prozeduren und SQL-Befehlen geschrieben wird. Wir verwenden anstelle der Klasse CTable die Ausführungsklasse CCommand, da diese besser mit unserer Architektur harmoniert. Die in diesem Beispiel aufgerufene gespeicherte Prozedur fügt zwei Zahlen hinzu und gibt anschließend den Wert zurück. Im Gegensatz zu ADO, wo für den Rückgabewert ein Resultset mit einer Spalte und Zeile verwendet wurde, nutzt dieser Rückgabewert einen SQL-Parameter. Die Definition der gespeicherten Prozedur addAndReturn lautet wie folgt: Create Procedure addAndReturn @var1 int, @var2 int, @value int OUTPUT As Select @value = @var1 + @var2 return 0
Die gespeicherte Prozedur fügt zwei Zahlen hinzu (@var1 und @var2) und gibt den Ergebniswert im dritten Parameter zurück (@value). Der Rückgabewert 0 kennzeichnet, dass der Prozess richtig ausgeführt wurde. Sie können das OLE DB-Consumer Template für die gespeicherte Prozedur addAndReturn mit Hilfe des Assistenten für OLE DB-Consumer Templates generieren. Der Assistent erzeugt als Ausführungsklasse ein CCommand-Template. Für die Ansichtsklasse wird der folgende Quellcode für die Strukturklasse CAccessor erzeugt: class CdboaddAndReturn1Accessor { public: LONG m_RETURNVALUE; LONG m_var1; LONG m_var2; LONG m_value; BEGIN_PARAM_MAP(CdboaddAndReturn1Accessor) SET_PARAM_TYPE(DBPARAMIO_OUTPUT) COLUMN_ENTRY(1, m_RETURNVALUE) SET_PARAM_TYPE(DBPARAMIO_INPUT) COLUMN_ENTRY(2, m_var1) COLUMN_ENTRY(3, m_var2)
Der erste Teil von CdboaddAndReturn1Accessor ist eine C++-Zuordnung der SQL-Datentypen. Sie repräsentieren in diesem Fall die Parameter der gespeicherten Prozedur. Es sind keine Datenmitglieder des Resultsets enthalten, da der Assistent für OLE DB-Consumer Templates beim Generieren des Codes Resultsetinformationen aus Microsoft SQL Server abgefragt hat. Dies ist nur möglich, wenn der Datenprovider dynamische Abfragen unterstützt, wie dies beispielsweise bei Microsoft SQL Server der Fall ist. Andernfalls erzeugt der Assistent für OLE DBTemplates keine richtige Zugriffsroutinenzuordnung und Sie müssten diese an die Ergebnisse der gespeicherten Prozedur anpassen. Der mittlere Teil der Klasse CdboaddAndReturn1Accessor definiert die Parameterzuordnung der gespeicherten Prozedur. Beachten Sie, dass diese Zuordnung große Ähnlichkeit mit der vorherigen C++-Zuordnung der Resultsetspalten der Datenklasse CdbousersAccessor aufweist, die durch das Makro BEGIN_COLUMN_ MAP definiert werden. Diese Ähnlichkeit ist darauf zurückzuführen, dass OLE DB die Daten der gespeicherten Prozeduren als kontinuierlichen Speicherblock weiterleitet. Der Datenblock der gespeicherten Prozedur im Speicher folgt den gleichen Regeln wie der Datenblock des Resultsets im Speicher. Der Speicherblock der gespeicherten Prozedur verfügt allerdings über eine zusätzliche Optimierung, um zu kennzeichnen, welche Parameter der gespeicherten SQL-Prozedur Eingabewerte und welche Ausgabewerte sind. Die Spalteneinträge (die Zeilen COLUMN_ENTRY) definieren die Parameter der gespeicherten Prozedur. Wenn das Makro SET_PARAM_TYPE verwendet wird, weisen alle Parameter der gespeicherten Prozedur nach der MACRO-Deklaration die gleichen Richtungsattribute auf. Der erste Parameter im zuvor angeführten Code, m_RETURNVALUE, ist beispielsweise eine Ausgabe. Beim zweiten Parameter, m_var1, handelt es sich um eine Eingabe. Der dritte Parameter verfügt über keine Deklaration, es wird daher die Deklaration des letzten Parametertyps verwendet, in diesem Fall Eingabe. Sind in der Spaltendeklaration keine Parameterdeklarationen angegeben, wird standardmäßig von einer Eingabe ausgegangen. Im letzten Teil der Struktur, dem Makro DEFINE_COMMAND, wird der Standardbefehl definiert, der beim Aufruf der Methode CdboaddAndReturn1.Open ausgeführt
560
Optimieren der Datenzugriffsschicht
wird. Mit Hilfe dieses Makros kann ein beliebiger Datenbankbefehlstyp definiert werden. In diesem Beispiel wird eine gespeicherte Prozedur aufgerufen, es wäre jedoch ebenso möglich, einen SQL-SELECT-Befehl auszuführen.
17.3.1 Ausführen des Befehls Im Folgenden wird erläutert, wie die Klasse CdboaddAndReturn1Accessor in einem Beispielclient verwendet wird. Sie werden feststellen, dass dieses Beispiel dem CTable-Beispiel im Abschnitt »Öffnen der Tabelle« ähnelt. Der Unterschied besteht lediglich darin, dass die Parameter der Klasse CdboaddAndReturn1Accessor gesetzt sein müssen, wenn die Methode CdboaddAndReturn1.Open aufgerufen wird. Sehen Sie sich die folgende Implementierung an: CoInitialize( NULL); try { CdboaddAndReturn1 var; var.m_var1 = 2; var.m_var2 = 3; _com_util::CheckError( var.Open()); printf("The answer of adding %ld and %ld is %ld\n", var.m_var1, var.m_var2, var.m_value); var.Close(); } catch( _com_error err) { printf(" Oooops an error happened.... %s\n", err.ErrorMessage()); } CoUninitialize();
17.3.2 Verwenden von Mehrfachzugriffsroutinen Es wurden zwei Arten von Spaltenzuordnungen dargestellt, Parameterzuordnungen und Resultsetzuordnungen. Ich möchte nun auf einige Details der Resultsetzuordnungen eingehen. Erinnern Sie sich an das Musikbeispiel. In diesem Beispiel wurde verdeutlicht, dass Sie mit OLE DB-Consumer Templates Resultsets bei Bedarf verknüpfen können. In den bisher angeführten Beispielen erfolgte die Bindung immer automatisch. Durch das Untersuchen der Details einer Resultsetzuordnung erfahren Sie, wie Daten manuell verknüpft werden können.
Aufrufen gespeicherter Prozeduren
561
Öffnen statischer Zugriffsroutinen Wir kommen nun zur C++-Datenklasse CdbousersAccessor zurück. Die zweite Hälfte der Klassendeklaration enthielt die Makrodeklaration BEGIN_COLUMN_MAP. Dieses Makro beinhaltet Folgendes: #define BEGIN_COLUMN_MAP(x) \ BEGIN_ACCESSOR_MAP(x, 1) \ BEGIN_ACCESSOR(0, true)
Das Makro BEGIN_COLUM_MAP wird verwendet, um eine einzelne Zugriffsroutinenzuordnung zu definieren, das Makro BEGIN_ACCESSOR_MAP öffnet die Zuordnung für die Mehrfachzugriffsroutine. Der erste Parameter von BEGIN_ACCESSOR_MAP ist der Name der Klasse, die die verschiedenen Datenmitglieder enthält, durch die das Resultset an Speicherblock gebunden wird. Der zweite Parameter von BEGIN_ ACCESSOR_MAP definiert die Anzahl der Zugriffsroutinen in der Zuordnung. Das Makro BEGIN_ACCESSOR definiert den Beginn einer Zugriffsroutine, der erste Parameter von BEGIN_ACCESSOR gibt die ID der Zugriffsroutine an. Die ID wird verwendet, wenn das Resultset an den Speicherblock gebunden wird, der durch die C++-Datenklasse definiert wurde. Der zweite Parameter von BEGIN_ACCESSOR legt fest, ob eine automatische Bindung verwendet wird. Beim Verwenden einer Zugriffsroutine werden die Daten an den Konsumenten gebunden. Bei der Ausführung eines Befehls wird auf dem Datenbankserver ein Resultset erzeugt, und OLE DB erhält einen Verweis auf dieses serverseitige Resultset. Im nächsten Schritt werden die Daten vom Datenbankserver zum Konsumenten verschoben. Dies wird als Bindung eines Resultsets an einen lokalen Speicherblock bezeichnet. Der Konsument bestimmt, wie die Daten an den Speicherblock gebunden werden. Mit OLE DB-Consumer Templates kann der zweite Schritt verzögert und manuell definiert werden. Wenn für die automatische Bindung FALSE eingestellt wurde, bleibt das Resultset als Verweis bestehen. Wird das Resultset mit einer C++-Datenklasse verknüpft, legt die Zugriffsroutine fest, welche Datenfelder des Resultsets verbunden werden. Definieren einer einfachen Mehrfachzugriffsroutine Wir implementieren nun die Anwendung zur Musikauswahl. Sehen Sie sich die folgende Musiktabellendefinition an: CREATE TABLE [dbo].[Music] ( [id] [int] IDENTITY (1, 1) NOT NULL , [song_name] [varchar] (255) NOT NULL , [description] [varchar] (512) NOT NULL ,
562
Optimieren der Datenzugriffsschicht
[song] [binary] (10) NULL , [genre] [varchar] (64) NOT NULL , [artist] [varchar] (64) NOT NULL)
Die Tabelle speichert die gesamte MP3-Audiodatei im Datenfeld song. Der Typ des Datenfeldes ist binary. Wir möchten jedoch zwei Zugriffsroutinen erstellen: Mit der ersten sollen Informationen zum Lied abgerufen werden, mit der zweiten das Lied selbst. Das Lied kann entweder mit einer SELECT-Anweisung oder mit einer gespeicherten Prozedur abgerufen werden. Da Sie bereits erfahren haben, wie Sie mit Hilfe des Objekts CCommand eine gespeicherte Prozedur aufrufen, verwenden wir im Folgenden eine parametrisierte SELECT-Anweisung. Diese Methode wird bei der Abfrage einer SQL-Sicht eingesetzt. Die C++-Datenklasse wird wie folgt implementiert: class CMusicAccessor { public: LONG m_id; TCHAR m_songname[ 255]; TCHAR m_description[ 64]; TCHAR m_genre[ 64]; TCHAR m_artist[ 64]; ISequentialStream*
DEFINE_COMMAND(CMusicAccessor, _T(" \ SELECT id, song_name, description, genre, artist, song \ FROM Music WHERE genre = ?")) };
Diese Klasse zeigt, wie eine Parameterzuordnung mit einer Resultsetzuordnung kombiniert wird. Der SQL-Befehl wird im Makro DEFINE_COMMAND definiert, und die SELECT-Anweisung beinhaltet die SQL-Anweisung WHERE, durch die eine Parametrisierung erfolgt. Im Falle der Klasse CMusicAccessor wählt der Parameter das Musikgenre aus, definiert durch das Fragezeichen. Wenn die Klasse CMusicAccessor mit einer Ausführungsklasse verbunden ist, erzeugt die SELECTAnweisung ein sechsspaltiges Resultset. Um dieses Resultset jedoch erstellen zu können, wird an den Server ein Parameter gesendet, der das Musikgenre kennzeichnet. Die Parameterzuordnung wird durch das Makro BEGIN_PARAM_MAP definiert und benötigt nur einen Spalteneintrag, um den Parameter genre wiederzugeben. Dieses Datenelement wird sowohl von der Parameterzuordnung als auch von der Spaltenzuordnung verwendet. Da die Parameterzuordnung festlegt, dass genre ein reiner Eingabeparameter ist, ist dies jedoch kein Problem. Wäre genre ein Ausgabeparameter, hätte ein zusätzliches Datenmitglied angegeben werden müssen – andernfalls würde zwar kein Fehler auftreten, aber der Speicher würde beim Binden des Resultsets an die Datenelemente überschrieben. Betrachten wir nun die Resultsetzuordnung (BEGIN_ACCESSOR_MAP). Es existieren zwei Zugriffsroutinen mit den IDs 0 und 1. Die Zugriffsroutine 0 bindet das Resultset automatisch. Sie enthält die Datenfelder m_id (Music.id), m_songname (Music.song_name), m_description (Music.description), m_genre (Music.genre) und m_artist (Music.artist). Das Resultset beinhaltet darüber hinaus das binäre Datenfeld des Liedes. Da dieses jedoch Teil der zweiten Zugriffsroutine ist, wird es erst nach Anforderung verbunden. Die Zugriffsroutine 1 enthält nur ein Datenfeld, pSong (Music.song). Der Unterschied zwischen den beiden Zugriffsroutinen besteht darin, dass der Datenfeldindex (der zweite Parameter von COLUMN_ENTRY) auf dem Standort des Resultsets des SQL-Befehls basiert. CMusicAccessor ist mit der CCommand-basierten Klasse CMusic verbunden. Da sie die gleiche Funktionalität wie die zuvor genannten CCommand-basierten Ausführungsklassen aufweist, wird die Implementierung von CMusic nicht dargestellt. Wenn der Konsument CMusic instanziiert und CMusic.Open aufruft, wird automatisch die Zugriffsroutine 0 an das Resultset gebunden. Zum Abruf der Zugriffsroutine 1 müssen Sie Methode CMusic.GetData mit der ID der zu bindenden Zugriffsroutine aufrufen. Eine Beispielimplementierung lautet folgendermaßen:
564
Optimieren der Datenzugriffsschicht
CMusic music; strcpy( music.m_genre, "Techno"); music.Open(); music.MoveFirst(); do { printf( "Music song is %s, %s\n", music.m_artist, music.m_songname); if( music.m_id > 0) { music.GetData( 1); // Do something with the accessor } } while (music.MoveNext() == S_OK); }
Beim Durchlaufen des Resultsets muss für jede Änderung des Datensatzes die Methode Music.GetData aufgerufen werden, da der Inhalt nach Bewegung des Cursors zwischen den Datensätzen nicht automatisch aktualisiert wird. Der einzige Schritt, der bisher außer Acht gelassen wurde, ist die Bearbeitung von Binärdaten. Dieses Thema wird in einem späteren Abschnitt im Rahmen der Erläuterung von BLOB-Daten (Binary Large Objects) behandelt.
17.3.3 Weitere Informationen zu COLUMN_ENTRY Bisher wurden ausschließlich einfache Spalten mit einfachen Speichervariablen definiert. Die COLUMN_ENTRY-Standardmakros können C++-Datentypen automatisch mit SQL-Datentypen verknüpfen. In gewissen Situationen müssen Sie jedoch steuern, wie SQL-Daten im Speicher verwaltet oder in C++-Datentypen konvertiert werden. Sie können zu diesem Zweck Spaltenmakros einsetzen, von denen im Folgenden einige erläutert werden. COLUMN_ENTRY_EX In manchen Fällen muss ein SQL-Datentyp explizit einem C++-Datentyp zugeordnet werden, z.B. wenn Sie einen numerischen SQL-Datentyp mit einem zeichenbasierten C++-Datentyp char verbinden möchten. Die Spalteneinträge benötigen in diesem Fall eine explizite Datentypzuordnung. Dabei wird folgendes Makro verwendet: COLUMN_ENTRY_EX(nOrdinal, wType, nLength, nPrecision, nScale, data, length, status)
Aufrufen gespeicherter Prozeduren
565
Dieses Makro definiert die Spalteneigenschaften des Konsumenten. Wenn beispielsweise length angegeben wurde, verweist dies auf das C++-Datenklassenmitglied. Der Parameter nOrdinal kennzeichnet den Standort des Feldes im Resultset oder den Parametersatz, der bei 1 beginnt. Der Parameter wType definiert die Art der gebundenen Variablen. Es kann sich bei der Bindung um eine Zeichenfolge, ein Datum, eine GUID (Globally Unique Identifier) oder reelle Zahlen handeln. Sie basiert auf dem OLE DB-Datentypzähler DBTYPEENUM. Sie können COM-Schnittstellen (DBTYPE_IUNKNOWN und DBTYPE_IDISPATCH) als Datenfelder definieren, Sie sollten jedoch in der Datenbankdokumentation überprüfen, welche COM-Schnittstellen offen gelegt sind. Der Parameter nLength wird verwendet, um die Länge der Zeichenfolge zu definieren, die von der C++-Datenklasse offen gelegt wird. Mit Hilfe der Parameter nScale und nPrecision können Sie eine eigene Sicht für numerische SQL-Datentypen erstellen. Der Parameter length gibt die tatsächliche Länge der gebundenen Daten an. Der Parameter status wird im Abschnitt »COLUMN_ENTRY_ STATUS« erläutert. COLUMN_ENTRY_TYPE Dieser Spalteneintrag entspricht dem regulären Makro COLUMN_ENTRY, es wird jedoch ein bestimmter Datentyp definiert. Wenn Ihr Datentyp beispielsweise eine Neudefinition eines anderen Basistyps ist, erzeugt der C++-Template Compiler ggf. einen Fehler, da keine sicherer Typecast ausgeführt werden kann. Da Sie den Basistyp kennen, können Sie den vorzunehmenden Typecast angeben. Das Makro ist wie folgt definiert: COLUMN_ENTRY_TYPE(nOrdinal, wType, data)
Der Parameter wType gibt die Datenmitgliedsvariable an. Dieser Parameter ist eine Deklaration des Typs DBTYPEENUM. COLUMN_ENTRY_LENGTH In bestimmten Situationen ist die Länge des C++-Datentyps unbekannt. In der Regel ist dies darauf zurückzuführen, dass der C++-Template Compiler die Länge nicht ermitteln kann oder dass Sie die Grenzen eines generischen Puffers festlegen. Zur expliziten Längendefinition eines C++-Datentyps wird das folgende Makro verwendet: COLUMN_ENTRY_LENGTH(nOrdinal, data, length)
Der Parameter length gibt die Länge des C++-Datenmitglieds an.
Dieses Makro kombiniert COLUMN_ENTRY_TYPE und COLUMN_ENTRY_ LENGTH, sodass Typ und Länge festgelegt werden können. Es ist einfacher zu verwenden als das Makro COLUMN_ENTRY_EX, da Sie nicht alle Details der Spalte oder des Parameters angeben müssen. COLUMN_ENTRY_STATUS Wenn Sie Daten aus einem Resultset abrufen, kann dies unter Umständen misslingen. Dies kann auf ein bestimmtes Datenfeld zurückzuführen sein. In der zuvor genannten Musikanwendung kann beispielsweise die Binärabfrage fehlschlagen. Oder bei der Datenfeldkonvertierung werden einige wichtige Zeichen abgeschnitten. Wenn Sie generische Fehler verwenden, kann nicht festgestellt werden, welche Felder den Fehler verursacht haben. Mit den OLE DB-Consumer Templates sind Sie jedoch in der Lage, dem Zuordnungseintrag COLUMN_ENTRY ein Statusflag hinzuzufügen. Dieses Statusflag zeigt an, ob die Datenfeldabfrage erfolgreich war oder ob sie teilweise oder vollständig fehlgeschlagen ist. Ein statusaktiviertes Spalteneintragsmakro kann beispielsweise folgendermaßen aussehen: COLUMN_ENTRY_STATUS(nOrdinal, data, status)
Der Typ des Parameters status lautet DBSTATUSENUM. Er ist vor allem bei der Konvertierung von Datentypen hilfreich. Dies ist nur ein Beispiel eines statusaktivierten Spalteneintragsmakros. Es gibt zahlreiche weitere statusaktivierte Spalteneintragsmakros (z.B. COLUMN_ENTRY_ LENGTH_STATUS). Die Benennungskonvention sieht vor, an Namen des Spalteneintragsmakros das Suffix _STATUS und anschließend das Statusflag anzuhängen. BOOKMARK_ENTRY Wenn Sie mit einem Resultset arbeiten, werden Sie wahrscheinlich in diesem blättern oder die Fähigkeit besitzen, den Cursor an eine vorherige Stelle zurückbewegen. Um den Cursor an einer bestimmten Stelle zu positionieren, können Sie ein Lesezeichen verwenden. Dabei handelt es sich um einen Spalteneintrag, der der Parameterzuordnung des Resultsets hinzugefügt wird. Das Makro ist wie folgt definiert: BOOKMARK_ENTRY(variable)
Aufrufen gespeicherter Prozeduren
567
Der Lesezeichenparameter variable ist eine Vorlage vom Typ CBookmark. Bei der Definition des Templates müssen Sie die Puffergröße des Lesezeichens angeben. Diese richtet sich nach der verwendeten Datenbank. Wenn die Puffergröße dynamisch zugewiesen werden soll, können Sie den Wert 0 übergeben. Sie wird so von CBookmark ermittelt. Im Folgenden wird die Musikanwendung so erweitert, dass Lesezeichen gesetzt werden können und der Benutzer zu einem späteren Zeitpunkt an diese Stelle zurückkehren und markierte Lieder hören kann. Die überarbeitete C++-Datenklasse lautet wie folgt: class CMusicAccessor { public: LONG m_id;
Das Lesezeichenmakro wird der Zugriffsroutine 0 hinzugefügt, sodass jede Positionierung an dieser Stelle eine automatische Bindung der Zugriffsroutine 0 auslöst. In diesem Beispiel wurden für CBookmark 4 Byte festgelegt, da das Lesezeichen in Microsoft SQL-Server 4 Byte beträgt und in diesem Buch Microsoft SQL Server ausgeführt wird. Wenn Sie die Länge nicht kennen, verwenden Sie eine Standardlänge von Null. Bei der Erstellung eines Resultsets werden standardmäßig keine Lesezeichenverweise definiert. Die Fähigkeit zum Erstellen von Lesezeichen muss dem Resultset in Form einer OLE DB-Eigenschaft hinzugefügt werden. Bei Klassen, die von den OLE DB-Consumer Templates generiert werden, werden die Eigenschaften dem OpenRowset der Ausführungsklasse hinzugefügt. Dies wird im folgenden Beispiel dargestellt: class CTestBookmark : public CCommand { public: HRESULT OpenRowset() { CDBPropSet
Die beiden Eigenschaften DBPROP_BOOKMARKS und DBPROP_IRowsetLocate müssen auf TRUE gesetzt werden. Die Eigenschaft DBPROP_IRowsetLocate muss aktiviert werden, da sie das Suchen in Vorwärts- und Rückwärtsrichtung ermöglicht. Um diese Klasse in einem Konsumenten tatsächlich zu nutzen, müssen Sie im Clientprogramm ein lokales Lesezeichen speichern. Das Lesezeichen der C++-Datenklasse ändert sich bei jeder Änderung der Cursorposition, es ist daher erforderlich, den Lesezeichenwert zu speichern. Deklarieren Sie zum Speichern eines Lesezeichens eine Variable vom Typ CBookmark. Um den Cursor zu einem gespeicherten Lesezeichen zu bewegen, rufen Sie die Methode CCommand.MoveToBookmark auf. Wenn wir die Musikanwendung bearbeiten, erhalten wir den nachstehend gezeigten Code. Ich habe an dieser Stelle aus Platzgründen Teile des Codes ausge-
Aufrufen gespeicherter Prozeduren
569
lassen, den vollständige Quellcode finden Sie auf der Begleit-CD-ROM zu diesem Buch. CMusic music; CBookmark< 4> bookmark; strcpy( music.m_genre, "Techno"); music.Open(); // Löschen der C++-Datenklasse und Öffnen des Resultsets do { // Durchlaufen der verschiedenen Songs if( music.m_id == 2) { // Speichern des aktuellen Lesezeichens bookmark = music.m_bookmark; } } while (music.MoveNext() == S_OK); music.MoveToBookmark( bookmark); }
Noch eine letzte Bemerkung zu Lesezeichen. Die in den Lesezeichen gespeicherten Werte gelten nur, solange das Resultset geöffnet ist. Wenn Sie das Resultset schließen, werden alle Lesezeichen ungültig.
17.4 Arbeiten mit BLOB-Daten Der letzte noch zu definierende Abschnitt der Musikanwendung ist der, in dem ein Lied abgerufen wird. Es ist nicht möglich, ein Standardtextfeld zu verwenden und zu hoffen, das es in einen Speicherblock passt, da manche Lieder wesentlich länger sind als andere. Daher ist ein Streaming der Feldinhalte erforderlich. Das Streaming erfolgt über eine COM-Schnittstelle, die Informationsblöcke aus dem Puffer abrufen kann. Das BLOB-Spalteneintragsmakro verweist auf einen COMSchnittstellenzeiger, mit dessen Hilfe der Inhalt eines SQL BLOB-Datenfeldes abgerufen wird. Das Makro ist wie folgt definiert: BLOB_ENTRY(nOrdinal, IID, flags, data)
Der zweite Parameter lautet IID, d.h. bei dem Datenfeld handelt es sich nicht um einen Teil des Speichers, sondern um einen COM-Schnittstellenzeiger. OLE DB führt unter Verwendung von COM-Schnittstellen ein Streaming von Daten durch. Meistens wird zu diesem Zweck die COM-Schnittstelle ISequentialStream eingesetzt.
570
Optimieren der Datenzugriffsschicht
Der Parameter flags gibt an, wie die Daten bearbeitet werden. Es gibt drei Optionen: Zum Lesen von Daten verwenden Sie STGM_READ, zum Schreiben STGM_WRITE und zum Lesen und Schreiben STGM_READWRITE. Wir betrachten nun die Klasse CMusicAccessor und die Implementierung des Makros BLOB_ENTRY. Hier wurden ebenfalls einige Teile des Codes aus Platzgründen ausgelassen, den vollständige Code finden Sie auf der Begleit-CD-ROM zu diesem Buch. class CMusicAccessor { public: ISequentialStream*
Die Daten sind zum Durchsuchen des Resultsets an die Zugriffsroutine 0 gebunden. Die Zugriffsroutine ist nicht an die Daten in der Datenbank gebunden, sondern führt zur Instanziierung einer IsequentialStream-Schnittstelle und kennzeichnet diese als schreibgeschützt (STGM_READ). Diese COM-Schnittstelle verfügt über eine Methode, die es dem Clientcode ermöglicht, Inhalte des Datenfeldes in Datenblöcken abzurufen. Die Daten werden wie folgt abgerufen: if( music.m_id > 0) { BYTE myBuffer[ 65536]; ULONG cb; music.GetData( 1); if( music.pSong != NULL) { music.pSong->Read(myBuffer, 65536, &cb); music.pSong->Release(); } }
Da das Datenfeld des Liedes eine COM-Schnittstelle ist, wird die Streamingschnittstelle wie alle COM-Objekte als Verweis angegeben. Dies bedeutet, dass Sie die Schnittstelle nach Verwendung mit Hilfe der standardmäßigen COM-Me-
Arbeiten mit BLOB-Daten
571
thode Release freigeben müssen. Andernfalls treten, je nach Datenbankprovider, merkwürdige Fehler auf. Darüber hinaus können Sie die COM-Schnittstellenzeiger durch den Aufruf von CMusic.FreeRecordMemory freigeben. Diese Möglichkeit ist etwas sicherer, da geprüft wird, ob auf BSTRs verwiesen wurde, die ebenfalls freigegeben werden müssen.
17.5 Massenabruf von Datensätzen Beim Lesen von Daten wird jeweils ein Handle verwendet. Es besteht die Möglichkeit, mehrere Datensätze abzurufen und diese gleichzeitig zu bearbeiten. Zur Verwendung dieser Klasse müssen zwei Änderungen vorgenommen werden. Zunächst muss das Template CCommand wie folgt deklariert werden: CCommand accessor;
Im Gegensatz zu den bisherigen Beispielen verwendet der zweite Parameter des CCommand-Templates CBulkRowset. Der Parameter wird von CRowset abgeleitet und ist diesem sehr ähnlich. Der einzige Unterschied besteht darin, dass Sie angeben können, wie viele Zeilen pro Aufruf abgerufen werden sollen. Der Standardwert lautet 10, Sie können diesen jedoch ändern: accessor.SetRows( 30);
In diesem Beispiel werden beim Abruf mehrerer Datensätze jeweils 30 Zeilen geladen. Der Clientcode beinhaltet weiterhin die gleichen Navigationsmethodenaufrufe (MoveFirst, MoveNext, usw.). Es ändert sich nur das Verhalten des Rowsets. Wird der Cursor auf die erste Position bewegt, werden die Datensätze 1 bis 30 geladen. Wenn Sie zu Datensatz 30 und anschließend zu Eintrag 31 navigieren, wird eine neue Datenmenge mit 30 Datensätzen geladen. Der Benutzer kann so Datensätze relativ schnell seitenweise durchlaufen. Der Haken ist, dass dies nur funktioniert, wenn Sie nicht zwischen den Datensätzen 30 und 31 wechseln. Sobald Sie die Grenzen des Rowsets überschreiten, werden neue Daten in den Speicher geladen – es gibt keine Lösung für dieses Problem.
17.6 Resümee Durch einen Freund von mir, der für die Klassen der OLE DB-Consumer Templates zuständig war, lernte ich viel über die Strukturierung eines Projekts. Darüber hinaus habe ich erfahren, dass die OLE DB-Consumer Templates zwar einfach, aber dennoch leistungsfähig sind.
572
Optimieren der Datenzugriffsschicht
Sie können OLE DB-Consumer Templates für sehr viele Dinge einsetzen. Diese Flexibilität ist darauf zurückzuführen, dass für eine enorm große Website mit Multimediainhalten OLE DB-Consumer Templates benötigt wurden. ADO war für diese Aufgabe nicht schnell genug. Als Visual C++-Entwickler nutze ich die OLE DB-Consumer Templates, ich habe sie jedoch an meine spezifischen Anforderungen angepasst. Dies ist auf die von mir definierte Architektur zurückzuführen, die zahlreiche gespeicherte Prozeduren umfasst. Hier konnten einige Verbesserungen eingebracht werden. Meine Optimierung ermöglicht beispielsweise, in die Ausführungsklasse anstelle eines einzelnen SQL-Standardbefehls mehrere SQL-Befehle zu integrieren. Darüber hinaus wurde eine Zugriffsroutine geschrieben, die direkt mit COM-Objekten und nicht mit einzelnen Datenstrukturen arbeitet. Diese Lösungen haben die OLE DBConsumer Templates nicht beeinträchtig, im Gegenteil: Sie haben sie erweitert. Dieses Beispiel zeigt, wie leistungsfähig und flexibel OLE DB-Consumer Templates wirklich sind. Soll das heißen, dass Sie bei Ihren Projekten als Erstes benutzerdefinierte Erweiterungen der OLE DB-Consumer Templates erstellen sollen? Nein. Nutzen Sie zunächst die vorhandenen Templates, und sammeln Sie Erfahrungen. Anschließend können Sie die Erweiterungen schreiben, die Sie benötigen.
Resümee
573
18 Verzeichnisdienste Ein Verzeichnisdienst dient dazu, Informationen in einer verteilten Computerumgebung aufzufinden. Die meisten Firmen besitzen ein Telefonverzeichnis, dessen Umfang von einer Seite bis hin zur Größe eines Buches schwankt. Nehmen Sie einmal an, dass Ihr Unternehmen regelmäßig mit einer anderen Firma kommuniziert, die ebenfalls ihr eigenes Telefonverzeichnis besitzt. Beide Unternehmen würden wahrscheinlich öffentliche Versionen ihrer Firmentelefonverzeichnisse austauschen. Nehmen Sie weiter an, dass noch eine andere Firma dazu käme, die ihr Telefonverzeichnis mit den beiden anderen austauschen würde. Die Datenmenge würde sehr schnell auf unübersichtliche Dimensionen anwachsen. Wie könnte man sie verwalten? Die Lösung ist ein sogenannter »Verzeichnisdienst«. Dieses Verzeichnis speichert nicht alle Telefonnummern aller Unternehmen, sondern nur das Telefonbuch eines einzigen Unternehmens. Diese Verzeichnis wird dann angewiesen, mit einem anderen Unternehmen zu kommunizieren, und die beiden Verzeichnisse werden zu einem umfangreicheren Verzeichnis kombiniert. Jedes Unternehmen ist jedoch nur für die Verwaltung seiner eigenen Daten verantwortlich. Ein Verzeichnisdienst verwaltet die Details, wo sich die Informationen befinden und wie diese zu einem größeren Verzeichnisdienst kombiniert werden. In Windows 2000 hat Microsoft das so genannte Microsoft Active Directory geschaffen, um Informationen für uns zu verwalten. In diesem Kapitel wird erläutert, was das Microsoft Active Directory ist, und wie es in eigenen Programmen genutzt werden kann.
18.1 Architektur von Microsoft Active Directory Das Microsoft Active Directory ist ein hierarchischer Datenspeicher. Er wurde speziell für häufiges Lesen von Daten optimiert. Innerhalb des Datenspeichers befinden sich Objekte, die in Beziehung zu jeweils einem übergeordneten und mehreren untergeordneten Objekten stehen. Es stehen verschiedene Standardprogrammierschnittstellen zur Verfügung, über die sich die Objekte innerhalb des Datenspeichers ändern lassen. Dazu gehören beispielsweise LDAP (Lightweight Directory Access Protocol) und ADSI (Active Directory Service Interfaces). Die Daten innerhalb von Active Directory können in verschiedenen Verzeichnisspeichern auf verschiedenen Computern repliziert werden. Die Architektur von Active Directory ist durch flexible Verbindungen gekennzeichnet. Wenn zwei Da-
Architektur von Microsoft Active Directory
575
tenspeicher vorhanden sind, kann jeder der beiden aktualisiert werden, ohne dass der andere sofort aktualisiert werden muss. Wenn die Daten zwischen zwei Datenspeichern repliziert werden, kümmert sich Active Directory automatisch um die Konvergenz der Daten.
18.1.1 Active Directory-Objekte Active Directory enthält zwei grundlegende Elemente: Klasse und Attribut. Auf diesen beiden Grundelementen ist die gesamte Struktur von Active Directory aufgebaut. Das Attribut ist ein Schlüsselwertpaar. So könnte beispielsweise ein Attribut name den Namen eines Benutzers, Computers oder Produkts darstellen. Zu diesem Zeitpunkt hat das Attribut noch keine Bedeutung. Diese erhält sie erst durch die Zuordnung eines Ressourcenbereichs. Dabei kann es sich um eine Zeichenfolge oder um numerische Daten handeln, es ist jedoch immer ein Active Directory-Datentyp. Bei Bedarf kann das Attribut auch Maximal- und Minimalwerte besitzen. Diese haben abhängig vom Datentyp des Attributs unterschiedliche Bedeutungen. Bei einer Zeichenfolge beziehen sich die Maximal- und Minimalwerte beispielsweise auf die Länge der Zeichenfolge. Bei einem numerischen Feld bezeichnen sie dagegen den größtmöglichen und den kleinstmöglichen Wert, den dieses Feld annehmen kann. Die Klasse ist eine Gruppe von Attributen. Eine Klasse in Active Directory hat nichts mit der Klasse in einer Programmiersprache zu tun. Eine Active DirectoryKlasse bezieht sich auf Attribute, die Attribute werden jedoch nicht in der Klasse selbst deklariert. Deshalb können sich mehrere Klassen auf dasselbe Active Directory-Attribut beziehen, wie in Abbildung 18.1 gezeigt.
Klasse
Attribut Attribut Attribut
Klasse
Attribut
Abbildung 18.1 Beziehung zwischen Klassen und Attributen
576
Verzeichnisdienste
Wenn eine Klasse instanziiert ist, wird sie als Active Directory-Objekt bezeichnet. Jedes Active Directory-Objekt muss eine Klassen- und Attributdefinition repräsentieren. Active Directory-Objekte können nicht ohne Typinformationen erstellt werden. Die Informationen über diese Klassen, Attribute und ihre Merkmale werden im Active Directory-Schema gespeichert. Das Schema setzt sich aus zwei Teilen zusammen: den definierten Attributen und den definierten Klassen. Die Einzelheiten eines Active Directory-Objekts Wenn Attribute zu einer Active Directory-Klassendefinition hinzugeführt werden, können sie einen Wertstatus besitzen. Attribute können beispielsweise verbindlich sein. In diesem Fall müssen die Attribute Werte enthalten, wenn die Active Directory-Klasse instanziiert wird. Dagegen brauchen optionale Attribute zum Zeitpunkt der Instanziierung der Active Directory-Klasse nicht unbedingt Werte zu enthalten. Active Directory-Attribute sind indiziert, um eine schnellere Abfrage zu ermöglichen. Bei Objekten ist dies nicht der Fall. Bevor Sie sich entschließen, alle Attribute zu indizieren, müssen Sie sich über die Auswirkungen im Klaren sein. Active Directory verwendet eine modifizierte Version des B-Stern-Indizierungsalgorithmus. Dies bedeutet, dass für jeden Eintrag, der neu in den Datenspeicher aufgenommen wird, Zeit für dessen Indizierung benötigt wird. Mit zunehmender Größe des Index dauert auch die Indizierung neuer Einträge immer länger. Wenn ein Objekt zehn verschiedene indizierte Attribute besitzt, dann kann das Einfügen eines neuen Eintrags sogar ziemlich viel Zeit beanspruchen. Attribute können einwertig oder mehrwertig sein. Ein einwertiges Attribut sieht folgendermaßen aus: schlüssel = wert
Ein mehrwertiges Attribut hat dagegen folgende Form: schlüssel = wert1, wert2, … , wertN
In einem mehrwertigen Attribut ist die Reihenfolge der einzelnen Werte nicht festgelegt. Beim Durchlaufen der einzelnen Werte können Sie sich also nicht darauf verlassen, dass sich ein bestimmter Wert an einer bestimmten Position befindet. Ein Attribut kann jedoch durch Definition eines oberen und eines unteren Bereichs eingeschränkt sein. Active Directory-Klassen unterstützen die Vererbung. Lassen Sie uns dies einmal anhand der Konferenzanmeldungsanwendung durchspielen. In der Anwendung gibt es zwei Arten von Benutzern: normale und angemeldete Benutzer. Mit der Vererbung in Active Directory wäre die Basisklasse user, von der die Klasse Regis-
Architektur von Microsoft Active Directory
577
teredUser ihre Eigenschaften erbt. Alle Active Directory-Klassen müssen von top abgeleitet sein. Dies ist die grundlegende Klasse von Active Directory irgendwo in der Vererbungskette. Es gibt drei verschiedene Klassentypen: 왘 Strukturell: Strukturelle Klassen sind nur diejenigen Klasen die in Active Direc-
tory instanziiert werden können. Strukturelle Klassen können von jedem der drei in Active Directory möglichen Verfahren abgeleitet werden. 왘 Abstrakt: Abstrakte Klassen dienen als Vorlagen zur Ableitung neuer abstrakter
und struktureller Klassen. Eine Erweiterungsklasse kann nur eine Unterklasse einer abstrakten Klasse ein. Abstrakte Klasse können in Active Directory nicht instanziiert werden. 왘 Erweiterungsklasse: Erweiterungsklassen ähneln den Include-Dateien in C++.
Eine Erweiterungsklasse enthält eine Liste von Attributen. Fügt man die Erweiterungsklasse zu der Definition einer strukturellen oder abstrakten Klasse hinzu, dann werden ihre optionalen und verbindlichen Attribute in die Definition aufgenommen. Eine Erweiterungsklasse kann nicht im Verzeichnis instanziiert werden. Neue Erweiterungsklassen können von bestehenden Erweiterungsklassen oder abstrakten Klassen abgeleitet werden. Active Directory-Container Active Directory besitzt eine besondere Klasse, die als Container bezeichnet wird. Eine Containterklasse unterschiedet sich von den normalen Active Directory-Klassen, weil sie andere Active Directory-Objekte enthalten kann. Dies wird in Abbildung 18.2 gezeigt.
Container
Objekt
Abbildung 18.2 Active Directory-Containerobjekt
578
Verzeichnisdienste
Das Containerobjekt enthält drei andere Objekte. Davon ist eines selbst ein Container, und die beiden anderen Objekte sind darin enthalten. Mit diesem Verfahren ist es möglich ein hierarchisches Repository aufzubauen. Das Containerobjekt und die Containerklasse verhalten sich wie alle anderen Active Directory-Klassen. Sie können auf mehrere Active Directory-Attribute verweisen oder auch in anderen Active Directory-Klassen als Unterklassen verwendet werden. Wie Daten repliziert und abgerufen werden Active Directory-Objekte werden von Active Directory systematisch repliziert. Bei der erstmaligen Instanziierung eines Objekts auf einem bestimmten Server, wird das Objekt auf dem Server gespeichert. Wenn ein Server zu einem Netzwerk von Active Directory-Servern hinzugefügt wird, werden die im globalen Katalog des lokalen Servers gespeicherten Informationen an die anderen Server repliziert. Der globale Katalog enthält neben allen Attributen, die als replikationsfähig definiert sind, auch einige der Stammattribute, die zwingend repliziert werden müssen, beispielsweise den Namen des Objekts. In die Replikation von Active DirectoryObjekten werden nur die Attribute einbezogen. Dadurch ist es möglich, dass die Hälfte eines Objektes sich im Netzwerk befindet, während die andere Hälfte nur auf dem ursprünglichen Server existiert. Wenn ein Benutzer das Active Directory durchsucht, bezieht sich diese Suche nur auf seinen lokal gespeicherten globalen Katalog. Interessiert er sich für ein Objekt, dann werden alle seine Attribute vom lokalen globalen Katalog abgerufen und angezeigt. Interessiert sich der Benutzer für ein bestimmtes Attribut, das repliziert wurde, dann sucht Active Directory am ursprünglichen Speicherort des Objekts und ruft den vollständigen Inhalt des Objekts ab. Dies bringt uns zum nächsten Punkt, nämlich wie Attribute und Klassen definiert werden. Oft wird gesagt, dass Objekte so kompakt wie möglich sein sollen. Diese Regel leuchtet mir aber nicht ein. Bei der Dimensionierung der einzelnen Attribute müssen drei wichtige Faktoren berücksichtigt werden: Replikationshäufigkeit, Abfragehäufigkeit und Größe des Attributs. Diese drei Faktoren müssen so miteinander in Einklang gebracht werden, dass der Datenverkehr im Netzwerk minimiert wird. Es kann sinnvoll sein, ein großes Bildattribut in einen globalen Katalog aufzunehmen, wenn Änderungen an dem Bild nur alle zwei Jahre vorgenommen werden und das Attribut häufig abgerufen wird. Dadurch wird die Replikation der Daten auf ein Minimum reduziert.
Architektur von Microsoft Active Directory
579
18.1.2 Ein praktisches Active Directory-Beispiel Nachdem wir uns einen grundlegenden Überblick über Active Directory verschafft haben, wollen wir uns jetzt eine Active Directory-Installation ansehen und darin mit Hilfe einiger Verwaltungstools einen Benutzer anlegen. Bei der Installation von Active Directory werden bestimmte Container erstellt. Davon interessieren uns die verschiedenen Benutzer, die in der Domäne angemeldet sind. Abbildung 18.3 enthält ein Beispiel dafür.
Abbildung 18.3 Hierarchie eines Benutzers
In Abbildung 18.3 sind eine Reihe von Einträgen der Typen DC und CN enthalten. Diese ergeben zusammen genommen einen definierten Namen, und sie wurden ursprünglich durch den X.500-Standard festgelegt. DC steht für »Domain Container«, und CN bedeutet »Common Name«. Active Directory macht ausgiebigen Gebrauch sowohl vom offenen Standard X.500 als auch vom LDAP-Standard. Letzterer dient zur Bereitstellung von Verzeichnisdiensten. Falls Ihnen die X.500-Notation nicht geläufig ist, sehen Sie sich die folgende Verzeichnisangabe an: C:\verzeichnis1\verzeichnis2\meinedatei.txt
Dieses Verzeichnis würde in X.500-Notation folgendermaßen angegeben: FILE=meinedatei.txt,DIRECTORY=verzeichnis2,DIRECTORY=verzeichnis1,DISK=c
In der X.500-Notation werden Schlüssel definiert, die mit Schlüsselwertpaaren verwendet werden, um die verschiedenen Attribute der Zeichenfolge zu kennzeichnen. Mit X.500 lassen sich alle Ressourcen oder Standorte definieren. Da das System auf Schlüsselwertpaaren basiert, kann es selbstständig ermitteln, worauf
580
Verzeichnisdienste
sich die einzelnen Schlüssel beziehen. Die am häufigsten vorkommenden X.500Schlüssel in Active Directory sind CN und DC. Grundlegendes zu definierten Namen In X.500 wird ein URL (Uniform Resource Locator) als definierter Name bezeichnet. Er dient zur eindeutigen Angabe aller Elemente im Verzeichnisdienst. Bei der Notation mit definierten Namen wird der Standort des jeweiligen Objekts in einer bestimmten Reihenfolge bezeichnet, und zwar beginnend mit dem Element der geringsten Bedeutung. In Abbildung 18.3 ist beispielsweise der definierte Name des Objekts CN=Christian Gross wie folgt: CN=Christian Gross,CN=Users,DC=i-devspace,DC=local
Als Trennzeichen wird hier das Komma verwendet, das Semikolon ist jedoch ebenfalls zulässig. Manuelles Hinzufügen eines Namens Es ist möglich, einen Benutzer manuell zu Active Directory hinzuzufügen. Hierzu verwenden Sie den ADSI-Editor (im Windows 2000 Resource Kit). Klicken Sie mit der Rechten Maustaste auf den Knoten CN=Users, und wählen Sie dann aus dem Kontextmenü das neue Objekt aus. Das in Abbildung 18.4 dargestellte Dialogfeld wird angezeigt:
Abbildung 18.4 Manuelles Erstellen eines Benutzerobjekts
Architektur von Microsoft Active Directory
581
Wählen Sie aus dem Listenfeld die Klasse user aus. Klicken Sie anschließend auf Weiter. Jetzt werden Sie zur Angabe Wertes für CN aufgefordert. Geben Sie An administrator ein, und klicken Sie auf Weiter. Jetzt werden Sie aufgefordert, einen Wert für sAMAccountName einzugeben. Geben Sie ebenfalls An administrator ein. Klicken Sie auf Fertig stellen. Wenn Sie sich das Verzeichnis anzeigen lassen, enthält dieses den folgenden zusätzlichen Eintrag: An administrator. Dieser Eintrag ist vom Typ user. Wenn Sie die Management Console zur Benutzerverwaltung öffnen, wird das in Abbildung 18.5 dargestellte Listenfeld angezeigt.
Abbildung 18.5 Geänderte Benutzerliste
Der Benutzer An administrator wurde zur Liste hinzugefügt. Ein Problem gibt es dennoch. Der Benutzer ist mit einem kleinen roten × gekennzeichnet. Es bedeutet, dass der Benutzer zwar zu Active Directory hinzugefügt wurde, es sich jedoch nicht um einen gültigen Benutzer handelt. Die Ursache liegt darin, dass beim Erstellen von Benutzern im Active Directory ADSI-Editor nicht alle Benutzerattribute ordnungsgemäß gesetzt werden. Dies ist nur mit dem Benutzerverwaltungstool von Windows 2000 möglich. Anzeigen der Benutzerattribute Das Active Directory-Benutzerobjekt ist eine Instanz des Objekts Person. Suchen Sie mit dem ADSI-Editor den gerade hinzugefügten Benutzer (An administrator) im Benutzercontainer. Klicken Sie dann mit der rechten Maustaste darauf, und wählen Sie Eigenschaften aus dem Kontextmenü aus. Wählen Sie aus dem Dialogfeld, das jetzt geöffnet wird, die verbindliche Eigenschaft objectCategory aus, wie in Abbildung 18.6 gezeigt. Was Sie jetzt sehen, ist der Name der Schemaklasse Person, von der das Administratorobjekt instanziiert wurde. Dies wird im ADSI-Editor zwar als Eigenschaft bezeichnet, ist jedoch in Wirklichkeit ein Active Directory-Attribut.
582
Verzeichnisdienste
Abbildung 18.6 Inhalt der Eigenschaft »objectCategory«
18.2 Zugreifen auf den Benutzer mit ADSI Ich erspare mir hier alle weiteren administrativen Details, weil diese Themen in entsprechenden Büchern eingehend beschrieben werden. Stattdessen wollen wir uns jetzt mit dem Schreiben von Programmen beschäftigen, die Änderungen an Active Directory-Objekten vornehmen. Für den Zugriff auf Active Directory können Sie die COM-Schnittstellen von ADSI verwenden. Es gibt aber jedoch noch andere Möglichkeiten. Auch das LDAP Client-SDK ermöglicht die Kommunikation mit Active Directory. Beachten Sie, dass ADSI nicht ausschließlich für Active Directory eingesetzt wird. ADSI dient zur Verwaltung von Microsoft IIS (Internet Information Server) und Microsoft Exchange Server. Wir konzentrieren uns jedoch darauf, ADSI zur Verwaltung von Active Directory zu nutzen. Herstellen der Verbindung mit rootDSE Wenn Sie eine Active Directory-Anwendung schreiben, müssen Sie wissen, welcher Domäne der Server angehört. Es ist nicht möglich, eine bestimmte Domäne
Zugreifen auf den Benutzer mit ADSI
583
einfach im Programmcode anzugeben. LDAP legt jedoch fest, dass die aktuelle Domäne dynamisch abfragbar sein muss. Dies ist ähnlich wie bei TCP/IP (Transmission Control Protocol/Internet Protocol), wo der localhost den lokalen Computer angibt. Wenn Sie eine LDAP-Verbindung aufbauen, wollen Sie eine Abfrage nach dem Objekt rootDSE durchführen. Mit rootDSE lässt sich der Standort des Schemas und des globalen Katalogs ermitteln. Um beispielsweise den Namen einer Domäne abzurufen, kann das Attribut defaultNamingContext abgefragt werden. Entsprechend lässt durch Abfragen des Schemas schemaNamingContext das Schema abrufen. Die Anwendung, die wir jetzt erstellen werden, soll es uns ermöglichen, nach einem bestimmten Benutzer zu suchen und einige Informationen über ihn abzurufen. Zur Verbindung mit dem LDAP-Server wird der neue LDAP COM-Moniker verwendet. Im nachstehenden Beispiel wird die Verbindung mit dem Objekt rootDSE mit Visual Basic gezeigt: Set rootDSE = GetObject("LDAP://rootDSE")
Normalerweise würde ich den entsprechenden Code in Visual C++ nicht gesondert aufführen, weil er bei der Verwendung von COM praktisch seinem Visual Basic-Gegenstück identisch ist. Ich möchte jedoch zeigen, wie die Instanziierung des rootDSE in Visual C++ durchgeführt wird. Mit Visual C++ kann man wahlweise die mit MIDL (Microsoft IDL) erzeugten ADSI-Headerdateien oder die Ausgabe des COM-Compilers in der Datei activeDS.tlb verwenden. Ich persönlich bevorzug die vom COM-Compiler generierten Klassen. Wie in Visual Basic muss zum Abrufen des COM-Objekts rootDSE der LDAP COM-Moniker verwendet werden. In Visual C++ geschieht dies mit dem folgenden Code: _com_util::CheckError( CoGetObject( L"LDAP://rootDSE", NULL, __uuidof( ActiveDs::IADs), (void **)&ptr));
In der Dokumentation zum Microsoft Plattform-SDK haben Sie vielleicht gesehen, dass es eine Reihe von ADSI-Hilfsfunktionen gibt. Ich halte diese jedoch größtenteils für unnötig, sodass ich Ihnen die Benutzung nicht empfehle. Die nachfolgenden Codebeispiele wurden mit Visual Basic erstellt. Im Quellcode zu diesem Buch (auf der Begleit-CD-ROM) finden Sie jedoch auch einige sehr gute Demonstrationen in Visual C++.
584
Verzeichnisdienste
IADs: Die COM-Basisschnittstelle Ebenso wie Attribut und Klasse die Grundlage von Active Directory bilden, ist die IADs COM-Schnittstelle die Basis aller ADSI-Objekte. Die IADs COM-Schnittstelle wird von allen in ADSI instanziierten Objekten unterstützt und kann zum Abrufen und Einstellen aller Objektattribute verwendet werden. In der IDL (Interface Definition Language) ist die IADS COM-Schnittstelle folgendermaßen definiert (einige Methoden und Eigenschaften wurden der besseren Übersicht wegen weggelassen): interface IADs : IDispatch { [id(0x00000002), propget]HRESULT Name([out, retval] BSTR* retval); [id(0x00000003), propget]HRESULT Class([out, retval] BSTR* retval); [id(0x00000004), propget]HRESULT GUID([out, retval] BSTR* retval); [id(0x00000005), propget]HRESULT ADsPath([out, retval] BSTR* retval); [id(0x00000006), propget]HRESULT Parent([out, retval] BSTR* retval); [id(0x00000007), propget]HRESULT Schema([out, retval] BSTR* retval); [id(0x00000008)]HRESULT GetInfo(); [id(0x00000009)]HRESULT SetInfo(); };
Mit Hilfe dieser Eigenschaften lassen sich die Namen und Werte von Attributen abfragen: IADs::NameAllgemeiner Name (CN = Common Name) des Objekts IADs::ParentStandort des Objekts IADs::AdsPathKombination aus dem Namen und den übergeordneten Eigenschaften IADs::ClassKlassentyp in der Schemadatenbank IADs::SchemaVollständiger definierter Name des Klassentyps Alle genannten Eigenschaften werden mit dem Format des definierten Namens angegeben. Die Methoden IADs::GetInfo und IADs::SetInfo dienen dazu, Daten vom Verzeichnis in den Cache zu übertragen. Denken Sie daran, dass das Objekt im globalen Katalog gesucht wird und der Aufruf dieser Methoden alle zu dem jeweiligen Objekt gehörenden Informationen abruft. Durch den Aufruf dieser Methoden wird der gesamte Objektstatus vom globalen Verzeichnis in den Cache übertragen oder umgekehrt. Die Methoden eignen sich auch gut, um den lokalen Cache zu aktualisieren.
Zugreifen auf den Benutzer mit ADSI
585
Beim Debuggen einer Anwendung kann es beispielsweise vorkommen, dass die durchgeführten Änderungen nicht verfügbar sind. In diesem Fall lässt sich der Cache durch den Aufruf von IADs::GetInfo aktualisieren. Eine solche Aktion kann jedoch eine Verzögerung hervorrufen und erhebliche Bandbreite beanspruchen. Falls nur einige bestimmte Eigenschaften geladen werden sollen, ist deshalb die Methode IADs::GetInfoEx besser geeignet. Die nächste Operation, die wir mit dieser Basisschnittstelle durchführen werden, ist das Abrufen oder Setzen von Attributen. Hierzu können die untenstehenden Methoden verwendet werden, die in der IADs COM-Schnittstelle über IDL bereitgestellt werden (einige Methoden und Eigenschaften wurden der besseren Übersichtlichkeit wegen weggelassen): interface IADs : IDispatch { [id(0x0000000a)]HRESULT Get( [in] BSTR bstrName, [out, retval] VARIANT* pvProp); [id(0x0000000b)]HRESULT Put( [in] BSTR bstrName, [in] VARIANT vProp); [id(0x0000000c)]HRESULT GetEx( [in] BSTR bstrName, [out, retval] VARIANT* pvProp); [id(0x0000000d)]HRESULT PutEx( [in] long lnControlCode, [in] BSTR bstrName, [in] VARIANT vProp); };
Die Methoden IADs::Put und IADs::Get dienen zur Bearbeitung von einwertigen Attributen. Die Methoden IADs::PutEx und IADs::GetEx ermöglichen das Verändern von mehrwertigen Attributen. Im vorherigen Beispiel haben wir einen Benutzer erstellt. Jetzt wollen wir denselben Zweig von Active Directory nach dem Benutzer Christian Gross durchsuchen und dessen E-Mail-Adresse abrufen. Hierzu dient der nachstehende Code: Set findObj = GetObject("LDAP://CN=Christian Gross;CN=Users, DC=ldevspace, DC=local") Debug.Print "Name is :" & findObj.Get("mail")
Das Attribut mail repräsentiert die E-Mail-Adresse. Bei mehrwertigen Attributen, stehen die VARIANT-Werte für Arrays der Daten.
586
Verzeichnisdienste
IADsContainer: Verwalten von Auflistungen Active Directory-Container können in COM nicht direkt dargestellt werden. Eine bessere Lösung besteht darin, den Container als eine COM-Auflistung darzustellen. Die COM-Schnittstelle zur Verwaltung von Containern ist IADsContainer. Das Abrufen eines Containerobjekts geschieht wie bei jedem normalen Objekt. Die einzige Ausnahme besteht darin, dass der definierten Name auf einen Container verweisen muss. Um einen Benutzer hinzuzufügen, verwenden Sie den folgenden Code: Dim contObj As IADsContainer Dim usr As IADs Set contObj = GetObject("LDAP://CN=Users,DC=ldevspace, DC=local") Set usr = contObj.Create("user", "CN=SimpleUser")
In diesem Codebeispiel wird die Schnittstelle IADsContainer abgefragt, wenn das Objekt mit dem Aufruf GetObject abgerufen wird. Diese Schnittstelle wird deshalb verwendet, weil die Auflistung IADsContainer einen Container repräsentiert. Die Schnittstelle enthält eine Reihe von Bearbeitungsroutinen für Container. Dazu gehört beispielsweise auch eine Routine, die das Erstellen eines Objekts ermöglicht. Um ein Objekt zu erstellen, wird die Methode IAdsContainer::Create aufgerufen. Der erste Parameter (user) dieser Methode gibt den Klassentyp an. Dieser muss im Schemakatalog vorhanden sein. (Eine nähere Beschreibung des Schemas finden Sie im Abschnitt »Das Schema« weiter unten in diesem Kapitel.) Der zweite Parameter (CN=SimpleUser) gibt den allgemeinen Namen (CN) des Objekts an. Der Name muss in der X.500-Notation angegeben werden, weil diese auch von der ursprünglichen Abfrage verwendet wurde. (Ich erwähne das, weil viele der älteren ADSI-Beispiele das Format WinNT::// verwenden, bei dem kein definierter Name erforderlich ist.) Wenn das Objekt erstellt wird, existiert es nur im ADSI-Cache. Es handelt sich um ein gültiges Objekt des betreffenden Klassentyps mit allen Attributen. Um es im Datenspeicher des Verzeichnisses zu speichern, muss die Methode IADs::SetInfo aufgerufen werden, wie im folgenden Beispiel: usr.Put "sAMAccountName", "example3" usr.SetInfo
Zugreifen auf den Benutzer mit ADSI
587
Den Benutzer, den wir gerade erstellt haben, können wir auch wieder löschen, und zwar mit folgendem Code: [id(0x00000007)]HRESULT Delete( [in] BSTR bstrClassName, [in] BSTR bstrRelativeName);
Die Parameter sind dieselben wie beim Erstellen des Objekts. Der erste Parameter (bstrClassName) gibt also die Klasse, und er zweite Parameter (bstrRelativeName) definiert den Namen der Klasse. Da wir mit der X.500-Notation arbeiten, muss auch der Name wieder in dieser Notation angegeben werden. Zum Verschieben oder Kopieren eines Objekts stehen die folgenden Methoden zur Verfügung: [id(0x00000008)]HRESULT CopyHere( [in] BSTR SourceName, [in] BSTR NewName, [out, retval] IDispatch** ppObject); [id(0x00000009)]HRESULT MoveHere( [in] BSTR SourceName, [in] BSTR NewName, [out, retval] IDispatch** ppObject);
Bei den Parametern handelt es sich in beiden Fällen um definierte Namen. Der erste Parameter (SourceName) ist ein vollständiger definierter Name für ein Objekt an einer beliebigen Position von Active Directory. Er wird dann in den lokalen Container kopiert und erhält dort den durch den zweiten Parameter (NewName) angegebenen allgemeinen Namen. Wenn die Operation erfolgreich war, wird das neue Objekt zurückgegeben. Standardmäßig werden beim Öffnen des Containers alle Elemente, die im Active Directory-Container enthalten sind, an die Auflistung IADsContainer übertragen. Es ist möglich, einen Teilsatz von Objekten anhand bestimmter Kriterien eines Attributs herauszufiltern. In der Auflistung users, auf die wir uns bezogen haben, befinden sich Objekte der Klassentypen user und group. Wenn wir nur die Benutzer durchsuchen möchten, können wir den nachstehenden Code schreiben: Dim users As IADsContainer Dim usr As IADs Set users = GetObject("LDAP://CN=Users, DC=ldevspace, DC=local") users.Filter = Array("User") For Each usr In users Debug.Print "Distinguished name :(" & usr.ADsPath & ") Class is :(" & usr.Class & ")" Next
588
Verzeichnisdienste
Optimieren des Zugriffs Das Objekt, das wir bisher bearbeitet haben, gehört dem Klassentyp user an. Um einige der Operationen für diesen Klassentyp zu vereinfachen, stehen verschiedene COM-Schnittstellen zur Verfügung, die einzelne Active Directory-Objekte darstellen. Die COM-Schnittstelle für den Klassentyp user ist IADsUser. Ihr Einsatz wird im folgenden Beispiel gezeigt: Dim findObj As IADs Dim userObj As IADsUser Set findObj = GetObject("LDAP://CN=Christian Gross;CN=Users, DC=ldevspace, DC=local") Set userObj = findObj Debug.Print "Email is :" & userObj.EmailAddress
Wenn wir uns noch einmal unsere Definition in IADs ansehen, erkennen wir, dass sie von der COM-Schnittstelle IDispatch abgeleitet ist. Beim Abrufen eines Active Directory-Objekts stellt die COM-Schnittstelle IDispatch also alle Active Directory-Attribute als COM-Eigenschaften dar. Dieser Ansatz funktioniert in Visual Basic oder mit Skripts sehr gut, weil sich die Schnittstelle IDispatch dynamisch abfragen lässt, ohne große Mengen von Code zu schreiben. In Sprachen wie Visual C++ und Visual J++ ist der Aufruf von IDispatch-Schnittstellen ebenfalls möglich, jedoch aufwändiger. Beide Sprache bevorzugen eine benutzerdefinierte COM-Schnittstelle. Hier zeigen sich die Vorteile von IADsUser mit ihren vordefinierten Methoden und Vorteilen. Es stehen noch andere COMSchnittstellen zur Verfügung, etwa zur Verwaltung von Computern (IADsComputer), Domänen (IADsDomain) und Gruppen (IADsGroup). Die Spezifikationen der verschiedenen Schnittstellen sind im Active Directory-Unterabschnitt des Plattform-SDKs definiert.
18.3 Erstellen benutzerdefinierter Objekte Der große Vorteil von Active Directory besteht darin, dass nicht nur die standardmäßigen Active Directory-Klassendefinitionen benutzt zu werden brauchen. Stattdessen können Sie ein Repository benutzerdefinierter Objekte erstellen. Damit sind Sie in der Lage, benutzerdefinierte Objekte zu erstellen und diese mit Ihren Geschäftspartner auszutauschen. Um eine benutzerdefinierte Klasse hinzuzufügen, müssen Sie das Schema erweitern. Dies ist eine einmalige Aktion, die nicht mehr rückgängig gemacht werden kann. Schemaerweiterungen können nicht gelöscht, sondern nur deaktiviert wer-
Erstellen benutzerdefinierter Objekte
589
den. Zur Erweiterung des Schemas gibt es zwei Möglichkeiten: mit dem SchemaManager oder mit einem selbst geschriebenen Programm. In diesem Buch verwende ich den Schema-Manager. Dieses Tool ist Bestandteil des Windows 2000 Resource Kits. Änderungen am Schema sind auch mit Hilfe von ADSI-Objekten möglich. In diesem Fall empfehle ich jedoch dringend, einige Skriptdateien zu erstellen, die mit COM-Automatisierung arbeiten. Die Verwendung einer anderen Programmiersprache ist zu kompliziert und zeitaufwändig. Denken Sie daran, dass die Erweiterung des Schemas eine einmalige Angelegenheit ist, die niemals von Endbenutzern durchgeführt wird. Deshalb wird dazu keine komfortable grafische Benutzeroberfläche (GUI) benötigt. Beispiel für die Verwendung von Active Directory Ein Beispiel für die Verwendung benutzerdefinierter Active Directory-Objekte ist die Übernahme der Benutzer der Konferenzanmeldung von der Datenbank in Active Directory. Dies bietet den Vorteil, dass die Benutzerinformationen ohne Zuhilfenahme eines Programms automatisch zwischen verschiedenen Anmeldungsanwendungen und Gemeinschaften repliziert werden. Indem wir die Benutzer selbst definieren, vermeiden wir auch, dass sie Teil der Domäne werden. Der Konferenzbenutzer wird nur für eine Konferenzanwendung benötigt und braucht sich deshalb nicht an jeden Computer der Domäne anzumelden. Ferner wollen wir unsere selbstdefinierten Benutzer auch nicht in den Container users aufnehmen. Stattdessen erstellen wir einen neuen Container speziell für die Konferenzbenutzer. Hinzufügen der Attribute Weiter oben haben wir bereits gesehen, wie Active Directory-definiert werden. Als ersten Schritt zur Erstellung unserer neuen selbst definierten Active DirectoryBenutzerklasse müssen wird die verschiedenen Attribute definieren, die Bestandteil der Klasse sein sollen. Eines der Attribute ist userRegistrationLevel, das zum globalen Katalog hinzugefügt wird. Klicken Sie im Schemaverwaltungsprogramm mit der rechten Maustaste auf den Knoten Attribute, und wählen Sie Attribute erstellen aus dem Menü aus. Ein Dialogfeld weist darauf hin, welche Folgen das Hinzufügen eines Attributs hat. Klicken Sie auf Weiter. Jetzt wird das in Abbildung 18.7 dargestellte Dialogfeld angezeigt. In diesem Dialogfeld kennzeichnet das Textfeld Gemeinsamer Name den CN des Objekts, das erstellt wird. Geben Sie userRegistrationLevel als Namen ein. Im
590
Verzeichnisdienste
Textfeld LDAP-Anzeigename wird der Name festgelegt, unter dem das Attribut angezeigt wird, wenn ein LDAP-Client mit Active Directory kommuniziert. Hier ist es zweckmäßig, den allgemeinen Namen zu übernehmen.
Abbildung 18.7 Attributeigenschaften
Im Textfeld eEindeutige X.500-OID wird das Attribut in Form einer numerischen Adresse identifiziert. Diese ist in der Funktion mit der GUID (Globally Unique Identifier, global eindeutige Kennung) identisch, verwendet jedoch ein anderes Format. Die OIDs (Object Identifiers, Objektbezeichner) verwenden die X.500Formatspezifikation. OIDs sind eindeutige numerische Werte, die von verschiedenen Autoritäten herausgegeben werden, um Datenelemente, Syntaxen und andere Bestandteile von verteilten Anwendungen eindeutig zu kennzeichnen. OIDs kommen in allen Anwendungen vor, in denen diese Eindeutigkeit von wichtig ist. Dazu gehören unter anderem OSI-Anwendungen (Open Systems Interconnection), X.500-Verzeichnisse oder SNMP-Protokolle (Simple Network Management Protocol). OIDs basieren auf einer Baumstruktur, in der eine übergeordnete ausstellende Autorität (beispielsweise ISO) einen »Ast« des Baums an eine Unterautorität vergibt, die ihrerseits untergeordnete Äste vergeben kann. Das LDAP-Protokoll setzt einen Verzeichnisdienst voraus, um Objektklassen, Attribute und Syntaxen mit OIDs identifizieren zu können. Dies ist eine Hinterlassenschaft von LDAP X.500.
Erstellen benutzerdefinierter Objekte
591
Neben OIDs, die von der ISO (International Standards Organization) für X.500Klassen und -Attribute stammen, enthält Active Directory auch von Microsoft und anderen ausstellenden Autoritäten herausgegebene OIDs. Die OID-Notation ist eine Zeichenfolge von Zahlen, die durch Punkte getrennt sind. Ein Beispiel wäre etwa 1.2.840.113556.1.5.4, das in der nachstehenden Tabelle aufgelöst wird: Wert
Beschreibung
1
ISO, die »Stammautorität« – hat »1.2« an ANSI vergeben
2
ANSI – hat »1.2.840« an USA vergeben
840
USA – hat »1.2.840.113556« an Microsoft vergeben
113556
Microsoft
1
Microsoft – Active Directory Service
5
Microsoft – Active Directory Service: Klassen
4
Microsoft – Active Directory Service: Klassen: Vordefinierte Domäne
OIDs können auf verschiedene Arten generiert werden. Die erste Möglichkeit besteht darin, eine OID von der ISO als namenvergebende Autorität anzufordern. Dies ist eine einmalige Aktion. Nachdem Sie eine Stamm-OID erhalten haben, können Sie den dadurch zur Verfügung stehenden OID-Namespace nach Belieben definieren und verwalten. Die zweite Möglichkeit zum Generieren einer OID besteht in der Verwendung des Befehlszeilenprogramms OIDGEN.EXE, das gültige OIDs erzeugt. Die OIDs werden anhand einer Basis-OID aus dem Microsoft Stamm des ISO OID-Baums generiert, wobei bei jeder Ausführung des Programms eine GUID erzeugt wird. Schließlich kann eine OID auch per E-Mail von Microsoft unter der Adresse [email protected] angefordert werden. Im Textfeld Syntax wird der Active Directory-Datentyp angegeben. In unserem Beispiel benötigen wir den Typ integer. Er besitzt einen einzelnen Wert, deshalb braucht das Kontrollkästchen Mehrwertig nicht aktiviert zu werden. Die Felder für die Maximal- und Minimalwerte des Textfelds brauchen ebenfalls nicht ausgefüllt zu werden. Klicken Sie zum Schluss auf OK, um das Attribut hinzuzufügen. Hinzufügen der Klasse Klicken Sie im Schemaverwaltungstool mit der rechten Maustaste auf den Knoten Klassen. Wählen Klassen erstellen aus dem Kontextmenü. Das in Abbildung 18.8 dargestellte Dialogfeld wird angezeigt:
592
Verzeichnisdienste
Abbildung 18.8 Klassendefinition
Die ersten drei Textfelder – Gemeinsamer Name, LDAP-Anzeigename und Eindeutige X500-OID – sind mit denen identisch, die wir beim Hinzufügen eines Attributs zum Schema verwendeten. Der allgemeine Name der Klasse ist webUser. Wenn Sie eine Klasse definieren, muss diese jedoch Attribute von einer anderen Klasse erben. In unserem Beispiel erbt die Klasse von der Klasse user. Neu in diesem Dialogfeld sind die Textfelder Übergeordnete Klasse und Klassentyp. Die Klasse webUser hat die Klasse user als übergeordnete Klasse. Der Klassentyp legt fest, wie die Klasse im Schema definiert wird; in unserem Fall ist der Klassentyp Strukturell. Klicken Sie auf Weiter, um das in Abbildung 18.9 gezeigte Dialogfeld zu öffnen. In diesem Dialogfeld können Sie festlegen, welche Attribute verbindlich und welche optional sind. Verbindliche Attribute müssen zwingend ausgefüllt werden, bevor sie in den Datenspeicher übernommen werden können. Optionale Attribute können auch leer gelassen und später ausgefüllt werden. In unserem Fall wird das einzige Attribut, das hinzugefügt werden muss, als optional betrachtet. Klicken Sie auf Hinzufügen, und suchen Sie in dem jetzt geöffneten Dialogfeld in dem Listenfeld der verfügbaren Attribute nach userRegistrationLevel. Klicken Sie anschließend auf OK. Klicken Sie erneut auf OK, um die Klassendefinition im Schema zu speichern. Jede Instanz von webUser erbt alle anderen in der Basisklasse user definierten Attribute.
Erstellen benutzerdefinierter Objekte
593
Abbildung 18.9 Definition der Klassenattribute
18.3.1 Arbeiten mit einem benutzerdefinierten Active Directory-Objekt Das Arbeiten mit einem benutzerdefinierten Objekt unterscheidet sich nicht wesentlich von der Verwendung eines »normalen« Objekts. Der einzige Unterschied besteht darin, dass ein benutzerdefiniertes Objekt mehr Attribute enthalten und mit ihm keine der optimierten Schnittstellen verwendet werden können. Wenn Sie eine optimierte Schnittstelle für den Zugriff auf das Objekt benötigen, können Sie diese selbst schreiben. Die Implementierung des optimierten Objekts verwendet die Kernschnittstellen von ADSI. Bei der Verwendung benutzerdefinierter Objekte gibt es einige Dinge zu beachten. Wird ein Active Directory-Objekt instanziiert und zu einem Container hinzugefügt, ist sein übergeordnetes Objekt ein Container. Wenn Sie eine neue Klasse definieren, die von der obersten Klasse erbt, muss die mögliche höhergestellte Eigenschaft das Containerobjekt enthalten. Beim Instanziieren einer Klasse muss diese an irgendeiner Stelle in Active Directory eingefügt werden. Die neu instanziierte Klasse ist untergeordnetes Objekt eines übergeordneten Objekts, bei dem es sich in den meisten Fällen um einen Container handelt. Wenn Sie ein mögliches übergeordnetes Objekt angeben, legen Sie deshalb auch fest, wo die Klasse instanziiert werden kann. Ist die mögliche übergeordnete Eigenschaft leer, dann bedeutet dies, dass die Klasse nirgends instanziiert werden kann.
594
Verzeichnisdienste
Schließlich darf beim Erweitern des Schemas nicht vergessen werden, das Aktualisierungsflag umzuschalten. Der entsprechende Schlüssel befindet sich an folgender Position der Registrierung: HKEY LOCAL MACHINE\System\Current Control Set\Services\NTDS\Parameters
Der einzufügende Schlüssel hat den Typ REG_DWORD und heißt Schema Updates Allowed. Sein Datenwert muss 1 betragen. Der gesamte Prozess wird in Artikel Q216060 der Microsoft Knowledgebase beschrieben.
18.4 Verwenden von OLE DB und ADO Um ein bestimmtes Element in Active Directory zu suchen, können Sie mit ADSI einen entsprechenden Filter setzen. Dieser ist jedoch spezifisch und bietet nur eingeschränkte Abfragemöglichkeiten. Windows 2000 enthält einen OLE DB Provider, der die Abfrage von Active Directory mit ADSI COM-Schnittstellen ermöglicht. Der OLE DB Provider ist zu bevorzugen, wenn Sie eine weitergehende und komplexere Syntax benötigen. Zur Durchführung von Suchen können zwei verschiedene Notationen verwendet werden: die LDAP-Notation und eine SQL-Notation. Die LDAP-Notation ist etwas gewöhnungsbedürftig. Wenn Sie mit SQL vertraut sind, ist die SQL-Notation einfacher zu handhaben.
18.4.1 Abfragen mit der LDAP-Notation Eine LDAP-Abfrage ist aus vier separaten Teilen zusammengesetzt: Root; Filter; Attributes; Scope
Root ist ein definierter Name, der angibt, wo die Suche beginnt. Wenn wir bei unserem Active Directory-Beispiel bleiben, könnten Sie innerhalb des Baums Users mit Hilfe des folgenden definierten Namens suchen:
Beachten Sie die spitzen Klammern. Diese werden benötigt, wenn eine Suche durchgeführt wird. Beim Suchen definieren Sie die Elemente, die Sie interessieren. Dies wird als Setzen der Eigenschaft Filter bezeichnet. Die Filter-Notation wird durch RFC 2254 definiert. (Das Plattform-SDK gibt fälschlicherweise RFC 960 an. Es sollte RFC 1960 heißen, dieser RFC wurde durch RFC 2254 ersetzt.) Der Filter ist eine Reihe von Schlüsselwertpaaren, die zu verschiedenen Operatoren kombiniert werden. Der Schlüssel im Filter kann jedes Attribut des Objekts
Verwenden von OLE DB und ADO
595
sein, etwa objectcategory oder mail address. Um Sie beispielsweise alle Klassen eines bestimmten Typs zu suchen, würden Sie das Attribut objectcategory wie im folgenden Beispiel verwenden: (objectcategory=user)
Der in den vorhergehenden Notationen verwendete Operator ist der Gleichheitsoperator (=). Es können auch andere Operatoren verwendet werden, etwa kleiner gleich (=). Wenn ein bestimmtes Attribut nicht gesucht werden soll, stellen Sie ihm den Operator NOT (!) voran. Um eine Suche mit mehreren Kriterien durchzuführen, können Sie die einzelnen Operatoren mit Filteroperatoren wie AND (&), OR (|), NOT (!) usw. verknüpfen. Das nachstehende Beispiel zeigt eine Abfrage nach mehreren Elementen: (|(objectCategory=user)(objectCategory=group)([email protected]))
Sie können beliebig viele Elemente verwenden. Die Kombination kann auch verschachtelt werden, wie im folgenden Beispiel gezeigt: (&(mail=*devspace.com)(|(objectCategory=user)(objectCategory=group)))
In dieser Suche wird das Platzhalterzeichen (*) verwendet, um Objekte des Typs user und group zu finden, deren E-Mail-Adresse mit devspace.com endet. Wenn ein Resultset gefunden wird, werden die darin angezeigten spezifischen Attribute durch den Bestandteil attributes der LDAP-Abfrage definiert. Der letzte Teil der LDAP ist scope und legt fest, wie die Suche durchgeführt werden soll. Es gibt drei verschiedene Sucharten: 왘 Base: Damit erstreckt sich die Suche auf das im definierten Namen angegebene
Objekt. Denken Sie daran, dass der definierte Name ein Objekt ist – normalerweise ein Containerobjekt, das untergeordnete Objekte enthält. 왘 One level: Damit erstreckt sich die Suche auf die Elemente innerhalb des Con-
tainers des Objekts, schließt jedoch nicht den Container selbst ein. 왘 Subtree: Damit erstreckt sich die Suche auf die Elemente im Container des Ob-
jekts, schließt jedoch nicht den Container selbst ein. Falls es sich bei den Objekten innerhalb des Containers selbst um Container handelt, wird die Suche innerhalb dieser Container fortgesetzt. Das nachstehende Beispiel zeigt eine vollständige LDAP-Abfrage: ;(objectCategory=*);name,mail;Base
596
Verzeichnisdienste
18.4.2 Abfragen mit der SQL-Notation Die SQL-Abfragenotation ist praktisch identisch mit der SQL-Syntax von Kapitel 15. Der einzige Unterschied besteht darin, dass sich die Abfrage nicht auf Tabellen, sondern auf Active Directory-Objekte bezieht. Die Tabelle wird hier durch einen Container ersetzt, und die SELECT-Anweisung bezieht sich auf den Inhalt des Containers. Nehmen Sie das folgende Beispiel: SELECT name FROM 'LDAP://CN=Users, DC=ldevspace, DC=local' WHERE objectCategory='user' ORDER BY name
Der definierte Name (LDAP://…) legt einen spezifischen Container fest. Von diesem Container ausgehend wählen wir alle Objekte aus, deren Attribut objectCategory gleich “user“ ist. Das Resultset wird nach dem Feld Name sortiert.
18.4.3 Verwenden von ADO Unabhängig davon, ob Sie für Abfragen die LDAP- oder die SQL-Notation verwenden, wird der Befehl immer so ausgeführt, als ob es sich dabei um eine andere Ressource handeln würde. Dies ist ja der Sinn des universellen Datenzugriffs (UDA, Universal Data Access), bei dem alle Elemente eine Form von Daten darstellen. Nähere Informationen zur Verwendung von ADO (Active Data Objects) finden Sie in Kapitel 16. Der Unterschied zu einer SQL-Datenbank besteht darin, dass die gerade definierte Abfragezeichenfolge bei ADO in Command.CommandText enthalten ist. Informationen zur Bearbeitung des generierten Resultsets finden Sie in Kapitel 16.
18.5 Resümee Active Directory ist eine Anwendung, die sich ähnlich wie eine Datenbank verhält. Die Marketingstrategen bezeichnen Active Directory als Verzeichnisdienst. Ein Verzeichnisdienst ist jedoch wirklich nicht mehr als eine spezialisierte Datenbank, die für die Replikation und Distribution von Daten ausgelegt ist. Alles, was in einem Verzeichnisdienst möglich ist, kann auch mit einer SQL-Datenbank erreicht werden. Mit einem Verzeichnisdienst sind diese Vorgänge jedoch optimiert. Sollten Sie einen Verzeichnisdienst in Ihrer Anwendung einsetzen? Die Antwort lautet »ja«, weil dies für zukünftige Anwendungen von größter Bedeutung sein wird. Vielleicht können Sie noch ein oder zwei Jahre ohne Verzeichnisdienst auskommen, danach werden Sie ihn jedoch brauchen. Deshalb ist es besser, sich schon jetzt in Ruhe damit vertraut zu machen, als zu warten und sich danach alles unter Zeitdruck aneignen zu müssen. Ein Verzeichnisdienst ermöglicht es, Informationen über Ihre Anwendung in einem Netzwerk speichern. Dies erleichtert
Resümee
597
die Wartung Ihrer Anwendung, weil keine lokalen Dateien oder Einstellungen übertragen werden müssen. Microsoft hat schon öfters Technologie entwickelt, die nur von Microsoft unterstützt wurde, bei Active Directory ist dies jedoch anders. Die Väter von Active Directory haben verstanden, dass Microsoft zwar groß, aber nicht allein auf der Welt ist. Active Directory bietet uneingeschränkte Unterstützung für LDAP, und ich habe es erfolgreich auf UNIX-Clients eingesetzt. Der einzige Unterschied besteht darin, dass sich das Layout der Hierarchie geringfügig von einigen traditionellen UNIX-Hierarchien unterscheidet.
598
Verzeichnisdienste
19 Der Qualitätskontrollprozess Qualitätskontrolle ist in der Softwareindustrie etwas, mit dem viele Menschen Probleme haben. Systemabstürze sind sehr häufig, und die Endbenutzer bleiben auf sich selbst gestellt. In diesem Zusammenhang müssen wir die folgende Frage beantworten: Wie können wir einen effizienten Qualitätskontrollprozess entwickeln? Der Qualitätskontrollprozess sollte nicht erst einsetzen, wenn die betreffende Komponente bereits entwickelt ist. Er sollte vielmehr ein laufender Bestandteil des gesamten Entwicklungsprozesses sein. In diesem Kapitel werden drei Konzepte besprochen. Zunächst die Frage, warum ein Qualitätskontrollprozess erforderlich ist. Das zweite Thema beschäftigt sich damit, wie wir die Qualität durch Testen der Anwendung erreichen können. Der dritte Aspekt schließlich behandelt die Frage, wie sich Qualität durch Testen der Leistung der Anwendung und Hardware erreichen lässt. Nur wenn alle drei Konzepte gemeinsam eingesetzt werden, wird die betreffende Anwendung für den Endbenutzer akzeptabel sein.
19.1 Definieren des Qualitätskontrollprozesses Die Qualitätskontrolle wird in der Softwareindustrie im Allgemeinen nicht angemessen angegangen oder durchgeführt. Microsoft besitzt einen umfangreichen, kompetenten Mitarbeiterstab für die Qualitätskontrolle, was sich jedoch nicht im Ruf seiner Software niederschlägt. Umgekehrt arbeitete ich einmal an einem Projekt mit, in dem der Qualitätskontrollprozess in der Hand eines einzigen Mitarbeiters lag, der jedoch genau wusste, was er tat. Das Ergebnis war ein stabiles Produkt, mit dem die Benutzer zufrieden waren. Ich befragte auch ein indisches Softwarehaus, in dem man die Qualität durch ein »Brute-Force«-Verfahren sicherte. Das Ergebnis war zwar auch ein stabiles Produkt, jedoch nur mit einem enormen Arbeitseinsatz, der nur in Ländern mit geringen Lohnkosten finanzierbar ist. In diesem Kapitel möchte ich ein Modell für einen Qualitätskontrollprozess vorstellen. Häufig enthält ein Qualitätskontrollprozess Schritte, die sinnvoll sind, jedoch nicht durchgeführt werden. So werden beispielsweise Codeüberarbeitungen, Dokumentation und die Einrichtung von Codierungsstandards als Mittel zur Verbesserung der Qualität propagiert. Sie können dies auch leisten, erfordern jedoch zu viel Zeit. Der Trick besteht darin, einen Qualitätskontrollprozess einzuführen, der effizient ist, jedoch nicht zu viel Zeit erfordert. Denken Sie bei der Definition eines Qualitätskontrollprozesses an die offensichtlichen Helfer für die Qualitätskontrolle. Ich habe die Erfahrung gemacht, dass
Definieren des Qualitätskontrollprozesses
599
man zwar »idiotensichere« Qualitätskontrollverfahren einführen kann, diese jedoch keine Reduzierung von Fehlern und Problemen ermöglichen, wenn das Entwicklungsteam sich damit nicht wohlfühlt. Die Harmonie im Team ist von größter Bedeutung Ich will nicht auf das Management und die Moral eines Teams eingehen, weil dies den Rahmen des Buches sprengen würde. Deshalb sei hier nur gesagt, dass glückliche Teams besseren Code produzieren. In vielen Fällen verbessert sich die Situation erheblich, wenn dieser Gesichtspunkt angemessen berücksichtigt wird. Eine weitere gute Qualitätsverbesserung besteht darin sicherzustellen, dass die richtigen Programmentwürfe erstellt werden. Gute Designs reduzieren die Anzahl von Fehlern und tragen zur schnelleren Marktreife des Produkts bei. Genau das ist der Zweck dieses Buches.
19.1.1 Definieren einer Metrik Nehmen wir jetzt an, Sie haben ein gutes Team und beim Design gute Arbeit geleistet. Dennoch gibt es noch Fehler – was sollen Sie tun? Die Antwort lautet, eine Metrik zu entwickeln. Softwareentwickler mögen Metriken im Allgemeinen nicht besonders, weil sie es ermöglichen, ihre Produktivität zu messen. Dies ist jedoch nicht unbedingt etwas Schlechtes. Metriken sind schlecht, wenn sie eingesetzt werden, um festzustellen, ob der Entwickler gut oder schlecht ist. Dies führt dazu, dass man nur einen Aspekt eines Entwicklers betrachtet, und wirkt sich negativ auf die Moral des Teams aus. Ich setze Metriken ein, um abzuschätzen, wann Teile eines Projekts fertiggestellt sein werden. Beim Schreiben dieses Buches habe ich beispielsweise den Zeitaufwand für jedes Kapitel protokolliert. Am Beginn dachte ich, dass ich für ein Kapitel ungefähr vier Tage brauchen würde. Aber ich irrte mich. Obwohl einzelne Kapitel sogar früher fertig waren, zeigte mein Diagramm an, dass ich durchschnittlich ungefähr 5,8 Tage zur Fertigstellung eines Kapitels benötigte. Die Gefahr in der Verwendung einer solchen Metrik besteht darin, dass man mich drängen könnte, schneller zu schreiben. Dies hätte jedoch eine geringere Qualität des Buches zur Folge. Die Schnittstellenmetrik Ich bevorzuge eine schnittstellenbasierte Metrik, bei der jede Schnittstelle als individuelle Einheit aufgezeichnet wird. Andere favorisieren Metriken auf der Basis von Entwurfspunkten, wobei dabei offen ist, was einen Entwurfspunkt charakterisiert. Der Einsatz einer Metrik auf der Basis eines UML-Diagramms (Unified Modeling Language) kann zu vage sein, weil einige Elemente möglicherweise noch nicht konzipiert sind, sodass die Diagramme und die Metrik ständig aktualisiert
600
Der Qualitätskontrollprozess
werden müssen. Die klassische Metrik des Ermittelns der Codezeilen ist wirklich schlecht, weil sie die Programmierer dazu verführt, ihren Code aufzublähen. In einer schnittstellenbasierten Metrik kann es sinnvoll sein, die Anzahl von Methoden der Schnittstelle zu verfolgen. Dies würde jedoch vom eigentlichen Thema ablenken, nämlich dem Einsatz einer Metrik. Man sollte nicht anfangen, Methoden zu zählen. Dies ist zu zeitaufwändig, und zu volatil – Schnittstellen ändern sich im Verlauf des Entwicklungsprozesses. Wir wollen annehmen, dass Sie ihre Schnittstellen gut konzipiert haben, und es keine Schnittstellen gibt, die 300 Methoden beinhalten. (Ich rede hier nicht über COM-Schnittstellen [Component Object Model], weil es sich dabei um andere Schnittstellentypen handelt. Beispiele dafür wären etwa Schnittstellen für gespeicherte SQL-Prozeduren.) Für Schnittstellen, die sich nicht zu Objekten gruppieren lassen, muss eine Namenskonvention verwendet werden. Die in Kapitel 15 entwickelte Namenskonvention ermöglicht es, gespeicherte SQL-Prozeduren zu gruppieren. Die Problembereiche Eine interessante Studie von IBM kam zu dem Ergebnis, dass 57 Prozent der Fehler sich auf 7 Prozent der Module konzentrieren. Dies ist ein Beispiel für die 80/ 20-Regel, die besagt, dass 80 Prozent der Probleme sich auf 20 Prozent der Bereiche konzentrieren. Die Studie kommt zu dem Schluss, dass der Schlüssel zur Entwicklung von Produkten mit weniger Fehlern darin besteht, die Problembereiche zu ermitteln und sich darauf zu konzentrieren. Wenn man die Metrik auf der Grundlage von Schnittstellen definiert, kann man die Problembereiche lokalisieren. Wenn die Schnittstelle einen Fehler oder eine Änderung enthält, kann dies auf einem Diagramm als Änderung gekennzeichnet werden. Das Verfolgen dieser Kennzeichnungen erleichtert das Lokalisieren der problematischen Schnittstellen. Es gibt fünf allgemeine Dynamiken, die gemessen werden sollen. 왘 Anzahl von Versionen einer Schnittstelle: Wenn man eine Schnittstelle imple-
mentiert, müssen darin immer Änderungen vorgenommen werden. Dies ist in Ordnung, solange die Schnittstelle noch nicht veröffentlicht ist. Wenn eine Schnittstelle jedoch veröffentlicht ist und geändert werden soll, muss möglicherweise auch der Konsument geändert werden. Eine große Anzahl von Änderungen könnte auf Fehler im Schnittstellenentwurf hindeuten. Oder aber die Schnittstelle muss auf verschiedene Funktionalitäten aufgeteilt werden. Wenn an einer Schnittstelle gar keine Versionsänderungen vorliegen, kann das darauf hindeuten, dass die Schnittstelle noch nicht in Benutzung ist oder noch nicht voll eingesetzt wird. Dies ist wie eine tickende Zeitbombe, die hochgehen kann, wenn die Schnittstelle tatsächlich benutzt wird. In dieser Phase ist die
Definieren des Qualitätskontrollprozesses
601
Entwicklung jedoch vielleicht schon zu weit fortgeschritten, als dass man noch Maßnahmen ergreifen könnte. 왘 Implementierungsgröße der Schnittstelle: Diese Metrik ist nicht immer ein
guter Indikator dafür, ob ein Problem vorliegt oder nicht. Der Extremfall eines aufgeblähten Codes kann jedoch auf ein Problem hindeuten. 왘 Anzahl von Schnittstellen, die durch die Implementierung verwendet wer-
den: Das Feststellen der Anzahl von Schnittstellen, die verwendet werden, ermöglicht Rückschlüsse auf die Komplexität der Implementierung. Ist die Zahl hoch, liegt auch eine relativ hohe Komplexität vor. In diesem Fall kann es sinnvoll sein, die Implementierung zu prüfen und zu versuchen, den Code zu vereinfachen. Ein weiteres potenzielles Problem einer solchen Implementierung besteht darin, dass sie ständig von Schnittstellenänderungen betroffen sein kann. Dies erfordert ständige Aufmerksamkeit, was unter Umständen zu Nachlässigkeiten bei der Anwendung einer Fehlerbehebung führen kann. 왘 Anzahl von Implementierungen, die eine Schnittstelle benutzen: Wird die
Schnittstelle von vielen verschiedenen Implementierungen benutzt, bedeuten Änderungen an der Schnittstelle automatisch auch Änderungen an allen diesen Implementierungen. Dadurch ist möglicherweise die Stabilität der Anwendung gefährdet. Deshalb muss unbedingt sichergestellt werden, dass eine angemessene Granularität vorliegt. 왘 Fehler pro Schnittstelle: Der Indikator »Fehler pro Schnittstelle« zeigt, welche
Schnittstellen wirklich sofortiges Eingreifen erfordern. Angenommen, Sie haben alle Schnittstellen Ihres Projekts identifiziert und alle gerade definierten Dynamiken gemessen. Dann verschaffen Sie sich als Nächstes anhand dieser Dynamiken ein Bild darüber, was geschehen könnte und was tatsächlich geschieht. Alle Dynamiken, mit Ausnahme der Fehleranzahl, sind Anzeichen für potenzielle Probleme und können in der Entwurfsphase ausgewertet werden. Es ist jedoch nicht möglich, eine Serie numerischer Werte zu definieren, die aussagen, dass eine Implementierung oberhalb einer bestimmten Größe geändert werden muss. Dies ist zu einfach. Was Sie erreichen möchten, ist die Aussage: »Schnittstelle Y scheint ein wenig zu kompliziert zu sein. Wir sollten sie uns genauer ansehen und prüfen, ob man sie vereinfachen kann. Oder wenn eine Vereinfachung nicht möglich ist, müssen wir sicherstellen, dass die Implementierung der Schnittstelle funktioniert, bevor irgendetwas Anderes getestet wird.«
602
Der Qualitätskontrollprozess
19.1.2 Protokollieren von Fehlern Eine wichtige Aufgabe in der Entwicklung von Software besteht in der Aufzeichnung der Fehler. Dies kann sowohl mit Papier und Bleistift als auch mit Hilfe einer softwarebasierten Methode erfolgen. Wie Fehler aufgezeichnet werden, ist völlig gleich. Wichtig ist nur die Tatsache, dass Sie aufgezeichnet werden. Ein Fehleraufzeichnungssystem Es gibt viele Möglichkeiten zum Aufzeichnen von Fehlern. Unabhängig davon, welches System Sie verwenden, sollten Sie zu jedem Fehler die folgenden Attribute aufzeichnen: 왘 Problem: Gibt eine Beschreibung des Fehlers. Hierzu genügt ein kurzer Absatz,
der das Problem beschreibt und angibt, wie das normale Verhalten gewesen wäre. 왘 Schritte zur Reproduktion des Problems: Dies ist eine Liste der Schritte, die zu
dem Fehler führten. (Darauf wird im Abschnitt »Testen der Anwendung« noch näher eingegangen.) 왘 Systemmodul oder Schnittstelle: Dies gibt an, in welchem Abschnitt der An-
wendung der Fehler auftrat. 왘 Stadium der Fehlerbehebung: Dies gibt an, was bisher unternommen wurde,
um den Fehler zu beheben. Zu den möglichen Stadien gehören: Fehlererkennung, Fehlerbehebung gestartet, Fehlerbehebung beendet, erneute Fehlerprüfung abgeschlossen, Fehler behoben. 왘 Priorität: Dies gibt die Bedeutung des Fehlers an. Mögliche Kategorien sind:
Kritisch (Fehler ist sehr störend oder führt zum Abbruch der Anwendung), Wichtig (Fehler ist zwar schwerwiegend, lässt jedoch einen Teil der Funktionalität zu), Geringfügig (Fehler beeinträchtigt die Funktionalität der Anwendung), Kosmetisch (die Benutzerschnittstelle reagiert nicht auf die Aktionen), »Nice to have« (die Funktion wäre eigentlich wünschenswert, ist jedoch nicht unbedingt wichtig). Organisieren des Fehlerprozesses Die Betatester liefern die verschiedenen Fehlerberichte. Ihre Betatester sind deshalb die wichtigsten Tester der Anwendungsstabilität. Aus eigener Erfahrung kann ich sagen, dass die besten Betatester diejenigen sind, die vom Unternehmen eigens zur Durchführung des Tests eingestellt werden. Verlassen Sie sich nicht ausschließlich darauf, dass Endbenutzer oder Kunden die Tests durchführen. Den meisten Menschen fehlt die Zeit oder das Wissen zum richtigen Testen. Die gängige Praxis, Betaversionen an die Öffentlichkeit herauszugeben, ist deshalb ein
Definieren des Qualitätskontrollprozesses
603
schlechter Prozess. Es mag zwar interessant sein, Kunden einen Blick auf Ihr Produkt zu gewähren; dennoch dürfen Sie nicht erwarten, anhand einer öffentlichen Betaversion ein stabiles Produkt erstellen zu können. Wenn Fehler vorhanden sind, müssen Sie diese für die individuelle Behandlung organisieren. Die Person, die mit der Organisation der Fehler betraut ist, sollte über gute Kenntnisse des Produkts verfügen und ein Interesse an seinem Erfolg haben. Normalerweise wird die Fehlerorganisation von Projektleitern übernommen. Diese sind nicht nur über die Funktionalität des Produkts informiert, sondern wissen auch am besten, wie ein Fehler zu beheben ist. Projektleiter sind auch in der Lage, die mit der Fehlerbehebung verbundenen Aspekte zu beurteilen. Das nachstehende Beispiel zeigt, wie Fehler nicht organisiert werden sollten. Ein großes Unternehmen hat beschlossen, zur Organisation von Fehlern eigens einen Helpdesk einzurichten, der nichts anderes macht, als Fehler zu erfassen und zu organisieren. Für ein großes Unternehmen ist dies relativ einfach zu bewerkstelligen. Das Problem besteht jedoch darin, dass die mit der Organisation beauftragten Personen keine Erfahrung mit dem Produkt haben und deshalb auch nicht beurteilen können, was die Person, die den Fehler einreichte, damit bezwecken will. Dadurch treten häufig Situationen auf, in denen ein Fehlerbericht mit der Begründung zurückgeschickt wird, dass er nicht reproduzierbar sei. Ein Fehler wird nur weitergeleitet, wenn er mit einem Screenshot belegt wird oder von einer Person des Entwicklungsteams bestätigt wird. Wann kann ein Produkt ausgeliefert werden? Die Frage, wann ein Produkt ausgeliefert werden kann, ist immer schwer zu beantworten. Ich arbeitete einmal mit einem Qualitätssicherungsmanager zusammen, der sein Geschäft wirklich beherrschte. Er konnte den Freigabezeitpunkt für ein Produkt ermitteln, indem er eine Tabelle der gemeldeten und der gelösten Fehler erstellte. Diese Tabelle ist nicht ganz einfach und lässt sich deshalb nicht als einzelnes Diagramm darstellen. Deshalb wollen wir uns einmal die Diagramme der gemeldeten Fehler über der Zeit, der behobenen Fehler über der Zeit, der offenen Fehler und die Fehlerbalken betrachten, die in vielen Unternehmen eingesetzt werden. Gemeldete Fehler Das Diagramm der gemeldeten Fehler ähnelt in der Regel dem in Abbildung 19.1 gezeigten Beispiel.
604
Der Qualitätskontrollprozess
Erzeugte Bugs
Zeit Abbildung 19.1 Typisches Beispiel für ein Diagramm der gemeldeten Fehler
Dieses Diagramm gibt die Anzahl der gemeldeten Fehler pro Zeiteinheit an. Es geht davon aus, dass mit fortschreitender Zeit die Anzahl der gefundenen Fehler sinkt oder die Schwierigkeit des Auffindens wächst. In jedem Fall werden weniger Fehler pro Woche gemeldet. Ich vergleiche diese Kurve im Allgemeinen mit einer inversen Exponentialkurve. In Wirklichkeit hat sie jedoch eher das gezackte Aussehen von Abbildung 19.2.
Erzeugte Bugs
Zeit Abbildung 19.2 Gezacktes Muster des Diagramms der gemeldeten Fehler
Das Zackenmuster tritt auf, weil es bei jeder neuen Betaversion zu einem Anstieg der gefundenen Fehler kommt. Falls es sich bei der freigegebenen Version nicht um eine Version mit eingefrorenem Funktionsumfang handelt, können die Ausschläge noch erheblich stärker sein als in der Abbildung. Um annähernd vorherzubestimmen, wann sich die Anzahl der gefundenen Fehler stabilisiert, kann man
Definieren des Qualitätskontrollprozesses
605
eine geschätzte Kurveneinpassung vornehmen. In allen Fällen erwartet man eine inverse Exponentialkurve. Behobene Fehler Das Diagramm der behobenen Fehler über der Zeit ist das am schwierigsten zu erstellende Diagramm. Dies liegt daran, dass die Fehlerbehebungsrate vom Team abhängt, das die Fehlerbehebung durchführt. Man möchte wissen, wie viele Fehler pro Woche behoben werden können. Um die Berechnung zu vereinfachen, wollen wir annehmen, dass die Anzahl der pro Woche behobenen Fehler eine Konstante ist, die den laufenden Durchschnitt der Anzahl pro Woche behobener Fehler darstellt. Dies ist zwar nicht der beste Indikator, aber der einfachste. Die mögliche Ausnahme, die eine Anpassung erforderlich macht, bilden Situationen, in denen ein definitiver Trend zur Behebung von mehr oder weniger Fehlern zu verzeichnen ist. Meiner Erfahrung nach gibt es jedoch solche Trends nicht, sondern nur einige gute und einige schlechte Tage. Ausstehende Fehler Kombiniert man die Kurve der gemeldeten Fehler mit der Kurve der behobenen Fehler, erhält man das in Abbildung 19.3 dargestellte Diagramm.
Ausstehende Bugs
Zeit Abbildung 19.3 Kurve der ausstehenden Fehler
Diese Kurve hat in jedem Fall eine Spitze, die anzeigt, dass die Anzahl der gelösten Fehler größer ist als die Anzahl der erzeugten Fehler. Nach der Spitze fällt die Kurve dann ab, bis schließlich keine ausstehenden Fehler mehr vorhanden sind. Die Kurve kann in einem Zackenmuster verlaufen, wenn Differenzen zwischen dem Prozentsatz der gemeldeten und behobenen Fehler auftreten.
606
Der Qualitätskontrollprozess
Die Vorhersage des Fertigstellungszeitpunkts der Software kann schwierig sein. In der Kurve der ausstehenden Fehler gibt es jedoch einen Punkt, an dem der Graph seinen Spitzenwert erreicht. Man muss diesen Spitzenwert finden und sicherstellen, dass der Abstieg begonnen hat. Von dort aus kann man mit Hilfe der Kurveneinpassung feststellen, wann eine Anwendung eine bestimmte Anzahl von Fehlern enthalten wird. Fehlerbalken Die meisten Unternehmen implementieren einen so genannten Fehlerbalken. Es ist nicht immer möglich, dass bei der Auslieferung eines Produkts alle Fehler behoben sind. Die Hersteller möchten zwar so viele Fehler wie möglich beheben, jedoch fehlt dazu meistens die Zeit. Deshalb müssen Entscheidungen darüber getroffen werden, welche Fehler behoben werden sollen und wann dies erfolgen soll. Dies wird mit einer mathematischen Gleichung ausgedrückt, in der die Priorität der Fehler, der zur Behebung erforderliche Zeitaufwand und die Kosten zur Behebung der Fehler gewichtet werden. Diejenigen Fehler, deren Behebung die meisten Vorteile bringt und den geringsten Kostenaufwand verursacht, werden als erstes in Angriff genommen. Die anderen Fehler werden behoben, sofern es die Zeit erlaubt. Diese Lösung ist nicht ideal, weil sie in Kauf nimmt, dass die freigegebene Software noch Fehler enthält. Dies lässt sich jedoch nicht immer vermeiden. Hier muss ich erneut betonen, dass sich diese Situationen weitgehend vermeiden lassen, wenn man vor Beginn des Codierens den Entwurf sauber durchdenkt. Ich habe einige sehr grundlegende Diagramme beschrieben, und Sie denken jetzt vielleicht: »Das weiß ich bereits«. Die Frage ist jedoch, ob Sie es auch tatsächlich anwenden. In einem Projekt, in dem der Qualitätssicherungsmanager wusste, wie die Diagramme einzusetzen sind, waren die Ergebnisse recht beeindruckend. Die Teamleiter wussten, wann sie Überstunden machen mussten und wann nicht. Am Ende wurde das Produkt, im Gegensatz zu den Produkten der Mitbewerber, rechtzeitig ausgeliefert. Die Einhaltung des Termins war von entscheidender Bedeutung, weil die Anwendung bei einer Behörde eingesetzt werden sollte und jede Verspätung das Unternehmen die Existenz gekostet hätte.
19.2 Testen der Anwendung Das Testen einer Anwendung ist nicht einfach. Das größte Problem beim Testen besteht darin, in der Lage zu sein, ein Testszenario einzurichten. Und dies ist in der Regel äußerst komplex. Es gibt kommerzielle Tools, die das Testen erleichtern – sie ermöglichen es, Skripts zu schreiben und die Ausgabe der Anwendung aufzuzeichnen. Danach muss der Tester die Tests durchgehen und sehen, was passiert oder nicht. Diese Tools bringen jedoch keine wesentliche Erleichterung des Testens.
Testen der Anwendung
607
Testtools sollten daher zur Unterstützung der Organisation und Wiederholung eines von Ihnen erstellten Testszenarios eingesetzt werden. Dies erfordert in der Regel das Erstellen einiger COM-Objekte sowie von Skripts und einigen Microsoft Office VBA-Anwendungen (Visual Basic für Anwendungen).
19.2.1 Definieren einer Teststrategie In diesem Abschnitt definieren wir eine Teststrategie auf der Basis der Konferenzanmeldungsanwendung. Die Konferenzanmeldungsanwendung besitzt eine Website, die mit der ASP-Bibliothek (Active Server Page) kommuniziert. Die ASPSkripts übernehmen entweder die Ausführung und Kommunikation mit der Datenbank direkt, oder sie kommunizieren mit einer Reihe von Anwendungsobjekten. Die Geschäftsobjekte bieten die Möglichkeit, Nachrichten zu versenden, andere Geschäftsobjekte aufzurufen oder die Datenbank direkt aufzurufen. Auf der Grundlage dieser Anwendung gibt es drei verschiedene Arten von Tests, die eingesetzt werden. Die einfachste Art ist der Einheitentest, bei dem die einzelnen Schnittstellen und ihre Implementierungen geprüft werden. Danach, wenn mehrere Implementierungen kombiniert sind, wird ein Integrationstest durchgeführt. Zum Schluss wird ein Systemtest durchgeführt, bei dem alle Komponenten zusammen geprüft werden. Die Reihenfolge dieser Tests muss zwingend eingehalten werden, alles andere wäre Zeit- und Geldverschwendung. Einheitentest Die Anwendung ist im Prinzip nichts weiter als eine Reihe von Objekten, die miteinander kommunizieren. (Ich verwende den Begriff »Objekt« sehr weit gefasst, um zwei Codeabschnitte zu bezeichnen, die miteinander kommunizieren.) Die Objekte sind in einer Schichtenhierarchie angeordnet. Das Herausgreifen eines Objekts aus der Hierarchie für Testzwecke wird als Einheitentest bezeichnet. Dabei wird das Objekt isoliert getestet, um festzustellen, ob es in sich irgendwelche Fehler enthält. Der Einheitentest wird in Abbildung 19.4 veranschaulicht. Ein Treiber ist ein Programm zur Steuerung des Objekts. Der Treiber ruft die Methoden auf und zeichnet dann die Ausgabe der Ergebnisse auf. Danach werden die Ausgaben untersucht, um festzustellen, was ordnungsgemäß durchgeführt wurde und wo Fehler auftraten.
608
Der Qualitätskontrollprozess
Treiber
Objekt
Stub Abbildung 19.4 Testen einer Schicht mit Hilfe eines Treiber und eines Stubs
Der Stub stellt die Objekte dar, die das Objekt aufruft. Der Stub ist ein einfaches COM-Objekt, das eine Antwort gemäß der Definition des von ihm implementierten COM-Objekts zurückgibt. Der Stub ermöglicht es, einen Fehler in einem bestimmten Objekt zu isolieren, sodass er behoben werden kann, bevor das Objekt wieder in die Anwendung eingegliedert wird. Die Verwendung von Stubs ist erforderlich, weil die zugrunde liegenden Objekte möglicherweise nicht perfekt und vollständig getestet sind. Das Testen eines Objekts, das von anderen fehlerhaften Objekten abhängt, kompliziert das Debuggen, weil man nicht sicher sein kann, ob der Fehler im isolierten Objekt auftritt oder durch ein fehlerhaftes anderes Objekt verursacht wird. Ermitteln, wann ein Stub benötigt wird Manchmal muss ein Stub geschrieben werden. Dies ist beispielsweise erforderlich, wenn Sie ein Rückrufobjekt testen, wie in Abbildung 19.5 gezeigt. In diesem Fall wird Objekt A getestet. Es ruft Objekt B auf, das über einen Rückruf mit Objekt A verknüpft ist. Um den Einheitentest ordnungsgemäß durchzuführen, muss angenommen werden, dass Objekt B fehlerfrei funktioniert, während Objekt A getestet wird. Dies erfordert jedoch, dass Objekt B erfolgreich getestet wurde, was wiederum voraussetzt, dass Objekt A einwandfrei funktioniert. Dieser Rückrufzyklus ist endlos. Um eines der beiden Objekte zu testen, muss daher ein Stub geschrieben werden, der an Stelle des jeweils anderen Objektes tritt. Eine weitere Situation, in denen Stubs erforderlich sind, besteht dann, wenn ein bestimmter Programmbereich sehr problematisch ist. Möglicherweise hat dieser Bereich den höchsten Anteil von Fehlern, und jeder behobene Fehler verursacht ein anderes Problem. In diesem Fall müssen Sie das Objekt vom restlichen System isolieren und danach vollständig testen, um zu sehen, welche Funktionen funktionieren und welche nicht.
Testen der Anwendung
609
Treiber
Objekt A
Objekt B
Abbildung 19.5 Rückrufobjekte
Integrationstest Wenn die Einheitentests für jedes Objekt ordnungsgemäß durchgeführt wurden, dann müssten alle Objekte jeweils einen Treiber und einen Stub besitzen. Das Erstellen eines Stubs für jedes Objekt ist jedoch sehr arbeitsaufwändig. In einigen Fällen ist es gar nicht möglich, einen effizienten Stub zu schreiben, weil ein echter Stub einen zu hohen Programmieraufwand erfordert. In diesen Fällen kann die Methode des Integrationstests verwendet werden. Beim Integrationstest können einige Stubs wegen des Schichtenaufbaus der Anwendung entfallen. In einer Schichtenarchitektur einer Anwendung gibt es immer eine grundlegende Schicht, für die kein Stub benötigt wird, weil es sich um den Kern der Anwendung handelt. Deshalb ist kein Stub erforderlich, wenn man mit dem Testen auf der untersten Schicht beginnt und von dort nach oben geht. Dieses Testmodell setzt voraus, dass die Schichten keine Aufrufe in höhere Schichten enthalten, sondern alle Aufrufe von ungetesteten Schichten nach unten zu getesteten Schichten erfolgen. In Kapitel 4 haben wir eine Schichtenarchitektur untersucht, in der die tieferen Schichten keine höheren Schichten aufrufen. Deshalb habe ich dort gesagt, dass ein guter Schichtenentwurf zu einem System mit weniger Fehlern führt. Es gibt Situationen, in denen mehrere Unternehmen oder Abteilungen eine Anwendung implementieren und ein vollständiger Integrationstest nicht möglich ist, weil einige Systeme noch nicht implementiert sind. In diesem Fall muss ein Stub geschrieben werden. Dieser vereinfacht auch das Testen, weil die verschiedenen Unternehmen oder Abteilungen ihre eigenen Fehler beheben können, bevor sie untersuchen, welche Fehler durch die Integration verursacht werden.
610
Der Qualitätskontrollprozess
Systemtest Der Systemtest ist der letzte Schritt beim Testen. Er findet erst statt, wenn alle Objekte zu einer Gesamtanwendung zusammengefügt wurden und zusammen getestet werden. Die Beteiligung von Betatestern oder Endbenutzern erfolgt in dieser Phase. Sie können erkennen, was das System leistet, und prüfen, ob die Funktionen visuell richtig sind. Der Systemtest hat einen anderen Schwerpunkt als der Integrations- oder Einheitentest. Während diese auf Spezifikationen basieren, werden Systemtests durchgeführt, um sicherzustellen, dass das Geschäftsproblem einwandfrei gelöst wird. Dies bedeutet jedoch nicht, dass der Systemtest zu einer größeren Umgestaltung des Entwurfs missbraucht werden kann. Das sollte viel früher in den Entwurfsund Prototypphasen getan worden sein. Mit dem Systemtest sollten funktionsbezogene Fragen geprüft werden, etwa, ob Meldungen ordnungsgemäß ausgegeben werden, Daten in der Datenbank gespeichert werden usw. Beim Systemtest werden Aspekte geprüft, die nur in der funktionierenden Anwendung auftreten. Zeitpunkt für den Systemtest Mit Visual Studio ermöglicht es Microsoft, eine Anwendung von Anfang bis Ende in einer Aktion zu debuggen. Das hört sich zwar gut an, sollte jedoch niemals versucht werden, wenn Sie das Debuggen in einem Systemtest durchführen, weil es der Suche nach der Nadel im Heuhaufen gleichkommt. Während des Systemtests ist es wegen der großen Zahl der zusammenwirkenden Objekte praktisch unmöglich, einen Fehler zu finden und seine Auswirkungen einzugrenzen. Ein Systemtest verwendet eine Benutzerschnittstelle. Beim Auftreten eines Fehlers muss daher ein Workaround entwickelt werden, damit Tester die anderen Funktionen testen können. Workarounds sind jedoch ungünstig, weil dadurch der Test nicht vollständig durchgeführt wird. Deshalb ist der Systemtest kein Ersatz für den Einheitentest oder den Integrationstest. Regressionstest Vor dem Einrichten eines Tests müssen einige andere Aspekte berücksichtigt werden. Wird beispielsweise ein Fehler gefunden, muss dieser behoben werden. Nach der Behebung muss ein erneuter Test durchgeführt werden. Dies wird als Regressionstest bezeichnet. Die ausgearbeiteten Tests müssen erneut durchgeführt werden um sicherzustellen, dass sie erneut erfolgreich sind. Regressionstests sind extrem langweilig. Viele Menschen umgehen deshalb diese Phase. Sie ist jedoch ein äußerst wichtiger Teil des Testens. Es ist von Bedeutung, dass Automatisierungstools eingesetzt werden, um die Regressionstests systematisch durchzuführen.
Testen der Anwendung
611
Coverage-Test Wie viele Tests reichen aus, um sicherzustellen, dass eine Anwendung als vollständig getestet gelten kann? Dies ist eine interessante Frage. Der traditionelle Ansatz geht davon aus, jeden Aspekt der Anwendung zu testen. Erst wenn alle Tests erfolgreich waren, gilt die Anwendung als fehlerfrei. Dieser Ansatz ist jedoch auch am zeitaufwändigsten. Er setzt voraus, dass Sie alle Möglichkeiten bedenken, und dann jede manuell durchspielen. Dabei besteht das Problem, dass mit zunehmender Komplexität des Systems dessen Kosten exponentiell ansteigen, und entsprechende zusätzliche Kosten für das Testen einkalkuliert werden müssen. Das Testen in komplexen Situationen, beispielsweise bei Computersystemen zur Steuerung von Kernkraftwerken, geht über den Rahmen dieses Buches hinaus. Wie lässt sich der Coverage-Test in Windows DNA effizient durchführen? Zunächst einmal können Sie keinen vollständigen Coverage-Test durchführen, weil Windows 2000 so viele Teilsysteme enthält, die nicht vollständig getestet wurden. Sie müssen davon ausgehen, dass Windows 2000 fehlerfrei ist. Sie können lediglich Einheits-, Integrations- und Systemtests für Ihre Anwendung erstellen und das Beste hoffen. Darauf läuft es letztendlich hinaus. Dies ist zugegebenermaßen keine gute Antwort, aber die beste, die ich anbieten kann. Wer ist Eigentümer der Testskripts? Das Schreiben einer Anwendung zum Durchführen von Tests obliegt in der Regel dem Qualitätssicherungsteam. Dies ist jedoch nicht der beste Ansatz. Das Qualitätssicherungsteam versteht vielleicht, was die Anwendung leisten soll, jedoch nicht den ihr zugrunde liegenden Entwurf. Den Entwurf versteht nur das Architekturteam. Deshalb sollte es auch für die Erstellung der Testanwendungen verantwortlich sein. Das Architekturteam braucht nicht die Tests selbst durchzuführen oder die Testergebnisse verarbeiten. Indem es die Tests schreibt, erkennt es jedoch die Vorteile oder Probleme seines eigenen Entwurfs. Dies erzeugt einen Feedbackzyklus, der zur Verbesserung der nächsten Anwendung eingesetzt werden kann. Außerdem kann das Architekturteam in der Testphase ohnehin nichts anderes tun. Wann soll das Projekt erstellt werden? Ob es sinnvoll ist, das Erstellen täglich durchzuführen, darüber gehen die Meinungen auseinander. Die Ursache für die unterschiedlichen Auffassungen besteht darin, dass der Umfang von Projekten verschieden ist. Ein befreundeter Consultant meint, dass ein tägliches Erstellen nur unnötig Entwicklungszeit kostet. Ein Freund bei Microsoft sagte dagegen, dass das tägliche Erstellen von Windows NT/ 2000 wegen der Größe der Anwendung erforderlich war.
612
Der Qualitätskontrollprozess
Auf jeden Fall muss das Erstellen häufig genug erfolgen. Häufig bedeutet nicht, dass jede Person ihren Quellcode jeden Abend einbringt. Dies würde nur dazu führen, dass der Entwicklungsprozess außer Kontrolle gerät. Kein Programmierer kann an einem Tag Code schreiben, der perfekt und fehlerfrei ist. In die Erstellung soll nur solcher Code einbezogen werden, für den ein Einheitentest durchgeführt wurde. Diese Strategie wurde beim täglichen Erstellen während der Entwicklung von Windows 2000 angewandt. Wegen des enormen Umfangs von Windows 2000 kam es immer zu Änderungen bei den Integrations- und Systemtests.
19.2.2 Entwickeln eines Frameworks Ein Framework ermöglicht es, Einheiten-, Integrations- und Systemtests durchzuführen. Es ist erforderlich, weil es das Auffinden von Fehlern wesentlich erleichtert. UNIX-Programmierer haben einen wirklich bescheidenen Debugger. Sie gleichen diesen Mangel jedoch mit Debug-Unterstützung für die Laufzeit in Form von Bibliotheken und Dienstprogrammen mehr als aus. In den nachstehenden Abschnitten fasse ich die verschiedenen Möglichkeiten zusammen, mit denen Sie die Qualität Ihres Codes verbessern können. Überprüfung des Quellcodes Der Quellcode sollte normalerweise im Rahmen von Codeüberarbeitungen überprüft werden. Dabei werden jedoch oft nur wenige Fehler gefunden, weil Codeüberarbeitungen vielfach nur durchgeführt werden, um sicherzustellen, dass allgemeine Programmiermethoden eingehalten wurden. Dies ist jedoch nur eine der Funktionen von Codeüberarbeitungen. Quellcodeüberarbeitungen lassen sich mit Hilfe von automatisierten Tools optimieren. Diese Tools gehen den Quellcode durch und machen auf typische Fehler aufmerksam. Die Firma NUMEGA ist für ihre hochwertigen Tools dieser Art bekannt und vertreibt Tools zur Quellcodeanalyse für alle Visual Studio Entwicklungsumgebungen. Es gibt auch zahlreiche Freewaretools im Internet, die dasselbe für C++ leisten. Ihre Handhabung ist etwas komplizierter. Dafür werden sie jedoch mit dem Quellcode geliefert, sodass Sie das Tool an die Programmierrichtlinien Ihres Unternehmens anpassen können. Nachdem Sie den Code mit einem dieser Tools analysiert haben, müssen Sie ihn auch noch selbst auf mögliche Codierungsprobleme untersuchen. Dabei müssen Sie ständig zwischen Entwurf und Implementierung hin und her wechseln. Dieser Prozess ist langwierig, aber notwendig. Statt von einer Codeüberarbeitung kann auch von einer Projektprüfung gesprochen werden.
Testen der Anwendung
613
Vorbereiten der Anwendung für das Testen Das Problem beim Debuggen umfangreicher verteilter Anwendungen besteht darin, die Fehler zu replizieren. Manche Fehler treten nur einmal in zehn Tests auf. Die neun zusätzlichen Debugvorgänge zum Herausfinden des einen Fehlers sind sehr aufwändig. Die Anwendung muss unbedingt in der Lage sein, den Status beim Auftreten von Fehlern zu erfassen. Schreiben von Protokollierungscode Die meisten unserer Implementierungen sind COM-Objekte. Die COM-Objekte befinden sich in der Schicht der Anwendungslogik, die der zentrale Ort ist, den alle Daten passieren müssen. Damit sind COM-Objekte auch der geeignete Ort, um Programmiercode zur Protokollierung einzufügen. Der Protokollierungscode ermöglicht es, beim Auftreten von Fehlern das Problem nachzuvollziehen. Das wahllose Protokollieren beliebiger Informationsarten ist jedoch nicht sehr sinnvoll. Vielmehr soll nur der Status des Objekts protokolliert werden. Unser Code enthält bereits eine Reihe von Vorkehrungen, die dieses Szenario unterstützen. Zunächst schreiben wir unseren Code unter Verwendung von Ausnahmen. Die Ausnahmen erfassen Statusinformationen und schreiben diese in eine Protokolldatei. Ferner wird die Ausnahme verbreitet, wodurch weitere Statusinformationen erfasst werden. Der Protokollierungscode des nachstehenden Beispiels wurde in Visual C++ implementiert: STDMETHODIMP CAGTProject::execute() { try { TNINTERFACELib::IImplResolverPtr ptrResolve( "TNInterface.ImplResolver.1"); TNINTERFACELib::IResolverPtr ptrFile = ptrResolve->getDefaultImpl(); … if( checkoutStatus != FALSE) { // Datei in der Struktur nach oben verschieben char tmpPath[ MAX_PATH]; if( getStringValue( SEC_ROOT, KEY_FTPDIRECTORY, tmpPath) == false) { GenError( "Cache directory key does not exist"); } } } catch( _com_error err) { TraceCOM(); return err.Error(); } return S_OK; }
614
Der Qualitätskontrollprozess
Der gesamte Code dieses Beispiels ist in einem Ausnahmeblock enthalten. Das ist an sich nichts Neues. Neu ist dagegen der Aufruf der Funktion GenError. Diese Funktion ist ein Makro, das den aufgetretenen Fehler protokolliert. Die Funktion zeichnet die Zeilennummer und die Datei auf, in der sich der Fehler befindet. Der Fehler wird dann in einen COM-Fehler konvertiert. Der COM-Fehler wird im Ausnahmeblock erfasst, und die Funktion TraceCOM wird aufgerufen, die den Fehler erneut protokolliert. Diese zweite Protokollierung ist erforderlich, weil mit der COM-Compilerunterstützung nicht alle Fehler ein Ergebnis des Aufrufs der Funktion GenError sind. Die Methode err.Error() gibt den Fehler als externen COM-Fehler zurück, der dann vom Aufrufer empfangen und behandelt wird. Wenn der Aufrufer diesen Vorgang wiederholt, liegt eine Ereignis- und Statuskette vor. Dadurch lässt sich leichter feststellen, welche Aktion das COM-Objekt ausführte und weshalb diese nicht erfolgreich war. Protokollieren von Code innerhalb eines Skriptkontextes Das obenstehende in Visual C++ geschriebene Beispiel lässt sich einfach auf einen Skriptkontext übertragen. Da die Protokollroutinen zur Aufzeichnung von Fehlern COM-Objekte verwenden, ist dies auch mit der Skriptsprache möglich. Der einzige Unterschied besteht darin, dass bei einem Skriptkontext keine Datei und Zeilennummer vorhanden sind und der Fehler mit Hilfe von Ausnahmen weitergeleitet werden kann. Wozu ein benutzerdefiniertes Protokollierungsobjekt erstellen? Hier stellt sich die Frage, weshalb wir benutzerdefinierte COM-Protokollierungsobjekte schreiben. Wäre es nicht effizienter, den Ereignisdienst von Windows 2000 zu nutzen? Als ich ursprünglich Systeme für Kunden erstellte, war das Protokollieren in das Ereignissystem nicht machbar, weil der Kunde die Möglichkeit zur Ferndiagnose benötigte. Mit Hilfe einer benutzerdefinierten Fehlerprotokollierungsfunktion war das System in der Lage, eine Protokolldatei zu schreiben und diese automatisch an mich zu senden – der Kunde brauchte selbst nichts weiter zu unternehmen, um uns die Fehlerinformationen zu senden. Darüber hinaus werden manchmal sehr große Mengen von Protokolldaten erzeugt, und der Ereignisdienst ist nicht unbedingt dafür konzipiert, mehrere Megabyte von Daten aufzunehmen. Schreiben eines Stubs Einen Stub zu schreiben ist nicht sonderlich kompliziert. Der Stub ist die Implementierung einer COM-Schnittstelle, die eine Reihe von Parametern und Methoden besitzt. Wie diese aufgerufen werden, hängt vom Entwurfsdokument ab, weil der Stub einfach das Entwurfsdokument implementiert. Im Prinzip wird eine
Testen der Anwendung
615
Wahrheitstabelle implementiert. Dies bedeutet, wenn der Parameter X und die Methode Y ist, dann sollte das Ergebnis Z sein.
19.2.3 Schreiben eines Treibers Um einen Treiber zu schreiben, müssen Sie das Entwurfsdokument durchgehen, die verschiedenen Szenarien aufrufen und testen, ob die Ergebnisse dem Entwurfsdokument entsprechen. Dies kann kompliziert sein, weil zum Erzeugen der verschiedenen Testbedingungen Permutationen und Kombinationen erforderlich sind. Ferner ist es auch kompliziert festzustellen, ob alles tatsächlich ordnungsgemäß ablief. Wir wollen einen Treiber schreiben, der den Einheitentest für eine bereits vorgestellte COM-Schnittstelle und Implementierung durchführt – nämlich das Operationsobjekt, mit dem ein neuer Benutzer zu der Konferenzanmeldungsanwendung hinzugefügt werden kann. Wenn Sie sich nicht sicher sind, wie die COMSchnittstelle und die Implementierung arbeiten, können Sie dies in den Kapiteln 8 und 9 nachlesen. Definieren eines Katalogs von Testbedingungen Für jede Schnittstelle, die getestet werden soll, muss unabhängig von ihrem Typ ein Bedingungskatalog definiert werden. Der Bedingungskatalog definiert die verschiedenen Bedingungen der zu verwendenden Methodenparameter. Es ist wichtig, die Bedingungen zu identifizieren, weil diese zum Erstellen eines Testplans verwendet werden. Bei einer zu geringen Zahl von Bedingungen wird der Plan nicht stabil und Fehler können auftreten. Ich will nicht das Testen des gesamten Operationsobjekts beschreiben, sondern lediglich eine einzelne Methode herausgreifen. Wir werden einen Bedingungskatalog für die folgende Methode erstellen: HRESULT ISimpleOperations::addUser( [out,retval]long *userId);
Für die Methode selbst gibt es keine Eingabebedingungen. Vielmehr gibt es nur einen Ausgabeparameter. Jetzt denken Sie vielleicht, dass das Erstellen eines Bedingungskatalogs einfach wird. Wenn die Methode aufgerufen wird, muss die Ausgabe größer 1 sein. Dies stimmt allerdings nicht ganz, weil die Parameter für diese Methode in der Schnittstelle ISimpleData gespeichert werden. Der Bedingungskatalog muss daher neben der Methode selbst auch die COM-Schnittstelle ISimpleData berücksichtigen, die folgendermaßen definiert ist:
Wenn Sie einen Parameter in den Bedingungskatalog aufnehmen, müssen Sie einen gültigen und einen ungültigen Wert angeben. Dies wird als Äquivalenzpartitionierung bezeichnet. Im Bedingungskatalog sollen nur die gültigen Werte definiert werden. In unserem Fall lautet der Bedingungskatalog für die Methode addUser: Variable
Bedingung
username
8 < length < 255
username
value != vorhandener Wert
Kennwort
8 < length < 255
userid
generiert
Gemäß unserem Konzept der Äquivalenzpartitionierung müssen für das Kennwort drei Bedingungen getestet werden: eine Zeichenfolge mit einer Länge von weniger als 9 Zeichen, eine Zeichenfolge mit einer Länge zwischen 9 und 254 Zeichen sowie eine Zeichenfolge mit einer Länge von mehr als 254 Zeichen. Das Testen weiterer Bedingungen wäre eine Verschwendung von Verarbeitungszyklen. Von den drei Bedingungen sind jedoch zwei negativ und eine ist positiv: Negative Bedingungen testen spezifische Situationen, in denen die Methode fehlschlagen sollte; positive Bedingungen testen einen erfolgreichen Methodenaufruf. Es ist verlockend, mehr Tests durchzuführen, denn wie können drei Tests ausreichen? Betrachten wir den Fall, dass nach einem Benutzer anhand einer userId gesucht wird. Theoretisch kann jede Zahl, die einer userId zugeordnet ist, einen Benutzer darstellen. Deshalb ist nur ein Test erforderlich, in dem die UserId zugeordnet wird. Falls dies funktioniert, müsste alles in Ordnung sein. Dies stimmt natürlich nicht. Denn der Benutzer kann ja entweder vorhanden sein oder nicht, und beide Situationen müssen geprüft werden. Hier tritt das Problem auf, dass die Testbedingungen mit den Szenarien des Implementierungstests vermischt wer-
Testen der Anwendung
617
den. Der Testkatalog legt fest, wie viele Tests mindestens ausgeführt werden müssen, um alle Möglichkeiten in Bezug auf die betreffenden Parameter zu prüfen. Wir haben jedoch vergessen, in die Suche nach einem Bedingungskatalog für den Benutzer den Geschäftsprozess zu berücksichtigen. Kehren wir noch einmal zum ursprünglichen Bedingungskatalog für die Methode adduser zurück. Die Bedingung, die aussagt, dass der username nicht bereits vorhanden sein darf. Diese Testbedingung basiert auf einer Anwendungslogik und kann nur durch die Implementierung der Methode ermittelt werden. Bei positiven Bedingungen ist eine weitere Situation denkbar, in der Fehler auftreten können, die so genannte Grenzbedingung. Von einer Grenzbedingung spricht man, wenn die Testbedingung an der Akzeptanzgrenze ausgeführt wird. Eine Grenzbedingung wäre etwa vorhanden, wenn die Zeichenfolge password eine Länge von acht Zeichen besitzt. Der Wert ist zwar gültig, aber viele Programmierer haben die Angewohnheit, die Zahl Acht mit Neun oder Sieben zu verwechseln. Dies hört sich zwar unsinning an, tritt jedoch gar nicht einmal selten auf. Mit der Aufnahme des Grenztests ist die Anzahl der Testbedingung für unser password jetzt vier (zwei negative und zwei positive Bedingungen, welche die Grenzbedingungen sind). Die verschiedenen Längen der Testzeichenfolge sind 7 und 8, sowie 254 und 255. Dies ist die maximal erforderlich Anzahl von Testszenarien für das Kennwort. Manche Menschen fühlen sich damit unwohl und empfehlen, mehr Tests durchzuführen. Um sie zu beruhigen, können zwei zusätzliche Tests aufgenommen werden, nämlich eine zufällige gültige Bedingung und eine zufällige ungültige Zufallsbedingung. Damit steigt die Zahl der Testbedingungen auf sechs. Jede weitere Testbedingung wäre reine Zeit- und Geldverschwendung. Mehr Testbedingungen werden lediglich beim Testen von mathematischen Algorithmen benötigt. Dies liegt daran, dass bei einer fehlerhaften Implementierung einer mathematischen Berechnung ein kleiner Testbedingungskatalog möglicherweise erfolgreich ausgeführt wird, fehlerhafte Berechnungen jedoch nur mit einem größeren Bedingungskatalog zu Tage treten. Definieren von Testfällen Nachdem die einzelnen Testbedingungen definiert wurden, müssen jetzt die einzelnen Testfälle definiert werden. Die Testfälle werden entweder als negativ oder als positiv definiert. Ein positiver Testfall bedeutet, dass alles in Ordnung und stabil ist. Ein negativer Testfall bedeutet, dass ein Fehler auftrat und protokolliert werden muss.
618
Der Qualitätskontrollprozess
Der Treiber verwendet die Testfälle zum Aufruf der Schnittstelle. Ein Testfall entsteht durch Kombinieren der verschiedenen Parameter mit einer Testbedingung. Für jeden Parameter und jede Testbedingung muss ein Testfall generiert werden. Die Aufgabe wird wirklich zu einer Sache von Permutationen der verschiedenen Parameter und Testbedingungen. Die einfachste Art, dies zu organisieren, besteht im Erstellen einer Testmatrix. In der folgenden Tabelle sind alle Testfälle für die COM-Schnittstelle unseres Beispiels definiert. Die Tabelle enthält Grenztests, jedoch keine zufälligen oder iterativen Testbedingungen. Nummer
Ich habe keine vollständige Permutation der Testbedingungen durchgeführt. Dies liegt nicht daran, dass ich dem Autoren des Codes traue oder aber faul bin. Vielmehr liegt die Ursache darin, dass ich die Testbedingungen optimiere. Sehen wir uns beispielsweise die Testfälle 1 und 2 an. Beide sind negative Tests. Der erste negative Testfall ist ein Ergebnis einer positiven und einer negativen Testbedingung. Der zweite Testfall ist das entgegengesetzte Szenario. Der fehlende negative Test wäre, wenn beide Testbedingungen negativ wären. Dieser Test ist jedoch unnötig, weil jeder Parameter bereits auf Negativität getestet wurde und durch die Kombination der beiden kein anderer Codepfad ausgeführt wird. Eine solche Optimierung ist jedoch nicht in jedem Fall möglich. Sie funktioniert bei einfachen Testbedingungen, unter Umständen aber nicht bei mathematischen Formeln, weil dort zweimal negativ auch positiv sein kann. Die beiden positiven Tests basieren auf den Grenztestbedingungen. Beim Schreiben des Treibers ist zu beachten, dass die beiden positiven Tests nicht dieselbe Testbedingung für username verwenden dürfen. Angenommen, Sie fügen einen Benutzer hinzu und der Treiber verwendet denselben Benutzernamen für die Testfälle 5 und 6. Bei Test 5, dem ersten positiven Testfall, würde dies funktionieren. Wenn Sie dieselbe Testbedingung für username jedoch in Test 6, dem zwei-
Testen der Anwendung
619
ten positiven Fall, verwenden, ist der Test nicht erfolgreich. Nehmen wir weiter an, dass Sie danach einen Fehlerbericht senden, der Entwickler den Code testet und dabei keine Fehler auftreten. Der Tester prüft den »behobenen« Code, und der Fehler tritt erneut auf. Jetzt liegen sich Tester und Entwickler in den Haaren. Wer hat Recht? Die Antwort lautet: der Entwickler. Denn bei dem vermeintlich positiven Testfall 6 handelt es sich eigentlich um den negativen Testfall 7. Der Treiber hat die falschen Daten und muss korrigiert werden. Schreiben eines Skripttreibers Die einfachste Möglichkeit zum Ansteuern einer Reihe von COM-Objekten besteht darin, mit Hilfe des Windows Scripting-Hosts eine Serie von Skripts zu schreiben. Standardmäßig installiert Windows 2000 die beiden Skriptsprachen VBScript und JScript (Microsoft Version von JavaScript). Andere Skriptsprachen wie beispielsweise Perl, Rexx oder Python lassen sich jedoch ebenfalls verwenden. Meine Beispiele wurden mit JavaScript geschrieben. Der nachfolgende mit JavaScript erstellte Beispieltreiber führt den Testfall 1 aus: var tstObject; var logObject; logObject = WScript.CreateObject("Tester.logging"); logObject.startTestCases("SimpleOperations::addUser method test"); tstObject = WScript.CreateObject( "SimpleProject.ImplOperations"); tstObject.simpleData.username = "A valid name"; tstObject.simpleData.password = "invalid"; try { tstObject.addUser(); logObject.failedNegativeTest( 1); } catch( e) { logObject.successfulNegativeTest( 1); } … logObject.endTestCases();
Ich erwarte, dass dieses Skript einen negativen Test darstellt und fehlschlägt. Das Skript enthält ein Protokollierungsobjekt, damit ich den Ablauf nachvollziehen kann. Dies erleichtert auch die Durchführung von Regressionstests. In diesem Testfall wird beispielsweise die Methode addUser getestet. Die Protokollobjektmethode startTestCases öffnet eine Datei, in der die Ausgabe der verschiedenen Testfälle gespeichert werden. Danach werden die verschiedenen Testbedingungen zugeordnet, und die Methode tstObject.addUser wird aufgerufen.
620
Der Qualitätskontrollprozess
Die Windows 2000-Version von JavaScript enthält eine neue Funktion zur Ausnahmebehandlung. Der Mechanismus zur Ausnahmebehandlung erfasst alle COM-Fehler. Damit wird das Skript beim Auftreten eines Fehlers diesen anhand des Fehlererzeugungsschemas aufzeichnen. Da es sich um einen negativen Test handelt, müsste eine Ausnahme generiert werden. Dies bedeutet, dass die Protokollobjektmethode successfulNegativeTest mit der Testfall-ID aufgezeichnet werden sollte. Falls keine Ausnahme erzeugt wird, muss dies als fehlgeschlagener Test mit Hilfe der Methode failedNegativeTest aufgezeichnet werden. Sie fragen sich jetzt vielleicht, weshalb der Status nicht gespeichert wird, wenn der Test fehlschlägt. Auf den ersten Blick sieht es so aus, als ob das Speichern der Statusinformationen das Debuggen der Schnittstelle erleichtern würde, dies ist jedoch nicht der Fall. Bei dieser Art von Einheiten- und Integrationstest lässt sich der Testfall einfach identifizieren und replizieren, sodass der Fehler in der Debugging-Sitzung gefunden werden kann. Wie Sie wissen, werden die Integrationstests Schicht für Schicht durchgeführt, was die Komplexität reduziert. Schreiben anderer Treiber Es gibt noch andere Arten von Treibern, die geschrieben werden müssen, beispielsweise Web- oder Datenbanktreiber. Alle diese Treiber werden nach demselben Muster entwickelt. Zuerst werden die Testbedingungen festgelegt, danach werden die Testfälle definiert, und zum Schluss werden die Testfälle mit der betreffenden Programmiersprache implementiert. Beim Testen einer Datenbank wird als Programmiersprache wahrscheinlich kein Skript, sondern vielleicht eher ein in den Sprachen Visual Basic oder Visual C++ geschriebenes Programm verwendet. Die Tools und Umgebungen variieren zwar, der Prozess des Testens ist jedoch immer gleich. Schreiben eines Benutzerschnittstellentreibers Beim Systemtest verwenden Sie die Benutzerschnittstelle. Dadurch wird das Testen erschwert. Es gibt zwar Skripttools, die Schnappschüsse von einem Testfall erstellen können, jedoch kann ein Computer damit nicht viel anfangen. Hier können Automatisierungstools helfen, die es ermöglichen, Bilder zu erfassen und zu vergleichen. Solche Tools sind sehr leistungsfähig und nützlich. Unverzichtbar für einen Benutzerschnittstellentreiber ist ein Dienstprogramm, mit dem sich Tastatur- und Mausaktionen aufzeichnen lassen. Falls Regressionstests erforderlich sind, braucht der Tester nur die aufgezeichnete Sitzung zu aktivieren und zu prüfen, ob alles ordnungsgemäß abgelaufen ist. Dies wirkt nicht nur der Langeweile des Testers entgegen, sondern gewährleistet auch die Konsistenz. Aufzeichnungsprogramme wie etwa Macro Magic sind schon für weniger als 100 DM erhältlich.
Testen der Anwendung
621
Derselbe Prozess des Definierens der Testfälle gilt auch für den Test der Benutzerschnittstelle. Ausnahme ist lediglich, dass die Testbedingungen nicht auf Zahlen oder Buchstaben basieren – sondern vielmehr auf den ausgeführten Aktionen. Denken Sie daran, dass während des Systemtests keine Einheiten- oder Integrationstests durchgeführt werden sollten. Diese sollten vorher abgeschlossen worden sein.
19.3 Leistungstests Beim Entwerfen, Entwickeln von Prototypen und Testen von Anwendungen steht immer die Frage im Vordergrund, wie sich eine bestmögliche Leistung erzielen lässt. Nicht immer bietet ein Entwurf, der eigentlich optimal funktionieren sollte, in der Implementierung die erwartete Leistung. In diesem Fall fällt meist die Entscheidung zur Anschaffung leistungsfähigerer Hardware. Wenn an der Leistungsschraube gedreht werden muss, versuchen die Entwickler, die Probleme zu finden und einige Änderungen vorzunehmen. Betrachten wir jedoch einmal die Situation beim Erstellen einer verteilten Anwendung. In einem solchen Fall müsste unter Umständen eine große Menge schnellerer Hardware angeschafft werden. Leistung kann nicht einfach in die Anwendung »hineingeplant« werden, weil man im Allgemeinen nicht weiß, wo die Anwendung verteilt wird. Deshalb ist es sinnvoll, die Leistung als weiteren quantifizierbaren Parameter in den Entwurfsprozess einzuführen. Hierzu muss der Leistungstest in zwei Kategorien aufgeteilt werden: Testen der Hardwareleistung auf der einen Seite und Testen der Leistung der Anwendung und der Infrastruktur auf der anderen Seite.
19.3.1 Testen der Hardware Während meiner Consulting-Tätigkeit habe ich es selten genug erlebt, dass sich ein Kunde die Zeit für einen Leistungstest der Hardware nimmt. Man kauft ganz einfach schnellere Computer, weil dies das Problem löst. Die wenigsten Menschen führen tatsächlich eine Rentabilitätsberechnung für eine Hardwarekomponente durch. Beim Testen der Hardware wird eine Gruppe von Benchmarks auf der Grundlage der betreffenden Umgebung und Bedingungen eingerichtet. Die Tests werden jedoch in einer Weise gesteuert, dass zwei verschiedene Geräte dieselbe Anwendung innerhalb desselben Kontextes durchführen. Mit dem Application Performance Explorer (APE) steht in Microsoft Visual Studio ein Tool zur Verfügung, das eine kontrollierte Umgebung zur Durchführung verteilter Verarbeitungsaufgaben bereitstellt. Die mit diesem Tool erzeugten Messwerte dienen jedoch nicht als allgemeine Benchmarks. Vielmehr dienen sie nur als relative Vergleichswerte zur Be-
622
Der Qualitätskontrollprozess
wertung der Leistung eines Computers. Anhand eines Vergleichs dieses Tests lässt sich dann ermitteln, welche Hardwarekomponente am kostengünstigsten ist. Der Application Performance Explorer Visual Studio Enterprise Edition enthält unter dem Namen APE ein nützliches und einfach zu handhabendes Dienstprogramm. Dieses in Abbildung 19.6 gezeigte Programm ermöglicht es, eine kontrollierte Umgebung für Hardware-Leistungstests bereitzustellen.
Abbildung 19.6 APE-Fenster
APE ist eine Dialoganwendung. Die Abbildung des APE-Programmfensters in Abbildung 19.6 zeigt einen Client, der mittels COM mit einem Element der mittleren Schicht kommuniziert. Dieses wiederum kommuniziert über Active Data Objects (ADO) mit Datendiensten. Dies ist ein Beispiel für eine verteilte Windows DNA-Anwendung. Aus der Abbildung geht hervor, dass wir das Szenario testen sollten, in dem fünf Arbeitstasks auf einem Clientcomputer ausgeführt werden. Der Client ruft einen einzigen Server auf, der eine maximale Anzahl von drei Arbeitsobjekten zur Bearbeitung der Clientanforderungen besitzt. Die Serveraktion der mittleren Schicht basiert auf einer Auswahl mit Hilfe eines einfachen Read Select auf eine Jet-Datenbank (Microsoft Access).
Leistungstests
623
Bei diesem APE-Szenario handelt es sich um ein definiertes und kontrolliertes Szenario. Dieses lässt sich auf anderer Hardware ausführen, für die eine Leistungsmessung durchgeführt werden kann. Durch Vergleichen der Benchmarkergebnisse verschiedener Hardware kann die Leistung der beiden Computer quantifiziert und in einer Rentabilitätsberechnung verwendet werden. Vergewissern Sie sich, dass auf der zu testenden Hardware keine anderen Anwendungen ausgeführt werden, und klicken Sie dann auf Start. Nach kurzer Zeit wird im Feld unterhalb des Diagramms eine Ausgabe ähnlich der folgenden eingeblendet: Click graphical button images to configure APE test scenarios. All requested clients were successfully created. All requested workers were successfully created. Test Started. Collecting and writing log records ... Total Calls = 500 Elapsed Time = 11.65 seconds (47.85 seconds in clients) Client Calls Per Second = 8.94 Overall Calls Per Second = 42.48
Die Ausgabe zeigt an, dass das beteiligte Szenario 500 Aufrufe (5 Clients mit jeweils 100 Aufrufen) durchführt. Für die Verarbeitung aller 500 Aufrufe wurde insgesamt eine Verarbeitungszeit von 11,65 Sekunden benötigt. Die 5 Clients benötigten zusammen 47,85 Sekunden, um die 500 Aufrufe zu verarbeiten. Jeder Client konnte nur 8,94 Aufrufe pro Sekunde verarbeiten. Dagegen konnte der Server 42,48 Aufrufe pro Sekunde verarbeiten. Wie können Sie diese Informationen verwenden, um die Leistung zu optimieren und mögliche Engpässe festzustellen? Die insgesamt benötigte Zeit hängt von der Geschwindigkeit der mittleren Schicht und des Datenbankservers ab. Sind beide Computer sehr schnell, hängt die gesamte verstrichene Zeit von den Netzwerkverbindungen zwischen ihnen ab. Eine Möglichkeit zur Reduzierung der Gesamtzeit besteht im Einsatz eines mit mehreren Prozessoren ausgestatteten Computers. Gesteigert werden sollen sowohl die Clientaufrufe pro Sekunde als auch die Gesamtzahl der Aufrufe pro Sekunde. Das ideale Verhältnis ist erreicht, wenn die Anzahl der Clientaufrufe pro Sekunde multipliziert mit der Anzahl der Kunden mit der Gesamtzahl der Aufrufe pro Sekunde übereinstimmt. In unserem Fall ist die Gesamtzahl der Aufrufe pro Sekunde geringer als das Produkt der Multiplikation. Dies bedeutet, dass der Client eine bestimmte Zeit warten muss. Die Wartezeit wird nicht von der Datenbank oder der Middleware verursacht, sondern vom Netzwerk, das zur Kommunikation zwischen den verschiedenen Schichten verwendet wird.
624
Der Qualitätskontrollprozess
Verwenden von APE Um die von APE durchgeführte Simulation zu verstehen, müssen Sie sich zunächst die verschiedenen Bereitstellungsmodelle klar machen, die von APE unterstützt werden. APE kann drei verschiedene remote Bereitstellungsmodelle simulieren: 왘 Synchrone Kommunikation 왘 Asynchrone Kommunikation 왘 Kommunikation von Warteschlangenobjekten mit Rückrufen
In Abbildung 19.7 wird ein Workflowmodell dargestellt, das die Architektur von APE-Simulationen veranschaulicht.
Abbildung 19.7 APE-Workflow
Im synchronen Kommunikationsmodus wartet der Client, bis der Aufruf abgeschlossen ist. Er kann erst fortfahren, nachdem er die Antwort vom Server erhalten hat. In Abbildung 19.7 erstellt der Client ein Worker-Objekt mit Hilfe des Job-Managers. Das Worker-Objekt erstellt seinerseits einen Dienst, der den Auftrag durchführt und den Wert an das Protokollierungsmodul zurückgibt. Nachdem die Aufgabe abgeschlossen ist, werden das Worker-Objekt und der Dienst zerstört. Diese an sich sehr ineffiziente Architektur ermöglicht jedoch eine sinnvolle Simulation, um den Durchsatz eines Netzwerks zu ermitteln. Im asynchronen Kommunikationsmodus werden die Worker-Objekte nach Verwendung nicht zerstört, sondern können erneut eingesetzt werden. Damit lässt sich die Zeit einsparen, die sonst für ihre Erstellung benötigt wird. In diesem Modell aktiviert der Client mit Hilfe des Pool-Managers ein freies Worker-Objekt. Dieser erstellt dann einen Dienst, der die Aufgabe ausführt. Im asynchronen Kommunikationsmodell gibt es keine Rückgabewerte. Im Warteschlangenbetrieb würde der Client den Job-Manager durchlaufen und dieser würde dann mit dem Pool-Manager kommunizieren, um ein freies Worker-Objekt zu aktivieren. Da-
Leistungstests
625
durch wird die Anzahl der Worker-Objekte in Grenzen gehalten. Clients werden so lange in die Warteschlange gestellt, bis ein Worker-Objekt aus dem Pool verfügbar ist. Bei der Objektkommunikation mit Rückrufen wird im Warteschlangenbetrieb ein Rückgabewert mit Hilfe von Rückrufen an den Client weitergeleitet. Durch die Verwendung von Rückrufen wird der Client wieder für andere Aufgaben frei, während er auf einen Rückgabewert vom Server warten muss. Im synchronen Kommunikationsmodus wäre er dagegen in dieser Zeit blockiert. Auswählen eines Profils Der erste Schritt bei der Arbeit mit APE besteht darin, ein Profil auszuwählen. Das in Abbildung 19.6 gezeigte Kombinationsfeld enthält eine Liste der vordefinierten Profile. Wenn Sie eine Simulation starten, erstellt der Client einen Dienst. Wie dies im Einzelnen geschieht, hängt vom ausgewählten Profil ab. Wenn Sie beispielsweise den synchronen Modus ausgewählt haben, dann erstellt der Client den Dienst direkt. Beim asynchronen Modus fordert der Client dagegen den Job-Manager auf, den Dienst zu erstellen. Beim Pooling wendet sich der Client an den Pool-Manager. In diesem Fall wird bei der Simulation ein Objekt aus dem Pool zur Durchführung einer Aufgabe verwendet. Alle Parameter der vordefinierten Profile lassen sich über Dialogfelder konfigurieren. Die vordefinierten Profile sollen Ihnen lediglich den Einstieg erleichtern. Wenn Sie mit der Funktionsweise von APE vertraut sind, können Sie selbst ein maßgeschneidertes Profil für Ihr System definieren. In unserem Beispiel entscheiden wir uns für das Profil »Typical performance, asynchronous (DB, Job)« als Ausgangspunkt. Dieses Profil passen wir durch entsprechende Änderungen der einzelnen Parameter an unsere Anforderungen an und speichern es dann unter einem anderen Namen. Clientoptionen Das Dialogfeld Client Options lässt sich auf zwei Arten öffnen: einmal durch Klicken auf die Schaltfläche Client (die Schaltfläche mit der Unterschrift »1 Client Machine, 5 Client«) oder über den Menübefehl View/Client Options…. Dieses Dialogfeld ist in mehrere Registerkarten gegliedert. Auf der ersten Registerkarte General können Sie festlegen, wie lange der Test dauern und welche Wartezeit zwischen Aufrufen verwendet werden soll. Als Verzögerung zwischen den Aufrufen übernehmen wir die Standardeinstellung von 0 ms. Für die Testdauer gibt es mehrere Möglichkeiten: Sie kann offen gelassen werden (in diesem Fall müssen Sie zum Beenden des Tests auf die Schaltfläche Stop klicken). Sie können auch festlegen, wie viele Aufrufe der Test bearbeiten soll. Schließlich ist es auch möglich, eine feste Zeitdauer für den Test vorzugeben.
626
Der Qualitätskontrollprozess
Je länger der Test ausgeführt wird, desto genauer sind seine Ergebnisse. In unserem Beispiel legen wir fest, dass sich der Test über 50 Aufrufe erstrecken soll. Auf der zweiten Registerkarte Concurrency können Sie festlegen, welche Computer als Clients fungieren und wie viele Prozesse auf jedem Computer ausgeführt werden sollen. Standardmäßig wird der lokale Computer als Client verwendet. Sie können jedoch auch Remotecomputer hinzufügen, wenn Sie die Leistung mit Hilfe von Prozessen von verschiedenen Computern testen möchten. Dies empfiehlt sich bei verteilten Anwendungen, weil auch das Netzwerk in den Test einbezogen wird. In unserem Beispiel lassen wir die Anzahl der Prozesse pro Computer auf der vorgegebenen Einstellung 5. Wir verwenden zwei Clientcomputer, die insgesamt zehn Prozesse erstellen. Durch Aktivieren des Kontrollkästchens Use Remote Client Machines wird die Schaltfläche Configure… verfügbar. Klicken Sie auf diese Schaltfläche, um das in Abbildung 19.8 gezeigte Dialogfeld Configure Remote Client Machines zu öffnen.
Abbildung 19.8 APE-Remoteclient
Bevor Sie die Clientcomputer für den Test auswählen können, müssen Sie sicherstellen, dass diese richtig konfiguriert sind: Auf allen Remoteclientcomputern muss Windows NT 4 oder Windows 2000 ausgeführt werden. Die Sicherheitseinstellungen der Computer müssen die Remoteerstellung von Prozessen zulassen. Sie können diese Einstellung mit Hilfe des RA-VerbindungsManagers anpassen. Hierzu klicken Sie auf Start, wählen Ausführen und geben dann Racmgr32.exe ein. Klicken Sie danach auf OK. Setzen Sie auf der Register-
Leistungstests
627
karte Client Access des RA-Verbindungs-Managers die Systemsicherheitsrichtlinie auf Allow All Remote Creates, und schließen Sie danach das Dialogfeld mit Hilfe des Menüs Datei/Beenden. Starten Sie den Automatisierungs-Manager. Den entsprechenden Befehl finden Sie im Menü von Visual Studio unter den Enterprise-Tools. Sie müssen dies auf allen Computern tun, die im APE-Test remote verwendet werden sollen. Nach der Konfiguration der Clientcomputer können Sie diese in den Test aufnehmen. Im Beispiel habe ich zwei Remoteclients und keinen lokalen Computer verwendet. (Sie können dies einstellen, indem Sie beim Hinzufügen von Remotecomputern im Dialogfeld Configure Remote Client Machine das Kontrollkästchen Use Client Manager Machine as Client Machine deaktivieren.) Damit können Sie angeben, welche Art und welche Mengen von Daten vom Client an die Serverdienste gesendet werden sollen. Diese Einstellung wird auf der Registerkarte Send Data vorgenommen. Der Datentyp sollte immer auf Variantarray eingestellt sein, weil die Übergabe einer Variantauflistung wegen des hohen Aufwands keine plausible Option darstellt. Danach können Sie die Größe der Daten festlegen – für diesen Test habe ich die Einstellungen so gewählt, dass 20+/–5 Byte/Zeile und 6+/0 Zeilen an Daten übergeben werden. Die Registerkarte Return Data ermöglicht dieselben Einstellungen wie die Registerkarte Send Data, wobei diese jedoch für die Daten gelten, die von den Serverdiensten zum Client übergeben werden. Hier habe ich dieselben Werte wie für die Einstellung Send Data gewählt. Zum Schluss müssen Sie noch auf der Registerkarte Callback die Rückrufoptionen festlegen. Diese Registerkarte ist nur von Bedeutung, wenn Sie den Job-Manager verwenden. In unserem Beispiel wird der Job-Manager eingesetzt, weil wir die asynchrone Übertragungsmethode ausgewählt haben. Deshalb muss der Rückruftyp definiert werden. Hierzu stehen drei Optionen zur Verfügung: 왘 Der Client übergibt das Rückrufobjekt nur einmal. 왘 Der Client übergibt ein Rückrufobjekt für jede Dienstanforderung. 왘 Es wird kein Rückrufobjekt zurückgegeben, sondern die Ergebnisse werden
durch Aktivieren von Ereignissen auf dem Client an diesen zurückgegeben. Im Beispiel wollen wir die erste Option verwenden. Damit ist die Definition der Clientoptionen abgeschlossen, und das Profil kann mit dem Menübefehl File/Save Profile As… gespeichert werden. Geben Sie dem neuen Profil einen sinnvollen Namen, damit Sie es in Zukunft wieder finden,
628
Der Qualitätskontrollprozess
wenn Sie den Leistungstest wiederholen möchten. Um die beste Architektur für Ihre Anwendung zu ermitteln, führen Sie normalerweise mehrere Tests durch. Dienstverbindungsoptionen Wenn Sie den Remoteserver steuern möchten, müssen Sie die Dienstoptionen festlegen. Hierzu wählen Sie den Menübefehl View/Service Connection Options…, um das Dialogfeld Application Service Connection Options zu öffnen. Sie können auch auf die Pfeilschaltfläche doppelklicken, die den Client- mit dem Servercomputer verbindet. In unserem Beispieltest übernehmen wir alle Standardeinstellungen. Die verschiedenen Optionen werden hier dennoch beschrieben, damit Sie sie in Ihren eigenen Tests verwenden können. Der Verbindungstyp ermöglicht es festzulegen, welche Art von Verbindung zu Remoteclientcomputern und/oder dem Remoteservercomputer verwendet wird. Zur Auswahl stehen die Optionen Distributed COM und Remote Automation. Wir haben unsere Clientcomputer bereits für die Verwendung von Remote Automation konfiguriert, sodass diese Einstellung ausgewählt werden muss. Das Dialogfeld ermöglicht es auch, den Standort des Servers anzugeben, der für den Test verwendet werden soll. Wenn Sie einen Remoteserver auswählen, muss dieser so konfiguriert sein, dass die remote Erstellung von Prozessen möglich ist. Ferner muss auf dem Server der Automatisierungs-Manager wie oben beschrieben für die Verwendung von Remoteclients ausgeführt werden. Wenn Sie als Verbindungstyp die Option Distributed COM ausgewählt haben und einen Remoteserver verwenden möchten, müssen Sie den Servercomputer konfigurieren. Hierzu ist eine Reihe von Schritten erforderlich, die jedoch ausführlich in der Hilfedatei der MSDN-Bibliothek beschrieben werden, weshalb ich sie hier nicht noch einmal ausführen möchte. Suchen Sie im Abschnitt »Installing Application Performance Explorer« des Kapitels »Application Performance Explorer Common Tasks« nach der Aufgabe »Configuring DCOM Access Permissions Using DCOMCNFG«. Anhand dieser Anleitungen können Sie den Server so konfigurieren, dass Sie den Verbindungstyp Distributed COM verwenden können. Dienstoptionen Um das Dialogfeld der Dienstoptionen zu öffnen, können Sie entweder den Menübefehl View/Service Options… verwenden oder auf die Schaltfläche oberhalb der Beschreibung des Servercomputers klicken. Im Dialogfeld können Sie festlegen, welche Arten von Aufgaben der Server während des Tests ausführen soll und wie diese Aufgaben verarbeitet werden sollen. Die Registerkarte DB Task ermöglicht es, die Datenbankbelastung festzulegen. Es sind zwei Arten von Aufgaben verfügbar, verteilte MTS-Transaktionen (Microsoft Transaction Server) und Datenbankabfrage. Die Option für verteilte MTS-Transaktionen ist nur verfügbar, wenn in der Datenbankoption eine SQL Server-Daten-
Leistungstests
629
bank ausgewählt wurde (dies wird weiter unten im Abschnitt »Datenbankoptionen« beschrieben). In unserem Beispiel verwenden wir eine Datenbankabfrage. Für Testzwecke sind vier verschiedene Abfragen verfügbar. Wenn Sie sie auswählen, können Sie den von ihnen ausgeführten SQL-Code sehen, aber keine Änderungen daran vornehmen. Für unser Beispiel verwenden wir die Abfrage »Simple Transaction«. Auf der Registerkarte CPU Task können die Details zum Simulieren von CPU-basierten Belastungen auf dem Servercomputer festgelegt werden. Hier kann sowohl die Dauer als auch die Ruhezeit für die betreffende Aufgabe angegeben werden. Die Dauer legt fest, wie lange die Aufgabe ausgeführt wird. Diese Einstellung ist nicht mit der Dauer der CPU-Task identisch, weil es bei der Ausführung einer einzelnen Aufgabe abhängig von der CPU-Verfügbarkeit zu Unterbrechungspausen kommen kann. In unserem Beispieltest messen wir nur die Leistung der Datenbankabfrage, deshalb wird das Kontrollkästchen Simulate CPU Based Task nicht aktiviert. Jetzt können Sie auf der Registerkarte Pooling noch die Poolingoptionen festlegen. Durch die Verwendung bereits im Pool befindlicher Ressourcen kann der Client direkt auf einen Workerpool zugreifen, statt jedes Mal ein neues Worker-Objekt erstellen zu müssen. Die Worker-Objekte werden automatisch verfügbar, sobald sie frei sind, und werden nach der Benutzung von APE wiederverwendet. Die Auswahl zur Verwendung des Job-Managers bedeutet, dass für Aufgaben sowohl der Pool- als auch der Warteschlangenbetrieb verwendet wird. Bei der Auswahl des Pool-Managers wird dagegen nur das Jobpooling verwendet. In beiden Fällen kann festgelegt werden, wie viele Worker-Objekte sich im Pool befinden. Für den Beispieltest verwenden wir sowohl Jobpooling als auch Jobqueueing mit einem Pool von drei Worker-Objekten pro Computer. In diesem Dialogfeld kann auch festgelegt werden, ob Worker-Objekte auf Remotecomputern ausgeführt werden sollen. In diesem Fall muss DCOM auf dem betreffenden Servercomputer so konfiguriert werden, wie dies im Kapitel »Application Performance Explorer Common Tasks« der Hilfedatei im Abschnitt »Installing Application Performance Explorer« unter der Aufgabe »Configuring DCOM Access Permissions Using DCOMCNFG« beschrieben ist. Zum Schluss können Sie auf der Registerkarte Binding angeben, welche Bindung für Dienstobjekte verwendet wird. Die frühe Bindung von COM bietet zwar definitiv die bessere Leistung. Wie hoch dieser Gewinn ausfällt, lässt sich jedoch nur im direkten Vergleich der beiden Bindungsarten feststellen. Aus Entwurfsgründen wird häufig die späte Bindung verwendet, was jedoch immer mit Leistungseinbußen verbunden ist. Mit APE können Sie den Leistungsunterschied messen und
630
Der Qualitätskontrollprozess
entscheiden, ob er in der jeweiligen Anwendung vertretbar ist. Durch Aktivieren des Kontrollkästchens Retain Service References können Sie festlegen, dass Dienste nur einmal erstellt werden und eine Verknüpfung darauf erhalten bleibt. Das Aktivieren des Kontrollkästchens Preload Services bewirkt, dass die Erstellung der Dienstobjekte nicht in den Leistungstest einbezogen wird, wenn das Kontrollkästchen Retain Service References aktiviert ist. Für den Beispieltext werden alle drei Kontrollkästchen aktiviert. Jetzt sollten Sie das Profil durch Klicken auf das Diskettensymbol speichern, das sich neben dem Dropdown-Listenfeld der Profilnamen befindet. Ein kleines Sternsymbol links vom Profilnamen im Kombinationsfeld Profile zeigt an, dass das Profil geändert wurde. Datenbankverbindung Als nächste Einstellung müssen Sie die Methode für den Datenbankzugriff festlegen. Zum Öffnen des Dialogfelds Database Connection Options klicken Sie entweder auf den Doppelpfeil, der Server und Datenbank verbindet, oder wählen Sie den Menübefehl View/Database Connection Options… aus. Der einzige Parameter, der eingestellt werden muss, ist die Zugriffsmethode. Hier haben Sie die Auswahl zwischen ADO, RDO (Remote Data Objects), DAO (Data Access Objects) und ODBC (Open Database Connectivity) API (Application Programming Interface). Probieren Sie verschiedene Einstellungen aus, und vergleichen Sie ihre Leistung. Beachten Sie jedoch, dass ADO jetzt die von Microsoft bevorzugte Datenbankzugriffsmethode ist. DAO wird nur noch aus Gründen der Abwärtskompatibilität unterstützt. Für die Auswahl einer Zugriffsmethode sind, ebenso wie bei anderen Architekturaspekten auch, nicht nur Leistungsgesichtspunkte entscheidend. Für unseren Test wählen wir die Zugriffsmethode ADO aus. Datenbankoptionen Zum Schluss können Sie im Dialogfeld Database Options noch auswählen, welche Datenbank verwendet werden soll. Um dieses Dialogfeld zu öffnen, können Sie entweder auf die Schaltfläche oberhalb der Datenbankbeschreibung klicken oder den Menübefehl View/Database Options… auswählen. Zwei Datenbanken werden unterstützt: Microsoft Access (Jet) oder SQL Server. Wenn Sie erstmalig Microsoft SQL Server als Datenbank auswählen, wird der Assistent zur Datenbankkonfiguration von APE geöffnet, der Sie durch die Auswahl der SQL Server-Datenbank führt. Der Assistent bietet als erste Auswahl an, entweder die Verbindung mit einer vorhandenen APE SQL Server-Datenbank herzustellen oder eine neue Datenbank zu
Leistungstests
631
installieren. Wenn Sie die Datenbank APETEST noch nicht installiert haben, wählen Sie die Option zur Installation einer neuen Datenbank. Der Assistent führt Sie dann durch den Prozess. Die Datenbank APETEST ist für die Durchführung des Tests zwingend erforderlich, weil sich die Abfragen darauf beziehen. Wenn Sie eine andere Datenbank verwenden möchten, können Sie ihre eigenen Abfragen für den APE-Quellcode erstellen, der sich am folgenden Standort befindet: ..\Programme\Microsoft Visual Studio\Common\Tools\Ape\Source\
Die Verfügbarkeit des Quellcodes bietet Ihnen uneingeschränkte Flexibilität, um APE an die Anforderungen Ihres Projekts anzupassen. Der Assistent zur Datenbankkonfiguration von APE erstellt eine ODBC-Benutzerdatenquelle auf dem Computer mit dem Namen APEDB_SQLServer, der auf die angegebene Datenbank verweist. Wenn Sie danach die Funktion zur Installation einer neuen Datenbank auswählen, wird die Datenbank auf dem Server Ihrer Wahl installiert. Wenn Sie den Assistenten zur Datenbankkonfiguration von APE später erneut ausführen möchten, müssen Sie den Benutzer-DSN löschen. Soll dagegen nur auf eine andere Datenbank verwiesen werden, können Sie den DSN selbst ändern. Nachdem die Datenbank APETEST erstellt wurde, können Sie den ersten Leistungstest durchführen.
19.3.2 Testergebnisse Ich habe den weiter oben beschriebenen Test auf zwei verschiedenen Computern ausgeführt. Einmal auf einem Computer mit zwei Prozessoren und das andere Mal auf einem Computer mit nur einem Prozessor. Jetzt wollen wir feststellen, welcher der beiden Computer das beste Preis-Leistungs-Verhältnis bietet. Die Ergebnisse der beiden Tests sind in der folgenden Tabelle zusammengestellt. (Die Ergebnisse sind hardwarespezifisch, deshalb erhalten Sie auf Ihrem Computer wahrscheinlich andere Werte.)
632
Computer mit zwei CPUs
Computer mit einer CPU
Total Calls=500
Total Calls=500
Elapsed Time=20.88 seconds (152.63 seconds in clients)
Elapsed Time=38.24 seconds (266.86 seconds in clients)
Client Calls Per Second=3.28
Client Calls Per Second=1.87
Overall Calls Per Second=23.95
Overall Calls Per Second=13.08
Der Qualitätskontrollprozess
Aus den Ergebnissen geht eindeutig hervor, dass der Dualprozessorcomputer für dieses Testszenario eine bessere Leistung bietet als der Computer mit nur einem Prozessor. Er ist tatsächlich fast doppelt so schnell: Während der Dualprozessorcomputer durchschnittlich 24 Aufrufe pro Sekunde ausführt, sind es beim Einzelprozessormodell nur 13 Aufrufe. Jetzt wollen wir eine einfache Rentabilitätsberechnung durchführen. Dabei gehen wir davon aus, dass der Einprozessorcomputer rund 2.000 DM kostet und der Preis für den mit zwei CPUs ausgestatteten Computer rund 6.000 DM beträgt. Theoretisch kann ich zwei Einprozessorcomputer kaufen und erhalte damit die Leistung des Dualprozessorgeräts. Ob dies jedoch tatsächlich möglich ist, hängt von der Lastenausgleichsfunktionalität ab. Dies ist wieder ein ganz anderes Szenario, das getestet werden muss. Anhand dieses Tests könnte dann eine Berechnung für den Rückfluss der Investitionen (ROI) durchgeführt werden.
19.3.3 Testen der Anwendung mit Visual Studio Analyzer Visual Studio Analyzer ist ein Microsoft Tool, das Ereignisse einer verteilten Anwendung während deren Betrieb nachverfolgen kann. Visual Studio Analyzer vermittelt eine allgemeine Sicht auf den Datenverkehr zwischen den verschiedenen Komponenten, aus denen Ihre Anwendung besteht, selbst wenn sie auf verschiedenen Computern ausgeführt werden. Damit können Sie die Interaktion zwischen den verschiedenen Bausteinen Ihrer Anwendung verfolgen, ohne sich detailliert damit befassen zu müssen, wie diese Bausteine konstruiert sind. Im Gegensatz zu APE misst Visual Studio Analyzer (VSA) die Leistung der Anwendung nicht in einer Simulation, sondern in einer realen Umgebung. VSA erfasst jedoch Ereignisse nur von modernen Technologien, die dafür konzipiert sind, Informationen an Visual Studio Analyzer zu übergeben. Dazu gehören COM, ADO und COM+. Sie können Ihre Anwendung auch entsprechend anpassen, um Informationen an Visual Studio Analyzer zu übergeben, dies ist jedoch nicht Gegenstand des vorliegenden Buches. Weitere Informationen finden Sie im Abschnitt über VSA der MSDN-Bibliothek. Verwenden von VSA Um mit Visual Studio Analyzer arbeiten zu können, benötigen Sie eine Anwendung, die überwacht werden soll. Wir verwenden dazu den Prozess zum Hinzufügen neuer Benutzer zu der Datenbank in der Konferenzanmeldungsanwendung, für den wir bereits unsere Testfälle erstellt haben.
Leistungstests
633
Jetzt müssen Sie noch in Visual Studio Analyzer ein Projekt für diese Anwendung erstellen. Das Aufzeichnen von Ereignissen aus einer Anwendung umfasst eine Anzahl von Schritten: 왘 Erstellen oder öffnen Sie ein VSA-Projekt. 왘 Stellen Sie die Verbindung zu den Computern her, die im Projekt aufgezeichnet
werden sollen. 왘 Erstellen oder öffnen Sie einen Sammlungsfilter, und kennzeichnen Sie diesen
als Aufzeichnungsfilter. 왘 Erstellen Sie ein neues Ereignisprotokoll, und starten Sie die Aufzeichnung. In
einem Ereignisprotokoll können Ereignisse nur einmal aufgezeichnet werden. 왘 Wenn Sie die Anwendung starten, beginnt VSA automatisch mit der Aufzeich-
nung der Ereignisse. Microsoft bietet einen Assistenten, der Sie durch die meisten dieser Schritte führt. Um ein neues VSA-Objekt mit dem Assistenten zu erstellen, öffnen Sie die Visual Studio-Entwicklungsumgebung und wählen den Menübefehl File/New Project aus. Dadurch wird ein Dialogfeld geöffnet, in dem Sie verschiedene Arten von Visual Studio-Projekten erstellen können. Wählen Sie den Typ Visual Studio Analyzer Project, und klicken Sie danach auf das Symbol des Analyzer-Assistenten auf der rechten Seite des Dialogfeldes. Sie brauchen lediglich einen Namen und einen Pfad für den Speicherort anzugeben. Danach können Sie Ihr erstes VSA-Projekt erstellen. Der VSA-Assistent führt Sie durch die verschiedenen Schritte, die zum Erstellen des Projekts erforderlich sind. Als erste Option können Sie auswählen, ob der Assistent das Netzwerk durchsuchen soll, um eine Liste der Computer zu erstellen. Sie können diese Option versuchen, in meinem Fall enthielt die Liste jedoch nur meinen lokalen Computer. Die anderen Computer, die ich in den Test aufnehmen wollte, musste ich manuell hinzufügen. Als Nächstes werden Sie nach den Komponenten gefragt, deren Ereignisse auf jedem der zur Überwachung ausgewählten Computer aufgezeichnet werden sollen. Zum Abschluss können Sie aus einer Liste die gewünschten Filter auswählen, die Sie in Ihre Anwendung aufnehmen möchten. Die Filter ermöglichen es, spezifische Teilsystemereignisse zu erfassen. Zunächst ist es sinnvoll, alle Ereignisse aufzuzeichnen. Während der Assistent das Projekt zum Starten der Aufzeichnung konfiguriert, wird eine Liste der auszuführenden Aufgaben angezeigt. Nachdem der Assistent seine Aufgaben abgeschlossen hat, beginnt die Aufzeichnung direkt. Wenn Sie die Aufzeichnung anhalten, können Sie nicht mehr in dasselbe Ereignisprotokoll
634
Der Qualitätskontrollprozess
schreiben, sondern müssen ein neues Protokoll erstellen. Deshalb sollten Sie die Aufzeichnung nicht beenden, sondern lediglich unterbrechen. Um sich die Aufzeichnung anzeigen zu lassen, doppelklicken Sie auf das aktuelle Ereignisprotokoll. Dadurch wird die Ereignislistenansicht geöffnet, in der Sie direkt sehen können, welche Ereignisse gerade erfasst werden. Starten Sie Ihre Anwendung, um mit der Aufzeichnung der Ereignisse zu beginnen. Im Ereignisprotokoll werden nur die Ereignisse erfasst und angezeigt, die im Aufzeichnungsfilter festgelegt wurden. Abbildung 19.9 enthält ein Beispiel.
Abbildung 19.9 Beispiel für ein VSA-Ereignis
In der Ereignisliste werden alle Ereignisse, die während der Ausführung der Anwendung erfasst wurden, nacheinander aufgezeichnet. Diese Ansicht kann sehr umfangreich sein, lässt sich jedoch anpassen. Es steht zwar nur ein Filter zur Verfügung, um die Ereignisse festzulegen, die im Ereignisprotokoll aufgezeichnet werden sollen. Sie können jedoch verschiedene Filter für die Ansichten des Ereignisprotokolls anwenden und damit die angezeigten Daten eingrenzen. Um einen Filter auf eine Ansicht anzuwenden, klicken Sie mit der rechten Maustaste auf den Filter und wählen dann Apply Filter to View aus dem Einblendmenü. Wenn Sie wissen möchten, welche Ereignisse ein Filter abdeckt, doppelklicken Sie einfach auf den Filter, um das Dialogfeld Edit Filter zu öffnen. Darin können Sie sehen, welche Ereignisse aufgezeichnet werden, und den Filter bei Bedarf anpassen.
Leistungstests
635
Die sinnvollste Ansicht zur Leistungsprüfung einer Anwendung ist die in Abbildung 19.10 dargestellte Diagrammansicht. Diese Ansicht erhalten Sie, indem Sie mit der rechten Maustaste auf das Ereignisprotokoll klicken und Chart aus dem Kontextmenü auswählen.
Abbildung 19.10 Darstellung der Anwendungsleistung in VSA
Anhand dieses Diagramms können Sie einfach diejenigen Aufgaben ermitteln, deren Ausführung längere Zeit in Anspruch nimmt. Um eine detaillierte Ansicht dieser Ereignisse zu erhalten, doppelklicken Sie einfach auf den gewünschten Bereich des Diagramms. Dadurch wird eine Ansicht geöffnet, die eine detaillierte Beschreibung des Ereignisses enthält. Auf diese Weise können Sie feststellen, welche Teile Ihrer Anwendung länger zur Ausführung benötigen. Im Fall unserer Beispielanwendung ist die Leistung ziemlich gleichmäßig. Das Einzige, was auffällt, ist die Tatsache, dass die Ausführung einiger JScript-Ereignisse (Microsoft-Version von JavaScript), die im Browser ausgeführt werden, manchmal länger dauert. Um die Analyse weiter zu unterstützen, können Sie ein Balkendiagramm der vom Windows NT-Systemmonitor aufgezeichneten Daten anzeigen. Hierzu klicken Sie mit der rechten Maustaste auf das Diagramm und wählen dann Add Graph aus dem Kontextmenü. Sie können nur Daten anzeigen, die mit dem Aufzeichnungsfilter erfasst wurden. Die vom Windows NT-Systemmonitor aufgezeichneten Daten werden in der Kategorie All Measured Events, Teilkategorie PerfMon ange-
636
Der Qualitätskontrollprozess
zeigt. Anhand dieser Daten können Sie diejenigen Bereiche eingrenzen, bei denen die mangelhafte Leistung nicht auf eine interne Anwendungsaufgabe oder Einstellung zurückzuführen ist, sondern durch den Computer bedingt ist. Ein Hinweis zu VSA Ich habe VSA nur kurz gestreift, weil dieses Tool in meinen Augen noch nicht fertig ist. VSA ermöglicht zwar die Aufzeichnung von Ereignissen. Festzulegen, welche Ereignisse aufgezeichnet werden, ist jedoch noch schwieriger als bei Verwendung einer UNIX-Befehlszeile. Die Beschreibungen sind sehr knapp, und für Personen, die sich mit der Anwendung nicht auskennen, ist es schwierig, eine Leistungsanalyse durchzuführen. Es kann dennoch interessant sein, Ihre benutzerdefinierten Ereignisse zu VSA hinzuzufügen und diese dann in der Anwendungsarchitektur aufzuzeichnen. Auf dieses Weise ist es möglich, die Anwendungsleistung zu optimieren und Problembereiche auszumachen. Wenn Sie VSA auf diese Weise einsetzen, erstellen Sie eine Art von »Superprofiler«.
19.4 Resümee Die Sicherstellung der Qualitätskontrolle ist nicht einfach. Manche Menschen glauben, ein Problem dadurch lösen zu können, indem sie mehr Ressourcen darauf verwenden. Dies funktioniert jedoch nicht. Andere bringen mehr Komplexität ins Spiel. Meiner Erfahrung nach lässt sich Qualität am besten erreichen, wenn alles so einfach wie möglich gehalten wird. Dies gilt auch für den Entwurfs- und Implementierungsprozess. Ich habe verschiedene Anmerkungen zum hier abgedeckten Themenbereich erhalten. Einige Programmierer sagen, dass die Fehlerdiagramme nicht so einfach sein können, wie sie hier scheinen. Tatsache ist aber, dass sie tatsächlich so einfach sind. Die einzige Schwierigkeit besteht darin, die Einpassung der Kurve vorzunehmen. Die Kurveneinpassung folgt nicht nur rein mathematischen Gesetzen, sondern erfordert auch ein gewisses Maß an Erfahrung. So hilft Ihnen die Mathematik nichts, wenn Sie feststellen müssen, dass einer der Manager inkompetent ist. Ein weiterer Kommentar lautet, dass mehr Testfälle benötigt würden. Aber auch dies stimmt nicht. In diesem Fall müssen wir uns auf die Mathematik verlassen, die sagt, dass nur eine bestimmte Anzahl von Testfällen erforderlich ist.
Resümee
637
Wenn Sie immer noch Zweifel haben, nehmen Sie einmal ein Buch über statistische Qualitätskontrolle zur Hand. Viele der darin enthaltenen Konzepte sind zwar nicht direkt anwendbar, können jedoch zum mathematischen Verständnis von Fehlern herangezogen werden.
638
Der Qualitätskontrollprozess
20 Erstellen von Diensten In den vorherigen Kapiteln wurden viele verschiedene Aspekte der Anwendungsentwicklung beleuchtet; es bleibt jedoch noch ein Thema, das bisher nicht besprochen wurde. Wie werden Aufgaben ausgeführt, deren Steuerung nicht über den Benutzer erfolgt? In Kapitel 12 wurde beispielsweise das Messaging erläutert. Hierbei wurde vorausgesetzt, dass die Nachricht an der Gegenstelle automatisch gelesen und ausgeführt wird. Wie wird dies jedoch erreicht? Die Antwort lautet: durch einen Dienst. Der Schwerpunkt des vorliegenden Kapitels liegt darauf, diese noch fehlenden Aspekte zum Schreiben verteilter Anwendungen zu ergänzen. Es gibt zwei Arten von Diensten, 24x7-Dienste und periodisch ausgeführte Dienste. Diese periodisch ausgeführten Dienste werden auch Stapelverarbeitungsprozesse genannt.
20.1 Ausführen eines 24x7-Prozesses Der Hauptgrund für die Verwendung von 24×7-Prozesse stellen Systemüberwachung und Bereitstellung von Feedback zur Überwachung dar. In Kapitel 12 wurde dieser Prozess im Rahmen des Messaging besprochen, da dieser Prozesstyp dem Abrufen von MSMQ-Nachrichten (Microsoft Message Queue) aus einer MSMQ-Warteschlange dient. Anschließend wird die MSMQ-Nachricht verarbeitet, und eine weitere Nachricht kann erzeugt und versendet werden. Sie können einen Dienst auch für einen COM+-Ereignisverleger (Component Object Model) verwenden. Es ist beispielsweise ein COM+-Ereignis vorhanden, mit dem Sie angeben können, dass ein Aktienkurs unter einen bestimmten Wert fällt. Anhand des Dienstes kann der Aktienkurs überwacht werden. Bei einer Änderung wird ein Ereignis an das COM+-Ereignissystem übermittelt. Ein COM-Dienst kann auch eingesetzt werden, wenn über ein COM-Objekt auf COM-Verbindungen basierende Ereignisse offen gelegt werden. Diese Ereignisse können nicht verarbeitet werden, wenn Sie eine typische COM+-Anwendung verwenden. Da es sich bei dem Dienst jedoch um einen persistenten Prozess handelt, kann dieser als Empfänger für die verbindungsbasierten Ereignisse eingesetzt werden. Dieses Verfahren ist effizienter als die ständige Antwortabfrage. Der Punkt ist, dass gelegentlich ein dedizierter Prozess benötigt wird, und unter Windows 2000 kann ein dedizierter Prozess nur durch das Schreiben eines Windows 2000-Dienstes erstellt werden. Bei einem Windows 2000-Dienst handelt es sich um einen anonymen Prozess, der beim Start von Windows 2000 ge-
Ausführen eines 24x7-Prozesses
639
laden wird. Ein Dienst ähnelt einem Konsolenprogramm – es handelt sich um ein ausführbares Programm, für das keine COM-Unterstützung erforderlich ist, mit dem jedoch COM-Objekte und APIs (Application Programming Interfaces, Anwendungsprogrammierschnittstellen) aufgerufen werden können.
20.1.1 Steuern von Diensten Ein Dienst wird über das Verwaltungsprogramm Dienste gesteuert, das für die Wartung der einzelnen Dienste sowie der zugehörigen Datenbanken zuständig ist. Das Programm Dienste ermöglicht über das in Abbildung 20.1 gezeigte Dialogfeld das Starten oder Stoppen von Diensten. In dieser Abbildung handelt es sich bei dem gesteuerten Dienst um den Webserver (W3SVC).
Abbildung 20.1 Dialogfeld mit Diensteigenschaften
Wird der Dienste-Datenbank ein Dienst hinzugefügt, muss der Starttyp angegeben werden. Der Starttyp definiert, wie der Dienst nach dem Starten von Windows 2000 gestartet wird. Sie können zwischen den folgenden Starttypen wählen: 왘 Automatisch: Der Dienst wird beim Computerstart automatisch gestartet. 왘 Manuell: Beim Start des Computers wird der Dienst nicht gestartet, kann je-
doch über Dienste aktiviert werden.
640
Erstellen von Diensten
왘 Deaktiviert: Der Dienst wird nicht beim Computerstart gestartet und kann
auch nicht über Dienste gestartet werden. Je nach gewähltem Starttyp spiegelt der Status des Dienstes den derzeitigen Ausführungsstatus des Dienstes wider. In Abbildung 20.1 lautet der Starttyp Automatisch, daher weist der Status den Wert Gestartet auf. Der Ausführungsstatus kann drei Werte annehmen: 왘 Gestartet: Der Prozess ist aktiv und reagiert auf Ereignisse und Prozessanforde-
rungen. 왘 Beendet: Der Prozess ist nicht aktiv und wird nicht ausgeführt. Der Dienst
wurde beendet. 왘 Angehalten: Der Prozess ist aktiv, akzeptiert momentan jedoch keine neuen
Aufgaben oder Ereignisse. Der zurzeit ausgeführte Vorgang wurde beendet. Dieser Vorgang wird auch als verzögertes Beenden bezeichnet. Dienstdetails Der nächste Schritt besteht im Schreiben eines Windows 2000-Dienstes. Beim Schreiben eines Dienstes besteht der schwierigste Teil darin, die Multithreadingkonzepte zu verstehen. Ein Dienst gleicht einer Konsolenanwendung. Beim Starten einer Konsolenanwendung wird zunächst die main-Funktion aufgerufen. Das gleiche geschieht bei einem Dienst, nur dass der Thread, der die main-Funktion aufruft, als Haupteintragsthread bezeichnet wird. Der Haupteintragsthread ist für das Initialisieren des Dienststatus verantwortlich. Sobald dieser Thread gestartet wird, wird der Status des Dienstes als instabil betrachtet. Das Programm Dienste wartet bis zur Initialisierung des Haupteintragsthreads und definiert den Einsprungpunkt ServiceMain. Die letzte Funktion, die der Haupteintragsthread aufruft, ist StartServiceCtrlDispatcher, durch die der Einsprungpunkt ServiceMain als Eingabe akzeptiert wird. Funktioniert diese Funktion, wird der Aufruf nicht zurückgegeben, bevor der Dienst beendet wurde. Nachdem die Diensteuerung die ServiceMain-Informationen erhalten hat, wird über Dienste ein weiterer Thread gestartet und die Funktion ServiceMain aufgerufen, die mehrere Funktionen erfüllt. Die erste Aufgabe besteht darin, eine Behandlungsroutine für die Dienststeuerung zu registrieren. Das Programm Dienste verfügt über die Fähigkeit, den hier geschriebenen Dienst zu starten, zu stoppen und anzuhalten. Wenn der Dienst gestartet, gestoppt oder angehalten wird, ruft Dienste die Behandlungsroutine zur Dienststeuerung auf. Dieser Aufruf erfolgt für einen Thread, der separat vom ServiceMain-Thread vorliegt, d.h. dieser Aufruf
Ausführen eines 24x7-Prozesses
641
wird zur gleichen Zeit vorgenommen, in der der Dienst Daten verarbeitet. Daher liegt es in der Verantwortung des Dienstes, auf die Behandlungsroutine zur Dienststeuerung zu reagieren. Ein zeitlicher Fehler hierbei führt zu einem Abbruch des Programms Dienste. Beim Start des ServiceMain-Threads befindet sich der Dienst weiterhin in einem nicht stabilen Zustand. Es liegt beim ServiceMain-Thread, den Threadstatus anzugeben, d.h. Informationen dazu bereitzustellen, ob der Dienst gestartet oder gestoppt wurde oder ausgeführt wird.
20.1.2 Entwickeln von Diensten Dienste werden im Allgemeinen mit C++ geschrieben, da C++ über Multithreading und Steuerungselemente verfügt. Es sind Add-Ons und Bibliotheken zum Schreiben von Diensten in anderen Sprachen vorhanden, C++ stellt jedoch die beste Alternative dar. Keine Angst, es muss nicht die gesamte Anwendung in C++ geschrieben werden. Ein Dienst hat die Fähigkeit, COM+-Anwendungen aufzurufen, beispielsweise die Anwendung zur Konferenzanmeldung. Des Weiteren ist ein kostenloses Visual C++-Add-On-Dienstprogramm vorhanden (der CodeGuru NT Service Wizard), der Sie bei der Erstellung von Dienstanwendungen unterstützt, daher brauchen Sie den C-Code nicht selbst zu schreiben. Die generierten Dienstanwendung kümmert sich um die Details der unteren Ebene zur Kommunikation mit dem Programm Dienste. Den von Joerg Konig geschriebenen Assistent finden Sie auf der CodeGuru-Site (www.codeguru.com). Der Assistent befindet sich ebenfalls im Quellcode der Begleit-CD-ROM zu diesem Buch im Verzeichnis util/ntservice. Verwenden des CodeGuru NT Service Wizard Das Dienstprogramm ist als Assistent geschrieben – nach der Installation wird es als Projekt-Assistent eingesetzt. Wenn Sie Visual C++ starten und eine neue Anwendung generieren, wird eine Auswahlliste des Projekt-Assistenten geöffnet, die der in Abbildung 20.2 gezeigten ähnelt.
642
Erstellen von Diensten
Abbildung 20.2 Projektauswahl
Der Name des Projekts kann beliebig gewählt werden. In Abbildung 20.2 lautet der Projektname »Example2Service«. Klicken Sie auf OK, um zur nächsten Assistentenseite zu wechseln, wie dargestellt in Abbildung 20.3.
Abbildung 20.3 Dienstdefinition
Ausführen eines 24x7-Prozesses
643
Auf dieser Assistentenseite erstellen Sie einen NT-Dienst oder eine anonyme Windows 95/98-Anwendung. Die Standardeinstellung sieht die Erstellung eines NT-Dienstes vor, d.h. dass es sich um einen Windows 2000-Dienst handelt. Da die zukünftige Entwicklung von Unternehmensservern Windows 2000 einschließt, ist dies die übliche Auswahl. Der im Textfeld Name of the Service eingegebene Text wird zur Ereignisaufzeichnung in den Ereignisdiensten verwendet. Der im Textfeld Display Name eingegebene Text wird verwendet, wenn der Dienst in der MMC (Microsoft Management Console) zur Dienststeuerung (Dienste) angezeigt wird. Klicken Sie auf Next, um zur nächsten Assistentenseite zu wechseln. Hier werden die verschiedenen Funktionen angezeigt, die der Dienst unterstützt, siehe Abbildung 20.4.
Abbildung 20.4 Unterstützte Dienstfunktionen
Auf dieser Seite werden drei Kontrollkästchen angezeigt, die aktiviert werden können. Diese Kontrollkästchen stehen in Beziehung zu den Nachrichten, die an die Behandlungsroutinenfunktion zur Dienststeuerung gesendet werden können – Stop, Pause/Continue und Shutdown. Wenn Sie keines der Kontrollkästchen aktivieren, betrachtet die Dienststeuerung den Dienst als »feindlich« und bricht diesen umgehend ab. Damit der Dienst als »guten« Dienst erkannt wird, müssen mindestens die Kontrollkästchen Stop und Shutdown aktiviert werden. Die in Abbildung 20.4 gezeigte Assistentenseite lässt die Vermutung zu, dass das Programm Dienste, d.h. die Funktion zur Dienststeuerung, Funktionen zur Nachrichtenfilterung besitzt. Dies ist jedoch nicht richtig. Standardmäßig werden alle
644
Erstellen von Diensten
Nachrichten an die Behandlungsroutine zur Dienststeuerung gesendet und der Dienst-Assistent generiert einen Stub, mit dem die Nachricht entweder implementiert oder übersprungen wird. Dies bedeutet, dass nach dem Generieren des Quellcodes für den Dienst diesem Messagingfunktionen hinzugefügt werden können. Standardmäßig ähnelt ein Dienst einem Konsolenprogramm, und ein Konsolenprogramm verfügt nicht über die Fähigkeit, Fenster und andere Clientbereiche zu verwalten. Üblicherweise weist ein Dienst die gleichen Einschränkungen auf. Muss Ihr Dienst über die Fähigkeit zur Erstellung von Fenstern und anderen Clientbereichen verfügen, muss eine Interaktion mit dem Desktop möglich sein. Aktivieren Sie hierzu das Kontrollkästchen Interact with the Desktop. Versuchen Sie jedoch nicht, zur Fehleranzeige Dialogfelder zu erzeugen. Hiermit frustrieren Sie lediglich den Administrator. Fehler sollten in das Windows 2000Ereignisprotokoll geschrieben oder per E-Mail an den Administrator gesendet werden. Klicken Sie auf Next, um zur nächsten Assistentenseite zu gelangen. Auf dieser Seite können Sie einen Benutzernamen und ein Kennwort eingeben. Diese Informationen werden im Hinblick auf die Sicherheitsrechte bei Dienstausführung benötigt. Auch wenn diese Informationen nützlich erscheinen, sind sie es nicht. Der Assistent speichert Benutzername und Kennwort im Quellcode, und dies fordert Sicherheitsprobleme geradezu heraus. Klicken Sie auf Finish, um den Quellcode zu generieren. Durchsicht des Quellcode Zum Verständis der Funktionsweise des Dienstes sollten Sie sich den Quellcode ansehen. Der Quellcode ist (abgesehen von den Windows-Headerdateien) von keiner Headerdatei abhängig – der Quellcode soll kompakt und wirkungsvoll sein. Dies bedeutet nicht, dass sich der Code nicht auf eine spezifische Bibliothek stützt, Sie können also MFCs (Microsoft Foundation Classes) oder ATLs (Active Template Libraries) oder eine eigene Bibliothek integrieren. Die Klassenansicht wird in Abbildung 20.5 dargestellt. Das Projekt umfasst zwei Klassen, CExampleService und CNTService. Die Klasse CNTService ist eine Unterklasse von CexampleService. Das Projekt ist so aufgebaut, dass die CNTService-Klasse alle Dienste-Anforderungen und Dienstdetails verwaltet, beispielsweise die Registrierung und die Ereignisverwaltung des Dienstes. Falls erforderlich, delegiert CNTService mit Hilfe virtueller C++-Funktionen Funktionalität an CExampleService. Die CExampleService-Klasse implementiert die Dienstfunktionalität und die Behandlungsroutinen für die Dienststeuerung.
Ausführen eines 24x7-Prozesses
645
Abbildung 20.5 Klassenansicht für den Beispieldienst
Innerhalb der CNTService-Klasse liegen verschiedene statische Methoden vor: static void WINAPI
ServiceCtrl(DWORD CtrlCode);
static void WINAPI
ServiceMain(DWORD argc, LPTSTR * argv);
static BOOL WINAPI
ControlHandler(DWORD CtrlType);
Diese Methoden werden über das Programm Dienste aufgerufen. Da es sich jedoch um statische Methoden handelt, besitzen Sie keinen Verweis auf die CExampleService-Klasse. Stattdessen definieren die Quellen eine globale Variable, gpTheService. Wenn Dienste die Dienststeuerung übernehmen möchte, wird die Methode ControlHandler aufgerufen. Die Methode ermittelt, welche Aktion angefordert wird, beispielsweise das Starten oder Stoppen des Dienstes. Diese Aktionen werden an die CNTService-Methodenimplementierungen der Aktionen delegiert die folgendermaßen definiert sind: virtual void
Stop();
virtual void
Pause();
virtual void
Continue();
virtual void
Shutdown();
Auf der in Abbildung 20.4 angezeigten Assistentenseite wurde angegeben, welche Nachrichten zur Dienststeuerung übermittelt werden sollten. Der Assistent implementiert die geeigneten Methoden in der CExampleService-Klasse. Angenommen, das in Abbildung 20.4 dargestellte Kontrollkästchen Stop ist aktiviert. In diesem Fall generiert der Codeguru NT Service Wizard eine Stoppmethode in
646
Erstellen von Diensten
CExampleService, die die gleiche Methodensignatur aufweist wie CNTService::Stop. Da die CNTService::Stop-Methode als virtuell definiert wurde, wird beim Aufruf der Stoppmethode durch die Methode ControlHandler die in CExampleService deklarierte Methode aufgerufen. Die Hauptdienstfunktionalität ist in der Methode Run implementiert, die durch die ServiceMain-Methode aufgerufen wird. Sendet das Programm Dienste jedoch Dienststeuerungsnachrichten an die Methoden Stop, Pause, Continue und Shutdown, geschieht dies auf Grundlage des Threads zur Dienststeuerung. Der Ausführungsthread ServiceMain dient einem anderen Zweck. Zur Synchronisierung der zwei Threads verwendet der NT-Dienst Appwizard ein Windows-API-Ereignis. Sehen Sie sich die folgende Implementierung der Methode CExampleService::Run an: void CExampleService::Run(DWORD dwArgc, LPTSTR * ppszArgv) { ReportStatus(SERVICE_START_PENDING); m_hStop = ::CreateEvent(0, TRUE, FALSE, 0); m_hPause = ::CreateEvent(0, TRUE, FALSE, 0); m_hContinue = ::CreateEvent(0, TRUE, FALSE, 0); ReportStatus(SERVICE_RUNNING); while( ::WaitForSingleObject(m_hStop, 10) != WAIT_OBJECT_0 ) { if(::WaitForSingleObject(m_hPause, 5) == WAIT_OBJECT_0) { while(::WaitForSingleObject(m_hContinue, 50) != WAIT_OBJECT_0) if(::WaitForSingleObject(m_hPause, 50) == WAIT_OBJECT_0) goto Stop; ::ResetEvent(m_hPause); ::ResetEvent(m_hContinue); } // TODO: Geben Sie hier die tatsächliche Dienstfunktionalität ein } Stop: if( m_hStop ) ::CloseHandle(m_hStop); if(m_hPause) ::CloseHandle(m_hPause); if(m_hContinue) ::CloseHandle(m_hContinue); }
Ausführen eines 24x7-Prozesses
647
Zu Beginn der Run-Methode wird die ReportStatus-Methode aufgerufen. Hierbei handelt es sich um einen wichtigen Methodenaufruf – die ReportStatus-Methode muss ab dem Aufruf von ServiceMain alle 20 Sekunden aufgerufen werden, um anzugeben, ob der Dienst noch aktiv ist und auf die Aktionen reagiert. Verstreichen die 20 Sekunden, ohne dass die ReportStatus-Methode aufgerufen wurde, geht der Dienst-Manager davon aus, dass der Prozess nicht mehr aktiv ist. Beim ersten ReportStatus-Aufruf wird die Konstante SERVICE_START_PENDING als Parameter übergeben. Als Nächstes werden unter Verwendung von CreateEvent die Ereignisobjekte stop (m_hStop), pause (m_hPause) und continue (m_hContinue) erstellt. Nach diesen Funktionsaufrufen können Sie eigene Initialisierungsroutinen hinzufügen. Bedenken Sie jedoch, dass nach Ablauf von 20Sekunden ein Aufruf von ReportStatus erforderlich ist. Nachdem die Initialisierung abgeschlossen wurde, wird ReportStatus aufgerufen und die Konstante SERVICE_RUNNING wird als Parameter übergeben. Im durch den Assistenten erzeugten Code wird eine generische while-Schleife gestartet, um in regelmäßigen Abständen zu prüfen, ob ein stop- oder pause-Ereignis ausgelöst wurde. Diese Ereignisse werden durch die Implementierungen der CExampleService-Dienststeuerungsmethode ausgelöst. Hat keine Ereignisauslösung stattgefunden, kann der Dienst Aufgaben ausführen, diese Aufgaben dürfen jedoch den Thread nicht belegen. Sie müssen in regelmäßigen Abständen prüfen, ob das Programm Dienste eine Anforderung zur Änderung des Dienstausführungsstatus gesendet hat. Hinzufügen des COM+-Anwendungscode Der Ausführungscode, den Sie dem Dienst hinzufügen, ist derselbe Code, den Sie zum Ansteuern einer COM+-Komponente in Visual C++ verwenden würden. Daraus folgt, dass Sie den Code mit Hilfe des COM-Compilers schreiben und zum Behandeln von Fehlern die Ausnahmebehandlung einsetzen. In der CExampleService::Run-Methode kann an der Stelle TODO der folgende Code eingefügt werden: try { SIMPLECOMLib::ITestInterfacePtr ptr( "SimpleCOM.TestInterface.1"); ptr->delayMethod( 10000); } catch( _com_error err) { ; }
648
Erstellen von Diensten
Denken Sie daran, dass bei der Initialisierung der CExampleService::Run-Methode der Funktionsaufruf CoInitialize eingefügt werden muss. Stoppen von Diensten Sendet das Programm Dienste eine Nachricht zum Stoppen des Dienstes, wird die Methode CExampleService::Stop aufgerufen. Die Standardimplementierung lautet folgendermaßen: void CExampleService::Stop() { ReportStatus(SERVICE_STOP_PENDING, 5000); if( m_hStop ) ::SetEvent(m_hStop); }
Die Implementierung sendet eine Meldung zurück, dass der Dienst gestoppt wird (ReportStatus), und dass dieser Vorgang fünf Sekunden in Anspruch nimmt. Dieser Wert kann natürlich geändert werden, das Problem besteht allerdings darin, dass nicht genau ermittelt werden kann, wie lange ein Dienst tatsächlich zum Stoppen benötigt. Windows 2000 ist kein Echtzeitbetriebssystem, bei dem ein Interrupt einen Prozessstopp erzwingt und den Fokus ändert. Stattdessen versetzt die Stop-Methode den Dienst in einen Wartestatus, und das Programm Dienste wartet darauf, dass der Dienst beendet wird bzw. dass ein anderes Ereignis eintritt. Wird der ServiceMain-Thread nicht richtig ausgeführt und erfolgt keine Antwort, kommt es zu einem Problem. Die einzige Möglichkeit zum Zerstören des Prozesses besteht in der Verwendung der Windows 2000-Dienstprogramme tlist und kill. Diese Dienste finden Sie in der technischen Referenz zu Windows 2000. Bei Verwendung dieser Methode ist ein Neustart des Computers nicht erforderlich. Dies ist jedoch nicht die einzige Möglichkeit, einen nicht funktionierenden Dienst unter Kontrolle zu bekommen. In der Implementierung von Stop können Sie auf eine Bestätigung des stop-Ereignisses warten. Während die Methode wartet, kann sie mit Hilfe der ReportStatus-Methode weiterhin eine noch ausstehende Stop-Anforderung senden. Erfolgt innerhalb einer festgelegten Zeitspanne keine Bestätigung, kann der ServiceMain-Thread zerstört werden. Der Vorteil bei diesem Ansatz besteht darin, dass belegte Ressourcen mit der Stop-Methode freigegeben werden können.
Ausführen eines 24x7-Prozesses
649
Behandlung von COM-Objekten in parallel ausgeführten Threads Unser Dienst führt momentan alle Aufgaben in einem einzelnen Thread aus. Es liegt keine parallele Verarbeitung vor. Angenommen, der Computer ist mit vier Prozessoren ausgestattet, wird nur ein Prozessor eingesetzt. Es soll erreicht werden, dass der ServiceMain-Thread weitere Threads erzeugt, damit parallel größere Datenmengen verarbeitet werden können. Skalierbarkeit durch Einsatz mehrerer Threads Bei Verwendung mehrerer Threads in einer Dienstanwendung treten zwei Probleme auf. Das erste Problem sind COM-Objekte, die in anderen Threads ausgeführte COM-Objekte aufrufen, das zweite Problem ist die Bereitstellung von Synchronisierung. Zur Lösung dieser beiden Probleme sowie zur Bereitstellung von Skalierbarkeit soll ein Methodenaufruf erstellt werden, der nahezu identisch mit der CExampleService::Run-Methode ist. Der einzige Unterschied besteht darin, dass der Methodenaufruf mehrmals ausgeführt wird. Es ist eine separate Methode erforderlich, da der CExampleService::Run-Thread (ServiceMain) und die generierten Threads anfänglich unterschiedliche Funktionen erfüllen. Der CExampleService::RunThread ist für das Generieren und Verwalten der weiteren Threads verantwortlich. Sobald diese Aufgabe von CExampleService::Run beendet wurde, können andere Aufgaben ausgeführt werden. In diesem Fall führen CExampleService::Run und die weiteren Threads den gleichen Code aus. Die Methode zur Durchführung dieser Aufgaben ist folgendermaßen definiert: void RunInThread( void *reference) { long refId = (long)reference; while( ::WaitForSingleObject(m_hStop[ refId], 10) != WAIT_OBJECT_0 ) { if(::WaitForSingleObject(m_hPause[ refId], 5) == WAIT_OBJECT_0) { while(::WaitForSingleObject(m_hContinue[ refId], 50) != WAIT_OBJECT_0) if(::WaitForSingleObject(m_hPause[refId], 50) == WAIT_OBJECT_0) goto Stop; ::ResetEvent(m_hPause[ refId]); ::ResetEvent(m_hContinue[refId]); } CoInitialize( NULL); try { SIMPLECOMLib::ITestInterfacePtr ptr( "SimpleCOM.TestInterface.1"); ptr->delayMethod( 10000);
Der Code ähnelt stark den zuvor gezeigten Codeabschnitten. Der Unterschied besteht darin, dass die pause-, continue- und stop-Ereignisobjekte Teile eines Arrays darstellen. Jedem Thread wird eine ID zugewiesen, durch die das betreffende Array indiziert wird. Sobald ein Thread gestoppt oder angehalten werden muss, wird das zugehörige Ereignis geprüft. Jeden Thread mit einem eigenen Ereignis auszustatten mag komplizierter erscheinen als die Verwendung einer globalen Synchronisierung. Das Problem bei der globalen Synchronisierung besteht jedoch darin, dass mehr Code erforderlich ist. Die Daten werden in einer lokalen Threadstruktur gespeichert, die folgendermaßen definiert ist: struct _threadData { HANDLE
m_hStop;
HANDLE
m_hPause;
HANDLE
m_hContinue;
};
Muss ein Thread einen anderen Thread aufrufen, wird einfach die ID des betreffenden Threads abgerufen. Die ID entspricht einem Index im Array _threadData. Durch Verwenden des Programms Dienste oder eines anderen Verwaltungstools ist es möglich, alle Threads anzuhalten oder einzelne Threads zu starten. Werden mehr Threads benötigt, werden weitere Threads gestartet. Hierbei verfügt jeder Thread über eine eigene ID, einem Index im Array _threadData. Mehrere Threads und gegenseitiger Aufruf Sie verfügen nun über eine Anwendung mit Multithreading und Synchronisierung. Bei der Ausführung von COM-Anwendungen mit Multithreading kann nicht einfach ein globales COMObjekt erstellt und in jedem Thread referenziert werden, da dies zu Konflikten mit dem COM-Threading führen kann. Beim COM-Threading gibt es drei verschiedene Modelle – freies Threading (MTA), Apartment-Threading (STA) und neutrales Threading (NTA). Alle COM- und COM+-Konsumentenobjektbeispiele sind STA-basierte Anwendungen. Der Unterschied zwischen den unterschiedlichen Modellen liegt darin, wie die COM-Objekte bei Verwendung durch einen Client synchronisiert werden. In STA
Ausführen eines 24x7-Prozesses
651
wird einem Client zugesichert, dass der Client bei einem Methodenaufruf für ein COM-Objekt der einzige Client ist, der einen Methodenaufruf durchführt. Alle weiteren Clients, die einen Methodenaufruf durchführen möchten, müssen warten, bis der erste Client den Methodenaufruf beendet hat. Im MTA-Modell können Clients parallel Methodenaufrufe für COM-Objekte durchführen. Im NTAModell wechselt das COM-Objekt automatisch zwischen STA- und MTA-Modus, je nach Client. So werden die COM-Methodenaufrufe beschleunigt, denn es ist kein Threadkontextwechsel erforderlich. Wie werden diese Konzepte auf Dienste mit Multithreading angewendet? Eine Verwendung des STA-Modells vereinfacht die Synchronisierung, da COM sich, unabhängig vom Threadingmodell für das COM+-Objekt, um die Details kümmert. Aufgrund der Funktionsweise von COM+ ist es besser, dass die Details der Synchronisierung über COM+ behandelt werden. Verwendet der Thread das MTA-COM-Threading und ist ein COM-Objekt vorhanden, dass beide Funktionen übernehmen kann, können Fehler auftreten, da das COM-Threading mit der schwarzen Magie verglichen werden kann – nur wenige sind eingeweiht. Einige Programmiersprachen, beispielsweise Visual Basic, können das MTA-Modell nicht einsetzen, daher bietet die Verwendung von MTA keine großen Vorteile. Das Ziel besteht darin, einen stabilen und zuverlässigen Dienst zu entwickeln, selbst, wenn dieser etwas langsamer ist. Angenommen, Sie verwenden STA. Ruft ein Thread ein in einem weiteren Thread ausgeführtes COM-Objekt auf, wird ein sicheres COM-Handle benötigt, andernfalls stürzt die Anwendung ab. Zum Abrufen eines sicheren COM-Handles muss der COM-Schnittstellenzeiger mit Hilfe der CoMarhalInterThreadInterfaceInStream-Funktion von einem Thread an einen anderen übergeben werden. Im Wesentlichen wird hierdurch der Schnittstellenzeiger in einem Stream gespeichert, der Stream wird anschließend durch den zweiten Thread gelesen. Wird der Schnittstellenzeiger aus dem Stream gelesen, wird automatisch ein Marshaling durchgeführt. Der Marshalingprozess stellt sicher, dass COM- und COM+-Kontext und -Attribute nicht umgangen werden. Das folgende Beispiel zeigt einen Thread, der einen COM-Schnittstellenzeiger instanziiert hat und diesen im Stream IStream speichern wird. IStream *gStream; void thread1( void *param) { … CoMarshalInterThreadInterfaceInStream( IID_ISimpleInterface, pInterface, &gStream);
652
Erstellen von Diensten
//Ereignis darüber informieren, dass für Schnittstelle Marshaling ausgeführt wurde }
Zum Lesen des gespeicherten COM-Schnittstellenzeigers würde der zweite Thread den folgenden Code verwenden: void thread2( void *param) { … CoGetInterfaceAndReleaseStream( gStream, IID_ISimpleInterface, pLocalInterface); }
Die CoGetInterfaceAndReleaseStream-Funktion liest den Schnittstellenzeiger und gibt den Stream wieder frei. Jetzt kann der zweite Thread den COM-Schnittstellenzeiger aufrufen und wie gewohnt fortfahren. Obwohl diese Vorgehensweise kompliziert erscheint, ist das Schreiben eines eigenen Synchronisierungscodes weitaus aufwendiger, der möglicherweise nicht COM+-kompatibel ist. COM+ kann Synchronisierung für die Dauer der Transaktion bereitstellen, die Informationen können im Dienst nicht decodiert werden.
20.2 Ausführen eines Stapelverarbeitungsprozesses Gelegentlich müssen auf wöchentlicher oder monatlicher Basis per Stapelverarbeitung Berichte erstellt werden. Die einfachste Methode zum Ausführen von Stapelverarbeitungsprozessen stellt der Windows 2000-Taskplaner dar. Über diese Funktion können Sie Skripts zur Kommunikation mit den COM+-Anwendungen ausführen.
20.2.1 Verwenden des Windows Scripting Host Im vorangegangenen Kapitel zum Debuggen wurde der Skripthost zwar als Treiber verwendet, dessen Funktionsweise wurde jedoch nicht erläutert. Eine Skriptdatei wird in einer skriptbasierten Sprache geschrieben, der Skripthost kann diese Datei ausführen und die Sprachbefehle verarbeiten. Eine Skriptdatei ähnelt einer Stapelverarbeitungsdatei darin, dass mit ihr ebenfalls komplexe Aufgaben (Tasks) ausgeführt werden können. Der Vorteil bei der Verwendung des Scripting Host besteht darin, dass es sich um eine COM-basierte Skriptumgebung handelt. Über das Skript können weitere COM-Objekte aufgerufen werden, und der Scripting Host wird gegenüber dem ausgeführten Skript als COM-Objekt offen gelegt.
Ausführen eines Stapelverarbeitungsprozesses
653
Zum Starten des Scripting Host führen Sie den Befehl cscript oder wscript aus. Beide Befehle können entweder eine JavaScript-Datei (.js) oder eine VB-Skriptdatei (.vbs) ausführen. Der Befehl cscript startet ein konsolenbasiertes Programm, wscript startet ein Windows-basiertes Programm. Zum Zweck der Aufgabenausführung auf dem Server ist der Konsolenmodus vorzuziehen. Verwenden von »WScript« Beim Scripting sind drei grundlegende COM-Objekte vorhanden, WScript, WshShell und WshNetwork. Für die meisten Tasks wird WScript eingesetzt. Beim Starten des Scripting können einige skriptsprachenspezifische Funktionen ausgeführt werden. Ihre Hauptaufgabe besteht jedoch in den meisten Fällen darin, weitere COM- oder COM+-Objekte zu erstellen und diese zu bearbeiten. Hierzu ist eine Referenzierung des COM-Objekts WScript erforderlich. Mit diesem COM-Objekt erstellen Sie weitere COM-Objekte. Zur Erstellung eines COMObjekts und anschließendem Methodenaufruf für das erstellte COM-Objekt könnte der folgende JavaScript-Code ausgeführt werden: var tempObject; tempObject = WScript.CreateObject("SimpleObject.SimpleInterface") temp.delayMethod( 1000);
Die Methode WScript.CreateObject akzeptiert als Parameter die PROG-ID des COM-Objekts. Bei der Objektinstanziierung wird die IDispatch-Schnittstelle abgerufen, so dass auf die einzelnen Methoden zugegriffen werden kann. Stelen Sie sicher, dass die COM- und COM+-Objekte IDispatch unterstützen. Zur Freigabe des Objekts wird die WScript.DisconnectObject-Methode aufgerufen. Beispiel: WScript.DisconnectObject( tempObject);
Müssen Informationen an den Desktop gesendet werden, wird hierzu die Methode WScript.Echo verwendet: WScript.Echo("Hello world");
Abschließend beenden Sie entweder das Skript, wenn dieses ausgeführt wurde, oder Sie rufen die Methode WScript.Quit auf. Ablaufverfolgung durchgeführter Aktionen Wenn Sie einen Stapelverarbeitungsprozess mehrmals ausführen, ist es häufig nützlich, eine Ablaufverfolgung einzurichten. Dies geschieht üblicherweise durch
654
Erstellen von Diensten
eine Datei oder eine Datenbank. Eine einfache Möglichkeit stellt die Registrierung dar. Das COM-Objekt WshShell weist drei Methoden auf, WshShell.RegRead zum Lesen eines Registrierungsschlüssels, WshShell.RegWrite zum Schreiben eines Registrierungsschlüssels und WshShell.RegDelete zum Löschen eines Registrierungsschlüssels. Nachfolgend einige Verwendungsbeispiele: tempObject = WScript.CreateObject( "WScript.Shell"); tempObject.RegWrite( "HKEY_CURRENT_USER\\Value", "Some string value"); WScript.Echo( tempObject.RegRead( "HKEY_CURRENT_USER\\Value"));
Ausführen eines anderen Programms Gelegentlich erfordert eine Stapeldatei die Ausführung eines weiteren Stapelverarbeitungsprozesses oder eines weiteren Konsolenprogramms. Zu diesem Zweck kann die WshShell.Run-Methode eingesetzt werden. Eine Beispielimplementierung lautet folgendermaßen: tempObject = WScript.CreateObject( "WScript.Shell"); tempObject.Run( "secondprocess.exe", 0, FALSE);
Der erste Parameter der tempObject.Run-Methode gibt den zu startenden Prozess an. Der zweite Parameter spezifiziert den Fensterstatus beim Start. Da es sich um einen serverseitigen Prozess handelt, lautet der Fensterstatus 0. Der letzte Parameter gibt an, ob der Scripting Host wartet, bis der Prozess beendet ist. Der Wert FALSE gibt an, dass der Skripthost nicht wartet, der Wert TRUE gibt an, dass der Skripthost wartet und den Fehlercode als Rückgabewert zurückgibt.
20.2.2 Hinzufügen einer geplanten Aufgabe Zum Hinzufügen geplanter Aufgaben zum Taskplaner können Sie COM-Schnittstellen verwenden und ein Programm schreiben, mit dem die Aufgabe automatisch hinzugefügt wird. Wir werden hier jedoch den bereitgestellten Assistenten zum Hinzufügen einer geplanten Aufgabe verwenden. Erweitern Sie zunächst im Explorer die Knoten Arbeitsplatz und Systemsteuerung, und wählen Sie den Knoten Geplante Tasks aus. Doppelklicken Sie im Detailbereich auf die Option Geplanten Task hinzufügen. Das Dialogfeld Assistent für geplante Tasks wird angezeigt. Klicken Sie auf Weiter, um das in Abbildung 20.6 gezeigte Dialogfeld zu öffnen. Die Liste der Anwendungen enthält alle Programme, die über den Taskplaner gestartet werden können. Die Anwendung cscript.exe ist in dieser Liste nicht enthalten. Klicken Sie zum Hinzufügen dieser Anwendung auf die Schaltfläche
Ausführen eines Stapelverarbeitungsprozesses
655
Durchsuchen, und suchen Sie nach der Anwendung. Üblicherweise lautet der Standort der Anwendung Windows\System32.
Abbildung 20.6 Programmauswahl
Klicken Sie auf OK und Weiter. Das in Abbildung 20.7 dargestellte Dialogfeld wird geöffnet. In diesem Dialogfeld können Sie einen Namen für die Aufgabe eingeben und festlegen, wann die Aufgabe ausgeführt wird. Es stehen verschiedene Optionen zur Verfügung, beispielsweise Täglich, Wöchentlich, Monatlich usw. Wählen Sie für den Moment die Option Täglich, und klicken Sie auf Weiter.
Abbildung 20.7 Definieren des geplanten Tasks
656
Erstellen von Diensten
Im nun angezeigten Dialogfeld können weitere Angaben zu Ausführungsdatum und -zeit vorgenommen werden. Übernehmen Sie die Standardeinstellungen, und klicken Sie auf Weiter. Im nächsten Schritt wird angegeben, im Namen welchen Benutzers der Prozess gestartet wird. Treffen Sie diese Auswahl sorgfältig, da Rechte zur Stapelverarbeitung erforderlich sind. Geben Sie Benutzername und Kennwort an, und klicken Sie auf Weiter. Im nächsten Dialogfeld, siehe Abbildung 20.8, wird der eben definierte Task zusammenfassend beschrieben. Aktivieren Sie die Option Erweiterte Eigenschaften für diesen Task beim Klicken auf »Fertig stellen« öffnen, und klicken Sie auf Fertig stellen.
Abbildung 20.8 Taskbeschreibung
Sehen Sie sich im Dialogfeld mit den erweiterten Eigenschaften der geplanten Aufgabe (siehe Abbildung 20.9) das Textfeld Ausführen in an. Dieses Textfeld enthält nur den Namen der Datei, die das auszuführende Skript enthält, wie hier gezeigt:
Schließen Sie das Dialogfeld, indem Sie auf OK klicken. So einfach erstellen Sie einen Stapelverarbeitungsprozess. Ist eine umfangreichere Funktionalität erforderlich, sollten Sie ein Konsolenprogramm oder zusätzliche COM-Objekte zur Bereitstellung der gewünschten Funktionalität schreiben.
20.3 Ein abschließendes Resümee Wir haben eine Anwendung entworfen, entwickelt und implementiert, wir haben Transaktionen verwendet, Nachrichten versendet, das Web eingesetzt, eine XMLVerarbeitung durchgeführt (Extensible Markup Language), gespeicherte Prozeduren geschrieben, einen Verzeichnisdienst verwendet, eine Verbindung zu einer Datenbank hergestellt und das Alles einer 24×7-Operation zur Verfügung gestellt. All diese Aufgaben wurden im Rahmen von Windows DNA durchgeführt, und es funktioniert! Das Schreiben großer Systeme bei Einsatz vieler verschiedener Systeme ist eine komplexe Angelegenheit – es handelt sich um eine Aufgabe, die nicht in einem Arbeitsschritt erledigt werden kann.
658
Erstellen von Diensten
In diesem Buch wurde aufgezeigt, wie eine verteilte Webanwendung geschrieben wird. In diesem Buch wurde nicht alles gesagt – dieser Themenbereich kann mit einem einzigen Buch nicht abgedeckt werden. Ich habe versucht, Ihnen einen Weg durch den Akronymdschungel zu schlagen und Ihnen ein Handbuch zur Verfügung zu stellen, mit dem Sie diese Aufgabe auch allein bewältigen können. Wenn Sie Anmerkungen zu diesem Buch haben, seien es positive oder negative, senden Sie diese an meine E-Mail-Adresse [email protected]. Und vergessen Sie nicht, die Website www.devspace.com/windna zu besuchen, auf der Sie die aktuellsten Informationen zum vorliegenden Buch finden. Christian Gross Cannes, Frankreich Dezember 1999
Ein abschließendes Resümee
659
A In diesem Buch verwendete Muster Dieser Anhang enthält eine Liste der im Buch aufgeführten Muster. Wenn das jeweilige Muster ursprünglich in einem anderen Buch beschrieben wurde, finden Sie einen entsprechenden Verweis.
A.1 Bridge-Muster Siehe Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software; Gamma u.a., Addison-Wesley, 1996; ISBN 3-893-19950-0 Problem Der Sinn der objektorienterten Entwicklung eines Systems besteht darin, den Entwurf zu abstrahieren und anschließend mehrere Implementierungen zu entwickeln. Das übliche Beispiel ist das Definieren einer generischen Shape-Klasse, von der die Klassen square, circle und andere abgeleitet werden. Dies ist ein guter Ansatz, der jedoch auch eine Nebenwirkung hervorruft. Wenn die Abstraktion an die Implementierung gebunden ist, ist der Client an die Implementierung gebunden. Auf diese Weise ist die gesamte Architektur schwer zu erweitern und zu ändern, da alle Klassen zu eng verbunden sind. Lösung Die Lösung dieses Problems besteht darin, eine Schnittstelle zu erstellen, die eine bestimmte Funktionalität definiert, und diese Schnittstelle von einer Klasse implementieren zu lassen. Der Vorteil dieser Lösung ist, dass keine dauerhafte Bindung definiert werden muss. Der Client ist an eine Schnittstelle und nicht an eine bestimmte Implementierung gebunden. Ein Beispiel hierfür lautet folgendermaßen: class Schnittstelle { public: virtual void Methode( long param1) = 0; }
Eine Beispielimplementierung wird folgendermaßen definiert: class Implementierung : public Schnittstelle { public: virtual Methode( long param) { } };
Bridge-Muster
661
Konsequenzen 왘 Schnittstelle
und Implementierung werden getrennt behandelt: Eine Schnittstelle ist nicht an eine bestimmte Implementierung gebunden. Wenn eine Schnittstelle mit einer Implementierung verknüpft wird, ist dies nicht von Dauer, und die Bindung der Schnittstelle kann zur Laufzeit verändert werden.
왘 Verbesserte Möglichkeit zur Erweiterung: Sowohl die Schnittstelle als auch
die Implementierung können unabhängig voneinander erweitert werden. 왘 Verborgene Implementierungsdetails: Da dem Client nur die Schnittstelle an-
gezeigt wird, bleiben die Details der Implementierung im Hintergrund. Die Implementierung könnte eine Klasse oder tausend Klassen umfassen. Das spielt in diesem Fall keine Rolle.
A.2 Façade-Muster Siehe Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software; Gamma u.a., Addison-Wesley, 1996; ISBN 3-893-19950-0 Problem Bei der Entwicklung eines Systems beginnt der Entwurf zunächst mit einer Gruppe gut durchdachter Objekte, und auch die Interaktion zwischen den Objekten ist genau definiert. Sowohl die Einhaltung zeitlicher Fristen als auch die Behebung von Bugs führen dazu, dass einige Shortcuts verwendet und einige im ursprünglichen Entwurf nicht eingeplante Objekte erstellt werden müssen. Dies hat zur Folge, dass sich die Objektanzahl erhöht und der Entwurf langsam in einzelne Fragmente zerfällt. Oder stellen Sie sich vor, ein System wird nach einem makellosen granularen Entwurf entwickelt. Auch wenn der Entwurf gut ist, muss der Client viele verschiedene Objekte bearbeiten, um eine bestimmte Aufgabe zu erfüllen, wie in Abbildung A.1 dargestellt ist. Auch möglich ist, dass ein Client in dieser Architektur ein Lowlevel-Objekt bearbeiten muss. Dadurch wird die Anzahl der Objektabhängigkeiten und die Anzahl der Clients, die bei Änderung des Objekts bearbeitet werden müssen, erhöht.
662
In diesem Buch verwendete Muster
Client
Anwendungssystem
Abbildung A.1 Feine granulare Objektarchitektur
Lösung Die Lösung liegt in der Erstellung eines Objekts, das die Funktionalität mehrerer Lowlevel-Objekte einkapselt. Das neu eingekapselte Objekt fungiert anschließend als eine Art übergeordnetes Objekt, das außer der Einkapselung der unteren Ebenen keinen weiteren Programmiercode enthält. Normalerweise sind diese Objekte auf die Lösung eines bestimmten Problems spezialisiert. Der entsprechende Entwurf ist in Abbildung A.2 dargestellt.
Client
Anwendungssystem
Abbildung A.2 Façade-Objektarchitektur
Façade-Muster
663
Konsequenzen Diese Architektur schirmt den Client vom zugrunde liegenden Teilsystem ab, so dass die Funktionsweise der Architektur leichter nachzuvollziehen ist, ohne dass alle Einzelheiten beachtet werden müssen. Der Client muss nur die Details eines einzigen Objekts nachvollziehen. Façade-Benutzer sind reine Anwendungsprogrammierer, die sich nicht um die Funktionen der darunter liegenden Ebene kümmern. Diese Architektur unterstützt die schwache Bindung des Clients mit dem zugrunde liegenden Teilsystem. Da die Definition der Façade-Schnittstelle einem bekannten Geschäftsprozess gleicht, kann die gesamte Implementierung sich ändern, ohne dass am Client Änderungen vorgenommen werden müssen.
A.3 Schichtenmuster Siehe Pattern Languages of Program Design; Vlissides u.a.; Addison Wesley, 1996; ISBN 0-201-89527-7 Problem In großen Systemen werden Objekte häufig für den Zugriff auf eine bestimmte Funktionalität entwickelt. Diese Funktionalität ist beispielsweise der Zugriff auf eine Ressource, die Schnittstelle zu einem Kunden oder etwas Ähnliches. Diese Komponenten greifen jedoch auf andere Komponenten ohne eine ordnungsgemäß definierte Abstraktion oder ein Protokoll zu. Das Wort Protokoll bezieht sich an dieser Stelle auf eine Aufrufsequenz, nicht auf einen Datenstream. Stellen Sie sich beispielsweise vor, dass ein Konsument eine vordefinierte Datenkomponente aufruft. Nehmen Sie an, diese Datenkomponente wird im Zuge einer schnellen Fehlerkorrektur umgangen und der Konsument greift direkt auf die Ressource zu. Das Ergebnis wäre Chaos. Wenn die Anzahl der schnellen Fehlerkorrekturen zunimmt, wird das System instabil, da die Auswirkungen der einzelnen Korrekturen nicht absehbar sind. Im Extremfall wird das ganze System zu einem einzigen Patch. Lösung Um diese Situation zu vermeiden, liegt die angemessene Strategie in der Erstellung einer Reihe von Schichten, die eine bestimmte Funktionalität definieren. Der Zweck dieser Schichten ist, dass eine Schicht für die Schicht, die deren Funktionalität verwendet, immer undurchsichtig bleibt. Das hat zur Folge, dass eine höhere Schicht nur die Funktionalität der unteren Schicht verwendet.
664
In diesem Buch verwendete Muster
In Abbildung A.3 werden die Schichten als eine Reihe von Kreisen dargestellt. Vergleichen Sie diesen Ansatz mit der Anwendung für die Konferenzanmeldung, die in diesem Buch entwickelt wurde. Im Mittelpunkt des Systems ist der Kern, der sich aus einer Reihe von Tabellen oder Kernobjekten zusammensetzen kann. Normalerweise handelt es sich um eine Ansicht der Daten. Eine Ebene darüber befinden sich die Dienstprogrammklassen, beispielsweise gespeicherte Prozeduren oder SQL-Sichten. Darüber wiederum befindet sich die Datenabstraktion, die die Verbindung zwischen der Ressource und der systemeigenen Sprache bildet. Diese Schicht wird durch die Helferobjektschicht eingekapselt. Die Helferobjekten sind Klassen, die bei der Durchführung bestimmter Aktionen unterstützen, jedoch nicht anwendungsbezogen sind. Schließlich folgen über den Helferklassen die Anwendungsklassen. Auf dieser Ebene können viele weitere Ebenen aufbauen, dies hängt jedoch ganz von der entwickelten Anwendung ab.
Erweiterte Fuktionalität Basis
Kern
Abbildung A.3 Schichtenarchitektur
Konsequenzen 왘 Organisation: Das gesamte System ist organisiert. Wenn eine Schicht verän-
dert wird, sind die Auswirkungen leicht nachzuvollziehen und zu korrigieren. Dies bedeutet jedoch nicht, dass der Entwurf zu einem einzigen großen Block verschmilzt, da innerhalb der Schichten bestimmte Einsprungpunkte definiert werden können. 왘 Abhängigkeit: Da eine Schicht von einer anderen abhängig ist, ist nur die je-
weils darüber liegende Schicht von einer Änderung direkt betroffen. 왘 Einzelner Thread: In der Regel wird beim Zugriff auf eine Schicht nur ein ein-
zelner Thread ausgeführt.
Schichtenmuster
665
A.4 Befehlsmuster Siehe Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software; Gamma u.a., Addison-Wesley, 1996; ISBN 3-893-19950-0 Problem Bei den meisten Anwendungsentwürfen ist die Architektur auf einen einzelnen Computer beschränkt. Wenn sie auf verschiedene Computer verteilt wird, verläuft die Kommunikation synchron (z.B. DCOM). Zuverlässige und stabile Anwendungen können mit Hilfe von Messaginganwendungen entwickelt werden. Das Problem beim Messaging liegt jedoch darin, dass eine Nachricht explizit definiert werden muss und dass die Daten explizit serialisiert werden müssen. In einer Messagingumgebung müssen außerdem die Sende- und Empfangsprozesse explizit programmiert werden. Durch all diese zusätzlichen Schritte gestaltet sich die Programmierung komplizierter. Lösung Die Lösung liegt in der Erstellung einer Klassendefinition, die über eine von einem Container aufgerufene Methode verfügt. Die Methode wird anschließend von einem Objekt implementiert, das seine eigenen Aktionen durchführt. Dies ist die flexibelste Anordnung, da die Funktionalität vom Container und vom Objekt getrennt wird. Die Klasse wird folgendermaßen definiert: class Command { public: virtual void execute( IUnknown *pUnk) = 0; };
Konsequenzen Dieses Muster hat folgende Konsequenzen: 왘 Der Entwurf wird unterteilt, so dass das Objekt, das den Vorgang aufruft, den
Vorgang nicht unbedingt durchführen können muss. 왘 Befehlsobjekte können ausgeführt, bearbeitet und beliebig angepasst werden,
solange sie die MSMQCommand-Schnittstelle implementieren. 왘 Befehle können eingebettet, mit anderen Befehlen kombiniert oder als zusam-
mengesetzte Befehle kombiniert werden.
666
In diesem Buch verwendete Muster
왘 Das Erstellen neuer Befehlsobjekte ist einfach, da der Container, der den Befehl
ausführt, den genauen Vorgang nicht kennen muss. Er muss lediglich über die MSMQCommand-Schnittstelle unterrichtet sein. Das bedeutet, dass die Anzahl der Befehle unbegrenzt ist.
A.5 Datenabstraktionsmuster Siehe Pattern Languages of Program Design; Vlissides u.a.; Addison Wesley, 1996; ISBN 0-201-89527-7 Problem Bei der Entwicklung von Anwendungen oder Komponenten gibt es das Problem der dauerhaften Datenspeicherung. Wenn Sie eine Persistenzlösung erstellen, müssen Sie festlegen, wie das natürliche Format der Daten, beispielsweise eine Datenbank, in eine bestimmte Sprache, beispielsweise Visual Basic, konvertiert wird. Nach dem Konvertieren der Daten müssen Sie sich überlegen, wie sie wieder in das ursprüngliche Format zurückgespeichert werden. Außerdem stellen sich folgende Fragen: Ist die Lösung skalierbar? Wie stabil ist die Lösung? Wie stimmig ist die Lösung in einem Szenario mit mehreren Benutzern? Diese Fragen kommen typischerweise bei der Entwicklung eines unternehmensweiten Systems auf. Lösung Die Lösung für dieses Problem liegt in der Abstraktion des ursprünglichen Datenformats in das sprachspezifische Datenformat. Eine Datenabstraktionsklasse wird zum Speichern der Daten verwendet, und eine andere Klasse, Operationsklasse genannt, arbeitet direkt mit der Datenklasse. Da mit den sprachspezifischen Datenformatklassen keine Ressourcen verknüpft sind, ist für die Operationsklasse nicht relevant, woher die Daten stammen. Die sprachspezifischen Datenformatsklassen bilden den Kern der Schichtenanwendung. Konsequenzen 왘 Skalierbar: Die Lösung kann skalierbarer gestaltet werden, da ein einziger Spei-
cherort vorliegt: das zentrale Repository. 왘 Zentrale Speicherung: Es gibt nur eine Kopie der Daten innerhalb des gesam-
ten Netzwerks. Für die Leistung und die Cachefunktionen ist die Ressource zuständig, nicht die Operationen, die die Datenstruktur bearbeiten.
Datenabstraktionsmuster
667
왘 Ressourcenunabhängigkeit: Die sprachspezifische Datenformatsklasse ist die
Darstellung eines zugrunde liegenden ursprünglichen Datenformats. Sie repräsentiert ein bestimmtes Format, da durch die Datenabstraktion nicht die Operationen definiert werden, die für den Zugriff auf das ursprüngliche Datenformat verwendet werden. 왘 Einkapselung durch Schnittstellen: Die Datenabstraktion stellt eine ursprüng-
liche Datenstruktur dar, sodass der Konsument nicht genau wissen muss, welches Datenformat eigentlich zugrunde liegt.
A.6 Remotesteuerungsmuster Siehe Kapitel 14 dieses Buches. Problem Die meisten für das Web geschriebenen Anwendungen sind Clientanwendungen, die keine Shortcuts für den fortgeschrittenen Benutzer anbieten. Im Web werden die Dinge üblicherweise selbsterklärend gestaltet, für einen fortgeschrittenen Benutzer sind diese zusätzlichen Informationen jedoch eher hinderlich und setzen die Produktivität herab. Der fortgeschrittene Benutzer kennt die Funktionsweise der Anwendung und möchte die einzelnen Schritte schnell durchlaufen. Er möchte nicht auf 50 Hyperlinks klicken, bevor er zur gewünschten Webseite gelangt. Es wäre jedoch unnötiger Aufwand, zwei Codevarianten für zwei Benutzertypen anzulegen. Die Kosten für Verteilung, Schulung und Entwicklung würden sich verdoppeln. Lösung Der englische Name für das Remotesteuerungsmuster (engl.: Remote Control Pattern) leitet sich von Remote Control ab, dem englischen Wort für die Fernbedienung für Fernseher, Videogeräte und andere elektronische Geräte. Auf diesen Fernbedienungen befinden sich Knöpfe für einfache und kompliziertere Funktionen. Die Knöpfe für einfache Funktionen, beispielsweise zum Ändern der Lautstärke und zum Wechseln des Kanals, sind für alle Benutzer bestimmt. Sie sind in der Regel groß und leicht zu finden. Der fortgeschrittene Benutzer möchte jedoch die Möglichkeit haben, den Dolby Surround-Klang einzustellen oder den neuesten Film vom Satellitensender aufzunehmen. Dieser Benutzer verwendet die kleinen Knöpfe auf der Fernbedienung, die komplexere Funktionen bieten. Die jeweiligen Schritte sind komplizierter und es werden möglicherweise Meldungen zur Programmierung auf dem Bildschirm angezeigt. Der fortgeschrittene Benutzer beeinflusst den Inhalt nicht, macht ihn jedoch für seine eigenen Anforderungen zugänglicher.
668
In diesem Buch verwendete Muster
Das Remotesteuerungsmuster nutzt dieses Konzept für die Benutzerschnittstelle. Mit Microsoft Internet Explorer ist es möglich, das Webbrowsersteuerelement in einer Anwendung einzubetten und das Steuerelement anschließend zu programmieren. Mit Hilfe dieser Technik kann der Browser angewiesen werden, den Anforderungen des fortgeschrittenen Benutzers entsprechend Seiten abzurufen und zu bearbeiten. Die Funktionalität des Webs wird nicht geändert, ebensowenig die Funktionsweise der Webanwendung. Fortgeschrittenen Benutzern wird lediglich eine Möglichkeit geboten, ihre Produktivität beim Surfen durch eine bestimmte Webanwendung zu optimieren. Konsequenzen 왘 Die Clientanwendung wird vom Webinhalt gelöst: Die auf der Darstellungs-
schicht hinzugefügten Funktionen beeinflussen nicht den gesamten Ablauf der Webanwendung, da Internet Explorer als Steuerelement eingebettet ist. Jeder in einer Programmiersprache erfahrene Benutzer kann den Browser seinem Geschmack entsprechend programmieren. Er erfüllt die Anforderungen sämtlicher Benutzer, auch wenn sie dieselbe Webanwendung verwenden. 왘 Zentraler Webinhalt: Bei der letztendlichen Lösung wird das Web noch immer
zum Abrufen des Inhalts und zur Anzeige für den Benutzer verwendet. Die Haupt-Webanwendung kann auf dem Server aktualisiert werden, ohne dass eine Aktualisierung der Remotesteuerungsmuster-Anwendung erforderlich ist. Herkömmliche Anwendungen würden eine Aktualisierung auf der Clientseite erfordern. 왘 Browserabhängigkeit: Für diese Lösung ist die Funktionalität des Microsoft In-
ternet Explorer-Webbrowserelements erforderlich. Es kann nicht durch eine Komponente eines anderen Herstellers ersetzt werden, da es keine Spezifikation für Browsersteuerungskomponenten gibt.
A.7 Muster zum Trennen von Format und Programmiercode Siehe Whitepaper in der MSDN-Bibliothek; Christian Gross; 1998. Dieses Whitepaper liegt dem Quellcode für dieses Buch bei, Sie finden es im Verzeichnis demo/pattern. Problem Stellen Sie sich eine herkömmliche Benutzeroberfläche mit Schaltflächen, Tabellen und Optionsschaltflächen vor. Wenn Sie auf eine Schaltfläche klicken, wird irgendeine Aktion an einer Tabelle oder einem Listenfeld ausgeführt. Die Schaltflächen stellen Elemente der Benutzeroberfläche dar, und die Aktion ist die Benut-
Muster zum Trennen von Format und Programmiercode
669
zeroberflächenlogik, der Programmiercode. In einer herkömmlichen Anwendung sind die Elemente der Benutzeroberfläche und der Programmiercode eng miteinander verbunden. Dies gehört zur schnellen Anwendungsentwicklung (Rapid Application Development, RAD) und der vereinfachten Erstellung von Benutzeroberflächen. Die Benutzeroberfläche setzt sich aus einer Reihe von Steuerelementen zusammen, die programmierbare Logik einkapseln. Dieser Ansatztyp gilt als die einzige erfolgreiche, wieder verwendbare Technologie. Auch wenn die vorhergehende Behauptung richtig ist, gibt es einige Abstufungen. Die enge Integration von Elementen und Logik hat zur Folge, dass der Code nicht wieder verwendbar ist. Es ist nicht möglich, Teile der Benutzeroberfläche einfach zu erweitern oder zu entfernen, ohne die Logik oder die Elemente zu beeinträchtigen. Außerdem ist es eigentlich nicht möglich, über die Steuerelemente der Benutzeroberfläche Daten in einem gewünschten Format anzeigen zu lassen. Das heißt, die Benutzeroberfläche muss entweder vollständig abgelehnt oder so hingenommen werden, wie sie ist. Eine der Lösungen ist, einen Thin Client zu erstellen, der nur Elemente der Benutzeroberfläche enthält und nur minimale Benutzeroberflächenlogik. Auch wenn hierdurch das Problem der Nichtwiederverwendbarkeit von Benutzeroberflächen gelöst wird, entsteht durch das Verschieben des gesamten Verarbeitungsaufwands auf den Server ein Nachteil. Größere Server und schnellere und größere Netzwerke sind erforderlich, um den zusätzlichen Verarbeitungsaufwand aufzufangen. Ein optimaler Client sollte nur Elemente der Benutzeroberfläche und Benutzeroberflächenlogik enthalten. Die Elemente der Benutzeroberfläche werden verwendet, um Daten von der Benutzeroberflächenlogik auf den Backendserver zu verschieben. Die Benutzeroberflächenlogik ist die Darstellung von Daten in einem bestimmten Format, das die Konvertierung aus einem anderen Format erfordert. Die Kurvenzeichnung ist ein Beispiel für einen fortgeschritteneren Client und der Byteanzahl nach ein großer Client. Das Generieren von sich drehenden Graphen oder das Durchspielen verschiedener Szenarien erfordert eine beträchtliche Menge an Programmierlogik, und diese gesamte Programmierlogik gilt der Benutzeroberfläche. Die Kurvenzeichnungsroutinen versuchen zu keinem Zeitpunkt, Daten der Ressource zu bearbeiten. Bei den Elementen der Benutzeroberfläche kann es sich um für die Entwicklungsumgebung spezifische Steuerelemente oder Basiselemente handeln, diese Benutzeroberflächenelemente, beispielsweise die Logik, können jedoch keine Daten auf der Serverseite bearbeiten und damit die Architektur umgehen. Wenn die Architektur nicht beachtet wird, stehen der Systemstatus und die Wartungsmöglichkeit auf dem Spiel.
670
In diesem Buch verwendete Muster
Lösung Die Lösung liegt darin, die Benutzeroberflächenelemente dynamisch mit der Benutzeroberflächenlogik zu verknüpfen. Dies ist ein typischer Ansatz für JavaScript, jedoch nicht für Benutzeroberflächenlogik, die innerhalb von Komponenten eingebettet ist. Die Benutzeroberflächenlogik wird durch einen Anwendungscontroller und bei Bedarf auch durch einzelne Anwendungsimplementierungen definiert. Der Zweck des Anwendungscontrollers ist, die Aufgabe der Anwendung in einen generischen Vorgang zu abstrahieren, beispielsweise Kaufen, Verkaufen und Organisieren. Der Controller stellt die Verbindung mit der Skriptschnittstelle dar. Der Controller muss generisch sein, sodass er in einer von der Anwendung festgelegten Situation implementiert werden kann. Die Anwendungsimplementierungen implementieren bestimmte Aufgaben und arbeiten dabei mit den entsprechenden Parametern zum Kaufen von Aktien, Anmelden von Urlaub beim Zeiterfassungssystem usw. Die Implementierungen sind vom Controller abhängig, nicht umgekehrt. Die Implementierungen registrieren sich beim Controller und implementieren eine Schnittstelle, die der Controller erwartet. Sobald sie beim Controller registriert sind, spielen die Implementierungen eine passive Rolle, und der Controller verwaltet ihre Aktionen. Die Benutzeroberflächenelemente werden bei den Komponenten registriert. Auf diese Weise kann die Benutzeroberfläche aktualisiert werden, während noch immer sämtliche Teile der Komponenten verwendet werden. All diese Verknüpfungen basieren auf dem Ansatz des Schichtenmusters. Die Implementierungen sind vom Controller abhängig, und der Controller ist von den Benutzeroberflächenelementen abhängig. In der umgekehrten Richtung besteht jedoch keine Abhängigkeit. Ein Benutzeroberflächenelement benötigt den Anwendungscontroller nicht, und der Anwendungscontroller benötigt keine Implementierung. Auf diese Weise wird der Einsatz neuer Komponenten vereinfacht. Konsequenzen Sprachunabhängigkeit: Die Architektur wird als eine Reihe von Schichten definiert, die mit Komponenten interagieren. Dies bedeutet, dass es möglich ist, jede der Schichten zu aktualisieren, ohne die gesamte Architektur zu beeinträchtigen. Die Komponenten können in einer beliebigen Sprache geschrieben werden. Controllerabhängigkeit: Wenn der Controller ungünstig entworfen wurde und nicht die Geschäftsvorgänge widerspiegelt, ist er bald überholt. Ein guter Controller erzwingt eine generische Definition des Prozesses.
Muster zum Trennen von Format und Programmiercode
671
Verborgene Implementierungsdetails: Auf den einzelnen Ebenen sind die jeweiligen Komponenten nicht dafür zuständig, wie die Implementierung vonstatten geht. Stattdessen beschränken sie sich auf die Kommunikation über die auf jeder Ebene erforderlichen Schnittstellen. Einfach: Der Entwurf trennt das Format der Benutzeroberfläche sauber von der Benutzeroberflächenlogik. Erweiterbar: Die Verknüpfung einer Ebene mit der nächsten wird über einen dynamischen Laufzeitmechanismus erreicht. Es ist daher möglich, sie während der Laufzeit zu aktualisieren, sodass Änderungen einfacher durchzuführen und Aktualisierungen weniger kostenintensiv sind.
672
In diesem Buch verwendete Muster
B Quellcodebeschreibung Nachfolgend werden die Inhalte der verschiedenen Verzeichnisse zum Windows DNA-Quellcode beschrieben, den Sie auf der Begleit-CD-ROM zu diesem Buch finden.
B.1 /ActiveDirectory Kapitel 18 In diesem Verzeichnis befinden sich die Visual Basic- und Visual C++-Beispiele zum Schreiben von Active Directory-Anwendungen mit Hilfe der ADSI-COMSchnittstellen.
B.2 /appConference Kapitel 9, 10, 11 In diesem Verzeichnis befinden sich die verschiedenen Versionen der Konferenzanmeldungsanwendung. Die letzten zwei Buchstaben der Unterverzeichnisnamen geben die Programmiersprache an, die zur Implementierung der Konferenzanmeldungsanwendung eingesetzt wurde.
B.2.1 Unterverzeichnis: /interfaces Dieses Verzeichnis enthält die verschiedenen COM-Schnittstellendefinitionen für die Konferenzanmeldungsanwendung.
B.2.2 Unterverzeichnis: /receiver Dieses Unterverzeichnis enthält die Visual C++-Anwendung zum Testen des asynchronen Empfängers.
B.3 /appMSGermany Kapitel 8, 9, 10 In diesem Verzeichnis befinden sich die verschiedenen Versionen der Webanmeldungsanwendung. Die letzten zwei Buchstaben der Unterverzeichnisnamen geben die Programmiersprache an, die zur Implementierung der Anwendung eingesetzt wurde.
/ActiveDirectory
673
B.3.1 Unterverzeichnis: /interfaces Dieses Verzeichnis enthält die verschiedenen COM-Schnittstellendefinitionen für die Benutzeranmeldungsanwendung.
B.4 /ASPIntegration Kapitel 13 In diesem Verzeichnis sind eine Visual Basic-Anwendung und eine Visual C++-Anwendung erhalten, die zeigen, wie ASP in die jeweilige Sprache integriert werden kann.
B.5 /COMEvents Kapitel 12 In diesem Verzeichnis befinden sich Projekte, die das Schreiben einer COM+-Ereignisanwendung verdeutlichen.
B.6 /common In diesem Verzeichnis sind verschiedene Headerdateien enthalten, die für Visual C++-Entwickler von Interesse sind. Sie werden für die Demos in diesem Buch benötigt. Durch die Headerdateien wird das Schreiben von Unternehmensanwendungen vereinfacht.
B.7 /demos B.7.1 Unterverzeichnis: /cppcomponents In diesem Verzeichnis finden Sie verschiedene Beispiele, die mit der E-Commerce-CD zu Microsoft VC++ vertrieben wurden. Sie werden dem Quellcode beigelegt, da auch Visual C++-E-Commerce-Komponenten geschrieben wurden. Weitere Informationen finden Sie in der Datei readme.doc, die sich ebenfalls in diesem Verzeichnis befindet.
B.7.2 Unterverzeichnis: /DataAccess Kapitel 16 In diesem Verzeichnis sind verschiedene ADO-Beispiele enthalten, die die Verwendung von ADO, das Datenshaping und die Datensatznavigation mit ADO 2.5 verdeutlichen.
674
Quellcodebeschreibung
B.7.3 Unterverzeichnis: /OLEDBExamples Kapitel 17 In diesem Verzeichnis befinden sich verschiedene Verwendungsbeispiele für die OLE DB-Consumer Templates in einer Visual C++-Anwendung.
B.7.4 Unterverzeichnis: /pattern In diesem Verzeichnis sind eine von mir verfasste Anwendung und ein Whitepaper enthalten, mit dem die Musteranwendung bei der Entwicklung von HTMLClientanwendungen aufgezeigt wird.
B.7.5 Unterverzeichnis: /persistence Kapitel 11 In diesem Verzeichnis sind verschiedene Anwendungen enthalten, die das dauerhafte Speichern von COM-Objekten mit Hilfe der MSMQ-COM-Komponenten verdeutlicht.
B.7.6 Unterverzeichnis: /VIWeb Kapitel 4, 5, 6 In diesem Verzeichnis finden Sie verschiedenen HTML-Demos, die die Bearbeitung von ASP, DHTML und XML veranschaulichen.
B.8 /errors Kapitel 9 Dieses Verzeichnis enthält die Visual C++-Komponente zur Fehlerbehandlung, mit der anhand eines generischen COM-Objekts Fehler in einer Protokolldatei aufgezeichnet werden können.
B.9 /IEIntegration Associated chapter: 14 In this directory are the various applications used to demonstrate how the hybrid client application can be built using Visual Basic and Visual C++.
/errors
675
B.10 /interfaces Kapitel 8, 9 In diesem Verzeichnis befinden sich verschiedene Anwendungen, die das Verwenden und Erstellen von COM-Objekten in den Sprachen Visual Basic, Visual C++ und Visual J++ verdeutlicht. Des Weiteren wird demonstriert, wie die verschiedenen IDL-Dateien erstellt werden.
B.11 /queued Kapitel 12 In diesem Verzeichnis finden Sie eine Beispielanwendung zum Schreiben von COM+ Queued Components.
B.12 /SQL Kapitel 15 In diesem Verzeichnis sind verschiedenen SQL-Skripts enthalten, die in der Konferenzanmeldungsanwendung eingesetzt werden.
B.13 /util B.14 Unterverzeichnis: /devaids Kapitel 19 In diesem Verzeichnis finden Sie zwei Beispielanwendungen, mit denen Bugs und Testfälle anhand von Access 97 verfolgt werden können. Die Anwendungen sind von hoher Qualität.
B.15 Unterverzeichnis: /ntservice Kapitel 20 In diesem Verzeichnis befindet sich der CodeGuru Service Wizard, der zum Generieren von Quellcode für einen NT- oder Windows 2000-Dienst verwendet werden kann.
B.16 Unterverzeichnis: /RegInterface Dieses Verzeichnis enthält eine sehr einfache Anwendung, die nach Kompilierung zur Registrierung der Typbibliotheken in der Registrierung eingesetzt werden kann.
676
Quellcodebeschreibung
B.17 Unterverzeichnis: /UML models Gilt für alle Buchkapitel In diesem Verzeichnis befinden sich die verschiedenen UML-Diagramme zum Entwickeln der Konferenzanmeldungsanwendung. Zum Lesen der UML-Diagramme ist Rational Rose erforderlich.
B.18 Unterverzeichnis: /XMLango Dieses Verzeichnis enthält eine zeitlich begrenzte Edition von XMLango, einem umfassenden Framework zur Entwicklung von XML- und webbasierten E-Commerce-Anwendungen.
B.19 /XML Kapitel 14 In diesem Verzeichnis finden Sie die verschiedenen XML-Beispiele zum Entwickeln der Hybridclientanwendung.
Unterverzeichnis: /UML models
677
Index Numerics 16-Bit-Prozessorbefehle 21 24x7-Prozesses 639 256-Farben-Palette 149 32-Bit-Windows 21
A Abschließende Dokumentation von Code und Anwendung 58 Abschnittsübereinstimmung 184 Abstrakt 578 Abstraktion 479 Activate 401 Active Directory 590 Active Directory-Container 578 Active Directory-Objekte 576, 577 Active Server Pages 35, 179 Active Template Library 235 ActiveX-Steuerelement 447 ADC 57, 89 ADO 38, 516, 517, 540, 550, 595, 597 ADO/OLE DB 103 ADO-Objektmodell 517 ADSI 583 Aggregatfunktionen 543 Aggregierbar 400 Akronymdefinition 123 Alias 527 Anonyme Datenänderung 117 Ansichtsklassen 551 Antimuster 54 Antwortwarteschlange 342 Anwendbarkeit 52 Anwendung 128 Anwendungsentwicklungszyklus 57, 89 Anwendungsfälle 79 Anwendungslogik 31, 179, 211, 430 APE 623, 625 APE-Programmfenster 623 Application 180 Application Development Cycle 57, 89 Application Performance Explorer 44, 623 Application Programming Interface 23 Application-Objekt 130 Architektur 215
Wir hoffen, Sie haben interessante Anregungen für Ihre Projekte erhalten.
Windows DNA Business-Applikationen mit Windows 2000, COM+, Visual Studio Christian Gross Galileo Computing 700 S., 2000, geb. 119,90 DM, 61,3 Euro ISBN 3-934358-75-6