Notes from lecture 3 – Design patterns
1 The visitor pattern
2 The state pattern

Notes from lecture 3 – Design patterns

As you must have noticed already, the language you use shapes the way you write code. In particular, it makes it easy to write in one way and harder in another pushing you to adapt your programming. This becomes a prickly issue when you try to extend a software system along a dimension that is not well-supported by the constructs you used in your language.

For example consider the following simple program written in the functional subset of Racket:

(struct circle (radius color))
(struct square (side color))
(define (color? c) (or (equal? c 'red) (equal? c 'blue) (equal? c 'green)))
(define (surface a-shape)
  (cond [(circle? a-shape) (* pi (sqr (circle-radius a-shape)))]
        [(square? a-shape) (sqr (square-side a-shape))]))
(define my-circle (circle 10 'green))
(define my-square (square 10 'red))

The program would look similar, give or take stylistic differences, in pretty much any other functional language, such as Haskell.

The program starts with the data definition for a shape that is either a circle or a square, and proceeds with the definition of a function surface followed by a couple of examples of shapes. We can pass the examples to surface to get the surface of either shape:
> (surface my-circle)

314.1592653589793

> (surface my-square)

100

It is pretty easy to extend the above program with additional operations. All it takes is adding another function definition such as get-color:
(define (get-color a-shape)
  (cond [(circle? a-shape) (circle-color a-shape)]
        [(square? a-shape) (square-color a-shape)]))
> (get-color my-circle)

'green

> (get-color my-square)

'red

The need for such non-local cases is evidence of the expressiveness limitations of a programming language.

However, if we restrict ourselves to a functional style of programming, adding new kind of shapes to the program (a different dimension of extensibility) is more involved than simply adding another function. It requires adding a new struct definition and, more annoyingly, adding cases for the new shape in the data definition for shapes and all functions that consume shapes. In other words such an extension requires non-local changes to our code and changes to existing code.

In contrast to that design of shapes, other programming languages and programming styles may allow easy extensions along the data-variance dimension (such as adding a new kind of shape) while making it harder to extend by adding new operations. For instance, consider the above program re-phrased in the class-based/object-oriented subset of Racket:
(define shape<%>
  (interface () surface))
(define circle%
  (class* object% (shape<%>)
    (super-new)
    (init-field radius)
    (define/public (surface)
      (* pi (sqr radius)))))
(define square%
  (class* object% (shape<%>)
    (super-new)
    (init-field side)
    (define/public (surface)
      (sqr side))))
(define my-circle (new circle% [radius 10]))
(define my-square (new square% [side 10]))

The program would look similar, give or take stylistic differences, in pretty much any other class-based language such as Java.

Here the program starts with the definition of the shape<%> interface that ensures the presence of the method surface. Given the definitions of these two classes we can instantiate objects for each one and invoke surface on them to get the surface of the corresponding shape:
> (send my-circle surface)

314.1592653589793

> (send my-square surface)

100

Adding more data variants in this setting is very easy. All it takes is adding a new class that implements the shape interface without touching the rest of the code:
(define composite%
  (class* object% (shape<%>)
    (super-new)
    (init-field shape-1 shape-2)
    (define/public (surface)
      (+ (send shape-1 surface) (send shape-2 surface)))))
(define my-composite
  (new composite% [shape-1 my-circle] [shape-2 my-square]))
> (send my-composite surface)

414.1592653589793

However adding new operations in this setting requires changes that touch multiple pieces of the code; it requires adding a new method in the interface and every class that implements it.

Software engineers observed early these expressiveness limitations of the programming languages they used and came up with systematic ways to engineer/organize their code to provide the extensibility points they need. Such engineering solutions are known in the context of software engineering as design patterns and the Gang of Four book provided their first systematic collection and categorization, akin to a technical handbook in other engineering disciplines.

Design patterns are not specific to a language but rather language-agnostic “idioms” for successfully arranging code elements such as classes and objects to solve specific facets of the extensibility problem. Their success claim is based on empirical “archaeological” evidence from long-living evolving projects. Over the years they have become an integral part of the vocabulary of every software engineer.

1 The visitor pattern

Let’s see how design patters can help us solve the functionality extensibility issue of the class-based code above. The high-level idea is that we are looking for a way to reorganize the code so that we can add functionality to classes without having to modify our class hierarchy.

As the Gang of Four book explains, the visitor pattern offers a solution to exactly this problem. Roughly, the way the visitor works is that we factor the traversal of some data structure (like shapes) into two parts: the part that knows what we want to do and the part that knows where to go. Here is how it works.

First we add the accept method to the shape<%> interface:
(define shape<%>
  (interface ()
    accept))
This method will play the generic entry point for invoking functionality on all objects that are instances of classes that implement the shape<%> interface. The specific functionality that accept invokes is determined by its argument, an object of the so-called visitor% class that comes with a method for every shape class. Concretely, the definitions of circle% and square% contain the definition of their accept method that simply invokes the corresponding method, visit-circle and visit-square, of its visitor argument v:

(define circle%
  (class* object% (shape<%>)
    (super-new)
    (init-field radius color)
    (define/public (accept v)
      (send v visit-circle this))))
(define square%
  (class* object% (shape<%>)
    (super-new)
    (init-field side color)
    (define/public (accept v)
      (send v visit-square this))))
(define my-circle (new circle% [radius 10] [color 'green]))
(define my-square (new square% [side 10] [color 'red]))
The methods visit-circle and visit-square are now the hooks where we can add the functionality that previously was part of each shape class’s method. For instance, if we want to equip our shapes with a surface computation, we can define a surface-visitor% whose methods compute the surface for each shape:
(define surface-visitor%
  (class object% (super-new)
    (define/public (visit-circle c)
      (* pi (sqr (get-field radius c))))
    (define/public (visit-square s)
      (sqr (get-field side s)))))

In reality the visitor object is nothing but a substitute for a λ-function...

To obtain the surface of a shape object all we need to do is invoke its accept method and pass it an instance of the surface-visitor%:
> (send my-circle accept (new surface-visitor%))

314.1592653589793

> (send my-square accept (new surface-visitor%))

100

Now if we want to add functionality to obtain the color of a shape all we have to do is define a new visitor and leave all the shape classes unchanged:
(define get-color-visitor%
  (class object% (super-new)
    (define/public (visit-circle c) (get-field color c))
    (define/public (visit-square s) (get-field color s))))
> (send my-circle accept (new get-color-visitor%))

'green

> (send my-square accept (new get-color-visitor%))

'red

While the visitor pattern provides some nice improvements, consider what happens if you want to add a new kind of shape and then a new operation on shapes. Is one step easier than the other? What does this reveal about the limitations of the visitor pattern?

2 The state pattern

In a board game, at each point in a game the game admin can ask a player to perform only a specific action. For instance, the game admin has to first ask the player to register, and after that to receive its token and, only then to make a move. The game admin has to be careful to follow this protocol.

The state pattern helps us structure code to remove the problem of having to rely on the game admin doing things in the right order. This is an instance of a general common issue that is not straight-forward to achieve with classes and objects. In many cases, an object has to behave as if it is an instance of a class that offers some functionality and some others an instance of a class that offers some other functionality. Concretely, if we represent the actions of a player with a class that implements the action<%> interface at a given point, objects of this class should enable to register or to receive a token or to make a move depending on the order of actions so far – but not both at the same time. In other words, the action<%> interface behaves as the interface of an automaton that is either in the register or the receive-token or the make-a-move state and switches between the two with each action of the player.

Using the state pattern, to implement such an interface we include in it a generic method act:
(define action<%>
  (interface ()
    act))
Then we define separate classes for each different action, i.e, register%, receive-token%, and make-a-move%, that implement act:
(define register%
  (class* object% (action<%>)
    (super-new)
    (define/public (act player args container)
      (begin0
        '(fake-retrieving-the-player-name)
        (send container next (new receive-token%))))))
(define receive-token%
  (class* object% (action<%>)
    (super-new)
    (define/public (act player args container)
      (begin0
        '(fake-telling-the-player-a-game-has-started)
        (send container next (new make-a-move%))))))
(define make-a-move%
  (class* object% (action<%>)
    (super-new)
    (define/public (act player args container)
      (begin0
        '(fake-asking-the-player-to-choose-a-next-move)
        (send container next (new make-a-move%))))))
The act method of these classes implements the functionality for either register or receive-stones or make-a-move and in addition sends a message next to an object that keeps track of the next allowed action. That latter object is an instance of the action-container% that has a field storing the next action, and the methods next and act from above. Specifically, the latter invokes act on the next-action object:
(define action-container%
  (class* object% (action<%>)
    (super-new)
    (define next-action (new register%))
    (define/public (next an-action)
      (set! next-action an-action))
    (define/public (act player args)
      (send next-action act player args this))))
Now our referee can instantiate action-container% and invoke act on the resulting object without having to worry whether it is time to make a move or to build. The pattern will take care of that by preserving the automaton-like invariant by construction:
> (define my-action (new action-container%))
> (send my-action act "a-player" '())

'(fake-retrieving-the-player-name)

> (send my-action act "a-player" '(this-is-not-really-a-token))

'(fake-telling-the-player-a-game-has-started)

> (send my-action act "a-player" '(this-is-not-really-a-board))

'(fake-asking-the-player-to-choose-a-next-move)