前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计模式——状态模式

设计模式——状态模式

作者头像
WEBJ2EE
发布2019-12-17 17:49:52
1K0
发布2019-12-17 17:49:52
举报
文章被收录于专栏:WebJ2EEWebJ2EE

图文无关:TCP 协议状态机

1. 什么是【状态机】?

有限状态机(Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。在计算机科学中,有限状态机被广泛用于建模应用行为、硬件电路系统设计、软件工程,编译器、网络协议、和计算与语言的研究。

状态机的 UML 表示法

基本元素:

入门示例:

复杂一点:

再复杂一点:

状态机设计注意事项:

  • 避免把某个“程序动作”当作是一种“状态”来处理。“动作”是不稳定的,即使没有条件的触发,“动作”一旦执行完毕就结束了;而“状态”是相对稳定的,如果没有外部条件的触发,一个状态会一直持续下去。
  • 状态划分时漏掉一些状态,导致跳转逻辑不完整。在设计状态机时,我们需要反复的查看设计的状态图或者状态表,最终达到一种牢不可破的设计方案。

2. 设计模式——状态模式

2.1. 什么是状态模式?

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

解释:

  • 所谓对象的状态,通常指的就是对象实例的属性的值;而行为指的就是对象的功能。
  • 状态模式的功能就是分离状态的行为,通过维护状态的变化,来调用不同状态对应的不同功能。(可以描述为:状态决定行为
  • 由于状态实在运行期被改变的,因此行为也会在运行期根据状态的改变而改变,看起来,同一个对象,在不同的运行时刻,行为是不一样的,就像是类被修改了一样。

2.2. 优缺点?

优点:

  1. 封装了转换规则。
  2. 枚举可能的状态,在枚举状态之前需要确定状态种类。
  3. 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  4. 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。

缺点:

  1. 状态模式的使用必然会增加系统类和对象的个数。
  2. 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  3. 状态模式对象之间耦合度高,破坏"开闭原则"。

2.3. 架构图?

  • Context:上下文是持有状态的对象,但是上下文自身并不处理跟状态相关的行为,而是把处理状态的功能委托给了状态对应的状态处理类来处理。客户端一般只和上下文交互
  • State:抽象状态类,定义一个接口以封装使用上下文环境的一个特定状态相关的行为。
  • ConcreteState:具体状态类,实现抽象状态定义的接口。

3. 案例分析——MsgBox

3.1. 接口设计

代码语言:javascript
复制
MsgBox.show(summary); // 只提示摘要信息
MsgBox.show(summary, detail); // 同时提示摘要、详细信息
MsgBox.hide(); // 隐藏信息提示框
MsgBox.fix(); // 固定在屏幕上(屏蔽超时自动隐藏)

功能性要求:
1. 只显示摘要信息时,5s 超时后自动隐藏;
2. 同时显示摘要、详细信息时,10s 超时后自动隐藏;
3. 同时显示摘要、详细信息时,可以控制展开、关闭详细信息面板;
4. 同时显示摘要、详细信息时,展开、关闭详细信息面板时,超时计时器重置;
5. 面板上提示自动关闭倒计时;

3.2. UI交互设计(灵魂版)

3.3. 行为驱动版设计(Vue实现)

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
    <title></title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style type="text/css">
        html,
        body {
            margin: 0;
            padding: 0;

            width: 100%;
            height: 100%;
        }

        .webj2ee-button {
            border: 2px solid orange;
            cursor: pointer;
        }

        .webj2ee-button.active {
            background-color: #6495ed;
        }

        .webj2ee-msgbox {
            position: absolute;
            top: 50px;
            left: 0;
            right: 0;

            margin: auto;

            width: 300px;

            border: 2px solid grey;
        }

        .webj2ee-msgbox-title {
            display: flex;
            flex-direction: row;
        }

        .webj2ee-msgbox-title-summary {
            border: 2px solid green;
            flex-grow: 1;
        }

        .webj2ee-msgbox-title-btngroup {
            border: 2px solid yellow;
        }

        .webj2ee-msgbox-content-detail {
            border: 2px solid purple;

            min-height: 100px;
        }
</style>
</head>
<body>
<div id="app">
    <div v-show="!hidden" class="webj2ee-msgbox">
        <div class="webj2ee-msgbox-title">
            <div class="webj2ee-msgbox-title-summary">
                {{ summary }}
            </div>
            <div class="webj2ee-msgbox-title-btngroup">
                <span>{{ countdown }}</span>
                <span class="webj2ee-button"
                      v-show="!!detail"
                      v-bind:class="{ active: showdetail }"
                      v-on:click="toggleShowDetail()">detail</span>
                <span class="webj2ee-button"
                      v-bind:class="{ active: fixed }"
                      v-on:click="toggleFixed()">fix</span>
                <span class="webj2ee-button"
                      v-on:click="hide()">×</span>
            </div>
        </div>
        <div class="webj2ee-msgbox-content" v-show="!!detail && showdetail">
            <div class="webj2ee-msgbox-content-detail">
                {{ detail }}
            </div>
        </div>
    </div>

    <div>
        <button v-on:click="show('Hello World!')">
            MsgBox.show("Hello World!")
        </button>
        <br/>
        <button v-on:click="show('Hello World!', 'A message from webj2ee!')">
            MsgBox.show("Hello World!", "A message from webj2ee!")
        </button>
        <br/>
        <button v-on:click="fix()">MsgBox.fix()</button>
        <br/>
        <button v-on:click="hide()">MsgBox.hide()</button>
    </div>
</div>

<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            hidden: true,
            summary: '',
            detail: '',
            showdetail: false,
            fixed: false,
            autoHideTimer: null,
            countdown: NaN,
            countdownInterval: null,
        },
        methods: {
            show: function (summary, detail = "") {
                this.hidden = false;
                this.summary = summary;
                this.detail = detail;
                this.showdetail = !!detail;
                this.fixed = false;

                clearTimeout(this.autoHideTimer);
                this.autoHideTimer = null;
                this.countdown = NaN;

                if (!this.detail) {
                    this.countdown = 5;
                    this.autoHideTimer = setTimeout(() => {
                        this.hide();
                        clearInterval(this.countdownInterval);
                        this.countdownInterval = null;
                    }, 5 * 1000);
                    clearInterval(this.countdownInterval);
                    this.countdownInterval = setInterval(() => {
                        this.countdown--;
                    }, 1000)
                } else {
                    this.countdown = 10;
                    this.autoHideTimer = setTimeout(() => {
                        this.hide();
                        clearInterval(this.countdownInterval);
                        this.countdownInterval = null;
                    }, 10 * 1000);
                    clearInterval(this.countdownInterval);
                    this.countdownInterval = setInterval(() => {
                        this.countdown--;
                    }, 1000);
                }
            },
            hide: function () {
                this.hidden = true;

                clearTimeout(this.autoHideTimer);
                this.autoHideTimer = null;
                this.countdown = NaN;
            },
            fix: function () {
                this.fixed = true;
                clearTimeout(this.autoHideTimer);
                this.autoHideTimer = null;
                this.countdown = NaN;
                clearInterval(this.countdownInterval);
            },
            toggleFixed: function () {
                this.fixed = !this.fixed;
                if (this.fixed) {
                    clearTimeout(this.autoHideTimer);
                    this.autoHideTimer = null;
                    this.countdown = NaN;
                    clearInterval(this.countdownInterval);
                } else {
                    if (!this.detail) {
                        this.countdown = 5;
                        this.autoHideTimer = setTimeout(() => {
                            this.hide();
                            clearInterval(this.countdownInterval);
                            this.countdownInterval = null;
                        }, 5 * 1000);
                        clearInterval(this.countdownInterval);
                        this.countdownInterval = setInterval(() => {
                            this.countdown--;
                        }, 1000);
                    } else {
                        this.countdown = 10;
                        this.autoHideTimer = setTimeout(() => {
                            this.hide();
                            clearInterval(this.countdownInterval);
                            this.countdownInterval = null;
                        }, 10 * 1000);
                        clearInterval(this.countdownInterval);
                        this.countdownInterval = setInterval(() => {
                            this.countdown--;
                        }, 1000);
                    }
                }
            },
            toggleShowDetail: function () {
                this.showdetail = !this.showdetail;

                clearTimeout(this.autoHideTimer);
                this.autoHideTimer = null;

                this.countdown = 10;
                this.autoHideTimer = setTimeout(() => {
                    this.hide();
                    clearInterval(this.countdownInterval);
                    this.countdownInterval = null;
                }, 10 * 1000);
                clearInterval(this.countdownInterval);
                this.countdownInterval = setInterval(() => {
                    this.countdown--;
                }, 1000);
            }
        }
    })
