作者 | 小牛
Java 工程师,关注服务端技术
首先我们先来简单了解一下什么是状态机和领域特定语言。
状态机(State Machine):定义事物状态以及这些状态之间转移和动作等行为的数学模型。一般可以分为有限状态机、并发状态机、分层状态机等。
领域特定语言( Domain Specific Language):简称 DSL,是指为特定领域(domain)设计的专用语言。
有限状态机(FSM)
我们通常所说的状态机都是指有限状态机(Finite State Machine,FSM)。
有限状态机有以下三个特点:
- 状态数量是有限的
- 某一时刻一定会处于其所有状态中的一个状态
- 接收某个事件后从一种状态切换到另一种状态
我们可以这样来定义有限状态机:有限个状态(State)以及在这些状态(State)之间的转移(Transition)和动作(Action)等行为的数据模型。
有限状态机的组成
核心是由四部分:
- 状态( State):对象在其生命周期中某一个时刻的运行情况。在不同形态下,可以有不同的行为和属性。
- 事件( Event):在某种状态下所发生的有意义的事情,通常会触发一个动作或者流转。
- 动作(Action):一个可执行的原子操作,通常由事件触发。
- 流转(Transition):从一个状态变化为另一个状态。
一般来说,状态可以分为现态和次态。次态是相对于现态而言的,次态一旦被激活,就转变成新的现态。
而动作是一个宽泛的概念,它可以产生输入输出,产生新的事件,或者代替 “流转” 做状态转换的工作。另外一个事件可以触发多个动作。
流转可以分为:
- 外部流转(External Transition):不同状态之间的流转。
- 内部流转(Internal Transition):同一个状态之间的流转。
此外某些情况下状态机还需要另外一个元素:
- 条件(Condition):表示是否允许到达某个状态。
有限状态机的表述
通常有两种表述形式:状态转换表和状态图。
下面我们假设一个预约流程中存在以下状态,以此来示例:
- 未使用
- 预约中
- 已预约
- 已取消
- 已完成
其中”已取消“和”已完成“都是最终态。“未使用” 状态的预约如果被预约则流转成 “预约中”状态,“预约中” 如果完成了支付可以流转成 “已预约”,“预约中” 和 “已预约” 状态 如果取消都会流转成“已取消”,“已预约”状态如果签到则流转成“已完成”。
上面一段话是否看起来很吃力?接下来我们换种方式表述再试试。
状态转换表
状态转换表有多种形式,我们这里的表现形式如下:
现态 S \事件 E | 预约 | 支付 | 取消 | 签到 |
未使用 | 预约中 | X | X | X |
预约中 | 已预约 | 已取消 | X | |
已预约 | X | X | 已取消 | 已完成 |
已取消 | X | X | X | |
已完成 | X | X | X |
其中
- 左侧栏代表当前的状态。
- 上边栏代表不同的事件或者说是系统行为。
- 剩余交叉部分的表格则代表进行流转之后的状态,其中有汉字的格子代表流转后的新状态,空白格子代表状态不变,标记为 “X” 则代表禁止此事件
这样看起来是不是清晰了很多,接下来我们再来试试以状态图的方式来表述
状态图
可以看出,状态表更着重于体现状态机中所包换的所有状态和事件,而状态图对状态间的流转表述的更为清晰。
现在我们已经可以清晰的表述出一个状态机了,那么接下来我们如何用代码来实现呢?
代码实现
首先最简单直观的方式就是用 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:该状态所对应的事件和流转的映射。
在这里定义了 S
, E
, C
三个泛型来代指状态,事件,上下文。下同。
事件:上文的泛型 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 实战 (第二版)》
- 《领域特定语言》
全文完