Angemessene Reaktion:
Abfertigung
Prinzip der Abfertigung
Regeln gegen die Komplexität
Symbolische Konstanten
Abfertigung über Arrays
Polymorphe Afertigung
Callback
Grundstruktur Abfertigung
(Dispatcher)
• Praktisch jedes Programm hat eine zentrale Abfertigung
("Eingangskontrolle")
• Grundstruktur:
if (bedingung1) aktion1();
else if (bedingung2) aktion2();
else if (bedingung3) ....
• Oft gibt es tiefer im Programm noch weitere solche Abfertiger.
• Abfertiger sind zentral für das Verständnis des Programms.
• Die Struktur ist oft nicht so einfach:
– schwierige kombinierte Bedingungen
– mehrstufige Fallunterscheidungen
Abfertigung
Metapher Schalterhalle:
• Anstellen immer an der kürzesten Schlange
• Abfertigungsschalter mit unterschiedlichen
Kompetenzen: D, EU, Int (je 2 D+EU, EU+Int)
• Abfertigungsstrategie:
Reihum aus Schlangen entnehmen. Falls freie
Kompetenz nicht passt, nächste Schlange.
Gerecht???
• Anstellen nach Nationalitäten
• Entnahme aus der Schlange mit der passenden
Nationalität, möglichst im Wechsel.
Kontext
class Zentrale {
List <Queue<Person>> halle = new List<>();
List <Schalter> schalter = new List<>();
Schalter aktuellerSchalter;
int next;
...
}
class Schalter extends Queue<Person> {
public Kompetenz kompetenz;
public static boolean passt(Nationalitaet,
Kompetenz) {...}
...
}
Abfertigungsstrategie
public void dispatch() {
for (int i=0: i<halle.size(); i++)
{ if (halle.get(next).size()!=0) break;
next=(next+1)%halle.size();
}
if (halle.get(next).size()==0) return;
if (Schalter.passt
(halle.get(next).peek().nationalitaet,
aktuellerSchalter.kompetenz))
aktuellerSchalter.add(halle.get(next)
.remove());
next=(next+1)%halle.size();
Wer versteht
das??
}
Break und Continue
• break
– verlässt den innersten switch-, while- oder
for-Block
– die entsprechende Schleife wird beendet
• continue
– verlässt den aktuellen while- oder for-Block
– die Schleife wird mit dem nächsten
Durchgang fortgesetzt.
Übersicht bitte!
public void dispatch() {
int next = naechsteNichtleereSchlange();
if (istLeer(next)) return;
if (aktuellerSchalterIstZustaendig())
entnimmUndBearbeite();
next = (next+1)%halle.size();
}
private boolean aktuellerSchalterIstZustaendig() {
return Schalter.passt
(halle.get(next).peek().nationalitaet,
aktuellerSchalter.kompetenz);
}
private void entnimmUndBearbeite() {
aktuellerSchalter.add(halle.get(next).remove());
Abfertigung als
Exception-Zentrale
• Abfertiger sind typische Exception-Zentralen:
– Prüfen der Verteilungskriterien –ggf. Exception werfen
– Fangen und Behandeln der Exceptions aus den Aktionen
• Grundstruktur:
if (bedingung1) {
try { aktion1(); }
catch (ActionException e)
{ /* Rep. oder Meldg. */ }
}else if (bedingung2)
try { aktion2(); }
catch (ActionException e)
{ /* Rep. oder Meldg. */ }
}else throw new DispatchException();
Verständnis-Alarm
• Abweichungen von der Grundstruktur sind die Regel:
• Nicht-triviale Bedingungen,
vielfach kombiniert
und ineinander verschachtelt
• verwirrend häufige Fehlerquellen!
• Abhilfe durch Programmierstil!
Zur Illustration genügt ein "harmloses" Beispiel...
Beispiel Taschenrechner
Rechenwerk
verarbeiteEingabe (char c) : String
je nach Eingabe muss
unterschiedlich verfahren
werden: Dispatch!
Rechenwerk
verarbeiteEingabe (char c) : String
public void act() {
char eingabe = RechnerGUI.naechsterButton();
verarbeiteEingabe(eingabe);
}
private verarbeiteEingabe(char eingabe) {
// ziffer
anhängen und anzeigen
// operator letzten operator ausführen;
//
zahl und operator speichern.
// etc.
}
class Rechenwerk{
double zahl0 = 0;
String zahl1 = "";
char op = '=';
boolean bruch=false;
public String verarbeiteEingabe (char c) {
if (c>='0'&&c<='9'){ zahl1+=c; return zahl1; }
else { if (c=='.') {
if (!bruch) { bruch = true; zahl1+=c; return zahl1;}
else return "error – more than one decimal point";
}
else { if (c=='=' || ( c>=42 && c<=47) ) {
switch (op) {
case '+': zahl0 +=
Double.parseDouble(zahl1);
zahl1=""; break;
case '-': zahl0 -=
Double.parseDouble(zahl1);
vielleicht richtig
zahl1=""; break; //etc...
(??)
case '=': zahl0 =
aber
Double.parseDouble(zahl1);
grottenschlecht
zahl1=""; break;
programmiert!
default: return "error – illegal operator";
}
op = c;
} else return "error – invalid input";
}
}
Rechenwerk mit Dispatch
Regeln gegen die Komplexität
1.
Trenne Fallunterscheidung und Aktion!
2.
Benenne die Aktionen durch Methoden!
Rechnerstatus
abf ertigen(char eingabe)
3.
Rechenwerk
zif f er( )
operator( )
gleich( )
v orzeichen( )
dezimalpunkt( )
Benenne die Fälle durch boolesche Funktionen!
private boolean istZiffer(char c);
private boolean istOperator(char c);
// etc.
4. Verwende kaskadiertes if!
• statt:
if (a) aa;
else /* also !a */
{ if (b) bb;
else /* also !a & !b */
{ if (c) cc;
else /* also !a & !b & !c */ dd;
}
}
"rechts-verzweigt"
• einfacher:
a
if (a) aa;
else if (b) bb;
b
aa
else if (c) cc;
else dd;
c
bb
(analog switch)
cc
dd
Komplexitäts-reduzierte Abfertigung
/** Bearbeitung eines Eingabezeiches:
* Weiterleitung an die Rechenwerk-Methoden
* Grundidee:
*
ziffer anhängen und Anzeigewert liefern
*
operator letzten operator ausführen;
*
zahl und operator speichern.
*
Anzeigewert liefern
*/
public String abfertigen(char eingabe) {
if (istZiffer(eingabe)) return werk.ziffer(eingabe);
else if (istOperator(eingabe))
return werk.operator(eingabe);
else if (istDezimalpunkt(eingabe))
return werk.dezimalpunkt(eingabe);
else if (istGleich(eingabe))
return werk.gleich(eingabe);
else return fehler(eingabe);
}
Vorsicht bei Seiteneffekten!
• Fallunterscheidung durch boolesche Funktionen
(oder Ausdrücke)
• Seiteneffekt: die Abfragereihenfolge ist nicht beliebig
(unlogisch!):
private boolean istZiffer() {
return (schlange.entnehmen()>='0'
&& schlange.entnehmen()<='9');
}
So etwas
passiert Ihnen
natürlich nicht...
// istOperator, istGleich entsprechend..
private abfertigen() {
if (istZiffer()) werk.ziffer(schlange.entnehmen());
else if (istOperator()) werk,operator(schlange.entnehmen());
// etc
}
Symbolische Konstanten
• Fallunterscheidung über eine kleine feste Menge von
Alternativen
• Darstellbar als Symbolische Konstanten
• Beispiel:
EtchASketch.GREEN
EtchASketch.BLUE
• Symbolische Konstanten sind static final und
typischerweise int-Werte (nicht notwendig)
• Seit Java5 gibt es dafür auch Enumerationstypen
Beispiele für Symbolische Konstanten
public static final int
PLUS_KNOPF = 10,
MINUS_KNOPF = 11,
MAL_KNOPF = 12;
java.awt.Color.GREY
java.awt.BorderLayout.CENTER
java.awt.Component.BOTTOM_ALIGNMENT
java.awt.Label.RIGHT
java.lang.Math.PI // "echte" Konstante
java.lang.Double.NaN
Grundsätzlich alle Literale benennen, außer 0 und 1!
Symbolische Konstanten
können mit == verglichen werden
Sollten nicht in arithmetischen Ausdrücken
vorkommen
sind oft Bestandteil eines Interfaces
Ab Java5 gibt es Enumerationen als Typ mit
ähnlichen Eigenschaften, aber darüber sprechen wir
ein anderes Mal.
Fallunterscheidung mit Symbolischen
Konstanten
• Oft ist es sinnvoll, symbolischen Konstanten eine Textform
zuzuordnen (analog toString())
• Schreiben wir eine statische Methode dafür:
public static String toString(int knopfID) {
if (knopfID == Rechner.PLUS_KNOPF)return "+";
else if (knopfID == Rechner.Minus_KNOPF)return "-";
else if (knopfID == Rechner.MAL_KNOPF)return "*";
// ...
}
• Beschriftung der Knöpfe ist dann ganz einfach:
Button plusknopf =
new Button(Rechner.toString(Rechner.PLUS_KNOPF));
Button minusknopf =
new Button(Rechner.toString(Rechner.MINUS_KNOPF));
Fallunterscheidung Array-Indizes
• Wir können das klasseninterne Wissen über die intKodierung der Tasten nutzen, um die Textdorm in
einem Array zu speichern:
static String[] tastentext =
{ "0","1","2","3",
"4","5","6","7",
"8","9","+","-",...
};
hier wird das
Geheimnisprinzip
wichtig!
Dann können wir die symbolischen Konstanten
zur Indizierung nutzen:
Button achtButton =
new Button(Rechner.tastentext[Rechner.ACHT_KNOPF]);
Fallunterscheidung ohne Abfrage
• Die Indizierung macht die Abfertigung einer Taste
möglich, ohne dabei die Taste "anzusehen" – d.h.
Fallunterscheidung ohne Abfrage:
class Rechner {
class
Rechner
public
void {
tasteAusgeben(int tastenID) {
switch (tastenID) {
public void
tastenID) {
case tasteAusgeben(int
PLUS_KNOPF:
ausgeben(tastentext[tastenID])
ausgeben(tastentext[PLUS_KNOPF]); break;
}
case MINUS_KNOPF:
}
ausgeben(tastentext[MINUS_KNOPF]); break;
// ...
}
}
}
"Übertragbares Verhalten"
Weiter im BeispielTaschenrechner:
Jeder Taste ist eine Reaktion zugeordnet
Bisher kennen wir Verteiler über if, switch
oder Array
Neuer Ansatz: Verhaltenskapsel
Objekte "wissen" selbst, wie sie sich verhalten
sollen
Verhalten kann Objekten zu Laufzeit zugeordnet
werden – gekapselt in ein Objekt.
Polymorphe Abfertigung
Interface Tastenbehandler mit drei
Implementierungen:
– Zifferntaste
– Operatortaste
– Cleartaste
Gemeinsame Methode
tasteGedrueckt()
Fallunterschiedung über Typen
if (taste instanceOf Zifferntaste)
((Zifferntaste)taste).tasteGedrueckt();
else if (taste instanceOf Operatortaste)
((Operatortaste)taste).tasteGedrueckt();
else if ((taste instanceOf Cleartaste)
((Cleartaste)taste).tasteGedrueckt();
else /* kann nicht sein */;
Polymorphie ersetzt die Fallunterscheidung
taste.tasteGedrueckt();
Taschenrechner – mit gekapseltem
Verhalten
gekapseltes
Verhalten
liesNaechsteTaste()
Rechner
tasteGedrueckt
(AnimateObject)
Tastenbehandler
RechnerGui
Ziffern
-taste
Operatortaste
Cleartaste
Rechner -Überblick
Der Rechner macht immer dasselbe:
– Taste von der Gui lesen
– Taste verarbeiten durch Rufen der Methode tasteGedrueckt()
eines Tastenbehandlers
Trotzdem passiert Unterschiedliches:
– verschiedene Tastenbehandler implementieren
"tasteGedrueckt" verschieden Polymorphie
Tastenbehandler kapseln Verhalten
– Verhalten kann dynamisch zugeordnet werden
Erforderlich: ein Interface
- als Vertrag über die Methodensignatur
Rechner – Gekapseltes Verhalten
interface Tastenbehandler {
tasteGedrueckt(Rechner rechner);
}
class Zifferntaste implements Tastenbehandler {
final int wert;
Zifferntaste(int wert) {
this.wert = wert;
}
void tasteGedrueckt(Rechner rechner) {
rechner.anhaengen(wert);
}
}
class Operatortaste implements Tastenbehandler
{ /* analog */ }
class Cleartaste implements Tastenbehandler
{ /* analog */ }
Rechner – Codegerüst:
Handler-Array initialisieren
class Rechner extends AnimateObject {
RechnerGUI gui = new RechnerGUI();
Tastenbehandler[] tasten = new Tastenbehandler[];
Rechner() {
// Dispatch-Array einrichten:
for (int i=0; i<10; i++)
tasten[i] = new Zifferntaste(i);
for (int i=Rechner.PLUS_TASTE;
i<Rechner.GLEICH_TASTE; i++)
tasten[i] = new Operatortaste(i);
tasten[Rechner.CLEAR_TASTE] = new ClearTaste();
}
Rechner – Codegerüst:
Polymorphe Bearbeitung
public class Rechner implements Animate {
private RechnerGUI gui;
private Tastenbehandler[] tasten;
void act() {
int tastencode = -1;
try { tastencode = gui.liesNaechsteTaste();
tasten[tastencode].tasteGedrueckt(this);
} catch (KeineTasteException e)
{ /* ignorieren */ }
}
}
polymorph!
Ein Standard-Interface
• Animate aus cs101 hat eine parameterlose
Methode: act()
• kann zur Verhaltenskapselung benutzt werden –
auch ohne Thread:
void bearbeite(Animate technik){
technik.act(); // polymorph!
}
• Ebenso eignet sich das Interface Runnable aus
java.lang – es definiert die Methode run():
void bearbeite(Runnable technik){
technik.run(); // polymorph!
}
Callback
liesNaechsteTaste()
Rechner
tasteGedrueckt(Rechner
r)
tasteGedrueckt
(AnimateObject)
Tastenbehandler
RechnerGui
anhaengen()
ausrechnen()
neueAufgabe()
Ziffern
-taste
void tasteGedrueckt(Rechner rechner)
{ rechner.anhaengen(wert); }
Operatortaste
Cleartaste
Callback
Callback bedeutet, dass die gerufene
Methode ihren Aufrufer "zwischendurch"
kontaktieren will.
Dazu muss sie den Aufrufer "kennen" – d.h.
eine Referenz auf ihn besitzen.
- Technik: Der Aufrufer gibt sie als Parameter mit.
Sie muss aber auch den Typ des Aufrufers
kennen!
- Technik: oft implementiert der Aufrufer ein
Interface
... genug für diesmal!
Üben Sie Abfertigung in Nodenet
– das Callback-Prinzip brauchen
wir für GUIs