</script>
</body>
</html>

行为驱动板实现的编码特点:

  • 就这么个小组件,代码很凌乱,可扩展性差;
  • 编码方式是从“行为”出发,控制各种“状态”;
  • 每个“行为”里面都要控制一堆状态(特别是那几个Timer...),很容易出现考虑不完善情况,编码出bug概率高;
  • 如果再扩充几个状态(例如:展示一下当前时间...)

1.4. 状态机版设计(Vue实现)

a. 首先绘制MsgBox的状态转换图:

b. 再根据状态转换图编码:

代码语言:javascript
复制
<!DOCTYPE html>
<html>
<head>
    <title></title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style type="text/css">
        html,
        body {
            margin: 0;
            padding: 0;

            width: 100%;
            height: 100%;
        }

        .webj2ee-button {
            border: 2px solid orange;
            cursor: pointer;
        }

        .webj2ee-button.active {
            background-color: #6495ed;
        }

        .webj2ee-msgbox {
            position: absolute;
            top: 50px;
            left: 0;
            right: 0;

            margin: auto;

            width: 300px;

            border: 2px solid grey;
        }

        .webj2ee-msgbox-title {
            display: flex;
            flex-direction: row;
        }

        .webj2ee-msgbox-title-summary {
            border: 2px solid green;
            flex-grow: 1;
        }

        .webj2ee-msgbox-title-btngroup {
            border: 2px solid yellow;
        }

        .webj2ee-msgbox-content-detail {
            border: 2px solid purple;

            min-height: 100px;
        }

        .webj2ee-msgbox-content-datetimer{
            text-align: center;
            color: purple;
        }
