作者 | 小牛



java状态机代码demo_Data

Java 工程师,关注服务端技术 

首先我们先来简单了解一下什么是状态机和领域特定语言。

状态机(State Machine):定义事物状态以及这些状态之间转移和动作等行为的数学模型。一般可以分为有限状态机、并发状态机、分层状态机等。

领域特定语言( Domain Specific Language):简称 DSL,是指为特定领域(domain)设计的专用语言。

有限状态机(FSM)

我们通常所说的状态机都是指有限状态机(Finite State Machine,FSM)。

有限状态机有以下三个特点:

  • 状态数量是有限的
  • 某一时刻一定会处于其所有状态中的一个状态
  • 接收某个事件后从一种状态切换到另一种状态

我们可以这样来定义有限状态机:有限个状态(State)以及在这些状态(State)之间的转移(Transition)和动作(Action)等行为的数据模型。

有限状态机的组成

核心是由四部分:

  1. 状态( State):对象在其生命周期中某一个时刻的运行情况。在不同形态下,可以有不同的行为和属性。
  2. 事件( Event):在某种状态下所发生的有意义的事情,通常会触发一个动作或者流转。
  3. 动作(Action):一个可执行的原子操作,通常由事件触发。
  4. 流转(Transition):从一个状态变化为另一个状态。

一般来说,状态可以分为现态和次态。次态是相对于现态而言的,次态一旦被激活,就转变成新的现态。

而动作是一个宽泛的概念,它可以产生输入输出,产生新的事件,或者代替 “流转” 做状态转换的工作。另外一个事件可以触发多个动作。

流转可以分为:

  • 外部流转(External Transition):不同状态之间的流转。
  • 内部流转(Internal Transition):同一个状态之间的流转。

此外某些情况下状态机还需要另外一个元素:

  • 条件(Condition):表示是否允许到达某个状态。
有限状态机的表述

通常有两种表述形式:状态转换表和状态图。

下面我们假设一个预约流程中存在以下状态,以此来示例:

  • 未使用
  • 预约中
  • 已预约
  • 已取消
  • 已完成

其中”已取消“和”已完成“都是最终态。“未使用” 状态的预约如果被预约则流转成 “预约中”状态,“预约中” 如果完成了支付可以流转成 “已预约”,“预约中” 和 “已预约” 状态 如果取消都会流转成“已取消”,“已预约”状态如果签到则流转成“已完成”。

上面一段话是否看起来很吃力?接下来我们换种方式表述再试试。

状态转换表

状态转换表有多种形式,我们这里的表现形式如下:

现态 S \事件 E

预约

支付

取消

签到

未使用

预约中

X

X

X

预约中

已预约

已取消

X

已预约

X

X

已取消

已完成

已取消

X

X

X

已完成

X

X

X

其中

  • 左侧栏代表当前的状态。
  • 上边栏代表不同的事件或者说是系统行为。
  • 剩余交叉部分的表格则代表进行流转之后的状态,其中有汉字的格子代表流转后的新状态,空白格子代表状态不变,标记为 “X” 则代表禁止此事件

这样看起来是不是清晰了很多,接下来我们再来试试以状态图的方式来表述

状态图



java状态机代码demo_有限状态机_02

可以看出,状态表更着重于体现状态机中所包换的所有状态和事件,而状态图对状态间的流转表述的更为清晰。

现在我们已经可以清晰的表述出一个状态机了,那么接下来我们如何用代码来实现呢?

代码实现

首先最简单直观的方式就是用 if-else 或者 switch-case。其次我们可以定义一个二维数组或者映射来存放状态间的关系,借助一个统一的方法来处理。再复杂一点的则可以使用状态模式。

上述的实现方案不是本文的重点,这里也就不一一举例了。接下来我们尝试着用 DSL 的方式来实现一个状态机。在开始之前,我们先了解一下什么是 DSL。

领域特定语言(DSL)

领域特定语言(Domain Specific Language,后面统称为 DSL)的特点是高度专用,可以简单分为:内部DSL和外部DSL。

外部 DSL 是不依赖项目本身,单独解析和实现的。比如 SQL 、级联样式表(CSS)和正则表达式。

内部 DSL 是依赖项目所用编程语言的,从某种意义上说,内部 DSL 可以认为是特殊的类库。例如 Mockito 和 JOOQ。

我们接下来所要说的就是内部 DSL。

内部 DSL

内部 DSL 虽然简单,但功能却可以十分强大,可以让我们的代码变得更加精炼、易读。

对于 Scala 来说,它的一些特性很适合去编写 DSL,如柯里化、隐式转换等。而对于 Java 而言,Lambda 表达式以及方法引用的引入,让使用 Java 语言编写 DSL 有了意义。当然不会如同 Scala 那般整洁,但足以实现清晰直观的 Fluent Interface。

