一文读懂状态模式

01 意图

状态是一种行为设计模式,它允许对象在其内部状态发生变化时改变其行为。看起来好像对象改变了它的类。

02 问题

状态模式与有限状态机的概念密切相关。

image.png

主要思想是,在任何给定时刻,程序可以处于有限数量的状态。在任何独特的状态中,程序的行为都不同,并且程序可以立即从一种状态切换到另一种状态。然而,根据当前状态,程序可能会或可能不会切换到某些其他状态。这些切换规则,称为转移,也是有限的和预先确定的。

您也可以将此方法应用于对象。想象一下,我们有一Document堂课。文档可以处于以下三种状态之一DraftModerationPublished。文档的publish方法在每个州的工作方式都略有不同:

  • Draft中,它将文档移至审核。

  • Moderation中,它使文档公开,但前提是当前用户是管理员。

  • Published中,它根本不做任何事情。

图片

状态机通常使用许多条件运算符(ifswitch)来实现,它们根据对象的当前状态选择适当的行为。通常,这个“状态”只是对象字段的一组值。即使您以前从未听说过有限状态机,您也可能至少实现过一次状态。以下代码结构是否敲响了警钟?


// The AudioPlayer class acts as a context. It also maintains a
// reference to an instance of one of the state classes that
// represents the current state of the audio player.
class AudioPlayer is
    field state: State
    field UI, volume, playlist, currentSong

    constructor AudioPlayer() is
        this.state = new ReadyState(this)

        // Context delegates handling user input to a state
        // object. Naturally, the outcome depends on what state
        // is currently active, since each state can handle the
        // input differently.
        UI = new UserInterface()
        UI.lockButton.onClick(this.clickLock)
        UI.playButton.onClick(this.clickPlay)
        UI.nextButton.onClick(this.clickNext)
        UI.prevButton.onClick(this.clickPrevious)

    // Other objects must be able to switch the audio player's
    // active state.
    method changeState(state: State) is
        this.state = state

    // UI methods delegate execution to the active state.
    method clickLock() is
        state.clickLock()
    method clickPlay() is
        state.clickPlay()
    method clickNext() is
        state.clickNext()
    method clickPrevious() is
        state.clickPrevious()

    // A state may call some service methods on the context.
    method startPlayback() is
        // ...
    method stopPlayback() is
        // ...
    method nextSong() is
        // ...
    method previousSong() is
        // ...
    method fastForward(time) is
        // ...
    method rewind(time) is
        // ...


// The base state class declares methods that all concrete
// states should implement and also provides a backreference to
// the context object associated with the state. States can use
// the backreference to transition the context to another state.
abstract class State is
    protected field player: AudioPlayer

    // Context passes itself through the state constructor. This
    // may help a state fetch some useful context data if it's
    // needed.
    constructor State(player) is
        this.player = player

    abstract method clickLock()
    abstract method clickPlay()
    abstract method clickNext()
    abstract method clickPrevious()


// Concrete states implement various behaviors associated with a
// state of the context.
class LockedState extends State is

    // When you unlock a locked player, it may assume one of two
    // states.
    method clickLock() is
        if (player.playing)
            player.changeState(new PlayingState(player))
        else
            player.changeState(new ReadyState(player))

    method clickPlay() is
        // Locked, so do nothing.

    method clickNext() is
        // Locked, so do nothing.

    method clickPrevious() is
        // Locked, so do nothing.


// They can also trigger state transitions in the context.
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)

一旦我们开始向类中添加越来越多的状态和依赖于状态的行为,基于条件的状态机的最大弱点就会显现出来Document。大多数方法将包含可怕的条件,这些条件会根据当前状态选择方法的正确行为。这样的代码很难维护,因为对转换逻辑的任何更改都可能需要在每个方法中更改状态条件。

随着项目的发展,问题往往会变得更大。在设计阶段预测所有可能的状态和转换是相当困难的。因此,用一组有限的条件构建的精益状态机会随着时间的推移变得臃肿不堪。

