状态模式——根据状态来分离和选择行为

  状态模式适用于不同的状态有不同的逻辑的场景,其中心思想就是根据状态分离和选择行为,体现在编程上就是分离大量的if … else语句。我们日常编程中处处可以见到状态模式思想的应用,比如我们可以根据一个字段的不同枚举值,需要有不同的业务逻辑走向,这就是典型的状态模式的应用场景,只是实际的编程中我们利用数据库将保存字段的值和判断逻辑这两个代码进行了分离,所以就很少用得上传统的状态模式的写法而已。
  怎么理解状态模式这个分离和选择行为呢?我们可以想想这样一个场景:比如我们向公司领导请假,每一级别的领导能批的假期天数是不一样的,如果只是一天,直属的项目主管就可以决定,如果是一周,可能就要部门经理审批才行,如果是一个月呢?可能需要总经理才能审批。在这个场景中,我们把不同级别的领导抽象成不同的审批状态,那领导的审批动作就是对应的行为,如果不用状态模式的时候,我们可能需要根据请假的天数,用 if … else if … else 语句进行大量的嵌套来实现这个逻辑,利用状态模式,我们首先就可以将用天数的if判断和用审批的if判断来分开,也就是将状态和行为进行分开,抽象出了不同的状态类,在这个状态类中,我们又将审批的动作抽象成一个方法,就是将特定的状态和对应的行为进行了一个选择绑定。所以说,状态模式分离的是状态判断和行为判断,选择的是特定状态和特定的行为,分离和选择是一个先后递进的过程

状态模式结构图

状态机模式结构图

  • Context : 上下文,通常定义客户感兴趣的接口,同时维护一个当前的状态示例对象,真正的行为调用是调用的当前状态对象的行为。
  • State:状态接口,用来封装统一的状态和行为,通常会持有上下文类,在状态改变的时候,改变上下文文类中的当前状态
  • ConcreteState:具体的状态的实现。

状态模式示例

  我们设想一个投票的场景,某个系统需要用户进行投票,但是又不能让客户重复投票。为了防止这种恶意投票,我们定义除了这样的一个规则:第一次投票是正常的投票,投票作数,如果反复投票3次以内,我们后面就提醒他是重复投票,如果在5次以内,就是恶心投票,进行警告恶意投票,如果大于5次,就拉入黑名单,投票作废。这个场景中,我们可以将不同阶段的投票次数抽象成对应的状态,也就有正常投票、重复投票、恶意投票和黑名单四个状态,不同的状态中,如果处理这种场景,就是不同的行为。特定状态有特定的处理行为。代码示例如下:

1. 定义投票的状态接口

/**
 * 投票状态接口
 */
public interface VoteState {

    /**
     * 投票方法
     *
     * @param userId :
     * @param context :投票的上下文,
     * 当状态很多的时候,我们可以将上下文传递给状态对象,让状态对象来改变上下文中的当前状态,这样更加方便状态的扩展
     */
    void vote(String userId, VoteContext context);
}

2. 设计上下文CoteContext类

  在状态模式中,客户端是不用感知具体的状态对象的,它只针对上下文进行操作,具体怎么实现,是上下文的功能,所以,上下文中一般会持有一个当前状态的对象。在实际的编程中,上下文一般会有两种的设计方式:

  • 其一是上下文中维护了所有的状态示例,客户端操作上下文的时候,让上下文来选择具体的状态对象,然后调用其行为,这样的上下文和状态对象是强耦合,同时在上下文中也需要用if来判断到底使用哪个状态对象,这种场景一般用在状态很少且后期不会扩展的场景,比如开门和关门等;
  • 另一种实现是上下文只持有一个当前状态,具体下一个状态怎么流转,让当前状态去决定,然后回调上下文设置上当前状态即可,上下文并不会去创建具体的状态示例,这样的实现可以增加扩展性,如果有新的状态加入,我们只需要在上一个状态中增加即可,也会避免了很多的if判断。本示例中,我们就采用第二种实现方式。

  在投票示例中,我们在上下文中维护两个Map,一个用来存放用户和当前的状态userCurrentStateMap,一个用来存放用户的投票次数userVoteCount,然后写一个供具体状态对象回调设置状态的方法setCurrentState(),客户端每次只需要调用投票方法vote()就可以,其内部的状态流转客户端并不感知。

/**
 * 投票上下文类
 */
public class VoteContext {
    /**
     * 存放用户的当前状态
     */
    private final Map<String, VoteState> userCurrentStateMap;

    /**
     * 存放用户的投票次数
     */
    private final Map<String, Integer> userVoteCount;

    public VoteContext() {
        userCurrentStateMap = new HashMap<>();
        userVoteCount = new HashMap<>();
    }

    public int getVoteCount(String userId) {
        if (userVoteCount.containsKey(userId)) {
            return userVoteCount.get(userId);
        }
        return 0;
    }

    /**
     * 封装一个增加投票次数的方法,供状态类调用
     * @param userId :
     */
    public void plusVoteCount(String userId) {
        if (userVoteCount.containsKey(userId)) {
            int plusCount = userVoteCount.get(userId) + 1;
            userVoteCount.put(userId, plusCount);
        } else {
            userVoteCount.put(userId, 1);
        }
    }

    /**
     * 设置当前的状态
     * @param userId :
     * @param currentState :
     */
    public void setCurrentState(String userId, VoteState currentState) {
        userCurrentStateMap.put(userId, currentState);
    }

