处理对象的多种状态及其相互转换——状态模式
“人有悲欢离合,月有阴晴圆缺”,包括人在内,很多事物都具有多种状态,而且在不同状态下会具有不同的行为,这些状态在特定条件下还将发生相互转换。就像水,它可以凝固成冰,也可以受热蒸发后变成水蒸汽,水可以流动,冰可以雕刻,蒸汽可以扩散。我们可以用UML状态图来描述H2O的三种状态,如图1所示:

图1 H2O的三种状态(未考虑临界点)
在软件系统中,有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换,而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计,我们可以使用一种被称之为状态模式的设计模式,本章我们将学习用于描述对象状态及其转换的状态模式。
案例:
软件公司欲为某银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,Sunny软件公司开发人员发现在该系统中,账户存在三种状态,且在不同状态下账户存在不同的行为,具体说明如下:
(1) 如果账户中余额大于等于0,则账户的状态为正常状态(Normal State),此时用户既可以向该账户存款也可以从该账户取款;
(2) 如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(Overdraft State),此时用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息;
(3) 如果账户中余额等于-2000,那么账户的状态为受限状态(Restricted State),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息;
(4) 根据余额的不同,以上三种状态可发生相互转换。
Sunny软件公司开发人员对银行账户类进行分析,绘制了如图2所示UML状态图:

