Lectures
Lecture 1: The Essence of Objects
Lecture 2: Unions of Objects
Lecture 3: Classes of Objects:   Data Definitions
Lecture 4: Classes of Objects:   Interface Definitions
Lecture 5: Interface Design:   Independent and Extensible
Lecture 6: Parametric Interface Definitions and Methods
Lecture 7: Introducing Java:   Syntax and Semantics
Lecture 8: Union, Interfaces, and Lists in Java
Lecture 9: Testing in Java
Lecture 10: Parametric Interfaces in Java
Lecture 11: Computations on Many Structural Arguments:   Double Dispatch
Lecture 12: Parameterized Types and Double Dispatch; Abstracting Values
Lecture 13: Abstracting Computation with Function Objects
Lecture 14: Function Objects & Parameterized Types; Anonymous Classes & Lambda
Lecture 15: The Fundamental List Abstraction:   Fold
Lecture 17: Midterm Review
Lecture 16: Properties of Equality:   Reflexive, Symmetric, Transitive, and Total
Lecture 19: Structural Equality with Double Dispatch; Abstracting and Overridding
Lecture 18: More Double Dispatch
Lecture 22: Optional, Maps, Sets, and Lifting Default Code to Abstract Classes
Lecture 23: The Visitor Pattern
Lecture 24: Implementing Visitors; Bank Accounts
Lecture 25: Imperatives:   Implicit Communication via Side-Effects
Lecture 26: Aside:   List Exercises
Lecture 27: Imperatives:   Cyclic Data
Lecture 28: Imperatives:   Methods over Cylic Data
Lecture 29: BSTs, Maps, The Law of Hash  Code, and Comparable vs Comparators
Lecture 30: Random access and Array  Lists
Lecture 31: Implementing Hash Tables
Lecture 32: Resizing Hash Tables
Lecture 33: Simple Iterators
Lecture 34: List Iterators and Iterator Combinators
Lecture 35: List Iterators and Iterator Combinators
Lecture 36: Zippers
Lecture 37: Naive Tree Iterators
Lecture 38: Efficient Pre-Order Tree Iterators
Lecture 39: Drills
Lecture 40: Drill Solutions
Lecture 41: Wrap-up
On this page:
1 An Exercise:   Developing Lists of Numbers
6.12

Lecture 5: Interface Design: Independent and Extensible

Video.

In this lecture, we explore the interface-based way of defining objects and observe that it enables two important properties of programs:

Let’s consider the alternative characterization of lights not in terms of what they are, but rather what they do that we say in Lecture 5: Interface Design: Independent and Extensible. A light does two things: it can render as an image and it can transition to the next light; hence our interface definition for a light is:

;; A Light implements
;; on-tick : -> Light
;; Next light after this light.
;; to-draw : -> Image
;; Draw this light.

Now it’s clear that each of the three light classes define sets of objects which are Lights, because each implements the methods in the Light interface, but we can imagine new kinds of implementations of the Light. For example, here’s a class that implements the Light interface:

;; A ModLight is a (new mod-light% Natural)
;; Interp: 0 = green, 1 = yellow, otherwise red.
(define-class mod-light%
  (fields n)
  ;; on-tick : -> Light
  ;; Next light after this light.
  (define (on-tick)
    (new mod-light% (modulo (add1 (send this n)) 3)))
 
  ;; draw : -> Image
  ;; Draw this light.
  (define (to-draw)
    (cond [(= (send this n) 0)
           (circle LIGHT-RADIUS "solid" "green")]
          [(= (send this n) 1)
           (circle LIGHT-RADIUS "solid" "yellow")]
          [else
           (circle LIGHT-RADIUS "solid" "red")])))

Notice that every ModLight is a Light. Moreover, any program that is written to use Lights will be compatible with any implemention of the Light interface, regardless of its representation. So notice that the world program only assumes that its light field is a Light; this is easy to inspect—the world never assumes the light is constructed in a particular way, it just calls on-tick and draw. Which means that if we were to start our program off with

(big-bang (new mod-light% 2))

it would work exactly as before.

We’ve now developed a new concept, that of an interface, which is a collection of method signatures. We say that an object is an instance of an interface whenever it implements the methods of the interface.

