设计模式——State(状态)模式

本文深入解析状态模式的概念,通过手机账户的实例,展示了如何利用状态模式处理对象在不同状态下的行为变化,强调了状态模式在复杂场景下的应用价值。

前言

体能状态先于精神状态,习惯先于决心,聚焦先于喜好。

状态模式

State 模式

书面用语

允许一个对象内部状态改变时改变它的行为。对象看起来改变了它的类。

状态模式的意图是将表示对象状态的逻辑分散到代表状态的不同类中。

大白话

设想你有一个对象,这个对象有一些特性,比如这个对象是一个手机账户,使用这个手机账户,你可以打电话,查询余额。但是你也应该知道,当余额不足的时候,就无法打电话了,因为账户此时的状态因为余额不足变成了冻结,再比如账户是销户,那么连余额也查询不了了。
如果让你写代码,代码结构极有可能是这样的:
调用打电话的方法-判断账户状态,只有在账户激活且余额充足的时候才允许打电话
调用查询余额的方法-判断账户状态,只有账户在非销户的状态下才可以
此外,当你的打电话后,余额不足了,账户的状态就会由可用变为冻结。
状态模式的处理方式是:将每一个状态下的行为单独放到一个类中,每一个类中针对每种行为各自进行定义,这样一来,一个对象在不同状态下的行为就固定了,你只需要在开始判断一次,然后调用对应的状态类方法即可。即销户的手机账户的打电话方法和查询余额方法都不可用。
状态类本身是没有主体的,它只能是一种行为,所以在状态模式中提供了一个上下文对象,通过上下文对象内部维护一个状态对象,上下文对象的状态对象要随着状态的变更改变实例化的对象——从而获得相应的状态下的行为。
由于状态是多个,而行为是特定的行为,所以要为状态提供一个抽象——抽象类或者接口,用以统一各状态的成员方法名称。
上下文对象一般来说是一个,所以可以是一个具体类,如果上下文对象也可能是多个,那么你可以使用父抽象类,此外,从行为上来说,上下文对象其实内部间接调用其内部的状态对象的成员方法,为了提高辨识度,其方法名称应该和状态类的方法名称尽可能一致。
再一点,对象不是孤立的,其自身有很多特性需要在状态方法中进行使用,比如打电话,余额就会减少,那么就需要在将上下文对象传递给状态对象,这样一来就变成了:上下文对象内部有一个状态类对象,而状态类对象又引用了上下文对象。
最后一点,由于状态的变更,上下文对象或者状态类对象都具有改变上下文对象状态的能力,但是出于统一处理的考虑,建议将该方法放到上下文中。

由于状态对象一般是不变的,所有我们会将其直接维护到上下文类中,即每一个状态类都会成为上下文的一个全局变量,这样在状态变更时无需从新实例化新的状态对象了。

吐槽一下

很复杂对吧,所以我觉得只有场景足够复杂时才值得使用状态模式,否则耦合度这么高的代码以后扩展起来必然要违反开闭原则—— 上下文类要添加新的状态类,如果新加了状态类型的话,上下文对象行为有变化,所有状态类都需要变更。

构造一个场景

这里我们依旧以手机账号打电话作为场景,并且为了简化问题,自定一些和实际生活不太相符的规则。

场景描述
  • 你有一个手机账户
  • 该手机账户有三种状态,激活、冻结、销户
  • 该手机账户有一个余额属性,当余额大于0时为激活状态;当余额为0时,账户状态将自动变为冻结。
  • 当手机账户为激活时,你可以打电话和查询余额
  • 当手机账户为冻结时,你只可以查询余额,而不能打电话
  • 当手机账户为销户时,你既不可以打电话,也不能查询余额
  • 每次打电话都会花费1元,并且当余额为0时,将手机状态修改为冻结
场景探究
  • 手机账户有多种状态,手机账户有多种功能,每种状态下,手机账户功能有所差异
  • 不同状态下,使用手机的功能有可能修改手机状态
  • 可以使用状态模式:手机状态建立一个抽象父类,三个子类,一个上下文类;上下文类内部维护一个状态对象;状态父类和子类维护一个上下文对象;上下文对象通过内部方法封装状态父类——根据具体状态由不同的子类实现;状态类内部根据自己维护的上下文对象获取余额——打电话减少额度的依据;上下文对象所子类维护余额、提供金额减少的同步方法、提供上下文对象变更的公共方法。
