On this page:
1 The Perils of Inheritance
2 Favor Composition Over Inheritance
3 Prepare and Document for Inheritance
4 Abstract Classes

Inheritance

Last updated: Thu, 23 Apr 2015 14:45:16 -0400

NOTE: The following article is an adaptation of Chapter 4 of [EffectiveJava].

1 The Perils of Inheritance

Imagine we have a Set% class that implements the following interface:

(define Set<%>
  (interface ()
    ; add! : Any -> Void
    ; EFFECT: adds the given value to the set if it's not already there
    add!
 
    ; add-all! : ListOf<Any> -> Void
    ; EFFECT: adds the given values to the set if they are not already there
    add-all!
 
    ; get-elem : -> ListOf<Any>
    ; Returns the elements of the set in any order as a list.
    get-elems))

Now imagine we want to extend Set% with a count of the attempted adds.

One approach is to use inheritance.

(define InstrumentedSet%
  (class* Set% (Set<%>)
    (init-field [count 0]) ; Natural, count of the number of attempted set adds
    ; May differ from number of set elements due to dups
 
    ; inc-count! : -> Void
    ; EFFECT: increments the add counter by n
    (define (inc-count! [n 1]) (set! count (+ count n)))
 
    ; add! : Any -> Void
    ; EFFECT: adds the given value to the set if it's not already there
    ;         and increments the count
    (define/override (add! x)
      (inc-count!)
      (super add! x))
 
    ; add-all! : ListOf<Any> -> Void
    ; EFFECT: adds the given values to the set if they are not already there
    ;         and increments the count by the length of the list
    (define/override (add-all! xs)
      (inc-count! (length xs))
      (super add-all! xs))
 
    ; get-count : -> Natural
    (define/public (get-count) count)
 
    (super-new)))

In Racket’s object system, define/override overrides method implementations and super invokes methods in a class’s superclass.

However, the InstrumentedSet% class does not work.

> (define iset (new InstrumentedSet%))
> (send iset add-all! (list 10 10 20))
> (send iset get-elems)

'(20 10)

> (send iset get-count)

6

Somehow the adds are getting counted twice.

The reason is because InstrumentSet% depends on its super class, which is implemented as:

(define Set%
(class* object% (Set<%>)
  (init-field [elems empty]) ; ListOf<Any>, with no duplicate elements
 
  ; add! : Any -> Void
  ; EFFECT: adds the given value to the set if it's not already there
  (define/public (add! x)
    (unless (member x elems)
      (set! elems (cons x elems))))
 
  ; add-all! : ListOf<Any> -> Void
  ; EFFECT: adds the given values to the set if they are not already in the set
  (define/public (add-all! xs)
    (for-each (λ (x) (send this add! x)) xs))
 
  ; get-elem : -> ListOf<Any>
  ; Returns the elements of the set in any order as a list.
  (define/public (get-elems) elems)
 
  (super-new)))

The add-all! in InstrumentedSet% calls add-all! in the Set% superclass. However, add-all! in Set% then calls add! in on this, but this is an instance of InstrumentedSet%, so InstrumentedSet%’s add! gets called and the adds are counted twice.

This example illustrates some pitfalls of inheritance. Inheritance breaks encapsulation and leads to fragile code because InstrumentedSet%’s behavior if altered by changes to another class Set%. Thus, to use inheritance, at a minimum both the superclass and inheriting class should be accessible by changeable by the same programmer.

Inheritance also breaks modularity and local reasoning because the functionality of InstrumentedSet% is spread over two classes. To change the behavior of InstrumentedSet%, two classes must be edited.

2 Favor Composition Over Inheritance

When possible, you should use composition over inheritance. This way, the extended set does not rely on any implementation details of the base Set% class. All interaction is accomplished through interface.

Concretely, the desired Set% extension could be implemented using the Decorator pattern, in other words a wrapper class.

> (define SetDecorator%
    (class* object% (Set<%>)
      (init-field [elems (new Set%)] ; Set%
                  [count 0])         ; Natural, count of the number of attempted set adds
  
      ; inc-count! : -> Void
      ; EFFECT: increments the add counter by n
      (define (inc-count! [n 1]) (set! count (+ count n)))
  
      ; add! : Any -> Void
      ; EFFECT: adds the given value to the set if it's not already there
      ;         and increments the count
      (define/public (add! x)
        (inc-count!)
        (send elems add! x))
  
      ; add-all! : ListOf<Any> -> Void
      ; EFFECT: adds the given values to the set if they are not already there
      ;         and increments the count by the length of the list
      (define/public (add-all! xs)
        (inc-count! (length xs))
        (send elems add-all! xs))
  
      ; get-elem : -> ListOf<Any>
      ; Returns the elements of the set in any order as a list.
      (define/public (get-elems) (send elems get-elems))
  
      ; get-count : -> Natural
      (define/public (get-count) count)
  
      (super-new)))
> (define setd (new SetDecorator%))
> (send setd add-all! (list 10 10 20))
> (send setd get-elems)

