状态模式
案例
张三所在公司欲开发一款纸牌游戏软件,在该游戏软件中用户角色具有入门级(Primary)、熟练级(Secondary)和高手级(Professional)三种等级,角色的等级与其积分相对应,游戏胜利将增加积分,失败则扣除积分。入门级具有最基本的游戏功能 ,熟练级增加了游戏胜利积分加倍功能,高手级在熟练级基础上再增加换牌功能。用程序来表示如下:
1.首先定义了 Player
类:
public class Player {
// 初始积分为 0
private int integral = 0;
// 初始等级为入门级
private String grade = "入门级";
public void play() {
System.out.println("开始游戏");
System.out.println("当前积分为:" + this.integral + "\t当前等级为:" + this.grade);
System.out.println("游戏进行中");
changeCards();
System.out.println("游戏结束");
// 这里方便演示结果,直接设置为胜利
System.out.println("游戏胜利,获得100积分");
score();
changeGrade();
System.out.println("当前积分为:" + this.integral + "\t当前等级为:" + this.grade);
}
// 换牌操作
private void changeCards() {
if (grade.equals("Primary") || grade.equals("Secondary")) {
System.out.println("该等级太低,不支持换牌操作");
} else if (grade.equals("Professional")) {
System.out.println("换牌成功");
}
}
// 计算得分
private void score() {
int integral = 100;
if (grade.equals("Primary")) {
System.out.println("正常累加" + integral + "积分");
} else if (grade.equals("Secondary") || grade.equals("Professional")) {
integral *= 2;
System.out.println("获得双倍积分" + integral + "积分");
}
this.integral += integral;
}
// 转换等级
private void changeGrade() {
// 这里假设小于 100 积分为入门级,100-300 为熟练级,大于等于 300 为高手级
if (this.integral < 100) {
this.grade = "入门级";
} else if (this.integral < 300) {
this.grade = "熟练级";
} else {
this.grade = "高手级";
}
}
}
2.客户端使用:
public class Main {
public static void main(String[] args) {
Player player = new Player();
player.play();
System.out.println("--------------------------------------");
player.play();
System.out.println("--------------------------------------");
player.play();
}
}
3.使用结果:
开始游戏
当前积分为:0 当前等级为:入门级
游戏进行中
该等级太低,不支持换牌操作
游戏结束
游戏胜利,获得100积分
正常累加100积分
当前积分为:100 当前等级为:熟练级
--------------------------------------
开始游戏
当前积分为:100 当前等级为:熟练级
游戏进行中
该等级太低,不支持换牌操作
游戏结束
游戏胜利,获得100积分
获得双倍积分200积分
当前积分为:300 当前等级为:高手级
--------------------------------------
开始游戏
当前积分为:300 当前等级为:高手级
游戏进行中
换牌成功
游戏结束
游戏胜利,获得100积分
获得双倍积分200积分
当前积分为:500 当前等级为:高手级
可以看到玩家通过纸牌游戏,随着积分的不断变化,玩家等级会在三个等级之间相互转化(这里方便演示结果,直接设置为胜利)。但是这里的changeCards()
和score()
方法都包含状态判断语句,以判断不同状态下该方法如何实现,导致代码非常冗长,可维护性较差。同时也导致了扩展新的状态会修改多个地方。为了解决这些问题,可以使用设计模式中的状态模式对其进行一定的改进,使代码具有更好的灵活性和可扩展性。
模式介绍
(源于Design Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
角色构成
Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
UML 类图
代码改造
下面就通过上面的介绍对纸牌游戏进行改造,
1.首先定义出状态接口:
public abstract class State {
public abstract String getName();
protected abstract void changeCards();
protected abstract int score(int integral);
}
2.三种等级的状态类:
入门级:
public class PrimaryState extends State {
@Override
public String getName() {
return "入门级";
}
@Override
protected void changeCards() {
System.out.println("该等级太低,不支持换牌操作");
}
@Override
protected int score(int integral) {
System.out.println("正常累加100积分");
return integral;
}
}
熟练级:
public class SecondaryState extends State {
@Override
public String getName() {
return "熟练级";
}
@Override
protected void changeCards() {
System.out.println("该等级太低,不支持换牌操作");
}
@Override
protected int score(int integral) {
integral *= 2;
System.out.println("获得双倍积分" + integral + "积分");
return integral;
}
}
高手级:
public class ProfessionalState extends State {
@Override
public String getName() {
return "高手级";
}
@Override
protected void changeCards() {
System.out.println("换牌成功");
}
@Override
protected int score(int integral) {
integral *= 2;
System.out.println("获得双倍积分" + integral + "积分");
return integral;
}
}
3.很显然这里的环境(Context)角色就是Player
类:
public class Player {
// 初始积分为 0
private int integral = 0;
// 初始等级为入门级
private State state = new PrimaryState();
public void play() {
System.out.println("开始游戏");
System.out.println("当前积分为:" + this.integral + "\t当前等级为:" + this.state.getName());
System.out.println("游戏进行中");
changeCards();
System.out.println("游戏结束");
// 这里方便演示结果,直接设置为胜利
System.out.println("游戏胜利,获得100积分");
score();
changeGrade();
System.out.println("当前积分为:" + this.integral + "\t当前等级为:" + this.state.getName());
}
// 换牌操作
private void changeCards() {
state.changeCards();
}
// 计算得分
private void score() {
int integral = 100;
this.integral += state.score(integral);
}
// 转换等级
private void changeGrade() {
if (this.integral < 100) {
this.state = new PrimaryState();
} else if (this.integral < 300) {
this.state = new SecondaryState();
} else {
this.state = new ProfessionalState();
}
}
}
4.客户端使用
public class Main {
public static void main(String[] args) {
Player player = new Player();
player.play();
System.out.println("--------------------------------------");
player.play();
System.out.println("--------------------------------------");
player.play();
}
}
5.使用结果:
开始游戏
当前积分为:0 当前等级为:入门级
游戏进行中
该等级太低,不支持换牌操作
游戏结束
游戏胜利,获得100积分
正常累加100积分
当前积分为:100 当前等级为:熟练级
--------------------------------------
开始游戏
当前积分为:100 当前等级为:熟练级
游戏进行中
该等级太低,不支持换牌操作
游戏结束
游戏胜利,获得100积分
获得双倍积分200积分
当前积分为:300 当前等级为:高手级
--------------------------------------
开始游戏
当前积分为:300 当前等级为:高手级
游戏进行中
换牌成功
游戏结束
游戏胜利,获得100积分
获得双倍积分200积分
当前积分为:500 当前等级为:高手级
可以看到,现在玩家类通过状态模式的改造,代码简洁了许多,主要是去掉了changeCards()
和score()
方法中的状态判断,而结果也是和改造前一样的。同时代码扩展性也是很强的,比如要增加一个状态,只需要增加一个状态类,并修改changeGrade()
方法就可以了。
总结
1.主要优点
- 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
- 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
2.主要缺点
- 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。
- 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
3.适用场景
- 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化。
- 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/state
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/6a21eb22e68b