Wednesday, March 31, 2010

Passive Views and View Interfaces

User interfaces are inherently difficult to test since logic buried in the GUI can't really be tested without bringing up the entire system, and this is not really practical for unit (or even integration) tests. It's common to say "Extract the logic outside the GUI" but in practice, I don't see it done as much as I think it should be.
First, let's distinguish between a passive view and and active view. An active view tends to register itself with the model, listen for changes, and update itself. The following code would be typical in an active view:

public class MyView {

public void init() {
model.addPropertyChangeListener(this);
}

public void propertyChanged(PropertyChangeEvent evt) {
String propName = evt.getPropertyName();
if ( propName.equals("prop1") ) {
updateViewWithProp1Change();
}
else if ( propName.equals("prop2")) {
updateViewWithProp2Change();
}
// ... etc ...
}
}

Typically the updateViewWithPropXXXChange methods would read the new value of the property and determine what to display on the view. To give a concrete example, imagine a view that updates a temperature display.

public void updateTemperatureDisplay(double newTempFarenheit) {
if ( mode.equals(FARENHEIT) ) {
tempLabel.setValue(Double.toString(newTempFarenheit));
}
else if ( mode.equals(CELSIUS) ) {
tempLabel.setValue(Double.toString(toCelsius(newTempFarenheit)));
}
}

In this example, the view does the conversion to Celsius. This logic is now difficult to test without showing the temperature display. This view is active. It's fast to write because the logic is contained in here, but difficult to test.
A passive view on the other hand can be thought of as a "dumb" view. It displays what it is told.

public interface MyPassiveView {
void setTemperatureText(String value);
}

public class MyPassiveVewImpl implements MyPassiveView {
public void setTemperatureText(String value) {
tempLabel.setText(value);
}
}
public class MyController {
private MyPassiveView view;

public void updateTemperatureDisplay(double newTempFarenheit) {
String temperatureText = "";
if ( mode.equals(FARENHEIT) ) {
temperatureText = Double.toString(newTempFarenheit);
}
else if ( mode.equals(CELSIUS) ) {
temperatureText = Double.toString(toCelsius(newTempFarenheit)));
}
else { // handle error }
view.setTemperatureText(temperatureText);
}

Now all the view does is set the label text. This is more work but the controller can be independently tested from the view. Notice in this example also the view implements an interface. View interfaces greatly simplify testability and make many of these patterns possible. I think this is frequently overlooked by developers and the view ends up being a concrete class.

There are various patterns that fall along this line, notably model-view-controller, model-view-presenter, and supervising controller all with varying degrees of passiveness. These can all be found on Martin Fowler's website (http://martinfowler.com/), and many other results can be found by a simple search.

4 comments:

  1. Good article...a GUI app I recently wrote uses a passive view and it works out pretty nicely!

    ReplyDelete
  2. Thanks, appreciate the readership.

    ReplyDelete
  3. I noticed you don't instantiate the view, which holds the GUI components. Should the Controller instantiate the View and should anyone who wants a View to be displayed call the Controller?

    ReplyDelete
  4. I would wire in the view through a dependency injection framework like Spring or Guice. This also makes it easier to wire in mock views for testing.

    ReplyDelete