Design a rubber band class hierarchy that you can reuse and extend Any user of modern graphical environments or applications has encountered a rubber band. In its most common use, the graphical rubber band selects, or lassos, a group of objects such as icons.The rubber band can do more than select objects. A GUI may also employ a rubber band to draw shapes or erase an area of the screen. Behind the scenes, a rubber band object may aid in the interactive resizing of a graphical component. Most shocking, and counter to the preconceived notions of the rubber band, this graphical oddity does not have to be rectangular. Yes, a rubber band may dare to be circular!Fortunately, Java makes things simple. Using the Abstract Windowing Toolkit, or AWT, as well as taking advantage of Java’s powerful interfaces, a developer can easily design a reusable rubber band class — and even extend it from GUI to GUI. When designing any class, we should always look for ways to keep the design flexible. The more flexible a class is, the more we will be able to use it again. And if we’ve done our job, the class will also allow for easy extensions so that we can add features without having to rewrite major sections of the class. Avoiding unnecessary future work is always a goal to strive for. Java is prewired to allow for flexible class structures. As we go through the design of the rubber band, we will rely heavily on Java interfaces. Java interfaces lend themselves to flexible and extendable architectures since they free us from rigid class hierarchies. Instead of being forced to inherit from a common ancestor, objects can play together as long as they implement the appropriate interface. Being locked into a particular class hierarchy can make it difficult to plug in new objects and can sometimes destroy an architecture. It is not always convenient or even possible to extend from a common object if the source is not readily available.Now, let’s get to the design.(To download the complete source code for this article as a jar file, see Resources.) The Pieces: RubberBand and RubberBandCanvasBefore implementing the rubber band, we need to isolate those elements involved in drawing and manipulating it. Roughly, we split the rubber band concept into two elements: the rubber band and the rubber band canvas. The rubber band is the object that draws a band on the canvas in response to events. The rubber band canvas is simply the place where the rubber band listens for events and where it draws itself.An optimal design will completely abstract the canvas from its rubber band; that is, the canvas shouldn’t have any knowledge of how the rubber band works. Likewise, the rubber band should interact with the canvas only in a well-defined but generic manner. A successful design will also allow the canvas to be any object that can display itself. By following these principles, we can design a rubber band that fits easily into any GUI.RubberBandIn our implementation, the RubberBand class does not extend from AWT’s Component or from Java Foundation Classes’ JComponent. The rubber band is never autonomous; instead, it is always associated with a canvas. That fact allows us to implement RubberBand as a lightweight displayable object. Instead of existing as a component that provides its own drawing context and events, RubberBand draws itself on a graphics object supplied by RubberBandCanvas. Likewise, RubberBand does not generate its own events. Rather, it responds only to events generated by RubberBandCanvas. Now let’s examine the RubberBand class: import java.awt.*; import java.awt.event.*; public class RubberBand { private RubberBandCanvas canvas; private Point startPoint; private Point endPoint; private boolean eraseSomething = false; private class MouseHandler extends MouseAdapter { public void mousePressed(MouseEvent e) { start(e.getPoint()); // anchor the RubberBand } public void mouseReleased(MouseEvent e) { erase(); // erase the final band } } private class MouseMotionHandler extends MouseMotionAdapter { public void mouseDragged(MouseEvent e) { erase(); // erase any old bands stop(e.getPoint()); // set end point draw(); // draw the new band } } public RubberBand(RubberBandCanvas c) { super(); setCanvas(c); getCanvas().addMouseListener(new MouseHandler()); getCanvas().addMouseMotionListener(new MouseMotionHandler()); } protected void draw() { Graphics g = getCanvas().getGraphics(); if(g != null) { try { // We always want to draw using XOR mode // so that we don't need to call redraw // to erase the band. g.setXORMode(canvas.getBackground()); drawRubberBand(g); // We have drawn something, set the flag // to indicate that there is something to erase. setEraseSomething(true); } finally { g.dispose(); } } } protected void drawRubberBand(Graphics g) { // The following if/else block determines where to draw the band. // Based on the anchor point, the band may be drawn in any of // four quadrants. The if/else determines which quadrant to draw // the band in. if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y > getStartPoint().y)) { g.drawRect( getStartPoint().x, getStartPoint().y, getEndPoint().x-getStartPoint().x, getEndPoint().y-getStartPoint().y ); } else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y < getStartPoint().y)) { g.drawRect( getEndPoint().x, getEndPoint().y, getStartPoint().x-getEndPoint().x, getStartPoint().y-getEndPoint().y ); } else if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y < getStartPoint().y)) { g.drawRect( getStartPoint().x, getEndPoint().y, getEndPoint().x-getStartPoint().x, getStartPoint().y-getEndPoint().y ); } else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y > getStartPoint().y)) { g.drawRect( getEndPoint().x, getStartPoint().y, getStartPoint().x-getEndPoint().x, getEndPoint().y-getStartPoint().y ); } } protected void erase() { // We only erase if there is something to erase! if(getEraseSomething()) { draw(); setEraseSomething(false); } } protected final RubberBandCanvas getCanvas() { return this.canvas; } protected final Point getEndPoint() { if(this.endPoint == null) { setEndPoint(new Point(0,0)); } return this.endPoint; } protected final boolean getEraseSomething() { return this.eraseSomething; } protected final Point getStartPoint() { if(this.startPoint == null) { setStartPoint(new Point(0,0)); } return this.startPoint; } protected final void setCanvas(RubberBandCanvas c) { this.canvas = c; } protected final void setEndPoint(Point newValue) { this.endPoint = newValue; } protected final void setEraseSomething(boolean newValue) { this.eraseSomething = newValue; } protected final void setStartPoint(Point newValue) { this.startPoint = newValue; } protected void start(Point p) { // anchor the band setEndPoint(p); setStartPoint(p); } protected void stop(Point p) { // set the end point, but no coordinate should be < 0 if(p.x < 0) { p.x = 0; } if(p.y < 0) { p.y = 0; } setEndPoint(p); } } The code above presents the initial implementation of the RubberBand class in its entirety. That implementation works as a baseline for the versions presented in the following sections.RubberBandCanvasThe implementation of RubberBandCanvas may or may not be a component. From the point of view of a RubberBand, it doesn’t matter what RubberBandCanvas‘s implementation is, just so long as RubberBand can obtain a Graphics object from RubberBandCanvas, query RubberBandCanvas‘s background color, and register itself with RubberBandCanvas as a Mouse and MouseMotion listener.For this reason, RubberBandCanvas should be implemented as an interface with the ability to supply a Graphics instance, supply the background color, and register Mouse and MouseMotion listeners. Since RubberBandCanvas is an interface, its actual implementation can be an applet or some other lightweight component. Moreover, its being an interface frees us from being tied down to any specific implementation. With this information, it is easy to formulate an initial RubberBandCanvas interface: public abstract interface RubberBandCanvas { public abstract Graphics getGraphics(); public abstract void addMouseMotionListener(MouseMotionListener listener); public abstract void addMouseListener(MouseListener listener); public abstract Color getBackground(); } The code contained below presents a basic example of an applet that implements the RubberBandCanvas interface and associates itself with a RubberBand instance:import java.applet.*; import java.awt.*; import java.awt.event.*; public class RubberBandTest extends Applet implements RubberBandCanvasIF { protected String str = "RubberBands!!!!"; private RubberBand rubberband; protected final RubberBand getRubberBand() { if(this.rubberband == null) { setRubberBand(new RubberBand(this)); } return this.rubberband; } public void init() { super.init(); getRubberBand(); } public void paint(Graphics g) { g.drawString(str, 5, 50); } protected final void setRubberBand(RubberBand newValue) { this.rubberband = newValue; } } With the applet above, RubberBand displays itself whenever the user presses and drags the mouse.This is all well and good. However, the current RubberBand is nothing more than eye candy. It doesn’t really do anything! RubberBand needs to communicate with the corresponding RubberBandCanvas. RubberBand can then notify RubberBandCanvas whenever it draws a bounding box. RubberBand communicationThe example above demonstrates a basic RubberBand. However, it lacks the facilities to do anything to the area it bounds. Embedding such logic inside the RubberBand class would restrict RubberBand‘s flexibility. The inflexibility would force the developer to extend RubberBand and reimplement the logic each time a RubberBand was needed.Instead, RubberBandCanvas needs to implement the application-specific logic. RubberBand simply needs a way to communicate the bounding area back to RubberBandCanvas. Fortunately, the problem is easily corrected: add one method to RubberBandCanvas and call that method at appropriate times from within RubberBand.First, we need to add a method — areaBounded() — to RubberBandCanvas that allows RubberBand to pass it the upper left-hand corner and lower right-hand corner of RubberBand‘s bounding box: public void areaBounded(int startX, int startY, int endX, int endY); Now RubberBand simply calls areaBounded() whenever it draws itself. Be aware that RubberBand does not call areaBounded() directly from its drawing code. Rather, RubberBand calls the helper method NotifyRubberBandCanvas(). NotifyRubberBandCanvas() calculates the bounding coordinates and then calls areaBounded(). The call to NotifyRubberBandCanvas() is made from within the inner class MouseMotionHandler‘s mouseDragged() method. The MouseMotionHandler class is shown below:private class MouseMotionHandler extends MouseMotionAdapter { public void mouseDragged(MouseEvent e) { erase(); stop(e.getPoint()); draw(); // Let the canvas know an area is bounded notifyRubberBandCanvas(); } } protected void notifyRubberBandCanvas() { int startX, startY, endX, endY; // We always want to return the upper left hand corner // and the lower right hand corner, the coordinates need // to be filtered accordingly and returned in the right order. if(getStartPoint().x < getEndPoint().x) { startX = getStartPoint().x; endX = getEndPoint().x; } else { startX = getEndPoint().x; endX = getStartPoint().x; } if(getStartPoint().y < getEndPoint().y) { startY = getStartPoint().y; endY = getEndPoint().y; } else { startY = getEndPoint().y; endY = getStartPoint().y; } getCanvas().areaBounded(startX, startY, endX, endY); } Since RubberBandCanvas is an interface, areaBounded() has no defined behavior. RubberBand simply makes the call; it doesn’t care what RubberBandCanvas does with the information. RubberBandCanvas can even ignore the information.Any class that implements RubberBandCanvas may implement areaBounded() as it sees fit. One program may wish to select all the objects that fall within RubberBand‘s bounding box. Another may want to erase everything in the area. Still another implementation may wish to play the sound of a snapping rubber band. Figure 2 demonstrates the newly added functionality. Now, RubberBandCanvas (here an applet) displays the coordinates of the bounding box whenever its areaBounded() method is called.Things work out nicely if we always want RubberBand to display itself in response to an event.Next, we turn our attention to the times when RubberBand shouldn’t draw itself. Hiding RubberBandAs we have seen, RubberBand independently listens for and responds to Mouse and MouseMotion events. However, at times, RubberBandCanvas may want RubberBand to temporarily stop responding to events. Take a graphical desktop environment, for example. When an icon is dragged on the desktop, RubberBand really shouldn’t draw itself. Instead, it should just ignore the event. Our current design draws the band anyway.To solve this problem, we can require RubberBandCanvas not to forward the events to RubberBand when it is not appropriate for RubberBand to display itself. However, that approach requires RubberBandCanvas to have special internal knowledge of RubberBand. RubberBandCanvas needs to know that RubberBand is registered for events. It then needs to treat RubberBand differently from its other listeners. Obviously, this is not an optimal approach.Another approach calls for the developer to subclass RubberBand and put some kind of application-specific hooks and logic inside the subclass. Again, this is not an appropriate solution, since RubberBand should be independent of RubberBandCanvas‘s implementation. It should have no special knowledge of its RubberBandCanvas. The solution, seen below, is to add the canDrawRubberBand() method to RubberBandCanvas:public boolean canDrawRubberBand(); RubberBand simply calls canDrawRubberBand() each time it wishes to draw itself. If the method returns true, RubberBand displays itself. If the method returns false, RubberBand simply disregards the event and does not display. Let’s look at this in action:private class MouseMotionHandler extends MouseMotionAdapter { public void mouseDragged(MouseEvent e) { // Before drawing make sure that we really should. if(getCanvas().canDrawRubberBand()) { erase(); stop(e.getPoint()); draw(); notifyRubberBandCanvas(); } } } protected void draw() { Graphics g = getCanvas().getGraphics(); if(g != null) { try { // Don't draw if we shouldn't. if(getCanvas().canDrawRubberBand()) { g.setXORMode(canvas.getBackground()); drawRubberBand(g); setEraseSomething(true); } } finally { g.dispose(); } } } Now RubberBandCanvas simply returns true or false depending on whether it wants RubberBand to draw itself at any given time. Let’s go back to our example. Whenever icons are dragged across the desktop, the desktop returns false when RubberBand calls canDrawRubberBand(). At other times, RubberBandCanvas returns true. Again, the behavior is implementation specific. Some implementations may always return true. A misguided implementation could even always return false. Pink elephant RubberBands?Up to now, RubberBand always displays itself as a rectangular bounding box. But an application may wish to draw rectangles, squares, circles, ellipses, triangles, or pink elephants. How can we turn our RubberBand into another shape? The first idea that comes to mind is that RubberBand is not a final class. A developer could subclass RubberBand. However, in its current implementation, subclassing RubberBand is difficult. The implementation requires the developer not only to define how the subclass displays itself but also to recode the logic that determines where to place RubberBand.The following code demonstrates the root of the difficulty:protected void drawRubberBand(Graphics g) { // If this method is overridden, the if else logic must be reproduced in the // over-ridding method! if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y > getStartPoint().y)) { g.drawRect( getStartPoint().x, getStartPoint().y, getEndPoint().x-getStartPoint().x, getEndPoint().y-getStartPoint().y ); } else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y < getStartPoint().y)) { g.drawRect( getEndPoint().x, getEndPoint().y, getStartPoint().x-getEndPoint().x, getStartPoint().y-getEndPoint().y ); } else if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y < getStartPoint().y)) { g.drawRect( getStartPoint().x, getEndPoint().y, getEndPoint().x-getStartPoint().x, getStartPoint().y-getEndPoint().y ); } else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y > getStartPoint().y)) { g.drawRect( getEndPoint().x, getStartPoint().y, getStartPoint().x-getEndPoint().x, getEndPoint().y-getStartPoint().y ); } } The problem: To redefine RubberBand as an ellipse, the developer needs to override drawRubberBand(). The developer then needs to cut and paste the superclass’s implementation of drawRubberBand() and replace each call to drawRect() with the appropriate call to drawOval(). To fix this problem, we need to abstract the definition of how RubberBand displays itself. We begin by adding the abstract method drawBoundingShape() and redeclaring RubberBand as an abstract class. Now the definition of what the rubber band looks like is deferred to the subclass:protected abstract void drawBoundingShape(Graphics g,int startx, int starty, int width, int height); Next, we need to replace each call to drawRect() in drawRubberBand() with a call to drawBoundingShape():protected void drawRubberBand(Graphics g) { //Instead of calling drawRect, we call the abstract method drawBoundingShape. if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y > getStartPoint().y)) { drawBoundingShape( g, getStartPoint().x, getStartPoint().y, getEndPoint().x-getStartPoint().x, getEndPoint().y-getStartPoint().y ); } else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y < getStartPoint().y)) { drawBoundingShape( g, getEndPoint().x, getEndPoint().y, getStartPoint().x-getEndPoint().x, getStartPoint().y-getEndPoint().y ); } else if((getEndPoint().x > getStartPoint().x) && (getEndPoint().y < getStartPoint().y)) { drawBoundingShape( g, getStartPoint().x, getEndPoint().y, getEndPoint().x-getStartPoint().x, getStartPoint().y-getEndPoint().y ); } else if((getEndPoint().x < getStartPoint().x) && (getEndPoint().y > getStartPoint().y)) { drawBoundingShape( g, getEndPoint().x, getStartPoint().y, getStartPoint().x-getEndPoint().x, getEndPoint().y-getStartPoint().y ); } } Each class that extends RubberBand is required only to define the graphical shape of the band. This approach successfully abstracts how RubberBand looks from how RubberBand behaves. Now an EllipseRubberBand is simply implemented by extending RubberBand and implementing drawBoundingShape(), as seen in the following code:import java.awt.*; public class RectangleRubberBand extends RubberBand { public RectangleRubberBand(RubberBandCanvasIF c) { super(c); } protected void drawBoundingShape(Graphics g, int startX, int startY, int width, int height) { g.drawRect(startX,startY,width,height); } } In the code below, we also implement a RectangleRubberBand by extending RubberBand and implementing method drawBoundingShape():import java.awt.*; public class EllipseRubberBand extends RubberBand { public EllipseRubberBand(RubberBandCanvasIF c) { super(c); } protected void drawBoundingShape(Graphics g, int startX, int startY, int width, int height) { g.drawOval(startX,startY,width,height); } } There is one last problem: Now that we have more than one type of RubberBand, it is entirely possible that a RubberBandCanvas will associate itself with more than one instance of RubberBand. Furthermore, the application may want only a certain RubberBand to draw itself at any given time. The call canDrawRubberBand() gives RubberBandCanvas the ability to turn all RubberBands on or off. However, it does not allow RubberBandCanvas to filter RubberBands and specify which should display and which should not display.Again, RubberBandCanvas should not make assumptions about RubberBand. To allow RubberBandCanvas to filter RubberBands, we must change the canDrawRubberBand() call so that the calling band passes a reference of itself to RubberBandCanvas, codified as follows: public void canDrawRubberBand(RubberBand band); Now RubberBandCanvas can check to see if it wants the specific calling RubberBand to display itself or not.The same problem holds true for RubberBand‘s call to areaBounded(). RubberBandCanvas may wish to treat one RubberBand‘s call to the method differently from another’s call. As in canDrawRubberBand(), we need to have the caller pass a reference of itself to RubberBandCanvas:public void areaBounded(RubberBand band, int startX, int startY, int endX, int endY); RubberBandCanvas may treat calls to areaBounded() differently, depending on which RubberBand makes the call.Good designs have good compromisesMy intention here was to remove all application-specific logic from RubberBand. Like any other design decision, it was a double-edged sword. On one hand, we have successfully removed all application logic from RubberBand. This frees the developer from having to extend RubberBand each time a RubberBand is needed.Woo hoo!Now for the “D’oh!” …Let’s suppose RubberBandCanvas has two RubberBands associated with it. Furthermore, let’s imagine that one RubberBand draws a rectangular band, the other an ellipical band, and that RubberBandCanvas wants to draw a shape corresponding to RubberBand when it is stretched.RubberBandCanvas must track which RubberBand drew itself and which shape corresponds to that band. RubberBandCanvas needs some internal information about RubberBand. Namely, it needs to know how RubberBand draws itself.D’oh!So, apparently, it might be a good idea for RubberBand to have some application-specific logic. We can solve this problem in a couple of ways. We canAdd an abstract method to the base RubberBand class, orAdd the ability to associate RubberBand with a Command objectThe first solution requires the addition of a method that is called right before the call to notifyRubberBandCanvas(). The method might look like this:protected abstract void action(); Our default implementations of RubberBand can just leave this method empty. If there is ever a need for this method to do something, we simply extend one of the RubberBand implementations, override the method, and provide the logic.Unfortunately, this approach leads to the extension and proliferation of RubberBand classes.The second solution attempts to avoid the shortcomings of the first. To wit, rather than extend RubberBand, a developer can embed application-specific logic inside a Command object. The programmer can then associate the Command object with the RubberBand instance. Now, right before RubberBand notifies RubberBandCanvas of its actions, it can execute the Command object. This frees the developer from having to extend RubberBand. Instead, the application-specific logic embeds nicely inside a Command object that may be swapped into RubberBands as needed. As a bonus, using Commands allows the GUI to be dynamic.Suppose we are writing a drawing program. We want a band that acts as both brush and eraser. If we follow solution one, we need two bands. If we follow solution two and write two Command objects, we need only one band. When we want the band to draw, we simply tell the band to use the drawing command. When we want the band to erase, we simply swap in the erase command.ConclusionFollowing a sound object-oriented approach, we designed a RubberBand class that is robust and reusable. By clearly separating the roles of RubberBandCanvas and RubberBand itself, the two pieces of our design are completely independent of each other. Neither piece needs to make assumptions about the other that would cause our design to break. Furthermore, by abstracting the behavior from the display of RubberBand, we can now easily build RubberBand subclasses of various shapes but identical behavior.Through these examples we see how the careful use of abstraction and Java’s powerful use of interfaces allow the development of robust and reusable objects. The previous examples contained a fair amount of source code. Because of space limitations not all of the code, such as the example applets for each revision, appear here. However, a jar file containing all of the source, example applets for each revision, and javadocs are available for downloading from Resources.Tony Sintes is a consultant at ObjectWave Corporation specializing in telecommunications. Before going to ObjectWave, Tony worked at DSC Communications, where he led the development of the TPMS GUI system for DSC’s Telecom platform. Tony has worked with object-oriented and various GUI technologies since 1995. Tony is a Sun-certified Java 1.1 programmer and Java 2 developer. He has just accepted an offer to author a monthly column in Silicon Prairie Magazine. Keep an eye out for the premier installment ( http://chicagotribune.com/tech/). JavaSoftware Development