Monday 22 March 2010

Design patterns in Processing applications

This is a follow on to my previous two posts (here and here). In those post I constructed a simple circuit for measuring the ambient temperature and, with the help of an Arduino board, sending the temperature data down a USB cable. I also wrote a program in Processing to read that data and display it on my PC's screen. I program also stores the temperature data in a simple text file in the hopes that I'll find something interesting to do with it.
As I mentioned at the end of the last post, I was unhappy with the quality of the Processing code. It was somewhat spaghetti like, with three distinct functions all being handled by the same bundle of code. So, in this post, I'll discuss how I teased the code apart and in particularly how I used two design patterns.

Design Patterns are easy

Processing as a language makes it really easy to write quick and dirty code, as we've seen, but this doesn't mean you have to. To tidy my code up, I used two design pattterns; singleton and observer. These are two very easy design patterns to use in Java; the observer pattern is built in!

Singleton

The singleton pattern is simple; A class that is a singleton ensures that there is only on instance of that class and also provides a method to get a reference to the single instance. The Singleton pattern is somewhat notorious in Java but it needn't be as it only requires a couple of lines of code to implement. I've used Bill Pugh's solution which is the neatest.
First of all, in the Processing environment, click that right arrow in the tabbed section and select 'new tab' (or Ctl-Shift N). Give the tab the name 'TemperatureObservable' which is also the name of the class. The follow code implements the singleton pattern.
static class TemperatureObservable {
  private TemperatureObservable() {
    super();
  }
  
  private static class SingletonHolder { 
     private static final TemperatureObservable INSTANCE = new TemperatureObservable();
  }
  
  public static TemperatureObservable getInstance() {
    return SingletonHolder.INSTANCE;
  }
}
There are four things to notice.
  • The class is declared as static. Processing complains about the getInstance() method if it isn't.
  • The class's constructor is private. This ensures that only this class can create an instance of it.
  • There is a private static inner class, SingletonHolder, that creates an instance of the singleton class. The inner class is used so that the singleton instance is only constructed when it is needed. This is called lazy initialization and ensures that the singleton isn't constructed if it isn't going to be used.
  • The static method getInstance() returns the singleton; this is the only way that code outside of this class can gain access to the singleton.

Observer

This pattern is built into Java. It consists of two parts. Firstly, there is an object that we call the observable; this is an object that is expected to change as certain points. Other objects, called observers, register their interest in the observable; when the observable object's state changes, it notifies it's observers.
In this application, the TemperatureObservable objects, our singleton, is also the observable object. Java provides a Class called Observable and any classes that extend this base class automatically get the Observer pattern functionality. Of course, we need to pull in the code from TempDisplay that this object taking over; the TemperatureObservable's job will be to read the temperature data from the serial connection and notify it's observers that a new reading is available.
static class TemperatureObservable extends Observable {
  
  private static PApplet pApplet = null;
  private Serial port = null;
  private String temperature = "0";
  
  private TemperatureObservable() {
    super();
  }
  
  public static void setParent(PApplet pApplet) {
    TemperatureObservable.pApplet = pApplet;
  }
  
  public void addObserver(Observer o) {
    super.addObserver(o);
    if (this.countObservers() == 1) {
      this.setup();
    }
  }
  
  public void deleteObserver(Observer o) {
    super.deleteObserver(o);
    if (this.countObservers() == 0) {
      this.tearDown();
    }
  }
  
  public void deleteObservers() {
    super.deleteObservers();
    this.tearDown();
  }
  
  public void setup() {
    if (this.port == null) {
      String portName = Serial.list()[0];
      this.port = new Serial(TemperatureObservable.pApplet, portName, 9600);
      this.port.write("get temp");
      this.tick();
    }
  }
  
  public void tearDown() {
    if (this.port != null) {
      this.port.stop();
      this.port = null;
    }
  }
  
  public void tick() {
    if (this.port.available()>0) {
      TemperatureObservable.pApplet.delay(100);
      this.temperature = port.readString().trim();
      this.setChanged();
      this.notifyObservers(this.temperature);
    }
  }
  
  public String getTemperature() {
    return this.temperature;
  }
  
   private static class SingletonHolder { 
     private static final TemperatureObservable INSTANCE = new TemperatureObservable();
   }
  
  public static TemperatureObservable getInstance() {
    return SingletonHolder.INSTANCE;
  }
  
}
The new code allows a PApplet object to be registered with the TemperatureObservable class; this is only needed because the Serial object constructor expects a PApplet object as it's first argument. Further on, we also use the PApplet's delay() method. PApplet, by the way, is the superclass of the class you enter into the Processing sketch's first tab. How PApplet child class is called TempDisplay.
addObserver(), deleteObserver() and deleteObservers() are all methods in the Observable class that we override so that we can set up and tear down the serial connection on demand. Lastly, the tick() method is needed to tell the TemperatureObservable object to check the serial connection for a new temperature; remember that in the previous version, this check happened in the PApplet's draw() method.
In order to use the Observable singleton, we need an Observer. Java provides an Observer interface which has just one abstract method that we need to implement, update(). Objects that implement this interface can be passed to the Observable object's addObserver(). Our first observer is quite a simple one and is charged with maintaining the main TempDisplay object's copy of the current temperature (the one that it renders each time draw() is called).
static class TempDisplayObserver implements Observer {
  
  private TempDisplay display = null;
  
