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