The idea of an interface is already hinted at in the concept of a union of objects since a function over a union of data is naturally written as a method in each class variant of the union. In other words, to be an element of the union, an object must implement all the methods defined for the union—the object must implement the union’s interface. But interfaces are about more than just unions. By focusing on interfaces, we can see there are two important engineering principles that can be distilled even from this small program:

  1. Representation independence

    As we’ve seen with the simple world program that contains a light, when a program is written to use only the methods specified in an interface, then the program is representation independent with respect to the interface; we can swap out any implementation of the interface without changing the behavior of the program.

  2. Extensibility

    When we write interface-oriented programs, it’s easy to see that they are extensible since we can always design new implementations of an interface. Compare this to the construction-oriented view of programs, which defines a set of values once and for all.

These points become increasingly important as we design larger and larger programs. Real programs consist of multiple interacting components, often written by different people. Representation independence allows us to exchange and refine components with some confidence that the whole system will still work after the change. Extensibility allows us to add functionality to existing programs without having to change the code that’s already been written; that’s good since in a larger project, it may not even be possible to edit a component written by somebody else.

Let’s look at the extensiblity point in more detail. Imagine we had developed the Light data definition and its functionality along the lines of HtDP. We would have (we omit draw for now):

;; A Light is one of:
;; - "Red"
;; - "Green"
;; - "Yellow"
 
;; light-tick : Light -> Light
;; Next light after the given light
(check-expect (light-tick "Green") "Yellow")
(check-expect (light-tick "Red") "Green")
(check-expect (light-tick "Yellow") "Red")
(define (light-tick l)
  (cond [(string=? "Red" l) "Green"]
        [(string=? "Green" l) "Yellow"]
        [(string=? "Yellow" l) "Red"]))

Now imagine if we wanted to add a new kind of light—perhaps to represent a blinking yellow light. For such lights, let’s assume the next light is just a blinking yellow light:

(check-expect (light-tick "BlinkingYellow") "BlinkingYellow")

That’s no big deal to implement if we’re allowed to revise light-tickwe just add another clause to light-tick handle "BlinkingYellow" lights. But what if we can’t? What if light-tick were part of a module provided as a library? Well then life is more complicated; we’d have to write a new function, say fancy-tick, that handled blinking lights and used light-tick for all non-blinking lights. And while that gets us a new function with the desired behavior, that won’t do anything for all the places the light-tick function is used. If we’re able to edit the code that uses light-tick, then we can replace each use of light-tick with fancy-tick, but what if we can’t...? Well then we’re just stuck. If we cannot change the definition of light-tick or all the places it is used, then it is not possible to extend the behavior of light-tick.

Now let’s compare this situation to one in which the original program was developed with objects and interfaces. In this situation we have an interface for lights and several classes, namely red%, yellow%, and green% that implement the on-tick method. Now what’s involved if we want to add a variant of lights that represents a blinking yellow light? We just need to write a class that implements on-tick:

;; Interp: blinking yellow light
(define-class blinking-yellow%
  ;; on-tick : -> Light
  ;; Next light after this blinking yellow light.
  (check-expect (send (new blinking-yellow%) on-tick)
                (new blinking-yellow%))
  (define (next) this))

Notice how we didn’t need to edit red%, yellow%, or green% at all! So if those things are set in stone, that’s no problem. Likewise, programs that were written to use the light interface will now work even for blinking lights. We don’t need to edit any uses of the on-tick method in order to make it work for blinking lights. This program is truly extensible.

Representation independent testing: Our current approach to testing is oriented around the idea of testing for structurally equal representations of values. Testing in a representation independent way requires it’s own technique, which we will see later in the course.

1 An Exercise: Developing Lists of Numbers

A new look at old friend. Let’s look at an object-oriented development of a list of numbers, in particular we will start with an interface-based view of these lists, meaning we will consider the behaviors of a list of numbers (the things we can compute in terms of lists of numbers) rather than their structure.

Let’s start by picking a couple of computations which can be done in terms of a list of numers: length and map.