Fluent Interface:连贯接口,也就是我们常说的流式接口。由埃里克·埃文斯和马丁·福勒(Martin Fowler)首创,是构建面向对象的API的一种方法,这种方式可以为代码带来的可读性和可理解性的明显提升。

以 JOOQ 为例:

dsl.select(BILL.fields()).from(BILL)
  .innerJoin(RECEIPT)
  .on(RECEIPT.BILLID.eq(BILL.ID))         
  .where(RECEIPT.ID.in(receiptIds))
  .fetchInto(Bill.class);

如上所示,我们得以 “用 JAVA 代码编写 SQL” 。这就是 DSL 和 Fluent Interface 带给我们的船新体验。

代码实现

假设我们的预约模型如下:

@Data
public class Booking {
    private Long patientId;
    private Long doctorId;
    private OffsetDateTime time;
    private Order order;
    private Clinic clinic;
}

@Data
public class Order {
    private String orderNo;
    private Long fee;
    private Payment payment;
}

@Data
public class Payment {
    private String paymentNo;
    private String type;
}

@Data
public class Clinic {
    private Integer id;
    private String name;
}

我们如果要创建一个Booking 的实例需要如下代码:

Booking booking = new Booking();
booking.setDoctorId(DOCTOR_ID);
booking.setPatientId(PATIENT_ID);
booking.setTime(BOOK_TIME);

Order order = new Order();
order.setOrderNo(ORDER_NO);
order.setFee(ORDER_FEE);

Payment payment = new Payment();
payment.setPaymentNo(PAY_NO);
payment.setType(PAY_TYPE);
order.setPayment(payment);

Clinic clinic = new Clinic();
clinic.setId(CLINIC_ID);
clinic.setName(CLINIC_NAME);

booking.setOrder(order);
booking.setClinic(clinic);
方法链接

上述代码看起来比较冗长,那么我们尝试用方法链接(method chainning)的方式去改造一下。

首先需要为 Booking,Clinic,order,Payment这四个模型分别创建对应的 builder:

public class BookingChainBuilder {

    private final Booking booking = new Booking();

    private BookingChainBuilder(Long patientId) {
        booking.setPatientId(patientId);
    }

    public static BookingChainBuilder from(Long patientId) {
        return new BookingChainBuilder(patientId);
    }

    public BookingChainBuilder book(Long doctorId) {
        booking.setDoctorId(doctorId);
        return this;
    }

    public BookingChainBuilder at(OffsetDateTime time) {
        booking.setTime(time);
        return this;
    }

    public OrderChainBuilder withOrder(String order) {
        return new OrderChainBuilder(this, order);
    }

    public ClinicChainBuilder at(Integer clinicId) {
        return new ClinicChainBuilder(this, clinicId);
    }

    public BookingChainBuilder setClinic(Clinic clinic) {
        booking.setClinic(clinic);
        return this;
    }

    public BookingChainBuilder setOrder(Order order) {
        booking.setOrder(order);
        return this;
    }

    public Booking complete() {
        return booking;
    }
}

BookingChainBuilder 的 from 方法是整个方法链的入口,它是一个静态方法,并且返回的是 BookingChainBuilder 本身。

complete 方法是整个链路的结尾,返回的是实例化好的Booking模型。

at 方法有两个重载:

