22.状态模式(行为型)

状态模式(行为型)

原书链接设计模式(刘伟)

在软件系统中,有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换,而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计,我们可以使用一种被称之为状态模式的设计模式进行系统设计。

一、相关概述

1). 概述

状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。

  • 状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。

状态模式.png

2). 相关角色

  1. Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
  2. State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
  3. ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。

3). 典型代码

  1. 抽象状态类
/**
 * 抽象状态类
 */
abstract class State {
    // 声明抽象业务方法不同的具体状态类可以不同的实现
    public abstract void handle();

    // 【可选】在状态类中根据环境类属性值进行状态转换:当然也可以写在环境类中。
    public void changeState(Context ctx) {
        // 状态转换逻辑
    }
}

抽象状态类中可以实现通用的方法

  1. 环境类
/**
 * 环境类
 */
class Context {
    // 维持一个对抽象状态的引用
    private State state;

    // 其他属性值,该属性值的变化可能会导致对象状态发生变化
    private int value;

    // 设置对象状态
    public void steState(State state) {
        this.state = state;
    }

    // 环境类中,根据属性值进行状态转换:当然,也可以在状态类中实现
    public void changeState() {
        // 判断转换代码
    }

    public void request() {
        // 其他代码

        // 调用状态对象的业务方法
        state.handle();

        // 其他代码
    }
}

在实际使用时,它们之间可能存在更为复杂的关系,State与Context之间可能也存在依赖或者关联关系。

  1. 具体状态类
/**
 * 状态实例类
 */
class ConcreteState extends State {

    @Override
    public void handle() {
        // 方法具体实现代码
    }

}

二、案例需求演示

1). 需求描述

Sunny软件公司欲为某银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,Sunny软件公司开发人员发现在该系统中,账户存在三种状态,且在不同状态下账户存在不同的行为,具体说明如下:

  1. 如果账户中余额大于等于0,则账户的状态为正常状态(Normal State),此时用户既可以向该账户存款也可以从该账户取款;
  2. 如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(Overdraft State),此时用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息;
  3. 如果账户中余额等于-2000,那么账户的状态为受限状态(Restricted State),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息;
  4. 根据余额的不同,以上三种状态可发生相互转换。

2). 类图设计


状态模式账户.png

3). 完整代码

  1. 环境类
/**
 * 环境类:银行账户
 */
public class Account {

    // 维持一个对抽象状态对象的引用
    private AccountState state;

    // 开户名
    private String owner;

    // 账户余额
    private Long balance = 0L;

    public Account (String owner, Long balance) {
        this.owner = owner;
        this.balance = balance;
        // 设置初始状态
        this.state = new NormalState(this);

        System.out.println(this.owner + "开户,初始金额为" + this.balance + "\n---------------------------------------------");
    }

    public Long getBalance() {
        return this.balance;
    }

    public void setBalance(Long balance) {
        this.balance = balance;
    }

    public void setState(AccountState state) {
        this.state = state;
    }

    // 存款
    public void deposit(Long amount) {
        System.out.println(this.owner + "存款" + amount);
        state.deposit(amount); // 调用状态对象的deposit()方法
        System.out.println("现在余额为"+ this.balance);
        System.out.println("现在帐户状态为"+ this.state.getClass().getName() + "\n---------------------------------------------");
    }

    // 取款
    public void withdraw(Long amount) {
        System.out.println(this.owner + "取款" + amount);
        state.withdraw(amount); //调用状态对象的withdraw()方法
        System.out.println("现在余额为"+ this.balance);
        System.out.println("现在帐户状态为"+ this. state.getClass().getName() + "\n---------------------------------------------");
    }

    // 计算利息
    public void computeInterest() {
        state.computeInterest(); //调用状态对象的computeInterest()方法
    }
}
  1. 抽象状态类
/**
 * 抽象状态类
 */
public abstract class AccountState {
    protected Account acc; // 维持一个账户的引用,子类可以访问
    public abstract void deposit(Long amount); // 存款
    public abstract void withdraw(Long amount); // 取款
    public abstract void computeInterest(); // 计算利息
    public abstract void stateCheck(); // 检查状态
}
  1. 具体状态类
/**
 * 正常状态:具体状态类
 */
public class NormalState extends AccountState {

    public NormalState(Account acc) { this.acc = acc; }

    public NormalState(AccountState state) { this.acc = state.acc; }

    // 存钱
    public void deposit(Long amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }

    // 取款
    public void withdraw(Long amount) {
        Long tempBalance = acc.getBalance() - amount;

        if (tempBalance < -2000) {
            System.out.println("取款额度超过上限,请重新输入金额!");
            return;
        }

        acc.setBalance(tempBalance);
        stateCheck();
    }

    // 计算利息
    public void computeInterest() {
        System.out.println("正常状态,无须支付利息!");
    }

    // 状态转换
    public void stateCheck() {

        if (acc.getBalance() > -2000 && acc.getBalance() <= 0) {
            acc.setState(new OverdraftState(this));
        } else if (acc.getBalance() == -2000) {
            acc.setState(new RestrictedState(this));
        } else if (acc.getBalance() < -2000) {
            System.out.println("操作受限!");
        }
    }
}


/**
 * 透支状态:具体状态类
 */
public class OverdraftState extends AccountState {
    public OverdraftState(AccountState state) { this.acc = state.acc; }

    // 存钱
    public void deposit(Long amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }

    // 取钱
    public void withdraw(Long amount) {
        Long tempBalance = acc.getBalance() - amount;

        if (tempBalance < -2000) {
            System.out.println("取款额度超过上限,请重新输入金额!");
            return;
        }

        acc.setBalance(tempBalance);
        stateCheck();
    }