'(20 10)

> (send setd get-count)

3

3 Prepare and Document for Inheritance

To avoid inheritance problems, a class should prepare and document exactly how it should be inherited and how its methods should be overridden.

For example, the InstrumentedSet% class did not work because we did not know that add-all! is implemented in terms of add!. The proper way to use inheritance in that case would have been to override add-all! but not add!. The Set% class should have documented this and written something "only one of add! or add-all! should be overridden. (Of course, this does not solve the problem that InstrumentedSet% depends on implementation details of Set%, but at least this way InstrumentedSet% would behave as intended.)

However, note that allowing inheritance puts constraints on how the class may be changed in the future. This is another example of how inheritance breaks encapsulation. Often, it’s better to simply disallow inheritance of class. Different languages have different means of doing so. In Racket declaring a method with define/public-final disallows overriding.

4 Abstract Classes

Thus far, we’ve only warned against not using inheritance. But is inheritance useful for anything? The answer is yes. Inheritance is useful for abstracting away duplicate code in class methods, when used in conjunction with an interface. It’s important to always communicate with an object through an interface. But it’s sometimes helpful to have a abstract class that accompanies the interface in order to provide default or boilerplate method implementations.

For example, in your shapes program from Problem Set 11 and Problem Set 12, You probably noticed many common methods between shapes. An abstract class can help with this. For example, you may define an abstract class with a field that represents the center of the shape, and a getter method for this field. Then each class that inherits from the abstract class does not need to implement this method.

> (define Shape<%>
    (interface ()
      ; get-center : -> posn
      ; Returns the center posn of the shape.
      get-center))
; abstract shape class
> (define Shape%
    (class* object% (Shape<%>)
      (init-field center) ; posn representing the center of the shape
      (define/public (get-center) center)
      (super-new)))
> (define Circle%
    (class* Shape% (Shape<%>)
      (inherit-field center) ; posn representing the center of this circle
      (super-new)))
> (send (new Circle% [center (make-posn 0 0)]) get-center)

(posn 0 0)

Even better, you can use the abstract to partially implement a method, and specify where subclasses are required to fill in the remaining parts of the implementation. This is a useful pattern called the Template and Hook pattern .

For example, every shape has a similar mouse handler. They only differ in details of the handler.

(define Shape<%>
  (interface ()
    ; get-center : -> posn
    ; Returns the center posn of the shape.
    get-center
    ; handle-mouse : Coord Coord MouseEvent -> Shape<%>
    handle-mouse))
 
; abstract shape class
(define Shape%
  (class* object% (Shape<%>)
    (init-field center) ; posn representing the center of the shape
 
    ; get-center : -> posn
    ; Returns the center posn of the shape.
    (define/public (get-center) center)
 
    ; handle-mouse : Coord Coord MouseEvent -> Shape<%>
    ; Computes a new shape in response to a mouse event.
    (define/public (handle-mouse x y mev)
      (cond
        [(mouse=? mev "button-down") (handle-button-down x y)]
        ...))
 
    ; handle-button-down : Coord Coord -> Shape<%>
    ; Computes a new shape in response to a mouse event.
    (define (handle-button-down x y)
      (if (in-shape? x y)
          (start-move x y)
          (if (on-ctrl-pt? x y)
              (start-resize x y)
              this)))
 
    (abstract
     ; in-shape : Coord Coord -> Boolean
     ; Returns true if the given coordinates are inside the shape.
     in-shape?
     ; on-ctrl-pt? : Coord Coord -> Boolean
     ; Returns true if the given coordinates are on perimeter control point.
     on-ctrl-pt?
     ; start-move : Coord Coord -> Shape<%>
     ; Starts a translation of this shape.
     start-move
     ; start-resize : Coord Coord -> Shape<%>
     ; Starts a resizing of this shape.
     start-resize)
 
    (super-new)))
 
(define Circle%
  (class* Shape% (Shape<%>)
    (inherit-field center) ; posn representing the center of this circle
 
    ; in-shape? : Coord Coord -> Boolean
    ; Returns true if the given coordinates are inside this circle.
    (define/override (in-shape? x y) ...)
 
    ; on-ctrl-pt? : Coord Coord -> Boolean
    ; Returns true if the given coordinates are on perimeter of the circle.
    (define/override (on-ctrl-pt? x y) ...)
 
    ; start-move : Coord Coord -> Shape<%>
    ; Starts a translation of this circle.
    (define/override (start-move x y) ...)
 
    ; start-resize : Coord Coord -> Shape<%>
    ; Starts a resizing of this circle
    (define/override (start-resize x y) ...)
 
    (super-new)))

Again, different languages have different ways to implement abstract classes. In Racket, abstract methods that subclasses must implement are declared with abstract. In the shapes example, the mouse-handler and button-down-handler methods always have the same structure. They differ in the functions that determine whether a coordinate is inside or on the edge of a shape, so the abstract class declares that each subclass must implement in-shape? and on-ctrl-pt?.