  public TempDisplayObserver(TempDisplay display) {
    this.display = display;
  }
  
  public void update(Observable o, Object arg) {
    this.display.setTemperature((String)arg);
  }
}
For this to work, the TempDisplay class needs a new method, setTemperature(). The TempDisplay code is as follows. Notice that the is much shorter than the version at the end last post, though I've also removed the code that saves the data to a file and we've yet to replace this.
import processing.serial.*;

String temperature = "0";
PFont font;


TemperatureObservable tempObs = null;

void setup() {
  TemperatureObservable.setParent(this);
  this.tempObs = TemperatureObservable.getInstance();
  this.tempObs.addObserver(new TempDisplayObserver(this));

  font = loadFont("Ziggurat-HTF-Black-32.vlw");
  textFont(font);
  textAlign(CENTER);
  size(200, 140);
  background(0);
  fill(0);
  smooth();
}

void draw() {
  background(255);
  this.tempObs.tick();
  text(temperature, width/2, height/2);
}

void setTemperature(String arg) {
  temperature = arg;
}
As you can see, at the start of the setup() method, we register the TempDisplay object with the TemperatureObservable class via it's setParent() method. we then get a reference to the singleton and add a new instance of TempDisplayObserver. Also notice in the draw() method, we call the singleton's tick() method.

File output again.

The second observer object is tasked with writing the data to the text file. Most of this code is simply copied from the previous version of TempDisplay.
static class TempFileObserver implements Observer {
  
  private String dataFolder = null;
  private PrintWriter output = null;
  private TimeZone tz = null;
  private DateFormat stamp = null;
  private Date lastSaveDate = null;
  
  public TempFileObserver(String dataFolder) {
    this.dataFolder = dataFolder;
    tz = TimeZone.getDefault();
    stamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
    stamp.setTimeZone(tz);
    lastSaveDate = new Date();
    String fileName = getFileName(lastSaveDate);
    output = getOutput(dataFolder, fileName);
  }
  
  public void update(Observable o, Object arg) {
    this.writeData((String)arg);
  }
  
  private void writeData(String temp) {
    if (this.output != null) {
      Date currentSaveDate = new Date();
      String message = stamp.format(currentSaveDate) + " " + temp;
      this.output.println(message);
      this.output.flush();
      if (this.isNextDay(this.lastSaveDate, currentSaveDate)) {
        this.output.flush();
        this.output.close();
        String fileName = this.getFileName(currentSaveDate);
        this.output = this.getOutput(dataFolder, fileName);
      }
      this.lastSaveDate = currentSaveDate;
    }
  }
  
  private String getFileName(Date date) {
      DateFormat dfm = new SimpleDateFormat("yyyyMMdd");
      dfm.setTimeZone(tz);
      return dfm.format(date) + ".data";
  }
  
  private PrintWriter getOutput(String folder, String file) {
    PrintWriter pw = null;
    File fileHandle = new File(folder, file);
    try {
      pw = new PrintWriter(new FileOutputStream(fileHandle, true));
    } catch (FileNotFoundException fnfe) {}
    return pw;
  }
  
  private boolean isNextDay(Date earlier, Date later) {
    boolean isNextDay = false;
    Calendar cEarlier = Calendar.getInstance();
    Calendar cLater = Calendar.getInstance();
    cEarlier.setTime(earlier);
    cLater.setTime(later);
    if (cLater.after(cEarlier)) {
      boolean dayIsAfter = cLater.get(Calendar.DAY_OF_YEAR) > cEarlier.get(Calendar.DAY_OF_YEAR);
      boolean yearIsAfter = cLater.get(Calendar.YEAR) > cEarlier.get(Calendar.YEAR);
      isNextDay = dayIsAfter || yearIsAfter;
    }
    return isNextDay;
  }   
}
To make use of this class, we just need to pass an instance of it to the singleton's addObserver() method. The TempDisplay's setup() method becomes:
void setup() {
  TemperatureObservable.setParent(this);
  this.tempObs = TemperatureObservable.getInstance();
  
  String dataFolder = selectFolder("Choose the folder where you want the temperature data to be recorded");
  if (dataFolder != null) {
    this.tempObs.addObserver(new TempFileObserver(dataFolder));
  }
  
  this.tempObs.addObserver(new TempDisplayObserver(this));
  //...
}

One last thing.

There is just one last thing that was bugging me; just occasionally, more than one temperature reading is available in the serial connection and this would cause the display to show more than one reading. This is easily fixed. We know that each reading should contain no white space and that individual reading are separated by a line break. Therefore, I changed the TemperatureObservable's tick method to split the value that it reads from the serial connection using a regular expression. The regular expression that I used is '[\\s]+'. This simply means 'one or more characters of white space.' The tick() method changes to the following:
public void tick() {
    if (this.port.available()>0) {
      TemperatureObservable.pApplet.delay(100);
      String outString = port.readString().trim();
      String[] temps = outString.split("[\\s]+");
      for (int i = 0; i < temps.length; i++) {
        this.temperature = temps[i];
        this.setChanged();
        this.notifyObservers(this.temperature);
      }
    }
  }

Conclusion

I was really pleased with the work I did here. Implementing these two design patterns was straight forward. It is just great that Processing allows me to simple programmes quickly while being able to leverage my Java knowledge. Maybe next time I'll do something more visually exciting.



No comments:

Post a Comment