    /**
     * 投票方法,如果是第一次投票,就新建一个正常投票的状态,如果不是,就直接交给当前的状态处理
     * @param userId :
     */
    public void vote(String userId) {
        VoteState currentState = userCurrentStateMap.containsKey(userId)
            ? userCurrentStateMap.get(userId)
            : new NormalVoteState();
        currentState.vote(userId, this);
    }
}

3. 实现具体的状态对象

/**
 * 正常投票:第一次投票算正常投票
 */
public class NormalVoteState implements VoteState{
    @Override
    public void vote(String userId, VoteContext context) {
        // 判断投票次数,如果为0,则是正常投票,否则交给重复投票
        int voteCount = context.getVoteCount(userId);
        if (voteCount == 0){
            System.out.println("投票成功");
            context.plusVoteCount(userId);
        }else {
            context.setCurrentState(userId,new RepeatVoteState());
            context.vote(userId);
        }
    }
}
/**
 * 重复投票:大于1,小于等于3属于重复投票
 */
public class RepeatVoteState implements VoteState{
    @Override
    public void vote(String userId, VoteContext context) {
        // 判断投票次数,如果小于等于3,提示重复投票,否则交给恶意投票
        int voteCount = context.getVoteCount(userId);
        if (voteCount <= 3 ){
            System.out.println("您已经投过票,请不要重复投票!!");
            context.plusVoteCount(userId);
        }else {
            context.setCurrentState(userId,new SpiteVoteState());
            context.vote(userId);
        }
    }
}
/**
 * 恶意投票:大于3,小于等于5属于恶意
 */
public class SpiteVoteState implements VoteState{
    @Override
    public void vote(String userId, VoteContext context) {
        // 判断投票次数,如果小于等于5,提示恶意投票,否则交给黑名单
        int voteCount = context.getVoteCount(userId);
        if (voteCount <= 5){
            System.out.println("检测到你多次恶意投票,请停止该行为,否则会加入黑名单并取消投票资格!!!");
            context.plusVoteCount(userId);
        }else {
            context.setCurrentState(userId,new BlackVoteState());
            context.vote(userId);
        }
    }
}
/**
 * 投票黑名单:大于5,加入黑名单
 */
public class BlackVoteState implements VoteState{
    @Override
    public void vote(String userId, VoteContext context) {
        // 提示加入黑名单,取消投票资格
        System.out.println("检测到您多次投票,已加入投票黑名单,并取消投票资格!!!");
    }
}

4. 编写一个测试类

public class Client {

    @Test
    public void testVote(){
        VoteContext voteContext = new VoteContext();
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
        voteContext.vote("aaa");
    }
}

测试结果如下:

投票成功
您已经投过票,请不要重复投票!!
您已经投过票,请不要重复投票!!
您已经投过票,请不要重复投票!!
检测到你多次恶意投票,请停止该行为,否则会加入黑名单并取消投票资格!!!
检测到你多次恶意投票,请停止该行为,否则会加入黑名单并取消投票资格!!!
检测到您多次投票,已加入投票黑名单,并取消投票资格!!!
检测到您多次投票,已加入投票黑名单,并取消投票资格!!!
检测到您多次投票,已加入投票黑名单,并取消投票资格!!!
检测到您多次投票,已加入投票黑名单,并取消投票资格!!!

5. 示例代码结构图

投票示例结构图

状态模式总结

  • 状态模式和策略模式的区别:很多人会发现,状态模式和策略模式都是上下文中持有一个对象的引用,然后客户端调用的时候有上下文调用具体的实现,好像很相似,实际上这两个有本质的区别:
    • 状态模式强调的是内部状态的流转,客户端只会调用上下文的方法,并不会感知到内部状态的变化,内部状态是系统内部实现的;但是策略模式强调的是用户来选择具体的策略实现,也就是说用户是要感知具体的实现类的
    • 关于策略模式的内容,可以查看文章《策略模式——分离算法,选择实现
  • 状态模式和责任链模式的区别:看了上面的示例代码,有的人就会有一个疑惑,由具体的状态对象去创建下一个状态对象,似乎这个创建就形成了一条链,那就和责任链模式很像了,状态模式和责任链模式区别如下:
    • 状态模式的上一个状态来创建下一个状态,写法上看着好像是串成了一条链,但是每次调用并不是真正的一条链,因为状态模式保存了当前的状态,下次调用的时候,并不是从头开始的,而是从当前状态开始的;但是责任链模式是构建的时候就构建成了一条链,客户端每次调用都是从头开始的。
    • 关于责任链模式,可以参考文章《责任链模式——分离职责,动态组合
  • 需要强调的是:设计模式强调的是解决特定问题的思路,而不是具体的代码实现,我们在学习设计模式的时候,很多时候会看到不同的设计模式实现方式好像差不多,那为啥还叫另一个名字?之所以会产生这种疑惑,主要是我们还是没有跳出具体的实现方式来看待设计模式,设计模式是思路的总结,而不是强调具体的实现,因为面向对象的设计语言中,无非就是继承、多态、封装这些特性,不用继承,就要用组合,这种语言上的特点就决定了不同的设计模式,在代码落地的时候还是要归结到继承和组合的选择上来,就会有这种看着很相似的现象。还有就是我们能列举出来的示例代码也有一定的局限性,因为我们能想到的,也是有些相似的,总有一些特定的场景比较适合特定的模式,只是我们为了方便理解,选择的示例实现比较相似而已。

后记
  个人总结,欢迎转载、评论、批评指正

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值