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.
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.
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
.
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
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
.
Public MustInherit Class DoorStatePatternBase
#Region "possible state transitions"
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
#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!
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 the Select
-Case
statement of the classic approach.
Public Class 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
End If
Return Singleton_
End Function
Public Overrides Function DoCloseDoor() As DoorStatePatternBase
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.:
Public Shared Function SetState() As DoorStatePatternBase
If Singleton_ Is Nothing Then
Singleton_ = New DoorLocked
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:
Dim DoorPattern1 As DoorTypePattern = New DoorTypePattern
Dim DoorPattern2 As DoorTypePattern = New DoorTypePattern
DoorPattern1.TryOpen()
DoorPattern2.TryOpen()
DoorPattern1.TryClose()
Console.WriteLine(DoorPattern2.ToString)
Fixed class DoorTypePattern
:
Public Class DoorTypePattern
Inherits DoorTestBase
Private MyState_ As DoorStatePatternBase
Public Sub New()
MyBase.New()
MyState_ = DoorClosed.SetState()
End Sub
Public Overrides Function ToString() As String
Dim TypeInfo As Type = MyState_.GetType
Return TypeInfo.Name
End Function
Public Overrides Sub TryClose()
MyState_ = MyState_.DoCloseDoor()
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
End Class
File: DoorClosed.vb
A possible door state: the door is closed.
Public Class DoorClosed
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 DoorClosed
End If
Return Singleton_
End Function
Public Overrides Function DoOpenDoor() As DoorStatePatternBase
Return DoorOpened.SetState()
End Function
Public Overrides Function DoLockDoor() As DoorStatePatternBase
Return DoorLocked.SetState()
End Function
End Class
File: DoorOpened.vb
A possible door state: the door is opened.
Public Class 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
End If
Return Singleton_
End Function
Public Overrides Function DoCloseDoor() As DoorStatePatternBase
Return DoorClosed.SetState()
End Function
End Class
File: DoorLocked.vb
A possible door state: the door is locked.
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
Public Interface IDoorTestActions
Sub TryOpen()
Sub TryClose()
Sub TryLock()
Sub TryUnlock()
End Interface
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
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()
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.
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