------ 本文引述阿里张建飞

DSL

在介绍如何实现状态机之前,不妨让我们先来看一下什么是DSL,在Martin Fowler的《Domain Specific Languages》书中。开篇就是以State Machine来作为引子介绍DSL的。有时间的话,强烈建议你去读读这本书。没时间的话,看看下面的内容也能掌握个大概了。

下面就让我提炼一下书中的内容,带大家深入了解下DS

什么是DSL

DSL是一种工具,它的核心价值在于,它提供了一种手段,可以更加清晰地就系统某部分的意图进行沟通。

这种清晰并非只是审美追求。一段代码越容易看懂,就越容易发现错误,也就越容易对系统进行修改。因此,我们鼓励变量名要有意义,文档要写清楚,代码结构要写清晰。基于同样的理由,我们应该也鼓励采用DSL。

按照定义来说,DSL是针对某一特定领域,具有受限表达性的一种计算机程序设计语言。这一定义包含3个关键元素:

  • 语言性(language nature):DSL是一种程序设计语言,因此它必须具备连贯的表达能力——不管是一个表达式还是多个表达式组合在一起。

  • 受限的表达性(limited expressiveness):通用程序设计语言提供广泛的能力:支持各种数据、控制,以及抽象结构。这些能力很有用,但也会让语言难于学习和使用。DSL只支持特定领域所需要特性的最小集。使用DSL,无法构建一个完整的系统,相反,却可以解决系统某一方面的问题。

  • 针对领域(domain focus):只有在一个明确的小领域下,这种能力有限的语言才会有用。这个领域才使得这种语言值得使用。

比如正则表达式,/\d{3}-\d{3}-\d{4}/就是一个典型的DSL,解决的是字符串匹配这个特定领域的问题。

DSL的分类

按照类型,DSL可以分为三类:内部DSL(Internal DSL)、外部DSL(External DSL)、以及语言工作台(Language Workbench)。

Internal DSL是一种通用语言的特定用法。用内部DSL写成的脚本是一段合法的程序,但是它具有特定的风格,而且只用到了语言的一部分特性,用于处理整个系统一个小方面的问题。 用这种DSL写出的程序有一种自定义语言的风格,与其所使用的宿主语言有所区别。例如我们的状态机就是Internal DSL,它不支持脚本配置,使用的时候还是Java语言,但并不妨碍它也是DSL。
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
External DSL是一种“不同于应用系统主要使用语言”的语言。外部DSL通常采用自定义语法,不过选择其他语言的语法也很常见(XML就是一个常见选 择)。比如像Struts和Hibernate这样的系统所使用的XML配置文件。
Workbench是一个专用的IDE,简单点说,工作台是DSL的产品化和可视化形态。
三个类别DSL从前往后是有一种递进关系,Internal DSL最简单,实现成本也低,但是不支持“外部配置”。Workbench不仅实现了配置化,还实现了可视化,但是实现成本也最高。他们的关系如下图所示:

不同DSL该如何选择

几种DSL类型各有各的使用场景,选择的时候,可以这样去做一个判断。

  1. Internal DSL:假如你只是为了增加代码的可理解性,不需要做外部配置,我建议使用Internal DSL,简单、方便、直观。
  2. External DSL:如果你需要在Runtime的时候进行配置,或者配置完,不想重新部署代码,可以考虑这种方式。比如,你有一个规则引擎,希望增加一条规则的时候,不需要重复发布代码,那么可以考虑External
  3. Workbench:配置也好,DSL Script也好,这东西对用户不够友好。比如在淘宝,各种针对商品的活动和管控规则非常复杂,变化也快。我们需要一个给运营提供一个workbench,让他们自己设置各种规则,并及时生效。这时的workbench将会非常有用。

    总而言之,在合适的地方用合适的解决方案,不能一招鲜吃遍天。就像最臭名昭著的DSL——流程引擎,就属于那种严重的被滥用和过渡设计的典型,是把简单的问题复杂化的典型。
    最好不要无端增加复杂性。然而,想做简单也不是一件容易的事,特别是在大公司,我们不仅要写代码,还要能沉淀“NB的技术”,最好是那种可以把老板说的一愣一愣的技术,就像尼古拉斯在《反脆弱》里面说的:

在现代生活中,简单的做法一直难以实现,因为它有违某些努力寻求复杂化以证明其工作合理性的人所秉持的精神。

Fluent Interfaces

