Object-Oriented Design |
The methodology that we use to do design is called object-oriented design. Grace is itself an object-oriented programming language. What does this mean?
An object is an entity that provides an abstraction in our program. We call it an abstraction because we can use an object without knowing how it is implemented. We abstract away from its implementation and can understand it at a higher level, its purpose. Object-oriented approaches typically attempt to model the real world and so I will be doing a lot of that today.
Ok, so when we are designing software, we want to build abstractions. We want to design our classes so that others can use them without understanding how they are implemented.
Let's consider balls. In the real world there are all kinds of balls: beach balls, tennis balls, baseballs, footballs, etc. What are some of the properties of balls?
and even location, if we consider its position with respect to the rest of the world to be an interesting characteristic.
Now, let's recall the boxball program. Here we see a model of balls. Do these balls have the same properties as the real world balls? Some of the properties have analogs:
The other properties are completely absent. For the properties, there are two key questions:
Why did we model the properties that we did? A program provides a model of the real world. When we write a program, we get to decide how realistic we want our model to be. Where we want realism, we include those properties. For our purpose, we found such things as surface texture to be irrelevant to our needs.
How do we model properties? We model properties with instance variables and constants. For example, the speed of a ball in the boxball game does not change. So we can model the speed of a ball with a constant.
The location of a ball is also modeled (although in 2-d, not 3-d). The location of a ball changes so we cannot model it with a constant. We must use an instance variable. How do we model the location? We know that the objectdraw library provides abstractions to us that encapsulate information about size, shape, and location of 2D objects. Thus, rather than trying to maintain information about the location explicitly, we use this abstraction to maintain this information for us. Within our Ball class, we just work with the abstraction:
private FilledOval shape;
We thus describe properties of objects with instance variables and constants. These allow us to describe our objects and to distinguish one object from another.
Modeling Behavior. Besides the properties we identified earlier, we can also observe that balls have behavior. What are some of their behaviors?
As with properties, we will want to model some of these behaviors in our programs and others we will omit. We should recognize that these are deliberate decisions. We are creating a model of the real world and controlling the realism in our model. In what ways is our model implemented in boxball unrealistic:
How do we model behavior? We define a method for each type of behavior that we want to allow. Thus, we need a way to drop a ball. How do we do this? We make the ball appear to drop by moving it a short distance, pausing, moving, pausing, etc. until it reaches the bottom. In fact, for boxball, this is the principal behavior of the ball. We know that we accomplish animation by making an ActiveObject and putting the animation code in the run method. Again, the important point here is that we model behavior with methods.
Using existing classes. When we discussed properties and behavior, we learned something interesting. Some work we have to do ourselves, like making the ball fall. Other work we can implement by using an existing abstraction, a filledOval object in this case, and make that object do the work for us. Thus, by creating a filledOval shape, when we want to drop the ball, we can tell the shape to move. When we want the ball to disappear, we can tell the shape to remove itself from the canvas. We have built our ball abstraction on top of a predefined filledOval abstraction. We can make things happen (like disappearing) without having a deep understanding at all of how to do it! Well-defined abstractions allow us to be lazy! We don't do things ourselves. Instead, we delegate to other objects and make them do the work!
Completing the abstraction. So now we know that a class contains constants, instance variables, and methods. You should know by now to make your instance variables are private. What does "private" mean? It means that the variable name is not known outside of that class. If we declare the size of a ball to be private, then nothing outside of the ball can refer to or modify the size of the ball. But since instance variables represent properties that we observed from the real world, shouldn't those properties be visible outside of the object??
There are two types of visibility we might be interested in. One type is physical visibility. Do we want to allow the human user running our program to observe these properties through the user interface? If so, we need to make those properties visible on the screen. These naturally happens for things like the sizes and colors of our objects. We can do this just by picking the right shapes, setting their colors, and placing them on the canvas.
A different type of visibility concerns whether we want other classes to be able to observe and/or modify these properties. We might decide that we want to allow both access and modification, we might want to allow access but not modification, or we might want to allow neither. Making this decision largely depends upon the purpose of the application. What I will focus on here is how we would provide these capabilities if we wanted them.
Accessor methods: If we have information in a private instance variable and we want to allow other classes to see that information, we write a method to do so.
Mutator methods: Sometimes we want to allow other classes to change the value of our instance variables as well. To allow this we define a setter method:
method color:= (newColor: Color) { myColor := newColor; }
Here, the method returns void. Its name is "set" followed by the thing being set. It takes a single parameter whose type matches the type of the instance variable we are setting. It contains a single statement that changes the value of the instance variable to the value of the parameter.
Constructors
The final piece that we need to complete an abstraction is a constructor. A constructor tells us how to create the object and how to initialize its instance variables. The parameters passed to a constructor tells us how to distinguish one object from another one of the same type initially. If all instances of an object started out the same initially, we wouldn't need any parameters in our constructor call! Usually, however, we do want to distinguish them, so we use these parameters to initialize our variables and provide those distinctions.
Our focus so far has been on objects that model the real world. We have written classes, however, that do not model the real world. In particular, we have written objects that inherit from graphicApplication. These types of objects do not really have an analog in the real world. Instead, they allow us to deal with user input. All nontrivial programs have some way of managing user input. The graphicApplication abstraction, in particular, gives us methods in which we can define how to handle mouse actions. All our programs will have some component dealing with the artificial world of user input.
Similarly, every interesting program provides user output. For us, user output is provided using the objects of objectdraw like rectangles, ovals, lines, and text. Again, we should see these in every program.
Object-Oriented Design |