Skip to content

浅谈 DSL #38

@srtian

Description

@srtian

前言

在 BPM 中台设计前端渲染操作模板接口的时候,有借鉴参考一些 DSL 的思想来设计模板接口,但对于 DSL 的认知还是有点松散,因此借着这篇文章整理一下自己对于 DSL 的一些的认知。

一、DLS 基础概览

DSL 是 Domain Specific Language 的缩写,中文翻译为领域特定语言,是一种为特定领域设计的,具有受限表达性的**编程语言。**只能在特定领域解决特定任务,常见的如:SQL,HTML,CSS 等。用 Martin Fowler 对于 DSL 的定义就是:DSL 通过在表达能力上的限制换取在某一领域内的高效

A computer programming language of limited expressiveness focused on a particular domain.

Martin 在讨论 DSL 的定义时,讲到了它的四个关键元素:

  • 针对领域(Domain focus)
  • 语言性(Language nature)
  • 受限的表达性(Limited expressiveness)
  • 计算机程序语言(Computer programming language)

二、DSL 和 通用编程语言的区别

那它和传统的编程语言有什么不同呢?其实与 DSL 相对应的就是GPL(General Purpose Language),即通用编程语言,也就是我们所熟悉的JavaScript、Python、Golang、Rust等。而它们最大区别就在于,GPL 是图灵完备的语言,可以编写任意的代码,表达任何可被计算的逻辑。

2.1 DLS的优势

那它为什么会出现呢?或者说,它给我们带来了什么?一方面是高级语言抽象带来的效率提升存在天花板,因此需要使用一些针对特定领域设计的语言来解决特定领域所存在的问题,从而提升我们在特定领域的开发效率。

这样说可能会比较模糊,但其实从宏观的角度来理解,语言编写的软件本质上都是要操作硬件去完成一系列目的的,而语言各自的语法其实就是一些系列的接口,我们可以通过这些接口去完成我们所要实现的目的。因此,我们在设计 DSL 时,本质上也只是在设计一种语法而已,只不过这种语法适用于特定的一些领域,并且可以具有**很好的表现能力。**这里引用 Martin Fowler 的《领域特定语言》中的创建一个 Computer 实例的代码:

Processor p = new Processor(2, 2500, Processor.Type.i386);
Disk d1 = new Disk(150, Disk.UNKNOWN_SPEED, null);
Disk d2 = new Disk(75, 7200, Disk.Interface.SATA);
return new Computer(p, d1, d2);

而使用内部 DSL 写出来则是这样的:

computer() 
  .processor()
    .cores(2) 
    .speed(2500) 
    .i386()
  .disk()
    .size(150)
  .disk()
   .size(75)
   .speed(7200) 
   .sata()
.end();

相较于 Java 传统的写法,DSL写出来的代码很明显具有声明式的味道,更专注于做什么而不是怎么做,成功的逻辑与意图进行了分离。这也有利于让我们在思考功能时变得更为清晰,而无需关心其中的具体实现。

另一方面在于,一旦我们有一个编译器(这主要是针对外部 DSL 来讲的),我们在此 DSL 所覆盖的软件开发的特定领域的工作将变得非常的高效,其中很典型的按理就是现在非常流行的 JSX,以及众多搭建平台用于描述页面的 DSL。我们通过在 React 写 JSX,就可以声明式的实现很多逻辑,这有效的提升了我们的开发效率以及开发体验。

2.2 DSL 的缺点

需要注意的是,DSL 虽然给我们带来了诸多的便利,但也存在一些局限性。就显著的就是 DSL 只能用于一个特定的领域,虽然大多 DSL 学习起来较之 GPL 要来的容易,但仍然存在上手成本。 同时,如果使用到了 DSL 相关工具,即使工作效率有所提升,但学习和配置这些工具也需要一定的工作量。因此,如何在保证效率提升的同时,最大化的减少使用者的学习成本也是 DSL 需要考虑的事情,这也对设计者提出了更高的要求,需要同时对专业领域知识以及开发语言都一定的掌握,才能设计出合理的 DSL。

三、DSL 的实现

DSL 从实现的角度来讲,又可以分为两种:

  • 内部 DSL: 建立在宿主语言之上的 DLS,它与宿主语言共享编译与调试工具,实现成本以及学习成本都更低。比较典型的就是 JQuery。
  • 外部 DSL:它是一种独立的编程语言,需要实现它自己的编译工具,将其编译为宿主环境的目标语言。它的缺点在于实现成本较高,优点在于语法灵活性高,具有较强的语言表达力。

3.1 外部DSL

上面是一种独立的编程语言,需要从解析器开始实现自己的编译工具,实现的成本较高。对于外部的 DSL 实现,很典型的就是诸多低代码平台,就用的是这种方式去实现自己的 DSL 去描述页面。至于如何设计一个 DSL,由于我之前只是进行了调研,并没有实际落地,所以在此也就不多扯了,具体可以参考 Phodal 大佬的文章:《领域特定语言设计技巧》。里面所提及的这五个步骤,我觉得很有道理(实际上,我在设计模板接口的时候也大致参考了它的这个步骤):

  1. 定义呈现模式。
  2. 提炼领域特定名词。
  3. 设计关联关系与语法。
  4. 实现语法解析。
  5. 演进语言的设计。

至于在语法解析方面,前端可以使用:Peg.js 来帮助我们去实现相应的解析功能:
image.png
感兴趣的同学可以去瞅瞅,我试了下的确很有意思。

3.2 内部DSL

是寄生于宿主语言的特殊 DSL,它与宿主语言共享编译与调试工具等基础设施,学习成本更低,也更容易被集成。他在语法上与宿主语言同源,但在运行时上需要做额外的封装。

说起来还是有点空洞,还是来举个例子吧,我们就以BPM中单据的审批来讲:我们期望实现一个这样的业务流程,这个流程是通过 iframe 接入的,但审批操作在 BPM 中台进行,期望在用户点击时发送消息给内部的iframe,由他们处理相关逻辑,然后在处理完之后会回传消息,然后再有BPM中台去执行平台方的审批操作,待审批成功后关闭页面。那么如果用链式的调用可以写成这样:

'approve'.postMessage().awaitMessageCallback().approve().closePage()

而具体这种链式调用也很简单, 我们只需在 String 原型上如此做即可:

String.prototype.postMessage = function() {
  // 具体业务逻辑
  console.log(this)
  return this
}

此外,其实内部 DSL 的语言风格也有很多种,感兴趣的可以去看这篇文章:《前端 DSL 实践指南(上)—— 内部 DSL》,非常精彩。

参考链接:

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions