Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

State Machine - State Pattern vs. Classic Approach

5.00/5 (7 votes)
5 Aug 2014CPOL7 min read 35.6K   227  
State pattern and procedural solution illustrated

Table of Contents

  1. Introduction
  2. Background
  3. Using the Code
  4. Classic State Machine Implementation
  5. State Pattern Implementation
  6. Appendix
  7. Output
  8. Footnotes
  9. History

1 Introduction

Recently, I was reading a book about design patterns [1]. I was especially interested in implementing a state machine in an elegant manner. Elegant in terms of being easily extendable, maintainable and testable. A common pitfall is for instance when one is tempted to do copy and paste of code fragments. If the copied part is not updated to suit the new context, bugs are introduced. Even though the code fragment works well in its original context. I will outline this later in the article.

The book illustrated a better way of implementation by using inheritance and polymorphism. But still, I was surprised by the object dependencies introduced by the design. Based on the given state pattern in the book, I reduced the dependencies and would like to present the result in this article to open a discussion on the pros and cons.

In a previous version of this article, I made an silly but interesting mistake, pointed out by Member 10630008. More about it is placed at a suitable place below.

I also read the Code Project article of Thomas Jaeger which covers the same subject [2]. I think he inspired me to use a door example to illustrate a state machine.

2 Background

Basic object oriented programming skills and knowledge of inheritance, polymorphism and interfaces might be helpful to follow the concept.

3 Using the Code

The code was written as console application in Visual Basic .NET, using Visual Studio 2008. As stated, I chose to use a door example. A door can be in open, closed or locked state. The transitions to switch from one state into the other are - under certain restrictions - opening, closing, locking and unlocking the door.

Image 1

The initial state, illustrated by the black circle is, of course, at your preference.

4 Classic State Machine Implementation

The following figure shows the class design of the classic approach. Please focus on the class DoorTypeTraditional:

Image 2

The base class DoorTestBase is to simplify and unify the test in the main module only. If both classes to test derive from it and therefore implement its interface IDoorTestActions, they can be tested with the same function: MainModule.TestDoor.

4.1 Class Traditional

Following is the source code to sketch the classic approach.

File: DoorTypeTraditional.vb

This class is using enumerations of states and enumerations of their transition actions. The logic is implemented in method DoAction with a Select Case statement. Depending on the current state, allowed actions are filtered to transit to the next state.

VB.NET
'
' Door State Machine Example
'   to demonstrate "traditional" implementation of a 
'   state machine compared to state pattern implementation
'   in Visual Basic .NET
'

''' <summary>
''' The door object.
''' A state machine implemented with a switch statement and enumerations
''' </summary>
''' 
''' <remarks>
''' Steps to extend this solution with further states and transitions:
''' 1) add state to enum DoorState
''' 2) add transitions to enum DoorTransition
''' 3) extend switch statement in method DoAction() with 
'''    a) case for new state and 
'''    b) if-else condition for new transitions
''' 
''' </remarks>
Public Class DoorTypeTraditional
    Inherits DoorTestBase

    Public Enum DoorState
        DoorClosed
        DoorOpened
        DoorLocked
    End Enum

    Public Enum DoorTransition
        CloseDoor
        OpenDoor
        LockDoor
        UnlockDoor
    End Enum

    Private CurrentState_ As DoorState

    Public Sub New()
        CurrentState_ = DoorState.DoorClosed
    End Sub

    Public Sub New(ByVal initialState As DoorState)
        CurrentState_ = initialState
    End Sub

    Public Overrides Function ToString() As String
        Return CurrentState_.ToString()
    End Function

#Region "state transition methods"
     Private Sub DoAction(ByVal action As DoorTransition)
        Dim throwInvalidTransition As Boolean = False

        Select Case CurrentState_
            Case DoorState.DoorClosed
                If action = DoorTransition.OpenDoor Then
                    CurrentState_ = DoorState.DoorOpened
                ElseIf action = DoorTransition.LockDoor Then
                    CurrentState_ = DoorState.DoorLocked
                Else
                    throwInvalidTransition = True
                End If

            Case DoorState.DoorLocked
                If action = DoorTransition.UnlockDoor Then
                    CurrentState_ = DoorState.DoorClosed
                Else
                    throwInvalidTransition = True
                End If

            Case DoorState.DoorOpened
                If action = DoorTransition.CloseDoor Then
                    CurrentState_ = DoorState.DoorClosed
                Else
                    throwInvalidTransition = True
                End If

            Case Else
                Throw New Exception("invalid state")
        End Select

        If throwInvalidTransition Then
            Throw New Exception("state transition '" & action.ToString & "' not allowed")
        End If

    End Sub

    Public Overrides Sub TryOpen()
        DoAction(DoorTransition.OpenDoor)
    End Sub

    Public Overrides Sub TryClose()
        DoAction(DoorTransition.CloseDoor)
    End Sub

    Public Overrides Sub TryLock()
        DoAction(DoorTransition.LockDoor)
    End Sub

    Public Overrides Sub TryUnlock()
        DoAction(DoorTransition.UnlockDoor)
    End Sub

