0%

设计模式之State模式

状态模式(State Pattern)是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。

介绍

状态模式与有限状态机的概念紧密相关.

  • 其主要思想是:
    在任意时刻仅可处于几种有限的状态中.在任何一个特定状态中,程序的行为都不相同,且可瞬间从一个状态切换到另一个状态.不过,根据当前状态,程序可能会切换到另外一种状态,也可能会保持当前状态不变.这些数量有限且预先定义的状态切换规则被称为转移。
  • 应用实例
    • 打篮球的时候运动员可以有正常状态、不正常状态和超常状态。
    • 操作系统, ready,runing,block状态.
    • 编译原理的DFA M,NFA M,
  • 优点:
    • 封装了转换规则。
    • 枚举可能的状态,在枚举状态之前需要确定状态种类。
    • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
    • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
    • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
  • 缺点:
    • 状态模式的使用必然会增加系统类和对象的个数。
    • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
    • 状态模式对”开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

      示例

  • 一定要用状态模式嘛?
    其实不用也是可以的,但是考虑到代码的可维护性,可扩展性,可复用性这些层面的话,状态模式就很有用了.
    比如,考虑一个银行系统,可以用来取款,打电话,报警,记录着四种功能,但是考虑如下需求:在白天如果我们去取款是正常的,晚上取款就要发出警报;在白天打电话有人接,晚上打电话启动留言功能;白天和晚上按警铃都会报警。那么我们应该如何设计这个程序呢,当然我们可以对每一个动作(作为一个函数),在这个函数内部,我们进行判断是白天还是黑夜,然后根据具体的情况做出反应。这样当然是可以的,但是假如我们的状态(白天和黑夜)非常的多呢,比如将24小时分成24个时间段(24个状态),那么我们对于每一个函数就要判断24遍,这无疑是非常糟糕的代码,可读性非常的差,并且如果需求发生了改变,我们很难去修改代码(很容易出现错误),但是如果我们考虑将这些状态都作为一个类,在每一个类内部进行处理、判断和相应的切换,这样思路就非常的清晰,如果再增加一种状态,代码需要修改的地方会非常的少,对于状态非常多的情景来说非常的方便。

  1. Context接口
    1
    2
    3
    4
    5
    6
    7
    8
    package com.edu.tju.GOF.State;
    public interface Context {

    public abstract void setClock(int hour); // 设置时间
    public abstract void changeState(State state); // 改变状态
    public abstract void callSecurityCenter(String msg); // 联系警报中心
    public abstract void recordLog(String msg); // 在警报中心留下记录
    }
  2. SafeFrame实现类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    package com.edu.tju.GOF.State;

    import java.awt.Frame;
    import java.awt.Label;
    import java.awt.Color;
    import java.awt.Button;
    import java.awt.TextField;
    import java.awt.TextArea;
    import java.awt.Panel;
    import java.awt.BorderLayout;
    import java.awt.event.ActionListener;
    import java.awt.event.ActionEvent;

    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("使用金库"); // 金库使用按钮
    private Button buttonAlarm = new Button("按下警铃"); // 按下警铃按钮
    private Button buttonPhone = new Button("正常通话"); // 正常通话按钮
    private Button buttonExit = new Button("结束"); // 结束按钮

    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");
    }
    }
  3. State接口
    1
    2
    3
    4
    5
    6
    7
    8
    package com.edu.tju.GOF.State;

    public interface State {
    public abstract void doClock(Context context, int hour); // 设置时间
    public abstract void doUse(Context context); // 使用金库
    public abstract void doAlarm(Context context); // 按下警铃
    public abstract void doPhone(Context context); // 正常通话
    }
  4. NightState实现类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.edu.tju.GOF.State;

    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 "[晚上]";
    }
    }
  5. DayState实现类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.edu.tju.GOF.State;

    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 "[白天]";
    }
    }
  6. Main类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package com.edu.tju.GOF.State;

    public class Main {
    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) {
    }
    }
    }
    }
    }
  • 可以看到状态模式的强大威力,是用最简洁的代码通过接口、抽象类、普通类、继承、委托、代理模式等方式,将状态抽象为类,然后通过控制状态的逻辑委托不同的状态去做不同的事情,对于每一个状态来说又再次委托控制状态的逻辑做出相应的动作和修改,这样看起来比较复杂,其实仔细阅读就会发现因为接口(抽象类)的原因,使得程序非常的简洁,各个状态分工明确,密切配合。
  • 但是状态模式也有一些缺点,正是因为各个状态密切配合,在一个状态之中要知道其他状态的对象,这就造成了一定的关联,状态与状态之间是一种紧耦合的关系,这是状态模式的一点缺点,针对于这一点,我们可以将状态迁移的代码统一交给SafeFrame来做,这样就要使用到了Mediator仲裁者模式了。
  • 使用单例的原因是如果一直创造新的对象会对内存产生浪费,因此单例即可。同样的使用状态模式通过接口使用state变量来表示相应的状态,不会产生混淆和矛盾,相比于使用多个变量来分区间表示状态来说是非常清晰简练的。State模式便于增加新的状态(也需要修改其他状态的状态迁移代码),不便于增加新的“依赖于状态的处理”,比如doAlarm等,因为一旦增加了,实现了State接口的所有状态都要增加该部分代码。
  • 同时我们也看到了实例的多面性,比如SafeFrame实例实现了ActionListener接口和Context接口,那么就可以将new SafeFrame()对象传入fun1(ActionListener a)和fun2(Context context)这两个方法之中,之后这两个方法对该对象的使用是不同的,权限也不一样,因此多接口就会产生多面性。状态模式其实是用了分而治之的思想,将不同的状态分开来讨论,抽取共同性,从而使问题变得简单。

角色

  1. 下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。

  2. 状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。

  3. 具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。
    状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。

  4. 上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。

实现方式

  1. 确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。
  2. 声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
  3. 为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
    在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式:
    • 将这些成员变量或方法设为公有。
    • 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。
    • 将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。
  4. 在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。
  5. 再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
  6. 为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、 各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。

    练习

    状态模式将根据当前回放状态, 让媒体播放器中的相同控件完成不同的行为。

    使用状态对象更改对象行为的示例
    播放器的主要对象总是会连接到一个负责播放器绝大部分工作的状态对象中。 部分操作会更换播放器当前的状态对象, 以此改变播放器对于用户互动所作出的反应。
    根据伪代码及类图实现状态模式.
  • 伪代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    // 音频播放器(Audio­Player)类即为上下文。它还会维护指向状态类实例的引用,
    // 该状态类则用于表示音频播放器当前的状态。
    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)
Thank you for your support