    // 计算利息
    public void computeInterest() {
        System.out.println("计算利息!");
    }

    //状态转换
    public void stateCheck() {

        if (acc.getBalance() > 0) {
            acc.setState(new NormalState(this));
        } else if (acc.getBalance() == -2000) {
            acc.setState(new RestrictedState(this));
        } else if (acc.getBalance() < -2000) {
            System.out.println("操作受限!");
        }
    }
}


/**
 * 受限状态:具体状态类
 */
public class RestrictedState extends AccountState {
    public RestrictedState(AccountState state) { this.acc = state.acc; }

    // 存钱
    public void deposit(Long amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }

    // 取款
    public void withdraw(Long amount) {
        System.out.println("帐号受限,取款失败");
    }

    // 计算利息
    public void computeInterest() {
        System.out.println("计算利息!");
    }

    //状态转换
    public void stateCheck() {
        if(acc.getBalance() > 0) {
            acc.setState(new NormalState(this));
        } else if(acc.getBalance() > -2000) {
            acc.setState(new OverdraftState(this));
        }
    }
}
  1. 客户端测试类
/**
 * @author Liucheng
 * @since 2019-09-13
 */
public class Client {
    public static void main(String[] args) {
        Account acc = new Account("段誉",0L);
        acc.deposit(1000L);
        acc.withdraw(2000L);
        acc.deposit(3000L);
        acc.withdraw(4000L);
        acc.withdraw(1000L);
        acc.computeInterest();
    }
}

三、拓展

1). 共享状态

在有些情况下,多个环境对象可能需要共享同一个状态,如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。

  1. 案例需求描述

某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。

  1. 类图设计

状态模式开关.png
  1. 完整代码
/**
 * 环境类
 */
public class Switch {

    //定义三个静态的状态对象,用于对象共享
    private static State state;
    private static State onState;
    private static State offState;

    private String name;

    public Switch(String name) {
        this.name = name;
        onState = new OnState();
        offState = new OffState();
        this.state = onState;
    }

    public void setState(State state) {
        this.state = state;
    }

    public static State getState(String type) {
        if (type.equalsIgnoreCase("on")) {
            return onState;
        }
        else {
            return offState;
        }
    }

    //打开开关
    public void on() {
        System.out.print(name);
        state.on(this);
    }

    //关闭开关
    public void off() {
        System.out.print(name);
        state.off(this);
    }

}
/**
 * 抽象状态类
 */
public abstract class State {
    public abstract void on(Switch s);
    public abstract void off(Switch s);
}


/**
 * 打开状态
 */
public class OnState extends State {
    public void on(Switch s) {
        System.out.println("已经打开!");
    }
    public void off(Switch s) {
        System.out.println("关闭!");
        s.setState(Switch.getState("off"));
    }
}


/**
 * 关闭状态
 */
public class OffState extends State {

    public void on(Switch s) {
        System.out.println("打开!");
        s.setState(Switch.getState("on"));
    }

    public void off(Switch s) {
        System.out.println("已经关闭!");
    }
}

/**
 * @author Liucheng
 * @since 2019-09-14
 */
public class Client {

    public static void main(String[] args) {
        Switch s1 = new Switch("开关1");
        Switch s2 = new Switch("开关2");

        s1.on();
        s2.on();

        s1.off();
        s2.off();

        s2.on();
        s1.on();
    }
}

2). 使用环境类实现状态转换

  1. 需求描述
    用户单击“放大镜”按钮之后屏幕将放大一倍,再点击一次“放大镜”按钮屏幕再放大一倍,第三次点击该按钮后屏幕将还原到默认大小。

  2. 类图设计


放大镜.png
  1. 完整代码
/**
 * 环境类:屏幕类
 */
public class Screen {
    // 枚举所有的状态,currentState表示当前状态
    private State currentState;
    private State normalState;
    private State largerState;
    private State largestState;

    public Screen() {
        this.normalState = new NormalState(); // 创建正常状态对象
        this.largerState = new LargerState(); // 创建二倍放大状态对象
        this.largestState = new LargestState(); // 创建四倍放大状态对象
        this.currentState = normalState; // 设置初始状态
        this.currentState.display();
    }

    public void setState(State state) { this.currentState = state; }

    // 单击事件处理方法,封转了对状态类中业务方法的调用和状态的转换
    public void onClick() {
        if (this.currentState == normalState) {
            this.setState(largerState);
            this.currentState.display();
        } else if (this.currentState == largerState) {
            this.setState(largestState);
            this.currentState.display();
        } else if (this.currentState == largestState) {
            this.setState(normalState);
            this.currentState.display();
        }
    }
}
/**
 * 抽象状态类
 */
public abstract class State {
    public abstract void display();
}

/**
 * 正常状态类
 */
public class NormalState extends State{
    public void display() {
        System.out.println("正常大小!");
    }
}

/**
 * 二倍状态类
 */
public class LargerState extends State{
    public void display() {
        System.out.println("二倍大小!");
    }
}

/**
 * 四倍状态类
 */
public class LargestState extends State{
    public void display() {
        System.out.println("四倍大小!");
    }
}
/**
 * 客户端测试
 * @author Liucheng
 * @since 2019-09-14
 */
public class Client {

    public static void main(String[] args) {
        Screen screen = new Screen();
        screen.onClick();
        screen.onClick();
        screen.onClick();
    }
}

四、总结

状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流和游戏开发中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。

1). 优点

  1. 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
  2. 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
  3. 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
  4. 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

2). 缺点

  1. 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
  2. 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。
  3. 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

3). 适用场景

  1. 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化。
  2. 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容