一个是BookingChainBuilder at(OffsetDateTime time) 返回的是 BookingChainBuilder本身,代表设置的是 booking 层面的属性,后续可以调用的也都是 ``BookingChainBuilder` 中声明的方法。

另一个则是 ClinicChainBuilder at(Integer clinicId) 返回的是 ClinicChainBuilder, 代表后续设置的是 clinic 层面的属性,后续可以调用的也都是 ClinicChainBuilder 中声明的方法。

public class ClinicChainBuilder {

    private final BookingChainBuilder builder;

    public final Clinic clinic = new Clinic();

    public ClinicChainBuilder(BookingChainBuilder builder, Integer id) {
        this.builder = builder;
        clinic.setId(id);
    }

    public BookingChainBuilder name(String name) {
        clinic.setName(name);
        return builder.setClinic(clinic);
    }
}

ClinicChainBuilder 中的 name 方法是该段链路的结束,它将 Clinic 实例 set 到了 Booking 中,并返回 BookingChainBuilder, 以便于后续的调用。

通过 BookingChainBuilder 和 ClinicChainBuilder 我们实现了一个两级的链路,以此类推可以实现更多层级的链路。示例如下:

public class OrderChainBuilder {

    private final BookingChainBuilder builder;

    public final Order order = new Order();

    public OrderChainBuilder(BookingChainBuilder builder, String orderNo) {
        this.builder = builder;
        order.setOrderNo(orderNo);
    }

    public OrderChainBuilder cost(Long fee) {
        order.setFee(fee);
        return this;
    }

    public PaymentChainBuilder by(String payNo) {
        return  new PaymentChainBuilder(this, payNo);
    }

    public BookingChainBuilder setPayment(Payment payment) {
        order.setPayment(payment);
        return builder.setOrder(order);
    }
}

public class PaymentChainBuilder {

    private final OrderChainBuilder builder;

    public final Payment payment = new Payment();

    public PaymentChainBuilder(OrderChainBuilder builder, String payNo) {
        this.builder = builder;
        payment.setPaymentNo(payNo);
    }

    public PaymentChainBuilder type(String type) {
        payment.setType(type);
        return this;
    }

    public BookingChainBuilder success() {
        return builder.setPayment(payment);
    }
}

可以看出实现方法链的核心就是:通过方法链中当前方法的返回值决定后续链路的可执行方法。

至此,我们可以通过如下代码去实例化预约模型:

BookingChainBuilder.from(PATIENT_ID)
                .book(DOCTOR_ID)
                .at(BOOK_TIME)
                .at(CLINIC_ID).name(CLINIC_NAME)
                .withOrder(ORDER_NO).cost(ORDER_FEE).by(PAY_NO).type(PAY_TYPE).success()
                .complete();

借助 lombok.Builder 我们也可以很方便的实现类似的效果。

上述代码看起来清晰了很多,不过在实现 DSL 的过程中,我们迫不得已使用了很多胶水代码,并且需要非常冗长的构造器。

在下一节(状态机的 DSL 实现),我们会使用更优雅的方式去完成 Fluent Interface 的构建。

使用Lambda表达式的函数序列

接下来我们尝试去使用 lambda 去做一些小优化。

public class BookingLambdaBuilder {
    private final Booking booking = new Booking();

    public static Booking booking(Consumer consumer) {
        BookingLambdaBuilder builder = new BookingLambdaBuilder();
        consumer.accept(builder);
        return builder.booking;
    }

    public void from(Long patientId) {
        booking.setPatientId(patientId);
    }

    public void book(Long doctorId) {
        booking.setDoctorId(doctorId);
    }

    public void at(OffsetDateTime time) {
        booking.setTime(time);
    }

    public void at(Consumer consumer) {
        ClinicLambdaBuilder builder = new ClinicLambdaBuilder();
        consumer.accept(builder);
        booking.setClinic(builder.clinic);
    }
}

public class ClinicLambdaBuilder {
    public final Clinic clinic = new Clinic();

    public void name(String name) {
        clinic.setName(name);
    }

    public void id(Integer id) {
        clinic.setId(id);
    }
}

如上所示,我们借助 Consumer 就可以使用 lambda 表达式的函数序列(function sequencing)去构建预约模型

BookingLambdaBuilder.booking(b -> {
            b.from(PATIENT_ID);
            b.book(DOCTOR_ID);
            b.at(BOOK_TIME);
            b.at(c -> {
                c.id(CLINIC_ID);
                c.name(CLINIC_NAME);
            });
        });

接下来我们还可以通过嵌套函数,方法引用等方式去继续优化我们的 DSL,这里就不做示例了。在实际 DSL 的编写中通常都是灵活运用上面所提到的多种方式去实现。

接下来我们就使用 DSL 的方式去定义一个状态机。

状态机的 DSL 实现

首先我们先给状态机的几个核心元素定义对应的模型:

模型

状态 :

@Data
public class State<S, E, C> {
    private S state;
    private HashMap> transitionMap = new HashMap<>();public void addTransition(E event, Transition transition) {
        transitionMap.put(event, transition);
    }public Optional> findTransition(E event) {return Optional.ofNullable(transitionMap.get(event));
    }
}

状态模型包含:

  • state: 对象本身的状态。
  • transitionMap:该状态所对应的事件和流转的映射。

在这里定义了 SEC 三个泛型来代指状态,事件,上下文。下同。

事件:上文的泛型 E

条件

public interface Condition<C> {
    boolean isSatisfied(C context);
}

一个用于校验的接口。

动作

public interface Action<S, E, C> {
    void execute(S from, S to, E event, C context);
}

流转

@Data
public class Transition<S, E, C> {

    private State source;private State target;private E event;private Condition condition;private Action action;public State transit(C ctx) {if(condition == null || condition.isSatisfied(ctx)){if(action != null){
                action.execute(source.getState(), target.getState(), event, ctx);return target;
            }
        }return source;
    }
}

定义了整个状态流转过程中的所有元素和动作。

状态机:

public class StateMachine<S, E, C> {

    private final Map> stateMap;public StateMachine(Map> stateMap){this.stateMap = stateMap;
    }public State fireEvent(S sourceState, E event, C ctx) {
        State state = StateHelper.getState(stateMap, sourceState);
        Optional> transition = state.findTransition(event);if(transition.isPresent()){return transition.get().transit(ctx);
        }return state;
    }
}

提供了事件触发的入口方法

构造器

首先定义一些流程动作:

public interface From<S, E, C> {
    To to(S stateId);
}

public interface To<S, E, C> {
    On on(E event);
}

public interface When<S, E, C>{
    void perform(Action action);
}

public interface On<S, E, C> extends When<S, E, C>{
    When when(Condition condition);
}

定义转换规则构造器:

public class TransitionBuilder<S, E, C> implements From<S, E, C>, To<S, E, C>, On<S, E, C>  {

    private State source;private State target;private Transition transition;private StateMachineBuilder builder;final Map> stateMap;public TransitionBuilder(StateMachineBuilder builder,
                             Map> stateMap) {this.builder = builder;this.stateMap = stateMap;
    }public From from(S state) {
        source = StateHelper.getState(stateMap, state);return this;
    }@Overridepublic To to(S state) {
        target = StateHelper.getState(stateMap, state);return this;
    }@Overridepublic When when(Condition condition) {
        transition.setCondition(condition);return this;
    }@Overridepublic On on(E event) {
        Transition newTransition = new Transition<>();
        newTransition.setSource(source);
        newTransition.setTarget(target);
        newTransition.setEvent(event);
        transition = newTransition;
        source.addTransition(event, transition);return this;
    }@Overridepublic void perform(Action action) {
        transition.setAction(action);
    }
}

在这里我们通过控制方法的返回值来保证了方法的使用顺序,例如 to 方法一定是在 from 之后才能使用。

StateHelper 是一个工具类,具体实现此处忽略,它的作用就是通过用户输入的状态,从 map 中拿到 DSL 中定义的状态模型,以便于后续的统一处理。

on 方法会组装好 Transition 模型,并添加 事件和流转的映射关系到初始状态对应的实例中去。

一个 TransitionBuilder 的构建过程即是一个状态流转的定义过程。

public class StateMachineBuilder<S, E, C> {

    private final Map> stateMap = new ConcurrentHashMap<>();private final StateMachine stateMachine = new StateMachine<>(stateMap);public static  StateMachineBuilder builder() {return new StateMachineBuilder<>();
    }public TransitionBuilder transition() {return new TransitionBuilder<>(this, stateMap);
    }public StateMachine build() {return stateMachine;
    }
}

StateMachineBuilder 会生成状态机模型,它对 TransitionBuilder 做了一层简单的代理,并且支持定义多组状态流转规则。

下面是一个简单的测试用例:

@Test
    public void testStateMachine() {
        StateMachineBuilder builder = StateMachineBuilder.builder();
        builder.transition()
                .from("状态:已预约")
                .to("状态:已取消")
                .on("事件:取消预约")
                .when(_condition())
                .perform(_doAction());
        StateMachine machine = builder.build();
        machine.fireEvent("状态:已预约", "事件:取消预约", "测试");
    }private Condition _condition() {return (ctx) -> true;
    }private Action _doAction() {return (from, to, event, ctx)-> {
            System.out.println(ctx + " from:" + from + " to:" + to + " on:" + event);
        };
    }

运行结果:

测试 from:状态:已预约 to:状态:已取消 on:事件:取消预约

上面了实现参考了阿里开源项目 COLA 中的状态机实现:https://github.com/alibaba/COLA/tree/master/cola-statemachine 。在其基础上抽取了最核心的逻辑,并做了部分简化,以便于大家理解。

总结

有限状态机(FSM)可以帮助我们去理解业务逻辑,特别是存在多种状态转换的情况下,通过状态转化表或状态图,可以简单明了的掌握整个业务流程的重要节点和行为。

领域特定语言(DSL)可以帮助我们更关注于解决代码本身,即使开发者对领域本身不太熟悉的情况下,也能借助 DSL 完成相应的功能。

那么什么时候需要使用 DSL 呢?我个人看法是:当你的 DSL 写的足够好的时候。

足够好的标准是什么呢?如下:

  • 用户友好:简洁、可读性、可维护性
  • 高度抽象
  • 性能良好

参考:

  • 给 DSL 开个脑洞:无状态的状态机
  • 开源项目 COLA
  • 《JAVA 实战 (第二版)》
  • 《领域特定语言》

全文完