In this lab we'll add two new tools to our drawing program: a tool for drawing circles in the canvas, and a tool for deleting shapes from the canvas. You can run the command ``drawing 2'' to see how your program should work after this lab.
To begin, type the following to set up the directory for Lab 9.
In Forte, close all currently open files in the editor (by repeatedly right-clicking in the Editor window and selecting Close). Then, modify the package lines of the files from the Lab9 package to reflect the new directory.% getcs 160 9 % cp ~/CS160/Lab7/*.java ~/CS160/Lab9
This lab illustrates the concept of iterative development, which means that we'll break the task into smaller pieces. For each step, you'll conclude with a complete, working program, which you should test completely before going onto the next step.
In this lab, we'll split the overall task into four steps. (Deciding what the individual, bite-size steps are takes some experience. In this case, the lab does that part of the job for you.)
Iterative development is a good habit to develop, since it makes the process of finding errors simpler. This is because, after each small step, you've only made a few changes. When you find errors during the testing process, you'll know that the fix must lie within the small set of changes.
When we are done, our program will have the following inheritance hierarchy. Two of the classes, Shape and Tool, are abstract classes and are represented in italics.
The first step is to alter Drawing so that it can hold any combination of shapes. To accomplish this, we'll define a Shape class. It will be abstract, since we'll never actually create a shape per se - the class exists only so that we can refer to rectangles and circles as abstract shapes within the Drawing class. (Drawing will never refer directly to Rectangle. Instead, it will just treat every object in its ArrayList as a Shape.)
The Shape class will define the single instance method draw().
Define the abstract Shape class, and modify Rectangle so that it extends Shape. Finally, modify Drawing so that it can hold any combination of Shapes. This should require only that you replace each reference to the Rectangle class in Drawing.java with Shape instead.
The purpose of a Tool object is to define what Canvas will do when the user drags the mouse. The abstract Tool class defines two abstract methods.
The first subclass of Tool, RectangleTool, will simply accomplish what the mouseDragging() and mouseDragged() methods of Canvas from Lab 7.
The Canvas class will have a Tool instance variable (whose initial value is an instance of RectangleTool) to track the currently selected tool. Its mouseDragging() and mouseDragged() methods will simply call the corresponding method of the current tool.
Before continuing with this lab, you should complete and test all of the above. When you run your program, it should work exactly as in Lab 7. So far, it doesn't look like much progress, but we've set things up so that adding the new features will be easier.
Now we'll add the capability to draw circles. First, define a new concrete class Circle that extends Shape. It will have the following constructor.
Also, define a concrete CircleTool class, extending Tool. Its definition will be quite similar to the RectangleTool class.
Finally, modify Canvas so that it now has a Tool menu, with both Rectangle and Circle options. Selecting the menu item should simply change the Canvas's Tool instance variable, so that subsequent mouse events get forwarded to this other Tool object instead.
When the deletion tool is selected, the shape should disappear from the canvas when the user clicks a shape. To accomplish this, perform the following tasks.
This lab illustrates how you can polymorphism (the ability of an object to masquerade as an instance of its superclass) and overriding (the ability of an object to redefine instance methods) to simplify a program and avoid explicitly testing for various cases.
Before we learned about inheritance at all, the way we would accomplish having different tools for the program would be to keep an integer variable, whose value would be 0 for creating rectangles, 1 for creating circles, and 2 for deleting shapes. Then, in the mouseDragging() and mouseDragged() method of Canvas, we would write something like the following.
I'll call this the else if technique. It gets to be a pain, but that's what we would have to do in order to handle the various cases.if(tool == 0) { // do what the rectangle tool should do } else if(tool == 1) { // do what the circle tool should do } else if(tool == 2) { // do what the delete tool should do }
Instead, we defined the abstract Tool class, and had an instance variable within Canvas to track the a current Tool instance. In mouseDragging() and mouseDragged(), we simply forward the call onto the current Tool instance. Since each Tool subclass overrides these methods, the behavior is different based on what the current tool is.
We played a similar trick with shapes. Without inheritance, we would have to track what sort of shape each thing in the Drawing is. Instead, we simply defined a Shape class, and had each subclass define its own draw() and find() methods.
This trick has two major advantages. First, it makes for a cleaner structure, with distinct responsibilities split between files. This is itself a major virtue: With larger programs, ways to split what would otherwise be a huge class into smaller pieces becomes more desirable.
The second advantage it makes the program more easily extendible. Suppose, for example, that we wanted to add the capability to draw line segments to the program. We would of course define Line and LineTool classes. We wouldn't have to change Drawing at all, and the only modification to Canvas would be for putting the new menu item in. If we used the else if technique, the changes to Drawing and Canvas would be much more extensive.
This is a good heuristic to keep in mind with object-oriented design: If you see any place in the program where you have several cases, there's a good chance that you can avoid it using polymorphism and overriding, just as we have done in this lab. A strong indication that this is worth considering is when you are tempted to include a long string of else if's. Such strings of else if's are confusing and usually avoidable with a better object-oriented design.