一、枚举类型——用枚举实现状态机

news2024/11/16 21:35:43

枚举类型很适合用来实现状态机。状态机可以处于有限数量的特定状态。它们通常根据输入,从一个状态移动到下一个状态,但同时也会存在瞬态。当任务执行完毕后,状态机会立即跳出所有状态。

每个状态都有某些可接受的输入,不同的输入会使状态机从当前状态切换到新的状态。由于枚举限制了可能出现的状态集大小(即状态数量),因此很适合表达(枚举)不同的状态和输入。

每种状态一般也会有某种对应的输出。

自动售货机是个很好的状态机应用的例子。首先,在一个枚举中定义一系列输入:

Input.java

import java.util.Random;

public enum Input {
    NICKEL(5), DIME(10), QUARTER(25), DOLLAR(100),
    TOOTHPASTE(200), CHIPS(75), SODA(100), SOAP(50),
    ABORT_TRANSACTION {
        @Override
        public int amount() { // Disallow
            throw new RuntimeException("ABORT.amount()");
        }
    },
    STOP { // 这必须是最后一个实例
        @Override
        public int amount() { // 不允许
            throw new RuntimeException("SHUT_DOWN.amount()");
        }
    };
    int value; // 单位为美分(cents)

    Input(int value) {
        this.value = value;
    }

    Input() {
    }

    int amount() {
        return value;
    }

    ; // In cents
    static Random rand = new Random(47);

    public static Input randomSelection() {
        //不包括 STOP:
        return values()[rand.nextInt(values().length - 1)];
    }
}

注意其中两个 Input 有着对应的金额,所以在接口中定义了 amount() 方法。然而,对另外两个 Input 调用 amount() 是不合适的,如果调用就会抛出异常。尽管这是个有点奇怪的机制(在接口中定义一个方法,然后如果在某些具体实现中调用它的话就会抛出异常),但这是枚举的限制所导致的。

VendingMachine(自动售货机)接收到输入后,首先通过 Category(类别) 枚举来对这些输入进行分类,这样就可以在各个类别间切换了。下例演示了枚举是如何使代码变得更清晰、更易于管理的。

VendingMachine.java

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

enum Category {
    MONEY(Input.NICKEL, Input.DIME, Input.QUARTER, Input.DOLLAR),
    ITEM_SELECTION(Input.TOOTHPASTE, Input.CHIPS, Input.SODA, Input.SOAP),
    QUIT_TRANSACTION(Input.ABORT_TRANSACTION),
    SHUT_DOWN(Input.STOP);
    private Input[] values;

    Category(Input... types) {
        values = types;
    }

    private static EnumMap<Input, Category> categories = new EnumMap<>(Input.class);

    static {
        for (Category c : Category.class.getEnumConstants()) {
            for (Input type : c.values) {
                categories.put(type, c);
            }
        }
    }

    public static Category categorize(Input input) {
        return categories.get(input);
    }
}

public class VendingMachine {
    private static State state = State.RESTING;
    private static int amount = 0;
    private static Input selection = null;

    enum StateDuration {TRANSIENT} // 标识 enum

    enum State {
        RESTING {
            @Override
            void next(Input input) {
                switch (Category.categorize(input)) {
                    case MONEY:
                        amount += input.amount();
                        state = ADDING_MONEY;
                        break;
                    case SHUT_DOWN:
                        state = TERMINAL;
                    default:
                }
            }
        },
        ADDING_MONEY {
            @Override
            void next(Input input) {
                switch (Category.categorize(input)) {
                    case MONEY:
                        amount += input.amount();
                        break;
                    case ITEM_SELECTION:
                        selection = input;
                        if (amount < selection.amount()) {
                            System.out.println(
                                    "Insufficient money for " + selection);
                        } else {
                            state = DISPENSING;
                        }
                        break;
                    case QUIT_TRANSACTION:
                        state = GIVING_CHANGE;
                        break;
                    case SHUT_DOWN:
                        state = TERMINAL;
                    default:
                }
            }
        },
        DISPENSING(StateDuration.TRANSIENT) {
            @Override
            void next() {
                System.out.println("here is your " + selection);
                amount -= selection.amount();
                state = GIVING_CHANGE;
            }
        },
        GIVING_CHANGE(StateDuration.TRANSIENT) {
            @Override
            void next() {
                if (amount > 0) {
                    System.out.println("Your change: " + amount);
                    amount = 0;
                }
                state = RESTING;
            }
        },
        TERMINAL {
            @Override
            void output() {
                System.out.println("Halted");
            }
        };
        private boolean isTransient = false;

