Recitation 7: Design Patterns

Thursday, March 18, 2004

Contents:

Announcements

Lab 2 is today in W20!

PS6 is due on Tuesday, April 6th:

PS6 checkpoint: CANCELED!

About PS6

PS6 overview

Overview (summarized from the PS6 instructions). In this problem set you will assemble the modules you developed in problem sets 3 through 5, as well as some additional modules, to build Traffick, a traffic simulator for Boston and Cambridge. The main components of the assignment are:

Review the suggested schedule. PS6 is even longer than the other problem sets.

Be confident, though. You already implemented many pieces that you need and the staff has provided some utility classes to help you with ps6.

PS6 checkpoint

Checkpoint: Deadline CANCELED.

The main challenge is to get started. You must show a TA or LA that your code can load the smallest of the databases of road segments and intersections (the tiny database).

The second challenge is to read the whole assignment and think about it:

ps6 has some performance requirements. If you find that your implementation performs badly, don't start optimizing all over the place. If you do so, you're unlikely to improve the performance dramatically, and you'll probably introduce bugs into your code. Instead:

PS6 provided code

ps6.reader

For the provided code, the place to start is the ps6.reader package. This package contains utilities for reading Traffick data out of XML files as well as utilities for reading RoadSegment data out of the Tiger database compiled by the US Census Bureau.

In the ps6.reader package, there is a ps6.reder.RoadSystemReader class. This class reads information about roads and intersections. This class will let you construct the topology of roads to simulate. When you create an instance of that class, you must specify a directory location. The class will look for three files in that directory:

Once created, the RoadSystemReader provides two methods that allow you to iterate through all the lanes and intersections that were read. Below is the object model of all objects reachable from the RoadSystemReader:


After creating the topology, you need to create traffic sources that will inject cars into the simulation. In the reader package, class TrafficSourceReader reads information about traffic sources. In its constructor, TrafficSourceReader takes as parameter a file with the xml description of all the traffic sources. Once it read the file, the TrafficSourceReader allows a client to iterate through a list of TrafficSourceDescriptors.

Finally, class RouteGenerator helps you generate Routes for TrafficSources manually.

ps6.graphics

In the graphics package, class TraffickPalette is designed to help you develop the GUI for the Traffick simulator. It contains methods such as drawCar and drawLane.

Class TraffickSample, in the top level package, shows examples of using the TraffickPalette. Take a look at these classes after doing Lab 2.

ps6

At the top level, class Traffick is the main entry point for the traffick simulator. It contains the programmatic interface and the main method starts the traffick GUI. The class already has a skeleton ready for you to fill out!

Finally, class TimeProfiler is a utility class. It can be used to instrument your code and understand where your system spends most of its time. To use TimeProfiler, you should call the start and stop methods at the entry and exit to some code fragment. You can then periodically call stats to print the statistics gathered so far by each timer instance.

Design Patterns

Note: material and examples in this section come from the GOF book, from lecture notes, and from Bloch.

A design pattern is:

The third point above is worth stressing. You may feel that some of the design patterns seem obvious or simple and also that you have used design patterns without realizing you were doing so. That is the whole point! Design patterns are nice when they are simple and most people can think of them on their own if needed. It is still important to study design patterns so that you have a common vocabulary that you can use to quickly convey your ideas to other engineers when discussing software design.

As a nice analogy, think of Economics. The best economists in the world disagree with each other on lots of points; however, it is still important for economics students to learn about all the different viewpoints. That way, when they debate over Keynsian multipliers, supply-side economics, monetary policy stimulated demand, and all the other models that exist, everyone speaks the same language and can successfully debate with each other about their views rather than having to restate each model every time.

When to use design patterns

You shouldn't go through the Gang Of Four book and try to cram every single design pattern possible into your PS6 or final project in hopes of winning the best design prize. It won't work!

Design patterns are not free! A design with a pattern is not necessarily better than a design without a pattern. Usually a pattern can solve one nagging problem but sacrifices some other convenience. A static factory method allows interning, but then makes subclassing more difficult! A singleton may seem nice and cool, but what if you really could use more than one instance of the object? A visitor may allow double-dispatch, but sometimes operations are context-sensitive and makes writing each visitor much more difficult.

Design patterns should only be introduced into your design or implementation after you have noticed a problem (hence the advantage of prototyping). After you have determined that there is a problem, and investigated the source of the problem, looking at a design patterns reference will often help you to solve your problem without having to reinvent the wheel.

We will cover the following design patterns today:

Singleton

The Singleton pattern ensures that a class has at most one instance. The Singleton provides a global point of access to the class. There is at most one instance rather than exactly one because the instance can be created lazily.

An IDE, for example, may want to ensure that at most one instance of the debugger exists at any time.

public class Debugger {
  private static final Debugger INSTANCE = new Debugger();
  private Debugger() { ... }
  public static Debugger getInstance() {
    return INSTANCE;
  }
}
Additionally, many development sessions do not involve the use of the debugger, so the instance doesn't have to be created until the first time it is used:
public class Debugger {
  private static Debugger INSTANCE;
  private Debugger() { ... }
  public static Debugger getInstance() {
    if ( INSTANCE == null )
      INSTANCE = new Debugger();
    return INSTANCE;
  }
}

