设计模式
设计模式简介
看懂UML类图和时序图
UML统一建模语言
UML类图及类图之间的关系
类关系记忆技巧
如何正确使用设计模式
优秀设计的特征
面向对象设计原则
创建型设计模式
工厂模式
抽象工厂模式
简单工厂模式
静态工厂模式(Static Factory)
单例模式
建造者模式
原型模式
结构型设计模式
适配器模式
桥接模式
组合模式
装饰器模式
外观模式
享元模式
代理模式
过滤器模式
注册模式(Registry)
行为型设计模式
责任链模式
命令模式
解释器模式
中介者模式
备忘录模式
迭代器模式
观察者模式
状态模式
策略模式
模板模式
访问者模式
规格模式(Specification)
J2EE 设计模式
MVC 模式
业务代表模式
组合实体模式
数据访问对象模式(DAO模式)
前端控制器模式
拦截过滤器模式
空对象模式
服务定位器模式
传输对象模式
数据映射模式(Data Mapper)
依赖注入模式(Dependency Injection)
流接口模式(Fluent Interface)
其他模式
对象池模式(Pool)
委托模式
资源库模式(Repository)
实体属性值模式(EAV 模式)
反面模式
归纳设计模式
本文档使用 MrDoc 发布
-
+
首页
状态模式
> 状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。  在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。 在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。 ## 问题 状态模式与[有限状态机](https://en.wikipedia.org/wiki/Finite-state_machine)的概念紧密相关。  其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为**转移**。 你还可将该方法应用在对象上。 假如你有一个 文档 Document 类。 文档可能会处于 草稿 Draft 、 审阅中 Moderation 和 已发布 Published 三种状态中的一种。 文档的 publish 发布方法在不同状态下的行为略有不同: - 处于 草稿状态时, 它会将文档转移到审阅中状态。 - 处于 审阅中状态时, 如果当前用户是管理员, 它会公开发布文档。 - 处于 已发布状态时, 它不会进行任何操作。 状态机通常由众多条件运算符 ( if 或 switch ) 实现, 可根据对象的当前状态选择相应的行为。 “状态” 通常只是对象中的一组成员变量值。 即使你之前从未听说过有限状态机, 你也很可能已经实现过状态模式。 下面的代码应该能帮助你回忆起来。 ``` class Document is field state: string // ... method publish() is switch (state) "draft": state = "moderation" break "moderation": if (currentUser.role == 'admin') state = "published" break "published": // 什么也不做。 break // ... ``` 当我们逐步在 文档类中添加更多状态和依赖于状态的行为后, 基于条件语句的状态机就会暴露其最大的弱点。 为了能根据当前状态选择完成相应行为的方法, 绝大部分方法中会包含复杂的条件语句。 修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句, 导致代码的维护工作非常艰难。 这个问题会随着项目进行变得越发严重。 我们很难在设计阶段预测到所有可能的状态和转换。 随着时间推移, 最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。 ## 解决方案 状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。 **原始对象被称为上下文 (context)**, 它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象。  如需将上下文转换为另外一种状态, 则需将当前活动的状态对象替换为另外一个代表新状态的对象。 采用这种方式是有前提的: 所有状态类都必须遵循同样的接口, 而且上下文必须仅通过接口与这些对象进行交互。 这个结构可能看上去与策略模式相似, 但有一个关键性的不同——在状态模式中, 特定状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。 ## 目的 状态模式可以基于一个对象的同种事务而封装出不同的行为。它提供一种简洁的方式使得对象在运行时可以改变自身行为,而不必借助单一庞大的条件判断语句。 ## 介绍 **意图:** 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。 **主要解决:** 对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。 **何时使用:** 代码中包含大量与对象状态有关的条件语句。 **如何解决:** 将各种具体的状态类抽象出来。 **关键代码:** 通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。 **应用实例:** 1. 打篮球的时候运动员可以有正常状态、不正常状态和超常状态。 2. 曾侯乙编钟中,'钟是抽象接口','钟 A'等是具体状态,'曾侯乙编钟'是具体环境(Context)。 **优点:** 1. 封装了转换规则。 2. 枚举可能的状态,在枚举状态之前需要确定状态种类。 3. 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4. 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5. 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。 **缺点:** 1. 状态模式的使用必然会增加系统类和对象的个数。 2. 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3. 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。 **使用场景:** 1. 行为随状态改变而改变的场景。 2. 条件、分支语句的代替者。 **注意事项:** 在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。 ## 结构  - 上下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。 - 状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。 - 具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。 状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。 - 上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。 ### 实例 我们将创建一个 *State* 接口和实现了 *State* 接口的实体状态类。*Context* 是一个带有某个状态的类。 *StatePatternDemo* ,我们的演示类使用 *Context* 和状态对象来演示 Context 在状态改变时的行为变化。  ## 伪代码 在本例中, 状态模式将根据当前回放状态, 让媒体播放器中的相同控件完成不同的行为。  ``` // 音频播放器(AudioPlayer)类即为上下文。它还会维护指向状态类实例的引用, // 该状态类则用于表示音频播放器当前的状态。 class AudioPlayer is field state: State field UI, volume, playlist, currentSong constructor AudioPlayer() is this.state = new ReadyState(this) // 上下文会将处理用户输入的工作委派给状态对象。由于每个状态都以不 // 同的方式处理输入,其结果自然将依赖于当前所处的状态。 UI = new UserInterface() UI.lockButton.onClick(this.clickLock) UI.playButton.onClick(this.clickPlay) UI.nextButton.onClick(this.clickNext) UI.prevButton.onClick(this.clickPrevious) // 其他对象必须能切换音频播放器当前所处的状态。 method changeState(state: State) is this.state = state // UI 方法会将执行工作委派给当前状态。 method clickLock() is state.clickLock() method clickPlay() is state.clickPlay() method clickNext() is state.clickNext() method clickPrevious() is state.clickPrevious() // 状态可调用上下文的一些服务方法。 method startPlayback() is // ... method stopPlayback() is // ... method nextSong() is // ... method previousSong() is // ... method fastForward(time) is // ... method rewind(time) is // ... // 所有具体状态类都必须实现状态基类声明的方法,并提供反向引用指向与状态相 // 关的上下文对象。状态可使用反向引用将上下文转换为另一个状态。 abstract class State is protected field player: AudioPlayer // 上下文将自身传递给状态构造函数。这可帮助状态在需要时获取一些有用的 // 上下文数据。 constructor State(player) is this.player = player abstract method clickLock() abstract method clickPlay() abstract method clickNext() abstract method clickPrevious() // 具体状态会实现与上下文状态相关的多种行为。 class LockedState extends State is // 当你解锁一个锁定的播放器时,它可能处于两种状态之一。 method clickLock() is if (player.playing) player.changeState(new PlayingState(player)) else player.changeState(new ReadyState(player)) method clickPlay() is // 已锁定,什么也不做。 method clickNext() is // 已锁定,什么也不做。 method clickPrevious() is // 已锁定,什么也不做。 // 它们还可在上下文中触发状态转换。 class ReadyState extends State is method clickLock() is player.changeState(new LockedState(player)) method clickPlay() is player.startPlayback() player.changeState(new PlayingState(player)) method clickNext() is player.nextSong() method clickPrevious() is player.previousSong() class PlayingState extends State is method clickLock() is player.changeState(new LockedState(player)) method clickPlay() is player.stopPlayback() player.changeState(new ReadyState(player)) method clickNext() is if (event.doubleclick) player.nextSong() else player.fastForward(5) method clickPrevious() is if (event.doubleclick) player.previous() else player.rewind(5) ``` ## 应用场景 - 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。 模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。 - 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时, 可使用该模式。 状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。 - 当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。 状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。 ## 实现方式 1. 确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。 2. 声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。 3. 为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。 在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式: - 将这些成员变量或方法设为公有。 - 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。 - **将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。** 4. 在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。 5. 再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。 6. 为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、 各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。 ## 优点 - 单一职责原则。 将与特定状态相关的代码放在单独的类中。 - 开闭原则。 无需修改已有状态类和上下文就能引入新状态。 - 通过消除臃肿的状态机条件语句简化上下文代码。 ## 缺点 - 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。 ## 与其他模式的关系 - 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。 - 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。 ## 示例代码 ### TS ```ts /** * The Context defines the interface of interest to clients. It also maintains a * reference to an instance of a State subclass, which represents the current * state of the Context. */ class Context { /** * @type {State} A reference to the current state of the Context. */ private state: State; constructor(state: State) { this.transitionTo(state); } /** * The Context allows changing the State object at runtime. */ public transitionTo(state: State): void { console.log(`Context: Transition to ${(<any>state).constructor.name}.`); this.state = state; this.state.setContext(this); } /** * The Context delegates part of its behavior to the current State object. */ public request1(): void { this.state.handle1(); } public request2(): void { this.state.handle2(); } } /** * The base State class declares methods that all Concrete State should * implement and also provides a backreference to the Context object, associated * with the State. This backreference can be used by States to transition the * Context to another State. */ abstract class State { protected context: Context; public setContext(context: Context) { this.context = context; } public abstract handle1(): void; public abstract handle2(): void; } /** * Concrete States implement various behaviors, associated with a state of the * Context. */ class ConcreteStateA extends State { public handle1(): void { console.log('ConcreteStateA handles request1.'); console.log('ConcreteStateA wants to change the state of the context.'); this.context.transitionTo(new ConcreteStateB()); } public handle2(): void { console.log('ConcreteStateA handles request2.'); } } class ConcreteStateB extends State { public handle1(): void { console.log('ConcreteStateB handles request1.'); } public handle2(): void { console.log('ConcreteStateB handles request2.'); console.log('ConcreteStateB wants to change the state of the context.'); this.context.transitionTo(new ConcreteStateA()); } } /** * The client code. */ const context = new Context(new ConcreteStateA()); context.request1(); context.request2(); ``` 输出: ```txt Context: Transition to ConcreteStateA. ConcreteStateA handles request1. ConcreteStateA wants to change the state of the context. Context: Transition to ConcreteStateB. ConcreteStateB handles request2. ConcreteStateB wants to change the state of the context. Context: Transition to ConcreteStateA. ```
追风者
2022年3月29日 20:17
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码