State Pattern


The State pattern appears whenever you have an abstraction that
executes a state machine.

A state machine comprises a set of states and event/action pairs.  When
the machine receives a message (an event happens), the machine does
something (performs some action).  How the machine reacts to the message
is a function of both the event and the current state of the machine.

In this implementation of the pattern, events map to operations of a
state class, and actions to the implementation of the operations.  Each
specific type in the class corresponds to a different connection state.

State transitions are effected by pointing to different state objects
that have different specific types.  Operation invocations dispatch on
the tag of the state object, which causes different actions occur for
the same event.

Implementation

The state machine in this example is a TCP connection, implemented as an
abstract data type whose full view has a pointer to a state object:

  with TCP.States;
  package TCP.Connections is

     type Connection_Type is limited private;

     <connection ops>

  private

     type Connection_Type is ...
       record
          State : State_Access := ...;
       end record;

  end TCP.Connections;


The state type is implemented as limited and indefinite, to control
instance creation:

  package TCP.States is

     type Root_State_Type (<>) is
       abstract tagged limited private;

     type State_Access is access all Root_State_Type'Class;
     ...

  end TCP.States;


In this particular case, the need for this strict kind of enforcement is
less compelling, since clients only use connection types, not state
types.  But, as we shall see, the types that derive from Root_State_Type
are all singletons, and it's nice to have a guarantee of this.

TCP connection operations are implemented by calling the corresponding
(primitive) operations of the state object, which dispatch on the tag.
Some of those invocations may cause a state transition to occur.

In order to allow the operation to change the state object to which the
connection object points, you have to pass the connection object as a
parameter of the state operation.

But state types don't know anything connection types.  Even if they did,
they still don't have access to the representation of connection type,
so how can they change its state object?