#End Region

End Class

4.2 Advantages

For a simple state machine, e.g., as the given example, the traditional method is sufficient.

  • Everything is in one, clearly represented source file
  • One can assume that the execution is fast, since few memory allocations are involved

4.3 Disadvantages

If - as it is usually the case - the number of states and transitions increase over time, the whole design becomes quickly very complex. For instance, could it be of interest one day, to distinguish between an inside and outside opened door. It could become necessary to know if the door is fully opened or half opened.

  • At least three places have to be maintained when states or transitions have to be added:
    1. Enum DoorState
    2. Enum DoorTransition
    3. Sub DoAction()

    Likely for changes is also the interface IDoorTestActions.

  • The DoAction method will become quickly confusing, i.e., unclear.
  • When adding states, one is tempted to copy and paste existing state blocks which can easily introduce new bugs: e.g., variable names are left unchanged or transitions are omitted.

Back To Top

5 State Pattern Implementation

The class design of this state pattern is as follows:

Please focus on the main class DoorStatePatternBase and its derivatives:

DoorOpened, DoorClosed, DoorLocked.

Image 3

Again, please note that the base class DoorTestBase solely exists to simplify and unify the test in the main module. All objects to test should derive from DoorTestBase. This will force implementation of its interface IDoorTestActions. So those objects can be tested with the same function: MainModule.TestDoor().

As you can see, this is done with the class DoorTypePattern. Objects of DoorTypePattern have a state which is implemented as DoorStatePatternBase. Objects of DoorTypePattern derive from DoorTestBase in order to be tested in the same manner by MainModule.Testdoor().

5.1 Key Features of this State Pattern

  • Each state is implemented in its own class.
  • Only valid transitions have to be implemented in each state class.
  • Each state class is a singleton.
  • DoorStatePatternBase does not need to know anything about its owner DoorTypePattern.

    The new states are returned by the transition functions.

5.2 Class StatePatternBase

Following is the source code to sketch this version of the state pattern.

File: DoorStatePatternBase.vb

This is the base class of the pattern design. Each state has its own class which has to derive from DoorStatePatternBase.

VB.NET
' file DoorStatePatternBase.vb 
'
' Door State Machine Example
'   to demonstrate state pattern in Visual Basic .NET
'

''' <summary>
''' State machine implemented with inheritance and polymorphism.
''' This is the base class of a door state machine.
''' A door can have three states: opened, closed, locked.
''' The locked state is only valid, if the door is closed. Otherwise locking 
''' is not possible. The state transitions are open, close, lock.
''' </summary>
''' 
''' <remarks>
''' Derive from this class for each state an override only the valid 
''' transitions from each state.
''' </remarks>
Public MustInherit Class DoorStatePatternBase

#Region "possible state transitions"
     ' If these methods are not overridden by the state classes
    ' an exception is thrown about invalid transition from that state
    ' The methods are prefixed by "Do", e.g. DoCloseDoor(), to be easily
    ' distinguishable by the interface routines for testing.
    ' refer to StateMachineObjBase

    Public Overridable Function DoCloseDoor() As DoorStatePatternBase
        Throw New Exception("state transition 'CloseDoor' not allowed")
        Return Me
    End Function

    Public Overridable Function DoLockDoor() As DoorStatePatternBase
        Throw New Exception("state transition 'LockDoor' not allowed")
        Return Me
    End Function

    Public Overridable Function DoOpenDoor() As DoorStatePatternBase
        Throw New Exception("state transition 'OpenDoor' not allowed")
        Return Me
    End Function

    Public Overridable Function DoUnlockDoor() As DoorStatePatternBase
        Throw New Exception("state transition 'UnlockDoor' not allowed")
        Return Me
    End Function

    '
    ' Add new transitions here
    '

