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

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

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

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

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

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 {@Overridepublic int amount() { // Disallowthrow new RuntimeException("ABORT.amount()");}},STOP { // 这必须是最后一个实例@Overridepublic int amount() { // 不允许throw new RuntimeException("SHUT_DOWN.amount()");}};int value; // 单位为美分(cents)Input(int value) {this.value = value;}Input() {}int amount() {return value;}; // In centsstatic Random rand = new Random(47);public static Input randomSelection() {//不包括 STOP:return values()[rand.nextInt(values().length - 1)];}
}

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

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

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} // 标识 enumenum State {RESTING {@Overridevoid next(Input input) {switch (Category.categorize(input)) {case MONEY:amount += input.amount();state = ADDING_MONEY;break;case SHUT_DOWN:state = TERMINAL;default:}}},ADDING_MONEY {@Overridevoid 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) {@Overridevoid next() {System.out.println("here is your " + selection);amount -= selection.amount();state = GIVING_CHANGE;}},GIVING_CHANGE(StateDuration.TRANSIENT) {@Overridevoid next() {if (amount > 0) {System.out.println("Your change: " + amount);amount = 0;}state = RESTING;}},TERMINAL {@Overridevoid 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> {@Overridepublic 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);}}@Overridepublic Input get() {if (!input.hasNext()) {return null;}return Enum.valueOf(Input.class, input.next().trim());}
}

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

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)实现,每台机器可能就只有一个应用程序。

本文链接:https://my.lmcjl.com/post/12799.html

展开阅读全文

4 评论

留下您的评论.