What we do is create a sort of forward declaration of the connection
type in the state package, and provide whatever operations are needed by
state types to manipulate connection instances.  It is this type that is
passed as a parameter in state operations, like this:


  package TCP.States is
     ...
     type Root_Connection_Type is                         <--
       abstract tagged limited null record;

     procedure Set_State
       (Connection : in out Root_Connection_Type;
        State      : in     State_Access) is abstract;

     procedure Process_Stream
       (Connection : in Root_Connection_Type;
        Item       : in Stream_Element_Array) is abstract;


     procedure Transmit
       (State      : access Root_State_Type;
        Connection : in out Root_Connection_Type'Class;   <--
        Item       : in     Stream_Element_Array);

     procedure Active_Open
       (State      : access Root_State_Type;
        Connection : in out Root_Connection_Type'Class);  <--
     ...
  end TCP.States;


The Connection_Type (in TCP.Connections) has to be in the
Root_Connection_Type (in TCP.States) class, in order to pass itself as
the connection parameter of state operations.  We therefore implement
Connection_Type as a private derivation from Root_Connection_Type:

    type Connection_Type is limited private;
    ...
  private

    type Connection_Type is
      new Root_Connection_Type with record
         State : State_Access := Get_Default;
      end record;


This is another example of Ada's expressive power: we can implement the
full view of a type as tagged, even though the partial view is not
tagged.  We're using tagged-ness strictly as an implementation
technique, because there's no reason for clients to extend this type.

The implementation of connection operations is now strait-forward:

   procedure Active_Open
     (Connection : in out Connection_Type) is
   begin
      Active_Open (Connection.State, Connection);
   end;

   procedure Passive_Open
     (Connection : in out Connection_Type) is
   begin
      Passive_Open (Connection.State, Connection);
   end;


As we mentioned earlier, the types in the state class are all
implemented as singletons.  Per that idiom, each specific type declares
a singleton instance in the package body, and exports a function that
returns a pointer to that instance:


  package TCP.States.Established is

    type Established_State_Type is new Root_State_Type with private;

    function State return State_Access;



  package body TCP.States.Established is

    Singleton : aliased Established_State_Type;

    function State return State_Access is
    begin
       return Singleton'Access;
    end;


Primitive operations of state type also take access parameters, so that
no explicit dereferencing is necessary.

   procedure Close
     (State      : access Established_State_Type;       <--
      Connection : in out Root_Connection_Type'Class);


This allows operation calls to have a natural syntax:

   procedure Close
     (Connection : in out Connection_Type) is
   begin
      Close (Connection.State, Connection);
   end;

(Connection.State is a pointer to a singleton.)


State type clients change the state of the connection by calling
Set_State, which has a trivial implementation (in TCP.Connections):

   procedure Set_State
     (Connection : in out Connection_Type;
      State      : in     State_Access) is
   begin
      Connection.State := State;
   end;


State type operations that do change the connection state pass the
singleton pointer of some other type in the class:

  with TCP.States.Listen;
  package body TCP.States.Established is

    procedure Close
      (State      : access Established_State_Type;
       Connection : in out Root_Connection_Type'Class) is
    begin
       -- send FIN, receive ACK of FIN

       Set_State (Connection, Listen.State);    <--
    end Close;



  with TCP.States.Established;
  package body TCP.States.Listen is

    procedure Send
      (State      : access Listen_State_Type;
       Connection : in out Root_Connection_Type'Class) is
    begin
       -- send SYN, receive SYN, ACK, etc

       Set_State (Connection, Established.State);    <--
    end Send;



  with TCP.States.Established;
  with TCP.States.Listen;
  package body TCP.States.Closed is

     procedure Active_Open
       (State      : access Closed_State_Type;
        Connection : in out Root_Connection_Type'Class) is
     begin
        -- send SYN, receive SYN, ACK, etc

        Set_State (Connection, Established.State);    <--
     end;


     procedure Passive_Open
       (State      : access Closed_State_Type;
        Connection : in out Root_Connection_Type'Class) is
     begin
        Set_State (Connection, Listen.State);  <--
     end;


Notice how this creates a mutual dependency among all the types in the
state class, although only at the body level.

One thing we need to do is initialize a connection object to a
closed-state default.  We don't want to do this:

    type Connection_Type is
      new Root_Connection_Type with record
        State : State_Access := TCP.States.Closed.State;  <--
      end record;

because that makes TCP.Connections depend on TCP.States.Closed in its
spec.  We want to move the dependency on TCP.States.Closed to the body.

So what we do is declare a function that returns the default state, and
call that during elaboration of connection objects:

   function Get_Default return State_Access;

   type Connection_Type is
     new Root_Connection_Type with record
        State : State_Access := Get_Default;
     end record;

This allows us to move the dependency on TCP.States.Closed to the body:

  with TCP.States.Closed;
  package body TCP.Connections is

    function Get_Default return State_Access renames
      States.Closed.State;

Notice that we implement the body of Get_Default as a renaming of the
selector that returns a pointer to the closed-state singleton.


Matt
<mailto:matthew_heaney@acm.org>

The code below is in a format suitable for use with gnatchop.  Note that
there's no driver program, since the real exercise was how to organize
modules.


--STX
with TCP.States.Closed;

package body TCP.Connections is

   function Get_Default return State_Access renames
     States.Closed.State;


   procedure Set_State
     (Connection : in out Connection_Type;
      State      : in     State_Access) is
   begin
      Connection.State := State;
   end;


   procedure Active_Open
     (Connection : in out Connection_Type) is
   begin
      Active_Open (Connection.State, Connection);
   end;


   procedure Passive_Open
     (Connection : in out Connection_Type) is
   begin
      Passive_Open (Connection.State, Connection);
   end;


   procedure Close
     (Connection : in out Connection_Type) is
   begin
      Close (Connection.State, Connection);
   end;


   procedure Send
     (Connection : in out Connection_Type) is
   begin
      Send (Connection.State, Connection);
   end;


   procedure Acknowledge
     (Connection : in out Connection_Type) is
   begin
      Acknowledge (Connection.State, Connection);
   end;


   procedure Synchronize
     (Connection : in out Connection_Type) is
   begin
      Synchronize (Connection.State, Connection);
   end;


   procedure Process_Stream
     (Connection : in out Connection_Type;
      Item       : in     Stream_Element_Array) is

      use Streams;
   begin
      Write (Connection.File, Item);
   end;


end TCP.Connections;


with TCP.States;
with TCP.Streams;
with Ada.Streams;  use Ada.Streams;

package TCP.Connections is

   pragma Elaborate_Body;


   type Connection_Type is limited private;

   procedure Active_Open
     (Connection : in out Connection_Type);

   procedure Passive_Open
     (Connection : in out Connection_Type);

   procedure Close
     (Connection : in out Connection_Type);

   procedure Send
     (Connection : in out Connection_Type);

   procedure Acknowledge
     (Connection : in out Connection_Type);

   procedure Synchronize
     (Connection : in out Connection_Type);


   procedure Process_Stream
     (Connection : in out Connection_Type;
      Item       : in     Stream_Element_Array);

private

   use States;

   function Get_Default return State_Access;

   type Connection_Type is
     new Root_Connection_Type with record
        State : State_Access := Get_Default;
        File  : Streams.File_Type;
     end record;

   procedure Set_State
     (Connection : in out Connection_Type;
      State      : in     State_Access);

end TCP.Connections;



with TCP.States.Established;
with TCP.States.Listen;

package body TCP.States.Closed is

   Singleton : aliased Closed_State_Type;


   procedure Active_Open
     (State      : access Closed_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      -- send SYN, receive SYN, ACK, etc

      Set_State (Connection, Established.State);
   end;


   procedure Passive_Open
     (State      : access Closed_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      Set_State (Connection, Listen.State);
   end;


   function State return State_Access is
   begin
      return Singleton'Access;
   end;

end TCP.States.Closed;


package TCP.States.Closed is

   pragma Elaborate_Body;


   type Closed_State_Type is new Root_State_Type with private;

   procedure Active_Open
     (State      : access Closed_State_Type;
      Connection : in out Root_Connection_Type'Class);

   procedure Passive_Open
     (State      : access Closed_State_Type;
      Connection : in out Root_Connection_Type'Class);


   function State return State_Access;

private

   type Closed_State_Type is new Root_State_Type with null record;

end TCP.States.Closed;


with TCP.States.Listen;

package body TCP.States.Established is

   Singleton : aliased Established_State_Type;


   procedure Transmit
     (State      : access Established_State_Type;
      Connection : in out Root_Connection_Type'Class;
      Item       : in     Stream_Element_Array) is
   begin
      Process_Stream (Connection, Item);
   end;


   procedure Close
     (State      : access Established_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      -- send FIN, receive ACK of FIN

      Set_State (Connection, Listen.State);
   end Close;



   function State return State_Access is
   begin
      return Singleton'Access;
   end;

end TCP.States.Established;



package TCP.States.Established is

   pragma Elaborate_Body;


   type Established_State_Type is new Root_State_Type with private;

   procedure Transmit
     (State      : access Established_State_Type;
      Connection : in out Root_Connection_Type'Class;
      Item       : in     Stream_Element_Array);

   procedure Close
     (State      : access Established_State_Type;
      Connection : in out Root_Connection_Type'Class);

   function State return State_Access;

private

   type Established_State_Type is
     new Root_State_Type with null record;

end TCP.States.Established;



with TCP.States.Established;

package body TCP.States.Listen is

   Singleton : aliased Listen_State_Type;


   procedure Send
     (State      : access Listen_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      -- send SYN, receive SYN, ACK, etc

      Set_State (Connection, Established.State);
   end Send;


   function State return State_Access is
   begin
      return Singleton'Access;
   end;


end TCP.States.Listen;



package TCP.States.Listen is

   pragma Elaborate_Body;


   type Listen_State_Type is new Root_State_Type with private;

   procedure Send
     (State      : access Listen_State_Type;
      Connection : in out Root_Connection_Type'Class);

   function State return State_Access;

private

   type Listen_State_Type is
     new Root_State_Type with null record;

end TCP.States.Listen;


package body TCP.States is

   procedure Transmit
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class;
      Item       : in     Stream_Element_Array) is
   begin
      null;
   end;


   procedure Active_Open
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      null;
   end;


   procedure Passive_Open
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      null;
   end;


   procedure Close
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      null;
   end;


   procedure Synchronize
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      null;
   end;


   procedure Acknowledge
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      null;
   end;


   procedure Send
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class) is
   begin
      null;
   end;

end TCP.States;



with Ada.Streams;  use Ada.Streams;

package TCP.States is

   pragma Preelaborate;


   type Root_State_Type (<>) is
     abstract tagged limited private;

   type State_Access is access all Root_State_Type'Class;


   type Root_Connection_Type is
     abstract tagged limited null record;

   procedure Set_State
     (Connection : in out Root_Connection_Type;
      State      : in     State_Access) is abstract;

   procedure Process_Stream
     (Connection : in Root_Connection_Type;
      Item       : in Stream_Element_Array) is abstract;


   procedure Transmit
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class;
      Item       : in     Stream_Element_Array);

   procedure Active_Open
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class);

   procedure Passive_Open
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class);

   procedure Close
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class);

   procedure Synchronize
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class);

   procedure Acknowledge
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class);

   procedure Send
     (State      : access Root_State_Type;
      Connection : in out Root_Connection_Type'Class);


private

   type Root_State_Type is
     abstract tagged limited null record;

end TCP.States;



package body TCP.Streams is

   Max_Streams : constant := 10;

   subtype Stream_Count is
     Natural range 0 .. Max_Streams;

   subtype Positive_Stream_Count is
     Stream_Count range 1 .. Stream_Count'Last;

   type Stream_Array is
      array (Positive_Stream_Count) of aliased TCP_Stream_Type;

   Streams      : Stream_Array;
   Streams_Last : Stream_Count := 0;


   procedure Open
     (File : in out File_Type;
      Name : in     String) is
   begin
      Streams_Last := Streams_Last + 1;
      File.Index := Streams_Last;

      null; -- open stream
   end Open;


   procedure Close
     (File : in out File_Type) is
   begin
      null; -- close stream
   end;


   function Get_Stream
     (File : File_Type) return Stream_Access is
   begin
      return Streams (File.Index)'Access;
   end;


   procedure Read
     (File : in     File_Type;
      Item :    out Stream_Element_Array;
      Last :    out Stream_Element_Offset) is

      Stream : TCP_Stream_Type renames
        Streams (File.Index);
   begin
      Read (Stream, Item, Last);
   end;


   procedure Write
     (File : in File_Type;
      Item : in Stream_Element_Array) is

      Stream : TCP_Stream_Type renames
        Streams (File.Index);
   begin
      Write (Stream, Item);
   end;


   procedure Read
     (Stream : in out TCP_Stream_Type;
      Item   :    out Stream_Element_Array;
      Last   :    out Stream_Element_Offset) is
   begin
      null; -- read elements from stream

      Item (Item'Range) := (others => 0);
      Last := 0;
   end;


   procedure Write
     (Stream : in out TCP_Stream_Type;
      Item   : in     Stream_Element_Array) is
   begin
      null; -- write elements into stream
   end;


end TCP.Streams;


with Ada.Streams;  use Ada.Streams;

package TCP.Streams is

   pragma Elaborate_Body;


   type Stream_Access is access all Root_Stream_Type'Class;

   type File_Type is limited private;


   procedure Open
     (File : in out File_Type;
      Name : in     String);

   procedure Close
     (File : in out File_Type);

   function Get_Stream
     (File : File_Type) return Stream_Access;

   procedure Read
     (File : in     File_Type;
      Item :    out Stream_Element_Array;
      Last :    out Stream_Element_Offset);

   procedure Write
     (File : in File_Type;
      Item : in Stream_Element_Array);

private

   type TCP_Stream_Type is
     new Root_Stream_Type with null record; --???


   procedure Read
     (Stream : in out TCP_Stream_Type;
      Item   :    out Stream_Element_Array;
      Last   :    out Stream_Element_Offset);

   procedure Write
     (Stream : in out TCP_Stream_Type;
      Item   : in     Stream_Element_Array);


   type File_Type is
      limited record
         Index : Natural := 0;
      end record;

end TCP.Streams;


package TCP is

   pragma Pure;

end TCP;

Contributed by: Matthew Heaney
Contributed on: May 24, 1999
License: Public Domain

Back