|
|
|
|
|
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:
|
|
|
Office.java |
|
|
| | |
|
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;
}
| |
| | |
|
|
|
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.
|
|
|
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.
|
|
|
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)
{
refreshOfficeDisplay();
}
| |
| | |
|
|
|
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. Wir erneuern das
FormSheet deshalb in einer eigenen
Methode, um diese Tätigkeit auch zu anderen Gelegenheiten
aufrufen zu können.
|
|
|
Wir implementieren deshalb zunächst die Methode
refreshOfficeDisplay .
|
|
|
| | |
|
protected void refreshOfficeDisplay()
{
}
| |
| | |
|
|
|
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 ein gültiges Display haben muß. Das wäre
beispielsweise dann nicht der Fall, falls man bei dem
betreffenden SalesPoint ein NullDisplay angemeldet hätte. Tritt das
abzufangende Ereignis ein, 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
kann, 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");
}
}
| |
| | |
|
|
|
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 Methoden vom Adapter zu erben.
|
|
|
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. Hier bedienen wir
uns wieder der Methode refreshOfficeDisplay , um
die Anzeige zu aktualisieren.
|
|
|
| | |
|
public void commitAddStockItems(StockChangeEvent sce)
{
refreshOfficeDisplay();
}
| |
| | |
|
|
|
Analog dazu wird die Methode commitRemoveStockItems in die anonyme Klasse
eingefügt. Diese reagiert, wenn vom Geldbestand des
Münzschachtes Beträge abgezogen werden. Da wir
sowohl bei dem Hinzufügen als auch beim Herausnehmen von
Münzen aus dem Münzschacht auf gleiche Weise
reagieren wollen, rufen wir hier die oben implementierte
Methode commitAddStockItems auf. Damit
gewährleisten wir, daß bei einer späteren
Änderung nur eine der beiden Methoden angepasst werden
muß.
|
|
|
| | |
|
public void commitRemoveStockItems(StockChangeEvent sce)
{
commitAddStockItems(sce);
}
| |
| | |
|
|
|
Damit sind die Reaktionen auf eine Änderung des
Geldbestandes adäquat umgesetzt.
|
|
|
MyLoggable.java |
|
|
Im folgenden sollen im RentProcess und im
GiveBackProcess die Vorgänge
mitprotokolliert werden. Hierfür muss die Klasse
MyLoggable jetzt implementiert werden. Diese soll
später alle Aktivitäten der Kunden im
RentProcess und GiveBackProcess
während des Ausleih- und Rückgabevorgangs mitloggen.
|
|
|
Um die erforderlichen Klassen aus dem Paket log verwenden zu können, muss die Klasse
MyLoggable folgende
import -Anweisungen enthalten:
|
|
|
| | |
|
import log.Loggable;
import log.LogEntry;
| |
| | |
|
|
|
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:
|
|
|
| | |
|
public class MyLoggable implements Loggable
{
String name;
String customerID;
Object date;
boolean rent;
}
| |
| | |
|
|
|
Bei dem Object date handelt es sich um die
aktuelle Shopzeit, dargestellt in Turns und die boolsche
Variable rent gibt an, ob ein Ausleih- oder ein
Rückgabevorgang protokolliert werden soll. Um die
Variablen zu initialisieren, werden zwei Konstruktoren
erstellt, je nachdem, welcher Prozess das Logging aufruft:
|
|
|
| | |
|
public MyLoggable(String name, String customerID, Object date)
{
super();
this.name = name;
this.customerID = customerID;
this.date = date;
rent = true;
}
public MyLoggable(StoringStockItemDBEntry cassetteItem,
Customer customer, Object date)
{
super();
name = cassetteItem.getSecondaryKey();
this.customerID = customer.getCustomerID();
this.date = date;
rent = false;
}
| |
| | |
|
|
|
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. Durch die boolsche Variable rent wird weiterhin
unterschieden, ob der Aufruf aus dem RentProcess
oder GiveBackProcess erfolgt, und damit der
spätere Logeintrag entsprechend angepasst.
|
|
|
Für den zweiten Konstruktor muss noch folgende Anweisung
ergänzt werden:
|
|
|
| | |
|
import data.ooimpl.StoringStockItemDBEntry;
| |
| | |
|
|
|
Die im Interface Loggable definierte
Methode getLogData muss zur
Vervollständigung der Klasse implementiert werden. Sie
holt sich den Log-Eintrag mit Hilfe der Klasse MyLogEntry :
|
|
|
| | |
|
public LogEntry getLogData()
{
return new MyLogEntry(name, customerID, date, rent);
}
| |
| | |
|
|
|
Am Ende der Datei MyLoggable.java wird die Klasse
MyLogEntry implementiert, die einen
selbstdefinierten Log-Eintrag beschreibt.
|
|
|
MyLogEntry.java |
|
|
| | |
|
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;
boolean rent;
| |
| | |
|
|
|
Im Konstruktor werden die Variablen initialisert:
|
|
|
| | |
|
public MyLogEntry(String name, String customerID, Object date, boolean rent)
{
super();
this.name = name;
this.customerID = customerID;
this.date = date;
this.rent = rent;
}
| |
| | |
|
|
|
Das Aussehen eines Log-Eintrags wird durch die folgende
toString -Methode bestimmt:
|
|
|
| | |
|
public String toString()
{
if (rent)
return name +
" rent by customer " + customerID +
" (ID) at turn " + date;
else
return "customer "+ customerID +
" (ID) gave back " + name +
" at turn " + date;
}
| |
| | |
|
|
|
Die Klasse MyLogEntry ist fertiggestellt.
|
|
|
RentProcess.java |
|
|
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:
|
|
|
| | |
|
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.
|
|
|
| | |
|
public Gate getLogGate()
{
return getStopGate();
}
| |
| | |
|
|
|
Office.java |
|
|
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.
|
|
|
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.
|
|
|
VideoMachine.java |
|
|
Jetzt muß noch in der Klasse VideoMachine
das Log-File geöffnet werden. Hierzu wird die
main-Methode folgendermaßen ergänzt:
|
|
|
| | |
|
try {
Log.setGlobalOutputStream(new FileOutputStream("machine.log", true));
}
catch (IOException ioex) {
System.err.println("Unable to create log file.");
}
| |
| | |
|
|
|
|
Hier der Quelltext der in diesem Kapitel geänderten Klassen:
|
|
|
|
|