Im folgenden Kapitel soll der eigentliche Ausleihvorgang beschrieben werden.
Ausgehend vom Anmeldeprozess gelangt der Kunde zum Startzustand des Ausleihprozesses, in welchem die gewünschten Videos ausgewählt werden können. Wurde eine Auswahl getroffen und bestätigt, gelangt man in den Bezahlzustand, wo die entsprechende Geldsumme beglichen werden muss. Sind ausreichend Münzen und/oder Scheine eingezahlt worden, kann zum Folgezustand gewechselt werden. In diesem werden die gewählten Videos, sowie etwaiges Wechselgeld angezeigt. Wurde die Anzeige bestätigt, werden die Änderungen durch Wechsel zum Commit-Gate
persistent gemacht. Befindet sich der Prozess im Bezahlzustand, kann zum vorherigen Ausgangszustand zurückgewechselt werden. Die bereits eingeworfenen Münzen werden in diesem Fall wieder ausgegeben. Im Startzustand kann der Ausleihzustand durch Wechsel zum Rollback-Gate
abgebrochen werden.
Abbildung
11.1 verdeutlicht den Ablauf des Prozesses anhand eines Zustandsdiagramms.
Abbildung 11.1: Der Ausleihprozess als Zustandsdiagramm
Die Gate
s Rollback, Commit, Log und Stop sowie die Übergänge zwischen diesen sind in der Klasse SaleProcess
vordefiniert (siehe Technischer Überblick).
Definition der Zustände
Als erstes wird analog zum Anmeldeprozess eine neue SaleProcess
-Spezialisierung geschaffen mit entsprechenden Rückgabemethoden
für die benötigten Zustände. Natürlich betrifft dies nur die drei selbstdefinierten Zustände
und nicht die von der Oberklasse bereits implementierten. Insbesondere ist wieder darauf zu achten,
dass der Startzustand über die Methode getInitialGate()
zurückgeliefert wird.
package videoautomat; public class SaleProcessRent extends SaleProcess { public static final String MB_TEMP_KEY = "mb_temp"; public SaleProcessRent() { super("SaleProcessRent"); } protected Gate getInitialGate() { . . . } public Gate getPayGate() { . . . } public Gate getConfirmGate() { . . . } }
Die Initialisierung des Ausleihprozesses wird im Hauptzustand der Klasse SaleProcessLogOn
vorgenommen.
Der Ausleihbutton im Formular des Hauptzustands wird mit einer Aktion verknüpft, die auf dem aktuellen SalesPoint
(dem Videoautomaten) mittels runProcess(SaleProcess p, DataBasket db)
einen neuen Prozess startet.
Bemerkenswert ist dabei, dass die Klasse SalesPoint
das komplette Prozessmanagement übernimmt. Intern wird durch diesen Aufruf der bis dato laufende Anmeldeprozess gestoppt
und auf einem Kellerspeicher abgelegt, solange bis der neugestartete Prozess terminiert.
Im Anschluss wird der Anmeldeprozess wieder aufgeweckt und fährt an der Stelle fort, wo er unterbrochen wurde,
nämlich am Main-Gate
. Für den Programmierer laufen diese Vorgänge jedoch völlig transparent ab.
public class LogOnSTFSContentCreator extends FormSheetContentCreator { private User user; protected void createFormSheetContent(FormSheet fs) { fs.addButton("Rent", 1, new RunProcessAction(new SaleProcessRent(), new DataBasketImpl())); fs.addButton("Hand back", 2, new RunProcessAction(new SaleProcessHandBack(), new DataBasketImpl())); } }
Außerdem ist darauf zu achten, dass hier der Methode runProcess(SaleProcess p, DataBasket db)
eine Instanz der
Klasse DataBasketImpl
übergeben wird. Dieses Objekt wird automatisch an den betreffenden Prozess gekoppelt und
für Transaktionen während des Prozessablaufs genutzt. Der Datenkorb lässt sich über die Methode getBasket()
vom Prozess zurückgeben.
Transaktionen mittels einer Tabelle
Nachdem die Möglichkeit geschaffen wurde den Ausleihprozess zu starten, soll die Interaktionsmöglichkeit für den Startzustand
implementiert werden, d.h. konkret der Kunde soll in die Lage versetzt werden, Videos des Sortiments auszuwählen. Das Framework bietet
hierfür eine besondere Form eines Tabellenformulars an, die Klasse TwoTableFormSheet
.
Ein solches Formular enthält eine linke und rechte Tabelle, jeweilig die Repräsentation einer frameworkinternen Datenstruktur. Zum Beispiel könnte die linke Seite einen Bestand B1 und die rechte Seite einen anderen Bestand B2 darstellen. Aufgrund der vorhandenen Verschiebebuttons in einem derartigen Tabellenformular ist der Anwender in der Lage, Einträge zwischen B1 und B2 hin- und herzuschieben. Das Verschieben wird gänzlich vom Formular und den beteiligten Datenstrukturen geleistet. Der Programmierer braucht sich um nichts weiter zu kümmern.
Analog zum SingleTableFormSheet
gibt es in der Klasse TwoTableFormSheet
zahlreiche create
-Methoden,
die nahezu alle Kombinationen der unterschiedlichen Datenstrukturen berücksichtigen und ein entsprechendes 2-Tabellen-Formular erzeugen.
Optional kann bei der Erzeugung dieser, auf Transaktionen ausgerichteten Formulare ein Datenkorb übergeben werden.
An dieser Stelle soll kurz auf die Funktionsweise eines Datenkorbs eingegangen werden. Angenommen der Kunde wählt verschiedene Videos aus. Die gewählten Videos werden in den Leihbestand des Kunden aufgenommen. Einen Moment später überlegt er sich die Sache noch einmal und kommt zu dem Schluss, dass er das Geld doch lieber für etwas Sinnvolleres ausgeben oder seine Zeit anders nutzen will. Der Leihvorgang wird abgebrochen. Die Videos müssen nun wieder aus dem virtuellen Bestand des Kunden entfernt und in den Bestand des Automaten aufgenommen werden, aber welche? Möglicherweise existieren bereits entliehene Videos im Bestand des Kunden, die nicht entfernt werden sollen.
Ein Datenkorb hilft hierbei als eine Art Puffer. Wird beim Entfernen oder Hinzufügen von Einträgen einer Datenstruktur ein Datenkorb
übergeben, zeichnet der Datenkorb die Aktion auf. Später kann ein rollback ausgeführt werden, welches
sämtliche protokollierte Aktionen rückgängig macht, oder es erfolgt ein commit um die Änderungen
dauerhaft zu machen. Wird beim ügen oder Entfernen statt des Datenkorbs null
übergeben, werden die gemachten Änderungen
sofort dauerhaft gemacht.
Ähnlich, wie bei der bisherigen Nutzung von FormSheet
s, so wird auch hier in der getInitialGate()
-Methode eine Instanz des
TwoTableFormSheet
s erzeugt und mit meinem speziellen FormSheetContentCreator
versehen.
Im Vorfeld wird in der Methode ein MoneyBag
initialisiert und den Prozessdaten des ProcessContext
übergeben.
Ähnlich, wie u.a. ein Nutzer oder ein DataBasket
im Prozesskontext gespeichert werden kann, so können beliebige Daten für den
gesamten Prozess zur Verfügung gestellt werden. D.h. in allen Gate
s ist ein Zugriff darauf möglich. Weiterhin wird
eine Spezialisierung der Klasse MoveStrategy
erzeugt. Wie der Name schon sagt ist sie für das Verhalten beim Verschieben
von Elementen zwischen den beiden Datenstrukturen verantwortlich ist.
Entsprechend gibt es verschiedene Spezialisierungen von MoveStrategy
, je nachdem welche Datenstruktur als Quelle und
welche als Ziel dient. In diesem Fall bedarf es einer Instanz der Klasse CSDBStrategy
.
Über eine der create
-Methoden der Klasse TwoTableFormSheet
wird ein Formular erzeugt
mit einer Tabelle für einen CountingStock
auf der linken Seite und einer Tabelle für einen
DataBasket
auf der rechten Seite.
Die Videos sollen vorläufig nur dem Datenkorb und nicht dem Bestand des Kunden zugefügt werden.
Der Methode muss darüberhinaus die UIGate
-Instanz übergeben werden,
mit der das Formular verknüpft werden soll. Ohne dieses Gate
funktioniert das Verschieben nicht.
Denn eine Verschiebenaktion ist bereits eine Transition, die dann wieder das gleiche Gate
anzeigt. Fehlt das Gate
kann also die Transition nicht mehr richtig arbeiten.
Als TableEntryDescriptor
-Objekt wird nur für den Video-Stock ein TED übergeben.
Für den DataBasket
-TED wird null
übergeben, wodurch der Standard-TED für einen DataBasket
genutzt wird.
Die boolesche Variable bestimmt, ob nicht vorhandene Elemente des CountingStock
angezeigt werden sollen.
Zum Schluss wird als letzter Parameter die spezialisierte MoveStrategy
übergeben.
public class SaleProcessRent extends SaleProcess { . . . protected Gate getInitialGate() { getContext().setProcessData(MB_TEMP_KEY, new MoneyBagImpl("mb_user", VideoShop.getCurrency())); CSDBStrategy csdbs = new CSDBStrategy(); UIGate uig_offer = new UIGate(null, null); TwoTableFormSheet ttfs_rent = TwoTableFormSheet.create( "Choose your videos!", VideoShop.getVideoStock(), getBasket(), uig_offer , null, null, false, new TEDVideoStock(), null, csdbs); ttfs_rent.addContentCreator(new RentTTFSContentCreator()); } . . . }
Die weiteren Anpassungen des FormSheet
s und die Aktionen werden wieder in einer speziellen Implementation des
FormSheetContentCreator
durchgeführt: RentTTFSContentCreator
.
package videoautomat.contentcreator; public class RentTTFSContentCreator extends FormSheetContentCreator { protected void createFormSheetContent(FormSheet fs) { fs.removeAllButtons(); fs.addButton("Rent", 1, new TransitWithAction(new RentSumUpTransition())); fs.addButton("Cancel", 2, new RollBackAction()); } }
Setzt man für die Aktion des Rent-Buttons testweise null
, übersetzt und startet die Anwendung, kann man einen Kunden oder den Administrator anmelden und
durch Betätigung des Rent-Button einen Verleihprozess starten.
Es öffnet sich das neue Tabellenformular, worin man Einträge zwischen rechter und linker Seite hin- und her verschieben kann.
Wählt man anschließend den Cancel-Button soll der Prozess eigentlich terminieren,
doch stattdessen erscheint eine Fehlermeldung. Der Grund dafür ist der Wechsel zum Log-Gate
in welchem versucht wird, den Prozessnamen und die aktuelle Zeit in die globale Logdatei zu schreiben.
Es wurde jedoch noch gar keine Logdatei definiert. Daher wird bei dem Schreibversuch eine Exception ausgelöst.
Die Definition der globalen Logdatei lässt sich schnell nachholen.
Objekte der Klasse Log
des Frameworks repräsentieren Protokolldateien. Es können verschiedene Logdateien für diverse Zwecke angelegt werden. Darüberhinaus lässt sich eine globale Logdatei über entsprechende statische Methoden in der Klasse Log
setzen und wiedergeben. Im Konstruktor von VideoShop
wird die globale Protokolldatei der Anwendung definiert.
public class VideoShop extends Shop { . . . public static final String FILENAME = "automat.log"; . . . public VideoShop() { . . . try { Log.setGlobalOutputStream(new FileOutputStream(FILENAME, true)); } catch (IOException ioex) { System.err.println("Unable to create log file."); } } . . . }
Bei einem erneuten Start der Anwendung mit existenter Protokolldatei lässt sich der Ausleihprozess ohne Probleme abbrechen. Werden während des Ausleihprozesses Videos aus dem Bestand des Automaten entfernt, so ist nach einem Abbruch und Neustart des Prozesses dank der Nutzung des Datenkorbs und des Rollback-Gate
s alles wie zuvor.
Dennoch lauert ein Schönheitsfehler im Verborgenen. Wählt man für die Anzahl der zu verschiebenden Elemente aus dem Bestand mehr als vorhanden sind, kommt es zu einem Fehler. Die CSDBStrategy
des Formulars erkennt den Fehler und zeigt eine Meldung an. Leider terminiert der Prozess nach Bestätigung der Meldung. Um dies zu umgehen, lässt sich die Fehlerbehandlung der verwendeten Strategie ändern. In der Methode getInitialGate()
des Ausleihprozesses muss der Fehlerbehandler des CSDBStrategy
-Objekts neu gesetzt werden, bevor das Objekt zur Formularerzeugung genutzt wird. Sinnvollerweise existiert eine ErrorHandler
-Konstante in der Klasse FormSheetStrategy
, die nur eine pop-up Nachricht anzeigt und sonst keine Änderungen vornimmt. Insbesondere wird der Ausleihprozess nicht einfach beendet. Diese Konstante soll hier verwendet werden.
public class SaleProcessRent extends SaleProcess { . . . protected Gate getInitialGate() { CSDBStrategy csdbs = new CSDBStrategy(); csdbs.setErrorHandler(FormSheetStrategy.MSG_POPUP_ERROR_HANDLER); . . . } . . . }
Die Bezahlung anzeigen
Die Auswahl der Videos kann jetzt getroffen werden. Im nächsten Schritt müssen die Verkaufskosten der gewählten Videos aufsummiert werden. Die erforderliche Summe soll dann im Bezahlzustand durch den Kunden beglichen werden.
Da für die Anwendung kein realer Automat und damit auch kein Erkennungsmechanismus für eingeworfene Münzen zur Verfügung steht, muss bei der Bezahlung ein klein wenig getrickst werden. Der Einwurf des Geldes wird durch ein TwoTableFormSheet
mit dem Währungskatalog als Quelle und einem Geldbeutel als Ziel simuliert.
Für diesen Zweck wird in der Methode getPayGate()
ein entsprechendes FormSheet
aufgebaut.
Der Aufbau des FormSheet
s ist nur geringfügig anders als das im Start-Gate
.
Quelle und Ziel unterscheiden sich und damit auch die benutzte MoveStrategy
,
was jedoch für den Programmierer kaum einen Unterschied macht.
Und es wird wird ein anderer ContentCreator genutzt.
public class SaleProcessRent extends SaleProcess { . . . public Gate getPayGate() { NumberValue nv_sum = (NumberValue) getContext().getProcessData(SUM_KEY); CCSStrategy ccss = new CCSStrategy(); ccss.setErrorHandler(FormSheetStrategy.MSG_POPUP_ERROR_HANDLER); UIGate uig_pay = new UIGate(null, null); TwoTableFormSheet ttfs_pay = TwoTableFormSheet.create( "Throw the money in the slot, please.", VideoShop.getCurrency(), (MoneyBag)getContext().getProcessData(MB_TEMP_KEY), getBasket(), uig_pay, new ComparatorCurrency(), new ComparatorCurrency(), false, null, null, ccss); RentPayFSContentCreator formSheetCC = new RentPayFSContentCreator(this); formSheetCC.setPayValue(VideoShop.getCurrency().toString(nv_sum)); ttfs_pay.addContentCreator(formSheetCC); return uig_pay; } . . . }
Bevor das Formular des Ausleihprozesses initialisiert wird,
soll ein Komparator für die Geldeinträge geschrieben werden. Bei den Tabellenformularen werden,
sofern bei der Initialisierung kein Komparator übergeben wird, die Einträge standardmäßig nach
ihrem Namen sortiert. Bei den Geldeinträgen ist diese Art der Anordnung jedoch irritierend,
da beispielsweise auf 1-Cent gleich 1-Euro folgt.
Wünschenswert wäre eine Anordnung nach den Werten. Folgende Implementation des Interface Comparator
leistet dies.
package videoautomat; public class ComparatorCurrency implements Comparator, Serializable { public ComparatorCurrency() { } public int compare(Object arg0, Object arg1) { if (arg0 instanceof CatalogItem) { return ((CatalogItem) arg0).getValue().compareTo( ((CatalogItem) arg1).getValue()); } if (arg0 instanceof CountingStockTableModel.Record) { Value v1 = ((CountingStockTableModel.Record) arg0) .getDescriptor() .getValue(); Value v2 = ((CountingStockTableModel.Record) arg1) .getDescriptor() .getValue(); return v1.compareTo(v2); } return 0; } }
Die zu implementierende Methode compare(Object o1, Object o2)
untersucht, ob es sich bei den zu vergleichenden Objekten um Tabelleneinträge eines Katalogs (Währungskatalog) oder eines zählenden Bestands (Geldbeutel) handelt, und delegiert den Vergleich an die zu untersuchenden Werte. Wichtig ist, dass der Komparator das Interface Serializable
implementiert. Damit signalisiert man Speicherfähigkeit des Objekts.
Über den Aufruf (MoneyBag)getContext().getProcessData(MB_TEMP_KEY)
wird zusätzlich eine Variable des Typs
MoneyBagImpl
übergeben. Diese wurde bereits beim getInitialGate()
initialisiert.
Der deklarierte Geldbeutel wird an Stelle des Automaten-Geldbeutels verwendet,
da es gewiss nicht wünschenswert ist, dass der Kunde die gesamte Barschaft im Automaten zu Gesicht bekommt.
Auß den bisher erwähnten Unterschieden wird dem ContentCreator ein NumberValue
übergeben.
Dieses wird auf dem Prozesskontext ausgelesen und wurde durch die Transition, die zu diesem Gate
führt, angelegt.
Es beherbergt den zu zahlenden Preis. Wie es genau bestimmt wird, soll später mit der Erklärung der Transition erfolgen.
Aus diesem NumberValue
wird nun eine String-Repräsentation gebildet und diese dem ContentCreator übergeben;
ähnlich der Fehlermeldung beim Anmelden neuer Kunden. Der RentPayFSContentCreator
sieht folgendermaßen aus:
public class RentPayFSContentCreator extends FormSheetContentCreator { private String payValue; private SaleProcessRent processRent; public RentPayFSContentCreator(SaleProcessRent process){ this.processRent = process; payValue = ""; } protected void createFormSheetContent(FormSheet fs) { JComponent jc = new JPanel(); jc.setLayout(new BoxLayout(jc, BoxLayout.Y_AXIS)); jc.add(Box.createVerticalStrut(10)); jc.add(new JLabel("You have to pay: " + payValue)); jc.add(Box.createVerticalStrut(10)); jc.add(fs.getComponent()); fs.setComponent(jc); fs.removeAllButtons(); fs.addButton("Pay", 1, new TransitWithAction(new RentPayConfirmTransition())); fs.addButton("Cancel", 2, new TransitWithAction(new RentPayRollbackTransition())); fs.getButton(1).setEnabled(false); setButtonStockListener(fs.getButton(1)); } public void setPayValue(String payValue){ this.payValue = payValue; } . . . }
Neben dem üblichen Setzen der Buttons wird das FormSheet weiter angepasst und auch der zu zahlenden Betrag dabei mit verwendet.
Anpassungen vorhandener Formulare sind, wie im Beispiel zu sehen ist, einfach zu realisieren. Man lässt sich die
Anzeigefläche des Formulars (die Buttonleiste ist davon ausgenommen) über die Methode getComponent()
zurückgeben. Diese Komponente kann man dann an beliebiger Stelle eines selbstdefinierten JComponent
-Objekts,
z.B innerhalb einer JPanel
-Instanz, platzieren. Die neu definierte Komponente wird über
setComponent
zum neuen Anzeigeobjekt des Formulars.
Den Datenkorb einteilen
Bevor der Zustandsübergang zum Bezahlzustand definiert und damit erst die Möglichkeit geschaffen wird, das so eben erzeugte Formular zu betrachten, soll folgende Überlegung angestellt werden:
Der Datenkorb des Ausleihprozesses wird für die Transaktionen der Videos und des Geldes genutzt. Kommt es zu einem Zustandswechsel vom Pay-Gate
zurück zum Initial-Gate
, müssen die Geldtransaktionen rückgängig gemacht werden, nicht aber die Videotransaktionen. Der Aufruf von rollback()
, in Bezug auf den Datenkorb, bewirkt jedoch ein Zurücksetzen aller Transaktionen, die mit diesem getätigt wurden. Abhilfe schafft hier die Definition von Sub-Datenkörben. Durch die Methode setCurrentSubBasket(String s)
kann man einen solchen bestimmen. Ein Rollback oder Commit lässt sich begrenzt auf einem Sub-Datenkorb ausführen.
Es werden zwei Strings zur Identifikation der Sub-Datenkörbe am Anfang der Klasse SaleProcessRent
definiert und vor der Initialisierung des Formulars für den Startzustand wird der Datenkorb-Abschnitt für die Videos bestimmt.
public class SaleProcessRent extends SaleProcess { . . . public static final String SUB_SHOP_VIDEO = "videos_cs"; public static final String SUB_TMP_MONEY = "money_temp"; . . . protected Gate getInitialGate() { getBasket().setCurrentSubBasket(SUB_SHOP_VIDEO); . . . } . . . }
Jetzt kann die Transition RentSumUpTransition
definiert werden, die im RentTTFSContentCreator
dem Rent-Button zugewiesen wurde.
Während des Zustandsübergangs wird der Inhalt des Datenkorbs aufsummiert und der resultierende
Wert der für diesen Zweck definierten Variablen im Prozesskontext zugewiesen.
Entspricht die Summe dem Wert Null, wird das Initial-Gate
, sonst das Pay-Gate
zurückgegeben.
public class RentSumUpTransition implements Transition { public Gate perform(SaleProcess sp, User user) { NumberValue nv_sum = (NumberValue) sp.getBasket().sumSubBasket( SaleProcessRent.SUB_SHOP_VIDEO, null, new SumBasketEntryValue(sp.getBasket()), new IntegerValue(0)); sp.getContext().setProcessData(SaleProcessRent.SUM_KEY, nv_sum); if (nv_sum.isAddZero()){ return ((SaleProcessRent)sp).restart(); } sp.getBasket().setCurrentSubBasket(SaleProcessRent.SUB_TMP_MONEY); return ((SaleProcessRent)sp).getPayGate(); } . . . }
Die Methode für das Aufsummieren des Sub-Datenkorbs erwartet den Namen desselben. Außerdem bedarf es eines
DataBasketCondition
-Objekts, sofern nur gewisse Einträge des Datenkorbs untersucht werden sollen.
Sollen alle Einträge berücksichtigt werden, wie in diesem Fall, kann null
übergeben werden.
Ferner braucht die Methode ein BasketEntryValue
-Objekt das über die Methode getEntryValue(DataBasketEntry dbe)
den Wert der Datenkorbeinträge liefert, der aufsummiert werden soll. In diesem Fall wird die Implementierung
des Interface BasketEntryValue
als innere Klasse vorgenommen.
public class RentSumUpTransition implements Transition { . . . private static class SumBasketEntryValue implements BasketEntryValue{ private DataBasket dataBasket; public SumBasketEntryValue(DataBasket db){ this.dataBasket = db; } public Value getEntryValue(DataBasketEntry dbe) { try { CatalogItem ci = VideoShop.getVideoCatalog().get( dbe.getSecondaryKey(),null, false); int count = ((Integer) dbe.getValue()).intValue(); return ((QuoteValue) ci.getValue()).getOffer().multiply(count); } catch (VetoException e) { e.printStackTrace(); dataBasket.rollback(); } return null; } } }
Als letzten Parameter erwartet die Methode für das Aufsummieren des Sub-Datenkorbs einen Startwert, von dem ausgegangen werden soll. In diesem Fall ist es ein IntegerValue
, das den Wert null repräsentiert.
Nachdem die Summe ermittelt wurde, wird sie dem Prozesskontext hinzugefügt, um an anderen Stellen darauf zugreifen zu können. Ist die Summe 0, wird das Start-Gate zurück gegeben, andernfalls das Pay-Gate. Beide sind im Prozess definiert und werden dort aufgerufen. Zu beachten ist, dass bei einem erfolgreichen Wechsel zum Bezahlzustand vorher noch der Sub-Datenkorb für die Geldtransaktionen gesetzt wird.
Der Wechsel vom Start- zum Bezahlzustand ist damit möglich. Der Übergang zurück zum
Startzustand lässt sich sehr einfach implementieren. Es wird wiederum eine Klasse für die entsprechenden
Transition
definiert.
Diese muss lediglich die Geldtransaktionen über ein Rollback
auf dem Sub-Datenkorb rückgängig machen und anschließend das Initial-Gate
zurückgeben.
package videoautomat.transition; public class RentPayRollbackTransition implements Transition { public Gate perform(SaleProcess sp, User user) { SaleProcessRent saleProcess = (SaleProcessRent) sp; saleProcess.getBasket().rollbackSubBasket(SaleProcessRent.SUB_TMP_MONEY); return saleProcess.restart(); } }
Nach erfolgter Übersetzung und Ausführung können die neuen Features des Programms getestet werden.
Der Button bestimmt, wann es weiter geht
Der Übergang vom Bezahl- zum Bestätigungszustand soll erst dann ermöglicht werden, wenn genügend Geld "eingeworfen" wurde. Praktisch wird dies dadurch realisiert, dass der entsprechende Button solange deaktiviert bleibt, bis die Summe der eingeworfenen Münzen und Scheine dem zu zahlenden Betrag entspricht oder diesen übersteigt.
Die Überprüfung der Summe nach jeder Geldtransaktion im Bezahlzustand lässt sich am einfachsten mit Hilfe eines
StockChangeListener
s realisieren. Ein solcher Zuhörer ist Bestandteil eines speziellen
Observer-Entwurfsmusters, dem sogenannten Event-Delegations-Muster, welches
häufig in Java insbesondere bei der Oberflächenprogrammierung zum Einsatz kommt. Das Prinzip ist denkbar einfach. Einem Objekt,
in diesem Fall dem Geldbeutel, können diverse Zuhörer (hier StockChangeListener
),
die sich für Änderungen dieses Objekts interessieren, zugeordnet werden. Die Zuhörer
implementieren ein zuvor definiertes Schnittstellenverhalten. Treten Änderungen auf (z.B. Hinzufügen von Geld in den Beutel),
informiert das Objekt über die von der Schnittstelle definierten Methoden seine Zuhörerschaft.
Das Interface StockChangeListener
definiert verschiedenste Methoden bezogen auf die Veränderungen eines Bestandes.
Hier sind nur die Methoden addedStockItems(StockChangeEvent e)
und removedStockItems(StockChangeEvent e)
von
Interesse. Erstere soll in diesem Fall überprüfen, ob die Summe im Geldbeutel ausreicht und wenn dies der Fall ist, den
Bezahl-Button aktivieren. Letztere soll prüfen, ob zuwenig Geld im Bestand steckt und ggf. den Button deaktivieren. Um nicht alle
weiteren Methoden des Interface implementieren zu müssen, wird auf die Klasse StockChangeAdapter
zurückgegriffen,
in der alle Methoden der Schnittstelle leer vordefiniert sind. In einer neuen Methode wird die Spezialisierung von StockChangeAdapter
implementiert und dem Geldbeutel, dem "zugehört" werden soll, zugefügt. Der Methode muss der Button, der de- bzw. aktiviert werden soll,
übergeben werden.
package videoautomat.contentcreator; . . . private void setButtonStockListener(final FormButton fb) { final MoneyBagImpl bag = (MoneyBagImpl) processRent.getContext().getProcessData(SaleProcessRent.MB_TEMP_KEY); final DataBasket basket = processRent.getBasket(); final NumberValue nv_sum = (NumberValue) processRent.getContext().getProcessData(SaleProcessRent.SUM_KEY); StockChangeAdapter sca = new StockChangeAdapter() { public void addedStockItems(StockChangeEvent e) { if (bag.sumStock(basket, new CatalogItemValue(), new IntegerValue(0)) .compareTo(nv_sum)>= 0){ fb.setEnabled(true); } } public void removedStockItems(StockChangeEvent e) { if (bag.sumStock(basket, new CatalogItemValue(), new IntegerValue(0)) .compareTo(nv_sum) < 0){ fb.setEnabled(false); } } }; bag.addStockChangeListener(sca); } }
Der hier angewandten Methode zum Aufsummieren des Geldbestands muss als erster Parameter der Datenkorb übergeben werden. Ohne diesen würden
die bisher nur temporär hinzugefügten Einträge des Geldbeutels nicht mitgezählt werden. Der zweite Parameter, die
CatalogItemValue
-Instanz, hat etwas mit den Katalogeinträgen zu tun, auf die die jeweiligen Bestandseinträge referenzieren.
Im Fall des Währungskatalogs kann die vorimplementierte Form genutzt werden. Sollten die Katalogeinträge jedoch Werte vom Typ QuoteValue
besitzen, muss über die CatalogItemValue
-Instanz entschieden werden, welcher Wert aufsummiert werden soll.
Das Hinzufügen des Zuhörers erfolgt in der createFormSheetContent()
-Methode im RentPayFSContentCreator
. Der
Bezahlbutton wird zunächst deaktiviert.
package videoautomat.contentcreator; public class RentPayFSContentCreator extends FormSheetContentCreator { . . . protected void createFormSheetContent(FormSheet fs) { . . . fs.getButton(1).setEnabled(false); setButtonStockListener(fs.getButton(1)); } . . . }
Die Videos und das Restgeld ausgeben
Es fehlen noch der Bestätigungszustand und der Übergang dorthin. In diesem Übergang müssen Einträge dem Bestand des Kunden hinzugefügt werden, die den ausgewählten Videos entsprechen. Außerdem muss das Geld, welches dem Geldbeutel des Ausleihprozesses zugefügt wurde, in den Geldbeutel des Automaten, sowie eventuelles Wechselgeld umgekehrt aus dem Automaten-Geldbeutel in den des Prozesses verschoben werden. Im Bestätigungszustand sollen die entliehenen Videos und das Wechselgeld angezeigt werden.
Es wird mit dem Zustandsübergang begonnen, der wie gehabt in einer eigenen Klasse definiert wird.
Zunächst wird lediglich das Hinzufügen der Videokassetten zum Bestand des Kunden implementiert. Dazu muss ein neuer Sub-Datenkorb definiert und gesetzt werden,
der nur die Transaktionen in den Bestand des Kunden protokolliert. Anschließend iteriert man über alle Einträge des Sub-Datenkorbs, der die vorerst entfernten
Videos des Automatenbestands enthält. Mit Hilfe der Namen der vom Iterator gelieferten Einträge erzeugt man entsprechende Instanzen der Klasse VideoCassette
und fügt sie mit Hilfe des Datenkorbs dem Bestand des Kunden hinzu.
package videoautomat.transition; public class RentPayConfirmTransition implements Transition { public Gate perform(SaleProcess sp, User user) { SaleProcessRent saleProcess = (SaleProcessRent) sp; DataBasket dataBasket = saleProcess.getBasket(); MoneyBag mb_temp = (MoneyBag) saleProcess.getContext().getProcessData(SaleProcessRent.MB_TEMP_KEY); dataBasket.setCurrentSubBasket(SaleProcessRent.SUB_USER_VIDEO); StoringStock ss_user = ((AutomatUser) ((SalesPoint) saleProcess.getContext()).getUser()) .getVideoStock(); Iterator i = dataBasket.subBasketIterator( SaleProcessRent.SUB_SHOP_VIDEO, DataBasketConditionImpl.ALL_ENTRIES); while (i.hasNext()) { VideoCassette vc = new VideoCassette( ((CountingStockItemDBEntry) i.next()) .getSecondaryKey()); ss_user.add(vc, dataBasket); } . . . } }
Als nächstes muss berechnet werden wieviel Wechselgeld der Kunde erhält. Anschließend kann das Geld aus dem Geldbeutel des Prozesses in den des Automaten verschoben werden.
package videoautomat.transition; public class RentPayConfirmTransition implements Transition { public Gate perform(SaleProcess sp, User user) { dataBasket.setCurrentSubBasket(SaleProcessRent.SUB_TMP_MONEY); NumberValue nv = (NumberValue)((NumberValue) mb_temp.sumStock(dataBasket, new CatalogItemValue(), new IntegerValue(0))). subtract((NumberValue)saleProcess.getContext().getProcessData(SaleProcessRent.SUM_KEY)); . . . } }
Die Aufsummierung des eingeworfenen Geldes wird genauso wie in der Methode setButtonStockListener(final FormButton fb)
getätigt. Anschließend
wird der zu zahlende Betrag subtrahiert, so dass der Wert des Wechselgelds übrig bleibt. Das Verschieben der Einträge aus dem Geldbeutel des Prozesses in den
des Automaten vollzieht sich durch die Methode addStock(Stock st, DataBasket db, boolean b)
, wobei der übergebene boolesche Parameter darüber
entscheidet, ob die Einträge des übergebenen Bestandes gleichzeitig aus diesem entfernt werden sollen, so wie in diesem Fall, oder nicht.
Für den nächsten Schritt wird ein Algorithmus benötigt, der für einen gegebenen Wert die passenden Geldgrößen (das Wechselgeld) aus dem
Geldbeutel des Automaten herausgibt. Dafür existiert die Methode transferMoney(MoneyBag mb, DataBasket db, NumberValue nv)
in der Klasse MoneyBagImpl
.
Die Methode verschiebt dem übergebenen Wert entsprechend Einträge des Objekts in den übergebenen Geldbeutel mittels des Datenkorbs. Gibt es keine Kombination
der vorhandenen Einträge, die dem Wert entspricht (existiert kein Wechselgeld), oder es ist nicht genügend Geld vorhanden, wird eine NotEnoughMoneyException
geworfen.
Auf Basis dieser Methode lässt sich das Wechselgeld zurückgeben. Ist nicht genug bzw. kein passendes Wechselgeld verfügbar, so werden die bisher im Zustandsübergang ausgeführten Transaktionen durch Rollbacks der entsprechenden Sub-Datenkörbe rückgängig gemacht und der Prozess wechselt zurück zum Bezahlzustand.
package videoautomat.transition; public class RentPayConfirmTransition implements Transition { public Gate perform(SaleProcess sp, User user) { VideoShop.getMoneyBag().addStock(mb_temp, dataBasket, true); dataBasket.setCurrentSubBasket(SaleProcessRent.SUB_SHOP_MONEY); try{ VideoShop.getMoneyBag().transferMoney(mb_temp, dataBasket, nv); }catch(NotEnoughMoneyException e){ dataBasket.rollbackSubBasket(SaleProcessRent.SUB_USER_VIDEO); dataBasket.rollbackSubBasket(SaleProcessRent.SUB_TMP_MONEY); dataBasket.rollbackSubBasket(SaleProcessRent.SUB_SHOP_MONEY); DisplayMoneyStockError dmse = new DisplayMoneyStockError(); dataBasket.setCurrentSubBasket(SaleProcessRent.SUB_TMP_MONEY); return saleProcess.getPayGate(); } return saleProcess.getConfirmGate(); . . . } }
Es wäre jedoch wünschenswert, wenn der Kunde zumindest darüber informiert wird, dass passend gezahlt werden muss. Theoretisch ließe sich dafür ein extra Zustand definieren.
Es soll aber stattdessen eine alternative Dialogmöglichkeit des Frameworks vorgestellt werden. Mittels der Klasse JDisplayDialog
lässt sich ein Dialogfenster einblenden,
welches ein Formular anzeigen kann.
Es wird eine neue Klasse definiert, die von JDisplayDialog
erbt. Im Konstruktor dieser Klasse wird ein zuvor in der Klasse Global
definiertes MsgForm
mit dem
Aufruf popUpFormSheet(FormSheet fs)
zur Anzeige gebracht. Bei Ausführung der Aktion des Ok-Buttons soll das Formular wieder geschlossen werden.
package videoautomat; public class DisplayMoneyStockError extends JDisplayDialog { public DisplayMoneyStockError() { super(); FormSheet fs = new MsgForm( "No change!", "There is not enough change in here.n" + "Please insert the correct amount of moneyn" + "or contact the hotline."); fs.addContentCreator(new FormSheetContentCreator() { public void createFormSheetContent(FormSheet fs) { fs.getButton(FormSheet.BTNID_OK).setAction(new Action() { public void doAction(SaleProcess p, SalesPoint sp) { closeFormSheet(); } }); } }); try { popUpFormSheet(fs); } catch (InterruptedException e1) { e1.printStackTrace(); } } }
Dem MsgForm
FormSheet wurde wieder ein ContentCreator hinzugefügt, diesmal wurde er als anonyme Klasse direkt implementiert. Es lässt sich leicht
erkennen, wie die Lesbarkeit des Quelltextes darunter leidet.
Ein Objekt der Klasse DisplayMoneyStockError
wurde bereits fast am Ende der perform()
-Methode in der
RentPayConfirmTransition
hinzugefügt.
Damit ist dieses Gate und der Übergang zum Bestätigungszustand fertig.
Es fehlt nur noch das Gate mit der Bestätigungsübersicht. Die zugehörige getConfirmGate
im Prozess
sieht folgendermaßen aus.
public class SaleProcessRent extends SaleProcess { . . . public Gate getConfirmGate() { UIGate uig_confirm = new UIGate(null, null); getBasket().setCurrentSubBasket(SUB_SHOP_VIDEO); SingleTableFormSheet fs = SingleTableFormSheet.create( "Confirm your transaction!", getBasket(), uig_confirm, DataBasketConditionImpl.allStockItemsWithDest( ((AutomatUser) ((SalesPoint) getContext()).getUser()).getVideoStock()), new DefaultStoringStockDBETableEntryDescriptor()); fs.addContentCreator(new RentConfirmFSContentCreator(this, uig_confirm)); uig_confirm.setFormSheet(fs); } . . . }
Es wird wie bisher ein FormSheet
erzeugt und diesem ein ContentCreator übergeben.
Für die benötigte DataBasketCondition
wird
auf die Methode DataBasketConditionImpl.allStockItemsWithDest(Stock s)
zurückgegriffen.
Wie der Name schon sagt, filtert das zurückgegebene Objekt alle Einträge heraus,
die als Ziel den übergebenen Bestand haben. Für den TED der Videotabelle wird die Klasse
DefaultStoringStockDBETableEntryDescriptor
genutzt.
Der RentConfirmFSContentCreator
bekommt diesmal die Referenz auf den Prozess und das UIGate
übergeben.
package videoautomat.contentcreator; public class RentConfirmFSContentCreator extends FormSheetContentCreator { private SaleProcessRent processRent; private UIGate gate; public RentConfirmFSContentCreator(SaleProcessRent process, UIGate gate){ this.processRent = process; this.gate = gate; } protected void createFormSheetContent(FormSheet fs) { SingleTableFormSheet stfs_money = SingleTableFormSheet.create("", (MoneyBag)processRent.getContext().getProcessData(SaleProcessRent.MB_TEMP_KEY), gate, processRent.getBasket()); JComponent jc = new JPanel(); jc.setLayout(new BoxLayout(jc, BoxLayout.Y_AXIS)); jc.add(new JLabel("All your rented videos:")); jc.add(fs.getComponent()); jc.add(new JLabel("The money you`ll get back:")); jc.add(stfs_money.getComponent()); jc.add(new JLabel("Please, click Ok to confirm the transaction!")); fs.setComponent(jc); fs.removeButton(FormSheet.BTNID_CANCEL); fs.getButton(FormSheet.BTNID_OK).setAction(new CommitAction()); } }
Innerhalb der createFormSheetContent()
-Methode wird ein zweites SingleTableFormSheet
erzeugt.
Dafür wird die übergebene Referenz des Prozesses, um den DataBasket
zu bekommen,
und des UIGate
s benötigt. Mit Hilfe der Swing Komponenten werden beide SingleTableFormSheet
s
kombiniert in einer Anzeige dargestellt.
Eines zeigt einen Datenkorb unter eingeschränkten Bedingungen an, die durch eine Instanz der Klasse
DataBasketCondition
vorgegeben werden müssen.
Das Andere repräsentiert den Geldbeutel und das in ihm befindliche Wechselgeld.
Die Anzeigekomponenten der beiden Instanzen werden in einem neuen JPanel
-Objekt zusammengefasst.
Der Cancel-Button wird entfernt, der Ok-Button beibehalten.
Wie man sieht wird in der Aktion des Ok-Buttons der Wechsel zum Commit-Gate
eingeleitet. Damit werden alle Änderungen, für die der Datenkorb benutzt wurde, auf einen Schlag persistent gemacht.
Der Ausleihprozess kann nach erfolgter Übersetzung und Ausführung komplett durchlaufen werden.
Es wird noch eine hässliche Exception geworfen, wenn der Kunde den Geldbetrag passend bezahlt und anschließend bestätigt. Die Ausnahme hat ihren Ursprung in den Untiefen des Pakets javax.swing
. Irgendwie wird versucht, nicht mehr existente Tabelleneinträge zu rendern, nachdem die Transaktion zwischen dem Geldbeutel des Prozesses und dem des Automaten stattfand. Um dies zu umgehen, kann man einfach das Formular des Automaten auf null
setzen, bevor die Einträge verschoben werden.
package videoautomat.transition; public class RentPayConfirmTransition implements Transition { public Gate perform(SaleProcess sp, User user) { . . . try { saleProcess.getContext().setFormSheet(saleProcess, null); } catch (InterruptedException e1) { e1.printStackTrace(); } . . . } }
Der Ausleihprozess ist nun wirklich abgeschlossen. Nach einer erneuten Übersetzung und Ausführung können nach Herzenslust Videos ausgeliehen werden.
Die Leih-Kassette | Das Protokollieren |