状态模式

状态模式

案例

张三所在公司欲开发一款纸牌游戏软件,在该游戏软件中用户角色具有入门级(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 类图

state-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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342