User Machine Interaction Example

Latest Update: March 7, 2016.

Introduction

This example illustrates how Waza can be used to develop robust and reliable interactions between processes. Here, I take two parallel processes, namely a user and a machine. This example shows a programmatic approach of how to describe the functional behavior of the machine and how to ensure a correct user-machine-interaction. I take this approach from an object-oriented view towards a process-oriented view. This way I show how process-orientation complements with object-orientation using Waza. This is in particular useful for software architects, designers and programmers.

Background assumptions

  • Familiarity with Java or a similar object-oriented programming language.
  • Read the Java API tutorial (here) in order to understand the concurrency constructs.
  • Awareness that defect-free interaction between components improves the quality of functional requirements.

User-Machine Context

The machine offers services, such as start(), stop(), doThis() and doThat(). These represent functional requirements of the machine and they are described by interface of the machine. See the IMachine interface below. The user can request or call these services on the machine.

/**
 * Service interface for the machine.
 */
public interface IMachine {
  /**
   * Start the machine.
   */
  public void start();

  /**
   * Stop the machine.
   */
  public void stop();

  /**
   * Do this when machine is running.
   */
  public void doThis();

  /**
   * Do that when machine is running.
   */
  public void doThat();
}

Listing 1. IMachine interface.

The user and machine are real-life concurrent processes. I should describe the machine a process, but in this example I treat the machine as an object. See the Machine class below. It acts as a driver for the physical process.

/** 
 * This class commands the machine to perform actions. 
 */
public class Machine implements IMachine { 
  /** 
   * {@inheritDoc} 
   */ 
  @Override public void start() { 
    System.out.println("Machine is starting."); 
    sleep(1000); 
    System.out.println("Machine has started."); 
  } 

  /** 
   * {@inheritDoc} 
   */ 
  @Override public void stop() { 
    System.out.println("Machine is stopping."); 
    sleep(2000); 
    System.out.println("Machine has stopped."); 
  } 

  /** 
   * {@inheritDoc} 
   */ 
  @Override public void doThis() { 
    System.out.println("Machine does this."); 
    sleep(500); 
  } 

  /** 
   * {@inheritDoc} 
   */ 
  @Override public void doThat() { 
    System.out.println("Machine does that.");
    sleep(1500);
  } 

  /** 
   * Sleep for specified time (milliseconds) 
   * @param msec The sleep time. 
   */ 
  private void sleep(int msec) { 
    try { 
      Thread.sleep(msec); 
    } catch (InterruptedException e) { 
      e.printStackTrace(); 
    } 
  } 
}

Listing 2. Machine class.

Objects can invoke the methods on the Machine object in any order. These methods are not thread-safe. The synchronized keyword can be used to make them thread-safe, but still the order of invocations could be very wrong. For example, calling start() two times in a row is not desired. Adding checks to the Machine class that validates if the right sequences of calls are performed can be quite complicated. Also describing all use-cases to capture the happy-flow and corner-cases of the invocations is quite exhaustive and error-prone. You could use a state-machine or a state design pattern but this will create an obscure and architecture-less structure. We can do better than this by taking the concurrent way!

The user is a process that interacts with the machine using a channel. I use a call-channel in this example. I introduce a controller process which interacts with the user and which controls the machine. The controller acts as a wrapper for the Machine object. The user sees the machine by its interface, that is the channel-end. The channel connects the console with the controller of the machine. See the context diagram in Figure 1. The controller takes the user input and invokes the methods on the machine object (driver). The controller protects the machine from illegal user inputs.

Context diagram

Figure 1. Context diagram.

The functional requirements of this machine are simple. The discovery of the services is the first step to describe the functional requirements of this system. Designing the behavioral relationships (or rules) between these services is an important second step. For this purpose Use-Cases can be used, but instead, I design choices that capture the desired behavior. The user can start and stop the machine by sending respectively start() and stop() to the controller. Once the machine has started, the user can send doThis() and doThat(). The machine will only perform these services when the machine was started. Sending doThis(), doThat(), or stop() before the machine has started is wrong and therefore this is illegal. Also starting the machine twice is odd and thus illegal. The machine must be stopped before the next start() can be accepted. This behavior is depicted by the choice diagram in Figure 2.

Choice diagram

Figure 2. Choice diagram.

The choice diagram connects the interface of the controller process with the implementation of the machine. The choice diagram looks like a state-transition diagram, but formally they are not quite the same. The major difference is that the choice diagram is concurrent, whereas a state-transition diagram is not concurrent. I will not discuss the differences between choice diagrams and state-transition diagrams here. If you are familiar with state-machines, you may notice the differences at the end of this example. For your convenience, you can read the choice diagram as a state-transition diagram.

Design approach

