10.2.2 Overriding methods

In addition to adding new methods, subclasses can replace the definitions of methods defined in the superclass. When a subclass replaces a method defined by its superclass, then the subclass method the superclass method. When the method is invoked on a subclass object, the new method will be used.

For example, we can define a subclass of reversible-counter that is not allowed to have negative counter values. If the counter would reach a negative number, instead of setting the counter to the new value, it produces an error message and maintains the counter at zero. We do this by overriding the set-count! method, replacing the superclass implementation of the method with a new implementation.

(define (make-positive-counter)
  (make-subobject
   (make-reversible-counter)
   (lambda (message)
     (cond
      ((eq? message 'set-count!)
       (lambda (self val) (if (< val 0) (error "Negative count")
                                        ...)))
      (else false)))))

What should go in place of the $\ldots$? When the value to set the count to is not negative, what should happen is the count is set as it would be by the superclass set-count! method. In the positive-counter code though, there is no way to access the count variable since it is in the superclass procedure's application environment. There is also no way to invoke the superclass' set-count! method since it has been overridden by positive-counter.

The solution is to provide a way for the subclass object to obtain its superclass object. We can do this by adding a get-super method to the object produced by make-sub-object:

(define (make-sub-object super subproc)
  (lambda (message)
    (if (eq? message 'get-super)
        (lambda (self) super)
        (let ((method (subproc message)))            
          (if method method (super message))))))

Thus, when an object produced by make-sub-object is passed the get-super message it returns a method that produces the super object. The rest of the procedure is the same as before, so for every other message it behaves like the earlier make-sub-object procedure.

With the get-super method we can define the set-count! method for positive-counter, replacing the $\ldots$ with:

(ask (ask self 'get-super) 'set-count! val))

Figure 10.3 shows the subclasses that inherit from counter and the methods they define or override.

Figure 10.3: Counter class hierarchy.

Consider these sample interactions with a positive-counter object:

> (define poscount (make-positive-counter))
> (ask poscount 'next!)
> (ask poscount 'previous!)
> (ask poscount 'previous!)
Negative count
> (ask poscount 'get-count)
0

For the first ask application, the next! method is invoked on a positive-counter object. Since the positive-counter class does not define a next! method, the message is sent to the superclass, reversible-counter. The reversible-counter implementation also does not define a next! method, so the message is passed up to its superclass, adjustable-counter. This class also does not define a next! method, so the message is passed up to its superclass, counter. The counter class defines a next! method, so that method is used.

For the next ask, the previous! method is invoked. Since the positive-counter class does not define a previous! method, the message is sent to the superclass. The superclass, reversible-counter, defines a previous! method. Its implementation involves an invocation of the adjust! method: (ask self 'adjust! -1). This invocation is done on the self object, which is an instance of the positive-counter class. Hence, the adjust! method is found from the positive-counter class implementation. This is the method that overrides the adjust! method defined by the adjustable-counter class. Hence, the second invocation of previous! produces the "Negative count" error and does not adjust the count to -1.

The property this object system has where the method invoked depends on the object is known as dynamic dispatch. The method used for an invocation depends on the self object. In this case, for example, it means that when we inspect the implementation of the previous! method in the reversible-counter class by itself it is not possible to determine what procedure will be applied for the method invocation, (ask self 'adjust! -1). It depends on the actual self object: if it is a positive-counter object, the adjust! method defined by positive-counter is used; if it is a reversible-counter object, the adjust! method defined by adjustable-counter class (the superclass of reversible-counter) is used.

Dynamic dispatch provides for a great deal of expressiveness. It enables us to use the same code to produce many different behaviors by overriding methods in subclasses. This is very useful, but also very dangerous --- it makes it impossible to reason about what a given procedure does, without knowing about all possible subclasses. For example, we cannot make any claims about what the previous! method in reversible-counter actually does without knowing what the adjust! method does in all subclasses of reversible-counter.

The value of encapsulation and inheritance increases as programs get more complex. Programming with objects allows a programmer to manage complexity by hiding the details of how objects are implemented from what those objects represent and do.

Exercise 10.3. Define a countdown class that simulates a rocket launch countdown: it starts at some initial value, and counts down to zero, at which point the rocket is launched. Can you implement countdown as a subclass of counter?

Exercise 10.4. Define the variable-counter object from Exercise 10.2 as a subclass of counter.

Exercise 10.5. Define a new subclass of parameterizable-counter where the increment for each next! method application is a parameter to the constructor procedure. For example, (make-parameterizable-counter 0.1) would produce a counter object whose counter has value 0.1 after one invocation of the next! method.