65.9K
CodeProject 正在变化。 阅读更多。
Home

状态机 - 状态模式 vs. 经典方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2014年5月29日

CPOL

7分钟阅读

viewsIcon

36656

downloadIcon

262

展示了状态模式和过程式解决方案。

目录

  1. 引言
  2. 背景
  3. Using the Code
  4. 经典的 State Machine 实现
  5. State Pattern 实现
  6. 附录
  7. 输出
  8. 脚注
  9. 历史

1 引言

最近,我正在阅读一本关于设计模式的书籍 [1]。我特别对以优雅的方式实现状态机感兴趣。优雅是指易于扩展、维护和测试。一个常见的陷阱是,例如,当一个人 tempted to do copy and paste of code fragments。如果复制的代码片段没有根据新上下文进行更新,就会引入 bug。即使代码片段在其原始上下文中运行良好。我将在文章后面概述这一点。

这本书通过继承和多态性展示了一种更好的实现方式。但即便如此,我还是对设计引入的对象依赖感到惊讶。基于书中给出的状态模式,我减少了依赖,并希望在本文中展示结果,以开启关于优缺点的讨论。

在本文的早期版本中,我犯了一个愚蠢但有趣的错误,该错误由 Member 10630008 指出。更多关于它的信息放在了 下面的合适位置

我还阅读了 Thomas Jaeger 的 Code Project 文章,该文章涵盖了相同的主题 [2]。我认为他启发了我使用门(door)的例子来阐述状态机。

2 背景

基本的面向对象编程技能以及继承、多态和接口的知识可能有助于理解这个概念。

3 使用代码

代码是用 Visual Basic .NET 编写的控制台应用程序,使用 Visual Studio 2008。如前所述,我选择使用门(door)的例子。一扇门可以处于打开、关闭或锁定状态。从一种状态切换到另一种状态的转换是——在某些限制下——门的打开、关闭、锁定和解锁。

起始状态,由黑圆圈表示,当然,这是您的偏好。

4 经典的 State Machine 实现

下图显示了经典方法的类设计。请关注 DoorTypeTraditional

基类 DoorTestBase 用于简化和统一主模块中的测试。如果两个要测试的类都派生自它,并因此实现了其接口 IDoorTestActions,那么它们就可以用相同的函数进行测试:MainModule.TestDoor

4.1 类 Traditional

以下是勾勒经典方法的源代码。

文件:DoorTypeTraditional.vb

此类使用状态的枚举和转换动作的枚举。逻辑在 DoAction 方法中使用 Select Case 语句实现。根据当前状态,过滤允许的动作以过渡到下一个状态。

'
' 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 优点

对于简单的状态机,例如给定的示例,传统方法就足够了。

  • 一切都在一个清晰表示的源文件中
  • 可以认为执行速度很快,因为涉及的内存分配很少

4.3 缺点

如果——一如既往——状态和转换的数量随时间增加,整个设计很快就会变得非常复杂。例如,有一天区分室内和室外打开的门可能会很有趣。可能需要知道门是完全打开还是半打开。

  • 添加状态或转换时,至少必须维护三个地方
    1. Enum DoorState
    2. Enum DoorTransition
    3. Sub DoAction()

    接口 IDoorTestActions 的更改也很可能。

  • DoAction 方法会很快变得令人困惑,即不清楚。
  • 添加状态时,人们会倾向于复制和粘贴现有的状态块,这很容易引入新的 bug:例如,变量名保持不变或遗漏了转换。

返回顶部

5 State Pattern 实现

这个状态模式的类设计如下

请重点关注主类 DoorStatePatternBase 及其派生类

DoorOpenedDoorClosedDoorLocked

同样,请注意基类 DoorTestBase 仅用于简化和统一主模块中的测试。所有要测试的对象都应派生自 DoorTestBase。这将强制实现其接口 IDoorTestActions。因此,这些对象可以使用相同的函数进行测试:MainModule.TestDoor()

