设计模式之状态模式(State Pattern)
一.什么是状态模式?
把所有动作都封装在状态对象中,状态持有者将行为委托给当前状态对象
也就是说,状态持有者(比如汽车,电视,ATM机都有多个状态)并不知道动作细节,状态持有者只关心自己当前所处的状态(持有的状态对象是哪个),再把一切事情都交给当前状态对象去打理就好了,甚至都不用去控制状态切换(当然,状态持有者有权利控制状态切换,也可以选择做甩手掌柜。。)
二.举个例子
假设我们要模拟一个ATM机,有以下需求:
- 取款,验证卡密,吐出现钞,结束服务
- 若卡密验证失败或者余额不足,则直接弹出卡片,结束本次服务
- 机器内无存钞,显示No Cash,并向银行发送无钞信息
- 机器故障,显示No Service,并向维修人员发送维修请求
那么用户取款的过程应该是这样的:
这是用户操作的流程,对于ATM机而言还需要注意下面几点:
- 获得用户输入金额后,不仅要验证用户卡内余额,还要验证ATM机内余额
- 每一次成功的取款服务结束后,都要检查ATM机内余额(若无钞则进行相应处理)
- 任何环节出现无法处理的错误,都按照故障来处理
-------
想想上面的取款过程,我们还必须考虑各种非法操作,比如:
- 不插卡直接输入密码
- 机器故障时仍然进行操作
- 机内无钞时要求取款
细分之后,我们会发现细节问题变得很麻烦,机器就摆在那里,所有的用户接口都开放着,用户完全有可能在任何时候使用任何接口
为了防止这些非法操作,我们不得不添加一系列的判断,比如:
- 验证密码之前应该检查是否已经插卡,以及机器是否发生了故障
- 插卡之前应该检查机器是否发生了故障
- 取款操作之前应该检查机器是否故障,是否无钞,是否。。。
我们的代码中需要设置大量的成员变量,用来标识机器的状态,更可怕的是我们的逻辑块里面存在大量的if-else,而且每一个操作里面的if-else块都只有细微的差别,看起来像冗余,但又不好处理(即使我们把每一个判断都提出来作为独立的方法,可以缩减代码规模,但在处理过程上仍然是冗余。。)
更好的处理方法就是使用状态模式,能够完全消除状态判断部分的冗余,并提供清晰整洁的代码结构
-------
下面用状态模式来实现例子中的需求
(1)首先找出ATM提供的所有接口
- 插卡
- 提交密码
- 取款(假设取款按钮是物理键)
- 查询(假设同上)
- 取卡
(2)再找出ATM的所有状态以及各个状态对应的动作
- 准备就绪(Ready),可用接口:全部
- 无钞(NoCash),可用接口:1,2,4,5
- 故障(NoService),可用接口:无
(3)编码实现
先定义State基类,类中封装了(1)列出的所有接口:
package StatePattern; /**
* 定义ATM机状态
* @author ayqy
*/
public interface ATMState {
/**
* 插卡
*/
public abstract void insertCard(); /**
* 提交密码
*/
public abstract void submitPwd(); /**
* 取款
*/
public abstract void getCash(); /**
* 查询余额
*/
public abstract void queryBalance(); /**
* 取卡
*/
public abstract void ejectCard();
}
再逐一实现三个状态
ReadyState:
package StatePattern; /**
* 实现ATM就绪状态
* @author ayqy
*/
public class ReadyState implements ATMState{
private ATM atm;//保留状态持有者的引用,以便对其进行操作 public ReadyState(ATM atm){
this.atm = atm;
} @Override
public void insertCard() {
System.out.println("插卡完成");
} @Override
public void submitPwd() {
System.out.println("密码提交完成");
//验证密码并做相应处理
if("123".equals(atm.getPwd())){
System.out.println("密码验证通过");
}
else{
System.out.println("密码验证失败");
//弹出卡片
ejectCard();
}
} @Override
public void getCash() {
if(atm.getTotalAmount() >= atm.getAmount() && atm.getBalance() >= atm.getAmount()){
//更新账户余额
atm.setBalance(atm.getBalance() - atm.getAmount());
//更新机内现钞总数
atm.setTotalAmount(atm.getTotalAmount() - atm.getAmount());
System.out.println("吐出¥" + atm.getAmount());
System.out.println("取款完成");
//弹出卡片
ejectCard();
//检查机内余钞
if(atm.getTotalAmount() == 0){//若无钞,切换到NoService状态
atm.setCurrState(atm.getNoCashState());
System.out.println("无钞信息已经发送至银行");
}
}
else{
System.out.println("取款失败,余额不足");
//弹出卡片
ejectCard();
}
} @Override
public void queryBalance() {
System.out.println("余额¥" + atm.getBalance());
System.out.println("余额查询完成");
} @Override
public void ejectCard() {
System.out.println("取卡完成");
}
}
注意我们在状态类中进行状态切换的部分:
if(atm.getTotalAmount() == 0){//若无钞,切换到NoService状态
atm.setCurrState(atm.getNoCashState());
}
我们并不是直接new具体状态对象,而是使用了ATM提供的set接口,这样做是为了尽量的解耦(兄弟对象彼此之间并不认识),获取更多的弹性
实现NoCashState:
package StatePattern; /**
* 实现ATM无钞状态
* @author ayqy
*/
public class NoCashState implements ATMState{
private ATM atm;//保留状态持有者的引用,以便对其进行操作 public NoCashState(ATM atm){
this.atm = atm;
} @Override
public void insertCard() {
System.out.println("插卡完成");
} @Override
public void submitPwd() {
System.out.println("密码提交完成");
//验证密码并做相应处理
if("123".equals(atm.getPwd())){
System.out.println("密码验证通过");
}
else{
System.out.println("密码验证失败");
//弹出卡片
ejectCard();
}
} @Override
public void getCash() {
System.out.println("取款失败,机内无钞");
} @Override
public void queryBalance() {
System.out.println("余额¥" + atm.getBalance());
System.out.println("余额查询完成");
} @Override
public void ejectCard() {
System.out.println("取卡完成");
}
}
实现NoServiceState:
package StatePattern; /**
* 实现ATM故障状态
* @author ayqy
*/
public class NoServiceState implements ATMState{
private ATM atm;//保留状态持有者的引用,以便对其进行操作 public NoServiceState(ATM atm){
this.atm = atm;
} @Override
public void insertCard() {
System.out.println("插卡失败,机器发生了故障");
} @Override
public void submitPwd() {
System.out.println("密码提交失败,机器发生了故障");
} @Override
public void getCash() {
System.out.println("取款失败,机器发生了故障");
} @Override
public void queryBalance() {
System.out.println("余额查询失败,机器发生了故障");
} @Override
public void ejectCard() {
System.out.println("取卡失败,机器发生了故障");
}
}
实现了具体的状态,就可以构造ATM类了,就像这样:
package StatePattern; /**
* 实现ATM机
* @author ayqy
*/
public class ATM {
/*所有状态*/
private ATMState readyState;
private ATMState noCashState;
private ATMState noServiceState; private ATMState currState;//当前状态
private int totalAmount;//机内现钞总数 /*测试用的临时变量*/
private String pwd;//密码
private int balance;//余额
private int amount;//取款金额 public ATM(int totalAmount, int balance, int amount, String pwd) throws Exception{
//初始化所有状态
readyState = new ReadyState(this);
noCashState = new NoCashState(this);
noServiceState = new NoServiceState(this); if(totalAmount > 0){
currState = readyState;
}
else if(totalAmount == 0){
currState = noCashState;
}
else{
throw new Exception();
} //初始化测试数据
this.totalAmount = totalAmount;
this.balance = balance;
this.amount = amount;
this.pwd = pwd;
} /*把具体行为委托给状态对象*/
/**
* 插卡
*/
public void insertCard(){
currState.insertCard();
} /**
* 提交密码
*/
public void submitPwd(){
currState.submitPwd();
} /**
* 取款
*/
public void getCash(){
currState.getCash();
} /**
* 查询余额
*/
public void queryBalance(){
currState.queryBalance();
} /**
* 取卡
*/
public void ejectCard(){
currState.ejectCard();
} public String toString(){
return "现钞总数¥" + totalAmount;
} /*此处略去大量getter and setter*/
}
一切都做好了,迫不及待的测试一下吧
三.运行示例
首先实现一个Test类:
package StatePattern; import java.util.Scanner; /**
* 实现测试类
* @author ayqy
*/
public class Test {
public static void main(String[] args) {
/*测试数据*/
/* 机内总数 账户余额 取款金额 密码
* 1000 500 200 123
* 1000 300 500 123
* 0 500 200 123
* */
try {
test(1000, 500, 200, "123");
System.out.println("-------");
test(1000, 300, 500, "123");
System.out.println("-------");
test(0, 500, 200, "123");
} catch (Exception e) {
System.out.println("机器故障,维修请求已经发送至维修方");
}
} private static void test(int totalAmount, int balance, int amount, String pwd)throws Exception{
//创建ATM
ATM atm;
atm = new ATM(totalAmount, balance, amount, pwd);
//输出初始状态
System.out.println(atm.toString());
atm.insertCard();
atm.submitPwd();
atm.getCash();
//输出结束状态
System.out.println(atm.toString());
}
}
我们设计的三个用例(正常取款,取大于余额的款,机内无现钞)运行结果如下:
四.状态模式与策略模式
还记得策略模式吗?难道不觉得这两者很相像吗?
没错,这两种模式的类图是完全相同的,解释一下:
- 状态主体(拥有者)持有状态对象,运行时可以通过动态指定状态对象来改变类的行为
- 策略主体持有算法族对象,运行时可以通过动态选择算法族中的算法(策略)来改变类的行为
也就是说,状态模式与策略模式都支持运行时的多态,并且其实现方式都是组合 + 委托
但是这并不代表这两种模式是相同的,因为它们的目标不同:
- 状态模式实现了算法流程可变(即状态切换,不同的状态有不同的流程)
- 策略模式实现了算法细节可选(即选择算法族内的算法,一个算法族包含多个可选算法)
设计模式之状态模式(State Pattern)的更多相关文章
- 乐在其中设计模式(C#) - 状态模式(State Pattern)
原文:乐在其中设计模式(C#) - 状态模式(State Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 状态模式(State Pattern) 作者:webabcd 介绍 允 ...
- 二十四种设计模式:状态模式(State Pattern)
状态模式(State Pattern) 介绍允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它所属的类. 示例有一个Message实体类,对它的操作有Insert()和Get()方法, ...
- [设计模式] 20 状态模式 State Pattern
在GOF的<设计模式:可复用面向对象软件的基础>一书中对状态模式是这样说的:允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它的类.状态模式的重点在于状态转换,很多时候,对 ...
- 【UE4 设计模式】状态模式 State Pattern
概述 描述 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类. 其别名为状态对象(Objects for States),状态模式是一种对象行为型模式. 有限状态机(FSMs) ...
- 【转】设计模式 ( 十七) 状态模式State(对象行为型)
设计模式 ( 十七) 状态模式State(对象行为型) 1.概述 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到.然后使用if... ...
- 设计模式 ( 十七) 状态模式State(对象行为型)
设计模式 ( 十七) 状态模式State(对象行为型) 1.概述 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理.最直接的解决方案是将这些所有可能发生的情况全都考虑到.然后使用if... ...
- 状态模式-State Pattern(Java实现)
状态模式-State Pattern 在状态模式(State Pattern)中,类的行为是基于它的状态改变的.当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. State接口 ...
- 北风设计模式课程---状态模式State(对象行为型)
北风设计模式课程---状态模式State(对象行为型) 一.总结 一句话总结: 状态模式 具体状态的行为在具体的状态类中就解决,不用交给外部做判断.实质是将多条件判断弄成了多个类,在不同的类中做判断 ...
- 设计模式2——状态模式State
参考链接: 设计模式之状态模式:https://www.cnblogs.com/haoerlv/p/7777789.html 设计模式系列之状态模式:https://www.jianshu.com/p ...
- 设计模式(十二):通过ATM取款机来认识“状态模式”(State Pattern)
说到状态模式,如果你看过之前发布的重构系列的文章中的<代码重构(六):代码重构完整案例>这篇博客的话,那么你应该对“状态模式”并不陌生,因为我们之前使用到了状态模式进行重构.上一篇博客我们 ...
随机推荐
- 安卓机在按HOME键时,UNITY触发的APPLICATION_PAUSE事件
安卓机在按HOME键时,UNITY触发的APPLICATION_PAUSE事件 此时安卓程序会返回,在这一瞬间,程序可以通过SOCKET发送数据包给服务器告知, 经测试在这短暂的时间内,这个数据包能发 ...
- ECMAScript5之JSON对象属性的遍历顺序
测试浏览器 Chrome.Safari 一 键可以用parseInt解析成整数的,按数值升序顺序. var intObj = { '3.3' : 3.3, '2' : 222, '1' :111 } ...
- python 网络客户端编程端口,模块
协议 功能 端口 模块 HTTP 网页 80 httplib,urllib,xmlrpclib NNTP Usenet 新闻组 119 nntplib FTP 文件传输 20(21控制和命令端口) f ...
- 【校招面试 之 剑指offer】第10-3题 矩阵覆盖问题
题目:我们可以使用2✖️1的小矩形横着或者竖着去覆盖更大的矩形.请问用8个2✖️1的小矩形无重叠地覆盖一个2✖️8的大矩形,共有多少种方法? 分析:当放第一块时(假定从左边开始)可以横着放,也可以竖着 ...
- vmware14中安装centos7并使用docker发布spring-boot项目
1.vmare中centos7安装(同一路由器无线网络下) 1.1选择桥接模式 1.2修改配置文件 vi /etc/sysconfig/network-scripts/ifcfg-ens33(这里不一 ...
- SVN:项目管理工具
svn:项目管理工具. 我们在进行团队开发的时候,每个人负责不同的层,比如:A负责DAO层,B负责SERVICE层,C负责DOMAIN层.我们开发完了自己管理的各层后需要将各层整合在一起,肯定不是拿U ...
- cvc-complex-type.2.4.a: Invalid content was found starting with element 'init-param'.
笔者最近学习一些spring mvc,在复制别人代码的时候报这个错.报错来源web.xml,原因是不符合xsd对xml的约束 源文件 <?xml version="1.0" ...
- SqlServer中批量update
现在我有两张表分别是S_PERSON,S_USER S_PERSON S_USER 我现在想把S_USER表中的ACCOUNT批量修改成S_PERSON的ACCOUNT 我们可以发现S_USER表中有 ...
- 泛型约束where条件的使用(可以通过类型参数动态反射创建实例)
定义抽象的人类 using System; using System.Collections.Generic; using System.Linq; using System.Text; using ...
- 由已打开的文件读取数据---read
头文件:#include<unistd.h> 函数原型:ssize_t read(int fd,void *buf,size_t count); 参数说明:fd:文件描述符 buf:存放读 ...