I did not start with drawing the choice diagram. The choice diagram is the result of the method I describe here. The choice diagram is established by designing and composing the individual choices. Designing a choice is a process of discovering and composing options. This design process is two ways. You can discover the options that belong to a choice. Or you discover options by which you discover a new choice to which it belongs to. Options are derived from the interface of the process and they map the services on the methods of the machine. The design process takes one choice at the time. I put the options of a choice together in a list which allows me to see what the relationships are between them. This way I may find overlap, redundancy or gaps in the list or I may find reasons to add priorities to the options. If needed, I can easily move options between the choices. Important is that the list of options should deal with all the services from the interface of the process. Don’t skip a service. Whether or not a service is important to the choice, it is helpful to reason about them one-by-one in the context of the choice and the interface. This allows me to see things I may have overlooked and the functionality of the machine becomes clear. After creating the first choice, other choices will be discovered. This design process repeats until I’ve discovered all choices (and all options) and the result is a working choice-machine that fulfills the happy-flow and corner-case scenarios (Use-Cases). This is a systematic and inuitive design method. This was done quickly, because there are only two choices in this example.

When the machine is powered on, the controller starts in the Idle choice. See the black dot and the arrow to the Idle circle in the figure. You may call it a state, but I prefer to call it a choice. This example implements choices instead of states. The controller process waits for input and on receiving input it performs a choice (= takes a decision). The other choice is the Operation choice. It is a choice that is made during the operation of the machine.

The arrows that are leaving the choice are the options of the choice. The arrows (options) are labeled. Here, user is a channel-end of a call-channel and it represents the interface of the controller. The notation user? represents the guarded input from the user. The notation user?start() ⟶ start() means that if service start() is called (or requested) on the channel then it is candidate to be selected. If it is selected by the choice then it immediately performs the start() method as specified behind the arrow symbol. The option terminates when start() terminates. The arrow symbol means a delegation. For example, user?start() ⟶ illegal means that if the user calls service start() then it performs an illegal process, of course, only when the option is selected. The illegal process throws an exception. The option is the transition to the next choice. Illegal does not terminate and therefore it has no next choice. This is depicted by the red dot. The choice diagram depicts a true event-loop of the controller.

In order to keep the example simple, it is restricted to one user and one machine. Adding multiple users is quite simple. The channel can be shared by multiple users. This example is already thread-safe, robust and secure for multiple users.

Before I develop the controller, I generate the call-channel from the IMachine interface. I use the WazaChannelGenerator tool to generate the channel class and a few helper classes. You need to download WazaCG.jar from www.wazalogic.com. The classes are generated with command:

java -jar ./WazaCG.jar -l java -n Machine -i IMachine

The language is ‘java’, the channel prefix is ‘Machine’ and the interface is ‘IMachine’. ‘ The file ‘IMachine.java’ is used as input. The tool creates the following files:

  • MachineChannel.java
  • MachineBufferedChannel.java
  • MachineUnbufferedChannel.java
  • IMachineAccept.java
  • IMachineAcceptGuard.java
  • IMachineAcceptOption.java

These files are copied to the project.

I use these classes to develop the controller. The choice diagram has been translated straightforward to Java using the Waza API for Java. The controller is described in the MachineController class.

import waza.Process;
import waza.*;

/**
 * This class accepts legal interactions (service-requests) and 
 * rejects illegal interactions from the user.
 * Legal interactions are passed on to the machine.
 */
public class MachineController implements Process {

  private IMachineAccept _user;
  private Machine _machine;
  private boolean _stop = false;
  private Choice _idleChoice;
  private Choice _operatingChoice;

  public MachineController(IMachineAccept user) {
    //
    // Connect to user via channel.
    //
    _user = user;
    //
    // Create the machine component.
    //
    _machine = new Machine();
    //
    // Allow start and other services are illegal.
    //
    // start  -> start()
    // doThis -> illegal
    // doThat -> illegal
    // stop   -> illegal
    //
    _idleChoice = new Choice(
      // Allow start() as an option.
      new IMachineAcceptOption(_user, IMachineAccept.Services.start, 
        new Process() {
          @Override
          public void run() throws Exception {
            // Now, accept and perform start().
            _user.accept(IMachineAccept.Services.start, _machine);
          }
        }
      ),
      // Disallow doThis(), doThat() and stop() as an option.
      new IMachineAcceptOption(_user, 
        IMachineAccept.values( 
          IMachineAccept.Services.dothis, 
          IMachineAccept.Services.dothat,
          IMachineAccept.Services.stop
        ),
        // Do not accept these calls, but instead throw an exception. 
        new Illegal("doThis, doThat and stop are illegal!")
      )
    );
    //
    // After start
    //   - allow doThis, doThat and stop and second start is illegal
    //   - after stop behave as _choice1.
    //
    // start  -> illegal
    // doThis -> doThis()
    // doThat -> doThat()
    // stop   -> stop()
    //
    _operatingChoice = new Choice(
      // Disallow start().
      new IMachineAcceptOption(_user, IMachineAccept.Services.start,
        // Do not accept this call, but instead throw an exception. 
        new Illegal("start is illegal!")
      ),
      // Allow doThis() and doThat().
      new IMachineAcceptOption(_user, 
        new IMachineAccept.Services[] { 
          IMachineAccept.Services.dothis, 
          IMachineAccept.Services.dothat
        },
        new Process() {
          @Override
          public void run() throws Exception {
            // Now, accept and perform doThis() and doThat().
            _user.accept(new IMachineAccept.Services[] {
                IMachineAccept.Services.dothis,
                IMachineAccept.Services.dothat
              },
              _machine
            );							
          }
        }
      ),
      // Allow stop() and break loop (_stop = true) as an option.
      new IMachineAcceptOption(_user, IMachineAccept.Services.stop, 
        new Process() {
          @Override
          public void run() throws Exception {
            // Now, accept and perform stop().
            _user.accept(IMachineAccept.Services.stop, _machine);
            _stop = true;
          }
        }
      )
    );
  }

  /**
   * Perform the choice-machine.
   */
  public void run() throws Exception {
    //
    // Machine is powered on and ready.
    //
    while (true) {
      _idleChoice.run();
      _stop = false;
      while (!_stop) {
        _operatingChoice.run();
      }
    }
  }
}

Listing 3. MachineController class.

Listing 3 enforces “unforgiven” interactions between user and machine. Any illegal interaction will be punished with an illegal process [lines 52 and 69]. The illegal process throws an exception, see Listing 4.

import waza.Process;

public class Illegal implements Process {

  private String _message;
	
  public Illegal(String message) {
    _message = message;
  }
	
  @Override
  public void run() throws Exception {
    throw new IllegalAccessError(_message);		
  }
}

Listing 4. Illegal process.

You can make the interactions “forgiven” so that user and machine are allowed to continue after an illegal interaction. For this you need to replacing the illegal processes by accepting the input (user.accept(..);) with a delegate that does nothing. The acceptance of the input “forgives” the illegal interaction. Hereafter, for example, a beep sound lets the user know that the call was wrong. In Listing 3 you could replace line 69 with Listing 5. This should also be done for line 52. Listing 5 shows an anonymous process, which is instantiated on every illegal interaction. To avoid garbage collection, a pre-instantiated class should be used instead of the anonymous class.

_user.accept(IMachineAccept.values(IMachineAccept.Services.start), 
  new IMachine() {
    @Override public void start() { }
    @Override public void stop() { }
    @Override public void doThis() { } 
    @Override public void doThat() { }
  }
);
Beep();

Listing 5. Forgiven modification.

The user is described by the User class. See Listing 6. The user process is a client process, which can be very well a sofware component of the system. The User class implements a ‘good’ scenario and a ‘bad’ scenario. All possible scenarios could be added this way. This is a good exercise. This user process can be used as a tester or simulator for testing the controller. The ‘bad’ scenario is switch(0) and the ‘good’ scenario is switch(1).

import waza.Process;

public class User implements Process {
	
  private IMachine _machine;
	
  public User(IMachine machine) {
    _machine = machine;
  }

  @Override
  public void run() throws Exception {
    switch (0) { // Choose 0 or 1 here.
      case 0:
        badScenario();
        break;
      case 1:
        goodScenario();
        break;
    }
  }

  public void badScenario() {
    _machine.start();
    _machine.start();
    _machine.doThis();
    _machine.doThat();
    _machine.stop();
  }

  public void goodScenario() {
    _machine.start();
    _machine.doThis();
    _machine.doThat();
    _machine.stop();
  }
}

Listing 6. User class.

The Program class describes a parallel composition of the user process and the controller process, being connected by a call-channel. See Listing 7.

import waza.Process;
import waza.*;

public class Program implements Process {

  private Parallel _parallel;

  public static void main(String[] args) throws Exception {
    Program program = new Program();
    program.run();
  }

  public Program() {
    MachineChannel channel = new MachineChannel();
		
    _parallel = new Parallel(
      new User(channel), 
      new MachineController(channel)
    );
  }

  @Override
  public void run() throws Exception {
    _parallel.run();
  }
}

Listing 7. Program class.

The main() method instantiates the Program class and runs the program.

Output

‘good’ scenario (switch(1))

Machine is starting.
Machine has started.
Machine does this.
Machine does that.
Machine is stopping.
Machine has stopped.

‘bad’ scenario (switch(0))

Machine is starting.
Machine has started.Exception in thread "Thread-0" 
java.lang.IllegalAccessError: start is illegal!
	at Illegal.run(Illegal.java:13)
	at waza.Choice.run(Choice.java:338)
	at MachineController.run(MachineController.java:117)
	at waza.LocalThread.run(LocalThread.java:90)

Exercise

This example describes an infinite process and it does not terminate unless power goes down. For most advanced systems this may not be a proper way of shutting down the system.

Find out how the user can shut down the machine at any time. Assume that after calling shut down, the machine will power off by itself. The machine and controller must terminate gracefully. Investigate the suitable functional requirements for graceful shut down.

Hints:

  • Add a shutdown() service to the machine interface and implement it with a println().
  • The outer while(true) in Controller class should become while(!_shutdown).
  • Redesign the choice-machine and regenerate the channel.

Observe the scalability of this programmatic approach.