</style>
</head>
<body>
<div id="app">
    <div v-show="!hidden" class="webj2ee-msgbox">
        <div class="webj2ee-msgbox-title">
            <div class="webj2ee-msgbox-title-summary">
                {{ summary }}
            </div>
            <div class="webj2ee-msgbox-title-btngroup">
                <span>{{ autoHiddenCountdown }}</span>
                <span class="webj2ee-button"
                      v-show="!!detail"
                      v-bind:class="{ active: detailExpaneded }"
                      v-on:click="toggleDetailExpaneded()">detail</span>
                <span class="webj2ee-button"
                      v-bind:class="{ active: fixed }"
                      v-on:click="toggleFixed()">fix</span>
                <span class="webj2ee-button"
                      v-on:click="hide()">×</span>
            </div>
        </div>
        <div class="webj2ee-msgbox-content" v-show="!!detail && detailExpaneded">
            <div class="webj2ee-msgbox-content-detail">
                {{ detail }}
            </div>
            <div class="webj2ee-msgbox-content-datetimer">
                {{ currentDateFormatted }}
            </div>
        </div>
    </div>


    <div>
        <button v-on:click="show('Hello World!')">
            MsgBox.show("Hello World!")
        </button>
        <br/>
        <button v-on:click="show('Hello World!', 'A message from webj2ee!')">
            MsgBox.show("Hello World!", "A message from webj2ee!")
        </button>
        <br/>
        <button v-on:click="fix()">MsgBox.fix()</button>
        <br/>
        <button v-on:click="hide()">MsgBox.hide()</button>
    </div>
