|
|
|
|
|
Der nächste Schritt der Vervollständigung des
Programms besteht darin, dem Manager zu ermöglichen, die
Zeit weiterzuschalten. Es wird das
"Manager"-Menü des Büros
erstellt. Es wird ein Menüpunkt benötigt, der eine
Aktion auslöst, die das vom Shop
verwendete Timer -Objekt ermittelt und
es weiterschaltet. Um den Menüpunkt als ersten des
Menüs einzubauen, wird in der Methode getDefaultMenuSheet zuerst eine neues
Menü für die Wartung angelegt und dort ein neuer
Eintrag eingefügt. In die Klasse Office wird
folgender Code geschrieben:
|
Menü des Managers
|
|
 |  |  |
 |
public MenuSheet getDefaultMenuSheet()
{
MenuSheet msSubMenu = new MenuSheet("Maintenance");
msSubMenu.add(new MenuSheetItem("Advanced time",
new sale.Action()
{
public void doAction(SaleProcess p, SalesPoint sp)
{
Shop.getTheShop().getTimer().goAhead();
}
}));
MenuSheet msMenu = new MenuSheet("Office menu");
msMenu.add(msSubMenu);
return msMenu;
}
|  |
 |  |  |
|
Zeit weiterschalten
|
|
Damit kann die Zeit weitergestellt werden. Leider ist damit
noch keine der am Tagesanfang anstehenden Aktionen
ausgeführt. Dazu gehört das Aktualisieren des
Standard-FormSheets .
|
|
|
Es ist zu beachten, daß die vom FormSheet angezeigten Daten ungültig
werden könnten. Dies kann geschehen, wenn entweder die
Zeit weitergeschaltet wird oder sich der Geldbestand im
Münzschacht ändert.
|
|
|
Die Zeit kann nur vom Manager weitergeschaltet werden. Es
muß also daran gedacht werden, danach das FormSheet neu aufzubauen. Dies kann entweder
direkt beim Weiterschalten geschehen oder indirekt durch
den Einsatz von Listenern.
|
|
|
Ein Listener ist Teil eines erweiterten
Observer-Patterns. Zwei weitere Bestandteile sind
die Quelle des Ereignisses (Observable) und des Ereignis (die
Erweiterung vom Observer-Pattern) selbst. Unter einem
Ereignis ist jedoch nicht der Vorfall im üblichen Sinne
zu verstehen, sondern vielmehr die Beschreibung des Vorfalls.
Diese Beschreibung wird von der Quelle erzeugt und an alle
Listener gesendet. Das geschieht, in dem an den Listenern eine
festgelegte Methode aufgerufen wird. Die Besonderheit an dem
Konzept ist, daß die Anzahl von Listenern vorher nicht
festgelegt ist. Vielmehr kann sich während des
Programmablaufs eine, meist beliebige, Anzahl von Listenern an
der Quelle an- und wieder abmelden. Ein weiteres Merkmal ist
die Kapselung der Daten. So werden nicht mehr Parameter an die
zuständige Methode des Listeners übergeben, sondern
lediglich das Ereignis, das dann Methoden zur Verfügung
stellt, um die enthaltenen Daten abzufragen.Die Methoden, die
ein Listener implementieren muß, um bestimmte Ereignisse
empfangen zu können, sind in einem zum Ereignismodell
gehörenden Listener-Interface beschrieben.
|
Listener
|
|
Um dies unabhängig von der Betätigung des
Menüpunktes auszuführen, werden die Aktionen nicht
direkt in die durch den Menüpunkt ausgelöste Aktion
eingefügt. Vielmehr wird am Timer
ein Listener angemeldet, der die anfallende Arbeit
übernimmt. Damit werden die Anweisungen relativ
unabhängig von weiteren Änderungen am Programm
ausgeführt. Es ist so z.B. möglich, den Timer auf andere Weise weiterzuschalten, als
mit dem eingefügten Menüpunkt, ohne das das einen
Effekt auf den Listener und die in ihm enthaltenen Anweisungen
hätte.
|
|
|
Ein Listener wird am Timer mit addTimerListener angemeldet. Er muß das
TimerListener -Interface
implementieren. Eine Klasse, die das bereits erledigt und
deren Objekte somit als Listener verwendet werden können,
ist TimerAdapter . Diese Klasse
enthält nur leere Methoden, die nach Bedarf
angepaßt werden können.
|
TimerListener und TimerAdapter
|
|
Um den Listener nur einmal, und zwar bei der Initialisierung
des Büros, anzumelden, wird der nötige Code als
letzte Anweisung in den Konstruktor von Office
eingefügt. Der Listener wird als anonyme Unterklasse von
TimeAdapter implementiert:
|
|
|
 |  |  |
 |
