Observers and Observables

The Observer interface and the Observable class in the java.util package are Java's implementation of the publisher-subscriber design pattern.

Brick CAD

Brick CAD is a CAD-CAM system for designing bricks. Luckily, bricks are fairly easy to design. They only have three attributes: height, width, and length. Brick CAD allows users to view bricks four different ways: front view, side view, top view, and control panel view. Of course multiple instances of each view are allowed:

Using a control panel view to update the brick's dimensions automatically updates all views of the brick.

Static Structure

Brick CAD refines the model-view-controller architecture by making models observables and views observers:

 

Brick

Bricks are pretty simple observables:

class Brick extends Observable
{
   private int length, width, height;
 
   public Brick()
   {
      length = 50;
      width = 40;
      height = 100;
   }

   public int getLength() { return length; }
   public int getWidth() { return width; }
   public int getHeight() { return height; }

   public void setDim(int l, int w, int h)
   {
      length = l;
      width = w;
      height = h;
      setChanged();
      notifyObservers();
   }

   public void hideViews()
   {
      setChanged();
      notifyObservers("QUITTING");
   }
}

 

Brick View

I decided to make views panels. Another option is to make views frames:

class BrickView extends Dialog implements Observer
{
   public BrickView(Brick brick, Frame parent, String title)
   {
      super(parent, title, false);
      setSize(300, 300);
      setLocation(100, 100);
      myBrick = brick;
      myBrick.addObserver(this);
      addWindowListener(new MyWindowListener());
      updateMetrics();
   }

   public void finalize() { myBrick.deleteObserver(this); }

   class MyWindowListener extends WindowAdapter
   {
      public void windowClosing(WindowEvent we) { setVisible(false); }
   }

   public void update(Observable brick, Object arg)
   {
      repaint();
      if (arg != null && arg.equals("QUITTING"))
      {
          myBrick.deleteObserver(this);
          setVisible(false);
      }
   }

   protected void updateMetrics()
   {
      Dimension d = getSize();
      Insets in = getInsets();
      int clientWidth = d.width - in.right - in.left;
      int clientHeight = d.height - in.bottom - in.top;
      int xUpperLeft = in.right;
      int yUpperLeft = in.top;
      xCenter = xUpperLeft + clientWidth/2;
      yCenter = yUpperLeft + clientHeight/2;
   }

   protected int xCenter, yCenter;
   protected Brick myBrick;
   protected Color brickRed = new Color(220, 10, 60);
}

 

 

Side, Front, and Top Views

The side, front, and top views only differ in their titles and the brick dimensions they are interested in:

class SideView extends BrickView
{
   public SideView(Brick brick, Frame parent)
   {
      super(brick, parent, "Side View");
   }

   public void paint(Graphics g)
   {
      int d1 = myBrick.getLength();
      int d2 = myBrick.getWidth();
      int x = xCenter - d1;
      int y = yCenter - d2;
      g.setColor(brickRed);
      g.fillRect(x, y, d1, d2);
   }
}

 

Panel View

class PanelView extends BrickView
{
   private TextField heightField, widthField, lengthField;

   public PanelView(Brick brick, Frame parent)
   {
      super(brick, parent, "Panel View");
      setLayout(new GridLayout(4, 1));

      heightField = new TextField(10);
      heightField.setText("" + brick.getHeight());
      LabeledTextField ltf1 =
         new LabeledTextField("Height", heightField);
      add(ltf1);

      widthField = new TextField(10);
      widthField.setText("" + brick.getWidth());
      LabeledTextField ltf2 =
         new LabeledTextField("Width", widthField);
      add(ltf2);

      lengthField = new TextField(10);
      lengthField.setText("" + brick.getLength());
      LabeledTextField ltf3 =
         new LabeledTextField("Length", lengthField);
      add(ltf3);

      Button apply = new Button("Apply");
      Panel p = new Panel();
      p.add(apply);
      add(p);
      apply.addActionListener(new ApplyListener());
   }

 

   public void update(Observable brick, Object arg)
   {
      super.update(brick, arg);
      lengthField.setText("" + myBrick.getLength());
      widthField.setText("" + myBrick.getWidth());
      heightField.setText("" + myBrick.getHeight());
   }

 

