Object-Oriented Modeling of a User Interface


These provide notes a recap of the important points presented in the object-oriented modeling lecture. The object-oriented modeling lecture was designed to emphasize the point that you should create explicit objects for each of the important elements in your view, and that these objects should take care of such things as rendering themselves and indicating whether or not they contained a mouse point. Later in the semester we will flesh out the object model by adding display managers and event handlers. Your projects should make use of object-oriented modeling.


Modeling View Elements as Objects

When designing your interface, you want to identify the important elements in your graphical interface and define classes for each of them. For example, in a game you are likely to have text strings for scores, timers, and instructions, and certain types of game pieces. You should therefore create a class for labels and a class for each type of game piece. If you had a boxes and arrows editor, then you might create separate classes for the boxes and the arrows. If there were different types of boxes, as you might have in a flow chart, then you might create a superclass for an abstract box and then concrete boxes for each of the box types. Even feedback objects should typically be modeled as objects and have their own draw methods.

Next you should decide assign properties to each of your objects. The properties allow you to customize the appearance of your objects. Typical properties include location and size, and often include color and line style. Each type of object might have its own unique properties as well. Labels might need a string and a font. An image class might need a source property that specifies where to find the bitmap, tiff, gif, or jpeg file for that image.

Finally you should write the draw and contains methods for each of your classes. The draw class should take the graphical context as its parameter and should use the use the various properties of the object to set up the graphics context and then draw the object to the screen. The paintComponent method should be a high-level manager that simply calls the draw methods of the various objects. Rarely should paintComponent have to resort to setting up the graphics context or making toolkit calls, such as drawRect, to draw objects.

The contains method should take the x ande y values of a mouse event and return true or false based on whether or not the object contains the mouse point.


Composition

If you want to inherit an object's implementation but not its interface, use composition. Composition is implemented by making object A be a container for another object, B, and having A delegate all of its implementation to B's methods. We say that A is composed from B.

The classic example of wanting to inherit an object's implementation but not its interface is based on a circle and an ellipse. A circle should be a restricted version of an ellipse, with the width and height constrained to be equal. As such it seems natural to make a circle be a subclass of an ellipse. Unfortunately, the circle does not want to inherit the setWidth or setHeight methods from an ellipse. Instead it wants to provide a setRadius and/or setDiameter method. However, the circle does want to inherit all of the ellipse's implementation.

Instead of making a circle a subclass of an ellipse, you should use composition to define the circle. This involves making a circle be a container object for an ellipse. For example:

class circle extends AbstractShape {
  ellipse my_ellipse;
}
Then implement each of circle's methods by delegating their implemention to the appropriate method in the ellipse:
  public draw(Graphics g) {
    my_ellipse.draw(g);
  }
  public setLeft(int left) {
    my_ellipse.setLeft(left);
  }
  public getLeft() { return my_ellipse.getLeft(); }
 
  public setRadius(int r) {
    my_ellipse.setWidth(2*r);
    my_ellipse.setHeight(2*r);
  }
  public getRadius() { return my_ellipse.getWidth(); }
Notice that in getRadius it does not matter whether we call the ellipse's getWidth or getHeight methods, because both will return the same value. Also note that setRadius calls two of the ellipse's methods, in order to ensure that the width and height will be the same.


Factorization

When designing an inheritance hierarchy I find it easiest to construct it from the bottom-up (i.e., from the leaves to the root). I often define all of the concrete classes that I will need and assign them instance variables and methods. I then try to factor out common instance variables and methods and create a new superclass to contain those instance variables and methods. This allows me to avoid duplicating code in different classes. For example, in yesterday's lecture, a rectangle, an ellipse, an image, and a text object all had instance variables for left, top, width, height, and visible. Hence I factored out these instanced variables and placed them in a new superclass that I called BoxShape. Later I found that the visible variable was a common feature of both a BoxShape, a LineShape, and a PolyLine, and hence I factored out that variable and promoted it to a new superclass, called AbstractShape. In a similar manner, the draw and contains methods ended up in the AbstractShape class, because they were common to every shape in the hierarchy. Get and Set methods for the left, top, width, and height classes would appear in the BoxShape class, because those methods are common to all shapes in the BoxShape sub-hierarchy.

You must also be prepared to be flexible and if necessary, move elements around your inheritance hierarchy. I did that yesterday with a Circle, which got moved from being a subclass of BoxShape to being a subclass of AbstractShape.

Finally, you should not be afraid to create an implemention for a method that will be used by most, but not all, of the subclasses. In yesterday's lecture, I indicated that it would be right to create an implementation for the contains method and place it in the BoxShape class, because it would be used by the rectangle, text, and image classes. The ellipse class might choose to override the contains method with its own implementation, but at least three classes would share the contains code placed in the BoxShape class.