Session 16

Programs that Share Control


CS 2530
Intermediate Computing


An Opening Exercise

Define a ShadowBall class. A ShadowBall is a Ball that becomes darker after every hundredth time it moves.

A Ball's own Color instance variable can help the ShadowBall do its task. Java Colors respond to the message:

public Color darker()

      Creates a darker version of this color.

Notice that, in response to the message, a Color returns a darker version of itself; the Color doesn't change itself.



An Opening Solution

A ShadowBall is a Ball. So our solution should be a subclass of Ball.

This means that I can use a ShadowBall any place that I use a Ball. So, I can test my new class in any Ball application, such as BallWorld.

We do have one problem. A Ball's color is an instance variable in the Disk class. ShadowBall needs either (1) a way to see its current color and a way to change its own color, or (2) a way to tell a Disk to grow darker. In either case, we will have to add a protected method to the Disk class.

I chose option two. I added a method called darker() to Disk. I called the method darker() to be consistent with the name used in the Color class. Consistency in naming can be a great help to all the programmers who read our code.

    protected void darker()     // in the Disk class
    {
      color = color.darker();
    }

Why did I choose option two instead of option one, which may seem more natural given our programming experience? Whenever possible, we should follow the Take Care of Business principle: Any code that manipulates an object's instance variables should be in the object's own class. This is true even when the code that wants to manipulate those instance variables is in a subclass. A subclass is a client, too.

With that method, writing ShadowBall is straightforward. I used this to emphasize to the reader that the receiver of the paint() message is sending itself a darker() message.

    public void paint( Graphics g )
    {
      super.paint( g );
      paintCount++;                // a new instance variable
    
      if ( paintCount >= 100 )
      {
        this.darker();
        paintCount = 0;
      }
    }

The program runs as we planned, but the ball leaves the window so quickly... Is it really getting darker? We could tell better if it bounced off the walls. A BoundedBall is a Ball, too, so we could have ShadowBall extend BoundedBall and still be a Ball!

Here is a solution in which ShadowBall extends BoundedBall. All we we had to change was the extends clause and the constructor, which must now accept a Frame.

    public class ShadowBall extends BoundedBall
    {
      private int paintCount;
  
      public ShadowBall( int x, int y, int r, int dx, int dy,
                         Frame f )
      {
        super( x, y, r, dx, dy, f );
        paintCount = 0;
      }
      ...
    }

Of course, we also have to change BallWorldFrame to pass itself as an argument when it creates the ShadowBall. More on that in a moment.

The two versions of ShadowBall pose an interesting dilemma. These classes are identical except for an argument to the constructor. How can we create a shadow ball that doesn't duplicate code in this way? Stay tuned. We will learn a neat technique for cutting this Gordian knot soon after the break!



Another Improvement to My Solution

Every time we create a new BallWorld, we have to change the line in the BallWorldFrame class to create the correct kind of Ball. This is a form of duplication, too. It mostly lies outside of our code, by making the programmer do something repeatedly. But we also have duplicated versions of the BallWorldFrame class.

How could the BallWorldFrame class help us?

Our BallWorldFrames all work the same way, regardless of the kind of Ball they use. So let's make the Ball a parameter!

First, let's add a new constructor...

Now, we can create a NewBallWorld that takes a Ball to display...

    Ball           ball  = new Ball(5, 5, 20, 3, 2);
    BallWorldFrame world = new BallWorldFrame( ball );

... and a ShadowBallWorld that takes a ShadowBall (the version that extends Ball, for now) to display.

    Ball           ball  = new ShadowBall(5, 5, 20, 3, 2);
    BallWorldFrame world = new BallWorldFrame( ball );

It works! Now we can clean up the code a bit. We copied the original constructor, pasted it as a new method, and changed one line of code. That leaves duplicated code, of course, but the kind that easy to improve. In the general case, we factor a helper method out of the duplicated code.

Here all the code is the same except for one line embedded in the method. Notice, though, that new BallWorldFrame constructor is more general than the old default constructor. So we could have the new constructor do the work for the old constructor. To do that, the new constructor will pass a default Ball as an argument.

However, we learned in Session 12: that constructors are not like other methods. In particular, we can't send messages that invoke them. Java provides a way for one constructor to invoke another, using a variation on our existing technique and the special variable this:

  public BallWorldFrame()
  {
    this( new Ball( 100, 100, 10, 10, 5 ) );
  }

