橘子学设计模式之状态模式

一、概述

目前此模式仍需在实际中使用,我目前用的比较少。
在软件系统中,有些对象具有多种状态,这些状态在某些情况下能够相互转换, 而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行 设计,我们可以使用一种被称之为状态模式的设计模式,来处理对象状态的切换,以及在各个状态下的特定的一些功能方法。

二、设计需求

现在要开发一个系统,系统中有一个银行账户(Account)是该系统的核心类之 一,通过分析,在该系统中,账户存在三种状态,且在不同状态下账户存在不同的行为,具体说明如下:
(1) 如果账户中余额大于等于0,则账户的状态为正常状态(Normal State),此时用户既可以向该账户存款也可以从该账户取款;
(2) 如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(Overdraft State),此 用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息;
(3) 如果账户中余额等于-2000,那么账户的状态为受限状态(Restricted State),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息;
(4) 根据余额的不同,以上三种状态可发生相互转换。具体的uml类图如下所示。
在这里插入图片描述
其中NormalState表示正常状态,OverdraftState表示透支状态,RestrictedState表示受限状态,在这三种状态下账户对象拥有不同的行为。
方法deposit()用于存款,withdraw()用于取 款,computeInterest()用于计算利息,stateCheck()用于在每一次执行存款和取款操作后根据余 额来判断是否要进行状态转换并实现状态转换,相同的方法在不同的状态中可能会有不同的 实现。
为了实现不同状态下对象的各种行为以及对象状态之间的相互转换,Sunny软件公司开 发人员设计了一个较为庞大的账户类Account,其中伪代码如下所示:

1、原始代码

class Account {
  private String state; //状态 
  private int balance; //余额 
  ...... 
  //存款操作 
  public void deposit() { 
  	//存款 
  	stateCheck(); 
  }
  //取款操作 
  public void withdraw() { 
  	if (state.equalsIgnoreCase("NormalState") || state.equalsIgnoreCase("OverdraftState ")) { 
  	//取款 
  	stateCheck(); 
  	}else {
  		//取款受限 
  	} 
  }
  //计算利息操作
  public void computeInterest() { 
  	if(state.equalsIgnoreCase("OverdraftState") ||state.equalsIgnoreCase("RestrictedState ")) { 
  	//计算利息 
  } 
 }
 	//状态检查和转换操作 
 	public void stateCheck() { 
 		if (balance >= 0) { 
 			state = "NormalState"; 
 		}else if (balance > -2000 && balance < 0) { 
 			state = "OverdraftState"; 
 		}else if (balance == -2000) { 
 			state = "RestrictedState"; 
 		}else if (balance < -2000) { 
 			//操作受限 
 		} 
 	}
 	......
}

以上代码结构设计存在一个问题:
(1) 几乎每个方法中都包含状态判断语句,以判断在该状态下是否具有该方法以及在特定状态下该方法如何实现,导致代码非常冗长,可维护性较差;
(2) 拥有一个较为复杂的stateCheck()方法,包含大量的if…else if…else…语句用于进行状态转 换,代码测试难度较大,且不易于维护;
(3) 系统扩展性较差,如果需要增加一种新的状态,如冻结状态(Frozen State,在该状态下既 不允许存款也不允许取款),需要对原有代码进行大量修改,几乎所有涉及的状态的代码块都要做修改,扩展起来非常麻烦。
为了解决这些问题,我们可以使用状态模式,在状态模式中,我们将对象在每一个状态下的 行为和状态转移语句封装在一个个状态类中,通过这些状态类来分散冗长的条件转移语句, 让系统具有更好的灵活性和可扩展性,状态模式可以在一定程度上解决上述问题。

2、状态模式的分析

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

也就是说,以前我们有问题,是因为我们把状态和判断全部耦合在一起了,那你必然耦合度高。
状态模式是把所有的状态一个个的拆开成一个个的类,每个类做自己的事情,这样耦合度低了,而且符合单一对象原则。

状态模式的设计:

在状态模式中引入了抽象状态类和具体状态类,它们是状态模式的核心,其结构如图所示:
在这里插入图片描述
如图所示在状态模式结构图中包含如下几个角色:

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