UML 图

在这里插入图片描述

关门,放代码

由于代码耦合度高,下面代码是逐步修改完成的
即状态抽象父类、上下文类、二者相互引用、状态子类、上下文类引用状态子类

本文代码不含包路径,如需复现请自行添加

抽象父类:StateAbstract

内部维护一个上下文对象

/**
 * 抽象状态 类
 * 定义上下文对象的基本行为的名称-由子类具体实现
 * @Author jie.wu
 */
public abstract class StateAbstract {
    /**内部维护一个上下文对象*/
    protected Context context;

    /**允许状态类获得上下文对象-从而获得一个额外属性*/
    public StateAbstract(Context context){
        this.context=context;
    }

    /**
     * 向 pnoneNumber 拨打电话的方法
     * */
    abstract void call(String phoneNumber);

    /**查询余额的方法*/
    abstract float searchBalance() throws Exception;
}
上下文类:Context

上下文类是外部调用的入口,相当于上下文类内部维护了一个可变的状态对象,然后对状态对象的方法进行封装,从上下文对象来看,由于内部状态对象变化,给上下文对象的功能带来了变化。
内部维护了所有状态对象的,如果有新加的状态对象记得修改

/**
 * 上下文类:
 * 提供对象属性;封装状态方法;提供公共方法
 * @Author jie.wu
 */
public class Context {
    /**手机号余额*/
    private float banlance;

    /**激活状态对象*/
    public final StateAbstract stateActive=new StateActive(this);
    /**冻结状态对象*/
    public final StateAbstract stateFreeze=new StateFreeze(this);
    /**注销状态对象*/
    public final StateAbstract stateCancel=new StateCancel(this);

    /**内部维护一个状态对象-由状态子类实现*/
    private StateAbstract state=stateActive;
    public StateAbstract getState() {
        return state;
    }
    public void setState(StateAbstract state) {
        this.state = state;
    }
    public float getBanlance() {
        return this.banlance;
    }
    public void setBanlance(float banlance) {
        this.banlance = banlance;
    }
    /*
    * 向 pnoneNumber 拨打电话的方法
    * */
    public void call(String phoneNumber){
        state.call(phoneNumber);
    }
    /**查询余额的方法*/
    public float searchBalance() throws Exception{
        return this.state.searchBalance();
    }
    /**
     * 余额减1
     */
    public synchronized float  balanceDecreaseOne(){
        return this.banlance--;
    }
}
状态子类——激活:StateActive

激活状态下可以打电话,每次打完电话余额减1,当余额为0时将上下文状态对象赋值为冻结状态对象

/**
 * 状态子类:激活 状态
 * @Author jie.wu
 */
public class StateActive extends StateAbstract {
    public StateActive(Context context) {
        super(context);
    }
    @Override
    void call(String phoneNumber) {
        System.out.println("激活状态,向"+phoneNumber+"拨打电话花费1元");
        //余额减1
        context.balanceDecreaseOne();
        //判断是否需要修改账户状态
        if(context.getBanlance()==0){
            //修改上下文状态为 冻结
            context.setState(context.stateFreeze);
        }
    }
    @Override
    float searchBalance() {
        System.out.println("激活状态查询余额:"+context.getBanlance());
        return context.getBanlance();
    }
}
状态子类——冻结:StateFreeze

冻结状态可以查询余额,但是不能打的那话

/**
 * 状态子类:冻结 状态
 * @Author jie.wu
 */
public class StateFreeze extends StateAbstract {
    public StateFreeze(Context context) {
        super(context);
    }
    @Override
    void call(String phoneNumber) {
        System.out.println("余额为0,冻结状态,无法提供电话服务");
    }
    @Override
    float searchBalance() {
        System.out.println("冻结状态查询余额:"+context.getBanlance());
        return context.getBanlance();
    }
}
状态子类——注销 :StateCancel