Some other advantages of the singleton pattern are:

Iterator

Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. An iterator exposes a generic interface.

Key idea: decouple the functionality of the collection from the traversal functionality.

public interface Aggregate {
  public Iterator iterator();
}

public interface Iterator {
  public boolean hasNext();
  public Object next();
  public void remove();
}

Below is the resulting MDD:


Example: In Java, all collections can return an Iterator that allows a client to iterate through the elements of the collection without knowing the concrete collection class. Some collections support different kinds of iterators: unidirectional, bi-directional.

The Iterator pattern is generally applicable to all kinds of data structures.

Factory Method

A factory method is a method that manufactures objects of a particular type. The intent of this pattern is to define an interface for creating an object which lets subclasses decide which classes to instantiate.

Consider a class Maze that represents the maze for your favorite video game.

class Maze {
    Maze() { ... }    
    void addRoom(Room r) { ... }
    Room roomNb(int nb) { ... }
    ...
}
Now let's write a class MazeGame that creates a maze. One straightforward way to create a maze is with a series of operations that add components to a maze and then interconnects them. For example, let's create a maze consisting of two rooms with a door between them:
class MazeGame {
    ...
    Maze createMaze() {
      Maze aMaze = new Maze();
      Room r1 = new Room(1);
      Room r2 = new Room(2);
      Door theDoor = new Door(r1,r2);
  
      aMaze.addRoom(r1);
      aMaze.addRoom(r2);

      r1.setSide(NORTH, new Wall());
      r1.setSide(EAST, theDoor);
      r1.setSide(SOUTH, new Wall());
      r1.setSide(WEST, new Wall());

      r2.setSide(NORTH, new Wall());
      r2.setSide(EAST, new Wall());
      r2.setSide(SOUTH, new Wall());
      r2.setSide(WEST, theDoor);

      return aMaze;
   }
   ...
}

Q: What is the problem with this method?

A: It hard-codes the classes of maze, rooms, doors, and walls. What if we wanted to create a BombedMazeGame or an EnchantedMazeGame?

We'll introduce factory methods to let subclasses choose these components. First we'll define factory methods in MazeGame for creating the maze, room, wall, and door objects:

class MazeGame {
  Maze createMaze() { ... }

  // Factory methods

  Maze makeMaze() { 
    return new Maze();
  }

  Room makeRoom(int n) {
    return new Room(n);
  }

  Wall makeWall() {
    return new Wall();
  }

  Door makeDoor(Room r1, Room r2) {
    return new Door(r1,r2);
  }
}
Each factory method returns a maze component of a given type. MazeGame provides default implementations that return the simplest kinds of maze, rooms, walls, and doors. Now we can rewrite CreateMaze to use these factory methods:
class MazeGame {
    ...
    Maze createMaze() {
      Maze aMaze = makeMaze();
      Room r1 = makeRoom(1);
      Room r2 = makeRoom(2);
      Door theDoor = makeDoor(r1,r2);
  
      aMaze.addRoom(r1);
      aMaze.addRoom(r2);

      r1.setSide(NORTH, makeWall());
      r1.setSide(EAST, theDoor);
      r1.setSide(SOUTH, makeWall());
      r1.setSide(WEST, makeWall());

      r2.setSide(NORTH, makeWall());
      r2.setSide(EAST, makeWall());
      r2.setSide(SOUTH, makeWall());
      r2.setSide(WEST, theDoor);

      return aMaze;
   }
   ...
}
To create the BombedMazeGame and EnchantedMazeGame, all we have to do is:
class BombedMazeGame extends MazedGame {
    BombedMazeGame() {}

    Room makeRoom(int n) {
      return new RoomWithABomb(n);
    }

    Wall makeWall() {
      return new BombedWall();
    }
}

class EnchantedMazeGame extends MazedGame {

    EnchantedMazeGame() {}

    Room makeRoom(int n) {
      return new EnchantedRoom(n,CastSpell());
    }

    Door makeDoor(Room r1, Room r2) {
      return new DoorNeedingSpell(r1,r2);
    }

    private Spell castSpell() { ... }

}

Below is the partial MDD:


Factory methods have several additional advantages compared to constructors:

When there are many objects to construct, we might want to group them all into a single factory object. Factory objects also allow sibling classes to share the same factory methods.

class MazeFactory {

    Maze makeMaze() {  return new Maze(); }
    Room makeRoom(int n) { return new Room(n); }
    Wall makeWall() { return new Wall(); }
    Door makeDoor(Room r1, Room r2) {
      return new Door(r1,r2);
    }
}

class BombedMazeFactory extends MazedFactory {
    Room makeRoom(int n) { return new RoomWithABomb(n); }
    Wall makeWall() { return new BombedWall(); }
}

class EnchantedMazeFactory extends MazedFactory {
    Room makeRoom(int n) { return new EnchantedRoom(n,CastSpell()); }
    Door makeDoor(Room r1, Room r2) {
      return new DoorNeedingSpell(r1,r2);
    }
    private Spell castSpell() { ... }
}
The MazeGame class can then be instantiated with the appropriate type of factory or can take a factory as parameter to the createMaze method.
class MazeGame {

