Videoautomat - Der Ausleihprozess

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.



Der Ausleihprozess als Zustandsdiagramm
Abbildung 11.1: Der Ausleihprozess als Zustandsdiagramm

Die Gates 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 {
    private MoneyBagImpl temporaryMoneyBag;
    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()));
    }
}
		

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 FormSheets, so wird auch hier in der getInitialGate()-Methode eine Instanz des TwoTableFormSheets erzeugt und mit meinem speziellen FormSheetContentCreator versehen.

Im Vorfeld wird in der Methode ein MoneyBag initialisiert und unserem Prozess zugewiesen, wofür wir uns einen entsprechenden Setter (setTemporaryMoneyBag(MoneyBagImpl temporaryMoneyBag)) geschrieben haben. Letzteres ist nicht nur für unser MoneyBag nützlich, sondern ganz allgemein für jede Art von Attributen, die über einen gesamten Prozess hinweg verfügbar sein sollen. D.h. in allen Gates 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. 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() {
        setTemporaryMoneyBag(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,
                    TwoTableFormSheet.RIGHT);
        ttfs_rent.addContentCreator(new RentTTFSContentCreator());
    }
		.
		.
		.
}
		

Die weiteren Anpassungen des FormSheets und die Aktionen werden wieder in einer speziellen Implementation des FormSheetContentCreator durchgeführt: RentTTFSContentCreator.

package videoautomat.contentcreator;
public class RentTTFSContentCreator extends FormSheetContentCreator {
   private static final long serialVersionUID = 2068978338716582033L;
   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-Gates 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 FormSheets 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 = getSumNumberValue();
        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(),
                getTemporaryMoneyBag(),
                getBasket(),
                uig_pay,
                new ComparatorCurrency(),
                new ComparatorCurrency(),
                false,
                null,
                null,
                ccss,
                TwoTableFormSheet.RIGHT);
        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<CatalogItem>, Serializable {
    public ComparatorCurrency() {
    }
    public int compare(CatalogItem arg0, CatalogItem 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 static final long serialVersionUID = 9181091654692530939L;
   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));
      ((SaleProcessRent)sp).setSumNumberValue(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 {
    private static final long serialVersionUID = -4980523266307469497L;
   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 StockChangeListeners 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 = processRent.getTemporaryMoneyBag();
      final DataBasket basket = processRent.getBasket();
      final NumberValue nv_sum = processRent.getSumNumberValue();

      StockChangeAdapter sca = new StockChangeAdapter() {
        private static final long serialVersionUID = 5693744331821735484L;
        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 = saleProcess.getTemporaryMoneyBag();


      dataBasket.setCurrentSubBasket(SaleProcessRent.SUB_USER_VIDEO);
      UserVideoStock 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(saleProcess.getSumNumberValue());
		.
		.
                .
   }
}
                

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);
         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 {
    private static final long serialVersionUID = 5857508089955396734L;
    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() {
            private static final long serialVersionUID = 7987316125303271072L;

            public void createFormSheetContent(FormSheet fs) {
                fs.getButton(FormSheet.BTNID_OK).setAction(new Action() {
                    private static final long serialVersionUID = 1060004498677039521L;

                    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 static final long serialVersionUID = 928189229586346904L;
   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("", processRent.getTemporaryMoneyBag(), 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 UIGates benötigt. Mit Hilfe der Swing Komponenten werden beide SingleTableFormSheets 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.


previous Die Leih-KassetteDas Protokollieren next



by Hannes John