在编写软件库的时候,我们有两种选择。一种是提供Command-Query API,另一种是Fluent Interfaces。比如Mockito的API **when(mockedList.get(anyInt())).thenReturn(“element”)**就是一种典型连贯接口的用法。
连贯接口(fluent interfaces)是实现Internal DSL的重要方式,为什么这么说呢?
因为Fluent的这种连贯性带来的可读性和可理解的提升,其本质不仅仅是在提供API,更是一种领域语言,是一种Internal DSL。
比如Mockito的APIwhen(mockedList.get(anyInt())).thenReturn(“element”)就非常适合用Fluent的形式,实际上,它也是单元测试这个特定领域的DSL。
如果把这个Fluent换成是Command-Query API,将很难表达出测试框架的领域。

String element = mockedList.get(anyInt());
boolean isExpected = "element".equals(element);

这里需要注意的是,连贯接口不仅仅可以提供类似于method chaining和builder模式的方法级联调用,比如OkHttpClient中的Builder

OkHttpClient.Builder builder=new OkHttpClient.Builder();OkHttpClient okHttpClient=builder.readTimeout(5*1000, TimeUnit.SECONDS).writeTimeout(5*1000, TimeUnit.SECONDS).connectTimeout(5*1000, TimeUnit.SECONDS).build();

他更重要的作用是,限定方法调用的顺序。比如,在构建状态机的时候,我们只有在调用了from方法后,才能调用to方法,Builder模式没有这个功能。
怎么做呢?我们可以使用Builder和Fluent接口结合起来的方式来实现,下面的状态机实现部分,我会进一步介绍。

状态机

好的,关于DSL的知识我就介绍这么多。接下来,让我们看看应该如何实现一个Internal DSL的状态机引擎。

状态机选型

我反对滥用流程引擎,但并不排斥状态机,主要有以下两个原因:
首先,状态机的实现可以非常的轻量,最简单的状态机用一个Enum就能实现,基本是零成本。
其次,使用状态机的DSL来表达状态的流转,语义会更加清晰,会增强代码的可读性和可维护性。
然而,我们的业务场景虽然也不是特别复杂,但还是超出了Enum仅支持线性状态流转的范畴。因此不得不先向外看看。
开源状态机太复杂
和流程引擎一样,开源的状态机引擎不可谓不多,我着重看了两个状态机引擎的实现,一个是Spring Statemachine,一个是Squirrel statemachine。这是目前在github上的Top 2 状态机实现,他们的优点是功能很完备,缺点也是功能很完备。
当然,这也不能怪开源软件的作者,你好不容易开源一个项目,至少要把UML State Machine上罗列的功能点都支持掉吧。
就我们的项目而言(其实大部分项目都是如此)。我实在不需要那么多状态机的高级玩法:比如状态的嵌套(substate),状态的并行(parallel,fork,join)、子状态机等等。

开源状态机性能差

除此之外,还有一个我不能容忍的问题是,这些开源的状态机都是有状态的(Stateful)的,表面上来看,状态机理所当然是应该维持状态的。但是深入想一下,这种状态性并不是必须的,因为有状态,状态机的实例就不是线程安全的,而我们的应用服务器是分布式多线程的,所以在每一次状态机在接受请求的时候,都不得不重新build一个新的状态机实例。

以电商交易为例,用户下单后,我们调用状态机实例将状态改为“Order Placed”。当用户支付订单的时候,可能是另一个线程,也可能是另一台服务器,所以我们必须重新创建一个状态机实例。因为原来的instance不是线程安全的。

这种new instance per request的做法,耗电不说。倘若状态机的构建很复杂,QPS又很高的话,肯定会遇到性能问题。
鉴于复杂性和性能(公司电费)的考虑,我们决定自己实现一个状态机引擎,设计的目标很明确,有两个要求:
1、简洁的仅支持状态流转的状态机,不需要支持嵌套、并行等高级玩法。
2、状态机本身需要是Stateless(无状态)的,这样一个Singleton Instance就能服务所有的状态流转请求了。

状态机实现

状态机领域模型

鉴于我们的诉求是实现一个仅支持简单状态流转的状态机,该状态机的核心概念如下图所示,主要包括:

  1. State:状态
  2. Event:事件,状态由事件触发,引起变化
  3. Transition:流转,表示从一个状态到另一个状态
  4. External Transition:外部流转,两个不同状态之间的流转 Internal
  5. Internal Transition:内部流转,同一个状态之间的流转
  6. Condition:条件,表示是否允许到达某个状态
  7. Action:动作,到达某个状态之后,可以做什么
  8. StateMachine:状态机

    整个状态机的核心语义模型(Semantic Model)也很简单,就是如下图所示:

    Note:这里之所以叫Semantic Model,用的是《DSL》书里的术语,你也可以理解为是状态机的领域模型。Martin用Semantic这个词,是想说,外部的DSL script代表语法(Syntax),里面的model代表语义(Semantic),我觉得这个隐喻还是很恰当的。

OK,状态机语义模型的核心代码如下所示:

