状态模式(学习笔记)

  1. 意图

  允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类

  2. 动机

  状态模式与有限状态机的概念紧密相关。其主要思想是程序在任意时刻仅可处于几种有限的状态中在任何一个特定状态中程序的行为都不相同且可瞬间从一个状态切换到另一个状态不过根据当前状态程序可能会切换到另外一种状态也可能会保持当前状态不变这些数量有限且预先定义的状态切换规则被称为转移  

  假如你有一个 文档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),它并不会自行实现所有行为,而是会保存一个指向表示当前状态的状态对象的引用,且将所有与状态相关的工作委派给该对象

     

  如需将上下文转换为另外一种状态,则需将当前活动的状态对象替换为另外一个代表新状态的对象。采用这种方式是有前提的:所有状态类都必须遵循同样的接口,而且上下文必须仅通过接口与这些对象进行交互。这个结构可能看上去与策略模式相似,但有一个关键性的不同——在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换;策略则几乎完全不知道其他策略的存在

  3. 适用性

  • 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为
  • 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态,这个状态通常用一个或多个枚举常量表示。通常,有多个操作包含这一相同的条件结构。State模式将每一个条件分支放入一个独立的类中。这样可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化

  4. 结构

         

  5. 效果

  1. 通过消除臃肿的状态机条件语句简化上下文代码

  2. 将与特定状态相关的行为局部化,并且将不同状态的行为分割开来(单一职责原则)

  3. 无需修改已有状态类和上下文就能引入新状态(开闭原则)

  4. State对象可被共享 各Context对象可以共享一个State对象。当状态以这种方式被共享时,他们必然是没有内部状态而只有行为的轻量级对象(Flyweight)

  6. 代码实现  

  本例中,状态模式允许媒体播放器根据当前的回放状态进行不同的控制行为。播放器主类包含一个指向状态对象的引用,它将完成播放器的绝大部分工作。某些行为可能会用一个状态对象替换另一个状态对象,改变播放器对用户交互的回应方式。

  states/State.java: 通用状态接口

package state.states;

import state.ui.Player;

/**
 * @author GaoMing
 * @date 2021/7/26 - 8:02
 */
public abstract class State {
    Player player;

    /**
     * Context passes itself through the state constructor. This may help a
     * state to fetch some useful context data if needed.
     */
    State(Player player) {
        this.player = player;
    }

    public abstract String onLock();
    public abstract String onPlay();
    public abstract String onNext();
    public abstract String onPrevious();
}

  states/LockedState.java

package state.states;

import state.ui.Player;

/**
 * @author GaoMing
 * @date 2021/7/26 - 8:04
 * Concrete states provide the special implementation for all interface methods.
 */
public class LockedState extends State{
    LockedState(Player player) {
        super(player);
        player.setPlaying(false);
    }

    @Override
    public String onLock() {
        if (player.isPlaying()) {
            player.changeState(new ReadyState(player));
            return "Stop playing";
        } else {
            return "Locked...";
        }
    }

    @Override
    public String onPlay() {
        player.changeState(new ReadyState(player));
        return "Ready";
    }

    @Override
    public String onNext() {
        return "Locked...";
    }

    @Override
    public String onPrevious() {
        return "Locked...";
    }

}

  states/ReadyState.java

package state.states;

import state.ui.Player;

/**
 * @author GaoMing
 * @date 2021/7/26 - 8:06
 * They can also trigger state transitions in the context.
 */
public class ReadyState extends State{
    public ReadyState(Player player) {
        super(player);
    }

    @Override
    public String onLock() {
        player.changeState(new LockedState(player));
        return "Locked...";
    }

    @Override
    public String onPlay() {
        String action = player.startPlayback();
        player.changeState(new PlayingState(player));
        return action;
    }

    @Override
    public String onNext() {
        return "Locked...";
    }

    @Override
    public String onPrevious() {
        return "Locked...";
    }

}

  states/PlayingState.java

package state.states;

import state.ui.Player;

/**
 * @author GaoMing
 * @date 2021/7/26 - 8:06
 */
public class PlayingState extends State{
    PlayingState(Player player) {
        super(player);
    }

    @Override
    public String onLock() {
        player.changeState(new LockedState(player));
        player.setCurrentTrackAfterStop();
        return "Stop playing";
    }

    @Override
    public String onPlay() {
        player.changeState(new ReadyState(player));
        return "Paused...";
    }

    @Override
    public String onNext() {
        return player.nextTrack();
    }

    @Override
    public String onPrevious() {
        return player.previousTrack();
    }
}

  ui/Player.java: 播放器的主要代码