图2 银行账户状态图
在图2中,NormalState表示正常状态,OverdraftState表示透支状态,RestrictedState表示受限状态,在这三种状态下账户对象拥有不同的行为,方法deposit()用于存款,withdraw()用于取款,computeInterest()用于计算利息,stateCheck()用于在每一次执行存款和取款操作后根据余额来判断是否要进行状态转换并实现状态转换,相同的方法在不同的状态中可能会有不同的实现。为了实现不同状态下对象的各种行为以及对象状态之间的相互转换,Sunny软件公司开发人员设计了一个较为庞大的账户类Account,其中部分代码如下所示:
class Account { private String state; //状态 private int balance; //余额 ...... //存款操作 public void deposit() { //存款 stateCheck(); } //取款操作 public void withdraw() { if (state.equalsIgnoreCase("NormalState") || state.equalsIgnoreCase("OverdraftState ")) { //取款 stateCheck(); } else { //取款受限 } } //计算利息操作 public void computeInterest() { if(state.equalsIgnoreCase("OverdraftState") || state.equalsIgnoreCase("RestrictedState ")) { //计算利息 } } //状态检查和转换操作 public void stateCheck() { if (balance >= 0) { state = "NormalState"; } else if (balance > -2000 && balance < 0) { state = "OverdraftState"; } else if (balance == -2000) { state = "RestrictedState"; } else if (balance < -2000) { //操作受限 } } ...... }
分析上述代码,我们不难发现存在如下几个问题:
(1) 几乎每个方法中都包含状态判断语句,以判断在该状态下是否具有该方法以及在特定状态下该方法如何实现,导致代码非常冗长,可维护性较差;
(2) 拥有一个较为复杂的stateCheck()方法,包含大量的if…else if…else…语句用于进行状态转换,代码测试难度较大,且不易于维护;
(3) 系统扩展性较差,如果需要增加一种新的状态,如冻结状态(Frozen State,在该状态下既不允许存款也不允许取款),需要对原有代码进行大量修改,扩展起来非常麻烦。
状态模式可以在一定程度上解决上述问题。
状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
在状态模式中引入了抽象状态类和具体状态类,它们是状态模式的核心,其结构如图3所示:

图3 状态模式结构图
在状态模式结构图中包含如下几个角色:
● Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例(State子类的对象)定义当前状态。
● State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
● ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
抽象状态类角色,其典型代码如下所示:
abstract class State { //声明抽象业务方法,不同的具体状态类可以不同的实现 public abstract void handle(); }
典型的具体状态类代码如下所示:
class ConcreteState extends State { public void handle() { //方法具体实现代码 } }
环境类维持一个对抽象状态类的引用,通过setState()方法可以向环境类注入不同的状态对象,再在环境类的业务方法中调用状态对象的方法,典型代码如下所示:
class Context { private State state; //维持一个对抽象状态对象的引用 private int value; //其他属性值,该属性值的变化可能会导致对象状态发生变化 //设置状态对象 public void setState(State state) { this.state = state; } public void request() { //其他代码 state.handle(); //调用状态对象的业务方法 //其他代码 } }
通常有两种实现状态转换的方式:
(1) 统一由环境类来负责状态之间的转换,此时,环境类还充当了状态管理器(State Manager)角色
…… public void changeState() { //判断属性值,根据属性值进行状态转换 if (value == 0) { this.setState(new ConcreteStateA()); } else if (value == 1) { this.setState(new ConcreteStateB()); } ...... } ……
(2) 由具体状态类来负责状态之间的转换
…… public void changeState(Context ctx) { //根据环境对象中的属性值进行状态转换 if (ctx.getValue() == 1) { ctx.setState(new ConcreteStateB()); } else if (ctx.getValue() == 2) { ctx.setState(new ConcreteStateC()); } ...... } ……
3 完整解决方案
Sunny软件公司开发人员使用状态模式来解决账户状态的转换问题,客户端只需要执行简单的存款和取款操作,系统根据余额将自动转换到相应的状态,其基本结构如图4所示:

图4 银行账户结构图
在图4中,Account充当环境类角色,AccountState充当抽象状态角色,NormalState、OverdraftState和RestrictedState充当具体状态角色。
Account环境类:
class Account { private AccountState state; //维持一个对抽象状态对象的引用 private String owner; //开户名 private double balance = 0; //账户余额 public Account(String owner,double init) { this.owner = owner; this.balance = balance; this.state = new NormalState(this); //设置初始状态 System.out.println(this.owner + "开户,初始金额为" + init); System.out.println("---------------------------------------------"); } public double getBalance() { return this.balance; } public void setBalance(double balance) { this.balance = balance; } public void setState(AccountState state) { this.state = state; } public void deposit(double amount) { System.out.println(this.owner + "存款" + amount); state.deposit(amount); //调用状态对象的deposit()方法 System.out.println("现在余额为"+ this.balance); System.out.println("现在帐户状态为"+ this.state.getClass().getName()); System.out.println("---------------------------------------------"); } public void withdraw(double amount) { System.out.println(this.owner + "取款" + amount); state.withdraw(amount); //调用状态对象的withdraw()方法 System.out.println("现在余额为"+ this.balance); System.out.println("现在帐户状态为"+ this. state.getClass().getName()); System.out.println("---------------------------------------------"); } public void computeInterest() { state.computeInterest(); //调用状态对象的computeInterest()方法 } }
抽象状态类:
abstract class AccountState { protected Account acc; public abstract void deposit(double amount); public abstract void withdraw(double amount); public abstract void computeInterest(); public abstract void stateCheck(); }
具体状态类:正常状态
class NormalState extends AccountState { public NormalState(Account acc) { this.acc = acc; } public NormalState(AccountState state) { this.acc = state.acc; } public void deposit(double amount) { acc.setBalance(acc.getBalance() + amount); stateCheck(); } public void withdraw(double amount) { acc.setBalance(acc.getBalance() - amount); stateCheck(); } public void computeInterest() { System.out.println("正常状态,无须支付利息!"); } //状态转换 public void stateCheck() { if (acc.getBalance() > -2000 && acc.getBalance() <= 0) { acc.setState(new OverdraftState(this)); } else if (acc.getBalance() == -2000) { acc.setState(new RestrictedState(this)); } else if (acc.getBalance() < -2000) { System.out.println("操作受限!"); } } }
具体状态类:透支状态:
class OverdraftState extends AccountState { public OverdraftState(AccountState state) { this.acc = state.acc; } public void deposit(double amount) { acc.setBalance(acc.getBalance() + amount); stateCheck(); } public void withdraw(double amount) { acc.setBalance(acc.getBalance() - amount); stateCheck(); } public void computeInterest() { System.out.println("计算利息!"); } //状态转换 public void stateCheck() { if (acc.getBalance() > 0) { acc.setState(new NormalState(this)); } else if (acc.getBalance() == -2000) { acc.setState(new RestrictedState(this)); } else if (acc.getBalance() < -2000) { System.out.println("操作受限!"); } } }
具体状态类:受限状态
class RestrictedState extends AccountState { public RestrictedState(AccountState state) { this.acc = state.acc; } public void deposit(double amount) { acc.setBalance(acc.getBalance() + amount); stateCheck(); } public void withdraw(double amount) { System.out.println("帐号受限,取款失败"); } public void computeInterest() { System.out.println("计算利息!"); } //状态转换 public void stateCheck() { if(acc.getBalance() > 0) { acc.setState(new NormalState(this)); } else if(acc.getBalance() > -2000) { acc.setState(new OverdraftState(this)); } } }
编写如下客户端测试代码:
浙公网安备 33010602011771号
编译并运行程序,输出结果如下:
4 共享状态
在有些情况下,多个环境对象可能需要共享同一个状态,如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。
下面通过一个简单实例来说明如何实现共享状态:
如果某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。
可以使用状态模式来实现开关的设计,其结构如图5所示:
开关类Switch代码如下所示:
抽象状态类如下代码所示:
两个具体状态类如下代码所示:
编写如下客户端代码进行测试:
输出结果如下:
开关1已经打开! 开关2已经打开! 开关1关闭! 开关2已经关闭! 开关2打开! 开关1已经打开!5 使用环境类实现状态转换
Sunny软件公司某开发人员欲开发一个屏幕放大镜工具,其具体功能描述如下:
用户单击“放大镜”按钮之后屏幕将放大一倍,再点击一次“放大镜”按钮屏幕再放大一倍,第三次点击该按钮后屏幕将还原到默认大小。
可以考虑使用状态模式来设计该屏幕放大镜工具,我们定义三个屏幕状态类NormalState、LargerState和LargestState来对应屏幕的三种状态,分别是正常状态、二倍放大状态和四倍放大状态,屏幕类Screen充当环境类,其结构如图6所示:
环境类(屏幕类):
抽象状态类:
具体状态类:
在上述代码中,所有的状态转换操作都由环境类Screen来实现,此时,环境类充当了状态管理器角色。
编写如下客户端代码进行测试:
输出结果如下:
正常大小! 二倍大小! 四倍大小! 正常大小!