        State() {
        }

        State(StateDuration trans) {
            isTransient = true;
        }

        void next(Input input) {
            throw new RuntimeException("Only call " + "next(Input input) for non-transient states");
        }

        void next() {
            throw new RuntimeException("Only call next() for " + "StateDuration.TRANSIENT states");
        }

        void output() {
            System.out.println(amount);
        }
    }

    static void run(Supplier<Input> gen) {
        while (state != State.TERMINAL) {
            state.next(gen.get());
            while (state.isTransient) {
                state.next();
            }
            state.output();
        }
    }

    public static void main(String[] args) {
        Supplier<Input> gen = new RandomInputSupplier();
        if (args.length == 1) {
            gen = new FileInputSupplier(args[0]);
        }
        run(gen);
    }
}

// 基本的稳健性检查:
class RandomInputSupplier implements Supplier<Input> {
    @Override
    public Input get() {
        return Input.randomSelection();
    }
}

// 从以“;”分割的字符串的文件创建输入
class FileInputSupplier implements Supplier<Input> {
    private Iterator<String> input;

    FileInputSupplier(String fileName) {
        try {
            input = Files.lines(Paths.get(fileName))
                    .skip(1) // Skip the comment line
                    .flatMap(s -> Arrays.stream(s.split(";")))
                    .map(String::trim)
                    .collect(Collectors.toList())
                    .iterator();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Input get() {
        if (!input.hasNext()) {
            return null;
        }
        return Enum.valueOf(Input.class, input.next().trim());
    }
}

下面是用于生成输出的文本文件:

VendingMachine.txt

QUARTER;QUARTER;QUARTER;CHIPS;
DOLLAR;DOLLAR;TOOTHPASTE;
QUARTER;DIME;ABORT_TRANSACTION;
QUARTER;DIME;SODA;
QUARTER;DIME;NICKEL;SODA;
ABORT_TRANSACTION;
STOP;

以下是运行参数配置:

在这里插入图片描述

运行结果如下:

在这里插入图片描述

因为通过 switch 语句在枚举实例中进行选择操作是最常见的方式(注意,为了使 switch 便于操作枚举,语言层面需要付出额外的代价),所以在组织多个枚举类型时,最常问的问题之一就是“我需要什么东西之上(即以什么粒度)进行 switch”。这里最简单的办法是,回头梳理一遍 VendingMachine,就会发现在每种 State 下,你需要针对输入操作的基本类别进行 switch 操作:投入钱币、选择商品、退出交易、关闭机器。并且在这些类别内,你还可以投入不同类别的货币,选择不同类别的商品。Category 枚举会对不同的 Input 类型进行分类,因此 categorize() 方法可以在 switch 中生成恰当的 Category。这种方法用一个 EnumMap 实现了高效且安全的查询。

如果你研究一下 VendingMachine 类,便会发现每个状态的区别,以及对输入的响应区别。同时还要注意那两个瞬态:在 run() 方法中,售货机等待一个 Input,并且会一直在状态间移动,直到它不再处于某个瞬态中。

VendingMachine 可以通过两种不同的 Supplier 对象,以两种方法来测试。RandomInputSupplier 只需要持续生成除 SHUT_DOWN 以外的任何输入。通过一段较长时间的运行后,就相当于做了一次健康检查,以确定售货机不会偏离到某些无效状态。FileInputSupplier 接收文本形式的输入描述文件,并将它们转换为 enum 实例,然后创建 Input 对象。下面是用于生成以上输出的文本文件:

FileInputSupplier 的构造器将这个文件转换为行级的 Stream 流,并忽略注释行。然后它通过 String.split() 方法将每一行都根据分号拆开。这样就能生成一个字符串数组,可以通过先将该数组转化为 Stream,然后执行 flatMap(),来将其注入(前面 FileInputSupplier 中生成的)Stream 中。结果将删除所有的空格,并转换为 List,并从中得到 Iterator。

上述设计有个限制:VendingMachine 中会被 State 枚举实例访问到的字段都必须是静态的,这意味着只能存在一个 VendingMachine 实例。这可能不会是个大问题——你可以想想一个实际的(嵌入式Java)实现,每台机器可能就只有一个应用程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/676753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

你应该知道的 Python 自动化脚本

概要 我们都有一些需要重复做的任务。幸运的是&#xff0c;我们可以将其中一些过程自动化&#xff0c;这样我们就可以专注于做其他真正需要精力和注意力的事情。 在这篇文章中&#xff0c;我们将谈论一些 Python 自动化脚本&#xff0c;你可以轻松地用它们来执行自动化任务。重…

巨星内马尔为孕期出轨道歉了!喊话女友:“我不能想象失去你”

近日&#xff0c;巴西球星内马尔在女友布鲁娜孕期出轨的传闻引起了社会广泛关注。 22日凌晨&#xff0c;内马尔在自己的社交媒体上发文回应并道歉&#xff0c;表示自己在球场内外都会犯错&#xff0c;但私生活的问题他会在家里解决。 他还重申了自己已经为犯下的错误和不必要的…

python爬虫_函数的使用

文章目录 ⭐前言⭐python函数&#x1f496; 参数传递—值&#x1f496; 参数传递—引用&#x1f496; 多参数(*)&#x1f496;lambda匿名函数 结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享关于python函数入门使用。 该系列文章&#xff1a; python爬虫_基本数…

【031】C++类和对象之运算符重载详解和代码实践(最全讲解)

C类和对象之运算符重载详解 引言一、运算符重载的基本概念1.1、可重载的运算符1.2、不可重载的运算符 二、重载 << 运算符&#xff08;全局函数实现&#xff09;三、重载 >> 运算符&#xff08;全局函数实现&#xff09;四、重载 运算符4.1、全局函数实现4.2、成员…

leetcode257. 二叉树的所有路径(java)

二叉树的所有路径 leetcode257. 二叉树的所有路径题目描述DFS深度优先遍历 二叉树专题 leetcode257. 二叉树的所有路径 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/binary-tree-paths 题目描述 给你一个二叉树的根节…

【系统开发】尚硅谷 - 谷粒商城项目笔记(十一):K8S

文章目录 K8s简介架构原理核心概念控制平面组件&#xff08;Control Plane Components&#xff09;kube-apiserveretcdkube-schedulerkube-controller-managercloud-controller-manager Node 组件kubeletkube-proxy容器运行时&#xff08;Container Runtime&#xff09; 插件&a…

Gee 项目复现

序言 复现&#xff1a;原链接 一个Web框架需要支持的功能&#xff0c; 路由&#xff0c;请求到响应函数的映射&#xff0c;支持动态路由如hello/:name,hello/*模板&#xff0c;使用内置模板引擎渲染机制。鉴权&#xff1a;分组插件&#xff1a;中间件 第一天 HTTP基础 启动…

基于边界点优化和多步路径规划的机器人自主探索

论文题目&#xff1a;Autonomous Robotic Exploration Based on Frontier Point Optimization and Multistep Path Planning 中文题目&#xff1a;基于边界点优化和多步路径规划的机器人自主探索 作者&#xff1a;Baofu Fang &#xff1b;Jianfeng Ding ; Zaijun Wang 作者机…

5.5.2 IPv6数据报格式

5.5.2 IPv6数据报格式 首先我们来回忆一下IPv4数据报首部格式&#xff08;5.2.3 IP数据报&#xff08;一&#xff09;IP数据报的格式&#xff09;&#xff0c;包括20个字节的固定部分和长度可变的选项部分&#xff0c;如图 红色方框标注的是在IPv6中会消失的字段&#xff0c;椭…

小白也会的------新建Python虚拟环境,查看该虚拟环境的路径,将该虚拟环境的所有库和版本号导出到一个 requirements.txt 文件中

我的目录标题 1、新建Python虚拟环境2、查看该虚拟环境的路径3、将该虚拟环境的所有库和版本号导出到一个 requirements.txt 文件中4、如果你只需要将当前虚拟环境中安装的所有库和版本号导出到一个 requirements.txt 文件中&#xff0c;而不需要包括每个库的来源&#xff0c;可…

KMP算法基础

前言 KMP算法是我们数据结构串中最难也是最重要的算法。难是因为KMP算法的代码很优美简洁干练&#xff0c;但里面包含着非常深的思维。真正理解代码的人可以说对KMP算法的了解已经相当深入了。而且这个算法的不少东西的确不容易讲懂&#xff0c;很多正规的书本把概念一摆出直接…

C++——命名空间(namespace)

目录 1. C语言命名冲突 2. 命名空间定义 3. 命名空间使用 可能大家在看别人写的C代码中&#xff0c;在一开始会包这个头文件&#xff1a;#include<iostream> 这个头文件等价于我们在C语言学习到的#include<stdio.h>&#xff0c;它是用来跟我们的控制台输入和输出…

带你见见红黑树-概念+插入篇

写的不好&#xff0c;见谅~ 目录 概念理解 红黑树规则 AVL树与红黑树的相爱相杀 红黑树的插入时的上色与旋转。 不上色&#xff08;shǎi&#xff09; 情况一&#xff1a;空树 情况二&#xff1a;非空树&#xff0c;父节点为黑 上色&#xff08;shǎi&#xff09; 情况…

【Linux】深入理解文件系统

系列文章 收录于【Linux】文件系统 专栏 关于文件描述符与文件重定向的相关内容可以移步 文件描述符与重定向操作。 可以到 浅谈文件原理与操作 了解文件操作的系统接口。 想深入理解文件缓冲区还可以看看文件缓冲区。 目录 系列文章 磁盘 结构介绍 定位数据 抽象管理…

【Linux】MySQL 高级 SQL 语句 (二)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 MySQL 高级 SQL 语句 连接查询CREATE VIEW 视图UNION 联集交集值无交集值CASE空值(NULL) 和 无值() 的区别正则表达式 连接查询 mysql> select * from xjz; #xjz表格 ---…

Linux5.8 MySQL主从复制与读写分离

文章目录 计算机系统5G云计算第四章 LINUX MySQL主从复制与读写分离一、概述及原理1&#xff09;什么是读写分离2&#xff09;为什么要读写分离呢3&#xff09;什么时候要读写分离4&#xff09;主从复制与读写分离5&#xff09;mysql支持的复制类型6&#xff09;主从复制的工作…

Rust语言从入门到入坑——(5)Rust 所有权

文章目录 0 引入1、所有权2、内存和分配3、移动与克隆3.1、移动3.2、克隆 4、引用与租借4.1、引用4.1、垂悬引用 5、函数中变量5.1 参数变量5.2 、返回值变量 0 引入 主要介绍Rust所有权的知识&#xff0c;涉及到变量的作用域&#xff0c;内存释放机制&#xff0c;移动&#x…

Python|Pyppeteer启动浏览器窗口,右侧出现空白区域怎么解决?(13)

前言 本文是该专栏的第13篇,结合优质项目案例持续分享Pyppeteer的干货知识,记得关注。 有些同学可能在使用pyppeteer的时候,在配置项里面,明明已经设置好了窗口最大化,而启动Chromium窗口,打开的窗口最右侧却是一大片空白区域,具体如下图所示: 那么,出现上述情况,需…

AutoGPT 英文版安装过程

自从2022年11月chatGPT的发布3.0GPT大模型&#xff0c;在中国掀起一股AI学习热潮&#xff0c;国内百度2023年4月份发布文心一言&#xff0c;把AI推上另一个高潮&#xff0c;最直接的是问答&#xff0c;我输入一句话&#xff0c;AI帮生成一段文字或一个视频&#xff0c;但是国内…

畅捷通T+ 反序列化漏洞复现(QVD-2023-13615)

0x01 产品简介 畅捷通 T 是一款基于互联网的新型企业管理软件&#xff0c;功能模块包括&#xff1a;财务管理、采购管理、库存管理等。主要针对中小型工贸和商贸企业的财务业务一体化应用&#xff0c;融入了社交化、移动化、物联网、电子商务、互联网信息订阅等元素。 0x02 漏…