设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
设计模式分为三种类型,共23种。
创建型模式(5):单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式(7):适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式(11):(父子类)策略模式、模版方法模式,(两个类)观察者模式、迭代器模式、职责链模式、命令模式,(类的状态)状态模式、备忘录模式,(中间类) 访问者模式、中介者模式、解释器模式。
“人有悲欢离合,月有阴晴圆缺”,包括人在内,很多事物都具有多种状态,而且在不同状态下会具有不同的行为,这些状态在特定条件下还将发生相互转换。就像水,它可以凝固成冰,也可以受热蒸发后变成水蒸汽,水可以流动,冰可以雕刻,蒸汽可以扩散。
在软件系统中,有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换,而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计,我们可以使用一种被称之为状态模式的设计模式,本章我们将学习用于描述对象状态及其转换的状态模式。
一.概述
银行系统中的账户类设计
Sunny软件公司欲为某银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,Sunny软件公司开发人员发现在该系统中,账户存在三种状态,且在不同状态下账户存在不同的行为,具体说明如下:
(1) 如果账户中余额大于等于0,则账户的状态为正常状态(Normal State),此时用户既可以向该账户存款也可以从该账户取款;
(2) 如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(Overdraft State),此时用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息;
(3) 如果账户中余额等于-2000,那么账户的状态为受限状态(Restricted State),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息;
(4) 根据余额的不同,以上三种状态可发生相互转换。
部分代码如下:
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):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
State Pattern:Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.
结构
在状态模式中引入了抽象状态类和具体状态类,它们是状态模式的核心,其结构如图3所示:
在状态模式结构图中包含如下几个角色:
- Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
- State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
- ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
实现
典型代码如下所示:
public abstract class State {
//声明抽象业务方法,不同的具体状态类可以不同的实现
public abstract void handle();
}
public class ConcreteState extends State {
public void handle() {
//方法具体实现代码
//setState(this);//状态转换
}
}
public class 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) 统一由环境类来负责状态之间的转换,此时,环境类还充当了状态管理器(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());
}
......
}
……
二.示例
银行账户
Sunny软件公司开发人员使用状态模式来解决账户状态的转换问题,客户端只需要执行简单的存款和取款操作,系统根据余额将自动转换到相应的状态.
Account充当环境类角色,AccountState充当抽象状态角色,NormalState、OverdraftState和RestrictedState充当具体状态角色。
public class AccountStateClient {
public static void main(String args[]) {
Account acc = new Account("段誉", 0.0);
acc.deposit(1000);
acc.withdraw(2000);
acc.deposit(3000);
acc.withdraw(4000);
acc.withdraw(1000);
acc.computeInterest();
}
}
class Account {
private AccountState state;// 当前状态
private double balance;// 余额
private String owner;// 开户
public Account(String owner, double balance) {
this.owner = owner;
this.state = new NormalState(this);
this.balance = balance;
System.out.println(this.owner + "开户,初始金额为" + balance);
System.out.println("---------------------------------------------");
}
// 存款
public void deposit(double amount) {
System.out.println(this.owner + "存款" + amount);
state.deposit(amount); // 调用状态对象的deposit()方法
stateCheck();
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()方法
stateCheck();
System.out.println("现在余额为" + this.balance);
System.out.println("现在帐户状态为" + this.state.getClass().getName());
System.out.println("---------------------------------------------");
}
// 计算利息
public void computeInterest() {
state.computeInterest(); // 调用状态对象的computeInterest()方法
}
public void stateCheck() {
if (getBalance() > 0) {
setState(new NormalState(this));
}else if (getBalance() > -2000 && getBalance() <= 0) {
setState(new OverdraftState(this));
} else if (getBalance() == -2000) {
setState(new RestrictedState(this));
} else if (getBalance() < -2000) {
System.out.println("操作受限");
}
}
public void setState(AccountState state) {
this.state = state;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
abstract class AccountState {
protected Account account;
public abstract void deposit(double amount);
public abstract void withdraw(double amount);
public abstract void computeInterest();
}
// 正常状态
class NormalState extends AccountState {
public NormalState(Account account) {
this.account = account;
}
@Override
public void deposit(double amount) {
account.setBalance(account.getBalance() + amount);
}
@Override
public void withdraw(double amount) {
account.setBalance(account.getBalance() - amount);
}
@Override
public void computeInterest() {
System.out.println("正常状态,无须支付利息!");
}
}
// 透支状态
class OverdraftState extends AccountState {
public OverdraftState(Account account) {
this.account = account;
}
@Override
public void deposit(double amount) {
account.setBalance(account.getBalance() + amount);
}
@Override
public void withdraw(double amount) {
account.setBalance(account.getBalance() - amount);
}
@Override
public void computeInterest() {
System.out.println("计算利息!");
}
}
// 受限状态
class RestrictedState extends AccountState {
public RestrictedState(Account account) {
this.account = account;
}
@Override
public void deposit(double amount) {
account.setBalance(account.getBalance() + amount);
}
@Override
public void withdraw(double amount) {
System.out.println("帐号受限,取款失败");
}
@Override
public void computeInterest() {
System.out.println("计算利息!");
}
}
输出结果:
段誉开户,初始金额为0.0
---------------------------------------------
段誉存款1000.0
现在余额为1000.0
现在帐户状态为NormalState
---------------------------------------------
段誉取款2000.0
现在余额为-1000.0
现在帐户状态为OverdraftState
---------------------------------------------
段誉存款3000.0
现在余额为2000.0
现在帐户状态为NormalState
---------------------------------------------
段誉取款4000.0
现在余额为-2000.0
现在帐户状态为RestrictedState
---------------------------------------------
段誉取款1000.0
帐号受限,取款失败
现在余额为-2000.0
现在帐户状态为RestrictedState
---------------------------------------------
计算利息!
放大镜
Sunny软件公司某开发人员欲开发一个屏幕放大镜工具,其具体功能描述如下:
用户单击“放大镜”按钮之后屏幕将放大一倍,再点击一次“放大镜”按钮屏幕再放大一倍,第三次点击该按钮后屏幕将还原到默认大小。
public class ScreenStateClient {
public static void main(String[] args) {
Screen screen = new Screen();
screen.onClick();
screen.onClick();
screen.onClick();
}
}
class Screen {
private ScreenState state, nomalState, largerState, largestState;
public Screen() {
state = nomalState = new ScreenNomalState();
largerState = new ScreenLargerState();
largestState = new ScreenLargestState();
state.display();
}
public void onClick() {
if (state == nomalState) {
state = largerState;
} else if (state == largerState) {
state = largestState;
} else if (state == largestState) {
state = nomalState;
}
state.display();
}
}
abstract class ScreenState {
public abstract void display();
}
class ScreenNomalState extends ScreenState {
@Override
public void display() {
System.out.println("正常大小!");
}
}
class ScreenLargerState extends ScreenState {
@Override
public void display() {
System.out.println("二倍大小!");
}
}
class ScreenLargestState extends ScreenState {
@Override
public void display() {
System.out.println("四倍大小!");
}
}
输出结果:
正常大小!
二倍大小!
四倍大小!
正常大小!
开关共享状态
在有些情况下,多个环境对象可能需要共享同一个状态,如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。
下面通过一个简单实例来说明如何实现共享状态:
例子:如果某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。
public class SwitchStateClient {
public static void main(String[] args) {
Switch s1 = new Switch("开关1");
Switch s2 = new Switch("开关2");
s1.on();
s2.on();
s1.off();
s2.off();
s2.on();
s1.on();
}
}
// 环境类
class Switch {
private SwitchState state;
private static SwitchState onState, offState; // 定义2个静态的状态对象
private String name;
public Switch(String name) {
this.name = name;
onState = new OnState();
offState = new OffState();
this.state = onState;
}
public void setState(SwitchState state) {
this.state = state;
}
public static SwitchState getState(String type) {
if (type.equalsIgnoreCase("on")) {
return onState;
} else {
return offState;
}
}
// 打开开关
public void on() {
System.out.print(name);
state.on(this);
}
// 关闭开关
public void off() {
System.out.print(name);
state.off(this);
}
}
abstract class SwitchState {
public abstract void on(Switch s);
public abstract void off(Switch s);
}
class OnState extends SwitchState {
public void on(Switch s) {
System.out.println("已经打开!");
}
public void off(Switch s) {
System.out.println("关闭!");
s.setState(Switch.getState("off"));
}
}
class OffState extends SwitchState {
public void on(Switch s) {
System.out.println("打开!");
s.setState(Switch.getState("on"));
}
public void off(Switch s) {
System.out.println("已经关闭!");
}
}
输出结果:
开关1已经打开!
开关2已经打开!
开关1关闭!
开关2关闭!
开关2打开!
开关1打开!
三.总结
优点如下:
在状态模式中,我们将对象在每一个状态下的行为和状态转移语句封装在一个个状态类中,使得对象看起来似乎修改了它的类,通过这些状态类来分散冗长的条件转移语句,让系统具有更好的灵活性和可扩展性。
缺点也是有的:
状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
以上大部分内容来自博文:JAVA设计模式(18):行为型-状态模式(State)
参考电子书下载:设计模式的艺术–软件开发人员内功修炼之道_刘伟(2013年).pdf
《道德经》第十一章:
三十辐共一毂(gu),当其无,有车之用。埏埴以为器,当其无,有器之用。凿户牖以为室,当其无,有室之用。故有之以为利,无之以为用。
译文:三十根辐条汇集到一根毂中的孔洞当中,有了车毂中空的地方,才有车的作用。揉和陶土做成器皿,有了器具中空的地方,才有器皿的作用。开凿门窗建造房屋,有了门窗四壁内的空虚部分,才有房屋的作用。所以,“有”给人便利,“无”发挥了它的作用。