State Machine - State Pattern vs. Classic Approach
State pattern and procedural solution illustrated
Table of Contents
- Introduction
- Background
- Using the Code
- Classic State Machine Implementation
- State Pattern Implementation
- Appendix
- Output
- Footnotes
- 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.
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
:
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.
'
' 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:
Enum DoorState
Enum DoorTransition
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.
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
.
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 ownerDoorTypePattern
.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
.
' 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:
-
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!' new transition Public Overridable Function OpenDoorInside() As DoorStatePatternBase Throw New Exception("state transition 'OpenDoorInside' not allowed") Return Me End Function
- 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 theSelect
-Case
statement of the classic approach.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.
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.:
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:
' 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
:
' 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.
' 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.
' 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.
' 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
6 Appendix
For completeness, here is the code of the other classes involved:
File: DoorTestBase.vb
' 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
' 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:
- Traditional Approach
- State Pattern
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