</div>

<script type="text/javascript">

    const MSGBOX_STATE_INIT = 0;
    const MSGBOX_STATE_SUMMARY_UNFIXED = 1;
    const MSGBOX_STATE_SUMMARY_FIXED = 2;
    const MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED = 3;
    const MSGBOX_STATE_DETAIL_EXPANDED_FIXED = 4;
    const MSGBOX_STATE_DETAIL_COLLAPSED_UNFIXED = 5;
    const MSGBOX_STATE_DETAIL_COLLAPSED_FIXED = 6;
    const MSGBOX_STATE_HIDDEN = 7;

    const MSGBOX_SUMMARY_AUTO_HIDDEN_TIMEOUT_SEC = 5;
    const MSGBOX_SUMMARY_DETAIL_AUTO_HIDDEN_TIMEOUT_SEC = 10;

    var app = new Vue({
        el: '#app',
        data: {
            state: MSGBOX_STATE_INIT,
            hidden: true,
            summary: '',
            detail: '',
            detailExpaneded: false,
            fixed: false,
            autoHiddenTimer: null,
            autoHiddenCountdown: NaN,
            autoHiddenCountdownRefreshInterval: null,
            currentDate: null,
            currentDateRefreshInterval: null,
        },

        computed:{
            currentDateFormatted: function(){
                const now = this.currentDate;
                if(!now){
                    return "";
                }else{
                    return `${now.getFullYear()}-${now.getMonth()+1}-${now.getDay()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`
                }
            }
        },

        mounted: function () {
            this.setState(MSGBOX_STATE_INIT);
        },

        methods: {
            /**
             * 辅助函数
             */
            _stopAutoHiddenTimer: function(){
                // stop auto hidden timer
                clearTimeout(this.autoHiddenTimer);
                this.autoHiddenTimer = null;

                // stop auto hidden countdown refresh
                this.autoHiddenCountdown = NaN;
                clearInterval(this.autoHiddenCountdownRefreshInterval);
                this.autoHiddenCountdownRefreshInterval = null;
            },
            _startAutoHiddenTimer: function(autoHiddenTimeoutSec){
                // 开启自动关闭超时控制
                clearTimeout(this.autoHiddenTimer);
                this.autoHiddenTimer = setTimeout(() => {
                    this.setState(MSGBOX_STATE_HIDDEN);
                }, autoHiddenTimeoutSec * 1000);

                // 开启自动关闭超时倒计时
                this.autoHiddenCountdown = autoHiddenTimeoutSec;
                clearInterval(this.autoHiddenCountdownRefreshInterval);
                this.autoHiddenCountdownRefreshInterval = setInterval(() => {
                    this.autoHiddenCountdown--;
                }, 1000);
            },
            _stopCurrentDateRefresh: function(){
                this.currentDate = null;
                clearInterval(this.currentDateRefreshInterval);
                this.currentDateRefreshInterval = null;
            },
            _startCurrentDateRefresh: function(){
                // 开启用当前时间展示
                this.currentDate = new Date();
                clearInterval(this.currentDateRefreshInterval);
                this.currentDateRefreshInterval = setInterval(() => {
                    this.currentDate = new Date();
                }, 1000);
            },
            /**
             * 状态 -> 行为控制
             */
            setState: function(state){
                this.state = state;

                if(state === MSGBOX_STATE_INIT ||
                    state === MSGBOX_STATE_HIDDEN){ // 初始、隐藏状态
                    this.hidden = true;
                    this.summary = "";
                    this.detail = "";
                    this.detailExpaneded = false;
                    this.fixed = false;

                    this._stopAutoHiddenTimer();
                    this._stopCurrentDateRefresh();
                }else if(state === MSGBOX_STATE_SUMMARY_UNFIXED){ // 摘要-不固定
                    this.hidden = false;
                    this.detailExpaneded = false;
                    this.fixed = false;

                    this._startAutoHiddenTimer(MSGBOX_SUMMARY_AUTO_HIDDEN_TIMEOUT_SEC);
                    this._stopCurrentDateRefresh();
                }else if(state === MSGBOX_STATE_SUMMARY_FIXED){ // 摘要-固定
                    this.fixed = true;

                    this._stopAutoHiddenTimer();
                }else if(state === MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED
                            || state === MSGBOX_STATE_DETAIL_COLLAPSED_UNFIXED){
                    this.hidden = false;
                    if(state === MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED){
                        this.detailExpaneded = true;
                    }else if(state === MSGBOX_STATE_DETAIL_COLLAPSED_UNFIXED){
                        this.detailExpaneded = false;
                    }
                    this.fixed = false;

                    this._startAutoHiddenTimer(MSGBOX_SUMMARY_DETAIL_AUTO_HIDDEN_TIMEOUT_SEC);
                    this._startCurrentDateRefresh();
                }else if(state === MSGBOX_STATE_DETAIL_EXPANDED_FIXED
                            || state === MSGBOX_STATE_DETAIL_COLLAPSED_FIXED){ // 摘要-固定
                    this.fixed = true;
                    this._stopAutoHiddenTimer();
                }
            },
            show: function (summary="", detail = "") {
                this.summary = summary;
                this.detail = detail;

                if(!detail){
                    this.setState(MSGBOX_STATE_SUMMARY_UNFIXED);
                }else{
                    this.setState(MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED);
                }
            },
            hide: function () {
                this.setState(MSGBOX_STATE_HIDDEN);
            },
            fix: function () {
                this.toggleFixed(true);
            },
            toggleFixed: function (forceFixed) {
                // 摘要状态
                if(forceFixed || this.state === MSGBOX_STATE_SUMMARY_UNFIXED){
                    this.setState(MSGBOX_STATE_SUMMARY_FIXED);
                }else if(this.state === MSGBOX_STATE_SUMMARY_FIXED){
                    this.setState(MSGBOX_STATE_SUMMARY_UNFIXED);
                }

                // 详细状态
                // --展开状态
                if(forceFixed || this.state === MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED){
                    this.setState(MSGBOX_STATE_DETAIL_EXPANDED_FIXED);
                }else if(this.state === MSGBOX_STATE_DETAIL_EXPANDED_FIXED){
                    this.setState(MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED);
                }
                // --收缩状态
                if(forceFixed || this.state === MSGBOX_STATE_DETAIL_COLLAPSED_UNFIXED){
                    this.setState(MSGBOX_STATE_DETAIL_COLLAPSED_FIXED);
                }else if(this.state === MSGBOX_STATE_DETAIL_COLLAPSED_FIXED){
                    this.setState(MSGBOX_STATE_DETAIL_COLLAPSED_UNFIXED);
                }
            },
            toggleDetailExpaneded: function () {
                if(this.state === MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED){
                    this.setState(MSGBOX_STATE_DETAIL_COLLAPSED_UNFIXED);
                }else if(this.state === MSGBOX_STATE_DETAIL_COLLAPSED_UNFIXED){
                    this.setState(MSGBOX_STATE_DETAIL_EXPANDED_UNFIXED);
                }
            }
        }
    })
</script>
</body>
</html>

状态驱动板实现的特点:

  • 状态定义清晰,状态转换规则明确;
  • 通过状态约束行为,而不是根据行为调整状态;
  • 更容易阅读,也更容易扩展;
  • 也算是状态模式的变种;

参考:

《大话设计模式》 《设计模式之禅 第2版》 《研磨设计模式》 《敏捷软件开发 原则、模式与实践》 《面向对象分析与设计》 《UML 基础、案例与应用》 《设计模式 可复用面向对象软件的基础》

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档