#End Region

End Class

5.3 Advantages

This design scales easily with the complexity of states and transitions. When extending the solution, only two minor changes are required:

  1. The class DoorStatePatternBase requires adding the new transitions as overridable. This is typically done by copying and pasting an existing transition and by updating to the new function name as well as updating the exception message. The only possible mistake is to forget to update the exception text. Which won't have any influence on the correctness of already implemented states!

    VB.NET
    ' new transition  
    Public Overridable Function OpenDoorInside() As DoorStatePatternBase
        Throw New Exception("state transition 'OpenDoorInside' not allowed") 
        Return Me
    End Function 
  2. New states have to be implemented, each with its own class implementation inherited from DoorStatePatternBase. Those state classes contain the logic for the transitions between states. They effectively replace the Select-Case statement of the classic approach.
    VB.NET
    Public Class DoorOpened ' 1st, updated name DoorClosed->DoorOpened
        Inherits DoorStatePatternBase
    
        Private Shared Singleton_ As DoorStatePatternBase = Nothing
    
        Protected Sub New()
            MyBase.New()
        End Sub
    
        Public Shared Function SetState() As DoorStatePatternBase
            If Singleton_ Is Nothing Then
                ' 2nd, updated type DoorClosed->DoorOpened
                Singleton_ = New DoorOpened 
            End If
    
            Return Singleton_
        End Function
    
        ' 3rd, remove transitions which are invalid for an opened door
    
        ' 4th, set new transition name
        Public Overrides Function DoCloseDoor() As DoorStatePatternBase
    
            ' 5th, updated new state name to transit to
            Return DoorClosed.SetState()
        End Function
    
    End Class 

5.4 Disadvantages

As usual, in life, there is always a drawback.

  • Since several classes are involved, more memory allocations - one per newly used state - do take place.
  • At first glance, the readability seems to suffer since more classes are involved.
    But on more complex scenarios, the classic approach suffers more in terms of readability and maintainability.

Further pros and cons are very welcome. Please comment below this article. Thank you.

Back To Top

5.5 Derived Classes of the State Base

The derived classes from DoorStatePatternBase do implement the logic of each possible state. There is one derived class for each state. These classes do actually have partly identical content which will lead to using copy and paste again. I have no clue yet how to eliminate those 'duplicates', e.g.:

VB.NET
Public Shared Function SetState() As DoorStatePatternBase
   If Singleton_ Is Nothing Then
       ' 3rd change here, should read = New DoorOpenedInside
       Singleton_ = New DoorLocked ' bug at least found by compiler
   End If

   Return Singleton_
End Function 

But at least they are designed to reduce potential errors to a minimum. Suggestions on better solutions are welcome.

File: DoorTypePattern.vb

The class which is using the state pattern. Objects of this class own a door state. As mentioned by the very first paragraph above, there was a silly mistake in the previous version of this class. The current or actual state was not owned by objects of this class, instead it was shared by all objects. You can click here to see the previous article version. This caused state interference when multiple different door objects of that class were instantiated:

Usage of old, shared state version:

VB.NET
' file MainModule.vb
Dim DoorPattern1 As DoorTypePattern = New DoorTypePattern
Dim DoorPattern2 As DoorTypePattern = New DoorTypePattern

DoorPattern1.TryOpen()
DoorPattern2.TryOpen()
DoorPattern1.TryClose() ' error in old version, state of DoorPattern2 changed too
Console.WriteLine(DoorPattern2.ToString)

Fixed class DoorTypePattern:

VB.NET
' file DoorTypePattern.vb
'
' Door State Machine Example
' to demonstrate state pattern in Visual Basic .NET
'

''' <summary>
''' The door object.
''' A state machine implemented with a the state pattern approach
''' </summary>