Shop.getTheShop().getTimer().addTimerListener(
new TimerAdapter()
{
});
|  |
 |  |  |
|
|
|
In dem so angemeldeten Listener muß nun die Methode
onGoneAhead angepaßt werden, die
vom Timer aufgerufen wird, wenn die
Zeit weitergeschaltet wurde. Es wird also folgendes in die
anonyme Klasse eingefügt:
|
|
|
 |  |  |
 |
public void onGoneAhead(TimerEvent trEvt)
{
}
|  |
 |  |  |
|
|
|
Die verwendeten Klassen bzw. Interfaces
TimerListener ,
TimerAdapter und
TimerEvent sind in
sale.events enthalten.
Das Paket wird daher importiert:
|
|
|
|
|
|
In der onGoneAhead -Methode wird das FormSheet neu dargestellt, wenn die Zeit
weitergeschaltet wurde. Das kann nur geschehen, wenn der SalesPoint ein benutzbares Display zur Verfügung stellt. Das sollte
hier der Fall sein, da der Manager im Büro ist und somit
sein Display zur Verfügung stellt.
|
|
|
Weiterhin ist es theoretisch möglich, daß die zu
verwendende Methode setFormSheet eine
InterruptedException erzeugt. Diese Exception
tritt hier jedoch nicht auf, da die Methode getDefaultFormSheet ein FormSheet zurückgibt, das die Methode
setFormSheet nicht blockiert. Es ist
jedoch nur einer blockierten setFormSheet -Methode möglich, die
Exception zu erzeugen. Im catch -Block müssen
daher keine Maßnahmen der Fehlerbehandlung
durchgeführt werden.
|
|
|
Daher wird eine if -Anweisung eingefügt, in
deren positivem Zweig ein try -Block enthalten
ist:
|
|
|
 |  |  |
 |
if (hasUseableDisplay(null)) {
try {
setFormSheet(null, getDefaultFormSheet());
}
catch(InterruptedException ie) {
}
}
|  |
 |  |  |
|
|
|
Der Listener für die Weiterschaltung der Zeit ist fertig
implementiert. Jetzt werden noch Listener benötigt, die
bei Veränderung des Geldbestandes im Münzschacht die
Oberfläche neu darstellen. In diesem Fall ist
offensichtlich der Stock mit dem Namen
"coin slot" Quelle der Ereignisse. Dieser
Stock wurde als Instanz der Klasse
MoneyBagImpl angelegt, die das ListenableStock -Interface implementiert. An
Stocks , die dieses Interface
implementieren, kann sich ein StockChangeListener mit addStockChangeListener an- und mit removeStockChangeListener wieder abmelden.
Eine Implementierung dieses Listeners ist mit StockChangeAdapter bereits im Framework
vorhanden. Ein Objekt dieser Klasse kann sich an- und abmelden
und implementiert alle vom Interface geforderten
Funktionen. Diese Funktionen sind jedoch nicht leer und
können in einer Unterklasse angepaßt werden. Auf
diese Weise ist es möglich, in einer eigenen
Listenerklasse nur die Funktionen zu implementieren, die auf
das gewünschte Ereignis reagieren und alle anderen vom
Interface geforderten Methode vom Adapter zu erben.
|
StockChangeListener und StockChangeAdapter
|
|
Um StockChangeAdapter verwenden zu
können, wird das Paket data.events
importiert, für MoneyBagImpl wird
data.ooimpl benötigt:
|
|
|
 |  |  |
 |
