Comp210: Principles of Computing and Programming
Fall 2004 -- Lecture #36   


State Design Pattern

Now that we've done abstract data types with overridden functions (behaviors/methods) and we've also done mutation using set!, let's put it all together:

When modelling real-life systems, we often find that certian objects in our system seem to change "state" during the course of a computation.

Examples of changing state:

  1. A kitten grows up into a cat
  2. A car runs into a telephone pole and becomes a wreck.
  3. A friend is sad one day, happy another, and grumpy on yet another day.
  4. A list changes from empty to non-empty when an element is added.
  5. A fractal becomes more complex when it grows
  6. etc. etc.

The cat and the kitten are the same animal, but they don't act identically. A car can be driven but a wreck cannot--yet they are the same entity fundamentally. Your friend is the same human being, no matter what their mood. Why shouldn't a list be the same list and a fractal be the same fractal?

When something changes state, it is the same object, but yet it behaves differently. This phenomenon of having an objects change its behavior as if it were suddenly belonging to a whole different class of objects is called "dynamic reclassification".

So far we've been using immutable data, and to create a non-empty list from an empty one, required that we make a whole brand-new list. With our use of set! previously, we've changed the value of a placeholder, but never its behavior.

Consider this notion: We want to change the type of the object but we want to encapsulate that change so that the outside world doesn't see the type change, only the behavior change.

Let's work with an example:

Remember the old arcade game, "Frogger"? That's the one where a traffic-challenged amphibian attempts to hop across multiple lanes of speeding cars and trucks, hopefully without being converted into the road-kill-du-jour.

(Here's an on-line version: http://www.gamesgnome.com/arcade/frogger/)

Well, let's look at what a frog is here:

A live frog

On the other hand, a dead frog

Using our trusty separation of variant and invariant, we can see that the position of a frog is an invariant but all the other behaviors are variants. Thus we want to separate out these variants into their own abstract class (structure):

The variant behaviors are represented by the abstract FrogState, with the deadState and liveState overriding its abstract behaviors.

For those variant behaviors, all the main Frog does is to delegate (pass the call on to) to the state that it has. If the state is a liveState, then the Frog will act as if were alive because the liveState only contains live-like behaviors. On the other hand, if the state is a deadState, then the delegation to the state will produce dead-like behavior. The liveState's getHit behavior will cause the Frog's state to change from a liveState to a deadState.

No conditionals are needed!!

The Frog behaves the way it does because of what it is at that moment, not because of what it can figure out about itself then.

This is an example of the State Design Pattern. More information on this design pattern can be found at: http://www.exciton.cs.rice.edu/JavaResources/DesignPatterns/StatePat.htm

So, how do we code this sucker up, anyway?

We start, as always, with the data definition for a Frog:

;; A Frog is a structure that has  two states: live or dead

;; A Frog has 4 behaviors:

;; getPos: --> posn, returns the position of the frog

;; getColor: --> symbol, live Frogs are green, dead frogs are red

;; moveBy: posn --> posn,  moves a live frog by the given amt.  

;;       Dead frogs don't move. The final position is returned.

;; getHit: --> Frog, live frogs become dead.  Dead frogs stay dead.  The frog is returned.

(define-struct Frog (getPos getColor moveBy getHit))

Notice that we really don't want to expose the position as a field in the structure because then it would enable an outside user to set the position, even if the Frog were dead. By the same token, we want the state to be hidden away as well. Thus we want the position hidden away in a closure somewhere, which makes sense, considering that more than one of the behaviors of the Frog needs the position value. Factories to the rescue again:

I have colored each section of the factory below:

;; frogFactory: posn -->Frog

;; Factory to create a live frog at a given position.

(define (frogFactory a-pos)

  (local 

      [(define pos a-pos)  ;; The position of the frog

       

       ;;Abstract state of a Frog. Implements the following state-dependent behaviors:

       ;; getColor: --> symbol   returns the color of the frog

       ;; moveBy: posn --> posn  moves the frog, if possible, by the given amount.  The final position is returned.

       ;; getHit: --> Frog  mutates the frog as if it got hit.

       (define-struct FrogState (getColor moveBy getHit))

       

       (define deadState   ;; The dead state of the frog

         (make-FrogState

          (lambda ()  ;; getColor

            'red)

          (lambda (delta) ;; moveBy

            pos)

          (lambda () ;; getHit

            this)))

       

       (define liveState  ;; The live state of the frog

         (make-FrogState

          (lambda ()  ;; getColor

            'green)

          (lambda (delta) ;; moveBy

            (begin

              (set! pos (make-posn (+ (posn-x pos) (posn-x delta)) 

                                   (+ (posn-y pos) (posn-y delta))))

              pos))

          (lambda ()  ;; getHit

            (begin

              (set! state deadState)

              this))))

       

       (define state liveState)  ;; the current state of the frog

       

       (define this    ;; this allows the frog to reference itself 

         (make-Frog

          (lambda ()  ;; getPos doesn't need to be delegated

            pos)

          (lambda () ;; getColor delegated to the state

            ((FrogState-getColor state)))

          (lambda (delta) ;; moveBy delegated to the state

            ((FrogState-moveBy state) delta))

          (lambda () ;; getHit delegated to the state

            ((FrogState-getHit state)))))]

    this))

From the outside, nothing about the internal implementation of Frog can be seen. All one can see is its public behavior. The implementations of the state design pattern are completely encapsulated. For instance, if one is moving a live Frog, it will dutifully move as directed, but if in the middle somewhere, the Frog is hit, then it will immediately stop moving, no matter how much it is asked to do so. If one is checking its color, the live Frog is a healthy green but right after its accident, it will report that it is deathly red.

Notice the use of set! in the above code. It is used for two purposes:

  1. The position of the Frog is changed by using set! on the local pos placeholder. Note that the input parameter to the factory cannot be used as the position because you cannot set! an input parameter.
  2. The state of the Frog is changed by using set! on the state placeholder. This is what causes the dynamic reclassification to take place.

Notice how the Frog changes its behavior and always behaves correctly for its situation, with no cond statements whatsoever.

Geek-Speak:

The ability for the Frog to use an abstract FrogState and get the concrete behaviors of the deadState and the liveState is an example of polymorphism. Polymorphism is the ability of one enitity (the abstract class here) to be ("morph" into) any of the sub-classes derived from it. Example: Apples, oranges and grapes are all fruit. Polymorphism means that anywhere that you want a fruit, you can use an apple, an orange or a grape, but the what happens exactly will depend on which fruit you use.

The replacing of the functions in FrogState to create the concrete deadState and the liveState is an example of virtual method dispatching, which means that the undefined functions of the abstract FrogState (the "virtual methods") are replaced with concrete functions to which the calls are directed (dispatched) when that function is used. This is in fact exactly how object-oriented systems like C++, Java and C# are implemented under the hood.

Download today's code: frog.scm

 

Looking way back to the beginning of the semester, we now have to ask, "Can we use this technology to create a mutable list? How will this affect the visitors and their execute function?" Stay tuned!

 


Last Revised Tuesday, 24-Aug-2004 13:49:01 CDT

©2004 Stephen Wong and Dung Nguyen