package state.ui;

import state.states.ReadyState;
import state.states.State;

import java.util.ArrayList;
import java.util.List;

/**
 * @author GaoMing
 * @date 2021/7/26 - 8:03
 */
public class Player {
    private State state;
    private boolean playing = false;
    private List<String> playlist = new ArrayList<>();
    private int currentTrack = 0;

    public Player() {
        this.state = new ReadyState(this);
        setPlaying(true);
        for (int i = 1; i <= 12; i++) {
            playlist.add("Track " + i);
        }
    }

    public void changeState(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void setPlaying(boolean playing) {
        this.playing = playing;
    }

    public boolean isPlaying() {
        return playing;
    }

    public String startPlayback() {
        return "Playing " + playlist.get(currentTrack);
    }

    public String nextTrack() {
        currentTrack++;
        if (currentTrack > playlist.size() - 1) {
            currentTrack = 0;
        }
        return "Playing " + playlist.get(currentTrack);
    }

    public String previousTrack() {
        currentTrack--;
        if (currentTrack < 0) {
            currentTrack = playlist.size() - 1;
        }
        return "Playing " + playlist.get(currentTrack);
    }

    public void setCurrentTrackAfterStop() {
        this.currentTrack = 0;
    }

}

  ui/UI.java: 播放器的 GUI

package state.ui;

import javax.swing.*;
import java.awt.*;

/**
 * @author GaoMing
 * @date 2021/7/26 - 8:07
 */
public class UI {
    private Player player;
    private static JTextField textField = new JTextField();

    public UI(Player player) {
        this.player = player;
    }

    public void init() {
        JFrame frame = new JFrame("Test player");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel context = new JPanel();
        context.setLayout(new BoxLayout(context, BoxLayout.Y_AXIS));
        frame.getContentPane().add(context);
        JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
        context.add(textField);
        context.add(buttons);

        // Context delegates handling user's input to a state object. Naturally,
        // the outcome will depend on what state is currently active, since all
        // states can handle the input differently.
        JButton play = new JButton("Play");
        play.addActionListener(e -> textField.setText(player.getState().onPlay()));
        JButton stop = new JButton("Stop");
        stop.addActionListener(e -> textField.setText(player.getState().onLock()));
        JButton next = new JButton("Next");
        next.addActionListener(e -> textField.setText(player.getState().onNext()));
        JButton prev = new JButton("Prev");
        prev.addActionListener(e -> textField.setText(player.getState().onPrevious()));
        frame.setVisible(true);
        frame.setSize(300, 100);
        buttons.add(play);
        buttons.add(stop);
        buttons.add(next);
        buttons.add(prev);
    }
}

  Demo.java: 客户端代码

package state;

import state.ui.Player;
import state.ui.UI;

/**
 * @author GaoMing
 * @date 2021/7/26 - 8:08
 */
public class Demo {
    public static void main(String[] args) {
        Player player = new Player();
        UI ui = new UI(player);
        ui.init();
    }
}

  运行结果

 

  7. 实现

  实现State模式时的考虑:

  1) 谁定义状态转换?     State模式不指定哪个参与者定义状态转换准则。如果该准则是固定的,那么它们可在Context中完全实现。然后若让State子类自身指定它们的后继者状态以及何时进行转换,通常更加灵活。更合适。这需要Context增加一个接口,让State对象显式的设定Context的当前状态。用这种方法分散转换逻辑可以很容易地定义新的State子类来修改和扩展该逻辑。这样做有一个缺点,一个State子类至少拥有一个其他子类的信息,这就在各子类之间产生了依赖

  2) 创建和销毁State对象  何时创建State对象?以及何时销毁他们? 是需要时再创建State对象,并随后销毁他们还是,提前创建它们并且始终不销毁它们。当要进入的状态是运行时不可知的,并且上下文不经常改变时,用第一种较为合适。如果State对象存储了大量信息,当状态改变很频繁时,第二种方法较好  

  8. 与其他模式的关系

  • 状态可被视为策略的扩展。两者都基于组合机制:它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态

  9. 已知应用  

  使用示例:在Java语言中,状态模式通常被用于将基于switch语句的大型状态机转换为对象
  核心Java程序库中一些状态模式的示例:
  javax.faces.lifecycle.LifeCycle#execute() (由 Faces­Servlet控制:行为依赖于当前 JSF 生命周期的阶段 (状态))
  识别方法: 方法受外部控制且能根据对象状态改变行为

posted @ 2021-07-28 16:24  慕仙白  阅读(213)  评论(0编辑  收藏  举报