;; A LoN implements:
;;
;; length : -> Number
;; Compute the length of this list of numbers
;;
;; map : [Number -> Number] -> LoN
;; Apply the given function to each element of this list of numbers

Now we have to think about designing an actual representation of a list of numnbers. Here we just follow the same design (albeit with objects) as we did last semester, using a recursive union data definition:

;; INTERP: Empty list of numbers
(define-class empty-lon%)
 
;; INTERP: Non-empty lists of numbers
(define-class cons-lon%
  (fields first rest))

Using this representation, we can implement the LoN interface by implementing the length and map methods.

Let’s start with empty-lon%. First we should declare our intention that empty-lon% objects implement the LoN interface:

;; A (new empty-lon%) implements LoN
;; INTERP: Empty list of numbers
(define-class empty-lon%)

To fulfill this intention, we must actually define the methods listed in the LoN interface.

Let’s make some examples of what these methods should produce in the case of an empty list of numbers.

Examples:
> (send (new empty-lon%) length)

0

> (send (new empty-lon%) map add1)

(new empty-lon%)

Writing the code for both is now obvious given the examples:

empty-lon%

;; Compute the length of this empty list of numbers
(check-expect (send (new empty-lon%) length) 0)
(define (length)
  0)
 
;; map : [Number -> Number] -> LoN
;; Apply the given function to each element of this empty list of numbers
(check-expect (send (new empty-lon%) map add1) (new empty-lon%))
(define (map f)
  (new empty-lon%))

(Notice how the purpose statements are specialized for the particular class in which we are defining the methods.)

Moving on to cons-lon%, we go through the same steps. Declare our intention that cons-lon% implements LoN and make examples:

;; A (new cons-lon% Number LoN) implements LoN
;; INTERP: Non-empty list of numbers
(define-class cons-lon%
  (fields first rest))

Examples:
> (send (new cons-lon% 3 (new cons-lon% 7 (new empty-lon%))) length)

2

> (send (new cons-lon% 3 (new cons-lon% 7 (new empty-lon%))) map add1)

(new cons-lon% 4 (new cons-lon% 8 (new empty-lon%)))

Since the cons-lon% class contains some data, it is useful to consider the template for cons-lon% methods:

empty-lon%

(define (cons-template ...)
  (send this first) ...
  (send (send this rest) cons-template ...))

Instantiating the template for length and map and adjusting the parameters, we get:

empty-lon%

(define (length)
  (send this first) ...
  (send (send this rest) length))
 
(define (map f)
  (send this first) ...
  (send (send this rest) map f))

Combining our knowledge of the examples and the partially filled in templates, we can see that (send this first) stands for 3 and (send this rest) stands for (new cons-lon% 7 (new empty-lon%)). Let’s make some more examples, based on the list contained in the rest field.

Examples:
> (send (new cons-lon% 7 (new empty-lon%)) length)

1

> (send (new cons-lon% 7 (new empty-lon%)) map add1)

(new cons-lon% 8 (new empty-lon%))

So in the original example, (send (send this rest) length) is 1 and (send (send this rest) map add1) is (new cons-lon% 8 (new empty-lon%)).

Our goal in length is 2, which can be computed by applying add1 to (send (send this rest) length).

Our goal in map is (new cons-lon% 4 (new cons-lon% 8 (new empty-lon%))), which can be computed by applying f to (send this first) and cons that result onto (send (send this rest) map add1). We are now in a position to write the code:

cons-lon%

 ;; length : -> Number
 ;; Compute the length of this non-empty list of numbers
 (check-expect (send (new cons-lon% 3 (new cons-lon% 7 (new empty-lon%)))
                     length)
               2)
 (define (length)
   (add1 (send (send this rest) length)))
 
;; map : [Number -> Number] -> LoN
;; Apply given function to each element of this non-empty list of numbers
(check-expect (send (new cons-lon% 3 (new cons-lon% 7 (new empty-lon%)))
                    map add1)
              (new cons-lon% 4 (new cons-lon% 8 (new empty-lon%))))
(define (map f)
  (new cons-lon%
       (f (send this first))
       (send (send this rest) map f)))

We’ve now accomplished our initial goal. On your own, work out a similar development for lists of strings.