Public Class DoorTypePattern
    Inherits DoorTestBase
    ' derives from that base solely to enable testability of all door types
    ' with the same test routines
    ' refer to MainModule.TestDoor(ByVal door As StateMachineObjBase)

    ''' <summary>
    ''' the door is using the state pattern, the state is owned by the door 
    ''' </summary>
    Private MyState_ As DoorStatePatternBase

    Public Sub New()
        MyBase.New()

        ' NOTE: set the initial state of the door, this is at your preference
        '       could equally be DoorOpened.SetState() or DoorLocked.SetState()
        MyState_ = DoorClosed.SetState()
    End Sub

    Public Overrides Function ToString() As String
        Dim TypeInfo As Type = MyState_.GetType
        Return TypeInfo.Name
    End Function

    ' NOTE: put all possible / available transitions here

    Public Overrides Sub TryClose()
        MyState_ = MyState_.DoCloseDoor() ' action must match calling fct!
    End Sub

    Public Overrides Sub TryLock()
        MyState_ = MyState_.DoLockDoor()
    End Sub

    Public Overrides Sub TryOpen()
        MyState_ = MyState_.DoOpenDoor()
    End Sub

    Public Overrides Sub TryUnlock()
        MyState_ = MyState_.DoUnlockDoor()
    End Sub

    '
    ' Add new transitions here
    '

End Class

File: DoorClosed.vb

A possible door state: the door is closed.

VB.NET
' file DoorClosed.vb

''' <summary>
''' This class describes a possible state of a door with its acceptable
''' transitions to other states.
''' </summary>
Public Class DoorClosed
    Inherits DoorStatePatternBase

    ' NOTE: is of base class type to avoid copy/paste errors
    Private Shared Singleton_ As DoorStatePatternBase = Nothing

    ''' <summary>
    ''' Constructor, must not be public, i.e. hidden constructor,
    ''' since object is a singleton. 
    ''' </summary>
    Protected Sub New()
        MyBase.New() ' important to initialize base 1st
    End Sub

    ''' <summary>
    ''' Creates objects only once.
    ''' Lifetime is as assembly lifetime.
    ''' Remember to update class type: Singleton_ = New ...
    ''' </summary>
    ''' <remarks>Note the 'shared' keyword</remarks>
    Public Shared Function SetState() As DoorStatePatternBase
        If Singleton_ Is Nothing Then
            Singleton_ = New DoorClosed 'NOTE: set type to this state class
        End If

        Return Singleton_
    End Function

    ''' <summary>
    ''' This state is Closed. 
    ''' The only valid transitions are to open or lock the door.
    ''' </summary>
    Public Overrides Function DoOpenDoor() As DoorStatePatternBase
        Return DoorOpened.SetState()
    End Function

    ''' <summary>
    ''' This state is Closed. 
    ''' The only valid transitions are to open or lock the door.
    ''' </summary>
    Public Overrides Function DoLockDoor() As DoorStatePatternBase
        Return DoorLocked.SetState()
    End Function

    ' NOTE: invalid transitions are not overridden here
    '       they are handled by base class automatically


End Class

File: DoorOpened.vb

A possible door state: the door is opened.

VB.NET
' file DoorOpened.vb
''' <summary>
''' This class is another possible state of a door.
''' This class is copied from DoorClosed.vb and updated as indicated by comments.
''' </summary>
Public Class DoorOpened ' 1st, updated name DoorClosed->DoorOpened
    Inherits DoorStatePatternBase

    Private Shared Singleton_ As DoorStatePatternBase = Nothing

    Protected Sub New()
        MyBase.New()
    End Sub

    Public Shared Function SetState() As DoorStatePatternBase
        If Singleton_ Is Nothing Then
            Singleton_ = New DoorOpened ' 2nd, updated type DoorClosed->DoorOpened
        End If

        Return Singleton_
    End Function

    ' 3rd, removed transitions which are invalid from an opened door

    ' 4th, set new transition name
    Public Overrides Function DoCloseDoor() As DoorStatePatternBase

        ' 5th, updated new state name to transit to
        Return DoorClosed.SetState()
    End Function

End Class

File: DoorLocked.vb

A possible door state: the door is locked.

VB.NET
' file DoorLocked.vb
''' <summary>
''' door is in locked state
''' </summary>
Public Class DoorLocked
    Inherits DoorStatePatternBase

    Private Shared Singleton_ As DoorStatePatternBase = Nothing

    Protected Sub New()
        MyBase.New()
    End Sub

    Public Shared Function SetState() As DoorStatePatternBase
        If Singleton_ Is Nothing Then
            Singleton_ = New DoorLocked
        End If

        Return Singleton_
    End Function

    Public Overrides Function DoUnLockDoor() As DoorStatePatternBase
        Return DoorClosed.SetState()
    End Function

End Class

Back To Top

6 Appendix

For completeness, here is the code of the other classes involved:

