状态模式思考 - 当有多组关联状态该如何使用
前言
状态模式(State Pattern)乍一看挺简单的。它的定义👇
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类
用大白话说就是:同一个对象,不同状态有不同的行为
脑测了一下觉得没啥
模式初识
状态模式定义了一套状态流转机制,开发能用到,值得深入学习
阿里规约里也提到超过3层的if-else
可以使用状态模式实现
通用实现
通用实现很简单,UML类图如下:
- State:状态接口,封装状态行为
- ConcreteState:具体实现状态处理的类
- Context:持有所有状态的实例,定义客户端需要的接口,并提供状态切换方法
简单示例
以订单创建->支付为例,状态流转为:
⭐⭐⭐如果分不出什么是状态、什么是方法,可以把状态流转图画出来,圆圈里的就是状态,箭头就是方法
先定义一个订单状态接口
public interface IOrderState {
void create(OrderContext orderContext);
void pay(OrderContext orderContext);
}
实现待创建、待支付、支付成功3种状态
// 待创建
public class Create implements IOrderState {
@Override
public void create(OrderContext orderContext) {
System.out.println("create():订单创建...创建成功");
orderContext.setCurrState(orderContext.waitPay);
}
@Override
public void pay(OrderContext orderContext) {
System.out.println("pay():请先创建订单");
}
}
// 待支付
public class WaitPay implements IOrderState {
@Override
public void create(OrderContext orderContext) {
System.out.println("create():订单已创建");
}
@Override
public void pay(OrderContext orderContext) {
System.out.println("pay():支付中...支付完成");
orderContext.setCurrState(orderContext.paySuccess);
}
}
// 支付成功
public class PaySuccess implements IOrderState {
@Override
public void create(OrderContext orderContext) {
System.out.println("create():订单已创建,请勿重复创建");
}
@Override
public void pay(OrderContext orderContext) {
System.out.println("pay():已支付,请勿重复支付");
}
}
创建 OrderContext
public class OrderContext {
IOrderState create = new Create();
IOrderState waitPay = new WaitPay();
IOrderState paySuccess = new PaySuccess();
private IOrderState currState;
public OrderContext() {
//默认初始化创建订单
this.currState = create;
}
public void setCurrState(IOrderState currState) {
this.currState = currState;
}
// 创建订单
public void create() {
currState.create(this);
}
// 支付订单
public void pay() {
currState.pay(this);
}
}
OK,使用一下
public static void main(String[] args) {
OrderContext orderContext = new OrderContext();
orderContext.pay();
orderContext.create();
orderContext.pay();
orderContext.pay();
orderContext.create();
orderContext.pay();
}
/* 输出:
pay():请先创建订单
create():订单创建...创建成功
pay():支付中...支付完成
pay():已支付,请勿重复支付
create():订单已创建,请勿重复创建
pay():已支付,请勿重复支付*/
从输出内容可以看出,同一个OrderContext
执行create()、pay()
的输出不同
这就叫同一个对象,不同状态有不同的行为
和策略模式的区别
状态模式和策略模式挺像的,区别在于:
状态模式:状态的转换是“自动”,“无意识”的,用户不参与状态的改变
策略模式:会控制对象使用什么策略
优缺点
优点:
- 封装了状态转换规则,避免巨大的 if-else 块
- 增强了程序的可拓展性,可以方便的增加 ConcreteState
- 状态流转更清晰
缺点:
- 类变多了
- 对"开闭原则"的支持并不太好,修改 ConcreteState 的行为需修改对应类的源代码
重构多组关联状态
为了深入学习状态模式,搞了个较复杂的业务场景:
一个篮球运动员有3种投篮状态(差、一般、好),投篮状态会影响到他的投篮命中率。而且每投篮3次,3次投篮的结果会改变投篮状态
- 投篮状态差:30%命中率
- 投篮状态一般:50%命中率
- 投篮状态好:70%命中率
状态流转图为:
不使用状态模式
很简单,声明一个篮球运动员Player
,然后用 if-else 搞定:
public class Player {
enum StatusEnum {BAD, NORMAL, GOOD}
private StatusEnum currStatus;
private List<Integer> shotResults = new ArrayList<>();
public Player() {
currStatus = StatusEnum.NORMAL;
}
public void mood() {
if (StatusEnum.BAD.equals(currStatus)) {
System.out.println("==投篮状态差 -> 投篮命中率变为30%");
} else if (StatusEnum.NORMAL.equals(currStatus)) {
System.out.println("==投篮状态一般 -> 投篮命中率变为50%");
} else if (StatusEnum.GOOD.equals(currStatus)) {
System.out.println("==投篮状态好 -> 投篮命中率变为70%");
}
System.out.println("---------------------");
}
/**
* 投篮
* @param shotNum 投篮次数
*/
public void shot(int shotNum) {
for (int i = 1; i <= shotNum; i++) {
if (StatusEnum.BAD.equals(currStatus)) {
this.shotSubFunc(0.3F);
} else if (StatusEnum.NORMAL.equals(currStatus)) {
this.shotSubFunc(0.5F);
} else if (StatusEnum.GOOD.equals(currStatus)) {
this.shotSubFunc(0.7F);
}
if (i % 3 == 0) {
int sum = shotResults.stream().mapToInt(Integer::intValue).sum();
if (sum == 3) {
currStatus = StatusEnum.GOOD;
} else if (sum == 2 && StatusEnum.BAD.equals(currStatus)) {
currStatus = StatusEnum.NORMAL;
} else if (sum == 1) {
if (StatusEnum.NORMAL.equals(currStatus)) {
currStatus = StatusEnum.BAD;
} else if (StatusEnum.GOOD.equals(currStatus)) {
currStatus = StatusEnum.NORMAL;
}
} else if (sum == 0) {
currStatus = StatusEnum.BAD;
}
mood();
shotResults.clear();
}
}
}
/**
* 模拟在投篮命中率影响下的进球or不进球
* @param hitRate 投篮命中率
*/
private void shotSubFunc(float hitRate) {
Random random = new Random();
// 随机生成[0,10)
int currRes = random.nextInt(10);
if (currRes < hitRate * 10) {
shotResults.add(1);
System.out.println("进");
} else {
shotResults.add(0);
System.out.println("没进");
}
}
}
使用一下
public static void main(String[] args) {
Player player = new Player();
player.shot(15); // 投篮15次
}
/* 输出:
进
进
进
==投篮状态好 -> 投篮命中率变为70%
-----------------------------
没进
没进
进
==投篮状态一般 -> 投篮命中率变为50%
-----------------------------
没进
没进
进
==投篮状态差 -> 投篮命中率变为30%
-----------------------------
进
进
进
==投篮状态好 -> 投篮命中率变为70%
-----------------------------
没进
没进
进
==投篮状态一般 -> 投篮命中率变为50%
-----------------------------*/
使用状态模式重构
在这个业务场景下,投篮状态差、一般、好确实可以抽象成投篮状态
那么该如何使用状态模式重构呢?
需求分析
根据上面提到的状态流转图中圆圈里的是状态,箭头是方法的思想进行分析
很明显“投篮状态差、一般、好”可以做为一组状态
那“3投0中、3投1中、3投2中、3投3中”就只是方法吗?
- 在影响投篮状态时,
3投0中()、3投1中()、3投2中()、3投3中()
确实是方法(或者说是“事件”,这样能更容易理解些) - 但3投0中、3投1中、3投2中、3投3中也是一种得分状态
- 即,得分状态影响投篮状态
把得分状态的流转图画出来:
可以看到,得分状态中需要有投中()、未投中()
两种方法
代码实现
声明一个投篮状态上下文PlayerShotState
(光声明先啥也不写)
public class PlayerShotState {}
定义一个投篮状态接口IShotState
,包含投篮命中率、3投0中、3投1中、3投2中、3投3中
5个方法
// 投篮状态:差、一般、好
public interface IShotState {
// 投篮命中率
float hitRate();
// 3投0中
void shot3Hit0(PlayerShotState playerShotState);
// 3投1中
void shot3Hit1(PlayerShotState playerShotState);
// 3投2中
void shot3Hit2(PlayerShotState playerShotState);
// 3投3中
void shot3Hit3(PlayerShotState playerShotState);
}
实现投篮状态(这里面就定义了投篮状态的流转)
// 投篮状态差
public class Bad implements IShotState {
@Override
public float hitRate() {
return 0.3F;
}
@Override
public void shot3Hit0(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.bad);
}
@Override
public void shot3Hit1(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.bad);
}
@Override
public void shot3Hit2(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.normal);
}
@Override
public void shot3Hit3(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.good);
}
@Override
public String toString() {
return "投篮状态差 -> 投篮命中率变为" + hitRate();
}
}
// 投篮状态一般
public class Normal implements IShotState {
@Override
public float hitRate() {
return 0.5F;
}
@Override
public void shot3Hit0(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.bad);
}
@Override
public void shot3Hit1(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.bad);
}
@Override
public void shot3Hit2(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.normal);
}
@Override
public void shot3Hit3(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.good);
}
@Override
public String toString() {
return "投篮状态一般 -> 投篮命中率变为" + hitRate();
}
}
// 投篮状态好
public class Good implements IShotState {
@Override
public float hitRate() {
return 0.7F;
}
@Override
public void shot3Hit0(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.bad);
}
@Override
public void shot3Hit1(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.normal);
}
@Override
public void shot3Hit2(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.good);
}
@Override
public void shot3Hit3(PlayerShotState playerShotState) {
playerShotState.setCurrStatus(playerShotState.good);
}
@Override
public String toString() {
return "投篮状态好 -> 投篮命中率变为" + hitRate();
}
}
修改投篮状态上下文PlayerShotState
,持有所有投篮状态实例,并对外提供设置投篮状态、3投x中
等方法
public class PlayerShotState {
IShotState bad = new Bad();
IShotState normal = new Normal();
IShotState good = new Good();
private IShotState currShotStatus;
public PlayerShotState() {
currShotStatus = normal;
}
public IShotState getCurrShotStatus() {
return currShotStatus;
}
public void setCurrShotStatus(IShotState currShotStatus) {
this.currShotStatus = currShotStatus;
}
public void shot3Hit0() {
currShotStatus.shot3Hit0(this);
}
public void shot3Hit1() {
currShotStatus.shot3Hit1(this);
}
public void shot3Hit2() {
currShotStatus.shot3Hit2(this);
}
public void shot3Hit3() {
currShotStatus.shot3Hit3(this);
}
}
声明一个得分状态上下文PlayerScoreState
public class PlayerScoreState {}
定义一个得分状态接口IScoreState
,包含得分情况、投中、没投中
2个方法
// 得分状态:3投0中、3投1中、3投2中、3投3中
public interface IScoreState {
// 得分情况
String getScoreType();
// 投中
void hit(PlayerScoreState playerScoreState);
// 没投中
void notHit(PlayerScoreState playerScoreState);
}
实现得分状态(这里面就定义了得分状态的流转)
// 3投0中
public class ZeroScoreState implements IScoreState {
@Override
public String getScoreType() {
return "zero";
}
@Override
public void hit(PlayerScoreState playerScoreState) {
// 投中0->1
playerScoreState.setCurrScoreState(playerScoreState.oneScoreState);
}
@Override
public void notHit(PlayerScoreState playerScoreState) {
// 没投中0->0
playerScoreState.setCurrScoreState(playerScoreState.zeroScoreState);
}
@Override
public String toString() {
return "3投0中";
}
}
// 3投1中
public class OneScoreState implements IScoreState {
@Override
public String getScoreType() {
return "one";
}
@Override
public void hit(PlayerScoreState playerScoreState) {
// 投中1->2
playerScoreState.setCurrScoreState(playerScoreState.twoScoreState);
}
@Override
public void notHit(PlayerScoreState playerScoreState) {
// 没投中1->1
playerScoreState.setCurrScoreState(playerScoreState.oneScoreState);
}
@Override
public String toString() {
return "3投1中";
}
}
// 3投2中
public class TwoScoreState implements IScoreState {
@Override
public String getScoreType() {
return "two";
}
@Override
public void hit(PlayerScoreState playerScoreState) {
// 投中2->3
playerScoreState.setCurrScoreState(playerScoreState.threeScoreState);
}
@Override
public void notHit(PlayerScoreState playerScoreState) {
// 没投中2->2
playerScoreState.setCurrScoreState(playerScoreState.twoScoreState);
}
@Override
public String toString() {
return "3投2中";
}
}
// 3投3中
public class ThreeScoreState implements IScoreState {
@Override
public String getScoreType() {
return "three";
}
@Override
public void hit(PlayerScoreState playerScoreState) {
// 投中3->4(下一轮投篮)
}
@Override
public void notHit(PlayerScoreState playerScoreState) {
// 没投中3->3(下一轮投篮)
}
@Override
public String toString() {
return "3投3中";
}
}
修改得分状态上下文PlayerScoreState
,持有所有得分状态实例,并对外提供设置得分状态、投中、没投中、得分状态初始化(因为3投为一组,每投3次就要回归到0分状态)
等方法
public class PlayerScoreState {
IScoreState zeroScoreState = new ZeroScoreState();
IScoreState oneScoreState = new OneScoreState();
IScoreState twoScoreState = new TwoScoreState();
IScoreState threeScoreState = new ThreeScoreState();
private IScoreState currScoreState;
public PlayerScoreState() {
// 最初是0分状态
currScoreState = zeroScoreState;
}
public IScoreState getCurrScoreState() {
return currScoreState;
}
public void setCurrScoreState(IScoreState currScoreState) {
this.currScoreState = currScoreState;
}
public void hit() {
currScoreState.hit(this);
}
public void notHit() {
currScoreState.notHit(this);
}
public void init() {
currScoreState = zeroScoreState;
}
}
改造Player
public class Player {
// 投篮状态、得分状态算是Player内部属性
private PlayerShotState playerShotState;
private PlayerScoreState playerScoreState;
public Player() {
playerShotState = new PlayerShotState();
playerScoreState = new PlayerScoreState();
}
public void shot(int shotCount) {
for (int i = 1; i <= shotCount; i++) {
shotSubFunc();
if (i % 3 == 0) {
// 3投后投篮状态改变
impactByScore();
// 进球情况
System.out.print(playerScoreState.getCurrScoreState() + ",");
// 当前投篮状态
System.out.println(playerShotState.getCurrShotStatus());
System.out.println("-------------");
playerScoreState.init();
}
}
}
private void shotSubFunc() {
Random random = new Random();
// 随机生成[0,10)
int currRes = random.nextInt(10);
if (currRes < playerShotState.getCurrShotStatus().hitRate() * 10) {
System.out.println("进");
playerScoreState.hit();
} else {
System.out.println("没进");
playerScoreState.notHit();
}
}
// 得分状态和投篮状态间的映射,此处可以用策略模式进一步封装
private void impactByScore() {
switch (playerScoreState.getCurrScoreState().getScoreType()) {
case "zero":
playerShotState.shot3Hit0();
break;
case "one":
playerShotState.shot3Hit1();
break;
case "two":
playerShotState.shot3Hit2();
break;
case "three":
playerShotState.shot3Hit3();
break;
default:
System.out.println("未知得分");
break;
}
}
}
使用方式不变
public static void main(String[] args) {
Player player = new Player();
player.shot(15);
}
/* 输出:
没进
没进
没进
3投0中,投篮状态差 -> 投篮命中率变为0.3
-----------------------------------
没进
进
进
3投2中,投篮状态一般 -> 投篮命中率变为0.5
-----------------------------------
进
没进
没进
3投1中,投篮状态差 -> 投篮命中率变为0.3
-----------------------------------
进
没进
进
3投2中,投篮状态一般 -> 投篮命中率变为0.5
-----------------------------------
进
进
进
3投3中,投篮状态好 -> 投篮命中率变为0.7
-----------------------------------*/
用策略模式封装Player.impactByScore()
新建一个得分接口IScore
(抽象策略接口)
public interface IScore {
// 3投后的得分影响投篮状态
void impactShotState(PlayerShotState playerShotState);
}
实现得分接口(具体策略),3投得0分、1分、2分、3分后对投篮状态的影响:
public class ZeroScore implements IScore {
@Override
public void impactShotState(PlayerShotState playerShotState) {
playerShotState.shot3Hit0();
}
}
public class OneScore implements IScore {
@Override
public void impactShotState(PlayerShotState playerShotState) {
playerShotState.shot3Hit1();
}
}
public class TwoScore implements IScore {
@Override
public void impactShotState(PlayerShotState playerShotState) {
playerShotState.shot3Hit2();
}
}
public class ThreeScore implements IScore {
@Override
public void impactShotState(PlayerShotState playerShotState) {
playerShotState.shot3Hit3();
}
}
创建策略封装类ScoreContext
public class ScoreContext {
private IScore score;
public ScoreContext(String scoreType) {
if ("zero".equals(scoreType)) {
score = new ZeroScore();
} else if ("one".equals(scoreType)) {
score = new OneScore();
} else if ("two".equals(scoreType)) {
score = new TwoScore();
} else if ("three".equals(scoreType)) {
score = new ThreeScore();
}
}
public void impact(PlayerShotState playerShotState) {
score.impactShotState(playerShotState);
}
}
改造Player.impactByScore()
private void impactByScore() {
ScoreContext scoreContext = new ScoreContext(playerScoreState.getCurrScoreState().getScoreType());
scoreContext.impact(playerShotState);
}
改造值得吗?
个人认为,是值得的
首先,从代码层面做到了解耦,虽然 class 变多了但状态流转却更清晰、改变状态流转也更容易,比如投篮状态好时,若3投1中那么投篮状态直接变为差(红色箭头和绿色箭头):
那么直接修改Good
即可:
public class Good implements IShotState {
@Override
public void shot3Hit1(PlayerShotState playerShotState) {
// playerShotState.setCurrShotStatus(playerShotState.normal);
playerShotState.setCurrShotStatus(playerShotState.bad);
}
}
只要重构后能达到状态流转更清晰的效果,那么 class 变多就不是个问题
其次,就是状态模式的另一个优点:可以方便的增加 ConcreteState,这个相信都能理解,就不赘述了
结语
篇幅有点长了
感谢能有耐心阅读到这里👍👍👍
这个重构例子里是我为了深入学习状态模式而设想的,有不妥之处还请指出。代码都可以复制出来运行,有兴趣的话可以跑一跑哈哈
欢迎讨论、指正~
转载自:https://juejin.cn/post/7228990110384980028