03 解决方案

状态模式建议您为对象的所有可能状态创建新类,并将所有特定于状态的行为提取到这些类中。

原始对象(称为context )不是自己实现所有行为,而是存储对表示其当前状态的状态对象之一的引用,并将所有与状态相关的工作委托给该对象。

图片

要将上下文转换为另一种状态,请将活动状态对象替换为表示该新状态的另一个对象。只有当所有状态类都遵循相同的接口并且上下文本身通过该接口与这些对象一起工作时,这才有可能。

这种结构可能看起来类似于策略模式,但有一个关键区别。在状态模式中,特定状态可能会相互了解并启动从一种状态到另一种状态的转换,而策略几乎从不知道彼此。

04 举个栗子

根据设备的当前状态,智能手机中的按钮和开关的行为会有所不同:

  • 当手机解锁时,按下按钮会导致执行各种功能。

  • 当手机被锁定时,按任何按钮都会进入解锁屏幕。

  • 手机电量低时,按任意键显示充电画面。

05 结构实现

图片

在此示例中,状态模式让媒体播放器的相同控件表现不同,具体取决于当前播放状态。

图片

播放器的主要对象始终链接到为播放器执行大部分工作的状态对象。一些动作用另一个动作替换了玩家的当前状态对象,这改变了玩家对用户交互的反应方式。

// The AudioPlayer class acts as a context. It also maintains a

06 适用场景

  • 当您的对象根据其当前状态表现不同、状态数量巨大且特定于状态的代码经常更改时,请使用状态模式。

该模式建议您将所有特定于状态的代码提取到一组不同的类中。因此,您可以相互独立地添加新状态或更改现有状态,从而降低维护成本。

  • 当您的类被大量条件污染时使用该模式,这些条件会根据类字段的当前值改变类的行为方式。

状态模式允许您将这些条件的分支提取到相应状态类的方法中。这样做时,您还可以从主类中清除状态特定代码中涉及的临时字段和辅助方法。

  • 当您在基于条件的状态机的相似状态和转换中有大量重复代码时,请使用 State。

状态模式允许您组合状态类的层次结构并通过将公共代码提取到抽象基类中来减少重复。

07 如何实施

  1. 决定哪个类将作为上下文。它可能是已经具有状态相关代码的现有类;或者一个新的类,如果特定于状态的代码分布在多个类中。

  2. 声明状态接口。尽管它可以反映上下文中声明的所有方法,但仅针对那些可能包含特定于状态的行为的方法。

  3. 对于每个实际状态,创建一个派生自状态接口的类。然后检查上下文的方法并将与该状态相关的所有代码提取到新创建的类中。

    在将代码移动到状态类时,您可能会发现它依赖于上下文的私有成员。有几种解决方法:

  • 公开这些字段或方法。

  • 将您提取的行为转换为上下文中的公共方法,并从状态类中调用它。这种方式很丑但很快,你以后总是可以修复它。

  • 将状态类嵌套到上下文类中,但前提是您的编程语言支持嵌套类。

  1. 在上下文类中,添加状态接口类型的引用字段和允许覆盖该字段值的公共设置器。

  2. 再次检查上下文的方法,并将空状态条件替换为对状态对象相应方法的调用。

  3. 要切换上下文的状态,请创建状态类之一的实例并将其传递给上下文。您可以在上下文本身、各种状态或客户端中执行此操作。无论在哪里完成,该类都依赖于它实例化的具体状态类。

08 优缺点

图片

欢迎查看公众号内容:
https://mp.weixin.qq.com/s?__biz=Mzk0NjI5NzE1Ng==&mid=2247483799&idx=1&sn=2b563aa54df15a766831a4f6a0b09b13&chksm=c30901bcf47e88aa904e699dcff01d84be425b517bf210cdf86838c84ddc813e9f2cff19e9c9&token=995892257&lang=zh_CN#rd

关注公众号可获取干货资料:《设计模式详解》


image.png

image.png
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352