likes
comments
collection
share

状态模式思考 - 当有多组关联状态该如何使用

作者站长头像
站长
· 阅读数 12

前言

状态模式(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,这个相信都能理解,就不赘述了

结语

篇幅有点长了

感谢能有耐心阅读到这里👍👍👍

这个重构例子里是我为了深入学习状态模式而设想的,有不妥之处还请指出。代码都可以复制出来运行,有兴趣的话可以跑一跑哈哈

欢迎讨论、指正~