结构图
角色
- 环境(Subject)角色:负责定义客户感兴趣的接口,并维护一个具体状态(ConcreteState)的实例,以及维护状态转换或为这种转换提供支持(因为状态转换有时候可能是由上层应用发起的)。
- 状态(State)角色:定义一个接口以封装与环境(Context)角色的一个特定状态相关的行为。
- 具体状态(ConcreteState)角色:)每一个子类实现一个与环境(Context)角色状态相关的行为。
动机
在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生变化,比如文档处于只读状态,其支持的行为和读写状态支持的行为就可能完全不同。
如何在运行时根据对象的状态来透明地更改对象的行为?而不会为对象操作和状态转化之间引入紧耦合?
意图
允许一个对象在其内部状态改变时改变它的行为。从而使对象看起来似乎修改了其行为。
示意性代码
'State pattern -- Structural example
'MainApp test application
Module MainApp
Public Sub Main()
'Setup context in a state
Dim c As New Context(New ConcreteStateA)
'Issue requests, which toggles state
c.Request()
c.Request()
c.Request()
c.Request()
'Wait for user
Console.ReadLine()
End Sub
End Module
'"State"
Public MustInherit Class State
Public MustOverride Sub Handle(ByVal context As Context)
End Class
'"ConcreteStateA"
Public Class ConcreteStateA
Inherits State
Public Overrides Sub Handle(ByVal context As Context)
context.State = New ConcreteStateB
End Sub
End Class
'"ConcreteStateB"
Public Class ConcreteStateB
Inherits State
Public Overrides Sub Handle(ByVal context As Context)
context.State = New ConcreteStateA
End Sub
End Class
'"Context"
Public Class Context
'Constructor
Public Sub New(ByVal state As State)
Me._state = state
End Sub
'Property
Private _state As State
Public Property State() As State
Get
Return _state
End Get
Set(ByVal value As State)
_state = value
End Set
End Property
Public Sub Request()
State.Handle(Me)
End Sub
End Class
一个实例
下面的状态模式代码演示了一个帐户在处于不同状态时可以进行不同的操作。在行为上的区别委派给以下三个对象红色状态(RedState),灰色状态(SilverState),金色状态(GoldState)。这些状态分别代表透支帐户,新开通的帐户,和拥有良好信誉的帐户。
'State pattern -- Real World example
'MainApp test application
Module MainApp
Public Sub Main()
'Open a new account
Dim account As New Account("Jim Johnson")
'Apply financial transactions
account.Deposit(500.0)
account.Deposit(300.0)
account.Deposit(550.0)
account.PayInterest()
account.Withdraw(2000.0)
account.Withdraw(1100.0)
'Wait for user
Console.ReadLine()
End Sub
End Module
'"Context"
Public Class Account
Private owner As String
Private serviceFee As Double
Private interest As Double
'Constructor
Public Sub New(ByVal owner As String)
'New accounts are 'Silver' by default
Me.owner = owner
_state = "SilverState"
End Sub
'Properties
Protected _balance As Double
Public ReadOnly Property Balance() As Double
Get
Return _balance
End Get
End Property
Private _state As String
Public Property State() As String
Get
Return _state
End Get
Set(ByVal value As String)
_state = value
End Set
End Property
Public Sub Deposit(ByVal amount As Double)
_balance += amount
StateChangeCheck()
Console.WriteLine("Deposited {0:C} --- ", amount)
Console.WriteLine(" Balance = {0:C}", Me.Balance)
Console.WriteLine(" Status = {0}", _
_state)
Console.WriteLine("")
End Sub
Public Sub Withdraw(ByVal amount As Double)
If _state = "RedState" Then
serviceFee = 15.0
amount -= serviceFee
Console.WriteLine("No funds available for withdrawal!")
Else
_balance -= amount
End If
StateChangeCheck()
Console.WriteLine("Withdrew {0:C} --- ", amount)
Console.WriteLine(" Balance = {0:C}", Me.Balance)
Console.WriteLine(" Status = {0}", _
_state)
Console.WriteLine("")
End Sub
Public Sub PayInterest()
Select Case _state
Case "RedState"
'No Interest is paid
Case "SilverState"
interest = 0.0
_balance += interest * _balance
Case "GoldState"
interest = 0.05
_balance += interest * _balance
End Select
Console.WriteLine("Interest Paid --- ")
Console.WriteLine(" Balance = {0:C}", Me.Balance)
Console.WriteLine(" Status = {0}", _
_state)
Console.WriteLine("")
End Sub
Private Sub StateChangeCheck()
If Balance < 0.0 Then
_state = "RedState"
ElseIf _balance < 1000 Then
_state = "SilverState"
Else
_state = "GoldState"
End If
End Sub
End Class
在开发中时常遇到的根据不同的状态需要进行不同的处理操作的问题,而这样的问题,大部分人是采用Select-Case/If-Else语句进行处理的,这样会造成一个问题:分支过多,而且如果加入一个新的状态就需要对原来的代码进行编译.State模式采用了对这些不同的状态进行封装的方式处理这类问题,当状态改变的时候进行处理然后再切换到另一种状态,也就是说把状态的切换责任交给了具体的状态类去负责.具体的状态类可以通过派生的方式不依赖于其他对象而独立变化。
'State pattern -- Real World example
'MainApp test application
Module MainApp
Public Sub Main()
'Open a new account
Dim account As New Account("Jim Johnson")
'Apply financial transactions
account.Deposit(500.0)
account.Deposit(300.0)
account.Deposit(550.0)
account.PayInterest()
account.Withdraw(2000.0)
account.Withdraw(1100.0)
'Wait for user
Console.ReadLine()
End Sub
End Module
'"State"
Public MustInherit Class State
'Fields
Protected interest As Double
Protected lowerLimit As Double
Protected upperLimit As Double
'Properties
Protected _account As Account
Public Property Account() As Account
Get
Return _account
End Get
Set(ByVal value As Account)
_account = value
End Set
End Property
Protected _balance As Double
Public Property Balance() As Double
Get
Return _balance
End Get
Set(ByVal value As Double)
_balance = value
End Set
End Property
Public MustOverride Sub Deposit(ByVal amount As Double)
Public MustOverride Sub Withdraw(ByVal amount As Double)
Public MustOverride Sub PayInterest()
End Class
'"ConcreteState"
'Account is overdrawn
Public Class RedState
Inherits State
Private serviceFee As Double
'Constructor
Public Sub New(ByVal state As State)
Me.Balance = state.Balance
Me.Account = state.Account
Initialize()
End Sub
Private Sub Initialize()
'Should come from a datasource
interest = 0.0
lowerLimit = -100.0
upperLimit = 0.0
serviceFee = 15.0
End Sub
Public Overrides Sub Deposit(ByVal amount As Double)
Balance += amount
StateChangeCheck()
End Sub
Public Overrides Sub Withdraw(ByVal amount As Double)
amount -= serviceFee
Console.WriteLine("No funds available for withdrawal!")
End Sub
Public Overrides Sub PayInterest()
'No intereset is paid
End Sub
Private Sub StateChangeCheck()
If Balance > upperLimit Then
Account.State = New SilverState(Me)
End If
End Sub
End Class
'"ConcreteState"
'Silver is non-interest bearing state
Public Class SilverState
Inherits State
'Overloaded constructors
Public Sub New(ByVal state As State)
Me.new(state.Balance, state.Account)
End Sub
Public Sub New(ByVal balance As Double, ByVal account As Account)
Me.Balance = balance
Me.Account = account
Initialize()
End Sub
Private Sub Initialize()
'Should come from a datasource
interest = 0.0
lowerLimit = 0.0
upperLimit = 1000.0
End Sub
Public Overrides Sub Deposit(ByVal amount As Double)
Balance += amount
StateChangeCheck()
End Sub
Public Overrides Sub Withdraw(ByVal amount As Double)
Balance -= amount
StateChangeCheck()
End Sub
Public Overrides Sub PayInterest()
Balance += interest * Balance
StateChangeCheck()
End Sub
Private Sub StateChangeCheck()
If Balance < lowerLimit Then
Account.State = New RedState(Me)
ElseIf Balance > upperLimit Then
Account.State = New GoldState(Me)
End If
End Sub
End Class
'"ConcreteState"
'Interest bearing state
Public Class GoldState
Inherits State
'Overloaded constructors
Public Sub New(ByVal state As State)
Me.new(state.Balance, state.Account)
End Sub
Public Sub New(ByVal balance As Double, ByVal account As Account)
Me.Balance = balance
Me.Account = account
Initialize()
End Sub
Private Sub Initialize()
'Should come from a datasource
interest = 0.05
lowerLimit = 1000.0
upperLimit = 10000000.0
End Sub
Public Overrides Sub Deposit(ByVal amount As Double)
Balance += amount
StateChangeCheck()
End Sub
Public Overrides Sub Withdraw(ByVal amount As Double)
Balance -= amount
StateChangeCheck()
End Sub
Public Overrides Sub PayInterest()
Balance += interest * Balance
StateChangeCheck()
End Sub
Private Sub StateChangeCheck()
If Balance < 0.0 Then
Account.State = New RedState(Me)
ElseIf Balance < lowerLimit Then
Account.State = New SilverState(Me)
End If
End Sub
End Class
'"Context"
Public Class Account
Private owner As String
'Constructor
Public Sub New(ByVal owner As String)
'New accoounts are 'Silver' by default
Me.owner = owner
_state = New SilverState(0.0, Me)
End Sub
'Properties
Public ReadOnly Property Balance() As Double
Get
Return _state.Balance
End Get
End Property
Private _state As State
Public Property State() As State
Get
Return _state
End Get
Set(ByVal value As State)
_state = value
End Set
End Property
Public Sub Deposit(ByVal amount As Double)
State.Deposit(amount)
Console.WriteLine("Deposited {0:C} --- ", amount)
Console.WriteLine(" Balance = {0:C}", Me.Balance)
Console.WriteLine(" Status = {0}", _
_state.GetType().Name)
Console.WriteLine("")
End Sub
Public Sub Withdraw(ByVal amount As Double)
State.Withdraw(amount)
Console.WriteLine("Withdrew {0:C} --- ", amount)
Console.WriteLine(" Balance = {0:C}", Me.Balance)
Console.WriteLine(" Status = {0}", _
_state.GetType().Name)
Console.WriteLine("")
End Sub
Public Sub PayInterest()
_state.PayInterest()
Console.WriteLine("Interest Paid --- ")
Console.WriteLine(" Balance = {0:C}", Me.Balance)
Console.WriteLine(" Status = {0}", _
Me.State.GetType().Name)
Console.WriteLine("")
End Sub
End Class
State Pattern模式的几个要点:
1、State模式将所有与一个特定状态相关的行为都放入一个State的子类对象中,在对象状态切换时,切换相应的对象;但同时维持State的接口,这样实现了具体操作与状态转换之间的解耦。
2、为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换是原子性的——即要么彻底转换过来,要么不转换。
3、如果State对象没有实例变量,那么各个上下文可以共享一个State对象,从而节省对象开销。
我的理解
封装与状态相关的行为,支持状态的变化。
参考资料
《C#面向对象设计模式纵横谈系列课程(21)》 李建中老师