正如你所见,这是通过 DoorTypePattern 类实现的。DoorTypePattern 的对象有一个状态,该状态实现为 DoorStatePatternBaseDoorTypePattern 的对象派生自 DoorTestBase,以便通过 MainModule.Testdoor() 以相同的方式进行测试。

5.1 此状态模式的关键特性

  • 每个状态都实现在自己的类中。
  • 每个状态类只需实现有效的转换。
  • 每个状态类都是一个单例。
  • DoorStatePatternBase 不需要了解其所有者 DoorTypePattern

    新状态由转换函数返回。

5.2 类 StatePatternBase

以下是勾勒此状态模式版本的源代码。

文件:DoorStatePatternBase.vb

这是模式设计的基类。每个状态都有自己的类,该类必须派生自 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 优点

这种设计可以轻松应对状态和转换的复杂性。扩展解决方案时,只需进行两次小更改

  1. DoorStatePatternBase 需要将新转换添加为可覆盖的。这通常通过复制和粘贴现有转换来完成,然后更新为新的函数名称,并更新异常消息。唯一可能的错误是忘记更新异常文本。这不会影响已实现状态的正确性!

    ' new transition  
    Public Overridable Function OpenDoorInside() As DoorStatePatternBase
        Throw New Exception("state transition 'OpenDoorInside' not allowed") 
        Return Me
    End Function 
  2. 新状态需要实现,每个状态都有自己的类实现,继承自 DoorStatePatternBase。这些状态类包含状态之间转换的逻辑。它们有效地取代了经典方法的 Select-Case 语句。
    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 缺点

一如既往,生活中总会有一些缺点。

  • 由于涉及多个类,会发生更多的内存分配——每个新使用的状态一次——这会发生。
  • 乍一看,由于涉及的类更多,可读性似乎受到了影响。
    但在更复杂的场景中,经典方法在可读性和可维护性方面受到的影响更大。

非常欢迎进一步的优缺点。请在本文下方评论。谢谢。

返回顶部

5.5 状态基类的派生类

来自 DoorStatePatternBase 的派生类实现了每个可能状态的逻辑。每个状态都有一个派生类。这些类实际上具有部分相同的​​内容,这将导致再次使用复制粘贴。我还没有头绪如何消除这些“重复项”,例如

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 

但至少它们被设计用来将潜在错误降至最低。欢迎对更好的解决方案提出建议。

文件:DoorTypePattern.vb

使用状态模式的类。此类的对象拥有一个门状态。如上文第一段所述,前一个版本的此类存在一个愚蠢的错误。当前或实际状态不归此类对象所有,而是由所有对象共享。 您可以在此处点击查看上一篇文章版本。这导致实例化多个不同的门对象时出现状态干扰。

使用旧的共享状态版本

' 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)

修复的类 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

文件:DoorClosed.vb

可能的门状态:门已关闭。

' 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

文件:DoorOpened.vb

可能的门状态:门已打开。

' 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

文件:DoorLocked.vb

可能的门状态:门已锁定。

' 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 附录

为了完整起见,这是其他相关类的代码

文件: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

文件: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 输出

这是上面 MainModule.vb 提供的两种策略的输出

  1. 传统方法
  2. 状态模式

8 脚注

  • [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
    注意:实际上,这是著名的“Gang of Four”(GoF)的书。但这是一个旧版本,而且是一个翻译得糟糕透顶的德语版本。我认为原版没问题,但我不能推荐这个翻译版。
  • [2] Thomas Jaeger: The State Design Pattern vs. State Machine, Code Project

9 历史

  • 2014年5月23日,V1.00 - 初始发布
  • 2014年6月5日,V2.00 - 修复了共享状态 bug:更新了文本、源代码和图表,修改了措辞并重命名了一些变量以提高清晰度
  • 2014年8月5日,V2.01 - 改进了关于共享状态错误的段落

返回顶部

© . All rights reserved.