//StateMachine
public class StateMachineImpl<S,E,C> implements StateMachine<S, E, C> {private String machineId;private final Map<S, State<S,E,C>> stateMap;...
}//State
public class StateImpl<S,E,C> implements State<S,E,C> {protected final S stateId;private Map<E, Transition<S, E,C>> transitions = new HashMap<>();...
}//Transition
public class TransitionImpl<S,E,C> implements Transition<S,E,C> {private State<S, E, C> source;private State<S, E, C> target;private E event;private Condition<C> condition;private Action<S,E,C> action;...
}

状态机的Fluent API
实际上,我用来写Builder和Fluent Interface的代码甚至比核心代码还要多,比如我们的TransitionBuilder是这样写的


class TransitionBuilderImpl<S,E,C> implements ExternalTransitionBuilder<S,E,C>, InternalTransitionBuilder<S,E,C>, From<S,E,C>, On<S,E,C>, To<S,E,C> {final Map<S, State<S, E, C>> stateMap;private State<S, E, C> source;protected State<S, E, C> target;private Transition<S, E, C> transition;final TransitionType transitionType;public TransitionBuilderImpl(Map<S, State<S, E, C>> stateMap, TransitionType transitionType) {this.stateMap = stateMap;this.transitionType = transitionType;}@Overridepublic From<S, E, C> from(S stateId) {source = StateHelper.getState(stateMap, stateId);return this;}@Overridepublic To<S, E, C> to(S stateId) {target = StateHelper.getState(stateMap, stateId);return this;}@Overridepublic To<S, E, C> within(S stateId) {source = target = StateHelper.getState(stateMap, stateId);return this;}@Overridepublic When<S, E, C> when(Condition<C> condition) {transition.setCondition(condition);return this;}@Overridepublic On<S, E, C> on(E event) {transition = source.addTransition(event, target, transitionType);return this;}@Overridepublic void perform(Action<S, E, C> action) {transition.setAction(action);}}

通过这种Fluent Interface的方式,我们确保了Fluent调用的顺序,如下图所示,在externalTransition的后面你只能调用from,在from的后面你只能调用to,从而保证了状态机构建的语义正确性和连贯性。

状态机的无状态设计
至此,状态机的核心模型和Fluent接口我已经介绍完了。我们还需要解决一个性能问题,也就是我前面说的,要把状态机变成无状态的。

分析一下市面上的开源状态机引擎,不难发现,它们之所以有状态,主要是在状态机里面维护了两个状态:初始状态(initial state)和当前状态(current state),如果我们能把这两个实例变量去掉的话,就可以实现无状态,从而实现一个状态机只需要有一个instance就够了。

关键是这两个状态可以不要吗?当然可以,唯一的副作用是,我们没办法获取到状态机instance的current state。然而,我也不需要知道,因为我们使用状态机,仅仅是接受一下source state,check一下condition,execute一下action,然后返回target state而已。它只是实现了一个状态流转的DSL表达,仅此而已,全程操作完全可以是无状态的。

采用了无状态设计之后,我们就可以使用一个状态机Instance来响应所有的请求了,性能会大大的提升。

使用状态机

状态机的实现很简单,同样,他的使用也不难。如下面的代码所示,它展现了cola状态机支持的全部三种transition方式。

StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();//external transitionbuilder.externalTransition().from(States.STATE1).to(States.STATE2).on(Events.EVENT1).when(checkCondition()).perform(doAction());//internal transitionbuilder.internalTransition().within(States.STATE2).on(Events.INTERNAL_EVENT).when(checkCondition()).perform(doAction());//external transitionsbuilder.externalTransitions().fromAmong(States.STATE1, States.STATE2, States.STATE3).to(States.STATE4).on(Events.EVENT4).when(checkCondition()).perform(doAction());builder.build(machineId);StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get(machineId);stateMachine.showStateMachine();

可以看到,这种Internal DSL的状态机显著的提升了代码的可读性和可理解性。特别是在相对复杂的业务状态流转中,比如下图就是我们用cola-statemachine生成的我们实际项目中的plantUML图。如果没有状态机的支持,像这样的业务代码将会很难看懂和维护。

这就是DSL的核心价值——更加清晰地表达系统中,某一部分的设计意图和业务语义。 当然External DSL所带来的可配置性和灵活性也很有价值,只是cola-statemachine还没有支持,原因很简单,暂时用不上。