import data.events.*;
import data.ooimpl.*;
|  |
 |  |  |
|
|
|
Es wird im Konstruktor von Office ein Listener am
Münzschacht angemeldet, der von StockChangeAdapter abgeleitet ist:
|
|
|
 |  |  |
 |
((MoneyBagImpl)Shop.getTheShop().getStock(
"coin slot")).addStockChangeListener(new StockChangeAdapter()
{
});
|  |
 |  |  |
|
|
|
In der anonymen Klasse muß die Methode
überschrieben werden, die auf das endgültige
Hinzufügen von Einträgen in den Stock reagiert. Es handelt sich um die
Methode commitAddStockItems , der von
der Quelle das Ereignis übergeben wird:
|
|
|
 |  |  |
 |
public void commitAddStockItems(StockChangeEvent sce)
{
}
|  |
 |  |  |
|
|
|
In dieser Methode muß sichergestellt werden, daß
nicht gerade ein Prozeß abläuft. Dieser stellt
seine eigenen FormSheets bereit, die
nicht einfach durch ein neues Standard-FormSheet überschrieben werden
dürfen. Sollte ein Prozeß laufen, wird bei dessen
Ende in jedem Fall das Standard-FormSheet gesetzt, so daß in diesem
Fall keine weiteren Schritte zu ergreifen sind. Den gerade
ablaufenden Prozeß erhält man über die Methode
getCurrentProcess . Liefert sie
null , läuft kein Prozeß.
|
|
|
Eine weitere Bedingung, die es zu beachten gilt, ist,
daß das Büro überhaupt ein gültiges Display hat. Das ist nur dann der Fall, wenn
ein Benutzer angemeldet ist. Tritt das abzufangende Ereignis
ein, wenn gerade niemand angemeldet ist, so wird die
entsprechende Methode im Listener trotzdem aufgerufen und darf
nicht versuchen, ein FormSheet zu
setzen. Die Überprüfung, ob ein benutzbares Display vorhanden ist, erfolgt mit der
Methode hasUsableDisplay . Ihr wird der
anfragende Prozeß übergeben. Da die Anfrage aus
keinem Prozeß heraus stattfindet, ist der Parameter in
diesem Fall null .
|
|
|
Wenn kein Prozeß läuft, aber ein Display vorhanden ist, so kann über
setFormSheet das neue FormSheet gesetzt werden. Dazu sind der
Prozeß, hier also null , und das FormSheet selbst zu übergeben. Diese
Anweisung wird in einen try -Block geschrieben, da
sie eine InterruptedException erzeugen
könnte. Da dieser Fall normalerweise aber nicht eintreten
wird, wird im catch -Block lediglich eine
Fehlermeldung auf die Standardfehlerausgabe geschrieben. In
die Methode wird folgender Code eingefügt:
|
|
|
 |  |  |
 |
if (getCurrentProcess()==null && hasUseableDisplay(null))
try {
setFormSheet(null, getDefaultFormSheet());
}
catch(InterruptedException iexc) {
System.err.println("Update interrupted");
}
|  |
 |  |  |
|
|
|
Analog dazu wird die Methode commitRemoveStockItems in die anonyme Klasse
eingefügt. Diese reagiert, wenn vom Geldbestand des
Münzschachtes Beträge abgezogen werden.
|
|
|
 |  |  |
 |
public void commitRemoveStockItems(StockChangeEvent sce)
{
if (getCurrentProcess()==null && hasUseableDisplay(null))
try {
setFormSheet(null, getDefaultFormSheet());
}
catch(InterruptedException iexc) {
System.err.println("Update interrupted");
}
}
|  |
 |  |  |
