Automaten und das State Pattern Axel Böttcher 24. September 2012
(GUI-)Logik mittels Automaten realisieren Das Problem, komplizierte Logik zu implementieren tritt recht häufig auf. Smells: häufig wiederholte switch-statements ( wicked switch ) komplizierte und verschachtelte if/else-konstrukte Häufig anzutreffen, um den Zustand von GUIs zu speichern. Es lohnt sich, darüber nachzudenken, ob sich eine standardisierte Automatenimplemntierung lohnt (muss nicht unbedingt das recht aufwändige State Pattern sein).
Exkurs: Endliche Automaten Automat zum Filtern von Kommentaren aus C-/Java-Code (nicht ganz vollständig!). Zeichenweises Lesen von Zeichen char. kein "*"-->"" "*"-->"" Comment kein "/"-->"" "*"-->"" "*"-->"" Asterisk kein "/"-->char "/"-->"" Slash "/"-->"" kein newline-->"" Programm kein "*"-->"/"+char newline-->"" LEComment "/"-->"" Legende: an den Zustandsübergängen bedeutet x > y, dass x eingelesen wurde und y ausgegeben wird.
Implementierungsvariante I (geradeaus) State wird durch enum modelliert: 1 public enum StateEnum { 2 PROG, SLASH, ASTERISK, COMMENT 3 } Programmausschnitt: 1 StateEnum state = StateEnum. PROG; 2 3 while((character = reader.read())!= -1) { 4 switch(state) { 5 case PROG: 6 if (character == / ) { 7 state = StateEnum.SLASH; 8 } 9 else { 10 writer.print(( char)character); 11 } 12 break; 13 case SLASH: 14 if (character == * ) { 15 state = StateEnum. COMMENT; 16 } 17 else { 18 writer.print("/" + ( char) character); 19 state = StateEnum.PROG; 20 } 21 break; 22...
Implementierungsvariante II (Tabellen-basiert) Zwei Interfaces beschreiben Zeichenprüfung und auszuführende Aktion(en): 1 private interface TestInput { 2 boolean test(char c); 3 } 4 5 private interface Output{ 6 void write(char c); 7 }
Basis ist eine Klasse für die Transitionen: 1 private static class Transition { 2 private StateEnum z1; 3 private StateEnum z2; 4 Output action; 5 private TestInput input; 6 7 public Transition( final StateEnum z1, 8 TestInput input, 9 StateEnum z2, 10 Output action) { 11 this.z1 = z1; 12 this.input = input; 13 this.action = action; 14 this.z2 = z2; 15 } 16 }
Automatentabelle Die Automaten-Tabelle wird fast so hingeschrieben, wie man das in den Grundlagenvorlesungen lernt: 1 private Transition[] autotab = new Transition[] { 2 new Transition( 3 StateEnum.PROG, 4 new TestInput() {public boolean test(char c){return c!= / ;}}, 5 StateEnum.PROG, 6 new Output() {public void write(char c){writer.print(c);}}), 7 new Transition( 8 StateEnum.SLASH, 9 new TestInput() {public boolean test(char c){return c!= * ;}}, 10 StateEnum.PROG, 11 new Output() {public void write(char c){writer.print("/" + c);}}), 12... 13 };
Tabelle wird bei jedem auftretenden Ereignis durchsucht: 1 public final void handleevent( final char event) { 2 for (int i = 0; i < autotab.length; i++) { 3 if (currentstate == autotab[i].z1 4 && autotab[i].input.test(event)) { 5 autotab[i]. action. write(event); 6 currentstate = autotab[i].z2; 7 break; 8 } 9 } 10 } 11
Hoher wieder verwendbarer Anteil Generierung automatisierbar Automatentabelle steht in Tabellenform da Leicht erweiterbar Günstig, wenn Events als kleiner enum-bereich dargestellt werden kann Aufwändig, wenn große Menge möglicher Events, von denen für viele eine Default-Aktion auszuführen ist Achtung: Dies ist nicht objekt-orientiert. Diese Variante bietet sich in embedded systems an.
Implementierungsvariante III (State Pattern) Für jeden möglichen Zustand gibt es eine Klasse 1 public interface AutoState { 2 public AutoState handlechar(char c, PrintStream writer); 3 }... 1 public class StateSlash implements AutoState { 2 3 public AutoState handlechar(char c, 4 PrintStream writer) { 5 if (c == * ) { 6 re turn new StateComment(); 7 } 8 writer.write( / ); 9 writer. write(c); 10 re turn new StateProg(); 11 } 12 }
Der Client delegiert dann einfach an das jeweilige Zustandsobjekt: 1 AutoState currentstate = new StateProg(); 2 3 while((character = br.read())!= -1) { 4 currentstate = currentstate. 5 handlechar(( char) character, writer); 6 }
Klassendiagramm
Beispiel: Refactoring einer GUI-Blob-Klasse 1 private final JButton door = new JButton(); 2... 3 private Timer timer; 4 timer = new Timer(6000, new ActionListener () { 5 public void actionperformed ( final ActionEvent e) { 6 tube. setbackground(color. GREEN); 7 lamp. setbackground(color. WHITE); 8 }}); 9 startbutton. addactionlistener (new ActionListener () { 10 public void actionperformed ( final ActionEvent e) { 11 if (!isdooropen) { 12 lamp. setbackground(color.red); 13 tube. setbackground(color. YELLOW); 14 timer.start(); 15 } 16 }});
1 door. addactionlistener (new ActionListener () { 2 public void actionperformed ( final ActionEvent e) { 3 if (!isdooropen) { 4 door. seticon(new ImageIcon(" openimage.gif")); 5 lamp. setbackground(color.red); 6 tube. setbackground(color. GREEN); 7 timer.stop(); 8 isdooropen = true; 9 } 10 else { 11 door. seticon(new ImageIcon(" closedimage. gif")); 12 lamp. setbackground(color. WHITE); 13 isdooropen = false ; 14 } 15 }});
1. Schritt: Modell extrahieren Das Modell ist relativ dumm (die Logik steckt später im Controller): 1 public interface OvenModel { 2 public void setlamp(boolean on); 3 public boolean islampon(); 4 public void setheating(boolean on); 5 public boolean isheating(); 6 public void setdoor(boolean open); 7 public boolean isdooropen(); 8 public void addovenmodelobserver ( final OvenModelObserver observer); 9 } Observer Pattern für Aktualisierung von Views erforderlich: 1 public interface OvenModelObserver { 2 public void update(ovenmodel model); 3 }
2. Schritt: Controller extrahieren Der Controller soll alle Events aus dem View behandeln: 1 public interface OvenController { 2 void setmodel( OvenModel model); 3 public void handleevent( final OvenEvent event); 4 } Die Listener der GUI-Widgets delegieren ausschließlich an den Controller weiter: 1 door. addactionlistener (new ActionListener () { 2 public void actionperformed ( final ActionEvent e) { 3 controller. handleevent( OvenEvent.DOOR); 4 }});
Sequenzdiagramm Die erforderlichen Events müssensntsprechend definiert werden: 1 private enum OvenState { 2 IDLE, OPEN, HEATING 3 } Die Controller-Implementierung ist damit vollständig austauschbar.
Übung: Controller implementieren