在状态模式中,我们将对象在不同状态下的行为封装到不同的状态类中,为了让系统具有更 好的灵活性和可扩展性,同时对各状态下的共有行为进行封装,我们需要对状态进行抽象(面向接口开发的思路), 引入了抽象状态类角色:

3、状态模式优化需求

开发人员使用状态模式来解决账户状态的转换问题,客户端只需要执行简单的存款和取款操作,不需要知道状态切换,和如何切换,切换交给模式设计了,系统根据余额将自动转换到相应的状态,其基本结构如图所示:
在这里插入图片描述

环境中心类:这里统一调度状态

package com.yx.state;

/**
 * @description:银行账户:环境类,这里暴露给用户,所有的曹邹都在这里,不管别的这里封装状态
 * @author:YX
 * @version:1.0
 */
public class Account {
	//维持一个对抽象状态对象的引用,传入具体的状态然后根据这个转态具体调度具体的实现,这里封装了状态
	// 由这个传进来的状态去走自己的状态逻辑
    private AccountState state; 
    private String owner; //开户名
    private double balance = 0; //账户余额
    public Account(String owner,double init) {
        this.owner = owner;
        this.balance = balance;
        this.state = new NormalState(this); //设置初始状态
        System.out.println(this.owner + "开户,初始金额为" + init);
        System.out.println("---------------------------------------------");
    }
    public double getBalance() {
        return this.balance;
    }
    public void setBalance(double balance) {
        this.balance = balance;
    }
    public void setState(AccountState state) {
        this.state = state;
    }
    public void deposit(double amount) {
        System.out.println(this.owner + "存款" + amount);
        // 走这个你传进来的状态的那个存款方法,你传那个就是哪个,然后你在你的哪个状态里面的方法里面,记得		
        // 根据余额切换当前状态
        state.deposit(amount); //调用状态对象的deposit()方法
        System.out.println("现在余额为"+ this.balance);
        System.out.println("现在帐户状态为"+ this.state.getClass().getName());
        System.out.println("---------------------------------------------");
    }
    public void withdraw(double amount) {
        System.out.println(this.owner + "取款" + amount);
        state.withdraw(amount); //调用状态对象的withdraw()方法
        System.out.println("现在余额为"+ this.balance);
        System.out.println("现在帐户状态为"+ this. state.getClass().getName());
        System.out.println("---------------------------------------------");
    }
    public void computeInterest() {
        state.computeInterest(); //调用状态对象的computeInterest()方法
    }
}

状态类:设计成抽象的,方便后面扩展

package com.yx.state;

/**
 * @description:抽象状态类
 * @author:YX
 * @version:1.0
 */
abstract class AccountState {
	// 注入一个环境类
    protected Account acc;
    // 存款操作
    public abstract void deposit(double amount);
    // 取款操作
    public abstract void withdraw(double amount);
    // 计算利息
    public abstract void computeInterest();
    // 状态检查和转换操作
    public abstract void stateCheck();
}

具体状态,每一种状态都拆分出一个类

package com.yx.state;

/**
 * @description:正常状态:具体状态类
 * @author:YX
 * @version:1.0
 */
class NormalState extends AccountState {
    // 注入环境类
    public NormalState(Account acc) {
        this.acc = acc;
    }
    // 存款
    public NormalState(AccountState state) {
        this.acc = state.acc;
    }
    public void deposit(double amount) {
        acc.setBalance(acc.getBalance() + amount); stateCheck();
    }
    // 退款
    public void withdraw(double amount) {
        acc.setBalance(acc.getBalance() - amount);
        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("操作受限!");
        }
    }
}
package com.yx.state;

/**
 * @description:透支状态:具体状态类
 * @author:YX
 * @version:1.0
 */
class OverdraftState extends AccountState {

    public OverdraftState(AccountState state) {
        this.acc = state.acc;
    }
    public void deposit(double amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }
    public void withdraw(double amount) {
        acc.setBalance(acc.getBalance() - amount);
        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("操作受限!");
        }
    }
}
package com.yx.state;

/**
 * @description:受限状态:具体状态类
 * @author:YX
 * @version:1.0
 */
