📜 ⬆️ ⬇️

Implementing a graphical interface as a finite automaton

Having come to a large project that uses Swing as a graphical interface, I realized that it’s not easy to understand the logic and connections between JDialog or JFrame components. And all the time that I dealt with this code, I tried to find some kind of universal solution that would avoid the complexity of the links between interface elements.



The main problem I encountered: each element has one or more PropertyChangeListener, which, depending on the situation (read on the state of other elements), affect other elements in some way. Those elements, in turn, also have their own PropertyChangeListener, which also affect others depending on the context. It was sometimes not very easy to understand how pressing a button would affect all other elements, and finding a piece of code where this happens is, all the more so. It also complicated the change of dependencies (to change the logical rule, we had to search for all the connections in different places on different components, and most likely something was missing) and debugging.
')
In such a situation, the Mediator pattern could help, but again it will be difficult to trace all the connections and the logic of interaction. You can break each JDialog into smaller JPanels that encapsulate certain logical places, but again, we need to keep these JPanels interact with each other. If JDialog is so simple, there will be nothing to break.

I came up with the idea that JDialog is always in strictly defined states at a certain point in time. For example, the dialogue has been created, it is in the “I just created” state, then the user or the program logic switches the dialogue to the “I process” state, if the user clicked on the cancel button during “I process”, the dialog changes to the “ I was interrupted. "

Why reason in this vein is good? At each separate moment of time we can concentrate on drawing one logical state. And, therefore, all the code responsible for the state of the elements will be in one logical and physical place. You will not need to search all PropertyChangeListeners to understand how the JButton “Cancel” can affect the JLabel status when it was clicked after certain user interactions. The same goes for making changes. For example, we have a dialogue that does something in the background thread and publishes its progress. The dialog displays a progress bar with current progress. If the user clicks the "Cancel" button, we reset the JProgressBar. Now we also decided to disable the background process launch button if it was canceled. When our interface is written using states, we simply find the “I was interrupted” state, and we add startButton.setEnabled (false) there, and we are sure of our change, knowing that it only affected the logical state “I was canceled”.

So, it is necessary to program the user interface as a set of states in which the dialogue enters and out of which. Less words more code!

As an example, we consider a dialogue that allows you to find out the answer to the main question.

this class represents one who knows the answer:
/** * class that can answer to the maing question * @author __nocach */ public class MeaningOfLifeAnswerer { public int answer(){ return 42; } } 


Since the answer is not so simple, we also need to prepare a class that can answer the main question. Preparation takes a long time, and it needs to be done through SwingWorker.
 /** * worker that prepares MeaningOfLifeAnswerer * @author __nocach */ public class PrepareToAnswerMeaningOfLife extends SwingWorker<MeaningOfLifeAnswerer, Void>{ @Override protected MeaningOfLifeAnswerer doInBackground() throws Exception { Thread.sleep(1500); return new MeaningOfLifeAnswerer(); } } 


The search for the answer itself is also quite a long operation, and should be done through SwingWorker:
 /** * worker that will retrieve answer to the main question using passed Answerer * @author __nocach */ public class RetrieveMeaningOfLife extends SwingWorker<Integer, Integer>{ private final MeaningOfLifeAnswerer answerer; public RetrieveMeaningOfLife(MeaningOfLifeAnswerer answerer){ if (answerer == null){ throw new NullPointerException("prepareProvider can't be null"); } this.answerer = answerer; } @Override protected Integer doInBackground() throws Exception { for(int i = 0; i < 100; i++){ Thread.sleep(10); setProgress(i); } return answerer.answer(); } } 


Requirements from the interface: After creating and displaying the dialog, we start the worker to initialize MeaningOfLifeAnswerer, disable the button for the search, and write in the status that we are preparing MeaningOfLifeAnswerer. As soon as MeaningOfLifeAnswerer is initialized, we enable the button for the search. By pressing the search button, we launch the RetrieveMeaningOfLife worker, disable the search button and write that we are in search. Once the answer is found, turn on the search button again and write on the button that we are ready to search again.