This is similar to what we do with super. Instead of invoking a constructor in the superclass, using this invokes a constructor in the same class.

That really is much better. We can run the old BallWorld class to be sure that it works. It does! Very nice.

Now let's wrap up this extended exercise by creating a BoundedBallWorld that takes a BoundedBall to display:

    Ball           ball  = new BoundedBall(5, 5, 20, 3, 2, this);
    BallWorldFrame world = new BallWorldFrame( ball );

Uh-oh. On this one, we have a problem.

What might we do to make this work? They involve broadening the public interface of at least two classes...



Inheritance and Java Graphics

Here is a catch phrase that captures how Java graphics work:

Don't call us. We'll call you.

Not only does BallWorldFrame inherit several methods that we use in our code, but many of the methods in Frame send messages the methods for which we implement in BallWorldFrame. Consider:

    public void paint( Graphics g )
    {
        for (int i = 0; i < BallArraySize; i++)
        {
            ballArray[i].paint( g );
            ballArray[i].move();
        }
        ...

        counter = counter + 1;
        if ( counter < 2000 )
           repaint();
        else
           System.exit(0);
    }

No where in our code do we send a paint() message to our BallWorldFrame. The BallWorldFrame does send itself a repaint() message. We can infer, then, that

Most of the control for our application lies in the Frame and Graphics classes provided by the Java AWT. Our application has just enough control to "kick off" the action.

We'll explore the ideas that underlie this style of programming more deeply in the second half of the course.



Who's the Boss?

Object-oriented programming shifts program control from a top-level "main" program into a set of collaborating objects. One object may be responsible for starting the action, or coordinating the action, but no one object is "in charge" -- at least not too much.

This is the design principle we have been following for our programs, and we can see that many parts of the Java standard library follows it, too.

This way of designing a program makes it easier to write and maintain modular code. It also has a second benefit: It makes it easier to write programs that shift control out of the program at all and into the hands of the user.



Event-Driven Programming

This style of programming is called event-driven because it builds program control around one or more events that the user causes.

Think of how a web server works...

The program retains low-level control of how to respond to each kind of event that the user can initiate.

Event-driven programming can also be done in environments with little or no user interaction. In such environments, the events are generated by other programs: objects that generate events to request services. (Think of how an operating system works...)

To write event-driven programs in Java, the programmer defines objects called listeners that wait for and respond to (user-initiated) events.

A listener can be attached to any object capable of generating an event caused by the user:

We can also attach listeners to non-UI components, but we won't write that sort of program this semester.



A Simple First Example

Let's start off with something simple: a BallWorldFrame that contains a Button. Whenever the user presses the button, the balls will move.

Creating a button requires three steps:

The listener waits for the user to press the button and executes its actionPerformed() method whenever it is pressed.

We can add the button to one of five geographical locations in a default frame: north, south, east, west, and center. Notice that the location is passed as a capitalized string.

Notice: The frame does not store its button in an IV. How can that work?

We define our listener class right inside the MultiBallWorldFrame class. Such use of an inner class is standard for listeners. They are an implementation detail of the class, so making them a private part of the class makes a lot of sense. Notice that, because the listener class is defined within the body of MultiBallWorldFrame, the listener object can access the private parts of the frame!



An Exercise: Freeze Tag!

Our simple first example shows how buttons and button listeners work in Java, but it results in a tedious "game": press the button, press the button, press the button, ad infinitum.

How can we make the button an on/off switch for moving?

The idea is this: Press the button once to make the balls move. Press it again, and they stop moving. Press it again, and they start moving again. Press it again, ...

What does our listener have to do now? Our frame?

The frame can keep track of whether the balls are moving or not, and send them a move() when appropriate. All the listener has to do is tell the frame to toggle a ballsAreMoving? variable. Here is one way to do it.

Notice the toggle variable's name. A boolean variable should read well in an if or while statement. This is the second time today that I highlighted the choice of a name. It is hard to overestimate the importance of good names in the design and implementation of a program.



Control in Event-Driven Programs

In this style of programming, the control structure of the program changes from:

Do this, then that, then this other thing.

to:

Respond to the user's action.

This is similar how we want all of our object-oriented programs to work. Objects provide services, interacting by sending one another messages. Control is distributed.



Wrap Up



Eugene Wallingford ..... wallingf@cs.uni.edu ..... October 11, 2012