不同DSL该如何选择
几种DSL类型各有各的使用场景,选择的时候,可以这样去做一个判断。
- Internal DSL:假如你只是为了增加代码的可理解性,不需要做外部配置,我建议使用Internal DSL,简单、方便、直观。
- External DSL:如果你需要在Runtime的时候进行配置,或者配置完,不想重新部署代码,可以考虑这种方式。比如,你有一个规则引擎,希望增加一条规则的时候,不需要重复发布代码,那么可以考虑External。
- 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的API when(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上罗列的功能点都支持掉吧。
就我们的项目而言(其实大部分项目都是如此)。我实在不需要那么多状态机的高级玩法: