PDF file - IDB - Universität Bonn

Werbung
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
Herunterladen