|
|
|
Damit sind die Reaktionen auf eine Änderung des
Geldbestandes adäquat umgesetzt.
|
|
|
Im folgenden sollen im RentProcess und im
GiveBackProcess die Vorgänge
mitprotokolliert werden. Am Beispiel des
RentProcess werden die Schritte erklärt, die
das Mitloggen von Aktivitäten der Kunden realisieren. Im
GiveBackProcess werden diese Schritte analog
angewendet.
|
Mitloggen
|
|
Um Klassen aus dem Paket log verwenden
zu können, muß die Klasse RentProcess
um eine import -Anweisung erweitert werden:
|
|
|
|
|
|
Am Ende der Datei RentProcess.java wird die
Klasse MyLogEntry implementiert, die einen
selbstdefinierten Log-Eintrag beschreibt.
|
LogEntry
|
|
 |  |  |
 |
class MyLogEntry extends LogEntry
{
}
|  |
 |  |  |
|
|
|
Es soll mitgeloggt werden, welcher Kunde welches Video zu
welcher Zeit ausgeliehen hat. Es müssen entsprechende
Variablen dafür deklariert werden:
|
|
|
 |  |  |
 |
String name;
String customerID;
Object date;
|  |
 |  |  |
|
|
|
Im Konstruktor werden die Variablen initialisert:
|
|
|
 |  |  |
 |
public MyLogEntry(String name, String customerID, Object date)
{
super();
this.name = name;
this.customerID = customerID;
this.date = date;
}
|  |
 |  |  |
|
|
|
Das Aussehen eines Log-Eintrags wird durch die folgende
toString -Methode bestimmt:
|
|
|
 |  |  |
 |
public String toString()
{
return name +
" rent by customer " + customerID +
" (ID) at turn " + date;
}
|  |
 |  |  |
|
|
|
Die Klasse MyLogEntry ist fertiggestellt.
|
|
|
Nach der Klasse RentProcess wird in der Datei
RentProcess.java die Klasse
MyLoggable implementiert. Sie definiert die
Schnittstelle zum Mitloggen. Es wird vom Interface Loggable geerbt, das ein Objekt
repräsentiert, das geloggt werden kann. Bestandteil
dieser Klasse ist der Konstruktor. In ihm werden die für
die Erstellung des Log-Eintrags wichtigen Variablen
übergeben. Diese müssen zuerst deklariert werden:
|
Loggable
|
|
 |  |  |
 |
class MyLoggable implements Loggable
{
String name;
String customerID;
Object date;
}
|  |
 |  |  |
|
|
|
Um die Variablen zu initialisieren, wird der eben
erwähnte Konstruktor erstellt:
|
|
|
 |  |  |
 |
public MyLoggable(String name, String customerID, Object date)
{
super();
this.name = name;
this.customerID = customerID;
this.date = date;
}
|  |
 |  |  |
|
|
|
Der erste Konstruktor erwartet Parameter, die schon in der
entsprechenden Form übergeben werden. Der zweite
Konstruktor läßt als ersten Parameter auch einen
Eintrag aus dem DataBasket des Kunden
zu, aus dem dann im Konstruktor die relevanten Daten geholt
werden.
|
|
|
Die im Interface Loggable definierte
Methode getLogData muß zur
Vervollständigung der Klasse überschrieben
werden. Sie holt sich den Log-Eintrag mit Hilfe der Klasse
MyLogEntry :
|
|
|
 |  |  |
 |
public LogEntry getLogData()
{
return new MyLogEntry(name, customerID, date);
}
|  |
 |  |  |
|
|
|
Um den Ausleihvorgang in die Log-Datei einzutragen, wird in
der toPayingTransition der Klasse
RentProcess , vor der endgültigen
Übernahme der Videokassetten in das Kundenkonto des
Kunden (nach Beginn der for -Schleife: for
(; number-- > 0;) ), folgender Code eingefügt:
|
Mitloggen im RentProcess
|
|
 |  |  |
 |
try {
Log.getGlobalLog().log(new MyLoggable(
cassetteItem.getSecondaryKey(),
customer.getCustomerID(),
date));
}
catch (LogNoOutputStreamException lnose) {
}
catch (IOException ioe) {
}
|  |
 |  |  |