【低代码与元数据】教你看清DSL的本质相关推荐

  1. 实现一个状态机引擎,教你看清DSL的本质

    最近在一个项目中,因为涉及很多状态的流转,我们选择使用状态机引擎来表达状态流转.因为状态机DSL(Domain Specific Languages)带来的表达能力,相比较于if-else的代码,要更 ...

  2. 穷游女生说:在川藏线上搭车,可以看清男人的本质

    穷游西藏有什么好处?为什么那么多文艺青年前赴后继的踏上穷游西藏之路?直到我今天在川藏线搭了一个穷游女生,才彻底的明白,印象特别深的是她说的一句话:"读万卷书,不如在川藏线上搭十辆车" ...

  3. level2买股技巧_同花顺Level-2教你看清个股真实交易数据

    您可能感兴趣的话题: 同花顺 核心提示:近几天,笔者在对比普通行情软件和同花顺Level-2行情软件在成交明细上的区别,发现这里面的学问非常大! 近几天,笔者在对比普通行情软件和同花顺Level-2行 ...

  4. level2买股技巧_同花顺Level-2教你看清个股真实交易

    近几天,笔者在对比普通行情软件和同花顺Level-2行情软件在成交明细上的区别,发现这里面的学问非常大! 回想之前的看盘,盘中突然看到一笔或者几笔上千手的大卖单,以为是主力在出逃,结果获利了结之后,股 ...

  5. 《人月神话》(The Mythical Man-Month)1 看清问题的本质:如果我们想解决问题,就必须试图先去理解它...

    第一章 焦油坑(The Tar Pit) 史前史中,没有比巨兽在焦油坑中垂死挣扎的场面更令人震撼的了.上帝见证着恐龙.猛犸象.剑齿虎在焦油中挣扎.它们挣扎得越是猛烈,焦油纠缠得越紧,没有任何猛兽足够强 ...

  6. 网页漂浮物代码_无代码VS低代码。 有什么不同? 它。 不。 物。

    网页漂浮物代码 You may hear the terms "No Code" and "Low Code" thrown around by develop ...

  7. 低代码技术与市场(Mendix与 OutSystems)

    低代码技术与市场(Mendix与 OutSystems) 本文主要参考文章 参考链接 https://mp.weixin.qq.com/s/OXCBORheAx99o3fS-ZfUdg https:/ ...

  8. 解放双手!推荐一款阿里开源的低代码工具,YYDS!

    之前分享过一些低代码相关的文章,发现大家还是比较感兴趣的.之前在我印象中低代码就是通过图形化界面来生成代码而已,其实真正的低代码不仅要负责生成代码,还要负责代码的维护,把它当做一站式开发平台也不为过! ...

  9. 低代码和无代码,完全是两回事

    ▲ 点击上方"分布式实验室"关注公众号 回复"1"抽取技术书 按规则这篇应该叫做"数字技术名词解释-低代码",但之前的名词解释篇都是一两天草 ...

最新文章

  1. linux下find,xargs命令详解和实例
  2. visual c 语言参考手册pdf,《Microsoft-Visual-C-6-0语言参考手册(三)》.pdf
  3. JavaScript-2(数组与字符串的方法)
  4. 复习笔记--计算机网络
  5. 基于AppDomain的插件式开发
  6. node.js-------使用路由模块
  7. 中科院超牛的物理学家,摇滚界无人不知的“李白”,你一定听过他的歌!
  8. 设置WordPress文章关键词自动获取,文章所属分类名称,描述自动获取文章内容,给文章的图片自动加上AlT标签...
  9. Centos 7.3 搭建php7,mysql5.7,nginx1.10.1,redis
  10. pytorch卷积模型定义
  11. Android Java开发实例项目+游戏视频教程免费下载咯。。
  12. Java:(练习)二、基于Map和面向对象方法的世界杯冠军信息系统
  13. 别只看钱:赛门铁克46.5亿美元现金收购Blue Coat为哪般?
  14. 过大年,和人工智能一起对对联!
  15. 机器学习_深度学习毕设题目汇总——运动活动动作
  16. 平均获客成本_获客成本(线上,线下)如何计算?
  17. html修改word页边距,Word怎么只修改一页的页边距,而不影响同一文件中的其他页?...
  18. BANANA PI 入门:香蕉派如何安装系统
  19. element-ui——安装element-plus
  20. 用python对excel进行打印操作

热门文章

  1. 【电力系统】基于matlab YALMIP+CPLEX求解带储能的微电网优化调度问题【含Matlab源码 2175期】
  2. 报表生成器FastReport .Net用户指南(四):Bands的属性
  3. ctf密码学在线转换网址
  4. 【C++】C++11新特性——可变参数模板|function|bind
  5. 李峋同款心跳Python代码
  6. 英语题目作业(30)
  7. 【Linux】_面试题总结之Docker、Lvs、chown、服务器和数据库的备份和还原、云计算的了解、服务器性能指标、服务器的资源管理、openstack的组件
  8. 米拓模板:农业农产品公司网站模板推荐
  9. 信息安全数学基础——整除的概念
  10. fsync mysql_使用O_DIRECT_NO_FSYNC来提升MySQL性能