The usual approach would look something like this:
 public class StandardWay extends javax.swing.JFrame { private Logger logger = Logger.getLogger(StandardWay.class.getName()); private class FindAnswerAction extends AbstractAction{ private final MeaningOfLifeAnswerer answerer; public FindAnswerAction(MeaningOfLifeAnswerer answerer){ super("Find"); this.answerer = answerer; } @Override public void actionPerformed(ActionEvent e) { RetrieveMeaningOfLife retrieveWorker = new RetrieveMeaningOfLife(answerer); retrieveWorker.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if ("progress".equals(evt.getPropertyName())){ progressBar.setValue((Integer)evt.getNewValue()); } if ("state".equals(evt.getPropertyName())){ if (StateValue.STARTED.equals(evt.getNewValue())){ //    doButton.setText("In Search"); doButton.setEnabled(false); labelStatus.setText("searching..."); } if (StateValue.DONE.equals(evt.getNewValue())){ RetrieveMeaningOfLife worker = (RetrieveMeaningOfLife)evt.getSource(); try{ Integer answer = worker.get(); //   logger.info("got the answer"); JOptionPane.showMessageDialog(rootPane, "THE ANSWER IS " + answer); } catch(Exception ex){ //     logger.info("error while retrieving the answer"); JOptionPane.showMessageDialog(rootPane, "Error while searching for meaning of life"); } labelStatus.setText("answer was found"); doButton.setText("Find again"); doButton.setEnabled(true); } } } }); retrieveWorker.execute(); } } /** * listener that updates gui state by progress of PrepareToAnswerMeaningOfLife worker * @author __nocach * */ private class PrepareToAnswerMeaningOfLifeListener implements PropertyChangeListener{ @Override public void propertyChange(PropertyChangeEvent evt) { if ("state".equals(evt.getPropertyName())){ if (StateValue.STARTED.equals(evt.getNewValue())){ //      MeaningOfLifeAnswerer labelStatus.setText("Prepearing... "); doButton.setEnabled(false); logger.info("preparing..."); } if (StateValue.DONE.equals(evt.getNewValue())){ //           labelStatus.setText("I am prepared to answer the meaning of life"); doButton.setEnabled(true); PrepareToAnswerMeaningOfLife worker = (PrepareToAnswerMeaningOfLife)evt.getSource(); try{ doButton.setAction(new FindAnswerAction(worker.get())); logger.info("prepared"); } catch(Exception ex){ //    MeaningOfLifeAnswerer JOptionPane.showMessageDialog(rootPane, "failed to find answerer to the question"); dispose(); logger.severe("failed to prepare"); } } } } } /** Creates new form StandardWay */ public StandardWay() { initComponents(); PrepareToAnswerMeaningOfLife prepareWorker = new PrepareToAnswerMeaningOfLife(); prepareWorker.addPropertyChangeListener(new PrepareToAnswerMeaningOfLifeListener()); prepareWorker.execute(); } //... //     JFrame  //... private javax.swing.JButton doButton; private javax.swing.JLabel labelStatus; private javax.swing.JProgressBar progressBar; } 


So, all the code for changing the state is hardcoded in two PropertyChangeListeners (the PrepareToAnswerMeaningOfLife worker and the RetrieveMeaningOfLife worker). The logic of the dialogue with PrepareToAnswerMeaningOfLifeListener begins, monitors the progress of the PrepareToAnswerMeaningOfLife started, then, after successful initialization, the search button receives the FindAnswerAction, which, when clicked, launches the RetrieveMeaningOfLife vorker. In the same place we add anonymous PropertyChangeListener to synchronize the state of our interface while searching for the answer to the main question. In fact, the above listing can be brought into an acceptable form, if each, logically whole state change is put into a separate method of the type setViewToPreparing (), where the code will be
 private void setViewToPreparing(){ labelStatus.setText("Prepearing... "); doButton.setEnabled(false); logger.info("preparing..."); } 

But if in your dialogue each such logical piece will be more than 20 lines, then the removed methods will need to be broken into even further small methods, in total, the JFrame class will be filled with a huge number of not particularly related private methods.

What would the approach look like using State Machine:
 public class StateMachineWay extends javax.swing.JFrame { private Logger logger = Logger.getLogger(StandardWay.class.getName()); /** * controlls switching between gui states */ private GuiStateManager stateManager = new GuiStateManager(); private class PreparingAnswererState extends BaseGuiState{ @Override public void enterState() { labelStatus.setText("Prepearing... "); doButton.setEnabled(false); } } private class ReadyToFindTheAnswer extends BaseGuiState{ private final MeaningOfLifeAnswerer answerer; public ReadyToFindTheAnswer(MeaningOfLifeAnswerer answerer){ this.answerer = answerer; } @Override public void enterState() { labelStatus.setText("I am prepared to answer the meaning of life"); doButton.setEnabled(true); doButton.setAction(new FindAnswerAction(answerer)); } } private class FoundAnswerState extends BaseGuiState{ private final Integer answer; public FoundAnswerState(Integer answer){ this.answer = answer; } @Override public void enterState() { labelStatus.setText("answer was found"); doButton.setText("Find again"); doButton.setEnabled(true); JOptionPane.showMessageDialog(rootPane, "THE ANSWER IS " + answer); } } private class FailedToPrepareAnswerer extends BaseGuiState{ @Override public void enterState() { JOptionPane.showMessageDialog(rootPane, "failed to find answerer to the question"); dispose(); } } private class FailedToFoundAnswer extends BaseGuiState{ @Override public void enterState() { labelStatus.setText("failed to find answer"); doButton.setText("Try again"); doButton.setEnabled(true); JOptionPane.showMessageDialog(rootPane, "Error while searching for meaning of life"); } } private class SearchingForAnswer extends BaseGuiState{ @Override public void enterState() { labelStatus.setText("searching..."); doButton.setText("In Search"); doButton.setEnabled(false); } } /** * actions that starts worker that will find the answer to the main question * @author __nocach * */ private class FindAnswerAction extends AbstractAction{ private final MeaningOfLifeAnswerer answerer; public FindAnswerAction(MeaningOfLifeAnswerer answerer){ super("Find"); this.answerer = answerer; } @Override public void actionPerformed(ActionEvent e) { RetrieveMeaningOfLife retrieveWorker = new RetrieveMeaningOfLife(answerer); retrieveWorker.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if ("progress".equals(evt.getPropertyName())){ progressBar.setValue((Integer)evt.getNewValue()); } if ("state".equals(evt.getPropertyName())){ if (StateValue.DONE.equals(evt.getNewValue())){ RetrieveMeaningOfLife worker = (RetrieveMeaningOfLife)evt.getSource(); try{ Integer answer = worker.get(); stateManager.switchTo(new FoundAnswerState(answer)); logger.info("got the answer"); } catch(Exception ex){ logger.info("error while retrieving the answer"); stateManager.switchTo(new FailedToFoundAnswer()); } } if (StateValue.STARTED.equals(evt.getNewValue())){ stateManager.switchTo(new SearchingForAnswer()); } } } }); retrieveWorker.execute(); } } /** * listener that updates gui state by progress of PrepareToAnswerMeaningOfLife worker * @author __nocach * */ private class PrepareToAnswerMeaningOfLifeListener implements PropertyChangeListener{ @Override public void propertyChange(PropertyChangeEvent evt) { if ("state".equals(evt.getPropertyName())){ if (StateValue.STARTED.equals(evt.getNewValue())){ logger.info("preparing..."); stateManager.switchTo(new PreparingAnswererState()); } if (StateValue.DONE.equals(evt.getNewValue())){ PrepareToAnswerMeaningOfLife worker = (PrepareToAnswerMeaningOfLife)evt.getSource(); try{ MeaningOfLifeAnswerer meaningOfLifeAnswerer = worker.get(); stateManager.switchTo(new ReadyToFindTheAnswer(meaningOfLifeAnswerer)); logger.info("prepared"); } catch(Exception ex){ logger.severe("failed to prepare"); stateManager.switchTo(new FailedToPrepareAnswerer()); } } } } } /** Creates new form StandardWay */ public StateMachineWay() { initComponents(); PrepareToAnswerMeaningOfLife prepareWorker = new PrepareToAnswerMeaningOfLife(); prepareWorker.addPropertyChangeListener(new PrepareToAnswerMeaningOfLifeListener()); prepareWorker.execute(); } //... //     JFrame  //... private javax.swing.JButton doButton; private javax.swing.JLabel labelStatus; private javax.swing.JProgressBar progressBar; } 


The main classes are GuiStateManager
 /** * State machine of swing gui * @author __nocach */ public class GuiStateManager { private GuiState currentState = new EmptyState(); /** * makes passed state current * @param newState not null new state */ public synchronized void switchTo(GuiState newState){ if (newState == null){ throw new NullPointerException(); } currentState.leaveState(); currentState = newState; currentState.enterState(); } public GuiState current(){ return currentState; } } 

A finite state machine class that is responsible for switching states and calling appropriate methods for entering and exiting states.

and state class GuiState
 public interface GuiState { /** * called when entering to this state */ public void enterState(); /** * called when leaving this state */ public void leaveState(); } 


When approaching through a finite state machine, we finally got the following inner classes:
PreparingAnswererState,
ReadyToFindTheAnswer,
FoundAnswerState,
FailedToPrepareAnswerer,
FailedToFoundAnswer

In the above example, instead of hard-coded logic, now the transition to the state and nothing more, for example:
 stateManager.switchTo(new ReadyToFindTheAnswer(meaningOfLifeAnswerer)); 

The entire logic of changing elements is concentrated in the state to which we switched (in this case, ReadyToFindTheAnswer, which makes the search button active and changes the state label and button labels). It is worth noting that now we can easily move the place to switch the state, or use the same switch in different places.

Someone may say that we just created a bunch of classes, but in this case we use 5 classes to increase the readability of the code, because Each class name is a state name.

Now, when we want to change the interface behavior at the moment when we have prepared MeaningOfLifeAnswerer (i.e. have made the answer search start button active), it will be enough for us to find the ReadyToFindTheAnswer state and add to it, for example, a pop-up dialog with a message that we are ready to respond to question.

Each state class can be freely refactored _locally_, without clogging the outer JFrame with unnecessary variables or methods. Classes of a status can be carr out in separate files for the subsequent testing.

Also, this approach _makes_ write the dialogue code more clearly, creating for each state a separate small class with a logical name. Although, of course, even here you can ruin everything with a huge number of anonymous classes.

The above finite state machine code is just an example of the realization of an idea. I’m not the first who decided to think of the user interface as a finite state machine, and therefore there is a ready-made framework, SwingStates , which has rather deeply thought through this idea.

Well and, obviously, this approach can be used in any desktop application in any language.

Sample code is available here.

Source: https://habr.com/ru/post/130256/


All Articles