State模式

State模式即状态模式,用类表示状态,通过切换类来方便地改变对象的状态。

示例程序

实现的功能

有一个金库和一个警报系统,金库里面有三个装置,根据时间变化,触发它们会产生不同的动作。

时间范围

白天:早上9点到下午5点

晚上:下午5点到第二天上午9点

三个装置

使用金库按钮:在白天使用,会在警报中心留下使用痕迹;在晚上使用,会触发警报中心的警铃。

警铃:在任何时候使用都会触发警报中心的警铃。

电话:在白天使用会拨通警报中心的电话;在晚上使用会给警报中心的电话留言。

不使用&使用状态模式对比

不使用状态模式

警报系统的类{
使用金库按钮方法(){
if (白天) {
在警报中心留下使用记录
} else if (晚上) {
触发警铃
}
}
警铃方法(){
报告紧急状态
}
电话方法(){
if (白天) {
拨通警报中心电话
} else if (晚上) {
给警报中心留言
}
}
}

使用状态模式

表示白天的状态类 {
使用金库按钮方法(){
在警报中心留下使用记录
}
警铃方法(){
报告紧急状态
}
电话方法(){
拨通警报中心电话
}
}
表示晚上的状态类 {
使用金库按钮方法(){
触发警铃
}
警铃方法(){
报告紧急状态
}
电话方法(){
给警报中心留言
}
}

在state模式中,用类表示状态,不需要用if else 来判断当前状态了。

示例程序的类图

代码

State接口

public interface State {
// 设置时间
void doClock(Context context, int hour);
// 使用金库按钮
void doUse(Context context);
// 按下警铃
void doAlarm(Context context);
// 正常通话
void doPhone(Context context);
}

Context接口

负责管理状态和做出响应处理的接口

public interface Context {
// 设置时间
void setClock(int hour);
// 改变状态
void changeState(State state);
// 联系警报中心
void callSecurityCenter(String msg);
// 在警报中心留下记录
void recordLog(String msg);
}

DayState类,表示白天的状态

doClock方法中,Context根据当前时间切换状态,利用单例模式避免浪费内存

public class DayState implements State {
private static DayState singleton = new DayState();
private DayState() { // 构造函数的可见性是private
}
public static State getInstance() { // 获取唯一实例
return singleton;
} public void doClock(Context context, int hour) { // 设置时间
if (hour < 9 || 17 <= hour) {
context.changeState(NightState.getInstance());
}
}
public void doUse(Context context) { // 使用金库
context.recordLog("使用金库(白天)");
}
public void doAlarm(Context context) { // 按下警铃
context.callSecurityCenter("按下警铃(白天)");
}
public void doPhone(Context context) { // 正常通话
context.callSecurityCenter("正常通话(白天)");
}
public String toString() { // 显示表示类的文字
return "[白天]";
}
}

NightState类,表示晚上的状态

public class NightState implements State {
private static NightState singleton = new NightState();
private NightState() { // 构造函数的可见性是private
}
public static State getInstance() { // 获取唯一实例
return singleton;
}
public void doClock(Context context, int hour) { // 设置时间
if (9 <= hour && hour < 17) {
context.changeState(DayState.getInstance());
}
}
public void doUse(Context context) { // 使用金库
context.callSecurityCenter("紧急:晚上使用金库!");
}
public void doAlarm(Context context) { // 按下警铃
context.callSecurityCenter("按下警铃(晚上)");
}
public void doPhone(Context context) { // 正常通话
context.recordLog("晚上的通话录音");
}
public String toString() { // 显示表示类的文字
return "[晚上]";
}
}

SafeFrame类,实现Context接口的具体类

public class SafeFrame extends Frame implements ActionListener, Context {
private TextField textClock = new TextField(60); // 显示当前时间
private TextArea textScreen = new TextArea(10, 60); // 显示警报中心的记录
private Button buttonUse = new Button("use"); // 金库使用按钮
private Button buttonAlarm = new Button("alarm"); // 按下警铃按钮
private Button buttonPhone = new Button("phone"); // 正常通话按钮
private Button buttonExit = new Button("cancel"); // 结束按钮 private State state = DayState.getInstance(); // 当前的状态 // 构造函数
public SafeFrame(String title) {
super(title);
setBackground(Color.lightGray);
setLayout(new BorderLayout());
// 配置textClock
add(textClock, BorderLayout.NORTH);
textClock.setEditable(false);
// 配置textScreen
add(textScreen, BorderLayout.CENTER);
textScreen.setEditable(false);
// 为界面添加按钮
Panel panel = new Panel();
panel.add(buttonUse);
panel.add(buttonAlarm);
panel.add(buttonPhone);
panel.add(buttonExit);
// 配置界面
add(panel, BorderLayout.SOUTH);
// 显示
pack();
show();
// 设置监听器
buttonUse.addActionListener(this);
buttonAlarm.addActionListener(this);
buttonPhone.addActionListener(this);
buttonExit.addActionListener(this);
}
// 按钮被按下后该方法会被调用
public void actionPerformed(ActionEvent e) {
System.out.println(e.toString());
if (e.getSource() == buttonUse) { // 金库使用按钮
state.doUse(this);
} else if (e.getSource() == buttonAlarm) { // 按下警铃按钮
state.doAlarm(this);
} else if (e.getSource() == buttonPhone) { // 正常通话按钮
state.doPhone(this);
} else if (e.getSource() == buttonExit) { // 结束按钮
System.exit(0);
} else {
System.out.println("?");
}
}
// 设置时间
public void setClock(int hour) {
String clockstring = "现在时间是";
if (hour < 10) {
clockstring += "0" + hour + ":00";
} else {
clockstring += hour + ":00";
}
System.out.println(clockstring);
textClock.setText(clockstring);
state.doClock(this, hour);
}
// 改变状态
public void changeState(State state) {
System.out.println("从" + this.state + "状态变为了" + state + "状态。");
this.state = state;
}
// 联系警报中心
public void callSecurityCenter(String msg) {
textScreen.append("call! " + msg + "\n");
}
// 在警报中心留下记录
public void recordLog(String msg) {
textScreen.append("record ... " + msg + "\n");
}
}

