思考并回答以下问题:
- 有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换。游戏中是什么对象?
- 统一由环境类来负责状态之间的转换和由具体状态类来负责状态之间的转换有什么不同?
本章导学
状态模式是一种较为复杂的设计模式,用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中的某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。
本章将学习状态模式的定义与结构,分析状态模式的特点,并结合实例学习状态模式的实现过程,学会如何在实际软件项目开发中应用状态模式。
本章知识点
- 状态模式的定义
- 状态模式的结构
- 状态模式的实现
- 状态模式的应用
- 状态模式的优缺点
- 状态模式的适用环境
- 共享状态
- 使用环境类实现状态的转换
状态模式概述
“人有悲欢离合,月有阴晴圆缺”,包括人在内,很多事物都具有多种状态,而且在不同状态下会具有不同的行为,这些状态在特定条件下还将发生相互转换。就像水,它可以凝固成冰,也可以受热蒸发后变成水蒸气,水可以流动,冰可以雕刻,水蒸气可以扩散。可以用UML状态图来描述H2O的3种状态,如图1所示。
图1 H2O的3种状态(未考虑临界点)
在软件系统中,有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换,而且对象在不同的状态下也将具有不同的行为。通常可以使用复杂的条件判断语句(例如if…else…语句)来进行状态的判断和转换操作,这会导致代码的可维护性和灵活性下降,特别是出现新的状态时,代码的扩展性很差,客户端代码也需要进行相应的修改,违背了开闭原则。为了解决状态的转换问题,并降低客户端代码与对象状态之间的耦合度,可以使用一种被称为状态模式的设计模式。
状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中的某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化。对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。
状态模式的定义如下:1
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
状态模式又称为状态对象(Objects for States),它是一种对象行为型模式。
状态模式的结构与实现
状态模式的结构
在状态模式中引入了抽象状态类和具体状态类,它们是状态模式的核心,其结构如图2所示。
由图2可知,状态模式包含以下3个角色。
图2 状态模式结构图
(1)Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性,且在不同状态下对象的行为有所不同,所以将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
(2)State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现了这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
(3)Concretestate(具体状态类):它是抽象状态类的子类,每一个具体状态类实现一个与环境类的一个状态相关的行为,对应环境类的一个具体状态,不同的具体状态类其行为有所不同。
状态模式的实现
在状态模式中,将对象在不同状态下的行为封装到不同的状态类中,为了让系统具有更好的灵活性和可扩展性,同时对各状态下的共有行为进行封装,需要对状态进行抽象化,引入了抽象状态类角色。其典型代码如下:1
2
3
4
5abstract class State
{
// 声明抽象业务方法,不同的具体状态类可以有不同的实现
public abstract void Handle();
}
在抽象状态类的子类(即具体状态类)中实现了在抽象状态类中声明的业务方法,不同的具体状态类可以提供完全不同的方法实现。实际使用时,在一个状态类中可能包含多个业务方法,如果在具体状态类中某些业务方法的实现完全相同,则可以将这些方法移至抽象状态类,实现代码的复用。典型的具体状态类代码如下:1
2
3
4
5
6
7class ConcreteState : State
{
public override void Handle()
{
// 方法具体实现代码
}
}
环境类维持一个对抽象状态类的引用,通过SetState()方法可以向环境类注入不同的状态对象,再在环境类的业务方法中调用状态对象的方法。其典型代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Context
{
private State state; // 维持一个对抽象状态对象的引用
private int value; // 其他属性值,该属性值的变化可能会导致对象的状态发生变化
// 设置状态对象
public void SetState(State state)
{
this.state = state;
}
public void Request()
{
// 其他代码
state.Handle(); // 调用状态对象的业务方法
// 其他代码
}
}
环境类实际上是真正拥有状态的对象,只是将环境类中与状态有关的代码提取出来封装到专门的状态类中。在状态模式结构图中,环境类Context与抽象状态类State间存在着单向关联关系,在Context中定义了一个State对象。在实际使用时,它们之间可能存在更为复杂的关系,State与Context之间可能也存在依赖或者双向关联关系。
在状态模式的使用过程中,一个对象的状态之间还可以进行相互转换,通常有两种实现状态转换的方式。
(1)统一由环境类来负责状态之间的转换,此时,环境类还充当了状态管理器(StateManager)角色,在环境类的业务方法中通过对某些属性值的判断实现状态转换,也可以提供一个专门的方法用于实现属性判断和状态转换,代码片段如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15...
public void ChangeState()
{
// 判断属性值,根据属性值进行状态转换
if (value == 0)
{
this.SetState(new ConcreteStateA());
}
else if (value == 1)
{
this.SetState(new ConcreteStateB());
}
...
}
...
(2)由具体状态类来负责状态之间的转换,可以在具体状态类的业务方法中判断环境类的某些属性值,再根据情况为环境类设置新的状态对象,实现状态变换。同样,也可以提供一个专门的方法来负责属性值的判断和状态转换。此时,状态类与环境类之间将存在依赖或关联关系,因为状态类需要访问环境类中的属性值,具体状态类ConcreteStateA的代码片段如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15...
public void ChangeState(Context ctx)
{
// 根据环境对象中的属性值进行状态转换
if (ctx.value == 1)
{
ctx.SetState(new ConcreteStateB());
}
else if (ctx.value == 2)
{
ctx.SetState(new ConcreteStateC());
}
...
}
...
状态模式的应用实例
下面通过一个应用实例来进一步学习和理解状态模式。
1.实例说明
某软件公司要为一银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,该软件公司开发人员发现在系统中账户存在3种状态,且在不同状态下账户存在不同的行为,具体说明如下:
(1)如果账户中余额大于等于0,则账户的状态为正常状态(NormalState),此时用户既可以向该账户存款也可以从该账户取款。
(2)如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(OverdraftState),此时用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息。
(3)如果账户中余额等于-2000,那么账户的状态为受限状态(RestrictedState),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息。
(4)根据余额的不同,以上3种状态可发生相互转换。
现使用状态模式设计并实现银行账户状态的转换。
2.实例类图
通过对银行账户类进行分析,可以绘制出图3所示的UML状态图。
图3 银行账户状态图
在图3中,NormalState表示正常状态,OverdraftState表示透支状态,RestrictedState表示受限状态。在这3种状态下账户对象拥有不同的行为,方法Deposit()用于存款,Withdraw()用于取款,ComputeInterest()用于计算利息,StateCheck()用于在每一次执行存款和取款操作后根据余额来判断是否要进行状态转换并实现状态转换,相同的方法在不同的状态下可能会有不同的实现。
使用状态模式对银行账户状态进行设计,所得的结构如图4所示。
图4 银行账户结构图
在图4中,Account充当环境类角色,AccountState充当抽象状态类角色,NormalState、OverdraftState和RestrictedState充当具体状态类角色。
3.实例代码
(1)Account:银行账户,充当环境类。
Account.cs
1 | using System; |
(2)AccountState:账户状态类,充当抽象状态类。
AccountState.cs
1 | namespace StateSample |
(3)NormalState:正常状态类,充当具体状态类。
NormalState.cs
1 | using System; |
(4)OverdraftState:透支状态类,充当具体状态类。
OverdraftState.cs
1 | using System; |
(5)RestrictedState:受限状态类,充当具体状态类。
RestrictedState.cs
1 | using System; |
(6)Program:客户端测试类。
Program.cs
1 | using System; |
4.结果及分析
编译并运行程序,输出结果如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24段誉开户,初始金额为0
---------------------------------------------
段誉存款1000
现在余额为1000
现在账户状态为StateSample.NormalState
---------------------------------------------
①段誉取款2000
现在余额为-1000
现在账户状态为StateSample.OverdraftState
---------------------------------------------
段誉存款3000
现在余额为2000
现在账户状态为StateSample.NormalState
---------------------------------------------
②段誉取款4000
现在余额为-2000
现在账户状态为StateSample.RestrictedState
---------------------------------------------
③段誉取款1000
账号受限,取款失败
现在余额为-2000
现在账户状态为StateSample.RestrictedState
---------------------------------------------
计算利息!
①②③部分对应客户端代码中3次调用取款方法Withdraw()的输出结果,由于对象状态不一样,因此这3次输出结果有所差异。第一次取款后账户状态由正常状态(NormalState)变为透支状态(OverdraftState);第二次取款后账户状态由正常状态(NormalState)变为受限状态(RestrictedState);在第三次取款时,由于账户状态已经为受限状态,因此取款失败。这3次取款操作体现了对象在不同状态下具有不同的行为,而且对象的转换是自动的,客户端无须关心其转换细节。
共享状态
在有些情况下,多个环境对象可能需要共享同一个状态,如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。下面通过一个简单实例来说明如何实现共享状态。
某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。
试使用状态模式来实现开关的设计。
通过分析,其结构如图5所示。
图5 开关及其状态设计结构图
开关类的代码如下:
Switch.cs
1 | using System; |
抽象状态类的代码如下:
SwitchState.cs
1 | namespace SwitchStateSample |
两个具体状态类的代码如下:
OnState.cs
1 | using System; |
OffState.cs
1 | using System; |
编写以下客户端代码进行测试:
Program.cs
1 | using System; |
输出结果如下:1
2
3
4
5
6开关1已经打开!
开关2已经打开!
开关1关闭!
开关2已经关闭!
开关2打开!
开关1已经打开!
从输出结果可以得知:两个开关共享相同的状态,如果第一个开关关闭,则第二个开关也将关闭,再次关闭时将输出“已经关闭”,打开时也将得到类似结果。
使用环境类实现状态的转换
在状态模式中实现状态转换时,具体状态类可通过调用环境类Context的SetState()方法进行状态的转换操作,也可以统一由环境类Context实现状态的转换。此时,增加新的具体状态类可能需要修改其他具体状态类或者环境类的源代码,否则系统无法转换到新增状态。但是对于客户端而言,无须关心状态类,可以为环境类设置默认的状态类,而将状态的转换工作交给具体状态类或环境类来完成,具体的转换细节对于客户端而言是透明的。
在之前的“银行账户状态转换”实例中,通过具体状态类来实现状态的转换,在每一个具体状态类中都包含一个StateCheck()方法,在该方法内部实现状态的转换。除此之外,还可以通过环境类来实现状态转换,环境类作为一个状态管理器,统一实现各种状态之间的转换操作。
下面通过一个包含循环状态的简单实例来说明如何使用环境类实现状态转换。
现要开发一个屏幕放大镜工具,其具体功能描述如下:
用户单击“放大镜”按钮之后屏幕将放大一倍,再单击一次“放大镜”按钮屏幕再放大一倍,第三次单击该按钮后屏幕将还原到默认大小。
试使用状态模式来设计该屏幕放大镜工具。
通过分析,可以定义3个屏幕状态类NormalState、LargerState和LargestState来对应屏幕的3种状态,分别是正常状态、二倍放大状态和四倍放大状态,屏幕类Screen充当环境类,其结构如图6所示。
图6 屏幕放大镜工具结构图
本实例的核心代码如下:
Screen.cs:屏幕类
1 | using System; |
ScreenState.cs:抽象状态类
1 | using System; |
NormalState.cs:正常状态类
1 | using System; |
LargerState.cs:二倍状态类
1 | using System; |
LargerState.cs:四倍状态类
1 | using System; |
在上述代码中,所有的状态转换操作都由环境类Screen来实现,此时,环境类充当了状态管理器角色。如果需要增加新的状态,例如“八倍状态类”,需要修改环境类,这在一定程度上违背了开闭原则,但对其他状态类没有任何影响。
编写以下客户端代码进行测试:
Program.cs:客户端测试类
1 | using System; |
输出结果如下:1
2
3
4正常大小!
二倍大小!
四倍大小!
正常大小!
状态模式的优缺点与适用环境
状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流、游戏等软件中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。
状态模式的优点
状态模式的主要优点如下:
- (1)状态模式封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
- (2)状态模式将所有与某个状态有关的行为放到一个类中,只需注入一个不同的状态对象即可使环境对象拥有不同的行为。
- (3)状态模式允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以避免使用庞大的条件语句将业务方法和状态转换代码交织在一起。
- (4)状态模式可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
状态模式的缺点
状态模式的主要缺点如下:
- (1)状态模式会增加系统中类和对象的个数,导致系统运行开销增大。
- (2)其结构与实现都较为复杂,如果使用不当将导致程序结构和代码混乱,增加系统设计的难度。
- (3)状态模式对开闭原则的支持并不太好,增加新的状态类需要修改负责状态转换的源代码,否则无法转换到新增状态,而且修改某个状态类的行为也需要修改对应类的源代码。
状态模式的适用环境
在以下情况下可以考虑使用状态模式:
- (1)对象的行为依赖于它的状态(例如某些属性值),状态的改变将导致行为的变化。
- (2)在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
本章小结
(1)在状态模式中,允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式是一种对象行为型模式。
(2)状态模式包含环境类、抽象状态类和具体状态类3个角色。其中,环境类是拥有多种状态的对象;抽象状态类用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现了这些方法;具体状态类是抽象状态类的子类,每一个具体状态类实现一个与环境类的一个状态相关的行为,对应环境类的一个具体状态,不同的具体状态类其行为有所不同。
(3)状态模式的主要优点包括它封装了状态的转换规则,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中;允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块;可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。其主要缺点包括状态模式会增加系统中类和对象的个数,导致系统运行开销增大;如果使用不当将导致程序结构和代码混乱,增加系统设计的难度;此外,状态模式对开闭原则的支持并不太好。
(4)状态模式适用的环境:对象的行为依赖于它的状态,状态的改变将导致行为的变化;在代码中包含大量与对象状态有关的条件语句。
(5)如果需要在系统中实现多个环境对象共享一个或多个状态对象,可以将这些状态对象定义为环境类的静态成员对象。
(6)在状态模式中,可以在具体状态类中实现状态之间的转换,也可以统一由环境类来负责状态之间的转换。