class RestrictedState extends AccountState {
    public RestrictedState(AccountState state) {
        this.acc = state.acc;
    }
    public void deposit(double amount) {
        acc.setBalance(acc.getBalance() + amount);
        stateCheck();
    }
    public void withdraw(double 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));
        }
    }
}

客户端:注意这里是理解重点

package com.yx.state;

/**
 * @description:客户端调用
 * @author:YX
 * @version:1.0
 */
public class Client {
    public static void main(String args[]) {
    	// 用户A,初始账户余额是0,开始走逻辑了
        Account acc = new Account("用户A", 0.0);
        //  存款1000
        acc.deposit(1000);
        // 扣款2000
        acc.withdraw(2000);
        acc.deposit(3000);
        acc.withdraw(4000);
        acc.withdraw(1000);
        // 核算利息
        acc.computeInterest();
    }
}
用户1开户,初始金额为0.0
---------------------------------------------
用户1存款1000.0
现在余额为1000.0
现在帐户状态为com.yx.state.NormalState
---------------------------------------------
用户1取款2000.0
现在余额为-1000.0
现在帐户状态为com.yx.state.OverdraftState
---------------------------------------------
用户1存款3000.0
现在余额为2000.0
现在帐户状态为com.yx.state.NormalState
---------------------------------------------
用户1取款4000.0
现在余额为-2000.0
现在帐户状态为com.yx.state.RestrictedState
---------------------------------------------
用户1取款1000.0
帐号受限,取款失败
现在余额为-2000.0
现在帐户状态为com.yx.state.RestrictedState
---------------------------------------------
计算利息!

我们看到上面的方式你要是新增一个状态,就不需要都改了,只新增一个,然后改下各个状态实现类里面的状态流转方法,不需要修改原来的逻辑,只需要加逻辑,是符合开闭原则的。
但是有一个不好的地方,就是每个状态实现类里面都有状态切换的方法,你要改都得改,这样其实还是有点耦合。
实际上我们既然有了一个环境类,那就可以把这个切换状态的逻辑统一放到环境类里面。
上面那个不想改了,我们以一个简单的需求说明一下问题:

三、新需求

欲开发一个屏幕放大镜工具,其具体功能描述如下:
用户单击“放大镜”按钮之后屏幕将放大一倍,
再点击一次“放大镜”按钮屏幕再放大一倍,
第三 次点击该按钮后屏幕将还原到默认大小。
可以考虑使用状态模式来设计该屏幕放大镜工具,我们定义三个屏幕状态类NormalState、 LargerState和LargestState来对应屏幕的三种状态,分别是正常状态、二倍放大状态和四倍放大 状态,屏幕类Screen充当环境类,其结构如图所示:
在这里插入图片描述

环境类,中心处理

package com.yx.state2;

/**
 * @description:环境类:屏幕类
 * @author:YX
 * @version:1.0
 */
public class Screen {
    //枚举所有的状态,currentState表示当前状态,实际上这里的状态可以通过配置,启动的时候放到一个全局的map重,无需罗列出来,看你自己的想法吧
    private State currentState, normalState, largerState, 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();
        }
    }
}

抽象状态类:

package com.yx.state2;

/**
 * @description:抽象状态类
 * @author:YX
 * @version:1.0
 */
abstract class State {
    public abstract void display();
}

具体状态实现:

package com.yx.state2;

/**
 * @description:正常状态类
 * @author:YX
 * @version:1.0
 */
class NormalState extends State{
    public void display() {
        System.out.println("正常大小!");
    }
}

package com.yx.state2;

/**
 * @description:二倍状态类
 * @author:YX
 * @version:1.0
 */
class LargerState extends State{
    public void display() {
        System.out.println("二倍大小!");
    }
}

package com.yx.state2;

/**
 * @description:四倍状态类
 * @author:YX
 * @version:1.0
 */
class LargestState extends State{
    public void display() {
        System.out.println("四倍大小!");
    }
}

客户端调用:

package com.yx.state2;

/**
 * @description:客户端代码
 * @author:YX
 * @version:1.0
 */
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) 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维 护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值