    Maze createMaze(MazeFactory mfactory) {
      Maze aMaze = mfactory.makeMaze();
      Room r1 = mfactory.makeRoom(1);
      Room r2 = mfactory.makeRoom(2);
      Door theDoor = mfactory.makeDoor(r1,r2);
  
      aMaze.addRoom(r1);
      aMaze.addRoom(r2);

      r1.setSide(NORTH, mfactory.makeWall());
      r1.setSide(EAST, theDoor);
      r1.setSide(SOUTH, mfactory.makeWall());
      r1.setSide(WEST, mfactory.makeWall());

      r2.setSide(NORTH, mfactory.makeWall());
      r2.setSide(EAST, mfactory.makeWall());
      r2.setSide(SOUTH, mfactory.makeWall());
      r2.setSide(WEST, mfactory.theDoor);

      return aMaze;
   }

}

This pattern is also called Abstract Factory.

Observer

The Observer pattern allows one or more objects (the observers) to watch another (the subject). The Observer pattern allows the subject and observer to form a publish-subscribe relationship. Through the Observer pattern, observers can register to receive events from the subject. When the subject needs to inform its observers of an event, it simply sends the event to each observer.

Consider a spreadsheet system that includes embedded graphs that view the data contained in the cells. In this system, as a user changes the data in the cells, the graphs should be notified of the changes and they should update their diagrams appropriately. The subject is the data model and the observers are the embedded graphs.

The benefit of the Observer pattern is that it decouples the observer from the subject. The subject doesn't need to know anything special about its observers. Instead, the subject simply allows observers to subscribe. When the subject generates an event, it simply passes it to each of its observers.

public interface Subject {
      public void addObserver(Observer o);
      public void removeObserver(Observer o);
}

public interface Observer {
      public void receiveUpdate(Subject s);
}

In the code above, the Subject interface defines the methods that a Subject must implement in order for Observers to add and remove themselves from the Subject. The Observer interface (above) lists the methods that an Observer must implement so that a Subject can send an update notification to the Observer.

So how would we go about coding the spread sheet example?

public class SpreadSheetData implements Subject {
    ...
    public void addObserver( Observer o ) { observers.add(o); }
    public void removeObserver( Observer o ) { observers.remove(o); }

    private Set observers = new HashSet();
    private void notify() {
      for ( Iterator i = observers.iterator(); i.hasNext(); ) {
        Observer o = (Observer)i.next();
        o.receiveUpdate();
      }
    }
}

public class SpreadSheetGraph implements Observer {
    ...
    public void receiveUpdate(Subject s) {
      ...      
    }
}

Even better, we could also have a GenericSubject class take care of the notifications:

public class GenericSubject implements Subject {

    public void addObserver( Observer o ) { observers.add(o); }
    public void removeObserver( Observer o ) { observers.remove(o); }

    private Set observers = new HashSet();

    protected void notify() {
      for ( Iterator i = observers.iterator(); i.hasNext(); ) {
        Observer o = (Observer)i.next();
        o.update();
      }
    }
    ...
}

public class Subject extends GenericSubject {
  ...
}

Below is the resulting MDD:


Java provides support for the Observer pattern:

public Interface Observer {
    public void update(Observable o, Object arg);
}

public class Observable {
    public void addObserver(Observer o)  { ... }
    public void deleteObserver(Observer o)  { ... }
    public void  notifyObservers(Observable o) { ... } 
    ...
}

Adapter (a kind of Wrapper)

The purpose of the Adapter pattern is to convert the interface of a class into another interface that clients expect. This can be necessary if you want to reuse code, but that code does not satisfy a required interface. For example, suppose you have written code that uses objects that satisfy the specification for Rectangle:
public interface Rectangle {
  /** @effects grow or shrink this by the given factor **/
  public void scale(float factor);

  // other operations
  public float area();
  public float getWidth();
  public float getHeight();
  ...
}
Suppose that you have an already implemented class NonScaleableRectangle, which does not have the scale method, but has setHeight and setWidth methods, and has all of the other methods in Rectangle. If we want to use NonScaleableRectangle to satisfy the Rectangle interface, we can write an adapter. We can do this using either subclassing or delegation. The solution using subclassing would look like:
public class AdaptingRectangle extends NonScaleableRectangle implements Rectangle {
  public void scale(float factor) {
    setWidth(factor*getWidth());
    setHeight(factor*getHeight());
  }
}
A solution using delegation would look like:
public class AdaptingRectangle implements Rectangle {
  private NonScaleableRectangle r;

  public AdaptingRectangle(width w, height h) {
    r = new NonScaleableRectangle(w,h);
  }

  public void scale(float factor) {
    r.setWidth(factor*getWidth());
    r.setHeight(factor*getHeight());
  }

  public float area() {
    return r.area();
  }

  public float getHeight() {
    return r.getHeight();
  }

  ...
}

Below are the resulting MDDs:


To summarize, the Adapter pattern is useful when you want to use an existing class, but its interface does not match the interface that you need.