|
|
|
log erzeugt zwei verschiedene
Exceptions, die hier jeweils explizit abgefangen werden (daher
die zwei catch -Blöcke).
|
|
|
Die IOexception benötigt noch das Paket
java.io :
|
|
|
|
|
|
Am Ende der Klasse RentProcess muß die
Methode getLogGate implementiert
werden, die das LogGate
übergibt. Da beim Beenden des Prozesses jedoch kein
Log-Eintrag geschrieben werden soll, wird das StopGate zurückgeliefert.
|
LogGate
|
|
 |  |  |
 |
public Gate getLogGate()
{
return getStopGate();
}
|  |
 |  |  |
|
|
|
Um die Log-Datei einsehen zu können, wird ein weiterer
Menüpunkt in der Methode getDefaultMenuSheet der Klasse
Office hinzugefügt:
|
|
|
 |  |  |
 |
msSubMenu.add(new MenuSheetItem("See log file", new sale.Action()
{
public void doAction(SaleProcess p, SalesPoint sp)
{
}
}));
|  |
 |  |  |
|
|
|
Zum Anzeigen von Log-Dateien wird vom Framework bereits ein
FormSheet zur Verfügung gestellt:
LogTableForm . Der Konstrutor dieses
FormSheets benötigt als Parameter
mindestens einen Titel und einen LogInputStream Um einen LogInputStream zu erzeugen, wird wiederum ein
FileInputStream benötigt. Es wird mit Hilfe
des Dateinamens der Log-Datei ein
FileInputStream , mit dessen Hilfe ein LogInputStream und mit diesem ein LogTableForm erstellt. Danach wird der nicht
benötigte "Cancel"-Button entfernt --
die Log-Tabelle kann lediglich zur Kenntnis genommen werden --
und das FormSheet kann mittels setFormSheet angezeigt werden. Ein Anpassen
des "Ok"-Buttons ist nicht notwendig, da die
standardmäßig ausgeführte Aktion bereits aus
einem einfachen Schließen des FormSheets besteht.
|
LogTableForm und LogInputStream
|
|
Die Befehlssequenz wird in einen try -Block
geschrieben, da einige der verwendeten Methoden Ausnahmen
auslösen können. In die
doAction -Methode wird folgendes eingefügt:
|
|
|
 |  |  |
 |
try {
FileInputStream fis = new FileInputStream("machine.log");
LogInputStream lis = new LogInputStream(fis);
LogTableForm ltf = new LogTableForm("View log file", lis);
ltf.removeButton(FormSheet.BTNID_CANCEL);
setFormSheet(null, ltf);
}
|  |
 |  |  |
|
|
|
Es müssen für die verwendeten Klassen die
benötigten Pakete importiert werden:
|
|
|
 |  |  |
 |
import log.*;
import log.stdforms.*;
import java.io.*;
|  |
 |  |  |
|
|
|
Außerdem müssen die Ausnahmen abgefangen
werden. Der Konstruktor des FileInputStreams
könnte eine FileNotFoundException
auslösen, der Konstruktor von LogInputStream eine IOException
und setFormSheet eine
InterruptedException . In der Praxis dieses
Programms wird das FormSheet jedoch
nicht unterbrochen, da keine externen Abläufe auf das
Office zugreifen.
|
|
|
Es werden drei catch -Blöcke an den
try -Block angehangen:
|
|
|
 |  |  |
 |
catch (FileNotFoundException fnfexc) {
try {
setFormSheet(null, new MsgForm("Error", "Log file not found."));
}
catch (InterruptedException inner_iexc) {
}
}
catch (IOException ioexc) {
try {
setFormSheet(null, new MsgForm("Error",
"Log file corrupt. It might be empty."));
}
catch (InterruptedException inner_iexc) {
}
}
catch (InterruptedException iexc) {
try {
setFormSheet(null, new MsgForm("Error", iexc.toString()));
}
catch (InterruptedException inner_iexc) {
}
}
|  |
 |  |  |
|
|
|
Damit hat der Manager die Möglichkeit des Log-File
einzusehen.
|
|
|
|
Hier der Quelltext der in diesem Kapitel geänderten Klassen:
|
|
|
|
|