注销状态下,既不能打电话,也不能查询余额
为了保持抽象方法的统一,该子类在处理返回值方法的不可用表达上使用了抛出异常的处理

/**
 * 状态子类:注销 状态
 * @Author jie.wu
 */
public class StateCancel extends StateAbstract {
    public StateCancel(Context context) {
        super(context);
    }
    @Override
    void call(String phoneNumber) {
        System.out.println("账户注销,无法拨打电话");
    }
    @Override
    float searchBalance() throws Exception{
        throw new Exception("账户注销,无法查询余额");
    }
}
测试方法和结果

需要在开始对上下文对象进行初始化-金额和初始状态
依次测试:
1、激活状态打电话、查余额
2、激活状态方法判断上下文状态修改为冻结
3、冻结状态打电话、查余额
4、手动将上下文状态修改为 注销
5、注销状态下打电话、查余额

从方法调用上看,上下文对象在不同状态下调用 打电话方法和查余额方法的形式是一致的——就好像你通过状态修改了上下文对象的功能一样

import org.junit.Test;
public class StateTest {
    @Test
    public void testState(){
        Context context=new Context();
        //初始化余额-2元
        context.setBanlance(2);
        System.out.println("初始化余额查询:"+context.getBanlance());

        String phoneNumber="15000000000";
        //1打电话
        context.call(phoneNumber);
        try{
            System.out.println("余额:"+context.searchBalance());
        }catch(Exception e){
            System.out.println("异常"+e);
        }
        System.out.println();

        //2打电话
        context.call(phoneNumber);
        try{
            System.out.println("余额:"+context.searchBalance());
        }catch(Exception e){
            System.out.println("异常"+e);
        }
        System.out.println();

        //3打电话
        context.call(phoneNumber);
        try{
            System.out.println("余额:"+context.searchBalance());
        }catch(Exception e){
            System.out.println("异常"+e);
        }
        System.out.println();

        //4打电话
        //将上下文状态修改为 注销
        context.setState(context.stateCancel);
        context.call(phoneNumber);
        try{
            System.out.println("余额:"+context.searchBalance());
        }catch(Exception e){
            System.out.println("异常"+e);
        }
    }
}
  • 测试结果展示
初始化余额查询:2.0
激活状态,向15000000000拨打电话花费1元
激活状态查询余额:1.0
余额:1.0

激活状态,向15000000000拨打电话花费1元
冻结状态查询余额:0.0
余额:0.0

余额为0,冻结状态,无法提供电话服务
冻结状态查询余额:0.0
余额:0.0

账户注销,无法拨打电话
异常java.lang.Exception: 账户注销,无法查询余额

观察者模式的专业术语

观察者模式有三个基本组成部分
  • Context(环境,上下文):比如上文的 Context类,维护一个ConcreteState 子类实例

  • State (状态):定义一个接口(或抽象类)以封装与Context 的一个特定状态相关的行为

  • ConcreteState subclasses:具体状态子类,每一个子类实现一个和Context的一个状态相关的行为

三个基本组成部分的协作
  • Context 将状态相关的操作委托给内部的 ConcreteState对象处理
  • Context 对象本身可以传递给 ConcreteState对象,即可以提供一些变量和方法,也允许自身状态被修改
  • Context 是客户使用的主要接口,理论上来说,客户都是通过Context对象间接操作系统中所有的 Concretestate 对象的——Context内部包含了所有ConcreteState的实例
  • Context 和 Concretestate 都可以决定 状态的变化规则

状态模式的适用场景

显而易见,状态模式下系统的耦合度在增加,所有只有你在不适用状态模式下系统显得很复杂时才值得使用状态模式。
何为复杂?
当系统中的对象有很多不同的状态,并且不同状态下的行为有所差异时。

参考链接

[1]、https://www.cnblogs.com/nnn123/p/6723729.html
[2]、https://www.runoob.com/design-pattern/state-pattern.html
[3]、http://c.biancheng.net/view/1388.html
[4]、https://blog.youkuaiyun.com/qq_40369829/article/details/80370618

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值