File: DoorTestBase.vb

VB.NET
' file DoorTestBase.vb
'
' Door State Machine Example
'   to demonstrate "traditional" implementation of a 
'   state machine compared to state pattern implementation
'   in Visual Basic .NET
'

''' <summary>
''' This interface shall ensure that all domain objects, i.e. doors, 
''' can be tested by the same test routines. It is independent of the 
''' actual implementation of the door.
''' <see cref=" MainModule.TestDoor">
''' </summary>
''' <remarks>This is actually part of the strategy pattern
''' </remarks>
Public Interface IDoorTestActions
    Sub TryOpen()
    Sub TryClose()
    Sub TryLock()
    Sub TryUnlock()
End Interface

''' <summary>
''' This base class shall ensures solely that derived objects can be passed as 
''' parameter to the test routine. It is independent of the 
''' actual implementation of the door. 
''' <see cref=" MainModule.TestDoor">
''' </summary>
''' <remarks>This is actually part of the strategy pattern
''' </remarks>
Public MustInherit Class DoorTestBase
    Implements IDoorTestActions

    Public MustOverride Sub TryOpen() Implements IDoorTestActions.TryOpen
    Public MustOverride Sub TryClose() Implements IDoorTestActions.TryClose
    Public MustOverride Sub TryLock() Implements IDoorTestActions.TryLock
    Public MustOverride Sub TryUnlock() Implements IDoorTestActions.TryUnlock

End Class

File: MainModule.vb

VB.NET
' file MainModule.vb
'
' Door State Machine Example
'   to demonstrate "traditional" implementation of a 
'   state machine compared to state pattern implementation
'   in Visual Basic .NET
'

Module MainModule

    Public Sub TestDoor(ByVal door As DoorTestBase)

        Try
            Console.WriteLine("---")
            Console.WriteLine("current state is '{0}'", door.ToString)

            Console.Write("Trying to open, current state is: ")
            door.TryOpen()
            Console.WriteLine(door.ToString)

            Console.Write("Trying to close, current state is: ")
            door.TryClose()
            Console.WriteLine(door.ToString)

            Console.Write("Trying to lock, current state is: ")
            door.TryLock()
            Console.WriteLine(door.ToString)

            Try
                Console.Write("Trying to open, current state is: ")
                door.TryOpen() ' intentional error in transition
                Console.WriteLine(door.ToString)
            Catch ex As Exception
                Console.WriteLine("still '{0}' !", door.ToString)
                Console.WriteLine(ex.Message)
            End Try

            Console.Write("Trying to unlock, current state is: ")
            door.TryUnlock()
            Console.WriteLine(door.ToString)

            Console.Write("Trying to open, current state is: ")
            door.TryOpen()
            Console.WriteLine(door.ToString)

        Catch ex As Exception
            Console.WriteLine("still '{0}' !", door.ToString)
            Console.WriteLine(ex.Message)
        End Try

    End Sub

    Sub Main()

        Dim DoorPattern_ As DoorTypePattern = New DoorTypePattern
        Dim DoorTraditional_ As DoorTypeTraditional = New DoorTypeTraditional

        Console.WriteLine("-- State Machine Demo --")

        Console.WriteLine("{0}{0}Testing Traditional...", Environment.NewLine)
        TestDoor(DoorTraditional_)

        Console.WriteLine("{0}{0}Testing Pattern...", Environment.NewLine)
        TestDoor(DoorPattern_)

        Console.WriteLine("{0}{0}Program End. Please press 'Return'", Environment.NewLine)
        Console.ReadLine()
    End Sub

End Module

7 Output

This is the output of both strategies as given by the MainModule.vb above:

  1. Traditional Approach
  2. State Pattern

Image 4

8 Footnotes

  • [1] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995, ISBN-13 978-3-8273-2199-2
    Note: Actually, this is the famous book of the Gang of Four (GoF). But it is an old edition and an awfully translated German version. I think the original version is fine, but I cannot recommend the translated one.
  • [2] Thomas Jaeger: The State Design Pattern vs. State Machine, Code Project

9 History

  • 23 May, 2014, V1.00 - Initial release
  • 05 June, 2014, V2.00 - Fixed shared state bug: updated text, source and diagrams, revised wording and renamed some variables to improve clarity
  • 05 August, 2014, V2.01 - Improved paragraph about shared state mistake

Back To Top

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)