   class ApplyListener implements ActionListener
   {
      public void actionPerformed(ActionEvent e)
      {
         int w = Tools.toInt(widthField.getText());
         int h = Tools.toInt(heightField.getText());
         int l = Tools.toInt(lengthField.getText());
         myBrick.setDim(l, w, h);
      }
   }
}

Labeled Text Field

This is a useful component that belongs in my util package. It creates a 2X1 panel containing a label and a text field:

class LabeledTextField extends Panel
{
   public Label myLabel;
   public TextField myField;

   public LabeledTextField(String lab, TextField tf)
   {
      setLayout(new GridLayout(1, 2));
      Panel p1 = new Panel();
      myLabel = new Label(lab);
      p1.add(myLabel);
      add(p1);
      myField = tf;
      Panel p2 = new Panel();
      p2.add(tf);
      add(p2);
   }
}

Main Frame

The application is identified with a frame called BrickCAD. The frame has three menus and keeps a reference to the current brick being modeled:

public class BrickCAD extends MainFrame implements ActionListener
{

   protected Menu fileMenu, viewMenu, helpMenu;
   protected MenuBar mbar;
   protected Object[] fileItems, viewItems, helpItems;
   protected String aboutText = "Brick CAD version 1.0";
   protected Brick currentBrick = null;

 

The constructor builds the menus:    public BrickCAD()
   {
      setTitle("Brick CAD");
      mbar = new MenuBar();
 
      // file menu
      fileItems =
         new Object[] {"New Brick", "Quit" };
      fileMenu = Core.makeMenu("File", fileItems, this);
      mbar.add(fileMenu);

      // view menu
      viewItems =
         new Object[] { "Side", "Top", "Front", "Panel" };
      viewMenu = Core.makeMenu("View", viewItems, this);
      mbar.add(viewMenu);
 
      // help menu
      helpItems = new Object[] { "About" };
      helpMenu = Core.makeMenu("Help", helpItems, this);
      mbar.add(helpMenu);

      setMenuBar(mbar);

   }

The action event handler dispatches the event:    public void actionPerformed(ActionEvent evt)
   {
      MenuItem c = (MenuItem)evt.getSource();
      String arg = c.getLabel();

      if(arg.equals("Quit")) handleQuit();
      else if (arg.equals("New Brick")) handleNew();
      else if (arg.equals("Side")) handleSide();
      else if (arg.equals("Front")) handleFront();
      else if (arg.equals("Top")) handleTop();
      else if (arg.equals("Panel")) handlePanel();
      else if (arg.equals("About")) handleAbout();
      else System.err.println("Selected item = " + arg);
   }

 

Handling quit and about are the same old boring routines:    protected void handleQuit()
   {
      System.exit(0);
   }

   protected void handleAbout()
   {
      MessageDialog mg = new MessageDialog(this, "About", aboutText);
      mg.setVisible(true);
   }

handleNew() gets rid of the existing views, redefines currentBrick, and sets up a modal dialog instructing users to select views:    protected void handleNew()
   {
      if (currentBrick != null)
         currentBrick.hideViews();
      currentBrick = new Brick();
      MessageDialog mg =
         new MessageDialog(this, "Note", "Select views");
      mg.setVisible(true);
   }
handleSide() creates a side view for the current brick. handlePanel(), handleFront() and handleTop() are essentially the same:    protected void handleSide()
   {
      if (currentBrick != null)
      {
         BrickView bv = new SideView(currentBrick, this);
         bv.setVisible(true);
      }
      else
         error("current brick = null");
  
   }

   protected void handleFront() { ... }
   protected void handleTop() { ... }
   protected void handlePanel() { ... }

The GUI is the best place to deal with errors:    protected void error(String msg)
   {
      MessageDialog mg = new MessageDialog(this, "Error!", msg);
      mg.setVisible(true);
   }

 

Here's main():    public static void main(String[] args)
   {
      BrickCAD bc = new BrickCAD();
      bc.setVisible(true);
   }
}