Rheinische Friedrich-Wilhelms-Universität Bonn Institut für Informatik III Diplomarbeit Entwurf und Implementierung eines datenbankgestützten Monitoringsystems für diskrete Prozesse Dexin Chen 17. Oktober 2008 Betreuer: Prof. Dr. Rainer. Manthey 1 Danksagung Diese Diplomarbeit entstand am Institut für Informatik der Rheinischen Friedrich-Wilhelms-Universität Bonn unter der Leitung von Herrn Prof. Rainer Manthey. Ganz besonders bedanken möchte ich mich bei Herrn Prof. Manthey für die tatkräftige Unterstützung bei der Erstellung meiner Diplomarbeit. Vielen Dank für die hilfreichen Anregungen und die Engelsgeduld. Zudem möchte ich meiner Frau danken, die mir viele Ratschläge gegeben und mich immer unterstützt hat. Über allem stehen natürlich meine lieben Eltern und meine liebe Schwester, ohne sie wäre dieses Studium nie möglich gewesen. 2 Inhaltverzeichnis 1 Einleitung..................................................................................................................4 2 Grundlagen von Zellularautomaten ..........................................................................7 2.1 Eindimensionale Zellularautomaten .............................................................8 2.2 Zweidimensionale Zellularautomaten...........................................................9 3 Grundlagen von SQL..............................................................................................12 3.1 Datenmanipulation......................................................................................13 3.2 Datendefinition ...........................................................................................18 3.3 JET SQL und MS Access............................................................................19 4 Grundlagen der Softwaretechnologie .....................................................................22 4.1 Java .............................................................................................................22 4.2 Grafikbibliotheken für Java ........................................................................23 4.3 JDBC...........................................................................................................25 4.4 Apache Ant..................................................................................................27 4.5 UML - Unified Modeling Language...........................................................30 4.6 Design Patterns ...........................................................................................33 5 Architektur und Funktionalität von LifeWatch .........................................................36 5.1 Systemarchitektur .......................................................................................36 5.2 Benutzeroberfläche und Funktionalitäten ...................................................37 5.2 Klassenstruktur ...........................................................................................40 5.3 Implementierung der ausgewählten Aspekte ..............................................43 6 Der GoL-Simulator von LifeWatch ........................................................................46 6.1 Initialisierung ..............................................................................................48 6.2 Berechnung der Nachbarzellen der lebendigen Zellen ...............................49 6.3 Berechnung der überlebenden Zellen .........................................................53 6.4 Berechnung der neugeborenen Zellen ........................................................55 6.5 Zustandsübergang .......................................................................................58 7 Mustererkennung und Musterklassifikation in LifeWatch .....................................59 7.1 Mustererkennung ........................................................................................59 7.1.1 Instanzorientierter Algorithmus zur Mustererkennung....................60 7.1.2 Mengenorientierter Algorithmus zur Mustererkennung ..................65 7.1.3 Erweiterte Optimierung ...................................................................68 7.2 Musterklassifikation....................................................................................70 7.2.1 Normalisierung der Koordinaten der Zelle ......................................71 7.2.2 Algorithmus der Klassifikation........................................................72 7.2.3 Implementierung ..............................................................................73 8 Zusammenfassung ..................................................................................................76 3 1 Einleitung Im Allgemeinen versteht man unter einem Prozess einen Ablauf. Nach den Arten der Veränderungen, die während eines Prozesses stattfinden, können Prozesse in zwei Kategorien klassifiziert werden, nämlich diskrete und kontinuierliche Prozesse. Bei kontinuierlichen Prozessen finden die auftretenden Änderungen kontinuierlich statt. Die Start- und Endpunkte solcher Prozesse haben meistens keinen Einfluss auf die Analyse. Bei diskreten Prozessen laufen die Veränderungen in diskreten Schritten ab. In der realen Welt sind die meisten Prozesse kontinuierliche Prozesse. Wenn wir die Veränderung von kontinuierlichen Prozessen im Computer analysieren wollen, müssen wir die kontinuierlichen Prozesse diskretisieren, d.h. mit den diskreten Prozessen die kontinuierlichen Prozesse approximieren. Nach den Art der Erzeugung der Veränderungen werden die Prozesse wieder in zwei Kategorien klassifiziert: sequentielle Prozesse und parallele Prozesse. Bei sequentiellen Prozessen werden die Änderungen nacheinander erzeugt, demgegenüber werden die Änderungen innerhalb von parallelen Prozessen gleichzeitig erzeugt. Monitoring ist eine gezielte Beobachtung, und ein Monitoringsystem ist ein Softwaresystem, das die Veränderungen sowie die Zustände von Prozessen nach bestimmten Monitoringkriterien überwacht und durch spezifische Verfahren in dem Computer analysiert und bearbeitet. Als Anwendungsbeispiel sehen wir ein Wetteranalysesystem: Die Zustände der Atmosphäre, z.B. Luftfeuchtigkeit, Luftdruck, Temperatur usw., werden durch meteorologische Satelliten aufgenommen und in ein Analysesystem abgebildet. Das System analysiert die Informationen und bestimmt das lokale Wetter. Ein Wetteranalysesystem ist ebenfalls ein Monitoringsystem. Ein anderes Beispiel ist ein Verkehrsüberwachungssystem. Das System überwacht die Zustände des Straßenverkehrs, analysiert die aufgenommenen Daten und kann dann bestimmte Fahrzeug erkennen. 4 Nach der Vorstellung obiger Beispiele können wir ein Monitoringsystem in der realen Welt darin gehend zusammenfassen, dass es folgende Eigenschaften hat: ¾ Überwachung der Ereignisprozesse ¾ Erkennung der von den Ereignissen bewirkten Zustandsänderungen ¾ Analyse diese Änderungen In dieser Diplomarbeit wird zuerst ein Simulator für das 1970 von John Conway entworfene „Game Of Life“ (kurz GoL) [Neu96] implementiert. GoL ist in der Lage, komplexe Phänomene wie z.B. die Evolution des Lebens mit einfachen Regeln in Form diskreter Prozesse zu simulieren. GoL ist einfach zu realisieren, erzeugt aber sehr reichhaltige Phänomene. Viele interessante Ereignisse können im Lauf von GoL auftauchen. GoL ist ein gutes Beispiel für diese Arbeit, um darauf ein Monitoringsystem „LifeWatch“ aufzubauen. Der Arbeitsbereich von GoL ist ein so genanntes Spielfeld, das in Zeilen und Spalten unterteilt ist. Jedes Gitterquadrat ist eine Zelle, die einen von zwei Zuständen annehmen kann, welche oft als „lebendig“ und „tot“ bezeichnet werden. Zunächst wird eine Anfangsgeneration von lebendigen Zellen auf dem Spielfeld platziert. Jede lebende oder tote Zelle hat auf diesem Spielfeld genau acht Nachbarzellen, die Zustände aller Zellen in der nächsten Generation sind völlig von den Zuständen ihrer Nachbarzellen abhängig. Bei jeder Generation gibt es manche lebendigen Zellen, die im Spielfeld miteinander benachbart sind und eine bestimmte Figur bilden. Wir nennen solch eine Figur im Kontext dieser Arbeit Muster. Die Muster sind nicht vorhersagbar, sondern können nur durch Analyse der Positionsinformationen aller lebendigen Zellen erkannt werden. Die Aufgabe von „LifeWatch“ ist die Echtzeit-Erkennung von Mustern. Diese Aufgabe wird noch in zwei Teilaufgaben unterteilt: Mustererkennung und Musterklassifikation. Im Spielfeld gibt es viele lebendige Zellen, einige Teilmengen davon können Muster aufbauen, andere nicht. Das Ziel der Mustererkennung ist es, alle auftauchenden Muster bei jeder Generation herauszufinden. Danach kommt die weitere Anforderung: Der Benutzer interessiert sich nur für spezifische Muster. Das ist das Ziel der Musterklassifikation. Sowohl der GoL-Simulator als auch der Monitor können mit einer Programmiersprache wie z.B. Java implementiert werden: Nach Einsetzen der Anfangsbedingungen werden die neuen Zustände von GoL in einer Schleife von einem Java-Programm berechnet. Eigentlich ist die Implementierung von LifeWatch“ ohne Datenbankunterstützung realisierbar. Aber wenn zunächst die einzelnen Spielzustände in einer DB gespeichert werden, dann kann Monitoring in SQL implementiert werden. Die Informationen über die Zellen in GoL, z.B. ihre Zustände und ihre Koordinaten, werden in einer Tabelle in der Datenbank gespeichert. Die Programme greifen auf die Datenbank durch eine Datenbankschnittstelle zu, um die gespeicherten Informationen abzufragen. 5 Der Schwerpunkt dieser Arbeit ist die Anwendung der Datenbanktechnik bei der Softwareentwicklung. Ziel ist, dass es einerseits möglich ist, mithilfe von reinem SQL den GoL-Algorithmus sowie Algorithmen für Mustererkennung und Musterklassifikation zu implementieren, und zwar ohne den Einsatz einer Programmiersprache. Anderseits werden die SQL-Anweisungen im DBMS als View eingebettet, damit keine SQL-Statements in den Programmen existieren. Die Programmiersprache und SQL-Anweisungen werden möglichst getrennt. Solch ein Entwurf hat folgende Vorteile: ¾ Eingebettete SQL-Views sind unter Umständen effizienter als äquivalente Java-Lösung. ¾ Programm-Code ist sauberer und kürzer. ¾ Man kann mit verschiedenen Programmiersprachen das System implementieren, aber die gleiche Datenbank verwenden. Eine grobe Struktur von „LifeWatch“ wird in der folgenden Abbildung dargestellt. Die erste Aufgabe dieser Arbeit ist es, einen Simulator für GoL zu entwickeln. Die Änderungen zu jedem diskreten Zeitpunkt werden in der Datenbank protokolliert. Die Monitorkomponente analysiert synchron die Daten in der Datenbank, um die Muster zu erkennen und zu klassifizieren. Im System „LifeWatch“ wird zudem eine graphische Benutzeroberfläche angeboten, damit der Benutzer das ganze System ansteuern und konfigurieren kann. Diese Arbeit besteht aus acht Kapiteln. In Kapitel 2 wird das System GoL vorgestellt. In Kapitel 3 wird die DB-Sprache SQL in Verbindung mit relationalen Datenbanken eingeführt. Die Grundlagen der Softwaretechnologie, die bei der Entwicklung von „LifeWatch“ angewendet werden, werden in Kapitel 4 eingeführt. Danach werden die Systemarchitektur und die Implementierung der ausgewählten Aspekte in Kapitel 5 erläutert. In Kapitel 6 und 7 wird erläutet, wie der Simulator für GoL aufgebaut und wie die Mustererkennung und Musterklassifikation in „LifeWatch“ implementiert werden. In Kapitel 8 wird die ganze Arbeit zusammengefasst und noch über die Erweiterbarkeit dieser Arbeit diskutiert. 6 2 Grundlagen von Zellularautomaten Im Jahre 1940 wurde der Begriff Zellularautomat von Stanislaw Ulam in Los Alamos vorgestellt [Neu96], um biologische Prozesse wie die Selbst-Reproduktion zu untersuchen. Sein Kollege, John von Neumann, hat diesen Begriff weiterentwickelt, um ein realistischeres Modell für das Verhalten komplexer Systems zu erstellen. Einen Zellularautomat kann man als stilisiertes Universum betrachten: Der Weltraum wird durch Uniform-Gitter repräsentiert, jede Zelle enthält ein paar Daten und die Zeit läuft in diskreten Schritten fort; Die Gesetze des Universums werden durch deterministische Regeln aus dem alten Zustand und den Zuständen der Nachbarn spezifiziert. Zellularautomaten bestehen aus vielen Zellen, jede Zelle kann sich in verschiedenen Zuständen befinden. Aber in jedem diskreten Zeitpunkt besitzt jede Zelle nur einen Zustand. Im Lauf der Zeit verändert jede Zelle ihren Zustand abhängig von den Zuständen im letzten Zeitpunkt ihrer Nachbarzellen mit bestimmten Regeln. Im Gebiet des künstlichen Lebens werden Zellularautomaten als eine Welt von vielen Einzellern betrachtet. Um einen Zellularautomat zu entwerfen, müssen die folgenden Aspekte berücksichtigt werden: ¾ ¾ ¾ ¾ das Raummaß für die Zellen die möglichen Zustände der Zellen die Regeln, wonach die Zellen ihre Zustände verändern die Anfangskonfiguration der Zellen im Zellularautomat Zellularautomaten können eindimensional, zweidimensional, dreidimensional oder mehrdimensional sein. Durch verschiedene Entwürfe haben Zellularautomaten vielfältige Aktivitäten dargeboten, davon phantastische Phänomene aus der Natur, z.B. wie die Linienmuster auf Muschelschalen aufgetaucht sind. Außerdem ist die Entwicklung der Gitter sehr ähnlich wie das Leben in der realen Welt: Die Zellen in den Automaten haben auch das Verhalten wie Bewegung, Aufwachsen, Sterben und Klo7 nen. Zellularautomaten haben folgende Eigenschaften: ¾ ¾ ¾ ¾ ¾ ¾ diskrete Zustände diskreter Zeitschritt räumlich lokal zeitlich lokal homogen parallele Berechnung Diskrete Zustände und diskreter Zeitschritt bedeuten, dass die Zustände der Zellen endlich sind; zeitlich und räumlich lokal bedeuten, dass der Zustand einer Zelle im nächsten Zeitpunkt nur von den Zuständen ihrer Nachbarn abhängt; homogen bedeutet, dass alle Zellen sich nach gleichen Regelungen entwickeln; parallele Berechnung bedeutet, dass alle Zellen sich gleichzeitig und synchronisiert verändern. 2.1 Eindimensionale Zellularautomaten Seit 1980 hat Stephen Wolfram von der Princeton Universität einfachere eindimensionale Zellularautomaten entwickelt. In solchen Automaten leben die Zellen wie nachfolgende dargestellt in einer Zeile. Jede Zelle hat also nur zwei Links- sowie Rechts-Nachbarn; die Zellen verändern sich nach spezifischen Regeln und befinden sich in einem von endlichen Zuständen, im einfachsten Fälle sind nur zwei Zustände vorhanden: lebendig oder tot. Die nächste Generation der Zelle wird genau darunter angezeigt. Abbildung 2.1 eindimensionale Zellularautomaten Es gibt viele Regeln für eindimensionale Zellularautomaten, hier wird die Regel als so genannte „Pascal’s Triangle Rule“ vorgestellt. Die Regel „Pascal’s Triangle Rule“ ist relativ einfach: Der Zustand der Zelle im nächsten Zeitpunkt ist nur abhängig von den Zuständen ihrer Links- und Rechts-Nachbarn und den Zuständen; jede Zelle hat in jedem Zeitpunkt nur eine von zwei Zuständen: lebendig oder tot. Wir bezeichnen den Zustand „lebendig“ mit 1 und „tot“ mit 0. Die Übergangsbedingungen werden in der Tabelle zusammengefasst (siehe Abbildung 2.2) 8 Abbildung 2.2: Pascal’s Triangle Rule Wir betrachten nur die rote Zelle aus der Zeile „aktuelle Zeitpunkt“. Die Zeile „nächster Zeitpunkt“ gibt ihren Zustand im nächsten Zeitpunkt nach ihren Umgebungszuständen aus. Offensichtlich gibt es insgesamt acht verschiedene Fälle, die Regel „Pascal’s Triangle Rule“ definiert die Übergangszustände für die acht Fälle (von 000 bis 111). Die nach „Pascal’s Triangle Rule“ erzeugte Graphik wird als „Pascal’s Triangle“ benannt, die Graphik wird immer ständig selbst reproduziert. Die Abbildung 2.3 gibt eine „Pascal’ Triangle“-Darstellung wieder. Abbildung 2.3: Pascal’s Triangle [Wol82] Außer der Regel „Pascal’s Triangle Rule“ gibt es noch viele weitere Regeln für solche eindimensionale Automaten, also insgesamt 256 Regeln, von 00000000 bis 11111111. Bislang haben wir nur einfachste eindimensionale Zellularautomaten diskutiert: jede Zelle hat nur zwei Zustände; nur drei aufeinander folgende Zellen werden berücksichtigt. Wenn es mehr als zwei Zustände gibt und der Zustand der Zelle abhängig von mehreren Nachbarn ist, werden die Zellularautomaten entsprechend komplizierter sein. 2.2 Zweidimensionale Zellularautomaten Der bekannteste zweidimensionale Zellularautomat heißt „Game Of Life“ (kurz GoL), der im Jahr 1970 von John Conway, dem Mathematiker an der Universität 9 Cambridge, vorgestellt wurde [Wol82]. Die Idee basiert auf folgendem Konzept: Gibt es Beschränkungen, wenn eine Menge von Zellen unter bestimmten Bedingungen aufwächst? John fand heraus, dass die Zellen nicht unbeschränkt aufwachsen können und er spezifizierte die Regeln, darunter wie die Zellen aufwachsen und sterben. Mit diesem Konzept hat John Conway einen einfacheren Zellularautomat entworfen. Eine Ebene wird in viele gleichartige Zellen aufgesplittert wie ein Brett, jede Zelle besitzt acht Nachbarn und zwei Zustände: lebendig oder tot. Die Regeln sind: ¾ Für eine lebende Zelle: ¾ Wenn sie keinen oder einen lebenden Nachbar hat, wird sie im nächsten Zeitpunkt sterben wegen Einsamkeit. ¾ Wenn sie zwei oder drei lebende Nachbarn hat, wird sie im nächsten Zeitpunkt noch lebendig bleiben. ¾ Wenn sie mehr als drei Nachbarn hat, wird sie im nächsten Zeitpunkt sterben wegen Überbevölkerung. ¾ Für eine tote Zelle ¾ Wenn sie genau drei lebende Nachbarn hat, wird sie im nächsten Zeitpunkt lebendig Als Conway sein „Game Of Life“ vorgestellte hat, sorgte dies sofort für Furore. Viele Mathematiker und Informatiker waren begeistert von ihm. Niemand hatte gedachtet, dass ein so komplexes Phänomen wie die Evolution des Lebens mit so einfachen Regeln simuliert werden könnte. Auf dem Spielfeld zeigt sich mit jedem Generationsschritt eine Vielfalt komplexer Strukturen. Einige typische Objekte lassen sich aufgrund eventuell vorhandener besonderer Eigenschaften in Klassen einteilen: ¾ Statische Objekte: Die Strukturen der statischen Objekten werden nie verändert während des Iterationsprozesses. Abbildung 2.4: Statische Objekte ¾ Periodische Objekte: Die Objekte verändern sich nach einem bestimmten Schema periodisch, d.h. nach einer endlichen, festen Anzahl von Generationen wird wieder der Ausgangszustand erreicht, wie z.B. „Blinker“ und „Unruhe“. 10 Abbildung 2.5: Periodische Objekte ¾ Erweiterte periodische Objekte: Zu dieser Klasse gehören solche oszillierenden Objekte, die eine feste Wegstrecke zurücklegen. Sie sind ein Beispiel für die Emergenz-Erscheinungen des Spiels des Lebens; die wenigen Regeln des Spiels sagen nichts über Formen aus, die sich unendlich weit fortbewegen, und doch entstehen die Objekte wegen dieser Regeln. Ein Beispiel ist der so genannte „Gleiter“: Abbildung 2.6: Gleiter Die oben erläuterten Objekte im „Game Of Life“ spielen eine besondere Rolle in dieser Arbeit. Sie haben eine stabile Struktur, wir nennen solche Objekte in dieser Arbeit Muster. Im nächsten Abschnitt werden wir den Begriff „Muster“ im Kontext unseres Monitoringsystems definieren und anschließend die Aufgaben des „LifeWatch“ spezifizieren. 11 3 Grundlagen von SQL In diesem Kapitel wird die Datenbanksprache SQL eingeführt. Die Darstellung in diesem Kapitel basiert aus den Quellen [Man04], [Vos00], [Kem06] und [Mol06]. Ein Datenbanksystem (DBS) ist ein System zur elektronischen Datenverwaltung. Die wesentliche Aufgabe eines DBS ist es, große Datenmengen effizient, widerspruchsfrei und dauerhaft zu speichern und benötigte Teilmengen in unterschiedlichen, bedarfsgerechten Darstellungsformen für Benutzer und Anwendungsprogramme bereitzustellen. Ein DBS besteht aus zwei Teilen: Datenbankmanagementsystem (DBMS) und Datenbanken (DB). Das Datenbankmanagementsystem (DBMS) ist die eingesetzte Software, die für das Datenbanksystem installiert und konfiguriert wird. Das DBMS legt das Datenbankmodell fest, hat einen Großteil der unten angeführten Anforderungen zu sichern und entscheidet maßgeblich über Funktionalität und Geschwindigkeit des Systems. Datenbankmanagementsysteme selbst sind hochkomplexe Softwaresysteme. RDBMS ist die relationale DBMS-Variante. Sie ist der heute vorherrschende Datenbanktyp. In der Theorie versteht man unter einer Datenbank einen logisch zusammengehörigen Datenbestand. Dieser Datenbestand wird von einem laufenden DBMS verwaltet und für Anwendungssysteme und Benutzer unsichtbar auf nichtflüchtigen Speichermedien abgelegt. Datenbanksprachen dienen dem Zugriff auf die Bestände und Strukturen einer Datenbank. Sie ermöglichen in solchen Beständen das Suchen, Löschen, Verändern und Einfügen von Daten in Tabellen (SELECT, DELETE, UPDATE und INSERT) bzw. die Definition, Änderung und Elimination der Tabellen selbst (CREATE, ALTER, DROP). Die in Klammern stehenden Begriffe entstammen der prominentesten unter diesen Sprachen, nämlich SQL, die in nahezu allen RDBMS üblich ist. 12 SQL (Structured Query Language) dient der Kommunikation mit relationalen Datenbanksystemen im direkten Dialog, also interaktiv (einfache, sparsame Oberflächengestaltung) oder eingebettet in einer Programmiersprache (Java, C/C++, Cobol etc.). SQL ist eine nicht prozedurale Sprache, d.h. durch ein SQL-Statement (Befehl) wird festgelegt, was man haben möchte, und nicht, wie man zum Ergebnis gelangen soll. Die Syntax von SQL ist relativ einfach aufgebaut und semantisch an die englische Umgangssprache angelehnt. SQL stellt eine Reihe von Befehlen zur Definition von Datenstrukturen nach der relationalen Algebra, zur Manipulation von Datenbeständen (Einfügen, Bearbeiten und Löschen von Datensätzen) und zur Abfrage von Daten zur Verfügung. Durch seine Rolle als Quasi-Standard ist SQL von großer Bedeutung, da eine weitgehende Unabhängigkeit von der benutzten Software erzielt werden kann. SQL-Befehle lassen sich in zwei Hauptkategorien unterteilen: ¾ DDL(Data Definition Language) ¾ DML(Data Manipulation Language) Die DDL enthält Befehlsformate, mit denen man Datenbankschemata definieren und manipulieren kann [4], die DML dagegen bietet Befehle zum Formulieren von Anfragen an und Änderungen von Datenbankzuständen. 3.1 Datenmanipulation Die SQL-Anweisungen zur Manipulation der Daten in einer Datenbank sind ¾ SELECT: aus einer oder mehreren Tabellen eine neue Tabelle zusammenstellen ¾ INSERT: Einfügen von neuen Zeilen in eine Tabelle ¾ UPDATE: Ändern von Zeilen in einer Tabelle ¾ DELETE: Löschen einer oder mehrerer Zeilen in einer Tabelle SELECT Zwei Möglichkeiten, Tabellen zu bearbeiten, bieten sich unmittelbar an, nämlich das zeilenweise und das spaltenweise Auswählen von Werten. Die entsprechenden Operationen auf einer Tabelle heißen Projektion für die Spaltenwahl und Selektion für die Auswahl von Zeilen. Beide wirken jeweils auf eine Tabelle, sind also so genannte unäre oder monadische Operationen. In der Datenbanksprache SQL hat diese Operation den Namen SELECT. Mit der SELECT-Anweisung können Tabellen – reale und virtuelle – zu neuen Tabellen zusammengestellt werden. Dabei sind die verwendeten Tabellen das Rohmaterial, aus dem Zeilen und Spalten ausgewählt oder neue, virtuelle Spalten berechnet werden können. Die SELECT-Anweisung hat die folgende Syntax: 13 SELECT [ALL | DISTINCT] spaltenListe FROM TabellenListe [WHERE bedingungsAusdruck] [GROUP BY spaltenListe HAVING bedingungsAusdruck] ] [ORDER BY spaltenListe] Hier und im Folgenden bedeuten […] kann weggelassen werden; ….|…. Alternative, d.h. entweder der linksseitige oder der rechtsseitige Teil wird verwendet. Werden zwei (oder mehr) Tabellen in der FROM-Klausel angegeben, so wird aus diesem das kartesische oder Kreuzprodukt gebildet, d.h. jede Zeile der einen Tabelle wird mit jeder Zeile der anderen kombiniert. Zu sinnvollen Ergebnissen kommt man allerdings erst durch Hinzunahme der WHERE-Klausel, also durch Untermengenbildung bzw. Selektion. SELECT kann als Postfix ALL oder DISTINCT haben: ¾ DISTINCT heißt, dass bei gleichlautenden Zeilen nur eine in die neue Tabelle aufgenommen wird, und ¾ ALL heißt, dass alle Zeilen berücksichtigt werden, es also zu Wiederholungen kommen kann. ALL ist als Standardeinstellung wirksam für den Fall, dass es als Postfix weggelassen wird. Mit so genanten Klauseln können Regeln und Einschränkungen für SQL-Ausdrücke festgelegt werden. So werden mit der WHERE-Klausel Zeilen aus der Ergebnistabelle von SELECT…FROM… ausgesondert, d.h. WHERE repräsentiert die Selektionsoperation: … WHERE bedingungsAusdruck Bedingungsausdrücke sind von booleschem Typ und entsprechend entweder „wahr“ oder „falsch“. In den beiden folgenden Beispielen finden sich einfache Bedingungsausdrücke für die Tabellen „Teilnehmer“ und „Dozenten“ (‚M%’ bezeichnet alles, was mit dem Buchstaben M beginnt): SELECT * FROM Teilnehmer WHERE dID=2 SELECT * FROM Dozenten WHERE nachname LIKE ‘M%’ 14 Aggregatfunktionen in SQL sind z.B.: ¾ COUNT(spalte): Zählt die Zeilen in der Spalte „spalte“ ab, nachdem Dubletten optional entfernt wurden ¾ COUNT(*): Zählt alle Zeilen ab; eventuelle Dubletten sind nicht entfernbar ¾ SUM(spalte): Spaltensumme nach optionaler Entfernung von Dubletten ¾ MIN(spalte): Bestimmt den kleinsten Wert in der Spalte „spalte“ Aggregatfunktionen können nur in der SELECT- und in der HAVING-Klausel verwendet werden. Von besonderem Interesse ist ihr Zusammenspiel mit der GROUP BY-Klausel. Wenn eine SELECT-Anweisung keine GROUP BY-Klausel hat, so wirken die Aggregatfunktionen auf alle Zeilen einer Tabelle. Als Beispiel diene die nun um eine Spalte „typ“ und „zeit“ erweiterte Tabelle „Kurse“. Die Spalte „typ“ kennzeichnet die Veranstaltungstypen der „Kurse“ (S für Seminar, V für Vorlesung, P für Praktikum), gliedert die Kurse also in Gruppen. Ähnliches leistet dID, die Kennung für die veranstaltenden Dozenten. Abbildung 3.3 die Tabelle „Kurse“ Eine Tabelle, die nur noch die Gliederungsbegriffe selbst beinhaltet (bei „typ“ die unterschiedlichen Buchstaben), erhält man mit der folgenden SQL-Anweisung: SELECT * DISTINCT typ FROM Kurse Das Ergebnis ist die von Dubletten bereinigte einspaltige Tabelle: 15 Statt der Verwendung von DISTINCT nach der SELECT-Klausel kann man auch die GROUP BY-Klausel verwenden, das Ergebnis ist das gleiche: SELECT typ FROM Kurse GROUP BY typ Die zweite Form hat gegenüber der ersten den Vorzug, dass beispielsweise die verschiedenen Gruppen abgezählt werden können oder dass in den Gruppen einzeln summiert werden kann. Ähnlich wie die Zeilen einer Tabelle als Kombination von Spaltenwerten darstellbar sind, kann das Resultat von einer JOIN-Operation als Kombination von Tabellenzeilen aller beteiligten Tabellen beschrieben werden. Die Kombination wird durch Selektion gesteuert, also durch Abhängigkeiten von Spalten der beteiligten Tabellen untereinander, beispielsweise über die WHERE-Klausel. In der überwiegenden Zahl der Fälle werden Tabellen über gleiche Spaltenwerte zusammengefügt. Das Zusammenfügen über gleiche Spaltenwerte ist zwar die häufigste, aber nicht die einzige Art, Tabellen zu verknüpfen. Spaltenwerte können auch über beliebige andere Bedingungsoperatoren zueinander in Beziehung gesetzt werden. Hier werden noch zwei JOIN-Operatoren INNER JOIN und OUTER JOIN vorgestellt. OUTER JOIN ist eine binäre Operation, mittels derer alle Zeilen der einen Tabelle mit einer passenden Auswahl von Zeilen der anderen Tabelle verbunden werden. Dabei kann es geschehen, dass zu Tabellenzeilen keine Entsprechungen in der anderen Tabelle existieren, also Lücken entstehen. Solche Lücken werden in der Resultattabelle mit Nullwerten aufgefüllt. Nullwerte zeigen an, dass ein Wert fehlt. Die Syntax ist: …linkeTabelle (LEFT|RIGHT) [OUTER] JOIN rechteTabelle ON bedingung Mit OUTER kann optional ein OUTER JOIN deutlich symbolisiert werden, ohne irgendwelche sonstigen Wirkungen zu haben. LEFT und RIGHT geben an, welche der Tabellen als Ganzes verwendet wird, nämlich die von der linken Seite des Operators bei LEFT und die von der rechten Seite bei RIGHT. Im folgenden Beispiel SELECT * FROM Dozenten LEFT JOIN Kurse ON Kurse.dID=Dozenten.dID besteht das Ergebnis also aus allen Zeilen der links stehenden Tabelle „Dozenten“ verbunden nur mit den Zeilen der rechts stehenden Tabelle „Kurse“, für die die Bedingung in der ON-Klausel zutrifft. Im Folgenden ist die Ausgabe der Anweisung: 16 Abbildung 3.7: Operator LEFT JOIN Der INNER JOIN führt Datensätze aus der linken und rechten Tabelle genau dann zusammen, wenn die angegebenen Kriterien alle erfüllt sind. Ist eines oder sind mehrere der Kriterien nicht erfüllt, so entsteht kein Datensatz in der Ergebnismenge. INSERT Neue Zeilen können in einer Tabelle mit der INSERT-Anweisung eingefügt werden. Dabei sind zwei Varianten anwendbar: ¾ INSERT INTO … VALUES … : In der Zieltabelle wird eine neue Zeile eingefügt und direkt mit den Werten einer Werteliste versehen. ¾ INSERT INTO … SELECT… : Mit SELECT ausgewählte Zeilen aus einer anderen Tabelle werden als neue Zeilen in die Zieltabelle importiert. In die gewählte Tabelle werden die Werte der Liste werteListe eingefügt, bei Weglassen der Spaltenliste in der Reihenfolge der Spalten in der Tabelle „Tabelle“, sofern keine entsprechende Spaltenliste angegeben wurde. Die Spaltenreihenfolge ist nach Standard optional, bei einigen wenigen DBS muss sie aber angegeben werden. Mit SELECT ausgewählte Zeilen aus einer anderen Tabelle werden als neue Zeilen in die Zieltabelle nach INTO aufgenommen. INSERT INTO Tabelle [(spaltenListe)] SELECT … Im Beispiel werden alle Dozenten, deren Nachname mit dem Buchstaben M beginnt, in die Tabelle „Personen“ eingefügt: INSERT INTO personen (vorname, nachname) SELECT vorname, nachname, FROM Dozenten WHERE nachname LIKE “M%” 17 DELETE Mit der DELETE-Anweisung können Zeilen aus Tabellen gelöscht werden. DELETE ist eine Operation, die auf eine Zeilenmenge wirkt. Die Menge der zu löschenden Zeilen wird mittels der WHERE-Klausel in der DELETE-Anweisung festgelegt. DELETE FROM Dozenten WHERE dID=2; UPDATE Die UPDATE-Anweisung erlaubt, Werte in den Spalten einer Tabelle zu verändern. Wie DELETE ist auch UPDATE eine Operation, die auf Zeilenmengen einwirkt, d.h. die einschränkende Anwendung der WHERE-Klausel ist wie bei DELETE von existentieller Wichtigkeit. Nach Angabe der Zieltabelle folgt auf SET eine Wertezuweisungsliste der Form spaltenName1=werte1, spaltenName2=wert2.etc. Wird WHERE weggelassen, so werden alle aufgefüllten Spalten auf einen gleich bleibenden Wert gesetzt. Sonst werden nur diejenigen Zeilen in den Spalten geändert, für die die Bedingung in der WHERE-Klausel den Wert „true“ hat. 3.2 Datendefinition Mit den SQL-Anweisungen für die Datendefinition werden insbesondere Tabellen und Tabellenstrukturen erzeugt, geändert und ggf. wieder vernichtet. Die wichtigsten Anweisungen zu diesen Zwecken sind: ¾ CREATE TABLE: Eine neue Tabelle anlegen ¾ ALTER TABLE: Die Struktur einer bestehenden Tabelle verändern ¾ DROP TABLE: Eine Tabelle löschen Da das Anlegen von Tabellen bzw. Tabellenstrukturen und deren Pflege selten mit JDBC und meist mit anderen Mitteln erfolgt, sollen die entsprechenden SQL-Anweisungen nur kurz und ausschließlich anhand einfacher Beispiele erläutert werden. CREATE TABLE Eine Tabelle in einer Datenbank definieren: CREATE TABLE Dozenten (dID INTEGER, vorname CHAR(25), nachname CHAR(25)) 18 ALTER TABLE Die Struktur einer Tabelle kann durch Hinzufügen neuer und durch Änderung des Typs bestehender Spalten manipuliert werden: ¾ Eine neue Spalte in einer bestehenden Tabelle erzeugen ALTER TABLE Dozenten ADD testSpalte INTEGER ¾ Den Typ der Spalte ändern ALTER TABLE Dozenten MODIFY testSpalte FLOAT NOT NULL ¾ Die Spalte aus der Tabelle entfernen ALTER TABLE Dozenten DELETE testSpalte DROP TABLE Eine Tabelle wird vollständig aus einer Datenbank entfernt mit DROP TABLE Dozenten 3.3 JET SQL und MS Access Man muss hier beachten, dass es viele verschiedene Versionen von SQL gibt. Um die Standard SQL’92 anzupassen, müssen alle Version die wichtigsten Anweisungen (wie z.B. SELECT, UPDATE, DELETE, INSERT usw.) unterstützen. Die Version von Microsoft heißt Jet-SQL, es ist die Datenbank-Engine hinter Microsoft Access. Microsoft Access ist ein Datenbankmanagementsystem der Firma Microsoft zur Verwaltung von Daten in Datenbanken und zur Entwicklung von Datenbankanwendungen [Min05]. MS Access ist Bestandteil des Office-Professional-Pakets und unterstützt SQL-92. MS Access ist ein relationales Datenbanksystem. In dieser Software ist es außerordentlich gelungen, die Vorzüge einer typischen MS Windows-Anwendung und die Leistungsfähigkeit einer professionellen Datenbanksoftware miteinander zu verbinden. Eine Accessdatenbank besteht sowohl aus den Daten als auch aus den für die Bearbeitung, Ansicht oder Manipulation der Daten notwendigen Werkzeugen, wie Abfragen, Formularen, Berichten, Makros und Modulen. Konsequenterweise wird darum die gesamte Datenbank in einer einzigen Datei gespeichert. Die Zielgruppe für diese 19 Anwendung erstreckt sich vom „blutigen“ Anfänger bis hin zum erfahrenen Datenbankprofi. Ein hervorstechendes Merkmal von MS Access ist eine große Leistungsfähigkeit bei gleichzeitig sehr einfacher Bedienung. Eine MS Access-Datenbank ist modular aufgebaut und setzt sich aus 6 Bestandteilen zusammen (Tabellen, Abfragen, Formulare, Berichte, Makros, Module). Jede dieser Gruppen kann in mehreren Ansichten betrachtet werden. ¾ Die Entwurfsansicht dient der Erstellung von Datenbankobjekten. ¾ Die Datenblattansicht dient dazu sich die selektierten Daten anzuschauen und auf ihre Richtigkeit zu überprüfen (nicht bei Makros und Modulen) ¾ Die SQL-Ansicht (nur bei Abfragen) ermöglicht die Eingabe von SQL-Statements. ¾ Die Formular- bzw. Berichtsansicht (wie der Name sagt nur bei Formularen und Berichten) ermöglicht die Überprüfung, ob das Layout eines Formulars oder Berichtes gelungen ist. Abfragen: Mittels der Abfragen kann man sehr schnell per Drag & Drop Daten für eine Auswertung zusammenstellen, gruppieren und sortieren. Hier gibt es zwei Möglichkeiten sich die Daten zusammenzustellen, nämlich: ¾ in der QBE (query by example) Ansicht, in der man sich die Abfrage mehr oder weniger zusammenklickt und dann entsprechende Kriterien definiert, welche Daten man angezeigt bekommen möchte, ¾ in der SQL- (structured query language)-Ansicht. Die Eingabe von Statements (Befehle, welche Daten selektiert werden sollen) gleichen einer Programmiersprache, ermöglichen aber komplexere Abfragen. Bei einfacheren Abfragen ist die Eingabe über diese Ansicht wegen der ganzen Tipperei recht mühsam. Ist die Abfrage fertig geschrieben, kann diese durch einen einfachen Mausklick ausgeführt werden, können die Ergebnisse auf ihre Richtigkeit geprüft werden und falls notwendig durch einen weiteren Mausklick, der wiederum zur Entwurfsansicht führt, ggf. modifiziert werden. Die Vorteile von MS Access liegen in der Einfachheit und der großen Flexibilität beim Erstellen von Datenbankanwendungen und der vorhandenen Möglichkeit, die anderen Office-Anwendungen aus dem Hause Microsoft problemlos an die Datenbankumgebung anzubinden. Es lassen sich beispielsweise recht problemlos Schnittstellenkonzepte für andere Plattformen realisieren (Großrechner, SAP). 20 MS Access ist erheblich besser als sein Ruf. Zu empfehlen ist diese Datenbankplattform für Privatanwender bis hin zu Unternehmen, da das Produkt vom Konzept her überzeugt und für gängige Anwendungsprofile vollkommen ausreicht. MS Access ist vom Preis- Leistungs-Verhältnis her wohl kaum zu schlagen. Ein weiterer großer Vorteil dürfte wohl die weite Verbreitung der Office-Produkte und die damit ebenso großen Einsatzgebiete sein, ohne auf eine zusätzliche Fremdsoftware zurückgreifen zu müssen. 21 4 Grundlagen der Softwaretechnologie In diesem Kapitel wird in einige Themen der Softwaretechnologie eingeführt, die bei der Entwicklung des Monitoringsystem „LifeWatch“ verwendet werden. 4.1 Java Bei der Implementierung von „LifeWatch“ wird die Programmiersprache Java verwendet. Die Sprache Java gehört zu den objektorientierten Programmiersprachen [Eck06]. Die Objektorientierung, kurz OO, ist ein Ansatz zur Entwicklung von Software, der darauf beruht, die zu verarbeitenden Daten anhand ihrer Eigenschaften und der möglichen Operationen zu klassifizieren. Die Absicht dahinter ist, große Softwareprojekte einfacher verwalten zu können und die Qualität der Software zu erhöhen. Ein weiteres Ziel der Objektorientierung ist ein hoher Grad der Wiederverwendbarkeit von Softwaremodulen. Eine OO-Sprache soll folgenden Eigenschaften charakterisieren: ¾ Kapselung: Objekte sind die Einheiten von Daten und Code. Durch Kapselung werden die Daten und der Code von Objekten versteckt. Die Sprache stellt sicher, dass der Zustand eines Objektes nur über die in seinem Interface spezifizierten Operationen manipuliert wird. ¾ Vererbung: Wenn eine neue Klasse durch Vererbung erzeugt wird, besitzt sie alle Daten und Operationen ihrer Vaterklasse. Damit wird die Wiederverwendbarkeit der Klasse verstärkt wird. ¾ Polymorphismus: Die Verwendung des gleichen Namens für unterschiedliche Operationen, die auf Objekten verschiedener Klassen auszuführen sind . Java-Programme werden in Bytecode übersetzt und dann in einer speziellen Umgebung ausgeführt, die als Java-Laufzeitumgebung (JRE) oder Java-Plattform bezeichnet wird. Der wichtige Teil davon ist Java Virtual Machine (JVM), die für die Aus22 führung des Java-Bytecodes verantwortlich ist. Hierbei wird im Normalfall jedes gestartete Java-Programm in seiner eigenen virtuellen Maschine ausgeführt. Die JVM dient dabei als Schnittstelle zur Machine und zum Betriebssystem und ist für die meisten Plattformen verfügbar, z.B. Linux, Mac, Solaris, Windows usw. Java-Programme laufen in der Regel ohne weitere Anpassung auf verschiedenen Betriebssystemen, für die eine JVM installiert wird, d.h. ein Java-Programm ist plattformunabhängig. Eclipse [Kün07] ist ein Open-Source-Framework zur Entwicklung von Software nahezu aller Art. Die bekannteste Verwendung ist die Nutzung als Entwicklungsumgebung (IDE) für die Programmiersprache Java. Eclipse selbst basiert auf Java-Technologie, und ist der Nachfolger von IBM Visual Age for Java 4.0. Der Quellcode für Eclipse wurde am 7.November 2001 von IBM freigegeben. Eclipse wird auch für die Entwicklung von Rich-Client-Application auf Basis der Eclipse Rich Client Plattform (RCP) zunehmend häufiger eingesetzt. D.h. Eclipse ist nicht auf Java festgelegt und wird aufgrund seiner offenen Plug-in-basierten Struktur mittlerweile für sehr unterschiedliche Entwicklungsaufgaben eingesetzt. 4.2 Grafikbibliotheken für Java Java bietet dem Entwickler ein Grafikbibliothek-Swing an, um grafische Benutzeroberflächen zu programmieren. Swing gehört zu den Java Foundation Classes (JFC), die eine Sammlung von Bibliotheken zur Programmierung von grafischen Benutzerschnittstellen bereitstellen. Zu diesen Bibliotheken gehören Java2D, das Accessibility-API, das Drag & Drop-API und das Abstract Window Toolkit (SWT). Swing baut auf dem älteren AWT auf und ist mit den anderen APIs verwoben. Eine Alternative zu Swing ist SWT, welches im Jahr 2001 von IBM für die Entwicklungsumgebung Eclipse entwickelt wurde [Dao07]. SWT nutzt dabei im Gegensatz zu Swing die nativen grafischen Elemente des Betriebssystems – wie das AWT von Sun – und ermöglicht somit die Erstellung von Programmen, die eine Optik vergleichbar mit „nativen“ Programmen aufweisen. Allerdings leidet SWT auf einigen Nicht-Windows-Plattformen unter Effizienzproblemen, da es viele Features eines Basistoolkits voraussetzt, welche - wenn nicht vorhanden – emuliert werden müssen. Zudem sind die SWT-Bibliotheken nicht standardmäßig auf dem ausführenden System verfügbar und müssen mit der Applikation ausgeliefert werden, während Swing Bestandteil der Java-Laufzeitumgebung (JRE) ist. Ein großes Problem seit der Entstehung von GUI-Toolkits ist, dass diese nicht wirklich mit mehreren Threads zusammenarbeiten können. Dies ist dadurch begründet, dass ein Thread, der auf eine GUI-Komponente zugreifen möchte, einen Lock auf diese benötigen würde. Das wäre bei mehreren Threads erforderlich, da sonst die GUI nicht mit den jeweils aktuellen Daten aufgebaut würde und die Inhalte und die Dar23 stellung der GUI nicht vorhersehbar wären. Doch das Setzen von Locks hat zwei große Nachteile. Zum Einen dauert das ständige Setzen und Aufheben von Locks sehr lange. Zum Anderen würde die Nutzung mehrerer Threads durch die Locks unweigerlich zu einem so genannten Deadlock führen. Denn die Komponenten werden zwar vom Java-Programm gesteuert, aber sie erhalten vom Betriebssystem ihre Eingaben. Deshalb würde es bei mehreren Threads nicht nur Locks aus der Schicht geben, in der das Java-Programm läuft, sondern auch aus der Systemschicht. Aus diesen beiden Gründen ist Swing aber auch SWT nicht threadsicher. Beide GUI-Toolkits sollten deshalb so verwendet werden, dass die gesamte Steuerung für die GUI in einem eigenen Thread abläuft. Dieser Thread heißt bei Swing „Event-Dispatch-Thread“. Ein Vorteil dieses Verfahrens liegt auch darin, dass die Benutzeroberfläche bei rechenintensiven Operationen des Programms nicht einfrieren kann. Sowohl Swing als auch SWT sind leistungsstarke Toolkits, deren Geschwindigkeitsunterschied bei Java-Applikationen auf modernen Computern kaum auffällt. Dennoch gibt es signifikante Unterschiede in den einzelnen Schichten der Toolkits, die bei einer Verwendung berücksichtigt werden sollten. Da Swing sämtliche Komponenten emuliert, benötigt es mehr Systemressourcen als SWT, um die Komponenten im Arbeitsspeicher abzulegen. Dafür ist es schneller, wenn Daten zwischen den GUI-Komponenten und Java ausgetauscht werden müssen, da sämtliche Daten abrufbereit vorliegen. SWT baut eine direkte Verbindung zu den Komponenten des Systems auf, was es beim Datentransfer zwischen GUI-Komponenten und Java langsamer macht. Dies hat vor allem auch in der intensiven Nutzung der JNI (Java Native Interface) seinen Grund. Allerdings ist SWT dafür wesentlich schneller, wenn die Daten vorliegen und die GUI-Komponenten gerendert werden sollen. Diese enge Bindung wurde allerdings ursprünglich auf Microsoft Windows konzipiert, was SWT auf anderen Plattformen langsamer machen kann. Durch die Nutzung der Komponenten des Betriebssystems benötigt SWT weniger Arbeitsspeicher und ist deshalb bei älteren Computern vorzuziehen. In SWT sind die Programmierer plattformspezifischen Bugs ausgesetzt, da jede Plattform in der Bereitstellung und Verarbeitung der Komponenten Fehler enthalten kann, die in anderen Plattformen nicht vorkommen. Dies kann die Entwicklungszeit verlängern, da SWT-Applikationen auf sämtlichen Plattformen getestet werden müssen. Da die SWT-Implementation auf jeder Plattform unterschiedlich ist, müssen SWT-Applikationen spezielle JAR-Daten für jede Plattform beigefügt werden. 24 4.3 JDBC JDBC (Java Database Connectivity) ist eine von SUN entwickelte Datenbankschnittstelle, die auf den Java-Basisklassen aufsetzt und über die Bereitstellung von relationalen Datenbankobjekten sowie der entsprechenden Methoden den Zugriff aus Java-Applikationen auf beliebige Datenbanken ermöglicht [Deh03]. Der Ablauf einer Datenbankanwendung mithilfe von JDBC sieht für eine normale Folge von Abfragen folgendermaßen aus: Durch die zentrale Klasse JDBC-Driver-Manager werden eine oder mehre Verbindungen (Connection) über den entsprechenden JDBC-Treiber für das verwendete DBMS hergestellt; von der Verbindung werden einzelne Anfragen (Statement) erzeugt und an die Datenbank übergeben; als Ergebnis wird eine Ergebnisliste (ResultSet) zurückgeliefert; schließlich wird die Verbindung wieder geschlossen. Die folgende Abbildung stellt diese Struktur dar: Abbildung 4.1: JDBC Treiber-Manager Im Folgenden wird der Ablauf der Programmierung mit JDBC kurz erläutert: Importieren der notwendigen Klassen Die für die Ausführung von JDBC notwendigen Klassen liegen allesamt im Package java.sql vor. Diese Klassen müssen vor der Anwendung der Java-Applikation importiert werden. Die Syntax für den Import der JDBC-Klassen lautet demnach wie folgt: import java.sql.*; 25 Durch den Platzhalter (*) kann man erreichen, dass alle Klassen des angegebenen Packages importiert werden. Registrieren mit Datenbank-Treiber Zur Ausführung der JDBC-Befehle muss ein Datenbanktreiber geladen werden, der die Anweisungen in eine Form umsetzt, die von speziellen Datenbanksystemen verstanden wird. Ein solcher Treiber kann die JDBC-ODBC-Bridge sein, die in Verbindung mit einem lokal installierten und eingerichteten ODBC-Treiber jede ODBC-Datenbank ansprechen kann. Der Treiber kann einfach durch einen String, der seinen Klassennamen oder eine URL darstellt, aufgerufen werden. Im folgenden Beispiel wird ein JDBC-Treiber für Access verwendet: try { Class.forName(„sun.jdbc.odbc.JdbcOdbcDriver“); }catch (Exception e) { e.printStackTrace(); } Verbindung zur Datenbank Zum Aufbau einer Verbindung zur Datenbank wird dem Treibermanager eine URL, Benutzername und ein Passwort der Datenbank übergeben. Die URL ist nach dem Schema jdbc:: aufgebaut. Das Subprotokoll spezifiziert dabei, welcher Treiber zur Übertragung verwendet werden muss. Mit den folgenden Anweisungen wird eine Verbindung zur Access-Datenbank erstellt. Hier werden die Parameter Benutzername und Passwort nicht eingesetzt. Connection conn; try { conn = DriverManager.getConnection(„jdbc:odbc:Driver = {Microsoft Access Driver (*.mdb)};DBQ = F:/GameOfLife/GameOfLife.mdb “ ); }catch (Exception e) { e.printStackTrace(); } Die Anfrage Nach Aufbau der Verbindung zur Datenbank kann man anschießend die Anfrage erstellen, wodurch ein Objekt der Klasse „Statement“ erzeugt wird, an dem die Methode executeQuery() mit einem SQL-Anfragestring als Parameter aufgerufen wird. Die Ergebniszeilen werden als Objekt vom Typ „ResultSet“ zurückgeliefert, die einzeln durchlaufen und ausgelesen werden können. Statement stmt = conn.createStatement(); String query = „SELECT * FROM Dozenten“; ResultSet rs = stmt.executeQuery(query); 26 Für andere Operationen wie UPDATE, INSERT und DELETE stellt das Statement die Methode executeUpdate() zur Verfügung, die die Anzahl der veränderten Tabellenzeilen zurückliefert: String query = „INSERT INTO Dozent VALUES (7, Mike, Steffen)“; int rowCount = stmt.executeUpdate(query); Die in der Datenbank integrierten Abfragen können durch das Statement mit der Methode execute(„EXEC xxx“) aufgerufen werden, wobei der Platzhalter xxx der Name der Abfrage ist. Als Beispiel wird eine Abfrage „SelectName“ in der Access-Datenbank erstellt: SelectName SELECT nachname FROM Dozenten Mit folgender Anweisung kann die Abfrage „SelectName“ aufgerufen werden und die Nachnamen in der Tabelle „Dozenten“ werden zurückgeliefert: stmt.execute(„EXEC SelectName“) Das Ergebnis Schließlich muss das Ergebnis, das nach einer Abfrage als Objekt der Klasse „ResultSet“ vorliegt, verarbeitet werden. Die Methode next()erlaubt es, durch die Zeilen des Ergebnisses zu blättern. Die einzelnen Spalten müssen mit einer getXXX()Anweisung, die den Typ des Datenfeldes kennen muss, ausgelesen werden. ResultSet rs = stmt.executeQuery(query); While (rs.next()) { String nachname = rs.getResultSet(“Nachname”); } 4.4 Apache Ant Apache Ant ist in der Softwareentwicklung eine Built-Umgebung, die das einfache und konsistente Übersetzen eines Projektes ermöglicht. Im Gegensatz zu „make“ aus dem C-Umfeld ist Ant in Java implementiert und benötigt somit zur Ausführung eine Java-Laufzeitumgebung (JRE) [Mat05]. 27 Ant wird durch eine XML-Datei gesteuert, die so genannte Built-Datei. Sie heißt standardmäßig build.xml. In der Built-Datei wird ein Projekt definiert. Dies ist das Wurzelelement der XML-Datei. Zu einem Software-Projekt sollten genau eine Built-Datei und damit genau ein Ant-Projekt gehören. Ein Projekt besteht aus einem oder mehreren Targets, die eine Reihe von auszuführenden Aufgaben (Tasks) beinhalten. Man kann ein Target für das Kompilieren, eines für das Erzeugen einer Jar-Datei usw. erstellen. Die verschiedenen Targets können voneinander abhängig sein. Diese Abhängigkeit wird von Ant aufgelöst, wobei es jedes Target nur einmal ausführt, auch wenn mehrere andere es mit „depends“ referenzieren. Im Folgenden wird ein Built-File als Beispiel angezeigt, das zwei Targets enthält: <?xml version="1.0" encoding="UTF-8"?> <project> <property name="build" value="build"></property> <property name="src" value="."></property> <target name="init" description="init System"> <mkdir dir="build"/> </target> <target name="compile" depends="init"> <javac srcdir="${src}" destdir="${build}"></javac> </target> </project> Target „init“ definiert eine Aufgabe, um einen Ordner unter dem Rootverzeichnis des Projekts zu erzeugen; Target „compile“ ist abhängig von Target „init“ und wird alle Sourcedateien kompilieren, die kompilierte binäre Datei wird in dem Ordner „build“ gespeichert. Ant lässt sich in fast jede populäre Entwicklungsumgebung integrieren und ist bei Bedarf auch beliebig erweiterbar. Die folgende Abbildung zeigt den Aufruf von Ant innerhalb von Eclipse: 28 Abbildung 4.2: Aufruf von Ant innerhalb Eclipse Für Ant gibt es viele Tasks und unter der Webseite von Apache-Ant gibt es einen Überblick. Die wichtigsten Tasks sind: Ant-Task javac jar manifest unjar javdoc copy exe mkdir echo junit Aufgabe Übersetzt mit Java-Compiler Bündelt Dokumente in ein Java-Archiv Erzeugt eine Manifest-Datei Packt Java-Archive aus Erzeugt die Java-Dokumentation Kopiert Dateien Startet ein externes Programm Legt ein Verzeichnis an Schreibt Ausgaben auf die Konsole Arbeitet Junit-Tests ab 29 Mittlerweile hat die Entwicklung von Ant eine ziemliche Dynamik erreicht und es ist für viele Open-Source-Projekte im Java-Umfeld zum Builttool der Wahl geworden (beispielsweise Tomcat, JBoss). 4.5 UML - Unified Modeling Language UML (Unified Modeling Language) ist ein Standard der OMG (http://www.omg.org/uml) und ist eine „Sprache“ zur Darstellung objektorientierter Entwürfe [For07]. Sie definiert eine Notation und Semantik zur Visualisierung, Konstruktion und Dokumentation von Modellen für die Geschäftsprozessmodellierung und für die objektorientierte Softwareentwicklung. Die graphische Notation ist jedoch nur ein Aspekt, der durch die UML geregelt wird. Die UML legt in erster Linie fest, mit welchen Begriffen und welchen Beziehungen zwischen diesen Begriffen so genannte Modelle spezifiziert werden – Diagramme der UML zeigen nur eine graphische Sicht auf Ausschnitte dieser Modelle. Die UML schlägt weiter ein Format vor, in dem Modelle und Diagramme zwischen Werkzeugen ausgetauscht werden können. Die UML kennt sechs Strukturdiagramme und sieben Verhaltensdiagramme. Im folgenden Abschnitt werden Klassendiagramme und Sequenzdiagramme vorgestellt, die bei der Entwicklung von „LifeWatch“ angewendet werden. Klassendiagramm Klassendiagramme werden verwendet, um die Menge aller Klassen, die im Verlauf der Anwendungsentwicklung modelliert werden, nach semantischen oder technischen Kriterien zu gliedern. Ein Klassendiagramm zeigt: ¾ Klassen – im aufgeklappten Zustand mit ihren Attributen und Methoden, ¾ Aggregationen und Assoziationen, die als Beziehungen zwischen den Instanzen der abgebildeten Klassen den Austausch von Botschaften ermöglichen, ¾ Generalisierungen von Klassen, die zur Vererbung von Eigenschaften und Verhalten dienen. Klassen Eine Klasse beschreibt die Struktur und das Verhalten ihrer Objekte. Sie wird als dreigeteiltes Rechteck dargestellt. Im oberen Bereich steht der Klassenname, in der Mitte die Attribute der Klasse und im unteren Drittel stehen die Operationen (Methoden) der Klasse. 30 Die Sichtbarkeit von Methoden und Attributen wird mit public, protected und private mit drei Symbolen „+“, „#“ und „-“ gekennzeichnet. Assoziation Die Assoziation stellt eine Beziehung zwischen Klassen dar. Sie kann einen Beziehungsnamen tragen. Der Pfeil am Namen gibt an in welcher Richtung der Beziehungsname gelesen werden muss. Die Rollen der an einer Assoziation beteiligten Klassen können mit Hilfe von Rollennamen beschrieben werden. Die Multiplizität gibt an wie viele Objekte an einer Beziehung beteiligt sein können. Aggregation Eine Aggregation ist eine Teil-von- oder Besteht-aus-Beziehung und sagt aus, dass ein Objekt Bestandteil eines anderen ist. Ein Auto besteht z.B. aus Fahrgestell, Motor, Rädern usw.. Die Einzelteile können aber unabhängig vom Ganzen als Objekte existieren. Komposition Auch eine Komposition ist eine Teil-von-Beziehung. Hier sind jedoch die Einzelteile vom Ganzen existenzabhängig. Eine Auftragsposition gehört immer zu einem Auftrag. Es gibt keine Auftragspositionen, wenn nicht vorher ein Auftrag erzeugt wurde. 31 Dieselbe Klasse kann dabei in mehreren Klassendiagrammen in jeweils unterschiedlichem Zusammenhang dargestellt werden. Dasselbe gilt auch für Beziehungen zwischen Klassen. Assoziationen, Aggregationen und Generalisierungen können zusammen mit den daran beteiligten Klassen in verschiedenen Klassendiagrammen wiederholt dargestellt werden. Klassendiagramme müssen also nicht redundanzfrei sein. Sequenzdiagramm Das Sequenzdiagramm beschreibt die zeitliche Abfolge von Interaktionen zwischen einer Menge von Objekten innerhalb einer bestimmten Szene [Kni06]. Es wird beschrieben, wie die an einer Szene beteiligten Objekte zusammenarbeiten müssen, damit das System die Leistung erbringt, die der Benutzer fordert. Sequenzdiagramme enthalten eine implizite Zeitachse. Die Zeit scheitet von oben nach unter fort. Objekt Die Objekte werden durch Rechtecke visualisiert. Von ihnen aus gehen die senkrechten Lebenslinien, dargestellt durch gestrichelte Linien, ab. Das schmale Rechteck auf der gestrichelten Linie stellt eine Aktivierung dar. Nur im Bereich der Aktivierung kann ein Objekt Nachrichten empfangen oder versenden. Eine Aktivierung ist der Bereich, in dem eine Methode aktiv ist. Auf einer Lebenslinie können mehrere aktive Bereiche enthalten sein. Nachrichten Objekte kommunizieren über Nachrichten. Nachrichten werden als Pfeile zwischen den Objekten eingezeichnet. Der Name der Nachricht steht an dem Pfeil. Die Angabe einer Bedingung ist optional. Bedingungen werden in eckigen Klammern dargestellt. Rekursion Wie in obiger Abbildung gezeigt wird, ruft bei rekursiven Nachrichten ein Objekt eine eigene Methode auf. 32 Sequenzdiagramme werden zur Modellierung der Dynamik des Systems eingesetzt. In Sequenzdiagrammen werden einzelne Szenarien des Systems modelliert und festgelegt, welche Objekte daran beteiligt sind, welche Nachrichten die Objekte sich zusenden und in welcher Reihenfolge die Nachrichten gesendet werden. Die Nachrichten werden in der zeitlichen Reihenfolge in der sie auftreten müssen von oben nach unten in einem Diagramm eingezeichnet. Ein System wird in der Regel nicht vollständig durch Sequenzdiagrame spezifiziert. Es werden nur die Szenen modelliert, die häufig vorkommen oder besonders wichtig sind. 4.6 Design Patterns Die Idee der Entwurfsmuster wurde in der Softwareentwicklung aus der Architektur übernommen [Fre04]. Ein Entwurfsmuster beschreibt in Textform eine in der Praxis erfolgreiche Lösung für ein mehr oder weniger häufig auftretendes Entwurfsproblem. Die Beschreibung eines Entwurfsmusters folgt gewissen Regeln: ¾ ¾ ¾ ¾ ¾ ¾ Beschreibung eines konkreten Problems Beschreibung einer konkreten Lösung Verallgemeinerung der Lösung Diskussion über Vor- und Nachteile des Musters Codebeispiele verwandte Entwurfsmuster Der primäre Nutzen eines Entwurfsmusters liegt in der Beschreibung einer Lösung für eine bestimmte Klasse von Problemen. Weiterer Nutzen ergibt sich aus der Tatsache, dass jedes Muster einen Namen hat. Dies vereinfacht die Diskussion unter Softwareentwicklern, da man abstrahiert über eine Softwarestruktur sprechen kann. So sind Entwurfsmuster zunächst einmal sprachunabhängig. Wenn der Einsatz von Entwurfsmustern dokumentiert wird, ergibt sich ein weiterer Nutzen dadurch, dass aufgrund der Beschreibung des Musters ein Bezug zur dort vorhandenen Diskussion des Problemkontextes und zu den Vor- und Nachteilen der Lösung hergestellt wird. Moderne Hochsprachen unterstützen einige der gängigen Entwurfsmuster bereits mit bestimmten Sprachmitteln, sodass man sich in der Praxis vor allem bei der Nutzung moderner Sprachen im Prozess der objektorientierten Analyse (OOA) und des objektorientierten Designs (OOD) der Entwurfsmuster bedient, die dort unter Umständen noch immer implementationsneutral in der Unified Modeling Language (UML, siehe nächster Abschnitt) angewandt werden. Im Lauf der Zeit wurden Lösungen für bestimmte Probleme gefunden, die erfolgreich eingesetzt wurden und somit einen Katalog mit insgesamt 23 Entwurfsmustern bilde33 ten. Laut GoF lassen sich sämtliche Entwurfsmuster in folgende drei grundlegende Mustergruppen zusammenfassen: ¾ Creational Patterns (Erzeugungsmuster) ¾ Structural Patterns (Strukturmuster) ¾ Behavioral Patterns (Verhaltensmuster) Zu den bekanntesten Entwurfsmustern gehören unter anderem der Model View Controler (MVC), das Actor-Role Pattern und das Singleton Pattern. Für eine Vielzahl von Entwicklern sind Entwurfsmuster aus der Anwendungsentwicklung nicht mehr wegzudenken. Wenn man sich näher mit Entwurfsmustern auseinandersetzen will, dann gilt es Folgendes zu beachten, um Missverständnisse von vornherein zu vermeiden: ¾ Entwurfsmuster sind keine Algorithmen. Algorithmen lösen Probleme (Suchen, Sortieren etc.) und bieten weniger Flexibilität in der Implementierung. ¾ Entwurfsmuster sind kein Allheilmittel! Erfindungsgeist ist bei der Anwendung von Entwurfsmustern immer noch gefragt. ¾ Entwurfsmuster sind keine Frameworks. Frameworks setzen sich aus wiederverwendbarem Code zusammen, Entwurfsmuster enthalten lediglich Beispiele von Code. Frameworks werden für festgelegte Anwendungsbereiche eingesetzt, Entwurfsmuster hingegen können überall eingesetzt werden. Singleton ist ein Entwurfsmuster und wird zu den Erzeugungsmustern gezählt. Während der Softwareentwicklung ist es manchmal notwendig, sicherzustellen, dass nur eine Instanz eines Objekts existiert. Dieser Fall tritt zum Beispiel auf, wenn auf eine Datenbank zugegriffen werden soll. Um kontrollieren zu können, dass nur ein solches Objekt – und nicht mehrere parallel – auf die Datenbank zugreifen, kann das Singleton-Pattern eingesetzt werden. Das Singleton Pattern kann eingesetzt werden, wenn: ¾ sichergestellt werden soll, dass das betreffende Objekt nur einmal instanziiert wird ¾ ein globaler Zugriffspunkt auf das Objekt existieren soll ¾ die Instanziierung des Objekts es ermöglichen soll, ggf. in der Zukunft doch mehrere Instanzen zu instanziieren, ohne die Clients reimplementieren zu müssen. 34 Der ganze Trick besteht darin, den Konstruktor private zu setzen, sodass dieser nur intern verwendet werden kann. Zudem braucht es einen statischen Member in der Klasse, der die einzigartige Instanz hält. Um trotzdem von Außen an eine Instanz zu kommen, wird der Klasse eine öffentliche Methode bspw. namens „getInstanz“ spendiert. In dieser wird ein Objekt erstellt und zurückgegeben. Die obige Abbildung stellt das UML-Klassendiagramm für das Singleton Pattern dar. 35 5 Architektur und Funktionalität von LifeWatch In diesem Kapitel werden zuerst die Systemarchitektur und Funktionalitäten von „LifeWatch“ erläutert und dann die Klassenstruktur und die Java-Implementierung mit einigen ausgewählten Aspekten ausgeführt, insbesondere, wie die Funktionen der Mustererkennung und Musterklassifikation in „LifeWatch“ integriert werden. Die grafische Oberfläche dieses Simulationssystems wird mit Java 1.6 implementiert; die Entwicklungsumgebung ist Eclipse 3.3. 5.1 Systemarchitektur „LifeWatch“ besteht aus folgenden Teilkomponenten: Simulator, um den diskreten Prozess GoL zu simulieren; Monitor, um das GoL zu überwachen; GUI, damit Benutzer das System ansteuern können; Datenbanksystem, um die Daten zu verwalten und zu bearbeiten. Die Abbildung 5.1 stellt die Struktur des Systems dar. Der Simulator ist zuständig für die Simulation der Zellen in der Oberfläche. Jede Zelle hat zwei Zustände: lebendig oder tot. Für eine lebendige Zelle simuliert der Simulator die Koordinateninformationen der Zelle in der entsprechenden Position. Die toten Zellen werden vom Simulator von ihrer Position weggestrichen. Die Informationen über die Zellen, z.B. ihre Koordinaten, Zustände, werden in einer Datenbank protokolliert. Der Monitor überwacht die in der Datenbank gespeicherten Informationen aller Zellen. Wenn z.B. die Koordinaten der Zellen verändert werden, wird der Monitor das sofort bemerken und meldet dies an den Simulator, damit der Simulator die Zellen neu si36 mulieren kann. Die zweite Aufgabe des Monitors ist die Überwachung der Muster. Wenn ein Muster auftaucht, informiert der Monitor den Simulator, damit der Simulator in der Oberfläche das Muster simulieren kann. Der Benutzer kann durch die Built-in-Anweisungen beliebige Zellen generieren. Und nach der Ausführung des Systems werden die in der Datenbank gespeicherten Informationen aller Zellen in jedem diskreten Schritt automatisch verändert, die abhängig von den Umgebungsbedingungen sind. Der Simulator und der Monitor kommunizieren durch eine Datenbankschnittstelle mit der Datenbank, damit die Veränderung der Informationen der Zellen und das Auftauchen des Musters gleichzeitig durch den Simulator in der Oberfläche angezeigt werden. Abbildung 5.1: Systemstruktur Nach dem Start des Systems liest der Simulator zuerst durch JDBC die Daten aus den Datenbanken, also die gespeicherten Koordinaten von lebenden Zellen, und stellt diese dann in der Oberfläche dar; danach analysiert der Monitor die Daten, sucht die für den Benutzer interessanten Muster und markiert die gefundenen Muster. Anschließend greift der Simulator wieder auf die Datenbanken zu. Wenn es markierte Muster gibt, werden sie in der Oberfläche simuliert, zum Schluss werden die Zustände der Zellen für den nächsten Zeitpunkt berechnet. Dieser Ablauf läuft immer weiter, bis der Benutzer das System beendet. 5.2 Benutzeroberfläche und Funktionalitäten In diesem Abschnitt wird die Bedienung des Simulationssystems beschrieben, Die Abbildung 5.2 stellt die Benutzeroberfläche dar, die aus sechs Teilbereichen besteht. 37 Abbildung 5.2: Benutzeroberfläche 38 Der Bereich 1 besteht aus drei Buttons. Nach Drücken des Buttons „Start“ wird das Simulationssystem ständig ausgeführt und gleichzeitig wird der Text des Buttons von „Start“ nach „Stop“ geändert. Nach Drücken von „Stop“ wird das Simulationssystem beendet. Der Button „Next“ bietet dem Benutzer die Möglichkeit an, das Simulationsverfahren Schritt für Schritt auszuführen. D.h. nach Drücken des Buttons „Next“ wird das Simulationsverfahren nur einmal ausgeführt, der Benutzer kann damit die Veränderung der Zellen genauer beobachten. Mit dem Button „Clear“ kann der Benutzer alle lebendigen Zellen von der Oberfläche löschen. Im Bereich 2 hat der Benutzer die Möglichkeit, verschiedene vordefinierte Graphen in der Oberfläche zu simulieren. Die Abbildung 5.2 zeigt die Oberfläche nach der Selektion von einem vordefinierten Graphen „Gosper_Glider_Gun“. Im Simulationssystem werden insgesamt sieben Graphen vordefiniert: „Glider“, „Small Exploder“, „Exploder“, „10 Cell Row“, „Lightweight spaceship“ und „Gosper Glider Gun“. Die sieben Graphen sind sehr bekannt in „Game Of Life“. Natürlich hat der Benutzer auch die Möglichkeit, beliebige Graphen in der Oberfläche zu erstellen. Diese Funktionalität wird nachher noch beschrieben. Abbildung 5.3: Selektion eines vordefinierten Graphen Im Bereich 3 gibt es verschiedene vordefinierte Muster. Wenn man eine davon ausgewählt hat, wird das Muster zur Laufzeit des Systems überwacht. Solange es auftaucht, wird ein kleinstes Rechteck mit roter Farbe dargestellt. Dann erfährt der Benutzer, dass das ihn interessierende Muster aufgetaucht ist. Die letzte Auswahl „All Group“ hat eine besondere Bedeutung, denn damit kann man alle Muster überwachen (siehe Abbildung 5.3). Für die einzelnen Muster kann zurzeit nur „Glider“ überwacht werden. Die Überwachung anderer Muster ist noch nicht implementiert. 39 Abbildung 5.4: Überwachung aller Muster Im Bereich 4 gibt es einen Slider, damit der Benutzer das Simulationstempo ansteuern kann. Der Simulator greift auf die Access-Datenbank in jeder Zeiteinheit zu. Der Slider entspricht der Zeiteinheit. Je weiter rechts der Slider eingestellt ist, desto kleiner ist die Zeiteinheit und das Simulationstempo wird schneller. Im Bereich 5 wird ein Text gezeigt, wie viele Generationen der Zellen nach dem Start des Systems erzeugt werden. Bereich 6 ist der Arbeitsbereich. Das Koordinatensystem und die lebendige Zellen werden hier simuliert. Im Arbeitsbereich kann man mit einem Mausklick den Zustand der Zellen verändern. Wenn eine lebendige Zelle angeklickt wird, wird sie sterben und von der Oberfläche gelöscht; wenn eine tote Zelle angeklickt wird, wird sie erzeugt und in der Oberfläche simuliert. Mit dieser Funktion kann der Benutzer beliebige Graphen in der Oberfläche erstellen. 5.2 Klassenstruktur Bei der Implementierung von „LifeWatch“ werden insgesamt 10 Klassen in vier Java-Package erstellt: ¾ de.dexin.swt: LifeWatch, SimulatorImpl ¾ de.dexin.swt.widgets: PatternTree, PropertyComposite ¾ de.dexin.db: DBConnection, PropertyLoader ¾ de.dexin.observerPattern: Monitor, CellsGeneratorImpl, CRectangle, Coordinate Die vier Klassen der ersten zwei Package sind zuständig für die Darstellung der GUI. Die Klasse „LifeWatch“ dient als Eingangsprogramm und definiert die Grundstruktur der GUI sowie die Toolbar mit drei Bedienungsbuttons (Bereich 1 in der Abbildung 40 5.2). Die Klasse „PatternTree“ implementiert ein Tree als SWT-Komponente und bietet dem Benutzer die Möglichkeit an, eine vordefinierte Figur als Anfangskonfiguration einzusetzen (Bereich 2 in Abb. 5.2). Die Klasse „PropertyComposite“ implementiert ein PropertyPane, die den Bereichen 3 und 4 in Abbildung 5.2 entspricht. Die Klasse SimulatorImpl entspricht dem Arbeitsbereich von GUI, also dem Bereich 6 in Abbildung 5.2. Die beiden Klasse im Package de.dexin.db sind zuständig für die Verbindung zur Datenbank. Die Informationen über die verwendete Datenbank werden in einer Java-Properties-Datei „DBProperties.properties“gespeichert. Die Java-Properties-Datei ist eine Textdatei, die als Konfigurationsmechanismus verwendet wird und die Dateiendung „.properties“ hat. Eine Property ist in diesem Zusammenhang ein Text, der unter einem bestimmten Namen abgelegt ist. Name und Text werden z.B. mit den Zeichen „:“ und „=“ getrennt. In „DBProperties.properties“ werden drei Property für JDBC definiert: db = F:/GameOfLife/GameOfLife.mdb url = jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ= driver = sun.jdbc.odbc.JdbcOdbcDriver „db“ definiert den Pfad der verwendeten Datenbank; „url“ plus „db“ ist die komplette JDBC-Verbingdungs-URL; „driver“ definiert die JDBC-Treiber. Die Properties-Datei hat folgenden Vorteil: Wenn man z.B. eine andere Datenbank verwenden will, braucht man dies nur in dieser Properties-Datei zu bearbeiten, anstatt einer Änderung in der Source-Code-Datei. Die Klasse „PropertyLoader“ umgreift die Properties-Datei „DBProperties.properties“ und leitet die Properties zur Klasse „DBConnection“ weiter. Die Klasse „DBConnection“ wird als Singleton entworfen und liefert die Verbindung zur Datenbank zurück. Die Klasse „CellsGeneratorImpl“ ist die wichtigste Klasse, sie hat die Verbindung zur Datenbank von „DBConnection“, womit sie die Informationen abholt, die in der Datenbank gespeichert werden. Die Informationen werden danach als Type „Coordinaten“ oder „CRectangle“ konvertiert, um sie für weitere Benutzungen vorzubereiten. Die Abbildung 5.5 stellt die Übersicht und Beziehungen aller Klassen dar. 41 Abbildung 5.5: Klassendiagramm 42 5.3 Implementierung der ausgewählten Aspekte Im Bereich 1 werden drei Buttons vom Typ „ToolItem“ implementiert: final ToolItem startItem = new ToolItem(toolBar, SWT.PUSH); startItem.setText("Start"); final ToolItem nextItem = new ToolItem(toolBar, SWT.PUSH); nextItem.setText("Next"); final ToolItem clearItem = new ToolItem(toolBar, SWT.PUSH); clearItem.setText("Clear"); Jedem Button wird ein SelectionListener hinzufügt, um die entsprechende Funktionalitäten zu aktivieren. Wenn der Button „Start“ gedrückt wird, wird ein Separater Thread aufgerufen, das Simulationsverfahren wird innerhalb dieser Threads durchgeführt. Das Monitoringsystem ist ein Multithreads-Programm, mehrere Threads können parallel laufen. Das heißt, während der Ausführung des Simulationsverfahrens werden andere Funktionalitäten wie z.B. das Verändern des Simulationstempos, die Selektion eines anderen Graphs, die Überwachung eines Musters und sogar das Aktivieren einer toten Zelle auch verfügbar. Der folgende Sourcecode implementiert den SelektionsListener für den Button „Start“. startItem.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { start = !start; new Thread() { public void run() { while(start){ try{ Thread.sleep(sleepTime); }catch(Throwable th) {} if(display.isDisposed()) { return; ).. .. } }); 43 display.syncExec(new Runnable(){ public void run() { try{ simulation(); } catch(Exception e) { e.printStackTrace(); } } }); } } }.start(); Um auf die Daten aus der Datenbank zuzugreifen, wird eine separate Klasse „DBConnection“ implementiert, die durch JDBC eine Verbindung zur Access-Datenbank erstellt. Alle anderen Klassen, die auf die Datenbank zugreifen wollen, müssen eine Instanz von „DBConnection“ bekommen. Um den Konflikt beim Zugriff auf die Datenbank zu vermeiden, darf es nicht für jede Klasse einer neuen Instanz von „DBConnection“ erstellt werden. Deshalb wird die Klasse „DBConnection“ mit dem Entwurfsmuster „Singleton“ implementiert. Der folgende Code zeigt die zwei Methoden aus „DBConnection“, um zu sichern, dass schließlich nur eine Instanz von „DBConnection“ existiert: public static DBConnection getInstance() { if(instance == null){ instance = new DBConnection(); } return instance; } public Connection getConnection() { Connection conn = null; try{ Class.forName(driver); conn = DriverManager.getConnection(url+db); } catch(Exception e) { e.printStackTrace(); } return conn; } 44 Die static Methode „getInstance“ liefert ein Objekt „instance“ vom Typ „DBConnection“ zurück. Die Methode „getConnection“ liefert eine Verbindung zur die Datenbank zurück. Jede Klasse, die auf die Datenbank zugreifen möchte, muss zuerst die Methode „DBConnection“ aufrufen, um eine Instanz von „DBConnection“ zu bekommen. Danach ruft sie mit der Instanz die Methode „getConnction“ auf, um eine Verbindung zur die Datenbank zu erstellen: DBConnection sql; Connection conn; sql = DBConnection.getInstance(); conn = sql.getConnection(); 45 6 Der GoL-Simulator von LifeWatch In diesem Kapitel wird der Aufbau des GoL-Simulators beschrieben. Zunächst werden noch einmal die Spielregeln von GoL wiederholt und dann diskutiert, wie ein Simulator für GoL implementiert wird. Die Zellen in GoL werden nach den Spielregeln in jedem diskretem Zeitpunkt entweder lebendig oder tot sein: ¾ Für eine aktuelle lebendige Zelle: ¾ Wenn sie keinen oder einen lebenden Nachbar hat, wird sie im nächsten Zeitpunkt sterben wegen Einsamkeit. ¾ Wenn sie zwei oder drei lebende Nachbarn hat, wird sie im nächsten Zeitpunkt noch lebendig sein. ¾ Wenn sie mehr als drei Nachbarn hat, wird sie im nächsten Zeitpunkt tot wegen Überbevölkerung tot sein. Für eine aktuelle tote Zelle: ¾ Wenn sie genau drei lebende Nachbarn hat, wird sie im nächsten Zeitpunkt lebendig, sonst bleibt sie tot. Die Spielregeln beschreiben, unter welchen Bedingungen eine lebendige Zelle im nächsten Zeitpunkt überleben oder sterben wird sowie eine tote Zelle neugeboren werden oder tot bleiben könnte. Der Simulator wird vorab bei der Initialisierung mit einigen lebendigen Zellen als Anfangsbedingungen konfiguriert und dann simulieren nach den Spielregeln die Zustände aller Zellen im nächsten Zeitpunkt. Der Simulator hat die folgenden drei Aufgaben: ¾ Die Berechnung der überlebenden Zellen ¾ Die Berechnung der neugeborenen Zellen ¾ Die Berechnung der toten Zellen 46 Die Simulierung wird mit der Access-Datenbank unterstützt. Am Anfang werden zuerst einige lebendige Zellen in der Tabelle „cellsStatus“ gespeichert und als Anfangsbedingungen für GoL eingesetzt. Die Tabelle „cellsStatus“ enthält vier Spalten: „x“, „y“, „status“ und „nextStatus“. Die Koordinate der Zelle wird als Ganzzahl mit „x“ und „y“ protokolliert. Weil jede Zelle eindeutige Koordinaten besitzen, sind „x“ und „y“ die Schlüsselwörter der Tabelle „cellsStatus“. „status“ speichert den Zustand der Zelle zum aktuellen Zeitpunkt, in der Tabelle „cellsStatus“ werden „1“ als lebendig und „0“ als tot bezeichnet. Und „nextStatus“ speichert den Zustand zum nächsten Zeitpunkt. Am Anfang ist dieser Wert noch nicht klar (mit „–1“ bezeichnet), nach Berechnung der Übergangsbedingungen wird „nextStatus“ der Zelle einen Wert von „0“ oder „1“ übergeben. Abbildung 6.1: Ablaufdiagramm des Simulators Das Schlüsselproblem der Spielregeln ist die Berechnung der Anzahl der Nachbarzellen. Ob eine Zelle im nächsten Zeitpunkt lebendig oder tot ist, ist abhängig von der Anzahl ihrer lebendigen Nachbarzellen. Danach wird die Anzahl geprüft. Wenn eine lebendige Zelle zwei oder drei lebendige Nachbarzellen hat, dann wird sie im nächsten Zeitpunkt überleben; wenn eine tote Zelle genau drei lebendige Nachbarzellen hat, 47 wird sie im nächsten Zeitpunkt neugeboren. Und alle anderen Zellen, die in der Tabelle „cellsStatus“ stehen, aber nicht im nächsten Zeitpunkt überleben oder neugeboren werden können, werden als tote Zellen markiert und vor der Berechnung eines neuen Zyklus aus der Tabelle „cellsStatus“ gelöscht. Deshalb werden die Regeln für die Berechnung der toten Zellen nicht explizit implementiert, sondern sie wird während der Berechnung der überlebenden Zellen und neugeborenen Zellen implizit realisiert. Die Abbildung 6.1 stellt den Ablauf des Simulators dar. Bei der „Initialisierung“ werden einige lebendige Zellen in die Tabelle „cellsStatus“ als Anfangsbedingungen eingefügt; anschließend werden beim „Nachbarberechnen“ die Nachbarzellen aller lebendigen Zellen aus der Tabelle „cellsStatus“ berechnet und in der Tabelle „cellsNeighbors“ gespeichert; Aus der Tabelle „cellsNeighbors“ werden die lebendigen Zellen und die toten Zellen nacheinander gefiltert, um die überlebenden Zellen und neugeborenen Zellen zu bestimmen; im letzten Schritt „Zustandsübergang“ werden in der Tabelle „cellsStatus“ die Zustände aller Zellen aktualisiert. Dieser Ablauf läuft solange, bis dem System eine Schlussanweisung gegeben wird. Die Berechnungsreihenfolge der überlebenden Zellen und neugeborenen Zellen ist egal, aber wichtig ist, dass die Berechnung der Nachbarzellen aller lebenden Zellen voraberfolgt, weil sie die Voraussetzung für die Berechnungen der überlebenden Zellen und neugeborenen Zellen ist. Dies ist offensichtlich bei der Berechnung der überlebenden Zellen, weil die Anzahl ihre lebenden Nachbarzellen ihre Zustände im nächsten Zeitpunkt bestimmt. Und für die neugeborenen Zellen muss man beachten, dass intuitive die Nachbarzellen aller toten Zellen berechnet werden sollen, dies aber aufwendig und auch nicht nötig ist. In Abschnitt 6.4 „Berechnung der neugeborenen Zellen“ wird ausführlich erklärt, wieso die Berechnung der neugeborenen Zellen von der Berechnung der Nachbarzellen aller lebendigen Zellen abhängig ist. Sowohl die überlebenden Zellen als auch die neugeborenen Zellen werden nach den Berechnungen in der Tabelle „cellsStatus“ gespeichert und unterschieden sich mit dem Attribut „status“, der den aktuellen Zustand einer Zelle bezeichnet. Hier in dieser Arbeit wird die Berechnung der überlebenden Zellen vor der Berechnung der neugeborenen Zellen durchgeführt. 6.1 Initialisierung Im Kapitel 5 wird es schon erläutert, dass man in LifeWatch zwei Möglichkeiten hat, um die Anfangsbedingungen zu konfigurieren, also entweder durch Mausklick im Arbeitsbereich von LifeWatch oder durch Auswahl des vordefinierten Graphen die Anfangskonfiguration einzusetzen. Die beide Aktionen werden durch Datenbank unterstützt: Eine bestimmte Menge von lebendigen Zellen wird in die Tabelle „cellsStatus“ eingefügt und ihr Attribut „status“, der den aktuellen Zustand der Zellen be48 zeichnet, mit „1“ eingesetzt („1“ bezeichnet den lebendigen Zustand der Zelle und „0“ bezeichnet eine tote Zelle). Ihr Attribut „nextStatus“, der den Zustand der Zellen im nächsten Zeitpunkt bezeichnet, wird mit „–1“ eingesetzt. „nextStatus“ hat drei Auswahlwerte: „–1“, „0“ und „1“. „0“ und „1“ bezeichnen eine tote sowie lebendige Zelle, „-1“ bedeutet, dass der Zustand der Zelle zurzeit noch nicht bekannt ist. Die entsprechende SQL-View, die dem Mausklick entspricht, lautet: insertCells INSERT INTO cellsStatus(x, y, status, nextStatus) VALUES (i, j, 1, -1) Die Parameter „i“ und „j“ entsprechen den Koordinaten, wo die Maus geklickt wird. Und wie oben erklärt wurde, werden die Attribute „status“ und „nextStatus“ mit „1“ und „–1“ vorausgesetzt. Für die zweite Möglichkeit wird vorab eine Tabelle erstellt, die zwei Spalten „x“ und „y“ hat, um die Koordinaten der Zellen, die das vordefinierte Graph zusammengebaut haben, zu speichern. Und wenn man diese Zellen als Anfangsbedingungen verwenden möchte, können solche Zellen direkt in die Tabelle „cellsStatus“ kopiert werden. So werden z.B. in der Tabelle „glider“ die Koordinaten von fünf Zellen gespeichert und mit folgender SQL-View alle fünf Zellen in die Tabelle „cellsStatus“ kopiert: insertGlider INSERT INTO cellsStatus(x, y, status, nextStatus) SELECT x, y, 1, -1 FROM glider; 6.2 Berechnung der Nachbarzellen der lebendigen Zellen In diesem Schritt werden die Nachbarzellen der lebendigen Zellen berechnet. Diese Nachbarzellen werden nach ihren aktuellen Zuständen in zwei Gruppen geteilt: lebendige Nachbarzellen und tote Nachbarzellen. Die Anzahl der lebendigen Nachbarzellen der lebendigen Zellen bestimmt, welche lebendigen Zellen im nächsten Zeitpunkt überleben können. Und die Anzahl der toten Nachbarzellen der lebendigen Zellen bestimmt, welche tote Zellen im nächsten Zeitpunkt neugeboren können. Um die Nachbarzellen zu speichern, wird eine Tabelle benötigt. Hier wird eine Tabelle „cellsNeighbors“ definiert, die zwei Spalten „x“ und „y“ hat, um die Koordinaten der gefundenen Nachbarzellen der lebendigen Zellen zu protokollieren. Vor der 49 Berechnung der Nachbarzellen der lebendigen Zellen muss vorab eine Frage spezifiziert werden: Was bedeutet „Nachbarschaft“ in GoL bzw. wie kann man wissen, dass zwei Zellen in GoL benachbart sind. Hierzu noch einmal eine Abbildung von GoL. Abbildung 6.2: Koordinatensystem Im GoL wird ein Koordinatensystem zugeordnet wie Abbildung 5.2. Darauf kann jetzt die Nachbarschaft der Zellen definiert werden: Zwei Zellen mit Koordinaten ( x1 , y1 ) und ( x2 , y2 ) in GoL sind benachbart, gdw. x1 − x2 < 1 && y1 − y2 < 1 . Mit dieser Aussage kann man jetzt für jede Zelle alle ihre Nachbarzellen spezifizieren. Für eine gegebene Zelle mit Koordinate (x, y) gibt es insgesamt neun Zellen, die die obigen Bedingungen der Nachbarschaft erfüllen. Die Abbildung 6.3 stellt alle Nachbarzellen einer Zelle dar: Abbildung 6.3: alle Nachbarzellen einer Zelle (x, y) 50 Nach obiger Abbildung wird eine SQL-Lösung mithilfe einer UNION-Operation dargestellt: SELECT x-1, y-1 FROM cellsStatus UNION (SELECT x, y-1 FROM cellsStatus UNION (SELECT x+1, y-1 FROM cellsStatus UNION (SELECT x-1, y FROM cellsStatus UNION (SELECT x+1, y FROM cellsStatus UNION (SELECT x-1, y+1 FROM cellsStatus UNION (SELECT x, y+1 FROM cellsStatus UNION (SELECT x+1, y+1 FROM cellsStatus))))))) Dieser Darstellung wird aus acht SELECT-Anfragen mit sieben UNION-Operatoren zusammengebaut. Jede SELECT-Anfrage entspricht einer Nachbarzelle der Zelle in der Tabelle „cellStatus“. Diese Lösung ist zwar die einfachste, aber zu lang und keineswegs intelligent. Eine Alternative lässt sich ist mithilfe des Java-Programms formulieren: INSERT INTO cellsNeighbors(x, y) SELECT x+i, y+j FROM cellsStatus WHERE status = 1 Hierbei werden die Parameter „i“ und „j” in Java-Code übergeben und für ihren Wert wird einer von drei möglichen Werten eingesetzt: „–1“, „0“ und „1“, damit (x+i) und (y+i) immer noch die Bedingungen für Nachbarschaft erfüllten können. Die WHERE-Klausel sichert, dass nur lebendige Zellen berücksichtigt werden. Die obige Anweisung wird neunmal aufgerufen, um alle neun Nachbarzellen aller lebendigen Zellen zu berechnen. Der Java-Code implementiert diese Schleife: for(int i =-1; i<2;i++) { for(int j=-1; j<2; j++) { stmt.execute("EXEC calNeighbors(i, j)") } } Hier muss man beachten, dass die Zelle (x, y) auch selbst die Bedingungen für Nachbarschaft erfüllt, das heißt, dass jede Zelle mit sich benachbart ist. In diesem Fall wird jede lebendige Zelle als selbstbenachbart in die Tabelle „cellsNeighbors“ eingefügt. Dieses Problem beeinflusst die folgende Berechnung. Auf der Grundlage der Spielregel: „Wenn eine lebendige Zelle genau zwei oder drei lebendige Nachbarzellen haben, wird sie im nächsten Zeitpunkt überleben“, wird in diesem Fall die Anzahl um eins 51 erhöht, weil eine davon die Zelle selber ist. Die neue Spielregel lautet: „Wenn eine lebendige Zelle genau drei oder vier lebendigen Nachbarzellen hat, wird sie im nächsten Zeitpunkt überleben.“ Die dritte Möglichkeit, um die Nachbarzellen zu berechnen, kann das obige Problem vermeiden, kostet aber Speicheraufwand. Die Idee besteht darin, dass die Koordinaten aller acht Nachbarzellen für eine Zelle (x, y) folgende sind (x+1, y+1), (x+1, y), (x+1, y–1), (x, y+1), (x, y–1), (x–1, y+1), (x–1, y) und (x–1, y–1) (siehe Abbildung 6.4 links). Hierzu benötigt man eine Hilfstabelle „neighborhood“, die zwei Attribute „dx“ und „dy“ hat, um das Inkrement der Koordinatenwerte der Nachbarzellen zu speichern. D.h. es gibt in der Tabelle „neighborhood“ insgesamt acht Datensätze, diese sind (1,1), (1,0), (1, –1), (0,1), (0, –1), (–1,1), (–1,0), (–1, –1) (siehe Abbildung 6.4 rechts) Abbildung 6.4: alle Nachbarn der Zelle (x, y) und die Tabelle „neighborhood“ Anschließend werden die Nachbarzellen mithilfe von der Tabelle „neighborhood“ einfach berechnet und in die Tabelle „cellsNeighbors“ eingefügt. die entsprechende SQL-Anweisung lautet: INSERT INTO cellsNeighbors (x, y) SELECT x+dx, y+dy FROM cellsStatus, neighborhood; Diese SQL-Anweisung hat eine Schleife implementiert, so dass die Koordinatenwerte jeder Zelle aus der Tabelle „cellsStatus“ mit dem Inkrement, das in der Tabelle „neighborhood“ gespeichert werden, addiert werden. Dadurch werden bei jeder Schleife nur acht echte Nachbarzellen einer Zelle eingefügt, es wird erfolgreich vermieden, die Zelle selber in die Tabelle „cellsNeighbors“ zu speichern. Die dritte Alternative Kosten nur wenig Speicherplatz mehr gegenüber der zweiten Alternative, hat aber große Vorteile. Einerseits werden nur echte Nachbarzellen berechnet, die Zelle selber wird nicht als Nachbarzelle berücksichtigt. D.h. es existieren 52 keine redundanten Informationen und man braucht auch nicht die Spielregeln zu verändern. Dazu ist der Zeitaufwand geringer; andererseits ist die Implementierung für die dritte Alternativ eine reine SQL-Darstellung, diese Darstellung wird in der Access-Datenbank verbaut und kann beim Aufruf schneller ausgeführt werden. Demgegenüber ist die zweite Alternative eine Java-SQL-Darstellung, die SQL-Anweisung wird in der Java-code-Datei gemischt und verursacht damit eine zu lange und nicht saubere Java-Datei. 6.3 Berechnung der überlebenden Zellen Nach Berechnung der Nachbarzellen der lebendigen Zellen ist es jetzt möglich, die überlebenden Zellen aus der Tabelle „cellsStatus“ herauszufinden. Eine Zelle in der Tabelle “cellsNeighbors” hat die innere Bedeutung, dass die Zelle von einigen lebendigen Zellen benachbart ist. Wenn sie z.B. dreimal in der Tabelle auftaucht bedeutet das, dass sie mit drei lebendigen Zellen benachbart ist. Und wenn sie selbst auch lebendig ist, wird sie die Überlebensbedingungen von GoL erfüllen. Deshalb muss man solche Zellen auswählen, die aktuell lebendig sind, und ihre Auftrittszahl zählen. Wenn diese Zahl einer Zelle genau gleich 2 oder 3 ist, d.h. diese Zelle genau 2 oder 3 Nachbarzellen hat, dann wird – nach der Spielregel – diese Zelle im nächsten Zeitpunkt überleben. Im Folgenden wird dies mit einem Beispiel dieses Algorithmus ausführlich erläutert. Gegeben seien drei Zellen, ihre Positionsbeziehungen werden mit folgender Abbildung beschrieben: Abbildung 6.5: drei lebendige Zellen Hier werden nur die drei lebendigen Zellen (4,2), (3,3) und (5,3) berücksichtigt. Zelle (4,2) hat acht Nachbarzellen: (3,1), (4,1), (5,1), (3,2), (5,2), (3,3), (4,3), (5,3). Davon sind (3,3) und (5,3) lebendig. Zelle (3,3) und (5,3) haben auch acht Nachbarzellen und davon ist nur (4,2) lebendig. Alle 24 Nachbarzellen werden in der Tabelle “cellsNeighbors” gespeichert und nach den Zuständen (lebendig oder tot) gefiltert. Die lebendigen Zellen werden verbleiben und ihre Auftrittszahl wird gezählt. Zelle (4,2) ist der Nachbar von (3,3) und (5,3), sie tritt zweimal in der Tabelle “cellsNeighbors” auf 53 – nach den Spielregeln wird sie im nächsten Zeitpunkt überleben. Die Zellen (3,3) und (5,3) treten nur einmal in der Tabelle auf, deshalb werden sie im nächsten Zeitpunkt verschwinden. Die SQL-Implementierung für die Berechnung der überlebenden Zellen wird wie folgt aufgebaut: Zunächst müssen die lebendigen Nachbarzellen in der Tabelle „cellsNeighbors“ herausgefunden werden. Weil die Zustände aller Zellen in der Tabelle „cellsStatus“ gespeichert werden, sind nur die Nachbarzellen in der Tabelle „cellsNeighbors“ lebendig, die auch in „cellsStatus“ gespeichert werden und deren Attribut „status“ mit „1“ gesetzt ist. Man kann die zwei Tabellen durch „INNER JOIN“ verknüpfen, um die lebendigen Nachbarzellen zu filtern. Anschließend gruppiert man alle herausgefundenen Nachbarzellen mit der SQL-Funktion „GROUP BY“ und mit der Funktion „COUNT( )“ wird die Auftrittszahl der lebendigen Nachbarzellen gezählt. Wenn diese Zahl gleich zwei oder drei ist, wird die entsprechende Zelle im nächsten Zeitpunkt überleben. Die entsprechende SQL-Anweisung lautet: SELECT a.x, a.y FROM [SELECT a.x, a.y, COUNT(*) AS summe FROM cellsNeighbors AS a INNER JOIN cellsStatus AS b ON (a.y=b.y) AND (a.x=b.x) WHERE b.status=1 GROUP BY a.x, a.y] WHERE summe=2 OR summe=3 Die innere Anweisung verknüpft die zwei Tabellen „cellsStatus“ und „cellsNeighbors“ mit „INNER JOIN“. Die Verknüpfungsbedingung ist die Identität der beiden Koordinatenwerte „a.x=b.x AND a.y=b.y“; die innere „WHERE“-Klausel „b.status=1“ sichert, dass nur lebendige Zellen aus der Tabelle „cellsStatus“ berechnet werden; mit der Aggregationsfunktion „COUNT(*)“ und „GROUP BY a.x, a.y“ wird die Auftrittszahl aller Zellen berechnet; wenn diese Anzahl einer Zelle gleich zwei oder drei ist, wird diese Zelle als überlebende Zelle ausgewählt. Nach Ausführung der obigen SQL-Anweisung wird eine Liste vom Typ „ResultSet“ zurückgeliefert, in der alle im nächsten Zeitpunkt überlebenden Zellen gespeichert werden. Anschließend werden die Zellen aus der Tabelle „cellsStatus“ durchgeprüft: sofern sie auch in der ausgelieferten Liste aufgeführt ist, wird das Attribut „nextStatus“ dieser Zelle in der Tabelle „cellsStatus“ als „1“ markiert. UPDATE cellsStatus SET nextStatus=1 WHERE x=i AND y=j 54 Die zwei Parameter „i“ und „“j werden durch Java-Codes ersetzt, die genau die x- und y-Koordinaten der Zellen in der ausgelieferten Liste sind. Diese SQL-Anweisung wird immer aufgerufen, bis alle Zellen in der Liste besucht werden. Die Schleife wird mit Java-Code implementiert: while(rs.next()){ stmt.excute(„EXEC updateStatus(?, ?)“); } Hierbei ist die Variable „rs“ ein Typus von „ResultSet“ und genau die ausgelieferte Liste; für alle in „rs“ gespeicherten x/y-Koordinaten wird die Java-Anweisung „stmt.excute(‚EXEC updateStatus(?,?)’)“ ausgeführt. Die Variable „stmt“ ist ein Typus von „Statement“, die durch die Methode „execute( )“ eine in der Datenbank verbaute SQL-Anweisung aufrufen kann. Nach der Ausführung dieser Schleife werden alle Zellen aus der Tabelle „cellsStatus“ geprüft, für die im nächsten Zeitpunkt überlebenden Zellen werden deren Attribut „nextStatus“ mit „1“ markiert. Und im nächsten Schritt werden dann die neugeborenen Zellen berechnet. 6.4 Berechnung der neugeborenen Zellen Nach der Berechnung der überlebenden Zellen werden nun in diesem Abschnitt die neugeborenen Zellen berechnet. Die folgende Spielregel beschreibt, unter welchen Bedingungen eine tote Zelle im nächsten Zeitpunkt geboren werden kann: ¾ Wenn die tote Zelle genau drei lebendige Nachbarzellen hat, wird sie im nächsten Zeitpunkt geboren. ¾ Sonst bleibt die tote Zelle tot. Die Idee ähnelt dem Algorithmus im letzten Abschnitt. Das Kernproblem besteht noch in der Anzahl der lebendigen Nachbarzellen. Man muss zuerst beachten, dass alle potentiellen neugeborenen Zellen definitiv die Nachbarzellen von aktuell lebendigen Zellen sind. D.h., die Tabelle „cellsNeighbors“ speichert nach der Berechnung der Nachbarzellen der lebendigen Zellen alle möglichen im nächsten Zeitpunkt neugeborenen Zellen. In dieser Phase werden zuerst die toten Nachbarzellen aus der Tabelle „cellsNeighbors“ herausgefunden. Hier muss man beachten, dass alle toten Nachbarzellen sich nicht in der Tabelle „cellsStatus“ befinden, weil bislang nur lebendige Zellen in der Tabelle „cellsStatus“ eingefügt werden. Man kann durch die SQL-Operation „LEFT JOIN“ die zwei Tabellen „cellsNeighbors“ und „cellsStatus“ wieder verbinden, wobei für die toten Nachbarzellen aus der Tabelle „cellsNeighbors“ keine identischen Zellen in der Tabelle „cellsStatus“ zugeordnet werden. Damit kann man alle toten Nachbarzellen auffinden. Anschließend wird mit der SQL Operation „GROUP BY“ und der 55 Funktion „COUNT( )“ die Auftrittszahl der gefundenen Nachbarzellen berechnet. Wenn diese Zahl genau drei ist, wird die entsprechende Zelle im nächsten Zeitpunkt neugeboren. Hier wird das Beispiel aus Abbildung 6.6 wieder betrachtet. Die 24 Nachbarzellen werden berechnet und die entsprechenden Koordinatenwerte in der Tabelle „cellsNeighbors“ gespeichert. Beispielweise ist die Zelle (4,3) in Abbildung 6.5, sie ist die Nachbarzelle von drei lebendigen Zellen (3,3), (4,2), (5,3), d.h. nach Berechnung aller Nachbarzellen tritt die Zelle (4,3) dreimal in der Tabelle „cellsNeighbors“ auf, und ihr Zustand im nächsten Zeitpunkt wird damit als lebendig markiert. Erinnerung an Abbildung 6.5 Bevor die Berechnung der neugeborenen Zellen mit SQL implementiert werden kann, muss man noch ein Randproblem beachten. In einem Koordinatesystem gibt es für jede Zelle acht Nachbarzellen, wenn das Koordinatensystem unendlich ist. Theoretisch ist es möglich, dass ein unbegrenztes Koordinatensystem existiert, aber es ist nicht möglich, solch ein Koordinatensystem zu simulieren. D.h., die graphische Oberfläche, die das Koordinatensystem simuliert, hat eine feste Größe. Die Zellen, die sich genau auf der Grenze befinden (siehe Abbildung 6.7), haben keine acht Nachbarzellen und müssen separat berücksichtigt werden. Abbildung 6.7: Die Zellen auf der Grenze 56 Es gibt nur zwei Fälle für die Zellen, die sich auf der Grenze befinden. Abbildung 6.7 stellt die zwei Fälle dar. Die Zelle (1,1) befindet sich in der Ecke, sie hat nur drei Nachbarzellen, auf der graphischen Oberfläche gibt es insgesamt nur vier solche Zellen. Die Zelle (5,3) steht nicht in der Ecke, sondern auf der Grenze, sie hat fünf Nachbarzellen. Wenn es auf der Grenze genau drei miteinander folgende Zellen gibt, wird zwar eine neue Zelle erzeugt und in der Tabelle „cellsStatus“ gespeichert, aber sie ist nicht sichtbar (siehe Abbildung 6.8). Das hat zur Folge, dass zu viele Zellen in der Tabelle „cellsStatus“ gespeichert, aber nur wenige auf der sichtbaren Oberfläche simuliert werden. Abbildung 6.8: Neugeborene Zelle außerhalb der sichtbaren Oberfläche Man kann natürlich dieses Grenzproblem nicht berücksichtigen, dennoch läuft das System noch. Aber in der Tabelle „cellsStatus“ werden zu viele redundante Zellen existieren, welche die die Effizienz des Algorithmus schwerwiegend beeinflussen. Im Lauf der Zeit wird das System sehr langsam werden. Die Lösung für das Grenzproblem ist aber auch nicht schwer. Bevor die neugeborenen Zellen in die Tabelle „cellsStatus“ eingefügt werden, werden sie daraufhin überprüft, ob sie sich außerhalb der Grenze befinden. Wenn ja, werden solche Zellen gefiltert und nicht in die Tabelle „cellsStatus“ eingefügt. Die entsprechende SQL-Darstellung lautet: INSERT INTO cellsStatus (x, y, status, nextStatus) SELECT cellsNeighbors.x, cellsNeighbors.y, 0, 1 FROM [SELECT COUNT(*) AS summe FROM cellsNeighbors AS cn LEFT JOIN cellsStatus AS cs ON cn.x=cs.x AND cn.y=cs.y WHERE cs.x IS NULL GROUP BY cn.x, cn.y] WHERE summe=3 AND cellsNeighbors.x>-1 AND cellsNeighbors.y>-1 AND cellsNeighbors.y<height AND cellsNeighbors.x<width; 57 In dieser Anweisung wird eine innere Anweisung verbaut. Die innere Anweisung verknüpft die zwei Tabellen „cellsStatus“ und „cellsNeighbors“ mit „LEFT JOIN“, die Bedingung ist der identische Koordinatenwert: „cn.x=cs.x AND cn.y=cs.y“. Die „Where“-Klausel „cs.x IS NULL“ filtert die Zellen, die in der Tabelle „cellsStatus“ gespeichert werden. Weil die neugeborenen Zellen aktuell tot sind, stehen sie nicht in der Tabelle „cellsStatus“. Schließlich wird die Auftrittszahl der verbliebenen Zellen durch die Aggregationsfunktion berechnet. Wenn die Anzahl gleich 3 ist, werden die entsprechenden Zellen im nächsten Zeitpunkt geboren. Hierzu werden noch die gültigen Bereiche definiert: wenn die Zellen außerhalb dieses Bereichs stehen, werden sie durch die äußere „Where“-Bedingung gefiltert. Die Parameter „height“ und „width“ sind die Höhe und Breite des gültigen Bereichs und werden während der Laufzeit durch die realen Werte ersetzt. Zum Schluss werden die neugeborenen Zellen in die Tabelle „cellsStatus“ eingefügt, ihre Attribute „status“ und „nextStatus“ werden als „0“ und „1“ eingesetzt. 6.5 Zustandsübergang Im letzten Schritt werden alle Zellen aus der Tabelle „cellsStatus“ geprüft, um die Simulation für den nächsten Zeitpunkt vorzubereiten: Datei werden diejenigen die, die im nächsten Zeitpunkt sterben werden, aus der Tabelle „cellsStatus“ gelöscht. Die Attributswerte solcher Zellen werden mit „0“ bezeichnet: DELETE * FROM cellsStatus WHERE nextStatus=0; Anschließend werden die Attributswerte aller verbleibenden Zellen aktualisiert. Ihr „status“ wird als „1“ markiert und ihr „nextStatus“ als „0“ markiert. UPDATE cellsStatus SET status = 1, nextStatus = 0; Zum Schluss werden alle Datensätze aus der Tabelle „cellsNeighbors“ gelöscht, um die nächste Berechnung vorzubereiten. Die obigen Schritte laufen immer weiter, bis das System beendet wird. Die Oberfläche aktualisiert sich zu jedem diskreten Zeitpunkt durch Auslesen der Datensätze aus der Tabelle „cellsStatus“ 58 7 Mustererkennung und Musterklassifikation in LifeWatch In diesem Kapitel wird in die Aufgaben einer Mustererkennung und einer Musterklassifikation eingeführt. Insbesondere werden die beiden Verfahren auf GoL bzw. die Entwürfe und die Implementierungen erläutert. 7.1 Mustererkennung Bevor das Verfahren der Mustererkennung in GoL vorgestellt wird, müssen zuerst folgende allgemeine Fragen beantwortet werden: ¾ Was ist ein Muster in dem Kontext GoL? ¾ Was sind die Aufgaben einerMustererkennung in GoL? Der Begriff „Muster“ hat in verschiedenen Kontexten unterschiedliche Bedeutungen. Um den Begriff „Muster“ in GoL zu definieren, werden vorab die zwei Begriffe „Zusammenhangskomponente“ und „Ein dem Gitter entsprechender Graph“ vorgestellt. ¾ Zusammenhangskomponente: Ein ungerichteter Graph G=(V, E) heißt genau dann zusammenhängend, wenn es für jedes Knotenpaar(v1, v2), wobei v1, v2 Elemente der Knotenmenge V sind, einen Weg von v1 nach v2 gibt. Eine Zusammenhangskomponente von G ist ein maximaler zusammenhängender Untergraph von G [Blu01]. ¾ Ein dem Gitter entsprechender Graph: alle lebendigen Zellen in dem Gitter können einen entsprechenden Graphen G=(V, E) abbilden, wobei die Knotenmenge V eine Menge von lebendigen Zellen ist; je zwei benachbarte 59 Zellen werden mit einer Kante verbunden, die die Kantemenge E aufstellt (siehe Abbildung 7.1). Abbildung 7.1: Ein dem Gitter entsprechender Graph Mithilfe der obigen Begriffe kann man jetzt den Begriff „Muster“ in GoL spezifizieren: „Ein Muster in GoL ist die maximale Zusammenhangskomponente im entsprechenden Graphen“. Jeder Knoten (jede lebendige Zelle in GoL) gehört genau zu einem Muster und jedes Muster enthält mindestens einen Knoten. Dabei kann die zweite Frage beantwortet werden: Die Zustände der Zellen im GoL werden in jeder Zeiteinheit durch den Simulator aktualisiert, entsprechend werden die davon abhängigen Muster auch in jeder Zeiteinheit verändert. Das Ziel des Mustererkennung ist, dass alle Muster in jeder Zeiteinheit zu erkennen. In folgenden Abschnitt werde ich zwei Algorithmen für die Mustererkennung vorstellen, nämlich einen instanzorientierter Algorithmus, und einen mengenorientierter Algorithmus. Man spricht von einem instanzorientierter Algorithmus, wenn ein Algorithmus einmal für jede Instanz ausgeführt wird; ein Algorithmus heißt mengenorientiert, wenn er einmal für alle Instanz ausgeführt wird. 7.1.1 Instanzorientierter Algorithmus zur Mustererken- nung Das erwartete Ergebnis nach dem Erkennungsalgorithmus ist die Gruppierung aller lebendigen Zellen aus der Tabelle „cellsStatus“. Jedes Muster besteht aus Zellen, die zur gleichen Gruppe gehören. Deshalb muss zuerst die Struktur der Tabelle „cellsStatus“ dahin gehend modifiziert werden, das Spalte „groupID“ eingefügt wird, um die Gruppengehörigkeit jeder Zelle zu bezeichnen. Bevor alle Zellen gruppiert werden, 60 werden die Werte ihrer „groupID“ als „–1“ eingesetzt. Das bedeutet, dass diese Zelle noch nicht gruppiert ist. Nach der Ausführung des Erkennungsalgorithmus werden die „groupID“ aller Zellen mit einer positiven Ganzzahl markiert. Die folgende Abbildung stellt den Ablauf der Mustererkennung dar: Abbildung 7.2: Ablaufdiagramm der Mustererkennung Zuerst wird irgendeine Zelle A von der Tabelle „cellsStatus“ ausgewählt und dann eine Breitensuche für A durchgeführt. Nach Durchführung wird eine Zusammenhangskomponente gefunden und die „groupID“ seiner Zellen mit einer positiven Ganzzahl eingesetzt. Danach wird die Tabelle „cellsStatus“ geprüft, ob es noch Zellen gibt, die noch nicht gruppiert werden. Wenn nicht alle Zellen gruppiert werden, wird der obige Ablauf wiederholt aufgerufen. Sonst werden alle Zellen gruppiert, das Erkennungsverfahren wird beendet. Der innere Ablauf „Breitensuche für A“ [Ott02] führt einen weiteren Prozess aus, um eine Zusammenhangskomponente zu finden, der Startknoten ist A. Der Ablauf dieser Breitensuche wird in der Abbildung 7.3 dargestellt: 61 Abbildung 7.3: Breitensuche In der SQL-Implementierung wird die Liste L mit der Tabelle „cellsStatus“ ersetzt und der Knoten A wird als Anfangsknoten ausgewählt, seine „groupID“ wird mit „–1“ eingesetzt. Daraufhin wird die Tabelle „cellsStatus“ überprüft, ob es noch Zellen in „cellsStatus“ gibt, die noch nicht gruppiert sind, D.h., einen „groupID“ gleich „–1“. SELECT TOP 1 x, y FROM cellsStatus WHERE groupID = -1 ORDER BY x, y Die Zellen im der „cellsStatus“, die noch nicht gruppiert sind, werden zuerst nach ihren Koordinaten sortiert; die erste Zelle in der Tabelle „cellsStatus“ wird ausgewählt und ihre „groupID“ mit „id“ eingesetzt. UPDATE cellsStatus AS table1, [SELECT TOP 1 x, y FROM cellsStatus WHERE groupid=-1 ORDER BY x, y] AS table2 SET groupid = id WHERE table1.x=table2.x AND table1.y=table2.y; 62 Hier wird die SQL-Operation „UPDATE“ verwendet, weil nur die Attribute „groupID“ aktualisiert werden. Die innere Selektion ordnet alle nicht gruppierten Zellen an, wobei immer die Zelle mit dem kleinsten x- und y-Koordinatenwert zunächst ausgewählt wird. Diese Zelle ist die links oben auf Zelle in der Oberfläche; anschließend wird das Attribut „groupID“ der ausgewählten Zelle mit einem ganzzahligen Parameter „id“ ersetzt. Danach werden alle Nachbarzellen des Knotens, deren „groupID“ gleich „id“ sind, ausgewählt, und ihre „groupID“ auch mit „id“ eingesetzt. UPDATE cellsStatus AS table1, [SELECT t1.x, t1.y FROM cellsStatus AS t1 INNER JOIN ( (SELECT x, y FROM cellsStatus WHERE groupID=id ) AS t2) ON ABS(t1.x-t2.x)<2 AND ABS(t1.y-t2.y)<2 WHERE t1.groupid=-1] AS table2 SET table1.groupID = id WHERE table1.x=table2.x AND table1.y=table2.y; Die innere Anweisung verknüpft zwei Mengen „t1“ und „t2“ von den Zellen, die Menge „t1“ besteht aus den Zellen, die in der Tabelle „cellsStatus“ mit „groupid“ gleich „–1“ (WHERE t1.groupid=-1), weil diese Zellen noch nicht gruppiert sind. Die andere Menge „t2“ besteht aus den Zellen, die im letzten Schritt ausgewählt und mit einer „groupID“ markiert werden (SELECT x, y FROM cellsStatus WHERE groupID=id ). Die Verknüpfungsbedingung ist die Nachbarschaft der Zellen aus beiden Mengen (ON ABS(t1.x-t2.x)<2 AND ABS(t1.y-t2.y)<2). Die in „t1“ zugeordneten Zellen sind die Zellen, denen „groupID“ mit dem Parameter „id“ aktualisiert werden sollen. Die obige Anweisung wird so lange wiederholt, bis keine nicht gruppierte Nachbarzellen für die Zellen, denen „groupID“ gleich „id“ ist, gefunden werden. Nach dem obigen Verfahren wird ein Muster erkannt. Danach werden alle Zellen in der Tabelle „cellsStatus“ geprüft. Wenn es noch Zellen gibt, die noch nicht gruppiert sind, wird die ganze Schleife wieder aufgerufen, und für ein neues Muster wird eine neue „groupID“ angegeben. Die Schleife, um alle Muster in der Tabelle „cellsStatus“ zu erkennen, wird vom Java-Programm angesteuert. Im Folgenden wird das entsprechende Java-Programm erläutert: 63 private synchronized void cellsGrouping() { boolean isEmpty;//steuert innere Schleife boolean flag = true; //steuert ganze Schleife int i, j;//Ausgabe nach Durchführung der SQL-Anweisungen int groupID = 0;//id für das erste Muster, folgende um 1 erhöht try { while(flag) { isEmpty = false; cStmt = conn.prepareCall("{call updateFirstGroupID(?)}"); cStmt.setInt(1, groupID); j = cStmt.executeUpdate(); if(j == 0) { flag = false; } else { while(!isEmpty) { cStmt = conn.prepareCall("{call updateGroupID(?)}"); cStmt.setInt(1, groupID); i = cStmt.executeUpdate(); if(i == 0) { isEmpty = true;//innere Schleife endet groupID++;//„groupID“ für nächster Muster } } } } ...... Die boolesche Variable „flag“ bezeichnet, ob es noch Zellen in der Tabelle „cellsStatus“ gibt, denen „groupID“ gleich –1 sind,d.h. sie sind noch nicht gruppiert. Wenn „flag“ gleich falsch ist, wird die ganze Schleife beendet, das bedeutet, dass alle Zellen gruppiert sind, die andere boolesche Variable „isEmpty“ steuert die innere Schleife und bezeichnet, ob es noch nicht gruppierte Zellen gibt, die mit den schon gruppierten Zellen benachbart sind. Wenn ja, werden alle Zellen in eine Gruppe eingefügt, ansonsten wird die innere Schleife beendet und ein Muster gefunden. Der Algorithmus ist instanzorientiert, weil zunächst immer eine Zelle ausgewählt wird, und dann alle folgenden Suchen beginnen. Das ist eine typische sequentielle Verarbeitung. Der Algorithmus wird für mehre Datensätze aufgerufen, d.h., wenn es z.B. fünf Muster in der Tabelle „cellsStatus“ gibt, wird der Algorithmus fünfmal aufgerufen. Gegenüber dem instanzorientierten Algorithmus gibt es noch einen mengenorien64 tierten Algorithmus, der nicht für einzelne Datensätze, sondern für alle Datensätze aufgerufen wird und nach wenigen Schritten werden alle Muster erkannt. Der mengenorientierte Algorithmus ist eine parallele Berechnung. Im nächsten Abschnitt wird ein mengenorientierter Algorithmus entworfen und implementiert, er ist deutlich effizienter und intelligenter als der instanzorientierte Algorithmus. 7.1.2 Mengenorientierter Algorithmus zur Mustererken- nung Für einen mengenorientierten Algorithmus muss man beachten, dass der Algorithmus nicht für jede Zelle aufgerufen wird. Im letzten Abschnitt wurde zunächst immer eine noch nicht gruppierte Zellen links oben ausgewählt und dann für jede solche Zellen wird der Algorithmus aufgerufen. In diesem Abschnitt wird ein Algorithmus entworfen, bei dem nach wenigen Aufrufen des Algorithmus allen Zellen gruppiert sind. Am Anfang wird jede Zelle als ein einzelnes Muster betrachtet und mit einer einzigartigen numerischen Identifikation markiert. D.h., die Struktur der Tabelle „cellsStatus“ wird wieder modifiziert, um eine Spalte „id“ einzufügen. Von ihren Positionsbeziehungen wird eine Adjazenzmatrix erstellt, damit man prüfen kann, ob die jeweiligen beiden Zellen benachbart sind. Diese Adjazenzmatrix wird mit einer Tabelle „adjacencyMatrix“ materialisiert, die vier Spalten hat: „cells_1“, „groupID_1“, „cells_2“ und „groupID_2“. Sie protokolliert die entsprechenden zwei benachbarten Zellen mit ihren jeweiligen „id“ und „groupID“. Abbildung 7.4: AdjacencyMatrix Die Abbildung unten links stellt einen Graph dar, worauf sechs Zellen stehen, je zwei benachbarte Zellen werden mit einer Kante verbunden. Und die Abbildung unten rechts ist die entsprechende Adjazenzmatrix, die die Informationen über die Nachbar65 schaft zwischen den Zellen angibt. Der „0“-Wert bedeutet, dass die zwei Zellen nicht benachbart sind, und der „1“-Wert bedeutet, dass sie benachbart sind. Jede Zelle ist mit sich selbst benachbart. Abbildung 7.5: Adjazenzmatrix Die Adjazenzmatrix ist symmetrisch gegen die Diagonale, d.h. die Informationen über die Benachbarkeit in der Matrix sind redundant. Hier werden nur die Informationen über die Diagonale berücksichtigt, die im Graph mit rot gefärbt werden. Die Idee des Algorithmus besteht darin, dass wenn zwei Zellen benachbart sind, ihnen die gleiche Identifikation vergeben wird, die genau die kleinere Identifikation der beiden Zellen ist. Dieses Verfahren wird solange wiederholt, bis keine Zellen ihre Identifikationen aktualisieren brauchen. Die Adjazenzmatrix bleibt zwar fest, aber die Tabelle „adjacencyMatrix“ muss bei jedem Schritt neu erstellt werden, weil die „groupID“ der Zelle bei jedem Schritt aktualisiert wird. Die Abbildung 7.6 stellt das Ablaufdiagramm des mengenorientierten Algorithmus dar: Im ersten Schritt „Initialisierung“ werden alle Zellen aus der Tabelle „CellsStatus“ mit eindeutigen numerischen Identifikationen zugewiesen; anschließend werden beim Schritt „GenerateAJMatrix“ die Informationen über die Adjazenzmatrix generiert und in der Tabelle „adjacencyMatrix“ gespeichert; im Schritt „UpdateCellsGID“ werden die „groupID“ der Zellen nach der Adjazenzinformation aus der Tabelle „adjacencyMatrix“ aktualisiert, Wenn es im letzten Schritt noch Zellen gibt, die aktualisiert werden, dann wird die Tabelle „adjacencyMatrix“ ausgeleert und weiter nach den aktuellen Zuständen der Zellen die neue Adjazenz-Information generiert; dieser Schritt wird immer wieder aufgerufen bis keine Zellen mehr aktualisiert werden. 66 Abbildung 7.6: Ablaufdiagramm Die neue eingefügte Spalte „id“ wird in die Access-Datenbank als Datentyp „AutoNumber“ eingestellt, um zu sichern, dass keine Zelle mit gleicher „id“ existiert. Am Anfang des Algorithmus werden alle Zellen als ein separates Muster betrachtet, wodurch ihre „groupID“ mit ihrer jeweiligen eigenen „id“ ersetzt wird. UPDATE cellsStatus, SET groupID=id Anschließend wird die Tabelle „adjacencyMatrix“ gefüllt. Die Paare von Zellen, die benachbart sind, werden gesucht und ihre „id“ und „groupID“ werden in der Tabelle „AdjacencyMatrix“ protokolliert. INSERT INTO adjacencyMatrix (cell_1, groupID_1, cell_2, groupID_2) SELECT a.id, a.groupID, b.id, b.groupID FROM cellsStatus AS a INNER JOIN cellsStatus AS b ON(ABS(a.x-b.x)<2)AND(ABS(a.y-b.y)<2)AND(a.groupID<b.groupID); 67 Die Anweisung verknüpft zunächst zwei Kopien der Tabelle „cellsStatus“ mit „INNER JOIN“, die JOIN-Bedingung besteht einerseits in der Nachbarschaft beider Zellen, anderseits in der Filterung der Informationen (a.groupID<b.groupID) durch die Duplikate, weil die Adjazenzmatrix symmetrisch gegen die Diagonale ist. Dann werden alle gefundenen Zellenpaare in der Tabelle „adjacencyMatrix“ gespeichert. Nach Generierung der Adjazenzmatrix gibt es jetzt genug Informationen, welche Zellen benachbart sind und noch nicht gruppiert sind. Die folgende SQL-Anweisung aktualisiert die „groupID“ der Zellen in der Tabelle „cellsStatus“. Die „groupID“ von zwei Zellen, die sich als Paar in der Tabelle „adjacencyMatrix“ befinden, werden vereinigt. Die vereinigte „groupID“ ist genau die kleinere „groupID“ beider Zellen. UPDATE cellsStatus AS a, adjacencyMatrix AS b SET a.groupID = b.groupID_1 WHERE a.id = b.cell_2; Nach Ausführung der obigen Anweisung wird das Ergebnis im Java-Programm geprüft, ob es noch Zellen gibt, bei denen ihre „groupID“ noch nicht aktualisiert wurde. Wenn ja, wird die Tabelle „adjacencyMatrix“ ausgeleert und eine neue Schleife weiter ausgeführt; ansonsten tritt das Programm aus der Schleife aus, alle Zellen sind gruppiert, d.h., alle Muster werden erkannt. Der in diesem Abschnitt vorgestellte Algorithmus ist mengenorientiert. Gegenüber dem instanzorientierten Algorithmus werden keine speziellen Zellen vorher ausgewählt, sondern es wird eine parallele Berechnung durchgeführt. Der Zeitaufwand bei diesem Algorithmus ist geringer.. Trotzdem gibt es bei diesem Algorithmus noch ein Problem: Alle Informationen in der Tabelle „adjacencyMatrix“ werden nur zwischengespeichert, sie werden bei jeder Schleife generiert und dann direkt gelöscht. D.h., die Tabelle „adjacencyMatrix“ braucht nicht materialisiert zu werden.. In nächsten Abschnitt wird eine weitere Optimierung des mengenorientierten Algorithmus entworfen, damit die „adjacencyMatrix“ nur virtualisiert wird. 7.1.3 Erweiterte Optimierung Ziel der Optimierung ist Virtualisierung der Tabelle „adjacencyMatrix“, d.h., die Adjazenzinformation aller Zellen aus der Tabelle „cellsStatus“ wird nur zwischenberechnet und braucht nicht in einer Tabelle gespeichert werden. Das Ablaufdiagramm ist genau wie Abbildung 7.7, der einzige Unterschied besteht darin, dass die beiden Schritte „Erzeuge Adjazenzmatrix“ und „Zustandsübergang“ zusammengebaut werden. 68 Abbildung 7.7 Ablaufdiagramm der optimierten Algorithmus zur Mustererkennung Die Idee dieses Algorithmus besteht darin, dass für jede Zelle A aus der Tabelle „cellsStatus“ eine Nachbarzelle B mit kleiner „groupID“ gesucht wird. Wenn eine solche Zelle B existiert, wird die „groupID“ von A durch „groupID“ von B ersetzt. Abbildung 7.8: optimierter mengenorientierter Algorithmus Im ersten Schritt wird die „groupID“ der Zellen wie im letzten Abschnitt initialisiert: UPDATE cellsStatus, SET groupID=id 69 Dann wird die folgende SQL-Anweisung ständig aufgerufen, bis keine Datensätze mehr aktualisiert werden: UPDATE cellsStatus AS a, cellsStatus AS b SET a.groupID = b.groupID WHERE (b.groupID<a.groupID) AND (abs(b.y-a.y)<2) AND (abs(b.x-a.x)<2); 7.2 Musterklassifikation Nach erfolgreicher Erkennung der Muster gibt es jetzt die Möglichkeit, die Muster zu klassifizieren. Bevor ein unbekanntes Muster klassifiziert werden kann, muss vorab das schon erkannte Muster als Standardmuster in einer Tabelle gespeichert werden, durch Vergleich der Eigenschaften der unbekannten Muster und Standardmuster erfolgt erst der Klassifikationsprozess. In diesem Abschnitt wird ein Muster „Glider“ als Beispiel verwendet, um den Klassifikationsprozess zu erläutern. Ein „Glider“ besteht aus 5 Zellen, das Bild links in der Abbildung 7.1 stellt einen „Glider“ dar. Hier muss man beachten, dass es vier Varianten von Mustern gibt, die nach Drehung von 90 Grad gleich sind. Und für jede Variante gibt es noch eine Spiegelvariante. D.h. für jedes Muster gibt es insgesamt acht Varianten, die die gleiche Figur sind. Das Bild rechts in der Abbildung stellt die acht Varianten von „Glider“ dar. Abbildung 7.9: Muster „Glider“ und acht Varianten Während der Laufzeit des Systems werden möglichweise unterschiedliche Varianten von „Glider“ auftauchen. Die Aufgabe des Systems im Klassifikationsprozess ist es, alle auftretenden Varianten von „Glider“ taktweise zu klassifizieren. 70 7.2.1 Normalisierung der Koordinaten der Zelle Nach dem Erkennungsprozess werden alle Muster in der Tabelle „cellsStatus“ markiert. Jede Menge von den Zellen, die mit gleicher „groupID“ sind, baut ein Muster zusammen. Beim Klassifikationsprozess wird noch zusätzlich eine Tabelle benötigt, die die Standardmuster speichern kann. Das Standardmuster ist das Muster, in das die Koordinaten ihrer Zellen normalisiert werden. Bevor die Normalisierung der Koordinaten erklärt wird, wird zuerst der Begriff „Die Bounding Box des Muster“ erläutert. Def 7.1: Bounding Box des Musters Sei M ein Muster, n die Anzahl der Zellen von M, (xi, yi) sind die Koordinaten von Zelle i, Ein Rechteck r(x, y, width, height) heißt die Bounding Box von M, wobei (x, y) die Koordinate des links obersten Scheitelpunktes r ist, width und height die Breite und Height von r sind, gdw. x = MIN(xi), y = MIN(yi) width = MAX(xi) - MIN(xi) und height = MAX(yi) - MIN(yi) Die folgende Abbildung stellt eine Bounding Box von einem Variante von „Glider“ dar Mithilfe des Bounding Box des Musters kann jetzt ein Standardmuster definiert werden. Def 7.2: Standardmuster Ein Muster heißt Standardmuster genau dann wenn, die Koordinaten des obersten linken Scheitelpunktes seiner Bounding Box gleich (0, 0) ist. Offensichtlich kann einfach durch Verschiebung der Muster in dem Koordinatensystem irgendein Muster in das Standardmuster transformiert werden. Im diesem Fall wird diese Verschiebungsverfahren auch als Normalisierungsverfahren der Koordinaten der Zellen des Musters genannt. Abbildung 7.10: Normalisierungsverfahren 71 Seien ein Muster M = {(x1,y1), ...,(xn, yn)} und seine Bounding Box r(M) = (x,y,width, height) nach dem Normalisierungsverfahren werden die neuen Koordinaten der Zellen sein: {(x1-x, y1-y),...,(xn - x, yn - y)}. Jedes Muster hat zwei Eigenschaften, damit es von anderen Mustern unterschieden werden kann: ¾ Anzahl der Zellen ¾ Positionsinformationen der gehörigen Zellen Als Beispiel wird eine Tabelle „gliderFamily“ erstellt, die drei Attributen haben: „x“, „y“ und „variantID“, um die Koordinaten der acht Varianten von „Glider“ zu speichern. Das Attribut „variantID“ bezeichnet, welche fünf Zellen zu einer Variante von „Glider“ gehören. Die Tabelle speichert nur die Standardmuster. 7.2.2 Algorithmus der Klassifikation Um ein gegebenes Muster zu beweisen, dass es eins Variante von „Glider“ ist, wird zuerst geprüft, ob die Anzahl seiner Zellen „fünf“ ist. Wenn ja, dann ist er ein Glider-Kandidat, sonst nicht. Abbildung 7.11: Ablaufdiagramm der Klassifikation 72 Danach werden die Koordinaten seiner Zellen normalisiert; zuletzt werden die Koordinaten der Zellen mit den Standardmustern verglichen, die in der Tabelle „gliderFamily“ gespeichert sind. Wenn die Koordinaten aller Zellen der Muster mit einem Standardmuster übereinstimmen, kann man sicher sein, dass das Muster auch eine Variante von „glider“ ist. Die Abbildung 7.11 zeigt den Ablauf dieses Algorithmus. 7.2.3 Implementierung In diesem Abschnitt wird erläutert, wie mit Hilfe des Klassifikationsverfahrens ein „glider“ aus der Tabelle „cellsStatus“ herausgefunden werden kann. Weil ein „glider“ aus fünf Zellen besteht, werden im ersten Schritt alle Muster in der Tabelle „cellsStatus“ geprüft, ob die Anzahl ihrer Zellen „fünf“ ist. Solche Muster, deren Zellenanzahl gleich fünf ist, werden als „glider“-Kandidaten bezeichnet und ihr Attribut „groupID“ wird ausgewählt. selectGliderKandidaten SELECT groupID, mx, my FROM SELECT groupID, MIN(x) AS mx, MIN(y) AS my,COUNT(*) AS summe FROM cellsStatus GROUP BY groupID WHERE summe = 5 Mit der inneren SQL-Anweisung werden alle Muster ausgewählt und ihre Zellenanzahl wird mit der Funktion „COUNT(*)“ berechnet; anschließend wird die „groupID“ von „glider“-Kandidaten protokolliert, deren Zellenanzahl gleich fünf ist, und der oberste linke Scheitelpunkt der entsprechenden Bounding Box des Musters wird auch in diesem Schritt berechnet. Die Berechnung des Scheitelpunktes ist die Vorbereitung für das Normalisierungsverfahren im nächsten Schritt. In zweitem Schritt werden die Normalisierungsverfahren für alle „glider“-Kandidaten durchgeführt. Jeder Kandidat wird nach links um x Einheiten und nach oben um y Einheiten verschoben, wobei (x, y) genau die entsprechenden Koordinaten des obersten linken Scheitelpunkts des Bounding Box des Musters sind. Normalisierung SELECT x-mx AS nx, y-my AS ny FROM cellsStatus WHERE groupID=gid 73 Hierbei sind „mx“, „my“ und „gid“ die Parameter, die von den Ergebnissen im ersten Schritt übergeben werden. Nach Ausführung dieser Anweisung werden die Koordinaten der „glider“-Kandidaten normalisiert und im nächsten Schritt mit der Koordinaten der Standardmuster, die in der Tabelle „gliderFamily“ gespeichert sind, verglichen. Im dritten Schritt wird das Vergleichungsverfahren durchgeführt. Die Koordinaten der „glider“-Kandidaten werden mit den Koordinaten der acht Varianten von „glider“Standardmustern verglichen. Wenn die Koordinaten aller fünf Zellen des Kandidaten mit einer Variante von „glider“ übereinstimmen, dann ist dieser Kandidat ein „glider“. compareWithStandard SELECT COUNT(*) FROM (Ergebnisse aus Normalisierung(?,?,?) ) AS table1 INNER JOIN (SELECT b.x, b.y FROM GliderFamily As b WHERE VariantID=vid) AS table2 ON (table1.ny=table2.y) AND (table1.nx=table2.x); In dieser Anweisung werden die Ergebnisse aus Normalisierung als virtuelle Tabelle „table1“ verwendet; die andere innere Anweisung liefert auch eine virtuelle Tabelle, die eine von acht Varianten von „Glider“ enthält, wobei der Parameter „vid“ eine Variante von „glider“ bezeichnet. Die beiden Tabellen werden mit „INNER JOIN“ verknüpft, die Funktion COUNT(*) berechnet nach der Verknüpfung die Anzahl der Datensätze. Wenn die Anzahl genau fünf ist, ist der Kandidat ein „Glider“. Das Vergleichungsverfahren wird innerhalb einer Schleife ausgeführt und der Kandidat nacheinander mit den acht Varianten von „Glider“ verglichen. Sobald der Kandidat einer Variante entspricht, wird die Schleife beendet, ansonsten wird das Verfahren maximal achtmal durchgeführt, um mit allen acht Varianten zu vergleichen. Die Schleife ist mit Java-Code implementiert und der Parameter „vid“ wird auch vom Java-Code mit einem Wert übergeben (siehe den Code in der nächsten Seite). Die FOR-Anweisung definiert eine Schleife mit acht Iterationen, um das Vergleichungsverfahren durchzuführen. Bei jeder Schleife wird der Parameter „vid“ einen neuen Wert übergeben, um die acht verschiedenen Varianten aufzurufen. Die Variable „zahl“ speichert das Ergebnis aus der SQL-Anweisung für den dritten Schritt. Wenn „zahl“ gleich fünf ist, wird die Schleife mit dem Befehl „break“ abgebrochen, ansonsten mit dem Befehl „continue“ weiter ausgeführt. 74 Nach der Schleife ist klar, ob ein „Glider“-Kandidat ein „Glider“ ist. Wenn man andere Muster klassifizieren will, muss man zuerst eine neue Tabelle für das Muster erstellen, um seine acht Standardvarianten zu speichern. Danach wird das gleiche Verfahren wie bei der „Glider“-Klassifikation durchgeführt. for( int i = 1; i <= 8; i++ ) { stmt.execute("EXEC compareWithStandard @vid=" + i); ResultSet rs1 = stmt.getResultSet(); if(rs1.next()) { int zahl = rs1.getInt(1); if (zahl == 5){ break; } else continue; } Das oben erläuterte Verfahren hat einen deutlichen Nachteil: Es wird zu viel Java-Code verwendet. Die Schleife wird mit Java-Code implementiert und viele Parameter werden noch durch Java-Code übergeben, was die Effizienz des Klassifikationsverfahrens maßgeblich beeinflusst. 75 8 Zusammenfassung In dieser Diplomarbeit wurde eine Monitoringsystem „LifeWatch“ entworfen und implementiert, dass einen diskreten Prozess überwachen und analysieren kann. Dieser diskrete Prozess wurde in dieser Arbeit mit „Game Of Life“ (kurz GoL) simuliert. Im Lauf von GoL überwacht „LifeWatch“ die Zustände der Zellen aus GoL in jedem diskreten Zeitpunkt; analysiert synchron die Daten der Zellen, z.B. ihre Koordinaten und Zustände; erkennt die auftauchenden Muster, die aus lebendigen Zellen bestehen und klassifiziert schließlich die Muster. Bei der Entwicklung von „LifeWatch“ und GoL-Simulator spielt die Datenbanktechnik eine wichtige Rolle: Die Informationen über die Zellen im GoL werden in der Datenbank protokolliert; die Algorithmen, womit die Muster erkannt und klassifiziert werden, werden mit der Datenbanksprache SQL dargestellt und in der Datenbank als View eingebettet; durch JDBC werden die Views von der Programmiersprache aufgeruft und ausgeführt. Mit der Anwendungen einer solchen Datenbanktechnik ergeben sich folgende Vorteile: ¾ Die Daten sind dauerhaft zu speichern. ¾ Die Source-Code-Datei wird kürzer und sauberer. ¾ Softwareentwicklung und Datenbankentwicklung werden getrennt. Im Lauf von GoL werden die Echtzeit-Informationen über die Zellen in der Tabelle von der Access-Datenbank protokolliert und in jedem Zeitpunkt aktualisiert. Nach Beenden des GoL-Simulators gehen die letzten berechneten Informationen nicht verloren, sondern werden in der Datenbank gespeichert und sind wiederverwendbar. Diese Funktionalität wird mit Unterstützung der Datenbank einfach erweiterbar, wenn z.B. der Benutzer nach Beenden der Applikation eine Statistik erstellen möchte, z.B. wie viele „Glider“ während der Laufzeit aufgetaucht sind oder in welcher Generation es „Glider“ gibt. Um solche Frage zu beantworten, braucht man nur eine zusätzliche 76 Tabelle in der Datenbank zu erstellen, dann werden alle vergangenen Informationen dauerhaft gespeichert und einfach abgefragt. Alle Abfragen auf der Datenbank werden als View in die Datenbank eingebettet, und in der Source-Code-Datei gibt es fast kaum SQL-Statements, damit der Souce-Code besser verstehen wird. Und: Wenn der Softwareentwickler kein SQL kennt, kann er trotzdem das datenbankunterstützte Monitoringsystem „LifeWatch“ implementieren, solange er JDBC und damit die entsprechende View aufrufen kann. Als Beispiel hat das „LifeWatch“ in dieser Arbeit nur einen „Glider“ von GoL überwachtet. Diese Funktionalität ist in Zukunft noch erweiterbar. Weil das Begriff in dieser Arbeit in einem engeren Sinn definiert wird: Ein Muster in GoL ist die maximale Zusammenhangskomponente im entsprechenden Graph (siehe Kapitel 6). Tatsächlich gibt es manche Figuren im GoL, die zwar nicht aus aufeinander folgenden Zellen bestehen, aber während der Laufzeit stabile Figuren beinhalten können, z.B. das so genannte „Lightweight Spaceship“: Lightweight Spaceship Nach der Definition von Muster ist „Lightweight Spaceship“ nicht ein Muster und damit nicht durch „LifeWatch“ erkennbar. Aber es verändert sich periodisch, d.h. seine Figur ist während der Laufzeit periodisch aufgetaucht. Solche Figuren sollen als generische Muster definiert werden. „LifeWatch“ soll auch solche Muster erkennen und klassifizieren. Diese Funktionalität kann in dieser Arbeit leider nicht realisiert werden. Hier sei aber die Idee zur Erkennung der generischen Muster kurz vorgestellt. Einige generische Muster sind keine Zusammenhangskomponenten, wie z.B. „Lightweight Spaceship“, und haben damit auch keine allgemeinen Eigenschaften zu spezifizieren. Deshalb muss man für jedes einzelne generische Muster spezifische Erkennungsverfahren entwerfen. Als Beispiel betrachten wir noch das Muster „Lightweight Spaceship“ (kurz LSship). LSship besteht aus neun Zellen, acht davon sind zusammenhängend. Man kann zunächst die acht Zellen als ein Teilmuster t_LSship betrachten und mit den Verfahren von Mustererkennung und Musterklassifikation, die in dieser Arbeit vorgestellt wurden, problemlos erkennen. Die Frage ist, wie die letzte Zelle, die separat steht, bestimmt werden kann. Eine Alternative ist die Zuhilfenahme der „Bounding Box“ von LSship. Die „Bounding Box“ von LSship und die von t_LSship sind gleich und wer77 den schon während des Klassifikationsverfahrens ausgegeben. Die letzte separate Zelle steht genau in der Ecke der „Bounding Box“. Man braucht nur zu prüfen, ob es eine lebendige Zelle gibt, die in der Ecke der „Bounding Box“ steht und nicht mit anderen Zellen benachbart ist. Wenn dies der Fall ist, kann man sagen, dass die separate Zelle und t_LSship zusammen ein LSship aufgebaut haben. Der wichtige Schritt für die Erkennung der generischen Muster ist die Analyse der Struktur der Muster, erst danach kann man ein spezifisches Verfahren entwerfen. Leider habe ich keine allgemeine Lösung für generische Muster. Und diese Arbeit ist noch erweiterbar, um mehre Muster zu erkennen und zu klassifizieren. 78 Literaturverzeichnis [Neu66] J. von Neumann „Theory of Self-Reproducing Automata“ Univ. of Illinois Press, 1966 [Wol82] S. Wolfram „Statistical Mechanics of Cellular Automata“ Caltech Preprint CALT-68-915, May 1982 [Man04] R. Manthey Vorlesung „Informationssystem“ Institut für Informatik, Univ. Bonn, 2004 [Kün07] T. Künneth „Einstieg in Eclipse 3.3“ Galileo Press, 2007 [Vos00] G. Vossen „Datenmodell, Datenbanksprachen und Datenbankmanagementsysteme“ Oldenbourg, 2000 [Kem06] A. Kemper, A. Eickler „Datenbanksysteme – Eine Einführung“ Oldenbourg, 2006 [Mol06] A. Molinaro, J. Gennick „SQL Cookbook“ O’Reilly, 2006 [Eck06] B. Eckel „Thinking in Java“ Prentice Hall International, 2006 [Kni06] G. Kniesel Vorlesung „Einführung in die Softwaretechnologie“ Institut für Informatik, Univ. Bonn, 2006 [Dau07] B. Daum „Rich-Client-Entwicklung mit Eclipse 3.3“ Dpunkt Verlag, 2007 79 [Deh03] W. Dehnhardt „Java und Datenbanken“ Hanser Fachbuchverlag, 2003 [Mat05] B. Matzke „Ant: Eine praktische Einführung in das Java-Build-Tool“ Dpunkt Verlag, 2003 [Blu01] N. Blum „Theoretische Informatik“ Oldenbourg, 2001 [Ott02] T. Ottmann, P. Widmayer „Algorithmen und Datenstrukturen“ Spektrum Akademischer Verlag, 2002 [Min05] A. Minhorst „Das Access 2003-Entwicklerbuch“ Addison-Wesley, 2005 [For07] P. Forbrig „Objektorientierte Softwareentwicklung mit UML“ Hanser Fachbuchverlag, 2007 [Fre04] Er. Freeman, El. Freeman, B. Bates, K. Siera, M. Loukides „Head First Design Patterns“ O’Reill Media, 2004 80 Ich, Dexin Chen (Student der Informatik an der Rheinischen Friedrich-Wilhelms-Universität Bonn, Matrikelnummer 1521257), versichere an Eides statt, dass ich die vorliegende Diplomarbeit selbstständig verfasst und keine anderen als die angegebenen Hilfsmittel verwendet habe. Die Arbeit wurde in dieser oder ähnlicher Form noch keine Prüfungskommision vorgelegt. Dexin Chen 81