Main

每隔1秒钟刷新一次时间并调用SafeFrame的setClock方法,如果当前时间所处区间发生了变化,则SafeFrame里面的state实例会被重新设置。因为点击按钮触发的方法是state的方法,所以点击按钮产生的效果会随着时间改变而改变。

	public static void main(String[] args) {
SafeFrame frame = new SafeFrame("State Sample");
while (true) {
for (int hour = 0; hour < 24; hour++) {
frame.setClock(hour); // 设置时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}

结果: 按下按钮后,根据时间不同会有不同的效果。

角色和类图

角色

  • State(状态)

State角色表示状态,是具体状态抽象出来的接口。接口中的方法是:处理内容依赖于状态的API的集合。本例中,由State接口扮演此角色。

  • ConcreteState(具体状态)

表示具体的状态,本例中由DayStateNightState扮演此角色。

  • Context(上下文)

该角色持有表示当前状态的ConcreteState角色。此外它还定义了供外部调用者使用State模式的接口。

在本例中Context接口和SafeFrame类分担了这个角色。Context提供供外部调用者使用的State模式的接口,SafeFrame则持有ConcreteState角色。

类图

拓展思路

分而治之

分治法是解决复杂问题的利器,State模式就体现了分治法。如果状态非常多,则需要相当多的if else判断,这些条件和处理方法散落在系统各处,很难查找、理解和修改。状态模式将程序的状态部分从业务代码中剥离,把每个状态聚合成为一个类,大大降低了查找、理解和修改的难度。

依赖于状态的处理

SafeFrame类中,setClock方法和actionPerformed方法都依赖于状态的处理,即根据状态执行的方法,都把方法委托给了状态类来处理。

如何实现依赖于状态的处理?

1.定义状态(State)接口,声明抽象方法。 2.定义多个具体的状态(ConcreteState)类,实现抽象方法。

这样,Context类内部只要保留有State的引用,调用抽象方法。此时方法就会根据不同的状态执行不同的结果了。

谁来管理状态迁移

在本例中,利用ConcreteState类来管理状态迁移,这样做有好有坏,好处是把状态迁移条件集中到了一个类里面,比如我们想知道什么时候由白天变成黑夜,只需要阅读白天的状态类代码即可。坏处是各个状态类的耦合度增加了。

也可以用Context类,结合Mediator模式管理各个状态之间的迁移,这样使得ConcreteState类直接更加独立。

也可以不使用State模式,使用状态迁移表来设计程序。

易于增加新状态

如果要增加新状态,只需要添加一个实现State接口的ConcreteState类即可,同时也要注意修改状态迁移代码。总的来说,比一个个修改if else强得多。

如果要增加新的状态处理方法则相对麻烦,需要在State接口中添加方法并在每个ConcreteState类中添加对应的实现方法,好在不会忘记:如果有的ConcreteState没有实现处理方法,编译都通不过。

实例的多面性

请注意SafeFrame中的两个语句

public SafeFrame(String title) {
...
buttonUse.addActionListener(this);
...
}
public void actionPerformed(ActionEvent e) {
...
if (e.getSource() == buttonUse) {
state.doUse(this);
}
...
}

两个this,第一个表示ActionListener,第二个表示Context,因为SafeFrame实现了ActionListenerContext两个接口,所以它既是前者,也是后者。注意这里体现出来的实例的多面性

《图解设计模式》读书笔记8-3 STATE模式的更多相关文章

  1. HeadFirst设计模式读书笔记(3)-装饰者模式(Decorator Pattern)

    装饰者模式:动态地将责任附件到对象上.若要扩展功能,装饰者提东了比继承更有弹性的替代方案. 装饰者和被装饰对象有相同的超类型 你可以用一个或者多个装饰者包装一个对象. 既然装饰者和被装饰对象有相同的超 ...

  2. HeadFirst设计模式读书笔记--目录

    HeadFirst设计模式读书笔记(1)-策略模式(Strategy Pattern) HeadFirst设计模式读书笔记(2)-观察者模式(Observer Pattern) HeadFirst设计 ...

  3. Head First 设计模式读书笔记(1)-策略模式

    一.策略模式的定义 策略模式定义了算法族,分别封装起来,让它们之间可以互换替换,此模式让算法的变化独立使用算法的客户. 二.使用策略模式的一个例子 2.1引出问题 某公司做了一套模拟鸭子的游戏:该游戏 ...

  4. C#设计模式学习笔记:(18)状态模式

    本笔记摘抄自:https://www.cnblogs.com/PatrickLiu/p/8032683.html,记录一下学习过程以备后续查用. 一.引言 今天我们要讲行为型设计模式的第六个模式--状 ...

  5. JavaScript设计模式:读书笔记(未完)

    该篇随我读书的进度持续更新阅读书目:<JavaScript设计模式> 2016/3/30 2016/3/31 2016/4/8 2016/3/30: 模式是一种可复用的解决方案,可用于解决 ...

  6. 图解http读书笔记

    以前对HTTP协议一知半解,一直不清楚前端需要对于HTTP了解到什么程度,知道接触的东西多了,对于性能优化.服务端的配合和学习中也渐渐了解到了HTTP基础的重要性,看了一些大神对HTTP书籍的推荐,也 ...

  7. Java设计模式学习笔记(二) 简单工厂模式

    前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 正文开始... 1. 简介 简单工厂模式不属于GoF23中设计模式之一,但在软件开发中应用也较为 ...

  8. Java设计模式学习笔记(三) 工厂方法模式

    前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 1. 简介 上一篇博客介绍了简单工厂模式,简单工厂模式存在一个很严重的问题: 就是当系统需要引入 ...

  9. Java设计模式学习笔记(四) 抽象工厂模式

    前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 1. 抽象工厂模式概述 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问 ...

  10. C#设计模式学习笔记:(23)解释器模式

    本笔记摘抄自:https://www.cnblogs.com/PatrickLiu/p/8242238.html,记录一下学习过程以备后续查用. 一.引言 今天我们要讲行为型设计模式的第十一个模式-- ...

随机推荐

  1. 00.AutoMapper 之入门指南(Getting Started Guide)

    转载(https://www.jianshu.com/p/29ee5a94c1d9) 入门指南(Getting Started Guide) AutoMapper 是什么? AutoMapper 是一 ...

  2. spring controller 方法测试

    controller 测试 不使用其他api接口测试工具 一般而言,我们写好一个模块后,会对其进行单元测试,再集成到现有的系统中. 但是呢~针对Controller.Service.Dao三层来说,我 ...

  3. js函数调用的几种方法

    js的函数调用会免费奉送两个而外的参数就是 this 和 arguments .arguments是参数组,他并不是一个真实的数组,但是可以使用.length方法获得长度. 书上有说4中调用方式: 方 ...

  4. 您的浏览器没有获得Java Virtual Machine(JVM)支持。可能由于没有安装JVM或者已安装但是没有启用。请安装JVM1.5或者以上版本,如果已安装则启用它。

    您的浏览器没有获得Java Virtual Machine(JVM)支持.可能由于没有安装JVM或者已安装但是没有启用.请安装JVM1.5或者以上版本,如果已安装则启用它. https://www.j ...

  5. XPath语法以及谓语的结合使用

    /* XPath 术语 节点(Node) 在 XPath 中,有七种类型的节点:元素.属性.文本.命名空间.处理指令.注释以及文档(根)节点.XML 文档是被作为节点树来对待的.树的根被称为文档节点或 ...

  6. JavaEE高级-Spring Data学习笔记

    Spring Data概述 - Spring Data : Spring 的一个子项目.用于简化数据库访问,支持NoSQL 和 关系数据存储.其主要目标是使数据库的访问变得方便快捷. - Spring ...

  7. 04-A的LU分解

    一.矩阵$AB$的逆 $(AB)^{-1}=B^{-1}A^{-1}$,顺序正好相反 二.$A=LU$ 如矩阵: $\left[\begin{array}{ll}{2} & {1} \\ {8 ...

  8. python打包命令

    打包成exe方法 (1)切换到该文件夹 (2)pyinstaller -F py文件 (py文件要英文才行) -F 生成单个可执行文件 -w 去掉控制台窗口 -p 自定义需要加载的类路径 -i 可执行 ...

  9. CTF各种资源:题目、工具、资料

    目录 题目汇总 Reverse 签到题 Web Web中等难度 Crypto 基础网站 各类工具 综合 Web Payloads 逆向 Pwn 取证 题目汇总 这里收集了我做过的CTF题目 Rever ...

  10. 写了一个简单的 Mybatis

    写了一个简单的 Mybatis,取名 SimpleMybatis . 具备增删改查的基本功能,后续还要添加剩下的基本数据类型和Java集合类型的处理. 脑图中有完整的源码和测试的地址 http://n ...